Skip to content

feat(harness): add a max_cost_usd budget cap for agent runs - #6588

Open
guyoron1 wants to merge 10 commits into
fullsend-ai:mainfrom
guyoron1:feat/review-cost-surfacing
Open

feat(harness): add a max_cost_usd budget cap for agent runs#6588
guyoron1 wants to merge 10 commits into
fullsend-ai:mainfrom
guyoron1:feat/review-cost-surfacing

Conversation

@guyoron1

@guyoron1 guyoron1 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Heyaa : )

Kept noticing that max_iterations bounds how many times an agent retries, but nothing bounds what those retries cost — a runaway run had no ceiling.

max_cost_usd: an optional per-harness cap (0 = unlimited, inherited by composition exactly like timeout_minutes), checked against aggregate cost after each iteration.

What it can and cannot do:

  • Claude Code reports total_cost_usd only in the final result event of a completed iteration, so the cap can't interrupt in-flight work — it stops the loop from starting another iteration. Documented where the field is configured, not implied away.
  • The check sits at the top of the loop, so the two mid-iteration continue paths can't buy an iteration the budget already refused.
  • The halt sets over_budget in metrics.json — it records why the run stopped retrying, not success (with a validation_loop it commonly accompanies a non-nil error, and the field doc says so). The same flag is now also set as a fullsend.over_budget attribute on the run's root span, so a budget-halted run is visible in traces, not just metrics.json.

metrics.json also gains a top-level duration_seconds — total wall-clock across all iterations, summed the same way cost is. Independent of the cap; nothing else surfaces run duration today.

TestRunAgent_DoesNotCancelTheRunContext pins a constraint that's genuinely easy to reintroduce: cancelling the run's own ctx rebinds the variable the status-comment defer closes over — defers run LIFO, so every run would report "cancelled". The test reads the source because the failure is invisible at the package boundary and .codecov.yml excludes run.go from patch coverage (#2831). Negative-checked: reintroducing the exact pattern fails it.

Tests: validation (negative/zero/positive), composition inheritance and child override, exceedsCostBudget boundaries, duration accumulation, the over_budget marker (metrics.json and now the root span). go build, go vet, internal/cli and internal/harness pass in full.

Docs note: the cost-cap decision is recorded as new ADR 0097 with the field-level contract in docs/normative/harness-budget/v1; accepted ADR 0024 gets only a one-line cross-reference annotation pointing at it — kept rather than reverted, since it's the pointer a future ADR-0024 reader needs to find where the cap actually lives.

No tracking issue — this came out of ad-hoc auditing (see PR body above), not a filed bug. Happy to open one if you want a paper trail.

@github-actions

Copy link
Copy Markdown

E2E tests did not run

E2E tests run automatically for org/repo members and collaborators on pull requests.

For other contributors, a maintainer must add the ok-to-test label after the latest push.

See E2E testing guide for details.

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown

Site preview

Preview: https://e41be1ba-site.fullsend-ai.workers.dev

Commit: 5972b8fd7ed483324118a2f2fec3ad1d88cd4659

@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@qodo-code-review

qodo-code-review Bot commented Sep 1, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Add max_cost_usd budget caps for harness runs

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Adds inherited max_cost_usd caps to prevent retries after aggregate cost exceeds budget.
• Records over-budget termination and cumulative agent duration in metrics.json.
• Documents budget semantics and tests validation, inheritance, boundaries, metrics, and context
 safety.
Diagram

graph TD
  A["Harness YAML"] --> B["Compose Config"] --> C["Validate Budget"] --> D["Run Iteration"] --> E["Aggregate Metrics"] --> F{"Over Budget?"}
  F -- "Within budget" --> D
  F -- "Exceeded" --> G["Stop Retries"] --> H["metrics.json"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Reserve estimated iteration cost
  • ➕ Could prevent a new iteration before the accumulated cost actually crosses the cap.
  • ➕ May reduce overshoot when individual iterations are expensive.
  • ➖ Requires unreliable cost prediction before token usage is known.
  • ➖ Could reject useful iterations despite remaining budget.
2. Provider-level spending limit
  • ➕ Can provide broader protection across concurrent runs and harnesses.
  • ➕ May enforce limits independently of runner correctness.
  • ➖ Is not scoped to one harness run or validation loop.
  • ➖ May report usage too late and cannot explain retry termination in run metrics.

Recommendation: Keep the completed-iteration aggregate check because it uses authoritative Claude Code cost data and cleanly gates retries without cancelling the run context. A provider-level spending limit can complement this as defense in depth, while predictive reservation is not reliable enough to replace the implemented behavior.

Files changed (9) +200 / -8

Enhancement (3) +59 / -6
run.goEnforce aggregate cost budgets in the agent retry loop +52/-6

Enforce aggregate cost budgets in the agent retry loop

• Tracks cumulative agent duration and marks runs that exceed 'max_cost_usd'. The retry loop checks the marker before starting more work and halts without cancelling the shared run context.

internal/cli/run.go

compose.goInherit cost budgets during harness composition +3/-0

Inherit cost budgets during harness composition

• Copies the base harness 'max_cost_usd' value when the child leaves it at the unlimited default.

internal/harness/compose.go

harness.goDefine and validate max_cost_usd +4/-0

Define and validate max_cost_usd

• Adds the YAML-backed cost-cap field to 'Harness'. Validation rejects negative values while treating zero as unlimited.

internal/harness/harness.go

Tests (4) +127 / -2
run_test.goTest cost-budget metrics, boundaries, and context safety +65/-0

Test cost-budget metrics, boundaries, and context safety

• Covers 'over_budget' JSON persistence and budget comparison boundaries. Adds a source-level regression guard preventing body-scope rebinding and cancellation of the run context.

internal/cli/run_test.go

telemetry_run_test.goVerify duration aggregation across iterations +4/-2

Verify duration aggregation across iterations

• Updates aggregate metric calls with iteration durations and asserts that cumulative duration is summed correctly.

internal/cli/telemetry_run_test.go

compose_test.goTest inherited and overridden cost budgets +41/-0

Test inherited and overridden cost budgets

• Verifies that composed harnesses inherit a base cost cap while preserving an explicit child override.

internal/harness/compose_test.go

harness_test.goTest max_cost_usd validation rules +17/-0

Test max_cost_usd validation rules

• Covers negative, zero, and positive cost-cap values to establish the supported configuration range.

internal/harness/harness_test.go

Documentation (2) +14 / -0
0024-harness-definitions.mdDocument harness cost-cap configuration and limitations +9/-0

Document harness cost-cap configuration and limitations

• Adds 'max_cost_usd' to the harness definition example. Explains unlimited behavior, retry-level enforcement, in-flight limitations, and the 'over_budget' metric.

docs/ADRs/0024-harness-definitions.md

harness-reference.mdAdd max_cost_usd to the harness reference +5/-0

Add max_cost_usd to the harness reference

• Documents the new field, its default, aggregation across validation retries, and why it cannot interrupt an active iteration.

docs/reference/harness-reference.md

@qodo-code-review

qodo-code-review Bot commented Sep 1, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. Inherited cap cannot disable ✓ Resolved 🐞 Bug ≡ Correctness
Description
A child harness specifying the documented unlimited value max_cost_usd: 0 still inherits a
positive base cap because composition treats zero as an absent value. Consequently, users cannot
disable an inherited budget without changing or abandoning the base harness.
Code

internal/harness/compose.go[R587-589]

+	if child.MaxCostUSD == 0 {
+		child.MaxCostUSD = base.MaxCostUSD
+	}
Relevance

●●● Strong

Explicit zero semantics and inheritance edge cases receive acceptance when configuration composition
changes behavior.

PR-#1325
PR-#2582

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
MaxCostUSD is a plain float field, so YAML omission and explicit zero have the same decoded value;
the new merge branch then replaces either zero with the base cap. Runtime enforcement separately
defines zero as unlimited, proving that an explicit child zero should have meaningful behavior but
cannot survive composition.

internal/harness/harness.go[339-343]
internal/harness/compose.go[584-589]
internal/cli/run.go[3147-3150]
docs/reference/harness-reference.md[143-146]

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 child harness cannot override an inherited positive `max_cost_usd` with the documented unlimited value `0`, because the merge logic cannot distinguish an omitted field from an explicit zero.

## Issue Context
`MaxCostUSD` is a plain `float64`, and composition currently copies the base value whenever the child value equals zero. Introduce presence-aware decoding or another representation that preserves the distinction between absent and explicitly configured zero, while keeping runtime zero semantics as unlimited.

## Fix Focus Areas
- internal/harness/harness.go[339-343]
- internal/harness/compose.go[584-589]
- internal/harness/compose_test.go[89-128]

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


2. Budget marker can be lost ✓ Resolved 🐞 Bug ☼ Reliability
Description
After setting OverBudget, runAgent can return on a target-repository extraction failure before
reaching the only normal metrics.json write. For harnesses without a validation loop, the promised
over_budget marker and the completed iteration's cost and duration metrics are therefore absent.
Code

internal/cli/run.go[R2043-2044]

+		if !aggMetrics.OverBudget && exceedsCostBudget(aggMetrics.TotalCostUSD, h.MaxCostUSD) {
+			aggMetrics.OverBudget = true
Relevance

●●● Strong

PR #1682 established that metrics accumulated before early returns must still be persisted.

PR-#1682

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Metrics are aggregated and OverBudget is set before repository extraction. Without a validation
loop, failures clearing or downloading the repository return immediately at lines 2150 or 2173,
while the normal metrics write is not reached until lines 2235-2237 and no deferred writer exists;
past PR #1682 documents the same lost-metrics-on-early-return failure pattern in this function.

internal/cli/run.go[2015-2045]
internal/cli/run.go[2144-2173]
internal/cli/run.go[2235-2237]
PR-#1682

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

## Issue description
Metrics accumulated for a completed over-budget iteration are lost when a later fatal repository-extraction operation returns before the normal `metrics.json` write.

## Issue Context
The runtime-error path writes partial metrics explicitly, but fatal errors after the new budget assignment do not. Ensure aggregate metrics are persisted on all exits after aggregation, preferably through a single deferred or centralized writer that logs write failures.

## Fix Focus Areas
- internal/cli/run.go[2015-2031]
- internal/cli/run.go[2035-2045]
- internal/cli/run.go[2144-2173]
- internal/cli/run.go[2235-2237]

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


3. Accepted ADR gains new requirement ✓ Resolved 📜 Skill insight ≡ Correctness
Description
The PR adds max_cost_usd semantics directly to the Decision section of already accepted ADR 0024.
Accepted ADRs may only receive status changes, cross-references, short connecting notes, or
non-substantive fixes; new schema decisions must be recorded separately.
Code

docs/ADRs/0024-harness-definitions.md[R422-425]

+# Optional hard cost cap in USD, checked against the run's aggregated
+# total_cost_usd (summed across validation_loop retries). 0 (default) means
+# unlimited. Claude Code only reports cost once, in the final result event
+# of a completed iteration, so this halts the run before starting another
Relevance

●●● Strong

Recent precedent rejects substantive edits to accepted ADR Decisions; this adds a new schema
requirement.

PR-#6769
PR-#1982

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
ADR 0024 is marked Accepted in both frontmatter and its Status section, and the changed passage is
inside its Harness YAML schema Decision subsection. The addition defines a new configuration field
and runtime behavior rather than an allowed annotation or typographical correction.

Rule 1062057: Restrict modifications to accepted ADRs on main
docs/ADRs/0024-harness-definitions.md[1-21]
docs/ADRs/0024-harness-definitions.md[313-326]
docs/ADRs/0024-harness-definitions.md[422-429]
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 accepted ADR 0024 is being substantively extended with a new harness schema field and its behavioral contract.

## Issue Context
Accepted ADR history must remain immutable except for short annotations and cross-references. Record the cost-cap decision in a new ADR, then add only a brief cross-reference from ADR 0024.

## Fix Focus Areas
- docs/ADRs/0024-harness-definitions.md[422-429]

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


View high (1)
4. Exact cap still retries ✓ Resolved 🐞 Bug ≡ Correctness
Description
exceedsCostBudget uses strict >, so a failed iteration that brings aggregate cost exactly to
max_cost_usd still starts another iteration despite having exhausted the hard cap. The documented
inability to interrupt in-flight work does not apply because the exact accumulated cost is already
known before that retry begins.
Code

internal/cli/run.go[R3149-3150]

+func exceedsCostBudget(totalCostUSD, maxCostUSD float64) bool {
+	return maxCostUSD > 0 && totalCostUSD > maxCostUSD
Relevance

●● Moderate

Boundary behavior is debatable: the implementation and tests intentionally define the cap as
exceeded only when cost is greater.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The helper returns true only for totals strictly greater than the cap, while the loop starts another
agent run whenever OverBudget remains false. Validation failures are the normal retry path, so an
exact-cap failure proceeds into additional paid work.

internal/cli/run.go[1914-1923]
internal/cli/run.go[2196-2220]
internal/cli/run.go[3147-3150]
docs/reference/harness-reference.md[146-146]

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 budget check treats a total exactly equal to `max_cost_usd` as still having budget and permits another retry. Stop before another iteration when aggregate cost has reached or exceeded the configured cap.

## Issue Context
The loop evaluates the flag before each iteration, after completed iteration costs have already been aggregated. Update boundary tests to reflect hard-cap behavior.

## Fix Focus Areas
- internal/cli/run.go[3147-3150]
- internal/cli/run_test.go[4567-4584]

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



Remediation recommended

5. Overshoot bound is inaccurate ✓ Resolved 🐞 Bug ≡ Correctness ⭐ New
Description
The consequence claims the cap is soft by only one iteration, but crashed or killed iterations can
report no cost and therefore allow multiple retries with uncounted spend. This overstates the budget
guarantee in the accepted decision record.
Code

docs/ADRs/0097-harness-max-cost-usd-budget-cap.md[R61-63]

+- The cap is soft by one iteration — cost is only known at iteration
+  end — leaving the Claude runtime's native per-invocation budget flag
+  as an undecided future tightening for that runtime.
Relevance

●●● Strong

Recent precedents accept corrections to inaccurate ADR consequence claims, especially when normative
behavior disproves them.

PR-#5244
PR-#5328

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The ADR states an unconditional one-iteration softness bound, while the normative contract says
crashed or killed Claude iterations contribute $0 despite spending tokens and explicitly notes that
missing cost weakens the cap. Such iterations leave the aggregate below the cap and can permit
further retries, disproving the unconditional bound.

docs/ADRs/0097-harness-max-cost-usd-budget-cap.md[61-63]
docs/normative/harness-budget/v1/README.md[58-66]

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

## Issue description
Correct the ADR consequence claiming that the cost cap is soft by only one iteration. Repeated crashed, killed, or otherwise unpriced iterations can contribute zero reported cost and continue retrying, so the overshoot is not always limited to one iteration.

## Issue Context
The normative cost-reporting contract already explains that runtime-reported cost may be absent and that this weakens enforcement. Preserve the one-completed-iteration description only as a conditional guarantee when each iteration reports its cost.

## Fix Focus Areas
- docs/ADRs/0097-harness-max-cost-usd-budget-cap.md[61-63]
- docs/normative/harness-budget/v1/README.md[58-66]

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


6. Consequences bullet has two sentences ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
The soft-cap consequence contains two distinct sentences, while ADR consequences must be 3–5
one-sentence bullet points. The second sentence should be merged into the first or removed.
Code

docs/ADRs/0097-harness-max-cost-usd-budget-cap.md[R63-64]

+  Wiring the Claude runtime's native per-invocation budget flag could
+  tighten this for that runtime; not decided here.
Relevance

●●● Strong

Recent ADR reviews accepted Consequences wording and formatting fixes; this is a deterministic
one-sentence-per-bullet compliance correction.

PR-#5328
PR-#6083

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1062091 requires every Consequences bullet to contain exactly one sentence. This
bullet ends its first sentence at line 62 and adds a second sentence on lines 63–64.

docs/ADRs/0097-harness-max-cost-usd-budget-cap.md[61-64]
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 soft-cap consequence contains two sentences, violating the required one-sentence-per-bullet ADR format.

## Issue Context
The bullet starts at line 61 and ends with a separate sentence about potentially wiring Claude's native budget flag.

## Fix Focus Areas
- docs/ADRs/0097-harness-max-cost-usd-budget-cap.md[61-64]

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


7. Budget halt reason misstated ✓ Resolved 🐞 Bug ◔ Observability
Description
The over_budget documentation says the halt is reached only after validation fails, but an
extraction failure can continue before validation and the top-of-loop budget guard then sets the
marker. A subsequent post-loop validation sweep can even pass, so consumers may incorrectly
interpret over_budget as evidence of validation failure.
Code

docs/cli/run.md[83]

+| `over_budget` | Present (as `true`) only when the harness's `max_cost_usd` cap suppressed a retry: aggregate cost reached the cap while the validation loop still had iterations left. It records why retries stopped — not a success signal (the halt is only reached after an iteration failed validation), and a run whose final iteration merely crossed the cap while ending anyway is not marked |
Relevance

●●● Strong

The description makes a concrete incorrect runtime claim; recent documentation precedents accept
fixes to technically inaccurate behavior descriptions.

PR-#6508
PR-#6683

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Repository clearing and safe-download failures continue without running inline validation; the newly
added top-of-loop guard then sets OverBudget. Afterward, postLoopValidationSweep may pass and
allow runAgent to return successfully, disproving the statement that this halt occurs only after
failed validation.

internal/cli/run.go[1953-1959]
internal/cli/run.go[2180-2207]
internal/cli/run.go[2264-2274]
internal/cli/run.go[2347-2365]

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

## Issue description
Correct the `over_budget` documentation and matching code comment. The marker can be set after a retry-triggering repository extraction failure, not only after failed validation, and the post-loop validation sweep may subsequently pass.

## Issue Context
Extraction failures continue directly to the next iteration. If the completed iteration exhausted the budget, the top-of-loop guard suppresses that retry and sets `over_budget` before the post-loop validation sweep runs.

## Fix Focus Areas
- docs/cli/run.md[82-83]
- internal/cli/run.go[183-191]
- internal/cli/run.go[1953-1959]

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


View medium (9)
8. Contract inlined in ADR ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
ADR 0097 embeds the field-level contract for max_cost_usd, including validation, inheritance,
boundary, and output semantics, instead of linking to a versioned normative specification. This
leaves compatibility-critical behavior outside the required docs/normative/<topic>/v<major>/
structure.
Code

docs/ADRs/0097-harness-max-cost-usd-budget-cap.md[R39-41]

+Add an optional `max_cost_usd` field to the harness schema: a hard budget
+in USD for one run, checked between iterations against the aggregated
+`total_cost_usd` across `validation_loop` retries.
Relevance

●●● Strong

Recent documentation precedents enforce normative-spec consistency and correct missing
field-reference documentation.

PR-#6083
PR-#6398

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new ADR specifies the field name and type-level behavior, exact >= boundary, accepted and
rejected values, presence-aware merge semantics, and the metrics.json marker contract. Rule
1525847 requires such field-level contracts to be maintained as versioned normative specifications
and referenced from the ADR.

docs/ADRs/0097-harness-max-cost-usd-budget-cap.md[39-55]
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 0097 inlines a detailed field-level contract that must live in a versioned normative specification.

## Issue Context
Move the exact `max_cost_usd` validation, composition, budget-boundary, and `over_budget` output rules into `docs/normative/<topic>/v1/`. Keep the architectural decision concise and link it to that normative contract.

## Fix Focus Areas
- docs/ADRs/0097-harness-max-cost-usd-budget-cap.md[39-55]
- docs/normative/harness-budget/v1/README.md[1-1]

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


9. ADR 0024 edit unannounced 📘 Rule violation § Compliance
Description
The PR modifies accepted ADR 0024, but the PR description does not identify that ADR or summarize
the cross-reference addition. Accepted ADR edits must be explicitly called out even when the edit
itself is a permitted annotation.
Code

docs/ADRs/0024-harness-definitions.md[422]

+# A hard cost cap (max_cost_usd) was added later — see ADR 0097.
Relevance

●●● Strong

Recent ADR precedents accept annotations and cross-reference corrections; this explicitly identifies
an unannounced accepted-ADR edit.

PR-#5798
PR-#6398

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
ADR 0024 is marked Accepted, and the focused change adds a new cross-reference at line 422. The
supplied PR description discusses the budget documentation generally but does not name ADR 0024 or
summarize this accepted-ADR edit as required by rule 1062059.

Rule 1062059: Call out edits to accepted ADRs in PR descriptions
docs/ADRs/0024-harness-definitions.md[1-3]
docs/ADRs/0024-harness-definitions.md[419-423]

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

## Issue description
The PR description does not explicitly identify the edit to accepted ADR 0024 or summarize the new ADR 0097 cross-reference.

## Issue Context
Update the PR description to mention `ADR 0024` and state that a cross-reference to ADR 0097 was added; no source-code change is required.

## Fix Focus Areas
- docs/ADRs/0024-harness-definitions.md[422-422]

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


10. make e2e-test not evidenced 📘 Rule violation ▣ Testability
Description
The PR changes internal/cli but provides no evidence that the required make e2e-test run passed.
The PR description reports package tests, build, and vet results, but not the required end-to-end
suite.
Code

internal/cli/run.go[R1921-1923]

+		if aggMetrics.OverBudget {
+			break
+		}
Relevance

●●● Strong

Repository precedent accepts explicit testability evidence and coverage requirements for changed
internal CLI logic.

PR-#5192
PR-#5615

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1062051 requires successful E2E evidence for internal/cli changes. Repository
guidance requires make e2e-test for this directory, and CI is configured to run it for changed Go
files, but the supplied PR description contains no execution or passing result.

Rule 1062051: Run end-to-end tests for critical internal modules before merge
internal/cli/run.go[1921-1923]
docs/contributing/go-code.md[58-63]
.github/workflows/e2e.yml[140-170]

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

## Issue description
Changes under `internal/cli` require `make e2e-test` to run and pass, but the PR provides no successful E2E result.

## Issue Context
The repository workflow classifies changed Go files as E2E-relevant and invokes `make e2e-test`. Run that suite and record the passing result in the PR or ensure the corresponding required CI check succeeds.

## Fix Focus Areas
- internal/cli/run.go[1921-1923]
- .github/workflows/e2e.yml[140-170]

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


11. runAgent budget halt untested 📘 Rule violation ▣ Testability
Description
No behavioral test verifies that exceeding max_cost_usd prevents a subsequent validation-loop
iteration. Regressions in the marker assignment or loop guard could therefore pass the current
predicate and serialization tests.
Code

internal/cli/run.go[R2043-2045]

+		if !aggMetrics.OverBudget && exceedsCostBudget(aggMetrics.TotalCostUSD, h.MaxCostUSD) {
+			aggMetrics.OverBudget = true
+			printer.StepWarn(fmt.Sprintf("Over max_cost_usd budget ($%.4f > $%.4f) — halting further iterations", aggMetrics.TotalCostUSD, h.MaxCostUSD))
Relevance

●●● Strong

Recent CLI precedents accept requests for behavioral tests exercising production paths, not merely
helper coverage.

PR-#5192
PR-#5961

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1062049 requires tests for modified Go logic paths. The loop is stopped by
aggMetrics.OverBudget, but the added tests only verify exceedsCostBudget and JSON serialization
rather than exercising a multi-iteration run and asserting that the next iteration is skipped.

Rule 1062049: Require tests for new or modified Go logic
internal/cli/run.go[1921-1923]
internal/cli/run.go[2043-2045]
internal/cli/run_test.go[4552-4585]

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 budget-halting path lacks a behavioral test proving that a subsequent validation-loop iteration is not started after aggregate cost exceeds `max_cost_usd`.

## Issue Context
Existing tests cover the budget predicate and metrics serialization independently, but not their integration with the `runAgent` retry loop.

## Fix Focus Areas
- internal/cli/run.go[1921-1923]
- internal/cli/run.go[2043-2045]
- internal/cli/run_test.go[4552-4585]

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


12. MaxCostUSD classification undocumented ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The new Harness.MaxCostUSD field and its inheritance behavior are absent from the authoritative
harness field reference. Developers therefore cannot determine its documented classification or
merge semantics from the required source of truth.
Code

internal/harness/harness.go[342]

+	MaxCostUSD             float64                 `yaml:"max_cost_usd,omitempty"` // hard cost cap in USD; 0 = unlimited (default)
Relevance

●●● Strong

Repository precedent accepts documentation updates for new Harness fields and their merge semantics.

PR-#2582
PR-#6272

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changed code introduces Harness.MaxCostUSD and zero-based inheritance. The contributing
reference explicitly requires updates whenever a Harness field or merge behavior is added, but its
field tables and merge rules do not contain max_cost_usd.

Rule 2809648: Align Harness and ForgeConfig struct fields with documented conventions
internal/harness/harness.go[339-345]
internal/harness/compose.go[584-592]
docs/contributing/harness-fields.md[3-6]
docs/contributing/harness-fields.md[37-84]

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

## Issue description
`MaxCostUSD` was added to `Harness` without updating the authoritative harness field classification and merge reference.

## Issue Context
Classify `max_cost_usd` as top-level-only or forge-overridable as appropriate, and document that zero inherits from the base while a positive child value overrides it.

## Fix Focus Areas
- internal/harness/harness.go[342-342]
- internal/harness/compose.go[587-589]
- docs/contributing/harness-fields.md[37-84]

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


13. New metrics fields undocumented ✓ Resolved 📘 Rule violation ⚙ Maintainability
Description
The PR adds duration_seconds and over_budget to the user-facing metrics.json, but the
canonical metrics.json fields table omits both fields. Consumers consulting the CLI reference will
therefore have an incomplete output contract.
Code

internal/cli/run.go[R152-154]

+	NumTurns        int     `json:"num_turns"`
+	TotalCostUSD    float64 `json:"total_cost_usd"`
+	DurationSeconds float64 `json:"duration_seconds,omitempty"`
Relevance

●●● Strong

Recent CLI changes received accepted findings requiring user-facing documentation updates for
changed behavior and outputs.

PR-#5763
PR-#5976

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changed aggregate structure serializes the two new JSON fields, while docs/cli/run.md presents
an authoritative field-by-field metrics table containing neither duration_seconds nor
over_budget. This leaves documentation inconsistent with the changed user-facing output format.

Rule 2748504: Update docs when changing CLI behavior or public API
internal/cli/run.go[152-154]
internal/cli/run.go[183-190]
docs/cli/run.md[71-84]

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 CLI metrics output now includes `duration_seconds` and conditional `over_budget`, but the documented field table remains unchanged.

## Issue Context
Describe the duration aggregation semantics and clarify that `over_budget` records why retries stopped rather than indicating success.

## Fix Focus Areas
- internal/cli/run.go[152-154]
- internal/cli/run.go[183-190]
- docs/cli/run.md[71-84]

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


14. Non-finite caps bypass budget ✓ Resolved 🐞 Bug ≡ Correctness
Description
Validation accepts .nan and positive infinity because it only rejects values below zero. NaN fails
maxCostUSD > 0, while no finite aggregate exceeds positive infinity, so either value silently
disables the cap despite zero being the only documented unlimited value.
Code

internal/harness/harness.go[R503-505]

+	if h.MaxCostUSD < 0 {
+		return fmt.Errorf("max_cost_usd must be non-negative, got %v", h.MaxCostUSD)
+	}
Relevance

●●● Strong

Non-finite numeric configuration values bypass the documented validation contract and can silently
disable enforcement.

PR-#1325
PR-#5083

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Harness YAML is unmarshaled directly into the float field before Validate runs, and validation
only checks < 0. Runtime enforcement additionally requires the cap to compare greater than zero,
which is false for NaN, and compares aggregate cost against the cap, which cannot cross positive
infinity.

internal/harness/harness.go[357-370]
internal/harness/harness.go[500-505]
internal/cli/run.go[3147-3150]
docs/reference/harness-reference.md[146-146]

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

## Issue description
Reject NaN and infinite `max_cost_usd` values during harness validation so non-finite YAML values cannot disable budget enforcement.

## Issue Context
The field is decoded directly into a `float64`; comparison-only validation does not reject every non-finite value. Add validation and YAML-loading tests for `.nan`, `.inf`, and `-.inf` as appropriate.

## Fix Focus Areas
- internal/harness/harness.go[500-505]
- internal/harness/harness_test.go[758-773]
- internal/cli/run.go[3147-3150]

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


15. Context guard is ineffective ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
The regression test scans from runAgent to end-of-file but only rejects cancellation variables
named cancel or budgetCancel, so an equivalent rebinding such as `ctx, stop :=
context.WithCancel(ctx)` passes while an unrelated later function using either checked spelling
fails. This provides false confidence about the cancellation regression and can also block valid
unrelated changes.
Code

internal/cli/run_test.go[R4611-4614]

+	assert.NotContains(t, string(body), "\n\tctx, cancel := context.WithCancel(ctx)",
+		"rebinding ctx at runAgent's body scope makes the status defer report 'cancelled'")
+	assert.NotContains(t, string(body), "\n\tctx, budgetCancel := context.WithCancel(ctx)",
+		"rebinding ctx at runAgent's body scope makes the status defer report 'cancelled'")
Relevance

●● Moderate

The concern is technically plausible, but source-scanning test conventions have mixed acceptance and
rejection precedent.

PR-#1627
PR-#5192

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The test assigns body := src[start:], which includes every declaration after runAgent, and its
two assertions match exact strings containing only two specific cancellation-variable names. The
stated invariant in the preceding comments is broader than either check.

internal/cli/run_test.go[4587-4615]

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 source-text regression test neither limits inspection to the `runAgent` function nor detects equivalent context rebinding with other cancellation-variable names.

## Issue Context
Parse `run.go` with Go's AST, isolate `runAgent`, and reject body-scope assignments that bind `ctx` from `context.WithCancel(ctx)` regardless of the second variable's spelling. Alternatively, add a behavioral test around status reporting if the relevant dependency can be injected.

## Fix Focus Areas
- internal/cli/run_test.go[4587-4615]

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


16. Over-budget marker misstates halt ✓ Resolved 🐞 Bug ◔ Observability
Description
The code sets over_budget immediately upon crossing the cap, even when this is the only iteration
or validation subsequently passes and the run would have stopped normally. This contradicts the
documented meaning that the marker identifies a run the cap stopped, misleading post-scripts about
why execution ended.
Code

internal/cli/run.go[R2043-2045]

+		if !aggMetrics.OverBudget && exceedsCostBudget(aggMetrics.TotalCostUSD, h.MaxCostUSD) {
+			aggMetrics.OverBudget = true
+			printer.StepWarn(fmt.Sprintf("Over max_cost_usd budget ($%.4f > $%.4f) — halting further iterations", aggMetrics.TotalCostUSD, h.MaxCostUSD))
Relevance

●● Moderate

The PR explicitly defines over_budget as a retry-halt marker, but the finding raises a genuine
semantic interpretation issue.

PR-#6022
PR-#5944

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The marker is assigned before the no-validation-loop break and before validation can pass. Those
paths stop naturally, yet the serialized metric says the cap halted the run, contrary to both the
field comment and reference documentation.

internal/cli/run.go[183-190]
internal/cli/run.go[2035-2045]
internal/cli/run.go[2178-2181]
internal/cli/run.go[2196-2200]
docs/reference/harness-reference.md[146-146]

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

## Issue description
Set `over_budget` only when the budget condition actually suppresses a retry, rather than whenever a completed iteration's total crosses the cap.

## Issue Context
Budget status is currently determined before extraction and validation reveal whether another iteration is needed. Preserve a separate exceeded condition until a retry would occur, including extraction-failure continue paths, or redefine and document the field consistently if it is intended to mean only that cost exceeded the cap.

## Fix Focus Areas
- internal/cli/run.go[2035-2046]
- internal/cli/run.go[2144-2173]
- internal/cli/run.go[2178-2220]
- internal/cli/run_test.go[4552-4565]

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


Grey Divider

Context sources
✅ Compliance rules (platform): 65 rules
Review mode: ⚖️ Balanced

Grey Divider

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

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread docs/ADRs/0024-harness-definitions.md Outdated
Comment thread internal/harness/harness.go Outdated
Comment thread internal/cli/run.go
Comment thread internal/cli/run.go Outdated
Comment thread internal/harness/harness.go Outdated
Comment thread internal/cli/run.go Outdated
@guyoron1
guyoron1 marked this pull request as draft September 1, 2026 12:15
@guyoron1
guyoron1 marked this pull request as ready for review September 1, 2026 12:53
Comment thread internal/cli/run.go Outdated
Comment on lines +2043 to +2045
if !aggMetrics.OverBudget && exceedsCostBudget(aggMetrics.TotalCostUSD, h.MaxCostUSD) {
aggMetrics.OverBudget = true
printer.StepWarn(fmt.Sprintf("Over max_cost_usd budget ($%.4f > $%.4f) — halting further iterations", aggMetrics.TotalCostUSD, h.MaxCostUSD))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. runagent budget halt untested 📘 Rule violation ▣ Testability

No behavioral test verifies that exceeding max_cost_usd prevents a subsequent validation-loop
iteration. Regressions in the marker assignment or loop guard could therefore pass the current
predicate and serialization tests.
Agent Prompt
## Issue description
The new budget-halting path lacks a behavioral test proving that a subsequent validation-loop iteration is not started after aggregate cost exceeds `max_cost_usd`.

## Issue Context
Existing tests cover the budget predicate and metrics serialization independently, but not their integration with the `runAgent` retry loop.

## Fix Focus Areas
- internal/cli/run.go[1921-1923]
- internal/cli/run.go[2043-2045]
- internal/cli/run_test.go[4552-4585]

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

Comment thread internal/cli/run.go Outdated
Comment on lines +1921 to +1923
if aggMetrics.OverBudget {
break
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. make e2e-test not evidenced 📘 Rule violation ▣ Testability

The PR changes internal/cli but provides no evidence that the required make e2e-test run passed.
The PR description reports package tests, build, and vet results, but not the required end-to-end
suite.
Agent Prompt
## Issue description
Changes under `internal/cli` require `make e2e-test` to run and pass, but the PR provides no successful E2E result.

## Issue Context
The repository workflow classifies changed Go files as E2E-relevant and invokes `make e2e-test`. Run that suite and record the passing result in the PR or ensure the corresponding required CI check succeeds.

## Fix Focus Areas
- internal/cli/run.go[1921-1923]
- .github/workflows/e2e.yml[140-170]

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

Comment thread internal/harness/compose.go Outdated
Comment thread internal/cli/run.go Outdated
Comment thread internal/cli/run_test.go Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 69300c0

@guyoron1

guyoron1 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

/review

@qodo-code-review

Copy link
Copy Markdown

PR Reviewer Guide 🔍

Warning

/review is deprecated. Use /agentic_review instead (removal date not yet scheduled).

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 4 🔵🔵🔵🔵⚪
🧪 PR contains tests
🔒 No security concerns identified
⚡ Recommended focus areas for review

Lost Metrics

writeAggMetrics marks metrics as written before writeMetricsJSON succeeds. If the eager write fails, the deferred fallback will not retry, despite its stated purpose of preserving metrics on failures. Set metricsWritten only after a successful write so the defer can make another best-effort attempt.

metricsWritten := false
writeAggMetrics := func() {
	metricsWritten = true
	if err := writeMetricsJSON(runDir, aggMetrics); err != nil {
		printer.StepWarn("Failed to write metrics.json: " + err.Error())
	}

@guyoron1

guyoron1 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

/agentic_review

# Hard timeout enforced by the runner. The sandbox is killed after this.
timeout_minutes: 30

# A hard cost cap (max_cost_usd) was added later — see ADR 0097.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. adr 0024 edit unannounced 📘 Rule violation § Compliance

The PR modifies accepted ADR 0024, but the PR description does not identify that ADR or summarize
the cross-reference addition. Accepted ADR edits must be explicitly called out even when the edit
itself is a permitted annotation.
Agent Prompt
## Issue description
The PR description does not explicitly identify the edit to accepted ADR 0024 or summarize the new ADR 0097 cross-reference.

## Issue Context
Update the PR description to mention `ADR 0024` and state that a cross-reference to ADR 0097 was added; no source-code change is required.

## Fix Focus Areas
- docs/ADRs/0024-harness-definitions.md[422-422]

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

Comment thread docs/ADRs/0097-harness-max-cost-usd-budget-cap.md Outdated
Comment thread docs/cli/run.md Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 2ddefc1

@rh-hemartin

Copy link
Copy Markdown
Member

Hello!

  • Add the over budget attribute to the root span, so we get that on telemetry.
  • I don't think there are tests for harness inheritance, add some so we are sure the inheritance works as described.
  • Feel free to undo the ADR0024 changes, I don't think they are needed.
  • Add the duration metric to the PR description in a more explicit way, it is something that could be pulled from this PR as it does not have impact, so at least document it properly on the PR body.
  • No issue?

@rh-hemartin

Copy link
Copy Markdown
Member

/fs-reivew

@rh-hemartin

Copy link
Copy Markdown
Member

Also rebase to solve the Build Site problem.

@guyoron1

guyoron1 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit e4a5843

@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 pass at head e4a5843. Two MEDIUM findings posted inline (ADR 0097 line 33, internal/cli/run.go line 2080). Neither overlaps the existing threads.

[operational-observability.md](../problems/operational-observability.md)).

Claude Code reports `total_cost_usd` once, in the final result event of a
completed iteration, so no mechanism at this layer can interrupt an

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 — Design premise "no mechanism can interrupt an in-flight iteration" is false for the Claude runtime (native --max-budget-usd exists and is unused)

Verified at head e4a5843. Three in-diff passages state as fact that in-flight interruption is unavailable: this ADR's Context ("no mechanism at this layer can interrupt an iteration already in flight; the only enforceable boundary is between iterations"), docs/normative/harness-budget/v1/README.md lines 45-47 ("an iteration already in flight is never interrupted; the cap is soft by at most one iteration"), and docs/reference/harness-reference.md line 147 ("it cannot interrupt one long iteration still in progress"). The PR body repeats it.

But Claude Code ships --max-budget-usd <amount> ("Maximum dollar amount to spend on API calls (only works with --print)") — present in claude --help on 2.1.258 and in the anthropics/claude-code CHANGELOG: added in 2.0.28 ("SDK: added --max-budget-usd flag"), subagent enforcement fixed in 2.1.217. The harness sandbox image pins CLAUDE_CODE_VERSION=2.1.252 (images/sandbox/Containerfile:54), so the flag is available in every fleet run. buildRunCommand (internal/runtime/claude.go:302-310) already runs claude --print --verbose --output-format stream-json, and git grep -i max-budget-usd over the head tree returns nothing, so the native per-iteration cap was neither used nor evaluated. The "soft by one iteration" consequence in the ADR rests on that unverified premise.

Suggestion: Either

(a) pass --max-budget-usd <max_cost_usd - aggregate so far> from buildRunCommand (via RunParams) for the Claude runtime whenever max_cost_usd > 0, so one long iteration cannot blow past the cap. A budget trip yields an error result in the transcript, which the existing is_error path (run.go ~2091) already turns into a failed iteration, and the between-iteration latch then suppresses the retry with no new loop logic; or

(b) keep the between-iteration-only design but rewrite the three passages (ADR 0097 Context/Consequences, normative README "Enforcement boundary", harness-reference.md max_cost_usd entry) to say fullsend deliberately does not use the runtime's native cap and why (e.g. runtime neutrality — pi/opencode have no equivalent).

Either way, the docs should not assert that the capability does not exist.

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 61d0964 — all three passages reworded: the iteration boundary is the runtime-agnostic choice (pi has no in-flight control), and Claude Code's native --max-budget-usd is named as an unused, possible tighter complement. Not wired here — docs stay truthful, feature stays out of a review-fix commit.

Comment thread internal/cli/run.go
// anyway. Cancelling the run's context here instead would reassign
// the ctx that the status-comment defer closes over, making every
// run report "cancelled".
if !budgetExhausted && exceedsCostBudget(aggMetrics.TotalCostUSD, maxCostUSD) {

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 — Cap is silently inert when the runtime reports zero cost; docs call it a "hard budget" without stating it depends on runtime-reported cost

Verified at head e4a5843. exceedsCostBudget (run.go:3192) is maxCostUSD > 0 && totalCostUSD >= maxCostUSD, and the only trigger is aggMetrics.TotalCostUSD, which aggregateRunMetrics (run.go:3173) sums from each iteration's runtime-reported value. Nothing in run.go distinguishes "cost $0" from "cost unknown" — there is no StepWarn on that path.

Under-count vectors confirmed in the tree:

  1. The Claude runtime sets TotalCostUSD only from the final result event (claude.go:136, claude_progress.go:345), and the runtime's own test asserts 0 cost when no result event arrives (claude_progress_test.go:631) — so a crashed/killed iteration contributes $0 to the aggregate even though tokens were spent.
  2. The pi runtime sums msg.Usage.Cost.Total (pi_progress.go:336) from a plain piCost{Total float64} struct (pi_progress.go:34), so a provider entry without pricing reports 0 per message.
  3. docs/guides/infrastructure/distributed-tracing.md:192 already states fullsend has no pricing table and "accepts the reported total" as-is.

Yet the new normative README line 19 calls the field "A hard budget in USD for one run" with no qualification, and the runtime-coverage caveat appears nowhere in the PR's docs — only Claude Code is named. A repo that sets max_cost_usd on a pi/opencode harness with an unpriced provider, or whose Claude iterations crash, gets no enforcement and no signal.

Suggestion: After aggregateRunMetrics (line 2055), when maxCostUSD > 0 && metrics.NumTurns > 0 && metrics.TotalCostUSD == 0, emit a StepWarn such as "runtime reported no cost for this iteration; max_cost_usd cannot be enforced against it" (optionally add a cost_unreported marker in metrics.json / the root span). In docs/normative/harness-budget/v1/README.md, qualify "hard budget" as enforced against the runtime's self-reported cost estimate and link the cost data contract in distributed-tracing.md, noting per-runtime coverage (Claude: final result event only, zero when the iteration does not produce one; pi/opencode: only when the provider/model entry carries pricing).

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 61d0964 — fullsend run now warns when a cap is set and a completed iteration reports no cost, and the normative spec gained a Cost reporting section naming the under-count vectors; "hard budget" is qualified as enforced against self-reported cost. Test added.

@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 pass at head e4a5843, supplementary to my earlier review: two MEDIUM findings from the second reviewer that were not relayed the first time (ADR 0097 relates_to line 5, internal/cli/budget_run_test.go line 58). Neither overlaps an existing thread.

title: "97. Harness-level max_cost_usd budget cap"
status: Accepted
relates_to:
- operational-observability

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

MEDIUM — ADR 0097 decides the threat model's open "hard stop vs. human-in-the-loop" cost-budget question, but neither problem doc points back to it

Verified at head e4a5843. docs/problems/security-threat-model.md line 383 proposes cost budgets that "require human approval before further agent invocations", and its Open questions (line 402) ask "Should cost budgets trigger a hard stop or a human-in-the-loop approval flow?". This ADR answers that at per-run granularity (hard stop between iterations, no approval flow), yet security-threat-model.md is not listed under relates_to: and neither it nor operational-observability.md (the one doc that is listed, whose "How much does it cost?" section at lines 74-84 is the other passage that discusses this) carries any pointer to ADR 0097. git grep 0097 docs/problems/ on the head tree returns nothing.

The repo convention for an Accepted ADR is an inline pointer at the open question it decides, not a rewrite: see the same Open questions section of operational-observability.md (lines 191-196), where ADR 0041, 0021, 0087 and 0050 are each linked from the question they resolved (struck through when fully decided, annotated when partial). Without that, a reader of the threat model's DOS section cannot discover that the decision was made, and the "hard stop vs. approval" question stays open in the living doc after it was closed here.

Suggestion: add security-threat-model to relates_to:; annotate the line-402 open question ("Per-run hard stop decided in ADR 0097; per-repo/per-org budgets and an approval flow remain open"); and add a one-line pointer under the cost section of operational-observability.md (e.g. after line 84: "Per-run cap: ADR 0097"). This is the same pattern ADR 0050 used for its "bootstrapping" question.

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 61d0964 — security-threat-model added to ADR 0097's relates_to with a Context mention, and the threat model's open question now carries the partial answer in place (per-run hard stop decided; per-repo/org budgets and approval flows remain open).

Comment thread internal/cli/budget_run_test.go Outdated
" esac ;;\n" +
// `sandbox download <name> <remote> <local>`: create the local
// destination so SafeDownload's sanitize walk sees a directory.
" 'sandbox download') mkdir -p \"$5\"; exit 0 ;;\n" +

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 — The extraction-failure over_budget path is untested: the stub makes sandbox download always succeed, so the top-of-loop guard that exists for that path is never exercised

Verified at head e4a5843. docs/normative/harness-budget/v1/README.md lines 51-54 make the failed-repo-extraction case a first-class part of the contract ("The suppressed retry can follow a failed validation or a failed repository extraction"). In internal/cli/run.go both extraction-failure branches (forceRemoveAll pre-clear at ~2184 and SafeDownload at ~2207) continue past the bottom-of-loop retry check at ~2250-2258, so the only place over_budget can be recorded for them is the top-of-loop guard at ~1958-1961 — whose own comment says "This guard is what halts the extraction-failure continue paths".

None of the three tests in this file reach it. This stub line answers every sandbox download with mkdir -p; exit 0, so extraction always succeeds and TestRunAgent_BudgetHaltsValidationRetries / …CrossedOnFinalIterationIsNotMarked / …UnderBudgetStillRetries all stop via the bottom-of-loop break after a failed validation script. If the top-of-loop guard were deleted (or its aggMetrics.OverBudget = true dropped), all three tests still pass, and an extraction-failure run over budget would silently start another iteration and never write the marker.

Suggestion: parameterise useBudgetRunStub (or add a sibling) so sandbox download can exit 1, then add one case with max_iterations: 3, cost over the cap on iteration 1, and a failing download: assert exactly one RUN line in the log and over_budget: true in metrics.json. That pins the guard the normative contract depends on.

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 61d0964 — the stub can now fail sandbox download; TestRunAgent_BudgetHaltsExtractionFailureRetries drives the continue path with the budget exhausted and asserts one invocation + over_budget: true.

@guyoron1

guyoron1 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

/agentic_review

Comment thread docs/ADRs/0097-harness-max-cost-usd-budget-cap.md Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 61d0964

@guyoron1

guyoron1 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

/agentic_review

Comment thread docs/ADRs/0097-harness-max-cost-usd-budget-cap.md Outdated
@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 8b72440

@guyoron1

guyoron1 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

/agentic_review

@qodo-code-review

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit c06fdba

@guyoron1
guyoron1 force-pushed the feat/review-cost-surfacing branch from c06fdba to 315e193 Compare September 3, 2026 09:05
@guyoron1

guyoron1 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

@rh-hemartin Going through these:

  • over_budget on the root span — added: fullsend.over_budget attribute in rootSpanEndAttrs (internal/cli/run.go), was metrics.json-only before. New test TestRootSpanEndAttrs_OverBudget.
  • Harness-inheritance tests — already there: TestLoadWithBase_LocalBase_MaxCostUSDInherit / MaxCostUSDChildOverride / MaxCostUSDChildExplicitZeroDisables in internal/harness/compose_test.go.
  • ADR0024 — kept the one-liner rather than reverting it; it's just a pointer to ADR 0097 for anyone reading 0024 later, no decision content duplicated.
  • Duration metric in the PR bodyduration_seconds now has its own explicit bullet.
  • No issue? — correct, none. This came out of ad-hoc auditing, not a filed bug. Can open one if you want a paper trail.
  • Rebase — done, onto main's SSH→HTTPS experiments submodule-URL fix. build is running on the rebased head now (was red on the old head from the fork-SSH issue, not from this PR's own code); will confirm once it reports.

Head is 315e193.

@guyoron1

guyoron1 commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

build passed on 315e193. All checks green now, blocked only on required review.

@rh-hemartin

Copy link
Copy Markdown
Member

No need for papel trail, just wanting someone with a use case or someone that will use it. We keep adding things that no one asked for. I'm fine with this, no worries.

@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 pass. One finding posted inline. Two additional MEDIUM findings below have no in-diff anchor (the specific lines fall outside this PR's diff hunks / outside files it touches), so they're included here instead:

MEDIUM — Merge-rules table omits max_cost_usd's presence-aware 0 semantics (docs/reference/harness-reference.md:179)

The field-details prose earlier in the same file (line 147) correctly states max_cost_usd merges by presence (absent inherits the base's cap; an explicit 0 in a child disables an inherited cap as unlimited). But the "Field merge rules" table further down the file has no row for max_cost_usd — it falls under the generic "Scalars (model, pre_script, policy, image, etc.)" row (line 179), which states the opposite rule (value-based, not presence-based, treating 0 as empty/no-override rather than an explicit override). A reader who only consults the table (not the prose above it) will misconfigure a child harness's cost cap.

Suggestion: add an explicit max_cost_usd row/footnote to the merge-rules table clarifying it merges by presence (nil vs. set), not by non-empty value, and that an explicit 0 means unlimited rather than inherit.


MEDIUM — New harness-level max_cost_usd collides in name with the pre-existing eval-case max_cost_usd threshold

docs/testing/functional-tests.md:165 already documents max_cost_usd as a per-eval-case post-run judge threshold (compared against metrics.json by the max_cost deterministic judge in eval.yaml). This PR adds a same-named but semantically different harness-level field (a mid-run hard stop that suppresses further retries) without touching functional-tests.md or the two problem docs that reference the pre-existing field: docs/problems/cross-run-memory.md:99 and docs/problems/flapping-convergence.md:51, both of which say cost is "already enforced via max_cost_usd in the functional test framework" — a sentence that is now ambiguous about which max_cost_usd is meant, since the harness-level field also exists. Confirmed via diff that this PR does not touch any of these three files, so the collision is a side effect of introducing the new field name.

Suggestion: disambiguate the two mechanisms wherever both could be confused — e.g. name them "harness max_cost_usd (hard stop, per-run)" vs. "eval max_cost_usd threshold (post-run judge)" in the harness reference, functional-tests.md, and the two problem-doc passages. A field rename isn't necessary since this is a documentation/mental-model collision, not a schema conflict.

Comment thread internal/cli/run.go Outdated
// run report "cancelled".
if !budgetExhausted && exceedsCostBudget(aggMetrics.TotalCostUSD, maxCostUSD) {
budgetExhausted = true
printer.StepWarn(fmt.Sprintf("Reached max_cost_usd budget ($%.4f of $%.4f) — no further iterations will start", aggMetrics.TotalCostUSD, maxCostUSD))

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 — Budget StepWarn fires even when no retry is actually suppressed

When aggregate cost first crosses max_cost_usd, this line unconditionally logs StepWarn("Reached max_cost_usd budget ... — no further iterations will start"). aggMetrics.OverBudget (the metrics.json / OTEL over_budget marker) is deliberately latched only at two later points: the top-of-loop guard (~line 2107-2109) and the bottom-of-loop retry-suppression guard (~line 2415-2424) — by design, per the comment above this block, so the marker records an actually-suppressed retry.

But if the very same iteration that crosses the cap then passes validation, the loop breaks at line 2401 before ever reaching the retry-suppression check at line 2415, so OverBudget never gets set even though the StepWarn already printed "no further iterations will start." This is distinct from the already-fixed zero-cost StepWarn on this same line range (fixed in 61d0964, which addressed under-counting, not this timing gap).

Suggestion: either move/gate this StepWarn so it only fires when a retry is genuinely about to be suppressed (next to the two OverBudget = true sites), or reword it to something like "cap reached; remaining retries, if any, will be skipped" so it doesn't imply the run was halted by budget when metrics.json's over_budget stays false.

A runaway agent had no cost ceiling: max_iterations bounds how many times
it retries, but nothing bounds what those retries cost. max_cost_usd is
an optional per-harness cap (0 = unlimited, inherited by composition like
timeout_minutes) checked against the aggregate cost after each iteration.

Claude Code reports total_cost_usd only in the final result event of a
completed iteration, so the cap cannot interrupt work already in flight —
it stops the loop from starting another iteration. The check is at the
top of the loop rather than the bottom so the two mid-iteration continue
paths cannot buy an iteration the budget has already refused.

The halt sets over_budget in metrics.json so a post-script can tell a
deliberate stop from a crash. It records why the run stopped retrying and
does not imply success: with a validation_loop the halt is only reachable
after an iteration failed validation, so over_budget commonly accompanies
a non-nil error.

metrics.json also gains duration_seconds, which post-scripts can surface
alongside cost.

TestRunAgent_DoesNotCancelTheRunContext pins a constraint that is easy to
reintroduce: cancelling the run's own ctx to stop the loop would rebind
the variable the status-comment defer closes over, and defers run LIFO,
so every run would report 'cancelled'. The test reads the source because
the failure is invisible at the package boundary — the run still
succeeds, only the reported status is wrong — and .codecov.yml excludes
run.go from patch coverage.

Signed-off-by: guy oron <goron@redhat.com>
Correctness:
- Treat an aggregate cost exactly equal to max_cost_usd as exhausted
  (>=): a budget that is exactly spent buys no further iteration.
- Reject non-finite max_cost_usd values (NaN, +/-Inf) during harness
  validation; either would silently disable the cap.
- Make Harness.MaxCostUSD presence-aware (*float64) so a child harness
  can override an inherited positive cap with an explicit 0 (unlimited);
  absent still inherits, and runtime treats nil and 0 identically.
- Set the over_budget marker only when the cap actually suppresses a
  retry that was otherwise due, instead of whenever a completed
  iteration's total crosses the cap, so the marker means "the cap
  stopped this run", not "the run was expensive".
- Persist aggregate metrics on every exit after iteration metrics have
  been aggregated via one deferred best-effort writer, so fatal
  repository-extraction paths no longer lose cost/duration/over_budget;
  write failures are logged.

Tests:
- Behavioral coverage of the runAgent loop through an openshell stub
  that streams canned cost results: the loop must not start another
  iteration once the cap is reached, must not mark a run that was
  ending anyway, and must keep retrying while under budget.
- Replace the source-text ctx-rebinding regression scan with a Go AST
  check confined to runAgent's body scope that rejects rebinding ctx
  from any cancellable context constructor, whatever the cancel
  variable is named.
- Update budget boundary, validation, and compose tests for the new
  semantics; add YAML .nan/.inf/-.inf loading tests.

Docs:
- Record the cost-cap decision as ADR 0097 and reduce the addition to
  accepted ADR 0024 to a one-line cross-reference.
- Document max_cost_usd classification and presence-aware merge rules
  in the harness field reference; document duration_seconds and
  over_budget in the metrics.json field table; update architecture.md
  and the harness reference for the final semantics.

Signed-off-by: guy oron <goron@redhat.com>
Address the second review pass on the budget cap:

- The field-level contract (validation, base: inheritance, enforcement
  boundary, over_budget semantics) moves to
  docs/normative/harness-budget/v1, following ADR 0015 and the shape of
  the prescript-output spec; ADR 0097 keeps the decision and links the
  spec instead of restating it.
- Correct the over_budget prose in docs/cli/run.md and the run.go field
  comment: the suppressed retry can follow a failed validation or a
  failed repository extraction (whose continue path never reaches
  validation), and the marker implies nothing about the final
  validation state — the post-loop sweep may still pass a completed
  iteration.

Signed-off-by: guy oron <goron@redhat.com>
Address the third review pass on the budget cap:

- Correct the false premise that no mechanism can interrupt an in-flight
  iteration: the boundary is between iterations because that is the
  runtime-agnostic layer (pi offers no in-flight budget control), while
  Claude Code's native per-invocation --max-budget-usd flag exists,
  is unused today, and could later complement the cap as a tighter
  in-flight bound. Reworded in ADR 0097, the normative spec, and the
  harness reference; wiring the flag is out of scope here.
- Warn when a cap is set and a completed iteration reports zero cost:
  enforcement relies on runtime-self-reported cost, so a crashed stream
  or an unpriced provider silently under-counts the aggregate. The
  normative spec and docs/cli/run.md now state the dependency and link
  the cost data contract; a test covers the warning.
- Cross-reference the threat model: ADR 0097 answers its open 'hard
  stop vs human-in-the-loop' cost-budget question at per-run
  granularity, so relates_to gains security-threat-model, the ADR
  context mentions it, and the open question is annotated in place
  (per-repo/per-org budgets and approval flows remain open).
- Pin the extraction-failure over_budget path: the openshell test stub
  can now fail sandbox download, exercising the top-of-loop guard that
  suppresses a retry after a failed extraction — previously only the
  validation-failure halt was tested.

Signed-off-by: guy oron <goron@redhat.com>
Signed-off-by: guy oron <goron@redhat.com>
…orted cost

The soft-by-one-iteration bound only holds when every iteration reports
its cost; crashed, killed, or unpriced iterations contribute $0 and can
widen the overshoot. Say so in ADR 0097 and the normative contract
instead of overstating the guarantee.

Signed-off-by: guy oron <goron@redhat.com>
metrics.json already carries over_budget; the root span didn't, so a
budget-halted run was invisible to trace-based alerting.

Signed-off-by: guy oron <goron@redhat.com>
The merge-rules table had no max_cost_usd row, so the field fell under the
generic scalar row ("child wins if non-empty") — the opposite of the rule
the field details state a few sections above, where an explicit 0 is an
override to unlimited and only an absent field inherits. A reader who
consults the table alone would misconfigure a child harness. Add the row
and a note that this is the one scalar the generic rule does not cover.

The harness field also shares its name with the eval-case max_cost_usd
threshold in the functional test framework: a post-run judge that grades a
finished run against metrics.json, where this one is a mid-run stop. Say
which is which in the harness reference, in functional-tests.md, and in
the two problem docs whose "already enforced via max_cost_usd" sentence
now reads ambiguously.

While there: name codex in the runtime cost-coverage notes — it reports no
cost at all, so a cap on a codex harness never trips; record in ADR 0097
that the one runtime shipping a native per-invocation budget flag does not
get it passed through, and why forwarding it is its own decision; and
point operational-observability's cost section back at the ADR, as the
threat model already does.

Signed-off-by: guy oron <goron@redhat.com>
The warning fired the moment aggregate cost reached the cap and claimed
"no further iterations will start", but at that point the loop does not
yet know whether anything will be suppressed: the same iteration may pass
validation and break out, or be the last one. In both cases nothing is
skipped and metrics.json keeps over_budget false, so the log contradicted
the marker. Reword it to the only thing crossing the cap guarantees —
remaining retries, if there are any, will be skipped.

The suppression sites are where a halt can honestly be announced, and the
top-of-loop guard (the one that stops an extraction-failure retry) was
breaking silently. Give it the same line the bottom-of-loop guard prints,
so every run that the budget really stopped says so.

The two suppression tests now assert the announcement, and the
crossed-on-the-final-iteration test asserts the absence of any halt claim.

Signed-off-by: guy oron <goron@redhat.com>
…ebase

Rebasing onto fullsend-ai#7049 brought a second pre-return metrics write, in the
path that fails to clear a stale iteration deadline. The deferred writer
registered before the loop already persists metrics.json on every fatal
exit once iterations have started, so the inline write was a duplicate;
drop it so there is one writer, as the comment above the loop says.

The budget contract now also states that an iteration killed at
timeout_minutes ends the run before any retry is due (ADR 0105), so it
is never marked over_budget, whatever its cost.

Signed-off-by: guy oron <goron@redhat.com>
@guyoron1
guyoron1 force-pushed the feat/review-cost-surfacing branch from 315e193 to 5972b8f Compare September 6, 2026 06:02
@guyoron1

guyoron1 commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

@waynesun09 @rh-hemartin: rebased onto main through #7049; run.go and telemetry_run_test.go conflicted, both sides kept. Three new commits:

  • 4f2c89cd: merge-rules row for max_cost_usd (presence-based, explicit 0 = unlimited); harness field and eval-case threshold named apart in four docs; ADR 0097 says the native budget flag is not forwarded; codex reports no cost, so the cap never trips there.
  • d5121672: the cap-reached warning no longer claims a halt before one is known; the top-of-loop suppression announces itself. Tests cover both.
  • 5972b8fd: drops the pre-return metrics write fix(#7042): no retry after a timeout, terminate the agent, export the budget to the sandbox #7049 added (the deferred writer covers it); the contract notes a timed-out iteration ends the run before the cap is consulted (ADR 0105).

Deferred: two qodo threads wanting PR-description edits.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants