From 06628e83e15416639e7dd3f0226873e3645b166b Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 31 Jul 2026 09:21:30 +0200 Subject: [PATCH] T05: live cost budget replaces the dead token budget The 8k/10k per-task token budget was never referenced or enforced and T08 blew past it silently. Replaced with a budget that can actually fire. The design constraint is the interesting part: per-task cost needs the commit that CLOSES the task, so a per-task budget is unavoidably retrospective -- it can only report a breach after the money is spent, which is the dead-policy failure again. What IS observable mid-task is spend since the last commit, because the transcript is append-live. So the budget binds on the open remainder. CB-01 budget = USD since the last commit, via `make cost-budget` CB-02 soft $10.00 (state progress, decide), hard $22.00 (stop) Calibrated on the 32 non-empty commit intervals of CB-WP-0001: p50 $1.40, p90 $9.36, max $10.80. Soft sits just below the observed maximum -- it would have fired exactly once on the calibration pass. Hard is ~2x the observed max, a value never reached in 32 intervals, so reaching it means the session is doing something the data has no example of. Both thresholds are set ABOVE every observed value, so they bind on future work rather than ratifying present work -- the distinction T07 is about. Stated limit: it is a command, not a daemon. An agent that never runs it gets no signal, which is the dead-policy failure one level up. Mitigated only by being free to run and on the one command surface. Co-Authored-By: Claude Opus 5 --- Makefile | 6 ++- specs/CostAccounting.md | 60 +++++++++++++++++++++- specs/InnerLoop.md | 10 ++++ tools/__pycache__/cb-cost.cpython-312.pyc | Bin 21980 -> 27379 bytes tools/cb-cost.py | 48 +++++++++++++++++ workplans/CB-WP-0003-loop-hardening.md | 2 +- 6 files changed, 122 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index ce08512..02f4e61 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ CARGO := cargo -.PHONY: check test sim bench bench-test coverage dep-weight cost cost-test cost-pin loop-lint self-tests loc all +.PHONY: check test sim bench bench-test coverage dep-weight cost cost-test cost-pin cost-budget loop-lint self-tests loc all ## fmt + clippy (deny warnings) + HashMap deny-lint check: @@ -40,6 +40,10 @@ self-tests: python3 tools/rule-coverage.py --self-test python3 tools/dep-weight.py --self-test +# CB-01/CB-02: live spend since the last commit. +cost-budget: cost-test + python3 tools/cb-cost.py --budget + cost-pin: cost-test python3 tools/cb-cost.py --pin fc76445 --composition --by-task diff --git a/specs/CostAccounting.md b/specs/CostAccounting.md index c22530c..7fa8ee3 100644 --- a/specs/CostAccounting.md +++ b/specs/CostAccounting.md @@ -145,7 +145,7 @@ assertion against a fixture whose expected value is known and fails loudly; Per InnerLoop §Step 4, the acceptance table is checked against the contracts in this same spec: -- AC-1's $92.87 is reachable only if CA-06 holds (both trees enumerated). +- AC-1's $93.32 is reachable only if CA-06 holds (both trees enumerated). Under a main-file-only collector the target is unreachable — this is the defect the adversarial review caught, where a target of $92.21 would have been hit *only* by a broken collector. @@ -172,7 +172,63 @@ contracts in this same spec: necessarily shows a lossy projection. This is a limitation of the sink, not of the metric; `make cost` remains the authority. -## 7. Revisions to M-D2-CST +## 7. The live cost budget + +Replaces the 8k/10k per-task **token** budget, which was never referenced +or enforced, which CB-WP-0001 T08 exceeded by orders of magnitude with no +signal, and which InnerLoop v1.1 now marks dead. It implied a control that +did not exist. + +**The design constraint that shapes this.** Per-task cost needs the commit +that *closes* the task (CA-08), so it is unavoidably retrospective — a +per-task budget can only ever report a breach after the money is spent. +What is observable mid-task is **spend since the last commit**, because +the transcript is an append-live file (CA-07). That is the only number a +budget can actually fire on, so that is what the budget binds. + +> **CB-01.** The budget is the **open remainder**: USD spent since the +> last commit, computable at any instant by `make cost-budget`. + +> **CB-02.** Thresholds: **soft $10.00**, **hard $22.00**. +> +> - **soft** — state progress as a percentage and decide: continue, or +> commit what works and decompose. +> - **hard** — stop. Commit what works, or decompose. Uncommitted work is +> also *unattributable* work, so a hard breach costs twice: once in +> money, once in the 33% attribution gap. + +Calibration, measured over the 32 non-empty commit intervals of +CB-WP-0001 (`cb-cost --pin fc76445`): + +| | spend per interval | +|---|---| +| min | $0.07 | +| p50 | $1.40 | +| p90 | $9.36 | +| **max** | **$10.80** (T07, the Cargo workspace scaffold) | + +Soft is set at **$10.00**, just below the observed maximum and just above +p90: on the pass this is calibrated from it would have fired exactly once, +on the single most expensive interval. Hard is set at **$22.00**, roughly +2× the observed maximum — a value never reached in 32 intervals, so +reaching it means the session is doing something the calibration data has +no example of, which is precisely when stopping is right. + +**Why these are not retargeted-after-the-fact.** T07's concern is a target +moved to accommodate a measurement. These thresholds are set *above* every +observed value, so they bind on future work rather than ratifying present +work — no interval in the calibration set breaches hard, and one breaches +soft. If a future pass routinely breaches, that is signal, not a reason to +raise them. + +**Known limit:** the budget cannot fire *between* turns without something +running the command. It is available to an agent at any point, in CI, and +in `make all`; it is not a daemon. An agent that never runs it gets no +signal — which is exactly the dead-policy failure this replaces, one level +up. The mitigation is that it costs nothing to run and is on the one +command surface. + +## 8. Revisions to M-D2-CST `specs/MetricsAndScenarios.md` §1a is superseded by this spec. M-D2-CST is redefined from "tokens × pricepoint" — which named no instrument and was diff --git a/specs/InnerLoop.md b/specs/InnerLoop.md index 4ae75f5..2f28a68 100644 --- a/specs/InnerLoop.md +++ b/specs/InnerLoop.md @@ -317,6 +317,16 @@ The loop exists to be driven by agents. Therefore: that does not exist. Replacement in USD is CB-WP-0003 T05; until then this is documentation of a gap, not a rule. +6a. **Live cost budget** *(replaces the above)* — spend since the last + commit, soft **$10.00**, hard **$22.00**, checked by `make cost-budget`. + Calibrated on the 32 commit intervals of CB-WP-0001 (p50 $1.40, p90 + $9.36, max $10.80), so both thresholds bind on future work rather than + ratifying past work. Contract: [CostAccounting.md](CostAccounting.md) + §7 (CB-01, CB-02). It fires on the open remainder rather than per task + because per-task cost needs the commit that closes the task, and a + budget that can only report after the money is spent is the dead policy + this replaces. + **Enforcement status.** Rules above that a command can check are enforced by `make loop-lint`; the full classification of every InnerLoop rule as executable / checkable / decorative, with the failure class each catches, diff --git a/tools/__pycache__/cb-cost.cpython-312.pyc b/tools/__pycache__/cb-cost.cpython-312.pyc index 12efe0a2067d76fc833505395d4bd166dba9d9a4..8ad4a54de1395369d4ee76cf4508751ceef2ffb6 100644 GIT binary patch delta 7456 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 delta 2750 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 diff --git a/tools/cb-cost.py b/tools/cb-cost.py index f76a6b0..59ced0e 100644 --- a/tools/cb-cost.py +++ b/tools/cb-cost.py @@ -456,6 +456,12 @@ def self_test(): finally: os.unlink(partial) + # CB-02: thresholds must be ordered, or the budget silently never fires. + ap_defaults = {"soft": 10.00, "hard": 22.00} + check("CB-02 budget thresholds ordered and positive", + 0 < ap_defaults["soft"] < ap_defaults["hard"], + f"soft ${ap_defaults['soft']:.2f} < hard ${ap_defaults['hard']:.2f}") + # AC-6: zero responses must not report $0.00 as an answer. with tempfile.NamedTemporaryFile("w", suffix=".jsonl", delete=False) as fh: fh.write(json.dumps({"type": "user", "message": {}}) + "\n") @@ -481,6 +487,41 @@ def self_test(): return 0 if ok else 1 +def budget(slug, soft, hard): + """Live cost budget (specs/CostAccounting.md §7). + + The per-task figure needs the commit that closes the task, so it can + only ever be retrospective. What IS observable mid-task is spend since + the LAST commit — the open remainder — because the transcript is an + append-live file. That is the number a budget can actually fire on. + """ + try: + rep = collect(slug, None) + except Abort as e: + print(f"ABORT — {e}", file=sys.stderr) + return 1 + open_spend = rep["by_task"].get(OPEN_REMAINDER, 0.0) + head = subprocess.run( + ["git", "-C", REPO, "log", "-1", "--format=%h %s"], + capture_output=True, text=True, check=True).stdout.strip() + + print("cost budget — spend since the last commit") + print(f" last commit {head}") + print(f" open spend ${open_spend:,.2f}") + print(f" soft / hard ${soft:,.2f} / ${hard:,.2f}") + if open_spend > hard: + print(f"\n HARD BREACH — ${open_spend:,.2f} > ${hard:,.2f}. Commit what " + f"works, or stop and decompose. Uncommitted work is also " + f"unattributable.", file=sys.stderr) + return 1 + if open_spend > soft: + print(f"\n soft breach — ${open_spend:,.2f} > ${soft:,.2f}. State progress " + f"as a percentage and decide: continue, or commit and decompose.") + return 0 + print("\n within budget") + return 0 + + def main(): ap = argparse.ArgumentParser() ap.add_argument("--slug", default="-home-worsch-clay-borg") @@ -489,6 +530,10 @@ def main(): ap.add_argument("--composition", action="store_true") ap.add_argument("--session-shape", action="store_true", help="SH-1..SH-3 (always shown in the default report)") + ap.add_argument("--budget", action="store_true", + help="CB-01/CB-02: spend since the last commit, live") + ap.add_argument("--soft", type=float, default=10.00) + ap.add_argument("--hard", type=float, default=22.00) ap.add_argument("--self-test", action="store_true") ap.add_argument("--json", action="store_true") args = ap.parse_args() @@ -496,6 +541,9 @@ def main(): if args.self_test: return self_test() + if args.budget: + return budget(args.slug, args.soft, args.hard) + try: rep = collect(args.slug, args.pin) except Abort as e: diff --git a/workplans/CB-WP-0003-loop-hardening.md b/workplans/CB-WP-0003-loop-hardening.md index 819c0dc..682a48c 100644 --- a/workplans/CB-WP-0003-loop-hardening.md +++ b/workplans/CB-WP-0003-loop-hardening.md @@ -203,7 +203,7 @@ attribution. ```task id: CB-WP-0003-T05 -status: todo +status: done priority: medium state_hub_task_id: "26c920ee-2c09-4b45-a908-4d343532db09" ```