Skip to content

feat(eval): add eval measurements and EM-001 trace_fitness scorer - #6036

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

feat(eval): add eval measurements and EM-001 trace_fitness scorer#6036
ascerra wants to merge 18 commits into
mainfrom
feat/eval-measurements

Conversation

@ascerra

@ascerra ascerra commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Introduce eval measurements: fail-open same-job scoring of wild agent OTEL traces (fullsend eval-measure), writing portable eval-measurements.jsonl beside telemetry when at least one new score is produced. First scorer is trace_fitness (EM-001). Decision recorded in ADR 0087.

Companion default-policy PR: fullsend-ai/agents#722 (manifests under eval/measurements/).

Activation is two-step: merge agents#722 and cut a v0.x.y release that re-points floating agents@v0. Until that release lands, managed GHA/GitLab measure steps stay provisional (clean skip when the remote manifest is missing). Tracking: #6384. Local FULLSEND_DIR overrides work today.

Ownership (please read)

Concern Repo
Parser, scorer implementations, CLI, GHA/GitLab post-step this PR (internal/evalmeasure/)
Default manifests for stock agents agents#722
Org overrides / BYOA manifests Consumer FULLSEND_DIR
  • Stock-agent defaults are SHA-pinned from fullsend-ai/agents@v0 when no local file exists — installs do not copy manifests to score stock agents.
  • Local ${FULLSEND_DIR}/eval/measurements/${AGENT}.yaml is override / opt-out / custom-agent only.
  • Executable logic stays in fullsend because eval-measure is the released binary that reads run-telemetry.jsonl (which fullsend writes). Agents is content/policy, not that binary.
  • EM-001 is a platform fitness check on the Level 1/2 telemetry metadata contract; stock agents enable it via agents manifests.
  • Planned: content-aware scorers on Level 3 prompt/completion capture once Level 3 is implemented — that is where quality judgments live. Metadata fitness is the foundation first.
  • Change guide: new Go scorer or (future) declarative assert: → fullsend PR; new id / enable / thresholds on an existing scorer for a stock agent → agents-only; org-specific policy → local override.
  • Planned (not in this PR): declarative logic-as-config in manifests so most agent-specific policy is YAML-only.

Tool-agnostic export

Core does not pick an observability product. Scores land in local eval-measurements.jsonl. Remote score export (when implemented) reuses the same OTEL_EXPORTER_OTLP_* path as ADR 0050. No vendor Assessments adapters or MLFLOW_* (or similar) wiring in managed workflows.

Related

  • Tracking agents@v0 cut: #6384
  • Companion manifests: agents#722
  • Span status from run outcome: #5944 (merged)
  • Harness snapshot / forge join keys: #5524 (open)
  • Level 3 activation draft / semantic-observability drafts closed without merge: #5947, #2423

Changes

  • ADR 0087 + guides (ownership, two-step v0 activation, declarative sketch, planned L3 content scorers), glossary / tracing cross-links
  • internal/evalmeasure parser + trace_fitness + local JSONL/ledger
  • fullsend eval-measure CLI; fail-open post-step in action.yml and GitLab fullsend-agent.yml
  • Manifest resolution: local FULLSEND_DIR then SHA-pinned agents@v0 fetch (--offline supported)
  • Platform telemetry discovery prefers host agent-<name>-… runDirs; EM-001 skips incomplete/runner-health traces (no agent span, missing root run span, pre-script skip)
  • GitLab: always export empty-or-real GITLAB_ISSUE_URL; keep host output/ out of sandbox tarball + git exclude

Testing

  • go test ./internal/evalmeasure/ (+ focused CLI / sandbox / scaffold tests)
  • Local FULLSEND_DIR manifest path produces eval-measurements.jsonl
  • pre-commit hooks on commit
  • Stock manifests on agents@v0 after merge + release cut (agents#722, #6384)

Optional dogfood (outside core): post-process the portable JSONL into an org-chosen backend. Example MLflow UI showing trace_fitness assessments — not core MLFLOW_* wiring:

image image

Checklist

  • PR title follows Conventional Commits
  • Commits are signed off (DCO)
  • I wrote this contribution myself and can explain all changes in it

Notes for reviewers

  • Portable OTLP score export is the ADR remote contract but not wired yet (local JSONL today).
  • CLI flag remains --registry (path to the YAML); rename to “manifest” is follow-up.
  • Explainer HTML kept local / out of this PR.
  • Managed measure steps are provisional until manifests exist on floating v0 (Track agents@v0 release cut for eval measurement manifests #6384).

@ascerra
ascerra requested a review from a team as a code owner August 10, 2026 11:44
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 10, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 11:45 AM UTC · Completed 12:02 PM UTC

Commit: fdf5632 · View workflow run →

@qodo-code-review

qodo-code-review Bot commented Aug 10, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Add eval measurements CLI and EM-001 trace_fitness scoring

✨ Enhancement 📝 Documentation 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add fail-open, same-job trace scoring that writes eval-measurements.jsonl beside telemetry.
• Introduce EM-001 trace_fitness scorer driven by per-agent measurement manifests.
• Document architecture/ownership via ADR 0087 and new operator guide.
Diagram

graph TD
  A["GitHub Action job"] --> B["fullsend run"] --> C[("run-telemetry.jsonl")] --> G{{"Resolve manifest"}} --> H["registry.yaml"] --> D["fullsend eval-measure"] --> E[("eval-measurements.jsonl")]
  E --> F["upload-artifact"]
  G --> I["skip scoring"]
  subgraph Legend
    direction LR
    _proc["Process"] ~~~ _file[("Artifact")] ~~~ _dec{{"Decision"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Embed stock manifests into the fullsend binary
  • ➕ No network fetch from agents@v0; works offline in CI
  • ➕ Eliminates runtime dependency on a pinned agents ref
  • ➖ Couples policy/content updates to fullsend releases
  • ➖ Harder for agents repo to evolve defaults independently
  • ➖ Increases binary churn and complicates ownership boundaries
2. Export measurements to OTLP immediately (instead of JSONL-first)
  • ➕ Immediate backend dashboards without additional ingestion work
  • ➕ Single ingestion path for traces and scores
  • ➖ Forces OTLP exporter configuration correctness for scoring to be useful
  • ➖ Increases surface area/risk for first ship (auth, batching, schema)
  • ➖ Conflicts with the stated tool-agnostic requirement if not done carefully
3. Make manifests fully declarative now (YAML asserts, no Go scorers)
  • ➕ Faster iteration for per-agent policy without code changes
  • ➕ Reduces need for many tiny scorer implementations
  • ➖ Requires designing a stable DSL/runtime early
  • ➖ Harder to express complex multi-span logic safely at first
  • ➖ Higher risk for correctness and backwards compatibility

Recommendation: Proceed with the PR’s approach: JSONL-first portable outputs + fail-open same-job scoring + agent-owned enablement manifests. This cleanly separates engine (fullsend) from policy (agents/local overrides), avoids prematurely committing to an OTLP score export schema, and provides an always-available artifact (eval-measurements.jsonl) for any downstream system.

Files changed (31) +1831 / -4

Enhancement (8) +818 / -0
evalmeasure.goAdd 'fullsend eval-measure' Cobra command +71/-0

Add 'fullsend eval-measure' Cobra command

• Implements the eval-measure CLI command, validates required flags, calls the evalmeasure engine, and prints pass/warn results without gating (exit 0 on failed scores).

internal/cli/evalmeasure.go

root.goRegister eval-measure subcommand on root CLI +1/-0

Register eval-measure subcommand on root CLI

• Wires the new eval-measure command into the CLI root so it is available in the released binary.

internal/cli/root.go

export_local.goImplement local JSONL export and idempotency ledger +83/-0

Implement local JSONL export and idempotency ledger

• Adds portable 'eval-measurements.jsonl' append logic and a simple ledger file used to ensure per-trace per-measurement idempotency.

internal/evalmeasure/export_local.go

fitness.goAdd EM-001 trace_fitness scorer implementation +190/-0

Add EM-001 trace_fitness scorer implementation

• Implements a trace fitness score over expected span tree and key attributes (identity, work item, model, usage, cost/tools/turns, exit presence). Produces a pass only when all checks pass; otherwise emits a detailed explanation.

internal/evalmeasure/fitness.go

parse.goParse run-telemetry.jsonl OTLP JSON lines into trace/span model +187/-0

Parse run-telemetry.jsonl OTLP JSON lines into trace/span model

• Adds a minimal OTLP TracesData JSON parser that merges spans by trace ID and extracts scalar attributes into a portable in-memory representation, with increased scanner buffer limits.

internal/evalmeasure/parse.go

registry.goLoad measurement manifest YAML and dispatch scorers +81/-0

Load measurement manifest YAML and dispatch scorers

• Introduces Registry/MeasurementSpec types, validation rules, and scorer dispatch. Only runs measurements when the trace agent matches the manifest agent; unknown scorers are skipped for forward compatibility.

internal/evalmeasure/registry.go

run.goOrchestrate parse → score → append JSONL with ledger idempotency +58/-0

Orchestrate parse → score → append JSONL with ledger idempotency

• Implements the main engine function to parse telemetry, load the registry, score matching traces, append results to JSONL, and then record ledger entries (persist-first ordering).

internal/evalmeasure/run.go

types.goDefine portable Span/Trace/EvaluationResult types and helpers +147/-0

Define portable Span/Trace/EvaluationResult types and helpers

• Introduces core data model for parsed spans and emitted measurement results, plus helpers for typed attribute access, durations, span lookup, and agent identity extraction.

internal/evalmeasure/types.go

Tests (13) +627 / -0
evalmeasure_test.goAdd CLI tests for eval-measure scoring and flag requirements +56/-0

Add CLI tests for eval-measure scoring and flag requirements

• Adds tests that run the command against fixtures and assert eval-measurements.jsonl output, verifies subcommand registration, and checks missing-required-flag behavior.

internal/cli/evalmeasure_test.go

export_local_test.goTest local export and ledger behavior +67/-0

Test local export and ledger behavior

• Covers empty writes, parent directory creation, ledger miss/hit semantics, and ledger file creation.

internal/evalmeasure/export_local_test.go

parse_test.goTest telemetry parsing, merging, and error handling +54/-0

Test telemetry parsing, merging, and error handling

• Adds tests for successful parsing, merging split traces across lines, invalid JSON line errors, and missing file behavior.

internal/evalmeasure/parse_test.go

registry_test.goTest manifest loading validation and error cases +86/-0

Test manifest loading validation and error cases

• Covers valid manifests and multiple failure cases (missing agent/id/scorer, invalid version, illegal characters, invalid YAML, missing file).

internal/evalmeasure/registry_test.go

run_test.goTest end-to-end scoring, idempotency, and cancellation +96/-0

Test end-to-end scoring, idempotency, and cancellation

• Adds integration-style tests for scoring via registry, idempotency across runs, persistence ordering (append before ledger), and error/cancelled-context behavior.

internal/evalmeasure/run_test.go

score_test.goTest trace_fitness pass/fail scenarios and scorer dispatch behavior +72/-0

Test trace_fitness pass/fail scenarios and scorer dispatch behavior

• Validates complete pass output, failure when required attributes are missing, work-item sentinel handling, agent mismatch behavior, and skipping unknown scorers.

internal/evalmeasure/score_test.go

README.mdDocument evalmeasure test fixtures purpose and ownership +6/-0

Document evalmeasure test fixtures purpose and ownership

• Explains that testdata JSONL and sample registry are synthetic fixtures and that production manifests live in the agents repository.

internal/evalmeasure/testdata/README.md

complete.jsonlAdd complete OTLP trace fixture for passing fitness +1/-0

Add complete OTLP trace fixture for passing fitness

• Provides a single-line OTLP JSON trace fixture with expected spans/attributes used by parser and scoring tests.

internal/evalmeasure/testdata/complete.jsonl

missing-cost.jsonlAdd OTLP fixture missing cost attribute for negative coverage +1/-0

Add OTLP fixture missing cost attribute for negative coverage

• Adds a variant fixture omitting cost fields to ensure trace_fitness fails appropriately.

internal/evalmeasure/testdata/missing-cost.jsonl

review-unknown-workitem.jsonlAdd OTLP fixture with unknown work item sentinel +1/-0

Add OTLP fixture with unknown work item sentinel

• Adds a fixture representing a review run where work_item_id is "unknown" to exercise work item fitness logic.

internal/evalmeasure/testdata/review-unknown-workitem.jsonl

sample-registry.yamlAdd sample measurement manifest fixture +5/-0

Add sample measurement manifest fixture

• Adds a minimal registry YAML enabling em-001/trace_fitness for triage, used by unit and CLI tests.

internal/evalmeasure/testdata/sample-registry.yaml

split.jsonlAdd split-line OTLP trace fixture for merge behavior +2/-0

Add split-line OTLP trace fixture for merge behavior

• Adds a multi-line fixture with the same trace split across lines to validate trace merging logic.

internal/evalmeasure/testdata/split.jsonl

types_test.goTest attribute coercion, duration math, and trace helpers +180/-0

Test attribute coercion, duration math, and trace helpers

• Adds unit tests for AttrString/AttrInt/AttrFloat conversions, DurationSeconds, span lookup helpers, and agent identity extraction priority.

internal/evalmeasure/types_test.go

Documentation (9) +340 / -4
config.tsExpose Eval Measurements guide in docs navigation +1/-0

Expose Eval Measurements guide in docs navigation

• Adds an "Eval Measurements" entry to the infrastructure guides sidebar for discoverability.

docs/.vitepress/config.ts

0050-distributed-tracing-instrumentation.mdCross-link distributed tracing ADR to eval measurements ADR +8/-0

Cross-link distributed tracing ADR to eval measurements ADR

• Records the follow-on decision that online trace scoring writes eval-measurements.jsonl and (planned) reuses OTLP configuration for remote export.

docs/ADRs/0050-distributed-tracing-instrumentation.md

0087-eval-measurements-online-trace-scoring.mdAdd ADR 0087: online trace scoring and tool-agnostic export +114/-0

Add ADR 0087: online trace scoring and tool-agnostic export

• Introduces an accepted ADR defining eval measurements, fail-open same-job scoring, ownership split (fullsend engine vs agents manifests), and per-measurement versioning (id@version). Establishes EM-001 trace_fitness as the first scorer and documents planned OTLP export alignment with ADR 0050.

docs/ADRs/0087-eval-measurements-online-trace-scoring.md

architecture.mdDocument eval measurements as an observability artifact +4/-0

Document eval measurements as an observability artifact

• Adds eval measurements to the observability architecture section, including the planned OTLP export direction and linking to ADR 0087 and the new guide.

docs/architecture.md

glossary.mdClarify terminology for eval measurements vs eval scenarios +4/-4

Clarify terminology for eval measurements vs eval scenarios

• Refines glossary definitions to distinguish online/trend measurements on wild traces from curated functional eval fixtures, with direct links to ADR 0087/0051 and the guide.

docs/glossary.md

README.mdList Eval Measurements guide under infrastructure guides +1/-0

List Eval Measurements guide under infrastructure guides

• Adds a new entry pointing operators to the eval measurements guide.

docs/guides/README.md

cli-internals.mdDocument new eval-measure CLI subcommand and flags +4/-0

Document new eval-measure CLI subcommand and flags

• Adds 'fullsend eval-measure' to the CLI internals reference, including telemetry, registry, and output directory flags.

docs/guides/dev/cli-internals.md

distributed-tracing.mdAdd section describing eval measurements alongside tracing +12/-0

Add section describing eval measurements alongside tracing

• Documents that eval-measure runs after each managed run, produces eval-measurements.jsonl, and will reuse OTEL_EXPORTER_OTLP_* for portable remote export when implemented.

docs/guides/infrastructure/distributed-tracing.md

eval-measurements.mdAdd operator guide for eval measurements, manifests, and CLI usage +192/-0

Add operator guide for eval measurements, manifests, and CLI usage

• Introduces a full guide covering prerequisites, artifacts, ownership boundaries, manifest resolution (local override vs agents@v0), and usage of 'fullsend eval-measure'. Includes a future-facing declarative manifest sketch and clarifies fail-open semantics.

docs/guides/infrastructure/eval-measurements.md

Other (1) +46 / -0
action.ymlAdd fail-open eval-measure post-step with manifest resolution +46/-0

Add fail-open eval-measure post-step with manifest resolution

• Adds a GitHub Actions step that locates run-telemetry.jsonl, resolves an agent measurement manifest (local FULLSEND_DIR override or agents@v0 fetch), and runs 'fullsend eval-measure'. The step is 'continue-on-error' so measurement failures never fail the agent job.

action.yml

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

Site preview

Preview: https://6cfb80c2-site.fullsend-ai.workers.dev

Commit: cc5d4e4a83c4a24fe8187eb5303465f580d47372

@ascerra
ascerra marked this pull request as draft August 10, 2026 11:50
@qodo-code-review

qodo-code-review Bot commented Aug 10, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Ledger suppresses failed writes ✓ Resolved 🐞 Bug ☼ Reliability
Description
MeasureAndExport calls RecordScored before AppendMeasurements, so if appending
eval-measurements.jsonl fails after the ledger write succeeds, the measurement is permanently
marked as done and will be skipped on future runs. This can silently lose measurement data and
prevent recovery without manual ledger repair.
Code

internal/evalmeasure/run.go[R44-48]

+			if err := RecordScored(ledgerPath, r.TraceID, r.Name, r.Version); err != nil {
+				return all, fmt.Errorf("record scored: %w", err)
+			}
+			if err := AppendMeasurements(measPath, []EvaluationResult{r}); err != nil {
+				return all, fmt.Errorf("append measurements: %w", err)
Relevance

●●● Strong

Clear reliability bug: ledger should not mark done before successful write; likely fixed.

PR-#1682

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The code checks AlreadyScored against the ledger, then records the ledger entry and only afterward
writes the JSONL. Any write failure after recording the ledger will cause permanent suppression on
subsequent runs.

internal/evalmeasure/run.go[33-49]
internal/evalmeasure/export_local.go[47-65]

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 idempotency ledger entry is recorded before the measurement is appended to `eval-measurements.jsonl`. If the append fails (e.g., ENOSPC/EIO/permission), the ledger still indicates success, preventing future re-writes.

## Issue Context
Idempotency should reflect successful persistence of the measurement record.

## Fix Focus Areas
- internal/evalmeasure/run.go[33-49]

## Suggested fix
Swap the order so `AppendMeasurements(...)` happens first, then `RecordScored(...)` only after append succeeds. Optionally, batch appends and then record ledger entries after the batch is flushed.

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


2. Guides README not updated ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
This PR adds a new guide under docs/guides/ but does not update docs/guides/README.md to index
it. This makes the guides index incomplete.
Code

docs/guides/infrastructure/eval-measurements.md[1]

+# Eval Measurements
Relevance

●●● Strong

Updating guides index/README when adding a new guide has clear accepted precedent.

PR-#5778

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires updating docs/guides/README.md when adding any new guide under
docs/guides/. The PR adds eval-measurements.md as a new guide but does not include a
corresponding README index update in the diff.

docs/guides/infrastructure/eval-measurements.md[1-1]
Skill: writing-user-docs

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

## Issue description
A new guide file was added under `docs/guides/`, but the guides index file `docs/guides/README.md` was not updated to include it.

## Issue Context
The index is the primary entry point for discovering guides; missing entries cause documentation drift.

## Fix Focus Areas
- docs/guides/infrastructure/eval-measurements.md[1-1]
- docs/guides/README.md[1-200]

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


3. Broken curl header args ✓ Resolved 🐞 Bug ≡ Correctness
Description
The Eval measurements step builds curl args via `${GH_TOKEN:+-H "Authorization: Bearer
${GH_TOKEN}"}`, but quotes produced by parameter expansion are not shell syntax and get passed
literally/word-split, so curl can fail whenever GH_TOKEN is non-empty and the remote manifest
fetch is needed. This causes eval measurement scoring to skip even when the agents manifest exists
upstream.
Code

action.yml[R500-503]

+          URL="https://raw.githubusercontent.com/fullsend-ai/agents/v0/eval/measurements/${AGENT}.yaml"
+          TMP="$(mktemp)"
+          if curl -fsSL ${GH_TOKEN:+-H "Authorization: Bearer ${GH_TOKEN}"} -o "${TMP}" "${URL}"; then
+            REGISTRY="${TMP}"
Relevance

●●● Strong

Shell quoting/arg-splitting robustness issues are routinely fixed; low-risk correctness fix.

PR-#2106

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR-added curl invocation relies on quotes embedded in a parameter expansion; since those quotes
are not interpreted as quoting, the -H value is split/broken and curl can fail, preventing remote
manifest retrieval.

action.yml[494-503]

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

## Issue description
`action.yml` uses bash parameter expansion to conditionally add a `curl -H "Authorization: Bearer …"` header. Because bash does not re-parse quotes introduced via expansion, the header is word-split into multiple argv entries and curl can fail when `GH_TOKEN` is set.

## Issue Context
This breaks the remote fallback manifest download path, which is the primary path until local manifests are present.

## Fix Focus Areas
- action.yml[500-503]

## Suggested fix
Replace the expansion with a safe conditional (or an argv array), e.g.:

```bash
CURL_ARGS=( -fsSL -o "${TMP}" "${URL}" )
if [[ -n "${GH_TOKEN:-}" ]]; then
 CURL_ARGS=( -fsSL -H "Authorization: Bearer ${GH_TOKEN}" -o "${TMP}" "${URL}" )
fi
if curl "${CURL_ARGS[@]}"; then
 REGISTRY="${TMP}"
else
 ...
fi
```

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


View high (3)
4. Guide missing prerequisites section ✓ Resolved 📜 Skill insight ✧ Quality
Description
docs/guides/infrastructure/eval-measurements.md includes procedural CLI usage without a clearly
labeled Prerequisites section before the procedure. This violates the guide structure requirement.
Code

docs/guides/infrastructure/eval-measurements.md[R115-122]

+## CLI
+
+```bash
+fullsend eval-measure \
+  --telemetry path/to/run-telemetry.jsonl \
+  --registry path/to/agents/eval/measurements/review.yaml \
+  --out-dir path/to/output
+```
Relevance

●●● Strong

Docs guides commonly add explicit Prerequisites before procedures; precedent accepted.

PR-#2277
PR-#2663

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires each documentation guide to include a prerequisites section before procedural
steps. The ## CLI section introduces how to run fullsend eval-measure without any preceding `##
Prerequisites` section.

docs/guides/infrastructure/eval-measurements.md[115-122]
Skill: writing-user-docs

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 new guide contains procedural instructions (CLI invocation) but does not include a clearly labeled `## Prerequisites` section before those steps.

## Issue Context
Compliance requires prerequisites to appear before step 1 of any procedure in guides.

## Fix Focus Areas
- docs/guides/infrastructure/eval-measurements.md[115-122]

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


5. ADR 0087 multiple decisions ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
ADR 0087 records multiple distinct decisions (mechanism, ownership, persistence, remote export,
first scorer, versioning) inside one ADR. This violates the requirement that each ADR record exactly
one decision.
Code

docs/ADRs/0087-eval-measurements-online-trace-scoring.md[R65-68]

+Introduce **eval measurements**: deterministic scorers that read
+`run-telemetry.jsonl` after `fullsend run` in the **same** managed job
+(`fullsend eval-measure` in `action.yml`), **fail-open**.
+
Relevance

●● Moderate

ADR splitting/“one decision” enforcement is subjective; no close accepted/rejected precedent found.

PR-#2743

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires an ADR to contain exactly one decision. The Decision section in ADR 0087
introduces the core mechanism and then adds several additional decisions as separate bolded
sub-items, indicating multiple decisions in one ADR.

docs/ADRs/0087-eval-measurements-online-trace-scoring.md[65-75]
Skill: writing-adrs

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

## Issue description
`ADR 0087` includes multiple distinct decisions within the Decision section. Compliance requires each ADR to record exactly one decision.

## Issue Context
The current Decision section contains multiple bolded sub-decisions (e.g., Ownership, Persistence, Remote export, First scorer, Versioning).

## Fix Focus Areas
- docs/ADRs/0087-eval-measurements-online-trace-scoring.md[65-88]

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


6. Planned OTLP noted without callout ✓ Resolved 📜 Skill insight ≡ Correctness
Description
The docs describe a not-yet-implemented OTLP export path as "planned" without using the required `>
**Planned:**` callout format and without an issue link. This can mislead readers about current vs
future behavior.
Code

docs/architecture.md[290]

+- Eval measurements: fail-open same-job scoring of wild-run traces into `eval-measurements.jsonl` beside telemetry; portable remote score export is designed to use the same OTLP configuration as agent traces (MLflow Assessments adapter available now; OTLP path planned); backend-specific UI adapters are optional ([ADR 0087](ADRs/0087-eval-measurements-online-trace-scoring.md)). See [Eval Measurements](guides/infrastructure/eval-measurements.md).
Relevance

●● Moderate

Planned-feature callout/link rule seems inconsistently enforced; similar issue-link add was
rejected.

PR-#3903

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires any mention of planned features to use the > **Planned:** callout format
and include an issue link. The added architecture line explicitly says the OTLP path is "planned"
but provides neither the callout format nor an issue link.

docs/architecture.md[290-290]
Skill: writing-user-docs

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

## Issue description
Documentation mentions a planned/not-yet-implemented feature without the required `> **Planned:**` callout format and without linking to a tracking issue.

## Issue Context
The checklist requires planned features to be clearly marked and traceable to an issue.

## Fix Focus Areas
- docs/architecture.md[290-290]
- docs/guides/infrastructure/eval-measurements.md[145-149]
- docs/ADRs/0087-eval-measurements-online-trace-scoring.md[80-83]

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



Remediation recommended

7. Ledger keys on name 🐞 Bug ≡ Correctness ⭐ New
Description
MeasureAndExport uses r.Name (which can be overridden via manifest measurements[].name) as part
of the ledger key, so renaming a measurement without changing id@version causes the same trace to
be re-scored and appended again. This breaks idempotency and can duplicate eval-measurements.jsonl
rows for the same (trace_id, id@version).
Code

internal/evalmeasure/run.go[R40-41]

+			done, err := AlreadyScored(ledgerPath, r.TraceID, r.Name, r.Version)
+			if err != nil {
Relevance

●● Moderate

Seems like a real idempotency bug, but docs imply ledger intentionally keys on name; could be design
choice.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
MeasureAndExport passes r.Name into ledger lookups/writes, while registry allows overriding
Name independent of ID/version; since the ledger key includes the name string, cosmetic renames
change idempotency behavior and can create duplicates.

internal/evalmeasure/run.go[37-54]
internal/evalmeasure/export_local.go[43-45]
internal/evalmeasure/registry.go[59-75]

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 idempotency ledger key currently includes `EvaluationResult.Name`, but `Name` can be a cosmetic display override (`measurements[].name`). This means changing only the display name invalidates the ledger entry and re-appends the same measurement for the same trace and `id@version`.

## Issue Context
- `MeasurementSpec.Name` is documented/implemented as an optional display override.
- `MeasurementSpec.versionString()` already produces the stable `id@version` identity.
- The ledger should key on stable measurement identity (e.g., `trace_id` + `id@version`), not presentation name.

## Fix Focus Areas
- internal/evalmeasure/run.go[37-53]
- internal/evalmeasure/export_local.go[43-45]
- internal/evalmeasure/registry.go[55-64]
- internal/evalmeasure/types.go[27-38]

### Suggested approach
- Change ledger key schema to drop `evalName` from the key, e.g. `traceID + "|" + version`.
- Alternatively, add a stable `MeasurementID` field to `EvaluationResult` (or pass `m.ID` through scoring) and key the ledger on `(traceID, measurementID@version)`.
- Update `AlreadyScored`/`RecordScored` call sites accordingly and adjust tests if needed.

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


8. Export errors not fail-open ⊘ Outdated 🐞 Bug ☼ Reliability
Description
ExportMLflowAssessments returns raw errors from trackingURIFromEnv() instead of wrapping them in
ExportError, so the CLI won’t classify them as warnings and fullsend eval-measure can exit
non-zero on export-related misconfiguration. This violates the documented “export failures warn and
exit 0” behavior.
Code

internal/evalmeasure/export_mlflow.go[R67-70]

+	base, err := trackingURIFromEnv()
+	if err != nil {
+		return err
+	}
Relevance

●●● Strong

Matches repo pattern: export-related misconfig should warn/fail-open, not abort CLI.

PR-#1682
PR-#1573

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The MLflow exporter returns a non-ExportError on URI derivation failure, while the CLI only treats
*ExportError as a warning; therefore this export failure path becomes fatal to the CLI command.

internal/evalmeasure/export_mlflow.go[61-70]
internal/cli/evalmeasure.go[42-50]
internal/cli/evalmeasure.go[64-67]

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

## Issue description
`trackingURIFromEnv()` derivation/parsing failures are export-related but are returned as plain errors, bypassing the CLI’s `ExportError` downgrade path.

## Issue Context
The CLI checks `errors.As(err, *ExportError)` to decide whether to warn-and-exit-0 vs fail. Export-related errors should consistently be `ExportError`.

## Fix Focus Areas
- internal/evalmeasure/export_mlflow.go[65-70]
- internal/cli/evalmeasure.go[42-49]

## Suggested fix
Change:
```go
base, err := trackingURIFromEnv()
if err != nil { return err }
```
To:
```go
base, err := trackingURIFromEnv()
if err != nil { return &ExportError{Err: err} }
```
(or, if you want explicit-vs-derived semantics, only wrap errors on the derived path but still keep the CLI behavior consistent with docs).

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


9. OTLP/OTEL jargon undefined ✓ Resolved 📜 Skill insight ✧ Quality
Description
The new guide introduces jargon/acronyms (e.g., OTLP, OTEL_EXPORTER_OTLP_*) without an inline
definition or a glossary link on first use. This reduces readability for new users.
Code

docs/guides/infrastructure/eval-measurements.md[R19-22]

+  └─ always writes  output/**/run-telemetry.jsonl
+  └─ if OTEL_EXPORTER_OTLP_* set → live OTLP export of agent spans
+       (any compatible backend — ADR 0050)
+
Relevance

●●● Strong

Docs feedback to define terms/acronyms on first use has been accepted in similar guides.

PR-#5778

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires jargon to be defined on first use via a glossary link or inline definition.
The guide uses OTEL_EXPORTER_OTLP_*/OTLP terminology immediately in the architecture diagram
without defining it or linking to the glossary.

docs/guides/infrastructure/eval-measurements.md[19-22]
Skill: writing-user-docs

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 guide uses domain jargon/acronyms on first use without defining them inline or linking to `docs/glossary.md`.

## Issue Context
The checklist requires jargon to be defined on first use to keep guides accessible.

## Fix Focus Areas
- docs/guides/infrastructure/eval-measurements.md[19-28]

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


View medium (2)
10. ADR 0050 edit not called out ✗ Dismissed 📘 Rule violation § Compliance
Description
An already-accepted ADR (ADR 0050) was modified, but the PR description does not explicitly
mention ADR 0050 and summarize the change. This makes review and auditing of decision-history
edits harder.
Code

docs/ADRs/0050-distributed-tracing-instrumentation.md[R154-159]

+**2026-08-10 — Eval measurements ([ADR 0087](0087-eval-measurements-online-trace-scoring.md)):**
+online scoring of wild-run traces writes `eval-measurements.jsonl` beside
+telemetry; portable remote score export is designed to follow the same OTLP
+configuration as this ADR (MLflow Assessments adapter available now; OTLP
+path planned). Backend-specific UI adapters remain optional. Distinct from
+functional eval fixtures ([ADR 0051](0051-agent-eval-harness-for-test-infrastructure.md)).
Relevance

●● Moderate

They avoid silent edits to accepted ADRs, but no precedent for PR-description callout requirement.

PR-#5244

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires explicit PR-description callouts when an accepted ADR is edited. The diff
shows a substantive new annotation added to accepted ADR 0050, triggering the requirement.

Rule 1062059: Call out edits to accepted ADRs in PR descriptions
docs/ADRs/0050-distributed-tracing-instrumentation.md[154-159]

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

## Issue description
Accepted ADR `0050-distributed-tracing-instrumentation.md` was modified, but the PR description must explicitly call out the ADR identifier/filename and summarize what changed.

## Issue Context
The PR description currently discusses ADR 0087 but does not explicitly name ADR 0050.

## Fix Focus Areas
- docs/ADRs/0050-distributed-tracing-instrumentation.md[154-159]

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


11. ADR 0087 too many consequences ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
ADR 0087's Consequences section contains 6 bullets, exceeding the required 3–5 bullets. This
breaks the standard ADR format and reduces readability.
Code

docs/ADRs/0087-eval-measurements-online-trace-scoring.md[R106-113]

+## Consequences
+
+- Wild runs produce a reviewable score file beside telemetry with or without a
+  remote backend.
+- Orgs that already set OTEL for traces get a portable score export contract
+  without a second auth scheme; optional adapters may enrich one product UI.
+- Missing manifests skip cleanly; measure failure never fails the agent job.
+- Functional scenarios (gate) and eval measurements (trend) stay separate;
Relevance

●● Moderate

Consequence bullet-count is a format nit; no strong precedent they enforce this strictly.

PR-#2582

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires 3–5 consequence bullets. The ADR includes six separate consequence bullets,
violating the requirement.

docs/ADRs/0087-eval-measurements-online-trace-scoring.md[106-119]
Skill: writing-adrs

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 ADR Consequences section must be 3–5 one-sentence bullet points, but this ADR has more.

## Issue Context
The current Consequences section includes 6 bullets.

## Fix Focus Areas
- docs/ADRs/0087-eval-measurements-online-trace-scoring.md[106-119]

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



Informational

12. Temp manifest not cleaned ⊘ Outdated 🐞 Bug ☼ Reliability ⭐ New
Description
The GitHub Action downloads the upstream manifest into a mktemp file but never deletes it on the
success path (or on failures after the download succeeds). This leaves temporary files behind on
self-hosted or reused runners.
Code

action.yml[R439-445]

+          TMP="$(mktemp)"
+          CURL_ARGS=( -fsSL -o "${TMP}" )
+          if [[ -n "${GH_TOKEN:-}" ]]; then
+            CURL_ARGS+=( -H "Authorization: Bearer ${GH_TOKEN}" )
+          fi
+          if curl "${CURL_ARGS[@]}" "${URL}"; then
+            REGISTRY="${TMP}"
Relevance

●●● Strong

Team often accepts adding EXIT traps after mktemp to prevent temp-file leaks under set -euo
pipefail.

PR-#4864
PR-#5442
PR-#2196

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The step creates TMP and assigns it to REGISTRY on successful curl, but only rm -f occurs in
the curl failure branch; there is no trap or post-command cleanup.

action.yml[423-450]
action.yml[453-456]
PR-#4864

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

## Issue description
`action.yml` creates a temporary file for the downloaded manifest (`TMP="$(mktemp)"`) but only removes it on the curl-failure branch. On success (and on later errors), the temp file is not removed.

## Issue Context
This is low-impact on ephemeral GitHub-hosted runners, but can accumulate on self-hosted runners or any environment that reuses workspaces.

## Fix Focus Areas
- action.yml[438-456]

### Suggested approach
- Immediately after `TMP="$(mktemp)"`, add a trap to delete it:
 - `trap 'rm -f -- "${TMP}"' EXIT`
- Ensure the trap is only set in the branch where `TMP` is created, so it doesn’t interfere with `LOCAL_REG` usage.

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


13. ADR 0087 context too long ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
ADR 0087's Context section exceeds the required 1–3 short paragraphs and includes extended bullet
lists. This makes the ADR harder to scan and pushes problem-details into the ADR instead of linking
out.
Code

docs/ADRs/0087-eval-measurements-online-trace-scoring.md[R23-31]

+## Context
+
+Agent runs already emit OpenTelemetry traces as `run-telemetry.jsonl`, with
+optional live OTLP export when `OTEL_EXPORTER_OTLP_*` is set
+([ADR 0050](0050-distributed-tracing-instrumentation.md)). Separately,
+[ADR 0051](0051-agent-eval-harness-for-test-infrastructure.md) owns the
+**functional** eval harness: curated fixtures / scenarios in
+`fullsend-ai/agents` `eval/<agent>/` that gate agent PRs. Those fixtures do
+not score wild production runs.
Relevance

● Weak

Nearly identical “Context must be 1–3 paragraphs” compliance ask was explicitly rejected.

PR-#2582

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires the ADR Context section to be 1–3 short paragraphs. The added ADR includes
multiple paragraphs and a multi-item bullet list under Context, exceeding the constraint.

docs/ADRs/0087-eval-measurements-online-trace-scoring.md[23-52]
Skill: writing-adrs

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 ADR Context section is longer than allowed (must be 1–3 short paragraphs) and includes additional long-form content.

## Issue Context
The Context section contains multiple paragraphs plus an extended "Adjacent telemetry proposals" list.

## Fix Focus Areas
- docs/ADRs/0087-eval-measurements-online-trace-scoring.md[23-53]

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


14. ADR 0087 exceeds 100 lines ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
ADR 0087 is 119 lines in this PR, exceeding the 100-line maximum (excluding frontmatter). This
suggests the ADR is too long and should be shortened or split.
Code

docs/ADRs/0087-eval-measurements-online-trace-scoring.md[R116-119]

+- Retro can recommend a **manifest scorer** or a **scenario fixture** — not
+  substitutes.
+- Richer telemetry (Level 3 / Status fixes) expands what scorers *can* assert;
+  it does not replace this same-job path.
Relevance

● Weak

ADR ≤100 lines enforcement was previously rejected when requested to shrink an ADR.

PR-#2582

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist sets a maximum of 100 lines of ADR content. The added ADR file in the diff is 119
lines long, exceeding that limit.

docs/ADRs/0087-eval-measurements-online-trace-scoring.md[1-119]
Skill: writing-adrs

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 ADR content exceeds the 100-line limit, indicating excessive scope or repeated context.

## Issue Context
The file added in this PR spans 119 lines; the checklist caps ADRs at 100 lines of content (excluding frontmatter).

## Fix Focus Areas
- docs/ADRs/0087-eval-measurements-online-trace-scoring.md[13-119]

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


View low (1)
15. eval-measurements.md wrong guide path 📜 Skill insight ⌂ Architecture
Description
A new guide was added under docs/guides/infrastructure/, but guides must live under either
docs/guides/admin/ or docs/guides/user/. This breaks the required guide directory convention and
makes guide organization inconsistent.
Code

docs/guides/infrastructure/eval-measurements.md[1]

+# Eval Measurements
Relevance

● Weak

Guide taxonomy violations (dev/infrastructure vs admin/user) have precedents being rejected.

PR-#5454

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist mandates that every file under docs/guides/ be located in either admin/ or
user/. This PR adds a guide in docs/guides/infrastructure/, violating that placement
requirement.

docs/guides/infrastructure/eval-measurements.md[1-1]
Skill: writing-user-docs

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

## Issue description
A new documentation guide was added at `docs/guides/infrastructure/eval-measurements.md`, but compliance requires guides to be placed under `docs/guides/admin/` or `docs/guides/user/`.

## Issue Context
This PR also links to the guide from VitePress config and other docs, so moving the file requires updating those links.

## Fix Focus Areas
- docs/guides/infrastructure/eval-measurements.md[1-1]
- docs/.vitepress/config.ts[262-262]
- docs/architecture.md[290-290]
- docs/guides/infrastructure/distributed-tracing.md[246-255]

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


Grey Divider

Context sources
✅ Compliance rules (platform): 54 rules

Grey Divider

Tip of the day
💡 Did you know, you can copy the agent prompt from any finding and feed it to your IDE agent

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread docs/guides/infrastructure/eval-measurements.md
Comment thread docs/guides/infrastructure/eval-measurements.md
Comment thread docs/architecture.md Outdated
Comment thread docs/guides/infrastructure/eval-measurements.md Outdated
Comment thread docs/ADRs/0050-distributed-tracing-instrumentation.md Outdated
Comment thread docs/ADRs/0087-eval-measurements-online-trace-scoring.md Outdated
Comment thread docs/ADRs/0087-eval-measurements-online-trace-scoring.md
Comment thread action.yml Outdated
Comment thread internal/evalmeasure/run.go Outdated
Comment thread internal/evalmeasure/export_mlflow.go Outdated
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review

Findings

Medium

  • [naming-consistency] internal/cli/evalmeasure.go — The eval-measure command has both --out-dir and --output-dir flags with different semantics (one for eval-measurements.jsonl placement, one for CI output base). The codebase uses --output-dir consistently elsewhere (fullsend run). Having near-identical flag names is confusing for users.
    Remediation: Rename --out-dir to something more descriptive that avoids collision, such as --measurements-dir or --score-dir.

  • [missing-authorization] — This PR introduces a significant new feature (new CLI command, new Go package, new ADR, GHA/GitLab CI wiring, documentation) but has no directly linked issue. The PR body references Track agents@v0 release cut for eval measurement manifests #6384 for tracking the agents@v0 release cut, but that tracks a subsequent release step, not the eval measurements feature itself.
    Remediation: Link or create an issue that covers the eval measurements feature.

Low

  • [edge-case] internal/evalmeasure/find.go:47hostRunDirPattern regex ^agent-(.+)-([0-9]+)-([0-9]+)$ uses greedy .+ for the agent name capture. For a hypothetical all-digit agent name (e.g., 123), directory agent-123-456-789 would parse as agent=123-456 due to greedy backtracking. All current stock agents use alphabetic names, so this is latent.

  • [cross-package-coupling] internal/evalmeasure/fitness.go:11UnknownSentinel is defined in the scoring package but its meaning originates in the CLI layer (resolveWorkItemID). The dependency direction is inverted, though both packages are under internal/ and cli already imports evalmeasure for other types.

  • [missing-doc-comment] internal/evalmeasure/export_local.go:13 — Exported constants MeasurementsFile and LedgerFile lack doc comments, while the sibling PlatformTelemetryFile in find.go has one.

  • [api-shape] internal/evalmeasure/run.go:23MeasureFile wraps MeasureAndExport but silently discards ParseStats. Only used in tests; consider removing or surfacing stats.

  • [exported-api-surface] internal/evalmeasure/export_local.go:18AppendMeasurements, AlreadyScored, and RecordScored are exported but only called within the evalmeasure package. Since the package is under internal/, this has no external impact but could be unexported for API hygiene.

  • [test-naming] internal/cli/evalmeasure_test.go — Helper writeTwoTraceTelemetry has the same name (different signature) as a helper in internal/evalmeasure/run_test.go. Valid Go but confusing when grepping.

  • [error-handling] internal/cli/evalmeasure.go:108evalMeasureFetchContext workspace fallback path: if UserCacheDir() and MkdirAll both fail, subsequent fetch operations will silently skip. This is correct fail-open behavior but the warn could note that manifest resolution will be skipped.

  • [race-condition] internal/evalmeasure/run.go:54 — The idempotency flow (AlreadyScoredAppendMeasurementsRecordScored) is not atomic. Concurrent processes could produce duplicate JSONL rows. Acknowledged in test comments; CI runs once per job.

Info

  • [provenance-warning] — Prior review context discarded: provenance validation failed (unverifiable-wrong-app). This review treats all findings as first-time assessments.
Previous run

Review

Findings

Low

  • [data-quality-on-partial-failure] internal/evalmeasure/run.go:65 — If AppendMeasurements succeeds but RecordScored fails, the measurement is persisted to eval-measurements.jsonl but the ledger is not updated. On retry, the same measurement is appended again (duplicate line). Documented in TestMeasureFile_AppendBeforeLedger; consumers can deduplicate on (trace_id, name, version).

  • [ledger-key-injection] internal/evalmeasure/export_local.go:51 — The ledger key format is traceID|evalName|version. Registry validation rejects pipe and newline in evalName fields, but traceID comes from parsed OTLP data and is not validated for pipe or newline characters. A malformed traceID containing | could cause a false-positive match in AlreadyScored, skipping a legitimate measurement. In practice, OTLP trace IDs are 32-character hex strings, and FindPlatformTelemetry ensures only host-written (top-of-runDir) files are scored.

  • [edge-case] internal/evalmeasure/find.go:37FindPlatformTelemetry returns at most one telemetry path when searching child runDirs (the newest by modification time). If an outputDir legitimately contains telemetry from multiple agent runDirs for different agents in the same job, only the newest is scored. This is documented and intentional.

  • [naming-inconsistency] internal/cli/evalmeasure.go — The CLI flag --out-dir uses a different naming convention than --output-dir. Both are defined on the same command with subtly different semantics (measurements output directory vs CI output base), creating potential confusion.

  • [duplicate-constant] internal/evalmeasure/find.go:13PlatformTelemetryFile duplicates telemetry.TelemetryFile. A test (TestPlatformTelemetryFileMatchesRecorder) guards the invariant, but importing the canonical constant would be cleaner.

  • [stale-example] docs/guides/dev/tracing.md:132 — Lists gen_ai.system as an agent span end attribute without noting the OTel GenAI semconv v1.37 deprecation in favor of gen_ai.provider.name. The PR updates the infrastructure tracing reference to show both names, but this dev guide only mentions the old name.

Previous run (2)

Review

Findings

Low

  • [data-quality-on-partial-failure] internal/evalmeasure/run.go:65 — If AppendMeasurements succeeds but RecordScored fails, the measurement is persisted to eval-measurements.jsonl but the ledger is not updated. On retry, the same measurement is appended again (duplicate line). Documented in TestMeasureFile_AppendBeforeLedger; consumers can deduplicate on (trace_id, name, version).

  • [ledger-key-injection] internal/evalmeasure/export_local.go:51 — The ledger key format is traceID|evalName|version. Registry validation rejects pipe and newline in evalName fields, but traceID comes from parsed OTLP data and is not validated for pipe or newline characters. A malformed traceID containing | could cause a false-positive match in AlreadyScored, skipping a legitimate measurement. In practice, OTLP trace IDs are 32-character hex strings, and FindPlatformTelemetry ensures only host-written (top-of-runDir) files are scored.

  • [edge-case] internal/evalmeasure/find.go:37FindPlatformTelemetry returns at most one telemetry path when searching child runDirs (the newest by modification time). If an outputDir legitimately contains telemetry from multiple agent runDirs for different agents in the same job, only the newest is scored. This is documented and intentional.

  • [naming-inconsistency] internal/cli/evalmeasure.go — The CLI flag --out-dir uses a different naming convention than --output-dir. Both are defined on the same command with subtly different semantics (measurements output directory vs CI output base), creating potential confusion.

  • [cross-package-sentinel] internal/evalmeasure/fitness.goUnknownSentinel is exported from internal/evalmeasure and imported by internal/cli/run.go for resolveWorkItemID(). The sentinel is a property of the run telemetry contract, not specific to eval measurements. Consider moving to a shared location (e.g. internal/telemetry).

  • [duplicate-constant] internal/evalmeasure/find.goPlatformTelemetryFile duplicates telemetry.TelemetryFile. A test (TestPlatformTelemetryFileMatchesRecorder) guards the invariant, but importing the canonical constant would be cleaner.

  • [stale-example] docs/guides/dev/tracing.md:132 — Lists gen_ai.system as an agent span end attribute without noting the OTel GenAI semconv v1.37 deprecation in favor of gen_ai.provider.name. The PR updates the infrastructure tracing reference to show both names, but this dev guide only mentions the old name.

Previous run (3)

Review

Findings

Low

  • [data-quality-on-partial-failure] internal/evalmeasure/run.go:65 — If AppendMeasurements succeeds but RecordScored fails, the measurement is persisted to eval-measurements.jsonl but the ledger is not updated. On retry, the same measurement is appended again (duplicate line). Documented in TestMeasureFile_AppendBeforeLedger; consumers can deduplicate on (trace_id, name, version).

  • [ledger-key-injection] internal/evalmeasure/export_local.go:51 — The ledger key format is traceID|evalName|version. Registry validation rejects pipe and newline in evalName fields, but traceID comes from parsed OTLP data and is not validated for pipe or newline characters. A malformed traceID containing | could cause a false-positive match in AlreadyScored, skipping a legitimate measurement. In practice, OTLP trace IDs are 32-character hex strings, and FindPlatformTelemetry ensures only host-written (top-of-runDir) files are scored.

  • [edge-case] internal/evalmeasure/find.go:37FindPlatformTelemetry returns at most one telemetry path when searching child runDirs (the newest by modification time). If an outputDir legitimately contains telemetry from multiple agent runDirs for different agents in the same job, only the newest is scored. This is documented and intentional.

  • [naming-inconsistency] internal/cli/evalmeasure.go — The CLI flag --out-dir uses a different naming convention than --output-dir. Both are defined on the same command with subtly different semantics (measurements output directory vs CI output base), creating potential confusion.

  • [cross-package-sentinel] internal/evalmeasure/fitness.goUnknownSentinel is exported from internal/evalmeasure and imported by internal/cli/run.go for resolveWorkItemID(). The sentinel is a property of the run telemetry contract, not specific to eval measurements. Consider moving to a shared location (e.g. internal/telemetry).

  • [duplicate-constant] internal/evalmeasure/find.goPlatformTelemetryFile duplicates telemetry.TelemetryFile. A test (TestPlatformTelemetryFileMatchesRecorder) guards the invariant, but importing the canonical constant would be cleaner.

  • [stale-example] docs/guides/dev/tracing.md:132 — Lists gen_ai.system as an agent span end attribute without noting the OTel GenAI semconv v1.37 deprecation in favor of gen_ai.provider.name. The PR updates the infrastructure tracing reference to show both names, but this dev guide only mentions the old name.

Previous run (4)

Review

Findings

Low

  • [data-quality-on-partial-failure] internal/evalmeasure/run.go:46 — If AppendMeasurements succeeds but RecordScored fails, the measurement is persisted to eval-measurements.jsonl but the ledger is not updated. On retry, the same measurement is appended again (duplicate line). Documented in TestMeasureFile_AppendBeforeLedger; consumers can deduplicate on (trace_id, name, version).

  • [ledger-key-injection] internal/evalmeasure/export_local.go:51 — The ledger key format is traceID|evalName|version. Registry validation rejects pipe and newline in evalName fields, but traceID comes from parsed OTLP data and is not validated for pipe or newline characters. A malformed traceID containing | could cause a false-positive match in AlreadyScored, skipping a legitimate measurement. In practice, OTLP trace IDs are 32-character hex strings, and FindPlatformTelemetry ensures only host-written (top-of-runDir) files are scored.

  • [edge-case] internal/evalmeasure/find.go:37FindPlatformTelemetry returns at most one telemetry path when searching child runDirs (the newest by modification time). If an outputDir legitimately contains telemetry from multiple agent runDirs for different agents in the same job, only the newest is scored. This is documented and intentional.

  • [naming-inconsistency] internal/cli/evalmeasure.go:643 — The CLI flag --out-dir uses a different naming convention than --output-dir. Both are defined on the same command with subtly different semantics (measurements output directory vs CI output base), creating potential confusion.

Previous run (5)

Review

Findings

Low

  • [data-quality-on-partial-failure] internal/evalmeasure/run.go:46 — If AppendMeasurements succeeds but RecordScored fails, the measurement is persisted to eval-measurements.jsonl but the ledger is not updated. On retry, the same measurement is appended again (duplicate line). Documented in TestMeasureFile_AppendBeforeLedger; consumers can deduplicate on (trace_id, name, version).

  • [non-determinism] action.yml:426 — The find command uses -print -quit to locate run-telemetry.jsonl, which returns the first match without guaranteeing order. In practice, fullsend run produces a single telemetry file, so this is unlikely to matter.

  • [ledger-key-injection] internal/evalmeasure/export_local.go:51 — The ledger key format is traceID|evalName|version. The registry validation rejects pipe and newline characters in ID, scorer, and name fields, but traceID comes from parsed OTLP data and is not validated for pipe or newline characters. A malformed traceID containing | could cause a false-positive match in AlreadyScored, skipping a legitimate measurement. In practice, OTLP trace IDs are 32-character hex strings, so this is unlikely outside adversarial input.

  • [GHA-workflow-command-injection] action.yml:444 — The printf statement uses %q for ${AGENT} (good), but %s for ${LOCAL_REG} and ${URL}, both of which contain the unsanitized ${AGENT} value. All known callers hardcode simple identifiers, keeping practical risk low.

  • [path-traversal] action.yml:433 — The AGENT input is interpolated into a filesystem path (${FULLSEND_DIR}/eval/measurements/${AGENT}.yaml) and a GitHub raw-content URL without path-component sanitization. LoadRegistry's strict schema validation limits exploitability to denial-of-service.

Previous run (6)

Review

Findings

Low

  • [data-quality-on-partial-failure] internal/evalmeasure/run.go:46 — If AppendMeasurements succeeds but RecordScored fails, the measurement is persisted to eval-measurements.jsonl but the ledger is not updated. On retry, the same measurement is appended again (duplicate line). Documented in TestMeasureFile_AppendBeforeLedger; consumers can deduplicate on (trace_id, name, version).

  • [non-determinism] action.yml:426 — The find command uses -print -quit to locate run-telemetry.jsonl, which returns the first match without guaranteeing order. In practice, fullsend run produces a single telemetry file, so this is unlikely to matter.

  • [GHA-workflow-command-injection] action.yml:444 — The printf statement uses %q for ${AGENT} (good), but %s for ${LOCAL_REG} and ${URL}, both of which contain the unsanitized ${AGENT} value. All known callers hardcode simple identifiers, keeping practical risk low.

  • [path-traversal] action.yml:433 — The AGENT input is interpolated into a filesystem path (${FULLSEND_DIR}/eval/measurements/${AGENT}.yaml) and a GitHub raw-content URL without path-component sanitization. LoadRegistry's strict schema validation limits exploitability to denial-of-service.

Previous run (7)

Review

Findings

High

  • [logic-error] action.yml:532 — The PR appends a new "Upload fullsend artifacts" step at the end of the composite action, but the original upload-artifact step at line 412 is left in place. Both use artifact name fullsend-${{ inputs.agent }} and path ${{ github.workspace }}/output. The existing upload runs BEFORE fullsend eval-measure, so eval-measurements.jsonl is never included in the artifact. The second upload will fail with a duplicate artifact name error (upload-artifact v7 default overwrite: false), and since it lacks continue-on-error: true, the failure affects job status.
    Remediation: Move the eval-measure step to run before the existing upload step at line 412 and remove the duplicate upload step at the end.

Low

  • [data-quality-on-partial-failure] internal/evalmeasure/run.go:46 — If AppendMeasurements succeeds but RecordScored fails, the measurement is persisted to eval-measurements.jsonl but the ledger is not updated. On retry, the same measurement is appended again (duplicate line). For a local JSONL artifact this is low-severity since consumers can deduplicate on (trace_id, name, version).

  • [GHA-workflow-command-injection] action.yml:521 — The echo statement interpolates ${AGENT} directly into stdout. While inputs.agent is workflow-author-controlled (not event-payload-sourced) and all known callers hardcode simple identifiers, the composite action is a public contract where external consumers could theoretically wire untrusted data.

  • [unused-parameter-idiom] internal/evalmeasure/run.go:18MeasureAndExport accepts context.Context but immediately discards it with _ = ctx. The parameter is reserved for future OTLP export per the doc comment.


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 (8)

Review

Findings

Low

  • [data quality on partial failure] internal/evalmeasure/run.go:48 — If AppendMeasurements succeeds but RecordScored fails, the measurement is persisted to eval-measurements.jsonl but the ledger is not updated. On retry, the same measurement is appended again (duplicate line). For a local JSONL artifact this is low-severity since consumers can deduplicate on (trace_id, name, version).

  • [error handling gap] internal/evalmeasure/run.go:37 — When the scoring loop encounters a write error, MeasureAndExport returns partial results alongside the error. The CLI handler discards results on error, so measurements already written to disk are not printed to stdout.

  • [test adequacy] internal/evalmeasure/run_test.go — No test covers partial-failure scenarios where AppendMeasurements or RecordScored fails mid-batch.

  • [unused parameter idiom] internal/evalmeasure/run.go:20MeasureAndExport accepts context.Context but immediately discards it with _ = ctx. The parameter is reserved for future OTLP export per the doc comment.

  • [cross-repo-schema] internal/evalmeasure/registry.go:27 — The measurement manifest YAML schema is introduced without a formal schema version field. ADR 0087 designs per-measurement versioning (id@version), and the YAML parser silently ignores unknown keys, so adding new optional fields later will not break existing binaries.

  • [cross-repo-compatibility] action.yml — The eval measurements step hardcodes the agents-repo manifest path convention (eval/measurements/${AGENT}.yaml) and pins to v0. This is consistent with other agents-repo fallbacks in this codebase.

Previous run (9)

Review

Findings

Low

  • [missing-docs-for-new-public-symbol] docs/guides/dev/cli-internals.md:112 — The CLI command tree documents every top-level subcommand but does not include the new eval-measure subcommand or its flags (--telemetry, --registry, --out-dir). The tree is already stale (missing poll), and eval-measure has its own dedicated guide.

  • [missing-docs-for-new-public-symbol] docs/cli/README.md:24 — The "Additional commands" table does not include eval-measure. Other CI-internal commands (post-review, post-comment, reconcile-status, poll) are also absent, suggesting the table intentionally covers only user-facing commands.

  • [data quality on partial failure] internal/evalmeasure/run.go:44 — If AppendMeasurements succeeds but RecordScored fails, the measurement is persisted to eval-measurements.jsonl but the ledger is not updated. On retry, the same measurement is appended again (duplicate line). For a local JSONL artifact this is low-severity since consumers can deduplicate on (trace_id, name, version).

  • [error handling gap] internal/evalmeasure/run.go:33 — When the scoring loop encounters a write error, MeasureAndExport returns partial results alongside the error. The CLI handler discards results on error, so measurements already written to disk are not printed to stdout.

  • [test adequacy] internal/evalmeasure/run_test.go — No test covers partial-failure scenarios where AppendMeasurements or RecordScored fails mid-batch.

  • [path traversal] action.yml:478AGENT input is interpolated into file paths and URLs without sanitization. Risk is minimal: the step has continue-on-error: true, AGENT comes from trusted workflow inputs (not PR content), and the worst case is a failed file check or 404.

  • [cross-repo-schema] internal/evalmeasure/registry.go:27 — The measurement manifest YAML schema is introduced without a formal schema version field. ADR 0087 designs per-measurement versioning (id@version), and the YAML parser silently ignores unknown keys, so adding new optional fields later will not break existing binaries.

  • [cross-repo-compatibility] action.yml:35 — The eval measurements step hardcodes the agents-repo manifest path convention (eval/measurements/${AGENT}.yaml) and pins to v0. This is consistent with other agents-repo fallbacks in this codebase.

  • [unused parameter idiom] internal/evalmeasure/run.go:17MeasureAndExport accepts context.Context but immediately discards it with _ = ctx. The parameter is reserved for future OTLP export per the doc comment.

Previous run (10)

Review

Findings

Critical

  • [reusable-workflow-contract-break] .github/workflows/reusable-{code,fix,prioritize,retro,review,triage}.yml — The PR removes the entire with: block from the composite action step (uses: ./.defaults/) in all 6 reusable workflows, deleting the required agent input along with version, fullsend-dir, run-url, status-repo, status-number, and mint-url. The composite action declares agent as required: true with no default in action.yml. Without the with: block, inputs.agent resolves to an empty string, causing fullsend run "" to execute with no agent name. Every consumer repo calling these reusable workflows will break. This change is also unrelated to the eval measurements feature — it is not mentioned in the PR title, body, or ADR 0087.
    Remediation: Restore the with: blocks on all 6 reusable workflow files. If the removal is intentional (e.g., moving input passing to a different mechanism), it should be landed as a separately scoped PR with an explanation of the alternative mechanism.

High

  • [protected-path] .github/workflows/reusable-{code,fix,prioritize,retro,review,triage}.yml — Six protected workflow files under .github/ are modified. The PR has no linked issue and the description does not explain the with: block removals from these governance and infrastructure files. Human approval is always required for changes to protected paths.

Medium

  • [data loss on partial failure] internal/evalmeasure/run.go:42RecordScored (ledger write) is called before AppendMeasurements (measurement write). If AppendMeasurements fails after RecordScored succeeds, the ledger marks the trace as already-scored but the measurement was never persisted. On retry, AlreadyScored returns true and the measurement is permanently lost. The code was rewritten since the prior review but the ordering bug persists.
    Remediation: Swap the order — call AppendMeasurements before RecordScored so the ledger entry is only written after the measurement is safely persisted. A duplicate measurement line on retry is recoverable; a lost measurement is not.

  • [missing-docs-for-new-public-symbol] docs/cli/README.md:24 — The "Additional commands" table lists fullsend run, lock, and scan but does not include the new fullsend eval-measure subcommand.
    Remediation: Add a row for fullsend eval-measure with description pointing to the eval measurements guide.

  • [missing-docs-for-new-public-symbol] docs/guides/dev/cli-internals.md:112 — The CLI command tree documents every subcommand but does not include the new eval-measure subcommand or its flags (--telemetry, --registry, --out-dir). The Key Source Files table also omits internal/cli/evalmeasure.go and internal/evalmeasure/.
    Remediation: Add eval-measure with its flags to the CLI command tree and add the new source files to the Key Source Files table.

Low

  • [missing-authorization] — No linked issue for a non-trivial change (1,700+ new lines of Go code, new CLI subcommand, new ADR, GHA post-step wiring, six reusable workflow modifications). ADR 0087 provides architectural authorization but an issue link would improve traceability.

  • [error handling gap] internal/evalmeasure/run.go:33 — When the scoring loop encounters a ledger or write error, MeasureAndExport returns partial results alongside the error. The CLI handler treats any non-nil error as fatal, so partial results already written to disk are not printed to stdout. The user sees an error but no indication of which measurements succeeded.

  • [test adequacy] internal/evalmeasure/run_test.go — No test covers partial-failure scenarios where RecordScored or AppendMeasurements fails mid-batch. The idempotency test covers the happy-path retry but not the failure-then-retry path where the ordering of RecordScored vs AppendMeasurements matters.

  • [naming specificity] internal/cli/evalmeasure.go:57 — The helper printResults is more generic than existing CLI helpers in this package (printResolvedDeps, printStatusTable, printSkipGuidance). Consider renaming to printMeasurementResults to match the established print<Domain> pattern.


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 (11)

Review

Findings

High

  • [protected-path] .github/workflows/reusable-{code,fix,prioritize,retro,review,triage}.yml — Six protected workflow files under .github/ are modified. The PR has no linked issue; human approval is always required for changes to governance and infrastructure files. Affected protected files: .github/workflows/reusable-code.yml, .github/workflows/reusable-fix.yml, .github/workflows/reusable-prioritize.yml, .github/workflows/reusable-retro.yml, .github/workflows/reusable-review.yml, .github/workflows/reusable-triage.yml.

Medium

  • [data loss on partial failure] internal/evalmeasure/run.go:42RecordScored (ledger write) is called before AppendMeasurements (measurement write). If AppendMeasurements fails after RecordScored succeeds, the ledger marks the trace as already-scored but the measurement was never persisted. On retry, AlreadyScored returns true and the measurement is permanently lost.
    Remediation: Swap the order — call AppendMeasurements before RecordScored so the ledger entry is only written after the measurement is safely persisted.

  • [missing consumer for new feature] .github/workflows/reusable-dispatch.ymlreusable-dispatch.yml (per-repo dispatch workflow) was not updated to forward MLFLOW_TRACKING_URI, MLFLOW_TRACKING_USERNAME, or MLFLOW_TRACKING_PASSWORD. It also does not declare MLFLOW_TRACKING_PASSWORD in its secrets section. All six reusable-{stage}.yml files were updated but this file was missed. Per-repo installations using reusable-dispatch.yml will silently skip MLflow Assessment export even when the org has the secrets configured. Per docs/contributing/workflow-contracts.md, both installation-mode chains must be updated when a reusable workflow adds a new secrets: entry.
    Remediation: Add MLFLOW_TRACKING_PASSWORD to the secrets section of reusable-dispatch.yml and forward MLFLOW_TRACKING_URI, MLFLOW_TRACKING_USERNAME, MLFLOW_TRACKING_PASSWORD in the env block of each action invocation.

  • [missing-docs-for-new-public-symbol] docs/cli/README.md:24 — The "Additional commands" table lists fullsend run, lock, and scan but does not include the new fullsend eval-measure subcommand.
    Remediation: Add a row for fullsend eval-measure with description pointing to the eval measurements guide.

  • [missing-docs-for-new-public-symbol] docs/guides/dev/cli-internals.md:112 — The CLI command tree documents every subcommand but does not include the new eval-measure subcommand or its flags (--telemetry, --registry, --out-dir).
    Remediation: Add an eval-measure entry to the CLI command tree.

Low

  • [missing-authorization] — No linked issue for a non-trivial change (1,700+ new lines of Go code, new CLI subcommand, new ADR, GHA post-step wiring, six reusable workflow modifications). ADR 0087 provides architectural authorization but an issue link would improve traceability.

  • [error handling gap] internal/evalmeasure/run.go:33 — On non-ExportError failures in the scoring loop, the CLI handler returns the error without printing partial results that were already written to disk.

  • [credential-scope] internal/evalmeasure/export_mlflow.go:97 — When MLFLOW_TRACKING_URI is unset, trackingURIFromEnv() derives the MLflow base URL from OTEL endpoints. If OTEL points to a non-MLflow backend, Basic Auth credentials would be sent to an unintended host (requires both a derived URI mismatch AND the password being set).

  • [comment consistency across workflows] .github/workflows/reusable-triage.yml:180 — The triage workflow's MLFLOW comment is 3 lines (includes "URI defaults from OTEL endpoint when unset") while the other five workflows use a 2-line comment.

  • [test seam pattern] internal/evalmeasure/export_mlflow.go:32 — The assessmentHTTPDo test seam var is annotated "Not safe for t.Parallel()" but there is no enforcement mechanism beyond the comment.

  • [test adequacy] internal/evalmeasure/run_test.go — No test covers partial-failure scenarios where RecordScored or AppendMeasurements fails mid-batch, which is particularly relevant given the ordering bug in the write path.

  • [missing-cross-reference] docs/guides/user/tracing-with-mlflow.md:97 — The MLflow tracing guide's "See also" section does not cross-reference the new eval measurements guide, which documents the MLFLOW_TRACKING_* env vars for the Assessments adapter.


Labels: PR adds new Go eval-measure package (internal/evalmeasure/), modifies CI workflows (.github/workflows/), and adds documentation (docs/guides/, docs/ADRs/)


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

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added type/feature New capability request component/ci CI pipelines and checks go Pull requests that update go code component/docs User-facing documentation labels Aug 10, 2026
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 10, 2026

Copy link
Copy Markdown

🤖 Review · ❌ Terminated · Started 1:34 PM UTC · Ended 1:54 PM UTC

Commit: 49f0d19 · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review

Copy link
Copy Markdown

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

Commit: 49f0d19 · View workflow run →

@ralphbean

Copy link
Copy Markdown
Member

Heads up — this looks adjacent to #5524 (ADR 0075, harness-snapshot.json). Both build on ADR 0050 and add a new file riding alongside run-telemetry.jsonl. ADR 0087 already lists #5947/#5944/#2423 as adjacent proposals — wonder if 0075 belongs there too?

ascerra added a commit that referenced this pull request Aug 11, 2026
- Fix data-loss bug: swap AppendMeasurements before RecordScored so ledger
  only marks scored after measurement is persisted
- Fix broken curl header args in action.yml using bash array instead of
  parameter expansion (word-split safe)
- Add Prerequisites section to eval-measurements guide
- Add OTEL/OTLP inline definitions on first use in guide
- Use > **Planned:** callout format for unimplemented OTLP export
- Update docs/guides/README.md with eval measurements entry
- Consolidate ADR 0087 Decision into single paragraph (was multiple
  sub-decisions); trim Consequences to 5 bullets
- ADR 0050 cross-reference uses Planned callout format
- Rename printResults → printMeasurementResults

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 11, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 1:04 AM UTC · Ended 1:14 AM UTC

Commit: 43a6ebd · View workflow run →

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 11, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 1:15 AM UTC · Completed 1:33 AM UTC

Commit: 4bf9b26 · View workflow run →

@fullsend-ai-review
fullsend-ai-review Bot dismissed stale reviews from themself August 11, 2026 01:33

Superseded by updated review

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 19, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 1:45 AM UTC · Completed 2:02 AM UTC

Commit: 1b87426 · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@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.

Review-only sweep (no approval, no change request) on the current head. Six findings posted inline: 2 HIGH on the GitLab scaffold job, 4 MEDIUM across find.go precedence, EM-001 skip classification, the guide's declarative example, and the ADR's activation dependency. Each was checked against the existing comment threads on this PR and is distinct from the issues already answered there.

case "${EVENT_TYPE:-}" in
issue_*)
export GITLAB_ISSUE_URL="${CI_SERVER_URL}/${CI_PROJECT_PATH}/-/issues/${STATUS_IID:-0}"
if [[ -n "${STATUS_IID:-}" && "${STATUS_IID}" != "0" ]]; 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.

[HIGH] Conditional GITLAB_ISSUE_URL export leaves the var truly unset, which strict harness env validation rejects before the sandbox starts

In-diff (verified against origin/main...origin/feat/eval-measurements): the unconditional export GITLAB_ISSUE_URL=".../-/issues/${STATUS_IID:-0}" (and the merge_requests equivalent) is replaced by a conditional export, so on the issue path when STATUS_IID is empty/"0", and on the MR path when both CI_MERGE_REQUEST_IID and STATUS_IID are empty/"0", GITLAB_ISSUE_URL is left truly unset rather than set to a bogus value.

That variable is consumed under fullsend's strict env gate. On fullsend-ai/agents@main, harness/triage.yaml:79 declares forge.gitlab.env.runner.ISSUE_URL: ${GITLAB_ISSUE_URL} (and :83 the sandbox equivalent); harness/code.yaml:120,125 does the same. internal/harness/harness.go:652-703 ValidateRunnerEnvWith documents and enforces "Variables set to an empty string are allowed; only truly unset variables produce an error", returning "%s: host variable %s is not set (referenced in %q)", and internal/cli/run.go:575 calls it before any expansion and before the sandbox is created (StepFail + validating env: ...).

Timing nuance: the current agents v0 tag (6bdcab69, 2026-08-10) has no gitlab: forge block and no env/gitlab/triage.env at all, so this does not detonate against today's v0 pin — it lands the moment the GitLab harness support on agents@main reaches a cut v0.x.y release (the same release cut this PR's measurement manifests are waiting on), or immediately for any consumer pinned to agents@main or carrying a local harness copied from it. This is the same lenient-to-strict env delivery regression class that broke agents#211.

The paired scaffold test only asserts the string shape ("${STATUS_IID}" != "0"); nothing asserts the harness env still validates.

Suggestion: Keep the intent (no invented .../issues/0) but export an empty value on every path instead of leaving the variable unset — e.g. initialise GITLAB_ISSUE_URL="" before the case, assign inside each branch only when the IID is real/non-zero, and export GITLAB_ISSUE_URL unconditionally after the esac. An empty string satisfies ValidateRunnerEnvWith, and it does not reintroduce greenwashing: resolveWorkItemID() (internal/cli/run.go:2416) does strings.TrimSpace(os.Getenv("GITLAB_ISSUE_URL")) and skips empty, so the run still records fullsend.work_item_id = "unknown" and EM-001's work_item check still fails. Add a scaffold assertion that export GITLAB_ISSUE_URL is present on every path.

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 8029af1.

Always initialize GITLAB_ISSUE_URL=\"\", set a real URL only when the IID is non-empty/non-zero, and export GITLAB_ISSUE_URL unconditionally after the esac. Empty still satisfies harness env validation and does not invent …/issues/0 for EM-001. Scaffold asserts GITLAB_ISSUE_URL=\"\" and export GITLAB_ISSUE_URL.

--fullsend-dir .fullsend \
--target-repo . \
--output-dir /tmp/fullsend-output \
--output-dir "${CI_PROJECT_DIR}/output" \

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] GitLab run output directory is now nested inside --target-repo, so host telemetry is uploaded into the sandbox and is committable by the agent

The PR moves GitLab's output from /tmp/fullsend-output to ${CI_PROJECT_DIR}/output (needed for the new artifacts: paths: [output/] block), but the same job passes --target-repo ., which resolves to $CI_PROJECT_DIR.

Verified ordering in internal/cli/run.go: step 3 creates runDir = <outputBase>/agent-<name>-<pid>-<ts> (run.go:903-910) and installs the file span exporter on it; step 8 then copies the target repo into the sandbox with sandbox.UploadDir(sandboxName, hostRepositoryDir, remoteRepositoryDir) (run.go:1260), an unfiltered tarball of everything under the repo root, tracked or not. agentWorkingDirExcludes (run.go:105-111) contains only .agentready/ and .fullsend-workspace/, so output/ is excluded neither from the tarball nor from the sandbox .git/info/exclude written by excludeAgentWorkingDirs (run.go:1302).

Net effect on GitLab only (GitHub Actions keeps output/ and the checkout as siblings under GITHUB_WORKSPACE): every span already flushed by the SimpleSpanProcessor file exporter (sandbox_create, pre-script) plus the runDir itself is handed to the sandboxed agent, and an agent doing git add -A can commit run telemetry into a consumer MR.

The guide's only mitigation is prose telling operators to gitignore output/, but internal/scaffold/fullsend-repo-gitlab/ ships no .gitignore at all (the tree contains only .fullsend/config.yaml, .gitlab-ci.yml and .gitlab/ci/*).

Suggestion: Make output/ a sibling of the checkout rather than a child of --target-repo while staying under $CI_PROJECT_DIR (e.g. --target-repo "${CI_PROJECT_DIR}/target-repo" with output at ${CI_PROJECT_DIR}/output), or add output/ to agentWorkingDirExcludes so it is excluded from both the sandbox tarball and the sandbox git index — and ship the mitigation with the scaffold (a .gitignore containing output/, plus a scaffold test asserting it) instead of only documenting it in the guide.

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 8029af1.

Added output/ to agentWorkingDirExcludes (sandbox .git/info/exclude) and exclude output/ from the UploadDir tarball so host telemetry is not uploaded or git-addable. Scaffold embeds a recommended .gitignore fragment with output/ (tested); Collect skips installing it as a root file so it cannot overwrite a consumer ignore list.

// It never walks deeper, so an agent-planted
// iteration-N/output/run-telemetry.jsonl is ignored.
func FindPlatformTelemetry(outputDir, agent string) ([]string, error) {
direct := filepath.Join(outputDir, PlatformTelemetryFile)

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] FindPlatformTelemetry's root-level short-circuit outranks the real agent runDir, newly reachable now that output/ lives in the repo

FindPlatformTelemetry stats <outputDir>/run-telemetry.jsonl first and returns it immediately (find.go:33-37), before the os.ReadDir branch that applies the agent-<name>-<pid>-<unix> pattern and the --agent filter (find.go:39-71). So a file literally named run-telemetry.jsonl at the base of --output-dir wins over the genuine runDir even when --agent is passed.

This is distinct from the already-answered threads on this file: the nested iteration-N/output/ copy case (fixed in c8438d83) and the sibling/leftover runDir case (fixed in 5a218846) both concern the ReadDir branch. Root-level precedence has not been raised on this PR.

It becomes reachable because of the paired GitLab change in this same PR: --output-dir is now ${CI_PROJECT_DIR}/output inside the target-repo working tree, and nothing excludes or gitignores output/, so an agent that commits output/run-telemetry.jsonl gets it restored by the CI checkout of a later pipeline — where it sits at exactly the path this short-circuit trusts, silently displacing the real run's telemetry for scoring.

Suggestion: Prefer the runDir-scoped file over a root-level file of the same name: skip the direct short-circuit when --agent is set and a matching child runDir exists (or gate the short-circuit on outputDir's own basename matching hostRunDirPattern). Add a test with a planted root-level run-telemetry.jsonl alongside a real agent-<name>-<pid>-<unix> runDir to lock the precedence in.

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 8029af1.

FindPlatformTelemetry prefers matching child runDirs over a root-level run-telemetry.jsonl. If a matching agent-<name>-… directory exists (even empty), the planted root file is ignored. Tests cover planted-root+runDir precedence and empty matching runDir.

// No agent span: run never reached an iteration (sandbox/provider/image
// failure). Exclude from pass/(pass+fail) so EM-001 trends measure the
// telemetry contract, not runner health.
if len(agents) == 0 {

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 scores a hard-killed run as fail rather than skip when agent spans flushed but the run span never ended

ScoreFitnessNamed excludes runner-health failures only through the len(agents) == 0 early return (fitness.go:53-74). The root run span's attributes (exit_code, gen_ai.request.model, fullsend.cost_usd, fullsend.num_turns, iterations) are set in a deferred closure at internal/cli/run.go:941-975 and the span is exported only when it ends. The file exporter is a SimpleSpanProcessor (internal/telemetry/telemetry.go:153), so agent spans hit run-telemetry.jsonl the instant they end — meaning a SIGKILL / OOM-kill / job timeout after at least one agent span produces a telemetry file with agent spans and no run span.

With hasRun false, run is the zero-value Span, so span_tree (fitness.go:84, requires hasRun), identity (identityOK reads run's fullsend.agent, :145-149), work_item (:161-164), operation (attrNonEmpty on run, :90) and exit all fail, while model/usage/cost_tools_turns can still pass off the agent spans — roughly 3/8, recorded as fail, not skip.

Both the action.yml step and the GitLab step are fail-open and run after the agent regardless of its exit status, so this hits exactly the jobs most likely to be killed, and the repo already treats hard-kill as a real scenario (the "Finalize orphaned status comment" step exists for it). The result is that runner health systematically depresses the EM-001 pass rate that ADR 0087 says measures the telemetry contract. This is the complement of the len(agents) == 0 case fixed in 5a218846, not the same case.

Suggestion: Treat "agent spans present but no root run span" the same way as len(agents) == 0: return LabelSkip with an explanation such as root run span missing; run terminated before flush — or require hasRun before scoring the 8-check contract at all.

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 8029af1.

When agent spans are present but the root run span is missing (hard kill / timeout before flush), EM-001 returns label: skip with explanation root run span missing; run terminated before flush — same exclusion class as no agent span.

checks:
- name: turn_token_ratio
assert: ratio_lte
numerator: gen_ai.usage.total_tokens

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] Guide's declarative-manifest example uses two attribute names that fullsend never emits

The "logic-as-config (future declarative engine)" YAML block documents the intended shape of a planned feature with numerator: gen_ai.usage.total_tokens and denominator: fullsend.turns (lines 129-130). Neither key exists:

  • gen_ai.usage.total_tokens is not an OTel GenAI semantic-convention attribute (the v1.37.0 GenAI registry defines input_tokens / output_tokens) and fullsend never emits it — internal/cli/run.go emits only gen_ai.usage.input_tokens, output_tokens, cache_creation.input_tokens, cache_read.input_tokens and conditionally reasoning_tokens (run.go:961-971, 2346-2354).
  • The emitted turns attribute is fullsend.num_turns (run.go:966), which is also what docs/guides/infrastructure/distributed-tracing.md documents and what this PR's own scorer reads via AttrFullsendNumTurns in internal/evalmeasure/fitness.go.

A not-yet-built config surface documented with invented keys is how wrong names get copied into the first real manifests written against agents#722.

Suggestion: Rewrite the example against attributes that are actually emitted — e.g. numerator: gen_ai.usage.output_tokens, denominator: fullsend.num_turns, or derive a total explicitly from input_tokens + output_tokens — and add a one-line note that the declarative block is illustrative and its key names are not yet a 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 8029af1.

Declarative example now uses emitted attrs gen_ai.usage.output_tokens and fullsend.num_turns, with an explicit note that the block is illustrative and not yet a contract.

vendor-specific score adapters in core. `fullsend` owns the parser, scorers,
CLI, and GHA step; `fullsend-ai/agents` owns per-agent measurement manifests
(`eval/measurements/<agent>.yaml`) that declare which scorers to enable.
Stock-agent defaults resolve from `agents@v0` at runtime; local files are for

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] Activation is a two-step dependency (merge and a v0 release cut) that nothing tracks, and no run has exercised the resolved-manifest path

ADR 0087:88 states stock-agent defaults "resolve from agents@v0 at runtime" and the PR body frames the companion fullsend-ai/agents#722 merge as the only remaining dependency ("Until those land on agents@v0, the measure step skips cleanly").

Verified upstream on 2026-08-19: eval/measurements/ does not exist on fullsend-ai/agents at main or at v0 (GET contents/eval/measurements → 404; git ls-tree -r origin/main → no match), and #722 is still open. v0 is a floating major tag re-pointed only when a v0.x.y release is cut — it currently resolves to 6bdcab69 (2026-08-10) while main is at 816b89be (2026-08-19), nine days stale. So merging #722 alone activates nothing; a release must additionally be cut and v0 re-pointed.

Consequently there is no end-to-end evidence in this PR that scoring produces rows on the shipped code path: manifest resolution returns a clean skip today, the action.yml and GitLab steps are guaranteed no-ops, the PR's own Testing checklist leaves that box unchecked, and the only e2e artifact offered (MLflow screenshots) covers the MLflow adapter that was removed in 67aee649 and is no longer in this diff. Nothing in CI would surface "measurements have been silently no-op for N weeks". This is distinct from the existing GetRef/token/rate-limit threads.

Suggestion: State the two-step dependency explicitly in ADR 0087 and the guide ("manifests must be merged and included in a v0.x.y release that re-points v0") and open/link a tracking issue for the release cut. Land at least one check that exercises the resolved-manifest path before merge — e.g. an e2e/behaviour case pointing --fullsend-dir at a fixture containing eval/measurements/<agent>.yaml and asserting eval-measurements.jsonl appears in the uploaded artifact — or mark the GHA/GitLab wiring explicitly provisional until the manifests are actually on v0.

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 8029af1.

ADR 0087 and the guide now state the two-step activation (merge manifests and cut a v0.x.y that re-points v0); managed wiring is marked provisional until then. Tracking: #6384. Added TestEvalMeasureCmd_LocalFullsendDirManifestProducesJSONL for the local FULLSEND_DIR resolved-manifest path.

@ascerra

ascerra commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Tracking the two-step agents@v0 activation (merge manifests and cut a release that re-points v0): #6384

Export empty GITLAB_ISSUE_URL on every GitLab path, keep host output/
out of the sandbox tarball and git exclude, prefer runDir telemetry
over planted roots, skip EM-001 when the run span never flushed, and
document two-step agents@v0 activation (#6384) plus planned Level 3
content scorers.

Signed-off-by: Adam Scerra <ascerra@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@ascerra
ascerra requested a review from waynesun09 August 19, 2026 18:59
@ascerra

ascerra commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 8029af1d addressing the six Aug-19 review threads (GITLAB_ISSUE_URL empty export, output/ sandbox exclude + gitignore fragment, FindPlatformTelemetry runDir precedence, EM-001 skip without run span, declarative attr names, two-step agents@v0 + #6384). Threads left unresolved for your re-check.

@fullsend-ai-review

fullsend-ai-review Bot commented Aug 19, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 7:01 PM UTC · Ended 7:03 PM UTC

Commit: 8029af1 · View workflow run →

Bring the branch current with main after the Aug-19 measurement review push.

Signed-off-by: Adam Scerra <ascerra@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 19, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 7:04 PM UTC · Ended 7:05 PM UTC

Commit: e3731af · View workflow run →

Pick up main commits landed after the previous sync.
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 19, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 7:07 PM UTC · Completed 7:24 PM UTC

Commit: cffdfc7 · View workflow run →

fullsend-ai-review[bot]

This comment was marked as outdated.

@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.

Review-only sweep on the current head (no approval, no change request) — 6 findings posted inline: 2 HIGH on the new output/ exclusion path (tar exclude anchoring differs between bsdtar and GNU tar; the exclusion is unconditional across forges and repos), and 4 MEDIUM on doc/implementation drift, the temp-dir fetch cache root, and the duplicated OTLP JSON schema. Each was checked against the existing threads on this PR; where a finding follows on from an already-answered thread, the inline comment says so.

Comment thread internal/sandbox/sandbox.go Outdated
continue
}
// Exclude the directory itself and its contents at the archive root.
tarArgs = append(tarArgs, "--exclude=./"+pattern, "--exclude=./"+pattern+"/*")

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] UploadDir exclude patterns are not directory-anchored on bsdtar — every nested output/ dir is silently dropped from the sandbox copy on macOS

The new variadic exclude support builds --exclude=./<pattern> and --exclude=./<pattern>/*, and the code comment claims it excludes "the directory itself and its contents at the archive root." That is only true for GNU tar. exec.Command("tar", ...) resolves to bsdtar on any macOS host (local fullsend run, and macos-* runners), where the match is non-anchored.

Verified empirically against a fixture tree containing output/tracked.txt, src/a.go, sub/output/keep.txt, keep-root.txt, using the exact argv the code builds:

  • bsdtar 3.5.3 / libarchive 3.7.4 (/usr/bin/tar on macOS): archive contains only ./, ./keep-root.txt, ./src/, ./src/a.go, ./sub/./sub/output/keep.txt is GONE.
  • GNU tar 1.35 (debian container, same fixture and flags): archive contains ./sub/output/, ./sub/output/keep.txt — only the root ./output is dropped, as the comment claims.

So on macOS, fullsend run now silently omits any directory named output at any depth (frontend/output/, docs/output/, testdata/output/, …) from the repo copy handed to the agent. .git IS uploaded in the same tarball (confirmed: the exclude list is the only filter, and the COPYFILE_DISABLE comment immediately below exists precisely because AppleDouble files corrupt .git after a round-trip), so those tracked files appear as deletions in the sandbox's git status. .git/info/exclude does not mask deletions of tracked paths, so an agent running git add -A / git commit -a commits the removal.

The new regression test TestUploadDir_ExcludesPatternsFromTarball (internal/sandbox/sandbox_test.go:676) only asserts that a root-level output/ is absent; it has no nested-directory case, so this platform divergence passes CI on both platforms and is untested. In-diff: verified against merge-base 17df6ebUploadDir gained the excludes ...string variadic and the --exclude loop in this PR.

Suggestion: Stop relying on tar's exclude-matching semantics, which differ across implementations. Enumerate the top-level entries of localPath with os.ReadDir, drop the excluded names, and pass the surviving entries as explicit ./<name> members instead of . — deterministic on bsdtar, GNU tar, and busybox alike. Add a regression case that creates sub/output/keep.txt and asserts it survives while root output/ is dropped. If --exclude is kept, the "at the archive root" comment must be corrected, since it is false for bsdtar (GNU's --anchored is not accepted by bsdtar).

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 8596989.

Stopped using tar --exclude (bsdtar matches output at any depth). UploadDir now enumerates top-level members and omits excluded basenames only, so nested sub/output/ survives. Nested exclude paths are rejected. Regression test asserts nested keep + top-level drop.

Comment thread internal/cli/run.go Outdated
copyStart := time.Now()
printer.StepStart("Copying project code into sandbox")
if err := sandbox.UploadDir(sandboxName, hostRepositoryDir, remoteRepositoryDir); err != nil {
if err := sandbox.UploadDir(sandboxName, hostRepositoryDir, remoteRepositoryDir, "output/"); err != nil {

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] output/ is excluded from the sandbox tarball and .git/info/exclude unconditionally, for every forge and every repo

sandbox.UploadDir(sandboxName, hostRepositoryDir, remoteRepositoryDir, "output/") hardcodes the literal "output/" with no relationship to where the run output actually lives, and agentWorkingDirExcludes (run.go:113) adds the same literal to the in-sandbox .git/info/exclude for every run.

The motivating problem is GitLab-only, and I verified the asymmetry at head: the GitLab scaffold passes --target-repo . with --output-dir "${CI_PROJECT_DIR}/output" (fullsend-agent.yml:390-393), so the run dir nests inside the checkout. On GitHub, action.yml:407-408 keeps them disjoint — --output-dir "${GITHUB_WORKSPACE}/output" while --target-repo defaults to ${GITHUB_WORKSPACE}/target-repo (action.yml:388) — so the exclusion there is pure collateral, as it is for any local run with --output-dir outside the repo.

For a consumer repo that versions a top-level output/ directory (build artifacts, generated docs, ML runs, test fixtures — a generic enough name that this is realistic), the tarball omits the tracked files while .git is still uploaded, so the sandbox working tree shows them as deleted. .git/info/exclude does not mask deletions of tracked paths, so git status reports D output/... and an agent doing git add -A / git commit -a commits the removal. Nothing in the diff scopes this to the forge or layout that needs it.

Note the existing answered thread at internal/scaffold/fullsend-repo-gitlab/.gitlab/ci/fullsend-agent.yml:393 introduced this fix; this finding is a critique of the fix's implementation and has no comment on internal/cli/run.go. In-diff: both the "output/" entry in agentWorkingDirExcludes and the UploadDir argument are added by this PR (verified against merge-base 17df6eb).

Suggestion: Derive the exclusion from the actual run layout instead of a hardcoded name: compute rel, err := filepath.Rel(hostRepositoryDir, outputBase) and only pass an exclude (and only append to .git/info/exclude) when err == nil && filepath.IsLocal(rel) — i.e. when the output base genuinely sits inside the target repo — using rel as the pattern. The existing entries (.agentready/, .fullsend-workspace/) are fullsend-reserved names; output/ is not, so it needs the path check. This fix alone does not resolve the bsdtar anchoring bug above; both are needed.

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 8596989.

Removed hardcoded output/ from agentWorkingDirExcludes. outputDirExcludeRel only excludes when --output-dir is a single-segment child of --target-repo (GitLab nested layout); sibling layouts (GHA) are unchanged. That relative name is passed to UploadDir and .git/info/exclude.

Comment thread docs/architecture.md Outdated

- What signals matter most — cost, latency, token usage, action logs, decision traces, or something else?
- ~~How do we balance detailed tracing (useful for debugging) with the volume of data agents will produce?~~ Decided in [ADR 0050](ADRs/0050-distributed-tracing-instrumentation.md): instrument all lifecycle steps comprehensively; volume is managed by backends not by suppressing data at the source.
- ~~How do we score wild agent traces for trends without a second export stack?~~ Decided in [ADR 0087](ADRs/0087-eval-measurements-online-trace-scoring.md): eval measurements write local JSONL beside telemetry; portable remote export uses the same OTLP config as traces (planned); local eval-measurements.jsonl is always written.

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] Living doc contradicts eval-measurements write behavior: "local eval-measurements.jsonl is always written"

The 'Open questions' section records the resolved answer as "...local eval-measurements.jsonl is always written." This contradicts ADR 0087, the eval-measurements guide, and the implementation.

Verified in internal/evalmeasure/export_local.go: AppendMeasurements opens the file with O_APPEND|O_CREATE only after an early if len(results) == 0 { return nil } — so with zero new rows the file is never created. Upstream of that, runEvalMeasure skips entirely when telemetry or the manifest is missing (resolveEvalMeasureRegistry returns "" on a 404, resolveEvalMeasureTelemetry returns no paths), and a fully-ledgered rerun produces no new results. So the file is absent, not empty, in every one of those cases — the opposite of what the current-truth doc tells operators.

Not covered by the existing docs/architecture.md:290 thread, which was about the > **Planned:** callout format for OTLP export and is already marked Resolved.

Suggestion: Reword line 314 to say the local JSONL is written only when new measurement rows are produced (including label: skip rows), matching ADR 0087 and the user guide, so operators reading the current-truth doc are not misled into expecting the file unconditionally.

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 8596989.

docs/architecture.md now matches ADR/guide/implementation: local eval-measurements.jsonl is written only when new measurement rows are produced (including label: skip); absent when telemetry/manifest missing, no traces match, or everything is already ledgered.

- name: turn_token_ratio
assert: ratio_lte
numerator: gen_ai.usage.output_tokens
denominator: fullsend.num_turns

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] Incomplete fix in 8029af1: the declarative example still names an attribute fullsend never emits on the span it scopes to

Follow-up on the answered thread at this file:129 ("declarative attr names", replied "Fixed in 8029af1"). The fix replaced two nonexistent names, but introduced a different instance of the same class of error, and the guide now makes a stronger claim than the example supports: "attribute names below match what fullsend emits today" (line 121-123).

The block scopes itself with where: span: agent (line 136-137) and then uses numerator: gen_ai.usage.output_tokens (line 139) and denominator: fullsend.num_turns (line 140). gen_ai.usage.output_tokens is indeed on agent spans, but fullsend.num_turns is not. Verified by grepping every emit site: it is set exactly once, at internal/cli/run.go:969, inside the runCount > 0 block of the root-span telemetry defer. agentSpanEndAttrs (internal/cli/run.go:2343-2360) emits iteration, exit_code, gen_ai.system, gen_ai.request.model, input/output/cache tokens, fullsend.cost_usd and fullsend.tool_calls — no num_turns.

This PR's own fixtures confirm the split: in internal/evalmeasure/testdata/complete.jsonl the run span carries fullsend.num_turns while the agent span carries only fullsend.cost_usd / fullsend.tool_calls. So the illustrative manifest, as written, would evaluate a missing attribute on the very span it selects.

Suggestion: Either change the denominator to an attribute that exists on agent spans (fullsend.tool_calls or iteration), or change where: to select the root run span so fullsend.num_turns is in scope. If the example is meant to stay purely illustrative, drop or soften the "match what fullsend emits today" claim so it is not read as a per-span 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 8596989.

Declarative example now uses where: span: run so gen_ai.usage.output_tokens and fullsend.num_turns are both on the scoped span. Softened the surrounding note to say attrs match the run span today and are not a contract.

Comment thread internal/cli/evalmeasure.go Outdated
func evalMeasureFetchContext(fullsendDir string, offline bool, printer *ui.Printer) (harness.ComposeOpts, forge.Client) {
workspace := fullsendDir
if workspace == "" {
workspace = os.TempDir()

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] eval-measure without --fullsend-dir uses os.TempDir() as the fetch cache root and audit-log location

evalMeasureFetchContext falls back to workspace = os.TempDir() when fullsendDir is empty (evalmeasure.go:198-201), and that value becomes both ComposeOpts.WorkspaceRoot and the base of AuditLogPath (filepath.Join(abs, ".fullsend-cache", "fetch-audit.jsonl")). fetch.CachePath builds <workspaceRoot>/.fullsend-cache/resources/sha256/<hash> (internal/fetch/cache.go:44-49, 162).

Verified reachable: --fullsend-dir defaults to "" (evalmeasure.go:79), and resolveEvalMeasureRegistry calls evalMeasureFetchContext(opts.fullsendDir, ...) on the remote-manifest path whenever no local override resolved (evalmeasure.go:172). So a plain fullsend eval-measure --agent X --telemetry <path> writes /tmp/.fullsend-cache/... and /tmp/.fullsend-cache/fetch-audit.jsonl.

/tmp is world-writable with the sticky bit, and this is a fixed, predictable path shared by every user on the host. On a shared runner or dev box the first invoking user creates the tree with their ownership and subsequent users' writes fail — and because the path is fail-open, the failure is silent. A local attacker can also pre-create /tmp/.fullsend-cache, or plant fetch-audit.jsonl as a symlink, to deny service or redirect the audit log. fullsend run never does this; it always has a real fullsend directory, and both managed workflows pass --fullsend-dir, which is why this is MEDIUM rather than HIGH — it bites direct CLI use only.

Not covered by the existing evalmeasure.go:204 / :208 threads, which I read in full: those concern the token warn-string wording and the --offline flag, not the workspace root.

Suggestion: Use a per-user location (os.UserCacheDir() plus a fullsend subdirectory) or a per-invocation os.MkdirTemp("", "fullsend-evalmeasure-*") removed on exit, instead of the shared os.TempDir() root. Alternatively require --fullsend-dir whenever the remote manifest fetch would be attempted, and skip cleanly otherwise.

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 8596989.

Empty --fullsend-dir now uses os.UserCacheDir()/fullsend/eval-measure (0700) instead of shared os.TempDir(), with a pid-scoped temp fallback if cache dir is unavailable. Test pins the UserCacheDir path under a temp HOME.

"os"
"strconv"
)

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] OTLP JSON structs are an unpinned second copy of the writer's types, with no test binding reader to writer

internal/evalmeasure/parse.go:11-40 redeclares otlpTracesData / otlpResourceSpans / otlpScopeSpans / otlpSpan / otlpStatus / otlpKeyValue as a private copy of the identically-named types in internal/telemetry/fileexporter.go:71-124. Nothing links them — verified they are two independent declarations in two packages, and that no test in internal/evalmeasure imports internal/telemetry or drives the real exporter (grep over internal/evalmeasure/*_test.go finds only hand-built run-telemetry.jsonl temp files).

The assumptions do hold against the current writer — hex traceId/spanId rather than base64, startTimeUnixNano/endTimeUnixNano as strings, intValue as a string, doubleValue as a number, status.code as an integer enum. The gap is that none of it is asserted anywhere: internal/evalmeasure/testdata/README.md states the fixtures are synthetic and for unit tests only, and every fixture is hand-authored.

If the exporter ever switches to protojson (which encodes enums as strings like "STATUS_CODE_ERROR" and bytes as base64), json.Unmarshal fails on the whole line, every line lands in stats.SkippedLines, and eval-measure fail-opens to "no traces" with no test failure — exactly the silent degradation EM-001 exists to catch. This PR already uses the guard-the-invariant-with-a-test pattern for the far smaller filename-constant coupling, so the schema coupling being unguarded is the outlier. Distinct from the answered parse.go:84/:85/:87 threads, which are all about fail-open handling of corrupt lines and spans.

Suggestion: Add a round-trip test in internal/evalmeasure that drives the real exporter (or a telemetry-package golden generated from it) through ParseTelemetryFile, asserting stats.SkippedLines == 0 plus the EM-001 attributes. Better still, export the OTLP JSON types from internal/telemetry (or a small shared package) so there is one definition instead of two that can drift. Span.StatusCode is parsed but read by no scorer — either drop it or cover it in that round-trip.

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 8596989.

Added TestParseTelemetryFile_RoundTripFromExporter: real telemetry.Setup file exporter → ParseTelemetryFile → EM-001 pass, with SkippedLines/SkippedSpans asserted zero. Shared OTLP types remain a follow-up; the round-trip gates encoding drift.

ascerra and others added 2 commits August 19, 2026 21:16
Enumerate UploadDir members instead of tar --exclude so nested output/
survives on bsdtar, scope host output exclusion to in-repo layouts,
fix architecture/declarative docs, use UserCacheDir for eval-measure
fetch cache, and round-trip the file exporter through ParseTelemetryFile.

Signed-off-by: Adam Scerra <ascerra@redhat.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the branch current after the evening measurement review fix.
@ascerra
ascerra requested a review from waynesun09 August 20, 2026 01:19
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 20, 2026

Copy link
Copy Markdown

🤖 Review · ❌ Terminated · Started 1:20 AM UTC · Ended 1:41 AM UTC

Commit: cc5d4e4 · View workflow run →

@ascerra

ascerra commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 85969892 (tip cc5d4e4a after merge of main) addressing the six Aug-19 evening threads (bsdtar-safe UploadDir members, layout-scoped output exclude, architecture/declarative docs, UserCacheDir fetch cache, exporter→parse round-trip). Threads left unresolved for re-check.

@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.

}

direct := filepath.Join(outputDir, PlatformTelemetryFile)
if st, err := os.Stat(direct); err == nil && !st.IsDir() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] edge-case

hostRunDirPattern regex uses greedy .+ for agent name capture. For a hypothetical all-digit agent name, the directory name would parse incorrectly due to greedy backtracking.

ScorerFitness = "trace_fitness"
LabelPass = "pass"
LabelFail = "fail"
LabelSkip = "skip"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] cross-package-coupling

UnknownSentinel is defined in the scoring package but its meaning originates in the CLI layer (resolveWorkItemID). The dependency direction is inverted.

)

const (
MeasurementsFile = "eval-measurements.jsonl"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] missing-doc-comment

Exported constants MeasurementsFile and LedgerFile lack doc comments, while the sibling PlatformTelemetryFile in find.go has one.

// MeasureFile parses telemetry, scores with the manifest, and writes local
// eval-measurements.jsonl. Idempotent per ledger.
func MeasureFile(telemetryPath, registryPath, outDir string) ([]EvaluationResult, error) {
r, _, err := MeasureAndExport(context.Background(), telemetryPath, registryPath, outDir)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] api-shape

MeasureFile wraps MeasureAndExport but silently discards ParseStats. Only used in tests.

)

// AppendMeasurements writes one NDJSON EvaluationResult per line.
func AppendMeasurements(path string, results []EvaluationResult) (retErr error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] exported-api-surface

AppendMeasurements, AlreadyScored, and RecordScored are exported but only called within the evalmeasure package.

if err != nil {
return nil, false, err
}
if registry == "" {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] error-handling

evalMeasureFetchContext workspace fallback: if UserCacheDir() and MkdirAll both fail, subsequent fetch operations will silently skip. Correct fail-open behavior.

ledgerPath := filepath.Join(outDir, LedgerFile)
measPath := filepath.Join(outDir, MeasurementsFile)
var all []EvaluationResult
hook, _ := ctx.Value(persistHookKey{}).(func())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] race-condition

Idempotency flow (AlreadyScored -> AppendMeasurements -> RecordScored) is not atomic. Concurrent processes could produce duplicate JSONL rows. Acknowledged in test comments; CI runs once per job.

@fullsend-ai-review fullsend-ai-review Bot added requires-manual-review Review requires human judgment and removed ready-for-merge All reviewers approved — ready to merge labels Aug 20, 2026
@fullsend-ai-review

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 1:20 AM UTC · Completed 1:41 AM UTC

Commit: cc5d4e4 · View workflow run →

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

Labels

component/ci CI pipelines and checks component/docs User-facing documentation go Pull requests that update go code requires-manual-review Review requires human judgment type/feature New capability request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants