Skip to content

feat(eval): add measurement manifests for online scoring - #722

Open
ascerra wants to merge 10 commits into
mainfrom
feat/eval-measurements-manifests
Open

feat(eval): add measurement manifests for online scoring#722
ascerra wants to merge 10 commits into
mainfrom
feat/eval-measurements-manifests

Conversation

@ascerra

@ascerra ascerra commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Add default measurement manifests for stock fullsend agents under eval/measurements/. These are the online-scoring policy that ships with the agents (“here is code, and here is what we measure on wild code runs”).

Companion engine PR: fullsend-ai/fullsend#6036 (ADR 0087, fullsend eval-measure, EM-001 trace_fitness).

Ownership (please read)

Concern Repo
Scorer implementations (Go), parser, CLI, job wiring fullsend#6036
Default manifests (this PR) agents eval/measurements/<agent>.yaml
Org overrides / BYOA Consumer FULLSEND_DIR
  • Managed jobs resolve local ${FULLSEND_DIR}/eval/measurements/${AGENT}.yaml if present, else fetch agents@v0 from this path. Stock-agent users do not copy these files into every repo.
  • Local files are for override, opt-out, or custom agents only.
  • This PR is policy only (YAML). Executable scoring stays in fullsend because the measure CLI reads run-telemetry.jsonl produced by fullsend.
  • Change guide: new Go scorer / (future) new declarative assert: → fullsend; new id / enable / disable / thresholds for a stock agent on an existing scorer → this repo; org-specific policy → consumer override.

Changes

  • eval/measurements/{code,fix,prioritize,retro,review,triage}.yaml — each enables trace_fitness (em-001) @ version 1
  • eval/measurements/README.md — ownership, resolution, when to PR where
  • eval/README.md — short section + links to fullsend#6036 / ADR 0087
  • Scribe intentionally omitted (no forge work-item identity for EM-001 today)

Test plan

  • Manifest YAML shape matches fullsend registry loader (agent + measurements[].{id,scorer,version})
  • Scribe excluded on purpose
  • Cross-check with fullsend#6036 / ADR 0087 before merge
  • After merge, ensure v0 (or the pin fullsend uses) includes these paths so wild runs stop skipping measure

Notes for reviewers

  • Functional eval fixtures under eval/<agent>/ are unchanged and separate.
  • Until this lands on the ref fullsend fetches (v0), measure steps skip fail-open — that is expected.

@ascerra
ascerra requested a review from a team as a code owner August 10, 2026 11:45
@qodo-code-review

qodo-code-review Bot commented Aug 10, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Add per-agent measurement manifests for online scoring (trace_fitness em-001)

✨ Enhancement 📝 Documentation ⚙️ Configuration changes 🕐 20-40 Minutes

Grey Divider

AI Description

• Add default per-agent measurement manifests for managed-job online scoring.
• Enable trace_fitness (em-001 v1) for six stock agents; omit others (e.g., scribe).
• Document manifest ownership, resolution order, and separation from functional eval harness.
Diagram

graph TD
  J(["Managed agent job"]) --> CLI["fullsend eval-measure"]
  CLI --> D{"Local manifest?"}
  D -->|"yes"| L["FULLSEND_DIR manifest"]
  D -->|"no"| R["agents@v0 manifest"]
  CLI --> T[("run-telemetry.jsonl")] --> S["trace_fitness scorer\n(em-001 v1)"] --> O[("Measurement results")]

  subgraph Legend
    direction LR
    _proc["Process/CLI"] ~~~ _dec{"Decision"} ~~~ _data[("Data")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Bake defaults into fullsend (no agents@v0 fetch)
  • ➕ Single release artifact controls both scorer code and default policy
  • ➕ No reliance on agents@v0 pin being updated to enable scoring
  • ➖ Harder for agents repo to own/iterate on policy independently
  • ➖ Forces fullsend release for simple enable/disable/threshold policy changes
2. Single manifest with per-agent sections
  • ➕ Fewer files to manage/version together
  • ➕ Easier to diff global policy changes across agents
  • ➖ Makes per-agent overrides less discoverable and more conflict-prone
  • ➖ Resolution logic becomes more complex (merge/selection vs direct fetch)
3. Remote registry service for measurement policy
  • ➕ Policy updates without Git pins; central governance and rollout controls
  • ➕ Can support targeting/experiments by org/repo
  • ➖ Adds new infrastructure dependency and failure modes
  • ➖ Overkill for initial ship; higher operational complexity

Recommendation: Keep the current approach: per-agent YAML manifests in this repo fetched via agents@v0 with explicit FULLSEND_DIR override. It cleanly separates engine/scorer implementation (fullsend) from policy defaults (agents), keeps overrides simple, and matches the ownership model described in the docs.

Files changed (8) +106 / -0

Documentation (2) +70 / -0
README.mdDocument measurement manifests as online-scoring policy +13/-0

Document measurement manifests as online-scoring policy

• Adds a new section explaining that 'eval/measurements/' defines default online-scoring policy for managed jobs. Clarifies separation from functional eval harness scenarios and links to the companion fullsend PR and ADR.

eval/README.md

README.mdAdd measurements README covering ownership and resolution +57/-0

Add measurements README covering ownership and resolution

• Introduces documentation for what measurement manifests are, why they live with agents, and how managed jobs resolve local overrides vs agents@v0 defaults. Defines responsibility boundaries between fullsend (scorer code) and agents (policy) and lists initial agent coverage for em-001.

eval/measurements/README.md

Other (6) +36 / -0
code.yamlAdd default manifest for code agent (em-001 v1) +6/-0

Add default manifest for code agent (em-001 v1)

• Adds a per-agent measurement manifest selecting 'trace_fitness' (id em-001) version 1 for the 'code' agent.

eval/measurements/code.yaml

fix.yamlAdd default manifest for fix agent (em-001 v1) +6/-0

Add default manifest for fix agent (em-001 v1)

• Adds a per-agent measurement manifest selecting 'trace_fitness' (id em-001) version 1 for the 'fix' agent.

eval/measurements/fix.yaml

prioritize.yamlAdd default manifest for prioritize agent (em-001 v1) +6/-0

Add default manifest for prioritize agent (em-001 v1)

• Adds a per-agent measurement manifest selecting 'trace_fitness' (id em-001) version 1 for the 'prioritize' agent.

eval/measurements/prioritize.yaml

retro.yamlAdd default manifest for retro agent (em-001 v1) +6/-0

Add default manifest for retro agent (em-001 v1)

• Adds a per-agent measurement manifest selecting 'trace_fitness' (id em-001) version 1 for the 'retro' agent.

eval/measurements/retro.yaml

review.yamlAdd default manifest for review agent (em-001 v1) +6/-0

Add default manifest for review agent (em-001 v1)

• Adds a per-agent measurement manifest selecting 'trace_fitness' (id em-001) version 1 for the 'review' agent.

eval/measurements/review.yaml

triage.yamlAdd default manifest for triage agent (em-001 v1) +6/-0

Add default manifest for triage agent (em-001 v1)

• Adds a per-agent measurement manifest selecting 'trace_fitness' (id em-001) version 1 for the 'triage' agent.

eval/measurements/triage.yaml

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 10, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 11:46 AM UTC · Ended 11:51 AM UTC

Commit: de3a60a · View workflow run →

@qodo-code-review

qodo-code-review Bot commented Aug 10, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (1)

Grey Divider


Remediation recommended

1. ID casing inconsistency ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
The measurement manifests set the trace_fitness measurement id to em-001, but the measurement
README refers to the same measurement as EM-001, creating ambiguity about the canonical identifier
to use in configs/docs/tooling.
Code

eval/measurements/code.yaml[R3-5]

+  - id: em-001
+    scorer: trace_fitness
+    version: 1
Relevance

●●● Strong

Trivial consistency fix; team has accepted casing-alignment changes in nearby review history.

PR-#592

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The repo’s new measurement README explicitly calls the scorer “EM-001” while each new manifest
defines the measurement id as “em-001”, so the PR introduces two different canonical spellings for
the same measurement identifier.

eval/measurements/README.md[10-11]
eval/measurements/code.yaml[1-5]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The measurement ID is referenced with different casing in different newly-added sources: manifests use `em-001`, while documentation calls it `EM-001`. This makes it unclear which identifier is canonical.

## Issue Context
All per-agent manifests added in this PR use `id: em-001`, but the README in the same directory describes the scorer as `EM-001`.

## Fix Focus Areas
- eval/measurements/README.md[10-11]
- eval/measurements/code.yaml[1-5]
- eval/measurements/fix.yaml[1-5]
- eval/measurements/prioritize.yaml[1-5]
- eval/measurements/retro.yaml[1-5]
- eval/measurements/review.yaml[1-5]
- eval/measurements/triage.yaml[1-5]

## Proposed fix
Pick one canonical representation (either `EM-001` everywhere or `em-001` everywhere) and update both:
1) the documentation string in `eval/measurements/README.md`, and
2) all manifest `id:` fields (if needed) to match.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Missing linked issue authorization 📜 Skill insight § Compliance
Description
This PR introduces a new measurement-manifest feature and adds 20+ lines of new configuration/docs,
but the provided PR description does not link an authorizing issue. This violates the requirement to
have explicit authorization for non-trivial work.
Code

eval/measurements/README.md[R1-4]

+# Measurement manifests
+
+Per-agent YAML that selects which **eval measurement** scorers run after a
+managed agent job (`fullsend eval-measure`). This is **not** the functional
Relevance

● Weak

Similar “add explicit authorization/linked issue” governance suggestions were rejected previously.

PR-#476
PR-#631

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Rule 1538390 requires an explicit linked issue for non-trivial changes. The diff adds a new
eval/measurements/ feature surface (manifests + documentation), but the provided PR description
contains no linked issue reference (e.g., Fixes #...).

eval/measurements/README.md[1-4]
Skill: pr-review

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR introduces a non-trivial new feature/config surface (measurement manifests) but has no linked issue authorizing the work in the PR description.

## Issue Context
Compliance requires non-trivial changes (20+ changed lines or structural changes) to include an explicit linked issue (e.g., `Fixes #123` / `Refs #123`, or a full issue URL) that authorizes the change.

## Fix Focus Areas
- eval/measurements/README.md[1-4]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
✅ Compliance rules (platform): 55 rules
✅ Skills: 4 invoked
  code-review
  code-implementation
  pr-review
  docs-review

Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread eval/measurements/code.yaml
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 10, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 11:52 AM UTC · Ended 11:54 AM UTC

Commit: fe9703a · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 10, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 11:55 AM UTC · Completed 12:08 PM UTC

Commit: c1dbd68 · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review

Findings

High

  • [protected-path] .pre-commit-config.yaml — This file is under a protected path. The PR has no linked issue in fullsend-ai/agents providing authorization for modifying governance/infrastructure files. The change adds a lint-measurements pre-commit hook entry, which is consistent with the PR's intent and mirrors the existing lint-agent-docs hook pattern, but human approval is always required for protected-path changes regardless of context.
    Remediation: Obtain explicit human approval for this infrastructure change, or link to a tracking issue in fullsend-ai/agents that authorizes the .pre-commit-config.yaml modification.

Low

  • [missing-authorization] This PR introduces a non-trivial new feature (measurement manifests, lint tooling, pre-commit hooks, documentation — ~600 lines across 14 files) with no linked issue in the fullsend-ai/agents repository. The PR body references a companion platform PR (fullsend#6036 / ADR 0087), which provides design-level context and adequate authorization signal.
    Remediation: Consider creating a tracking issue in fullsend-ai/agents for traceability.

Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run

Review

Findings

High

  • [protected-path] .pre-commit-config.yaml — This PR modifies .pre-commit-config.yaml, a protected governance/infrastructure file. The PR has no linked issue to establish authorization for this change. Human approval is always required for protected-path changes.
    Remediation: Link an authorizing issue to this PR that justifies modifying .pre-commit-config.yaml, or obtain human approval for the protected-path change.

Low

  • [naming-convention] eval/lint-measurements.sh:1 — The new pre-commit local hook entry script uses a .sh extension (eval/lint-measurements.sh), while the existing analogous hook script has no extension (hack/lint-agent-docs). Both are language: script hooks serving the same purpose. The codebase convention for pre-commit local-hook linter scripts is extensionless.

  • [missing-authorization] No linked issue for this non-trivial feature PR (578 additions, 14 files). The PR body provides context via companion PR feat(eval): add eval measurements and EM-001 trace_fitness scorer fullsend#6036 and ADR 0087 but no authorizing issue is linked in this repository for scope traceability.


Labels: PR adds new eval/measurements subsystem (YAML manifests, linter, tests) — feature addition, not purely documentation


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (2)

Review

Findings

High

  • [protected-path] .pre-commit-config.yaml — This PR modifies .pre-commit-config.yaml, which is a protected governance/infrastructure file. The PR has no linked issue to authorize changes to protected paths. Human approval is always required for protected-path changes.
    Remediation: Ensure a human reviewer explicitly approves the changes to .pre-commit-config.yaml.

Low

  • [variable-naming-consistency] eval/lint-measurements.sh:24 — Counter variable naming is inconsistent with eval/lint-cases.sh, the closest sibling linter in the eval/ tree. eval/lint-cases.sh uses uppercase ERRORS=0, while this script uses lowercase errors=0 and file_count=0. Although hack/lint-agent-docs uses lowercase, eval/lint-cases.sh is the more relevant local precedent.

  • [error-message-consistency] eval/lint-measurements.sh:179 — Final error message uses failure(s) while the sibling linter eval/lint-cases.sh uses failures without the parenthetical.

  • [yaml-document-marker-consistency] eval/measurements/code.yaml:1 — The six new measurement manifests use the YAML document start marker (---) on line 1, while all existing YAML files under eval/ (e.g. eval/code/eval.yaml, eval/triage/cases/001-bug-url-encoding/input.yaml) omit it. This introduces a minor style inconsistency within the eval/ tree.

  • [incomplete documentation] LOCAL.md:133LOCAL.md describes the eval/ directory as containing only 'functional test scenarios that run agents against ephemeral GitHub repos and score the results.' With this PR, eval/ now also contains eval/measurements/ — default online-scoring measurement manifests consumed by fullsend eval-measure. The description is not wrong but is now incomplete; eval/README.md (updated in this PR) serves as the canonical reference.
    Remediation: Add a brief mention of measurement manifests to the eval/ description in LOCAL.md.

  • [missing documentation] README.md:18 — The 'Repository structure' section lists all major directories but omits eval/, a significant top-level directory. While this omission predates this PR, it becomes more noticeable with measurement manifests added.
    Remediation: Add eval/ to the directory listing in README.md.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR
Previous run (3)

Review

Findings

Low

  • [yaml-document-marker-consistency] eval/measurements/code.yaml:1 — The six new measurement manifests use the YAML document start marker (---) on line 1, while all existing YAML files under eval/ (e.g. eval/code/eval.yaml, eval/triage/cases/001-bug-url-encoding/input.yaml) omit it. This introduces a minor style inconsistency within the eval/ tree. Since these files live in their own subdirectory and serve a distinct purpose, this is cosmetic rather than harmful, but dropping the --- would align with the established convention.

  • [incomplete documentation] LOCAL.md:133 — LOCAL.md describes the eval/ directory as containing only 'functional test scenarios that run agents against ephemeral GitHub repos and score the results.' With this PR, eval/ now also contains eval/measurements/ — default online-scoring measurement manifests consumed by fullsend eval-measure. The description is not wrong but is now incomplete; a reader following this guide would not learn that eval/ also houses measurement manifests.
    Remediation: Add a sentence after line 135 mentioning the measurement manifests, e.g.: 'The eval/measurements/ subdirectory holds default online-scoring manifests (see eval/measurements/README.md).'

Previous run (4)

Review

Findings

Low

  • [documentation-link-consistency] eval/README.md:151 — Reference to 'fullsend ADR 0087' uses plain text while adjacent references in the same sentence use markdown hyperlinks (e.g., [fullsend#6036](...)). The eval/measurements/README.md already hyperlinks the same ADR with a direct URL, so the link target is known and available.

Labels: PR adds YAML policy files and documentation for eval measurement manifests

Previous run (5)

Review

Findings

Low

  • [scope-architectural-inconsistency] eval/measurements/prioritize.yaml — Measurement manifest exists for the 'prioritize' agent, but no functional eval directory exists at eval/prioritize/. The measurements README explains these are separate systems, but an explicit note that measurement coverage is independent of functional eval coverage would prevent future confusion.

  • [scope-architectural-inconsistency] eval/measurements/retro.yaml — Same as above for the 'retro' agent — no functional eval directory at eval/retro/. Consider a one-line clarification in the measurements README.

  • [scope-missing-dependency] eval/measurements/README.md:14 — References 'fullsend ADR 0087' as the authoritative specification without a direct URL. The ADR lives in the external fullsend-ai/fullsend repository (a standard cross-repo reference pattern), but adding a direct link when the ADR is published would improve discoverability.

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added the ready-for-merge All reviewers approved — ready to merge label Aug 10, 2026
@ascerra
ascerra force-pushed the feat/eval-measurements-manifests branch from c1dbd68 to 74b8959 Compare August 10, 2026 13:27
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 10, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 1:28 PM UTC · Ended 1:33 PM UTC

Commit: 74b8959 · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 10, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 1:34 PM UTC · Completed 1:48 PM UTC

Commit: 93d3db9 · View workflow run →

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: No CI/pre-commit validation for eval/measurements/*.yaml — bad scorer/agent values silently no-op forever

(Referencing .pre-commit-config.yaml line 49, which isn't part of this PR's diff, so noting it here in the review body instead of inline.)

Confirmed on head 93d3db9f: the repo has a pre-commit hook lint-agent-docs (./hack/lint-agent-docs) scoped via files: ^(harness/|docs/|hack/lint-agent-docs) that validates every harness/*.yaml has a valid doc: field, and a separate eval/lint-cases.sh that validates eval/<agent>/cases and eval.yaml judges — but neither covers the new eval/measurements/*.yaml files added in this PR, and no .github/workflows/*.yml step does either. Only the generic check-yaml pre-commit hook (syntax-only) applies.

On the consumer side, in the companion PR fullsend-ai/fullsend#6036, internal/evalmeasure/registry.go's ScoreTrace has:

switch m.Scorer {
case ScorerFitness: ...
default: // Unknown scorers are skipped (forward-compatible)
}

i.e. a typo'd scorer: value (e.g. trace-fitness with a hyphen) or a misspelled agent: value that never matches a trace's agent name would pass this repo's CI cleanly, merge, and then silently produce zero measurements for that agent in production with no error/log/CI failure.

The six current manifests all correctly use the literal trace_fitness matching const ScorerFitness = "trace_fitness" in fitness.go, so nothing is broken today, but there's no guardrail against this on the next manifest edit or new-agent addition.

Suggestion: Add a small lint script (sibling to hack/lint-agent-docs, or extend eval/lint-cases.sh) that validates eval/measurements/*.yaml: agent: matches an existing agents/<name>.md, measurements[].id is unique per file, and .scorer/.version are present/well-formed (and, once the fullsend-side registry is stable, checked against a known-scorer allow-list). Wire it into .pre-commit-config.yaml (files: ^eval/measurements/) so a typo fails CI instead of silently no-op'ing at runtime.

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added the documentation Improvements or additions to documentation label Aug 10, 2026
@ascerra
ascerra marked this pull request as draft August 11, 2026 01:40
@ascerra
ascerra force-pushed the feat/eval-measurements-manifests branch from 93d3db9 to 228c7b5 Compare August 13, 2026 11:49
@ascerra
ascerra marked this pull request as ready for review August 13, 2026 11:50
@fullsend-ai-review

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Ended 11:50 AM UTC

Commit: 228c7b5 · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 13, 2026

Copy link
Copy Markdown

🤖 Review · ❌ Terminated · Started 11:51 AM UTC · Ended 12:04 PM UTC

Commit: 228c7b5 · View workflow run →

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 228c7b5

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 11:51 AM UTC · Completed 12:04 PM UTC

Commit: 228c7b5 · View workflow run →

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review sweep — 3 findings (0 duplicates skipped after checking existing inline comments/reviews on this PR).

Comment thread eval/measurements/review.yaml Outdated
@@ -0,0 +1,6 @@
---

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: review.yaml enables em-001 even though its work_item sub-check can never pass for review traces

Verified against the companion PR's actual code (fullsend-ai/fullsend#6036, internal/evalmeasure/fitness.go @ 615dd691c2): ScoreFitnessNamed requires 8/8 sub-checks to pass for label="pass". One sub-check, workItemOK(run), requires fullsend.work_item_id to be set and not the literal "unknown". The code's own comment states: '"unknown" is the CLI sentinel when no ISSUE_*/GITHUB_ISSUE_URL is set (common for review, which wires PR_NUMBER / GITHUB_PR_URL instead).' This is confirmed by the companion PR's own test fixture testdata/review-unknown-workitem.jsonl (fullsend.work_item_id="unknown") and score_test.go's TestScoreFitness_ReviewUnknownWorkItemFails, which asserts Explanation contains work_item=fail and missing: work_item for an agent:review trace. Since this agents-repo PR enables em-001 for review, review's fitness score is structurally capped at 7/8=0.875 and will always report label=fail by design — nothing in this PR or the companion PR's manifest/README documents that cap specifically for review, and it is presented as uniform across all six enabled agents.

Suggestion: Add a note next to review.yaml (or in the README's per-agent table) that review's em-001 label will always read 'fail' until the scorer gains a PR-based identity check or review starts populating fullsend.work_item_id, so downstream trend consumers don't mistake the permanent fail for a regression.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disagree — this is not a structural cap on review, and we are not going to document a permanent fail or drop review.yaml.

The comment you quoted from fitness.go @ 615dd691c2 was already stale relative to production CLI. resolveWorkItemID() gained a PR-env fallback in fullsend-ai/fullsend#5622 (fixes fullsend-ai/fullsend#5621, merged 2026-07-27, present in v0.33.0 through current v0.36.0). After issue-shaped env is exhausted it uses REPO_FULL_NAME+PR_NUMBER, then GITHUB_PR_URL, then bare PR_NUMBER.

Current fullsend-ai/fullsend#6036 fitness.go matches that: remaining "unknown" is a real fitness fail, not expected for review. TestScoreFitness_ReviewUnknownWorkItemFails / review-unknown-workitem.jsonl are a synthetic regression fixture (work_item_id="unknown"), not wild review traces.

Proof from a live review job today on v0.36.0 (https://github.com/fullsend-ai/.fullsend/actions/runs/32057512371, reviewing fullsend#6111):

  • GITHUB_ISSUE_URL empty (the exact #5621 case)
  • PR_NUMBER=6111, GITHUB_PR_URL=https://github.com/fullsend-ai/fullsend/pull/6111
  • root run span: "fullsend.work_item_id": "fullsend-ai/fullsend#6111"

workItemOK still rejects the sentinel — that is the scorer working. Review can pass 8/8. Leaving review.yaml enabled.

Comment thread eval/measurements/README.md Outdated

Executable logic stays in fullsend because `fullsend eval-measure` is the
released binary that reads `run-telemetry.jsonl` (produced by fullsend). This
repo is content/policy, not that binary. Platform checks like EM-001

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: EM-001/em-001 casing inconsistency reintroduced after prior fix

A prior review thread on this PR (resolved) flagged the manifests using 'em-001' while the README used 'EM-001', and commit e8b75c0 ('fix(eval): normalize ID casing and add YAML document markers') fixed that original occurrence, correctly using lowercase in the 'First ship' section and the id: field. However, a later commit, 4d5728a ('docs(eval): clarify measurement manifest ownership'), rewrote/expanded the README and introduced a brand-new sentence at this line — 'Platform checks like EM-001 (trace_fitness) still get enabled here for each stock agent' — using uppercase again, while lines 47/54 in the same file (added by the same rewrite) correctly use lowercase 'em-001' matching the manifests' literal id: fields. This is a new, unresolved instance of the exact ambiguity the earlier fix addressed, at a different location than the already-resolved thread, so it was never caught by the earlier fix.

Suggestion: Change 'Platform checks like EM-001' to 'Platform checks like em-001' so the file consistently uses the manifest's literal id: em-001 casing everywhere.

@ascerra ascerra Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a309a25 — that sentence now uses em-001 to match the manifest id: fields and the First ship section.

Comment thread eval/measurements/README.md Outdated
| Custom policy for one org or a BYOA agent | Local override in the consumer repo |

Companion platform PR: [fullsend-ai/fullsend#6036](https://github.com/fullsend-ai/fullsend/pull/6036)
([ADR 0087](https://github.com/fullsend-ai/fullsend/blob/main/docs/ADRs/0087-eval-measurements-online-trace-scoring.md)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: ADR 0087 hyperlink 404s (target doesn't exist on fullsend main)

Both this line and eval/README.md:151 render a live markdown hyperlink to https://github.com/fullsend-ai/fullsend/blob/main/docs/ADRs/0087-eval-measurements-online-trace-scoring.md. Verified with curl (https://raw.githubusercontent.com/fullsend-ai/fullsend/main/docs/ADRs/0087-eval-measurements-online-trace-scoring.md -> HTTP 404) and via gh pr view 6036 --repo fullsend-ai/fullsend (state OPEN, mergedAt null) that ADR 0087 only exists on the still-unmerged companion PR fullsend-ai/fullsend#6036. This is distinct from the two already-posted review comments on this PR (one asked to add a direct URL 'when the ADR is published', the other flagged plain-text-vs-hyperlink inconsistency) — commit 228c7b5 ('docs(eval): hyperlink ADR 0087 in eval README', current head) added the real hyperlink before the ADR was published, so the link now renders and 404s for anyone who clicks it prior to #6036 merging.

Suggestion: Hold merging this PR until fullsend-ai/fullsend#6036 lands, or temporarily point both links at the PR itself (https://github.com/fullsend-ai/fullsend/pull/6036) and swap to the permanent docs/ADRs/0087 URL once #6036 merges.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a309a25. Both eval/measurements/README.md and eval/README.md no longer hyperlink docs/ADRs/0087-*.md on fullsend main (still 404 while #6036 is open). They point at fullsend-ai/fullsend#6036 and say ADR 0087 lands with that PR.

Merge order stays engine first (fullsend#6036) then this PR; we can swap to the permanent ADR blob URL after #6036 is on main if we still want a direct file link.

@ascerra
ascerra force-pushed the feat/eval-measurements-manifests branch from 228c7b5 to b1de714 Compare August 17, 2026 19:31
@ascerra

ascerra commented Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

@waynesun09 on the 10 Aug review-body finding (no CI for eval/measurements/*.yaml):

Fixed in b1de714. Added eval/lint-measurements.sh (wired in .pre-commit-config.yaml as lint-measurements, files ^eval/measurements/) plus eval/lint-measurements-test.sh on make script-test.

It fails CI on: unknown scorer: (allow-list is trace_fitness, matching fullsend ScorerFitness), missing/duplicate id, uppercase ids, missing/non-integer version, agent: that does not match agents/<name>.md, and filename stem ≠ agent: (jobs fetch ${AGENT}.yaml). A hyphenated trace-fitness typo now errors instead of silently scoring nothing.

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 17, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 7:33 PM UTC · Completed 7:48 PM UTC

Commit: b1de714 · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot removed the ready-for-merge All reviewers approved — ready to merge label Aug 17, 2026

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review sweep — 5 findings (0 duplicates skipped after checking existing inline comments/reviews on this PR).

Comment thread eval/measurements/README.md Outdated
Managed fullsend jobs resolve manifests as:

1. Local `${FULLSEND_DIR}/eval/measurements/${AGENT}.yaml` if present (override / BYOA)
2. Else this repo at the `v0` pin:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH: README documents an insecure/unpinned raw-URL manifest fetch that contradicts the companion PR's actual (tested) implementation

README states as fact that managed jobs fetch stock manifests via https://raw.githubusercontent.com/fullsend-ai/agents/v0/eval/measurements/${AGENT}.yaml. Verified against the companion PR fullsend-ai/fullsend#6036 (head 3d5aefc6) that this repo's PR is coupled to: action.yml line ~422 explicitly comments "For GetRef of agents@v0 (SHA pin). Not sent to raw.githubusercontent.com." and requires GH_TOKEN; internal/cli/evalmeasure.go warns "No GH_TOKEN/GITHUB_TOKEN; agents@v0 GetRef is unauthenticated"; and internal/cli/evalmeasure_test.go::TestActionYML_EvalMeasureNoFloatingV0Curl asserts action.yml does NOT contain the string raw.githubusercontent.com/fullsend-ai/agents/v0/eval/measurements. The actual mechanism is an authenticated, SHA-pinned GitHub GetRef call, not the unauthenticated floating-v0 curl this README asserts as fact. This claim is still present unmodified at current head and has not been raised or addressed in any existing review thread on this PR.

Suggestion: Update the README to describe the actual mechanism (SHA-pinned GitHub API GetRef requiring GH_TOKEN/GITHUB_TOKEN), or defer to fullsend-ai/fullsend's own docs for the resolution mechanism instead of asserting a specific (incorrect) URL scheme here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2a54535. The README no longer claims a floating raw.githubusercontent.com/fullsend-ai/agents/v0/... fetch. It now matches fullsend#6036: local FULLSEND_DIR override, else SHA-pin tags/v0 via GitHub GetRef (managed jobs pass GH_TOKEN / GITHUB_TOKEN) and fetch eval/measurements/${AGENT}.yaml at that commit.

Comment thread eval/lint-measurements.sh Outdated
name="$(basename "$yaml_file")"
stem="${name%.yaml}"

mapfile -t parsed < <(parse_manifest "$yaml_file")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HIGH: New pre-commit-hook script uses Bash 4+ constructs (mapfile, declare -A), breaking the repo's established macOS bash-3.2 compatibility convention for locally-executed hooks

eval/lint-measurements.sh uses mapfile -t parsed < <(...) (line 92) and declare -A seen_ids=() (line 122), both requiring Bash >=4. It is wired into .pre-commit-config.yaml as a language: script local hook (entry: ./eval/lint-measurements.sh, files: ^(eval/measurements/|eval/lint-measurements)), meaning it runs automatically on every local commit touching those paths, on whatever bash the contributor's machine provides. This repo has an established, explicit convention of avoiding these exact constructs for such scripts: scripts/lib/gitleaks-install.lib.sh (used by pre-commit-invoked scripts/post-fix.sh/scripts/post-code.sh) states in a comment "Uses case statements (not declare -A / mapfile) so the script runs on bash 3.2 (macOS system bash)", and the sibling pre-commit hook script hack/lint-agent-docs likewise avoids mapfile/declare -A. (Note: mapfile/declare -A do appear elsewhere in the repo, e.g. scripts/bundle-sh.sh and .github/scripts/select-eval-agents.sh, but neither of those is wired as an automatic local pre-commit hook the way this new script is — they run via make or in CI.) On stock macOS /usr/bin/bash (3.2.57), this hook will hard-fail with mapfile: command not found / declare: -A: invalid option, blocking local commits to eval/measurements/*.yaml even though CI (Linux, bash 5) passes fine.

Suggestion: Rewrite the parser to avoid mapfile/declare -A (e.g. plain indexed arrays, a temp file, or a case-statement pattern like scripts/lib/gitleaks-install.lib.sh), consistent with this repo's existing convention for pre-commit-invoked scripts, or explicitly document/enforce a Bash >=4 requirement for this hook if the team decides to break that convention.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2a54535. eval/lint-measurements.sh is a bash 3.2 wrapper that python3s the checks — no mapfile, no declare -A. Same pattern as keeping pre-commit hooks runnable on macOS /usr/bin/bash.

Comment thread eval/lint-measurements.sh Outdated
file_errors=$((file_errors + 1))
continue
fi
if [[ ! "$mid" =~ ^[a-z][a-z0-9]*-[0-9]+$ ]]; then

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: Lint enforces a stricter measurement-id format than fullsend's actual manifest loader requires

The lint hard-fails any measurement id not matching ^[a-z][a-z0-9]*-[0-9]+$ ("must be lowercase like em-001"). Verified against the companion PR's actual LoadRegistry validation (fullsend-ai/fullsend#6036, internal/evalmeasure/registry.go): it only requires ID != "" and rejects pipe/newline characters — there is no lowercase/hyphen/digit-suffix requirement in the accepted contract. This id-format convention is invented in this repo's linter and enforced as a hard failure with no indication it's a local-only style rule rather than an upstream-mirrored constraint, so it could reject an otherwise-valid id that fullsend's loader would accept.

Suggestion: Document this as an agents-repo-only style convention independent of fullsend's validation, or relax the regex so it doesn't reject ids that fullsend's registry would accept.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept the em-001 lowercase style for stock manifests in this repo (it is the casing this PR standardized), and documented it as an agents-repo style convention stricter than fullsend LoadRegistry (non-empty id, no pipe/newline). The linter header, README, and the error string now say that explicitly so it is not mistaken for an upstream contract.

Comment thread eval/lint-measurements.sh Outdated
name="$(basename "$yaml_file")"
stem="${name%.yaml}"

mapfile -t parsed < <(parse_manifest "$yaml_file")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: Python parser subprocess failures inside process substitution are swallowed under set -e, producing a misleading error

mapfile -t parsed < <(parse_manifest "$yaml_file") uses process substitution. The script sets set -euo pipefail (line 14), but a non-zero exit from a command substituted via <(...) is not propagated to set -e in the calling shell (a well-known bash limitation — set -e does not observe failures inside process substitutions). If the embedded Python heredoc raises (e.g. a UTF-8 decode error or unexpected traceback), the script won't fail with a clear parser error; parsed ends up empty/short, agent resolves to an empty string, and the script instead reports the misleading "missing agent: field" error (lines 98-102) rather than the real parser crash.

Suggestion: Capture the Python subprocess's exit code explicitly — e.g. parsed_raw=$(parse_manifest "$yaml_file") || { echo "ERROR: $name: parser failed"; exit 1; } using command substitution instead of process substitution — so a genuine parser crash is reported distinctly from a malformed manifest.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2a54535. The bash wrapper uses command substitution into python3 (not process substitution / mapfile), so a parser crash exits non-zero and prints parser failed instead of a fake missing-agent: error.

Comment thread eval/lint-measurements.sh Outdated
exit 1
fi

parse_manifest() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: Hand-rolled line-oriented regex parser is not real YAML and will silently misparse as the manifest schema grows

The embedded Python parser (lines 35-85) is a regex/state-machine line scanner, not a YAML parser: it only recognizes block-style lists and a fixed id|scorer|version key set. eval/measurements/README.md (lines ~35-39 in its "What lives where" table) already anticipates a future declarative assert: block under a measurement item; with the current parser, any nested - line under in_measurements (lines 56-64) is treated as a new top-level measurement rather than a nested structure, silently producing a wrong count and confusing errors instead of a clean parse failure. Similarly, valid flow-style YAML (e.g. measurements: [{id: em-001, ...}]) that the real runtime parser (yaml.v3 in fullsend's LoadRegistry) accepts fine is invisible to this parser and reported as the generic "measurements list is empty" error.

Suggestion: Either constrain the parser strictly to the currently-shipped three-field block-style shape and document that assumption prominently (failing closed with a clear "unsupported YAML shape" error when zero items are extracted from a non-empty measurements: key), or parse with a real YAML library (e.g. PyYAML) inside the Python heredoc so the lint stays correct as the schema grows.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2a54535. The parser is still a constrained reader for the shipped three-field block-style schema (no PyYAML in this repo's lint CI), but it now fails closed on anything else: flow-style measurements: [...], nested lists, and extra fields such as future assert:. Tests cover flow-style and nested assert:.

@ascerra
ascerra force-pushed the feat/eval-measurements-manifests branch from b1de714 to 2a54535 Compare August 18, 2026 01:53
@ascerra

ascerra commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Review-agent sticky findings on b1de714 / follow-up 2a54535:

  • protected-path on .pre-commit-config.yaml: left in place. The lint-measurements hook was added to answer Wayne's 10 Aug finding (unknown scorer: would silently no-op). Needs human approval of that protected-path change — not something we should revert.
  • LOCAL.md: eval/ description now mentions eval/measurements/ online-scoring manifests.
  • README.md repository structure: added eval/.
  • Inline lows (counter naming, failure(s), YAML ---) addressed in 2a54535; replies are on those threads.

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 18, 2026

Copy link
Copy Markdown

🤖 Finished Review · ❌ Failure · Started 1:54 AM UTC · Completed 1:56 AM UTC

Commit: 2a54535 · View workflow run →

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review sweep on head 2a545357 — 5 findings (0 duplicates skipped after checking existing inline comments/reviews on this PR). All findings are inline; each is verified against this head and against the companion engine PR fullsend#6036 @ 61b9bae7.

Comment thread eval/lint-measurements.sh
continue
if um and um.group(1) not in FIELD_KEYS:
raise UnsupportedShape("unsupported field %r" % um.group(1))
if re.match(r"^\S", stripped):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: Linter fails OPEN on unknown top-level keys after the measurements list, contradicting the "fails closed" claim

The list-exit branch at lines 96-100 (if re.match(r"^\S", stripped): in_measurements = False; items.append(current); current = None; continue) consumes the first non-indented line after the measurements list without validating it, so the unsupported-top-level-field check at lines 102-103 is never reached for it. Once in_measurements is False, every subsequent indented line also falls through all branches silently.

Reproduced twice against head 2a545357 (linter run with MEASUREMENTS_DIR/AGENTS_DIR fixtures):

  1. agent: code + a valid measurement + trailing not_a_real_key: true -> code.yaml: OK (agent=code, 1 measurement(s)), exit 0. The same key placed before measurements: correctly errors with "unsupported top-level field".
  2. Worse, the realistic typo case: a valid block followed by a typo'd mesurements: key holding scorer: totally_bogus_scorer also reports OK, exit 0.

This falsifies the header comment at lines 20-21 ("other shapes fail closed as 'unsupported YAML shape'") and the stated goal of commit 2a545357. It is not covered by the existing line-35 thread (whose substance was "hand-rolled parser is not real YAML") — this is a specific verified fail-open bug in the replacement parser that the thread's reply ("Fixed in 2a54535 ... it now fails closed on anything else") introduced.

End-to-end there is no second net: fullsend's LoadRegistry (internal/evalmeasure/registry.go @ 61b9bae7) uses plain yaml.Unmarshal with no KnownFields(true), so yaml.v3 silently drops the unknown key at runtime too — neither lint nor runtime surfaces the typo, which is exactly the silent-zero-score class this linter exists to catch.

Suggested fix: Re-dispatch the terminating line through the top-level-field validation instead of continue-ing past it — factor lines 102-103 into a helper and call it from both the loop fallthrough and the end-of-list branch. Also treat any indented line seen while in_measurements is False as UnsupportedShape rather than ignoring it. Add regression cases to eval/lint-measurements-test.sh for a trailing unknown top-level key and for the mesurements: typo.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 70e7591. Ending the measurements list now re-dispatches the terminating line through top-level validation instead of continue-ing past it. Trailing not_a_real_key: and a typo'd mesurements: after a valid block both fail as unsupported top-level field. Regression cases added in eval/lint-measurements-test.sh.

Comment thread eval/lint-measurements.sh Outdated
MEASUREMENTS_DIR, AGENTS_DIR = sys.argv[1], sys.argv[2]
KNOWN_SCORERS = frozenset({"trace_fitness"})
ID_STYLE = re.compile(r"^[a-z][a-z0-9]*-[0-9]+$")
FIELD_KEYS = frozenset({"id", "scorer", "version"})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: Linter rejects name:, a field the fullsend engine supports today (not a future field)

FIELD_KEYS = frozenset({"id", "scorer", "version"}) causes any other measurement key to raise UnsupportedShape at line 94. Reproduced against head 2a545357: a manifest with name: Trace Fitness fails with unsupported YAML shape (unsupported field 'name'), exit 1.

Verified against fullsend#6036 head 61b9bae7, internal/evalmeasure/registry.go: MeasurementSpec declares Name string `yaml:"name"` // optional display override; default = Scorer. It is consumed by evalName(), which ScoreTrace passes to ScoreFitnessNamed, and LoadRegistry explicitly validates it (rejects pipe/newline in .name). So name is a functional, shipped engine field — a manifest that is valid and meaningful to the engine hard-fails agents CI with a misleading message.

This is distinguishable from the existing "fail closed on extra fields" reply (comment 3800328729), which justified the strict shape by pointing at a future assert: block. assert: does not exist engine-side; Name does, today.

Related: eval/measurements/README.md:26 ("fullsend's loader only requires a non-empty id") also understates LoadRegistry, which additionally requires non-empty scorer, version >= 1, and no pipe/newline in id/scorer/name.

Suggested fix: Add name to FIELD_KEYS and validate it for pipe/newline the way LoadRegistry does. If excluding it from stock manifests is deliberate, say so explicitly in the script header and README ("stock manifests deliberately omit the engine's optional name: override") rather than reporting it as an unsupported YAML shape. Also correct the README.md:26 parenthetical to match LoadRegistry's real contract.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 70e7591. name is in FIELD_KEYS and rejected for pipe/newline the same way fullsend LoadRegistry does. Optional name: Trace Fitness now lints green. README parenthetical updated to the real contract (non-empty id/scorer, version>=1, no pipe/newline in id/scorer/name).

Comment thread eval/lint-measurements.sh
print(" ERROR: %s: agent %r does not match filename stem %r (jobs fetch ${AGENT}.yaml)" % (name, agent, stem))
errors += 1
file_errors += 1
if not os.path.isfile(os.path.join(AGENTS_DIR, agent + ".md")):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: A scribe.yaml would lint green but can never be fetched, producing the exact silent-zero-score outcome the linter targets

The agent-name check at line 148 accepts any agent: value that has a matching agents/<name>.md. agents/scribe.md exists in this repo, so a scribe.yaml passes cleanly — reproduced against head 2a545357: scribe.yaml: OK (agent=scribe, 1 measurement(s)), exit 0.

But the engine gates the manifest fetch on a narrower list. Verified in fullsend#6036 head 61b9bae7, internal/cli/run.go: defaultAgentsRepoKnownAgents = {triage, code, fix, review, retro, prioritize} (no scribe), and tryAgentsRepoMeasurementManifest returns early for anything not in that map, so eval/measurements/scribe.yaml is never fetched.

The trap is live because eval/measurements/README.md frames scribe's absence as a policy choice ("scribe has no forge work-item identity today"), inviting a contributor to just add the file — it would lint green, merge, and silently score nothing forever. The same trap applies to any future stock agent added here before the engine map is updated.

Suggested fix: Do not hard-code the engine's fetchable set in the linter (that map is explicitly transitional per its own comment — ADR 0058 Phase 5 removes it). Instead add a note to eval/measurements/README.md, and a comment beside the agents/<name>.md check, stating that a manifest only takes effect for agents in fullsend's first-party fetch allow-list (defaultAgentsRepoKnownAgents in internal/cli/run.go), so adding scribe requires a fullsend change first.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documented in 70e7591 (did not hard-code the engine allow-list in the linter — that map is transitional per ADR 0058 Phase 5). README First ship + the linter header now say a file only takes effect for agents in fullsend defaultAgentsRepoKnownAgents (the six we ship); agents/<name>.md alone is not enough, so scribe needs a fullsend change first.

Comment thread eval/measurements/README.md Outdated
| Change | PR |
|---|---|
| New Go scorer or (future) new declarative `assert:` | `fullsend` |
| New measurement id / enable / disable / thresholds for a stock agent using an existing scorer | **agents** (this repo) |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: Change-guide claims "thresholds" are an agents-repo change, but no threshold field exists in the schema

Line 44 states: | New measurement id / enable / disable / thresholds for a stock agent using an existing scorer | **agents** (this repo) |.

Verified against fullsend#6036 head 61b9bae7: MeasurementSpec in internal/evalmeasure/registry.go has only ID, Scorer, Name, Version — there is no threshold field, and ScoreTrace dispatches purely on Scorer. This PR's own linter would reject a thresholds: key as "unsupported YAML shape" (FIELD_KEYS at eval/lint-measurements.sh:42), and nothing in this PR's diff implements or tests such a field. The fullsend guide docs/guides/infrastructure/eval-measurements.md itself lists declarative checks / ratio-threshold bands as "Planned (not in first ship)".

The table is internally inconsistent on this point: the adjacent fullsend row at line 43 correctly marks assert: as "(future)", while the agents row leaves "thresholds" unmarked as if it ships today. A contributor following this table would open an agents PR that cannot pass lint and would have no runtime effect.

Suggested fix: Drop "thresholds" from the agents-side row, or mark it "(future)" the same way the adjacent row marks assert:, mirroring the fullsend guide's "Planned (not in first ship)" wording.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 70e7591. Dropped "thresholds" from the agents-side change-guide row. Thresholds / declarative assert: stay on the fullsend row as "(future)", matching the fullsend guide's "Planned (not in first ship)".

Comment thread eval/measurements/README.md Outdated
1. Local `${FULLSEND_DIR}/eval/measurements/${AGENT}.yaml` if present (override / BYOA)
2. Else a SHA-pinned fetch from `fullsend-ai/agents` at the `v0` tag:
`fullsend eval-measure` resolves `tags/v0` via GitHub `GetRef` (managed jobs
pass `GH_TOKEN` / `GITHUB_TOKEN`) and then fetches

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: "managed jobs pass GH_TOKEN / GITHUB_TOKEN" is false for GitLab-managed jobs

Lines 16-20 state that the SHA-pinned tags/v0 GetRef works because "managed jobs pass GH_TOKEN / GITHUB_TOKEN", with no forge qualification. That holds for GitHub Actions but not GitLab.

Verified against fullsend#6036 head 61b9bae7:

  • internal/scaffold/fullsend-repo-gitlab/.gitlab/ci/fullsend-agent.yml:296-297 comments: "This job has no GH_TOKEN; stock-agent manifests skip unless an operator exports GH_TOKEN or GITHUB_TOKEN. Local override still works."
  • internal/cli/evalmeasure.go:204 warns: "No GH_TOKEN/GITHUB_TOKEN; agents@v0 GetRef is unauthenticated. GitLab jobs skip stock manifests unless an operator wires a GitHub token."

So on GitLab the GetRef is unauthenticated/rate-limited and these stock manifests generally never load — the README asserts forge-wide default coverage the engine does not provide.

This is not a re-litigation of the resolved HIGH thread on line 16 (floating raw-URL wording, fixed in 2a54535): it is a new inaccuracy in the replacement text that fix introduced.

Suggested fix: Qualify per forge in that sentence: GitHub Actions jobs pass GH_TOKEN/GITHUB_TOKEN; GitLab jobs have no GitHub token by default, so stock manifests skip unless an operator wires one — use a local FULLSEND_DIR override there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 70e7591. README now qualifies per forge: GitHub Actions jobs pass GH_TOKEN/GITHUB_TOKEN for the SHA-pinned GetRef; GitLab-managed jobs do not by default, so stock manifests skip unless an operator wires a GitHub token — use a local FULLSEND_DIR override there.

@ascerra
ascerra force-pushed the feat/eval-measurements-manifests branch from 2a54535 to 70e7591 Compare August 18, 2026 22:38
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 18, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 10:40 PM UTC · Completed 10:55 PM UTC

Commit: 70e7591 · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added the enhancement New feature or request label Aug 18, 2026

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review sweep on head 70e7591 — 3 findings (0 duplicates skipped after checking existing inline comments/reviews on this PR). All findings are inline; each is verified against this head and against the companion engine PR fullsend#6036 @ 1b874261.

Comment thread eval/lint-measurements.sh Outdated
current = None

for raw in text.splitlines():
stripped = raw.split("#", 1)[0].rstrip()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: Linter's naive # stripping accepts values YAML reads literally — silent zero measurements, contradicting the "fail closed" header

Line 81 is stripped = raw.split("#", 1)[0].rstrip(), which treats every # as a comment start. YAML only starts a comment at line start or after whitespace, so code#x and trace_fitness#typo are plain scalars. This contradicts the header claim at lines 24-26 that non-shipped shapes "fail closed as 'unsupported YAML shape'".

Reproduced against PR head 70e7591 (linter run with fixture MEASUREMENTS_DIR/AGENTS_DIR) and against gopkg.in/yaml.v3 v3.0.1 using fullsend's exact Registry/MeasurementSpec struct tags from fullsend#6036 @ 1b874261 internal/evalmeasure/registry.go:

  • agent: code#x -> linter prints code.yaml: OK (agent=code, 1 measurement(s)), exit 0. yaml.v3: err=<nil>, Agent="code#x". In ScoreTrace (registry.go:74) the guard name != "" && name != UnknownSentinel && !strings.EqualFold(name, reg.Agent) returns nil — zero measurement rows, and the CLI prints the benign "No new measurements (already scored or no matching traces)" (internal/cli/evalmeasure.go printMeasurementResults). Silent forever.
  • scorer: trace_fitness#typo -> linter OK, exit 0. yaml.v3: Scorer="trace_fitness#typo" -> ScoreTrace default: branch -> a skip row, never a fitness score.

The pre-commit check-yaml --unsafe hook does not help: both files are valid YAML (verified with PyYAML — yaml.safe_load('agent: code#x') -> {'agent': 'code#x'}).

This is a residual hole in the same hand-rolled parser, not a regression: 70e7591 does fix what it claims (trailing unknown top-level key, mesurements: typo), and eval/lint-measurements-test.sh passes 14/14.

Suggestion: Match YAML's comment rule instead of splitting on any #: strip only a # at column 0 or preceded by whitespace (e.g. re.split(r'(?:^|\s)#', raw, 1)[0]), then raise UnsupportedShape on any residual # left inside an extracted agent/id/scorer/name value. Add regression cases for agent: code#x and scorer: trace_fitness#typo to eval/lint-measurements-test.sh, matching the convention used for the other fail-closed cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3cf6c3d. Comment stripping now matches YAML (# only at column 0 or after whitespace). Residual # inside agent / id / scorer / name values raises UnsupportedShape. Regression cases: agent: code#x, scorer: trace_fitness#typo, and a real trailing comment still passes.

Comment thread eval/lint-measurements.sh
print(" ERROR: %s: unknown scorer %r (allowed: %s)" % (name, scorer, ", ".join(sorted(KNOWN_SCORERS))))
errors += 1
file_errors += 1
if not re.match(r"^[1-9][0-9]*$", version or ""):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: Linter green-lights two shapes LoadRegistry hard-rejects (quoted version:, duplicate top-level key) — fail-open job, no scores

Two shapes lint green at head 70e7591 but hard-fail yaml.Unmarshal in LoadRegistry. Verified by running gopkg.in/yaml.v3 v3.0.1 (the version pinned in fullsend go.mod) against fullsend#6036 @ 1b874261 internal/evalmeasure/registry.go struct tags, and by running the linter with fixture dirs:

  1. version: "1" — values are unquoted by .strip("'\"") at lines 116/123 before the ^[1-9][0-9]*$ check at line 248, so it lints OK, exit 0. yaml.v3: yaml: unmarshal errors: line 5: cannot unmarshal !!str `1` into int (MeasurementSpec.Version is int).
  2. Duplicate top-level key (e.g. agent: twice) — the parser overwrites and reports OK, exit 0. yaml.v3: yaml: unmarshal errors: line 2: mapping key "agent" already defined at line 1.

Neither is caught by the check-yaml --unsafe pre-commit hook: both are valid YAML, and PyYAML silently last-wins on duplicate keys (confirmed: yaml.safe_load('agent: code\nagent: code') -> {'agent': 'code'}, no error).

Because MeasureAndExport returns fmt.Errorf("load registry: %w", err) and the managed jobs are explicitly fail-open (.gitlab/ci/fullsend-agent.yml:399-403 fullsend eval-measure ... || true under the comment "Eval measurements (fail-open) ... Never fail the agent job"; continue-on-error in the GitHub action), the manifest merges, CI stays green, and every wild run produces zero measurements — only a warn line in job logs. This directly contradicts the header claims at lines 22-26 ("stricter than fullsend LoadRegistry", "other shapes fail closed") added by the fix commits.

(Tab indentation is NOT part of this finding — check-yaml does reject it in-repo, verified.)

Suggestion: Validate version against the RAW token before quote-stripping so "1" / '1' are rejected with a message naming the engine's int field. Track top-level keys already seen and raise UnsupportedShape on a second agent: / measurements:, since yaml.v3 treats duplicates as a hard error. Add both as regression cases in eval/lint-measurements-test.sh, and narrow the line-22/24-26 header claims to enumerate exactly which LoadRegistry rules are mirrored.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3cf6c3d.

  1. version is validated on the raw token before quote-stripping — "1" / '1' fail with an unquoted-integer message (LoadRegistry int field / yaml.v3).
  2. Duplicate top-level agent: / measurements: raise UnsupportedShape (yaml.v3 hard error).

Header comments now enumerate exactly which LoadRegistry rules we mirror vs agents-repo style extras. Regression cases added for both shapes.

Comment thread eval/measurements/README.md Outdated
fetches `eval/measurements/${AGENT}.yaml` at that commit. It does **not**
curl the floating `raw.githubusercontent.com/fullsend-ai/agents/v0/...` URL.
GitHub Actions jobs pass `GH_TOKEN` / `GITHUB_TOKEN` for that `GetRef`.
GitLab-managed jobs do not have a GitHub token by default, so stock

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: README's new GitLab wording is now wrong in the opposite direction — engine moved to 1b874261 and reversed the behaviour

NEW drift, not a re-litigation of thread 3805744921 (README line 18). That thread was correct against fullsend#6036 @ 61b9bae7, and commit 70e7591 fixed it by writing what the engine said at that SHA. The engine has since changed under it: at fullsend#6036 head 1b874261 the behaviour is reversed, so the newly-added lines 20-23 are now false.

Current README lines 20-23: "GitHub Actions jobs pass GH_TOKEN / GITHUB_TOKEN for that GetRef. GitLab-managed jobs do not have a GitHub token by default, so stock manifests skip unless an operator wires one — use a local FULLSEND_DIR override there."

All three primary sources at 1b874261 contradict "stock manifests skip":

  1. docs/ADRs/0087-eval-measurements-online-trace-scoring.md:123-124: "fetch from public agents@v0 even without GH_TOKEN (rate-limited); a token is recommended on shared runners."
  2. internal/scaffold/fullsend-repo-gitlab/.gitlab/ci/fullsend-agent.yml:382-384: "agents is public, so GetRef works without GH_TOKEN, but unauthenticated calls share GitHub's ~60 req/hr per-IP limit — export GH_TOKEN / GITHUB_TOKEN on busy shared runners. Local override skips the fetch." The job wires an unconditional fullsend eval-measure ... || true (lines 399-403).
  3. internal/cli/evalmeasure.go evalMeasureFetchContext: on a missing token it does not skip — it emits StepWarn("No GH_TOKEN/GITHUB_TOKEN; agents@v0 GetRef runs unauthenticated (public repo, ~60 req/hr per IP). Prefer a token on shared runners; local FULLSEND_DIR override skips the fetch.") and returns gh.New(token) with the empty token.

So GitLab jobs do fetch stock manifests; the real failure mode is per-IP rate limiting on busy shared runners — a different operational instruction than "use a local FULLSEND_DIR override there", which tells GitLab operators to build an override they do not need. This is exactly the drift the PR body's own unchecked "Cross-check with fullsend#6036 / ADR 0087 before merge" item exists to catch.

The rest of the resolution documentation verified accurate at this head: local-first then SHA-pinned fetch, tags/v0 via GetRef (internal/cli/run.go:3867), the defaultAgentsRepoKnownAgents six-agent gate (run.go:3843), and the LoadRegistry contract restated at README lines 28-31.

Suggestion: Replace lines 20-23 with wording quoted from ADR 0087 rather than paraphrased, e.g.: "agents is public, so the GetRef works without a token on both GitHub Actions and GitLab; unauthenticated calls share GitHub's ~60 req/hr per-IP limit, so export GH_TOKEN/GITHUB_TOKEN on busy shared runners (GitHub Actions passes GH_TOKEN automatically). A local FULLSEND_DIR manifest skips the fetch entirely." Re-verify at whatever SHA #6036 finally merges at, then tick the cross-check box.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3cf6c3d. Re-checked against fullsend#6036 current tip (evalMeasureFetchContext warn + ADR 0087 wording): agents is public, so GetRef works without a token on both forges; the real limit is ~60 req/hr per IP on busy shared runners. README now says that and that a local FULLSEND_DIR override skips the fetch entirely — no longer claims GitLab stock manifests skip by default.

@ascerra

ascerra commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

After this merges, stock manifests still need a v0.x.y cut that re-points floating v0 before managed jobs pick them up. Tracking: fullsend-ai/fullsend#6384

ascerra and others added 10 commits August 19, 2026 15:13
Introduce per-agent measurement manifest YAML files that select which
eval measurement scorers run after managed agent jobs. Every agent
except scribe gets trace_fitness (EM-001) at initial ship.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Adam Scerra <ascerra@redhat.com>
Lowercase EM-001 to em-001 in README to match manifest YAML files.
Add --- document start markers to all measurement manifests for
consistency with harness/*.yaml convention.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Adam Scerra <ascerra@redhat.com>
Replace ambiguous "every listed agent" phrasing with an explicit list
of the six agents that enable trace_fitness at initial ship.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Adam Scerra <ascerra@redhat.com>
Spell out agents@v0 defaults vs local overrides, engine-vs-policy split
with fullsend#6036, and when a change needs fullsend vs agents.

Signed-off-by: Adam Scerra <ascerra@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Adam Scerra <ascerra@redhat.com>
Match eval/measurements/README.md link style for ADR 0087 so the
measurement manifest section uses consistent markdown hyperlinks.

Signed-off-by: Adam Scerra <ascerra@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Adam Scerra <ascerra@redhat.com>
Keep the measurement id casing aligned with manifest id fields.
Point readers at fullsend#6036 for ADR 0087 until that PR lands on main.

Signed-off-by: Adam Scerra <ascerra@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Adam Scerra <ascerra@redhat.com>
Reject unknown scorers, missing fields, duplicate ids, and agent
names that do not match agents/<name>.md so a typo fails CI instead
of silently producing zero scores.

Signed-off-by: Adam Scerra <ascerra@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Adam Scerra <ascerra@redhat.com>
Rewrite the pre-commit linter in python3 so it runs on macOS bash 3.2,
fails closed on unsupported YAML, and documents em-001 as an agents-repo
style rule. Describe the SHA-pinned v0 GetRef fetch instead of a floating
raw URL, drop eval/ --- markers, and mention eval/ in README and LOCAL.md.

Signed-off-by: Adam Scerra <ascerra@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Adam Scerra <ascerra@redhat.com>
Fail closed on unknown top-level keys after the measurements list,
accept optional name: matching fullsend MeasurementSpec, drop the
false thresholds claim, and document GitLab + fetch-allowlist limits.

Signed-off-by: Adam Scerra <ascerra@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: Adam Scerra <ascerra@redhat.com>
Match YAML comment rules so code#x / trace_fitness#typo fail closed,
reject quoted version and duplicate top-level keys that yaml.v3 rejects,
and align the GitLab GetRef docs with the public unauthenticated fetch.

Signed-off-by: Adam Scerra <ascerra@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ascerra
ascerra force-pushed the feat/eval-measurements-manifests branch from 70e7591 to 3cf6c3d Compare August 19, 2026 19:15
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 19, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 7:17 PM UTC · Completed 7:30 PM UTC

Commit: 3cf6c3d · View workflow run →

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the review comment for full details.

Comment thread .pre-commit-config.yaml
language: script
files: ^(harness/|docs/|hack/lint-agent-docs)
pass_filenames: false
- id: lint-measurements

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[high] protected-path

This file is under a protected path (.pre-commit-config.yaml). The PR has no linked issue in fullsend-ai/agents providing authorization for modifying governance/infrastructure files. The change adds a lint-measurements pre-commit hook entry, which is consistent with the PR's intent and mirrors the existing lint-agent-docs hook pattern, but human approval is always required for protected-path changes regardless of context.

Suggested fix: Obtain explicit human approval for this infrastructure change, or link to a tracking issue in fullsend-ai/agents that authorizes the .pre-commit-config.yaml modification.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants