chad-code 1.0.1__tar.gz → 1.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. {chad_code-1.0.1/src/chad_code.egg-info → chad_code-1.0.3}/PKG-INFO +15 -15
  2. {chad_code-1.0.1 → chad_code-1.0.3}/README.md +14 -14
  3. {chad_code-1.0.1 → chad_code-1.0.3}/pyproject.toml +1 -1
  4. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/__init__.py +1 -1
  5. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/agent.py +150 -20
  6. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/cli.py +17 -2
  7. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/guardrails.py +256 -43
  8. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/levers.py +40 -6
  9. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/prompt.py +15 -1
  10. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/syntaxgate.py +25 -4
  11. {chad_code-1.0.1 → chad_code-1.0.3/src/chad_code.egg-info}/PKG-INFO +15 -15
  12. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad_code.egg-info/SOURCES.txt +1 -0
  13. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_agent_e2e.py +40 -8
  14. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_agent_guards.py +165 -4
  15. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_done_audit.py +97 -0
  16. chad_code-1.0.3/tests/test_garble_invariant.py +86 -0
  17. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_intent.py +27 -0
  18. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_lever_bite.py +53 -6
  19. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_syntaxgate.py +25 -0
  20. {chad_code-1.0.1 → chad_code-1.0.3}/LICENSE +0 -0
  21. {chad_code-1.0.1 → chad_code-1.0.3}/setup.cfg +0 -0
  22. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/atif.py +0 -0
  23. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/base_engine.py +0 -0
  24. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/bench.py +0 -0
  25. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/compaction.py +0 -0
  26. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/completion_engine.py +0 -0
  27. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/config.py +0 -0
  28. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/diag.py +0 -0
  29. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/engine.py +0 -0
  30. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/ignore.py +0 -0
  31. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/lsp.py +0 -0
  32. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/mcp.py +0 -0
  33. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/mcp_oauth.py +0 -0
  34. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/mlx_fastpath.py +0 -0
  35. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/mlx_qsdpa.py +0 -0
  36. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/profiles.py +0 -0
  37. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/prove.py +0 -0
  38. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/render.py +0 -0
  39. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/repomap.py +0 -0
  40. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/session.py +0 -0
  41. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/skills.py +0 -0
  42. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/symbols.py +0 -0
  43. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/toolcall_parse.py +0 -0
  44. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/tools.py +0 -0
  45. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/tui.py +0 -0
  46. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/validate.py +0 -0
  47. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad_code.egg-info/dependency_links.txt +0 -0
  48. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad_code.egg-info/entry_points.txt +0 -0
  49. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad_code.egg-info/requires.txt +0 -0
  50. {chad_code-1.0.1 → chad_code-1.0.3}/src/chad_code.egg-info/top_level.txt +0 -0
  51. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_agent.py +0 -0
  52. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_atif.py +0 -0
  53. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_bench.py +0 -0
  54. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_cli.py +0 -0
  55. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_compact_notice.py +0 -0
  56. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_compaction.py +0 -0
  57. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_completion_engine.py +0 -0
  58. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_config.py +0 -0
  59. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_confirm_preview.py +0 -0
  60. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_drift_warn.py +0 -0
  61. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_edit.py +0 -0
  62. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_edit_corruption.py +0 -0
  63. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_engine.py +0 -0
  64. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_engine_kvquant.py +0 -0
  65. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_feel_pack.py +0 -0
  66. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_gate.py +0 -0
  67. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_ignore.py +0 -0
  68. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_levers.py +0 -0
  69. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_log_redaction.py +0 -0
  70. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_lsp.py +0 -0
  71. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_mcp.py +0 -0
  72. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_mcp_oauth.py +0 -0
  73. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_mlx_fastpath.py +0 -0
  74. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_mlx_qsdpa.py +0 -0
  75. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_plan_review.py +0 -0
  76. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_prove.py +0 -0
  77. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_render.py +0 -0
  78. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_replace_lines.py +0 -0
  79. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_repomap.py +0 -0
  80. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_session.py +0 -0
  81. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_skills.py +0 -0
  82. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_subagent.py +0 -0
  83. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_symbols.py +0 -0
  84. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_toolcall_parse.py +0 -0
  85. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_tools.py +0 -0
  86. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_tui.py +0 -0
  87. {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_validate.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chad-code
3
- Version: 1.0.1
3
+ Version: 1.0.3
4
4
  Summary: Local MLX-backed, Claude-Code-style coding agent (Apple Silicon, Ornith 35B/9B)
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/nathansutton/chad
@@ -39,7 +39,7 @@ Dynamic: license-file
39
39
 
40
40
  [![tests](https://github.com/nathansutton/chad/actions/workflows/tests.yml/badge.svg)](https://github.com/nathansutton/chad/actions/workflows/tests.yml)
41
41
 
42
- <img src="docs/tbench-size-vs-score.png" width="840" alt="Terminal-Bench 2.0: accuracy vs. model size. chad + Ornith (a 35B MoE) lands on the Claude Sonnet 4.5 line, matching open models many times its size and standing alone in the laptop-class ≤40B band.">
42
+ <img src="docs/tbench-size-vs-score.png" width="840" alt="Terminal-Bench 2.1: accuracy vs. cost per run. Every verified entry is a proprietary frontier model in a datacenter, costing $130–$2,000 per run. chad + Ornith (a 35B MoE) clears 57% on an Apple Silicon laptop for the electricity — the only no-API-cost point on the board.">
43
43
 
44
44
  > Claude can do anything, for anyone, anywhere. chad does one thing. 🗿
45
45
  > *Coding under supervision.*
@@ -86,20 +86,20 @@ instrument:
86
86
  > Real session, unedited (cold model load cut): a local 35B reasons through the failure,
87
87
  > edits the file, reruns the tests, confirms green.
88
88
 
89
- ## Sonnet on your laptop
89
+ ## Frontier scores, laptop cost
90
90
 
91
91
  The exam is [Terminal-Bench](https://www.tbench.ai/leaderboard), the standard benchmark for
92
- CLI coding agents. chad won't top it — the A students are frontier models in datacenters.
93
- The number worth looking at is **how much capability chad wrings out of 35B parameters**:
94
- on Terminal-Bench 2.1, chad + Ornith lands around the **Claude Sonnet 4.5** line, matching
95
- open models many times its size and beating every open model in its own weight class by a
96
- wide margin. On a laptop, *capability per parameter* is the axis you actually compete on —
97
- and that's where chad + Ornith is out on the frontier alone.
98
-
99
- > **Placeholder number** — the full verified run is still in flight (`≈50%`, k=1). The whole
100
- > benchmark is **publicly reproducible from a Mac**: the exact Harbor adapter, the runner,
101
- > and the recipe live in [`benchmarks/tb2/`](benchmarks/tb2/README.md). Check it, don't
102
- > trust it.
92
+ CLI coding agents. chad won't top it — every verified entry on Terminal-Bench 2.1 is a
93
+ frontier model in a datacenter, scoring 59–84%. The number worth looking at is what that
94
+ capability **costs**: the paid field spends **$130–$2,000 in API fees per run**. chad +
95
+ Ornith clears **57%** on an Apple Silicon laptop, for the electricity — no API, no tokens,
96
+ no datacenter. On a laptop, *capability per dollar* is the axis you actually compete on, and
97
+ chad is the only point on the board that runs there.
98
+
99
+ > **Provisional number** — `57%` (51/89, k=1, self-run, not yet leaderboard-verified). The
100
+ > whole benchmark is **publicly reproducible from a Mac**: the exact Harbor adapter, the
101
+ > runner, and the recipe live in [`benchmarks/tb2/`](benchmarks/tb2/README.md). Check it,
102
+ > don't trust it.
103
103
 
104
104
  ## The bet: at this end of the report card, the harness beats the model
105
105
 
@@ -214,7 +214,7 @@ Both are covered in full in the [Configuration reference](docs/configuration.md)
214
214
  cache, the trimmable/append-only trade, and the ideas borrowed from other agents.
215
215
  - **[Throughput & performance](docs/benchmarks.md)** — prefill / decode / warm-step numbers
216
216
  you can reproduce with `chad-bench`.
217
- - **[Terminal-Bench 2.0 reproduction](benchmarks/tb2/README.md)** — the exact Harbor adapter
217
+ - **[Terminal-Bench 2.1 reproduction](benchmarks/tb2/README.md)** — the exact Harbor adapter
218
218
  and runner behind the chart; serve Ornith yourself and check the number.
219
219
  - **[Configuration reference](docs/configuration.md)** — Agent Skills, MCP servers, the
220
220
  context window, every environment variable, and the safety opt-outs.
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![tests](https://github.com/nathansutton/chad/actions/workflows/tests.yml/badge.svg)](https://github.com/nathansutton/chad/actions/workflows/tests.yml)
4
4
 
5
- <img src="docs/tbench-size-vs-score.png" width="840" alt="Terminal-Bench 2.0: accuracy vs. model size. chad + Ornith (a 35B MoE) lands on the Claude Sonnet 4.5 line, matching open models many times its size and standing alone in the laptop-class ≤40B band.">
5
+ <img src="docs/tbench-size-vs-score.png" width="840" alt="Terminal-Bench 2.1: accuracy vs. cost per run. Every verified entry is a proprietary frontier model in a datacenter, costing $130–$2,000 per run. chad + Ornith (a 35B MoE) clears 57% on an Apple Silicon laptop for the electricity — the only no-API-cost point on the board.">
6
6
 
7
7
  > Claude can do anything, for anyone, anywhere. chad does one thing. 🗿
8
8
  > *Coding under supervision.*
@@ -49,20 +49,20 @@ instrument:
49
49
  > Real session, unedited (cold model load cut): a local 35B reasons through the failure,
50
50
  > edits the file, reruns the tests, confirms green.
51
51
 
52
- ## Sonnet on your laptop
52
+ ## Frontier scores, laptop cost
53
53
 
54
54
  The exam is [Terminal-Bench](https://www.tbench.ai/leaderboard), the standard benchmark for
55
- CLI coding agents. chad won't top it — the A students are frontier models in datacenters.
56
- The number worth looking at is **how much capability chad wrings out of 35B parameters**:
57
- on Terminal-Bench 2.1, chad + Ornith lands around the **Claude Sonnet 4.5** line, matching
58
- open models many times its size and beating every open model in its own weight class by a
59
- wide margin. On a laptop, *capability per parameter* is the axis you actually compete on —
60
- and that's where chad + Ornith is out on the frontier alone.
61
-
62
- > **Placeholder number** — the full verified run is still in flight (`≈50%`, k=1). The whole
63
- > benchmark is **publicly reproducible from a Mac**: the exact Harbor adapter, the runner,
64
- > and the recipe live in [`benchmarks/tb2/`](benchmarks/tb2/README.md). Check it, don't
65
- > trust it.
55
+ CLI coding agents. chad won't top it — every verified entry on Terminal-Bench 2.1 is a
56
+ frontier model in a datacenter, scoring 59–84%. The number worth looking at is what that
57
+ capability **costs**: the paid field spends **$130–$2,000 in API fees per run**. chad +
58
+ Ornith clears **57%** on an Apple Silicon laptop, for the electricity — no API, no tokens,
59
+ no datacenter. On a laptop, *capability per dollar* is the axis you actually compete on, and
60
+ chad is the only point on the board that runs there.
61
+
62
+ > **Provisional number** — `57%` (51/89, k=1, self-run, not yet leaderboard-verified). The
63
+ > whole benchmark is **publicly reproducible from a Mac**: the exact Harbor adapter, the
64
+ > runner, and the recipe live in [`benchmarks/tb2/`](benchmarks/tb2/README.md). Check it,
65
+ > don't trust it.
66
66
 
67
67
  ## The bet: at this end of the report card, the harness beats the model
68
68
 
@@ -177,7 +177,7 @@ Both are covered in full in the [Configuration reference](docs/configuration.md)
177
177
  cache, the trimmable/append-only trade, and the ideas borrowed from other agents.
178
178
  - **[Throughput & performance](docs/benchmarks.md)** — prefill / decode / warm-step numbers
179
179
  you can reproduce with `chad-bench`.
180
- - **[Terminal-Bench 2.0 reproduction](benchmarks/tb2/README.md)** — the exact Harbor adapter
180
+ - **[Terminal-Bench 2.1 reproduction](benchmarks/tb2/README.md)** — the exact Harbor adapter
181
181
  and runner behind the chart; serve Ornith yourself and check the number.
182
182
  - **[Configuration reference](docs/configuration.md)** — Agent Skills, MCP servers, the
183
183
  context window, every environment variable, and the safety opt-outs.
@@ -4,7 +4,7 @@
4
4
  # import name, and command name are independent. `uvx chad-code` runs the alias
5
5
  # script added under [project.scripts].
6
6
  name = "chad-code"
7
- version = "1.0.1"
7
+ version = "1.0.3"
8
8
  description = "Local MLX-backed, Claude-Code-style coding agent (Apple Silicon, Ornith 35B/9B)"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -4,4 +4,4 @@ A flat collection of cooperating modules behind one console script (``chad``):
4
4
  the inference engine, the tool layer, the agent loop, and the terminal UI.
5
5
  """
6
6
 
7
- __version__ = "1.0.1"
7
+ __version__ = "1.0.2"
@@ -786,6 +786,13 @@ class Agent:
786
786
  gate_nudges = 0 # times the investigation->edit gate fired this turn
787
787
  subagent_sigs = set() # (description, prompt) of sub-agents already spawned this turn
788
788
  truncation_nudges = 0 # times we pushed past a token-cap truncation this turn
789
+ garble_nudges = 0 # malformed-tool-call re-nudges (own counter, so a step-0
790
+ # cap-hit can't spend the garble recovery budget)
791
+ consecutive_garbles = 0 # back-to-back garbled steps (drives exemplar + scrub)
792
+ last_garble_idx = None # self.messages index of the previous garbled assistant
793
+ # message (scrubbed if the next step garbles too)
794
+ done_audit_bounces = 0 # total audit bounces this turn (hard cap 2)
795
+ audit_absent_list = [] # task-named paths the FIRST audit stat'ed as absent
789
796
  landing_nudges = 0 # one-shot "you're out of steps, land the edit" near the cap
790
797
  consecutive_failed_bash = 0 # back-to-back errored bash with no edit (thrash)
791
798
  thrash_nudges = 0
@@ -807,12 +814,18 @@ class Agent:
807
814
  # Turn-level cumulative think budget: distinct from self.think_tokens
808
815
  # (lifetime across the whole Agent instance, used for cross-turn telemetry) — this
809
816
  # is THIS turn's spend, reset every run_turn call, checked against a wall- and
810
- # decode-speed-aware budget. turn_think_exhausted is a PERSISTENT latch (unlike
811
- # capped_stall_streak's one-shot no_think_next above): once it fires, every
812
- # remaining step in the turn decodes with thinking off.
817
+ # decode-speed-aware budget. turn_think_exhausted latches the once-only
818
+ # log/steer; past it, no-think steps are paid on a duty cycle
819
+ # (guardrails.turn_think_throttle) rather than muting the rest of the turn —
820
+ # the blanket mute regressed run1 passes with garbled no-think tails (plan 107).
821
+ # landing_no_think is the 103 landing's own unconditional latch: once the hard
822
+ # wrap-up fires, the landing and everything after it stay no-think regardless.
813
823
  turn_think_tokens = 0
814
824
  turn_think_half_fired = False
815
825
  turn_think_exhausted = False
826
+ turn_think_nothink_paid = 0
827
+ turn_think_budget_now = 0
828
+ landing_no_think = False
816
829
  # Runaway-turn governor state. turn_start drives the optional wall
817
830
  # budget; gov_band is the highest budget checkpoint already evaluated; gov_progress
818
831
  # tracks whether a change landed+verified within the CURRENT band (resets each time
@@ -845,6 +858,12 @@ class Agent:
845
858
  # where it also selects the plan-mode prefix.)
846
859
  action_task = self.mode != "plan" and _intent["action"]
847
860
  read_only_intent = _intent["read_only"]
861
+ # Run-task intent (start/boot/serve/… — system-state imperatives with no file
862
+ # deliverable): arms the anti-bail nudges alongside action_task but is kept OUT
863
+ # of the no-empty-diff done gates below, which demand a landed edit a run task
864
+ # legitimately never makes (plan 107 follow-up: qemu-startup classified as
865
+ # neither, so a prose give-up with 81% of the wall left took the weakest path).
866
+ run_task = self.mode != "plan" and _intent.get("run", False)
848
867
  # Progress-aware step cap (see guardrails.extend_step_cap): max_steps is the
849
868
  # WINDOW size, not a hard kill. A window that landed+verified a change earns
850
869
  # another window (warm cache — no re-prefill, unlike a governor rollover); a
@@ -963,9 +982,19 @@ class Agent:
963
982
  # <think> off so the model must act, then the flag clears and thinking restores.
964
983
  # Consumes the one-shot here; the render/decode/accounting below all key off
965
984
  # `step_thinking` rather than self.thinking for exactly this step.
966
- # Turn-level think budget: once exhausted, thinking stays off for
967
- # every remaining step — a persistent override, unlike no_think_next below.
968
- step_thinking = self.thinking and not turn_think_exhausted
985
+ # Turn-level think budget: past exhaustion, forced no-think steps are
986
+ # paid on a duty cycle (one per TURN_THINK_REARM_TOK further think tokens)
987
+ # so thinking RESTORES once the model stops over-spending — a blanket
988
+ # rest-of-turn mute regressed run1 passes (plan 107 F1). The 103 landing's
989
+ # landing_no_think stays unconditional.
990
+ _tt_throttled = (turn_think_exhausted
991
+ and guardrails.turn_think_throttle(
992
+ turn_think_tokens, turn_think_budget_now,
993
+ turn_think_nothink_paid))
994
+ if _tt_throttled:
995
+ turn_think_nothink_paid += 1
996
+ step_thinking = (self.thinking and not landing_no_think
997
+ and not _tt_throttled)
969
998
  if no_think_next:
970
999
  no_think_next = False
971
1000
  capped_stall_streak = 0
@@ -1203,9 +1232,15 @@ class Agent:
1203
1232
  # turns (a Q&A turn shouldn't be pushed to "act now") and without a configured
1204
1233
  # wall budget (interactive/unmetered runs, like wrapup_window above).
1205
1234
  turn_think_tokens += _think_delta
1206
- if (self._turn_budget_s and self.mode != "plan" and not read_only_intent
1235
+ # Inert below TURN_THINK_MIN_WALL_S: a short auto-continue tail clamps to
1236
+ # the LO budget and half-fires on its first step, churning against
1237
+ # hard_wrapup's landing (plan 107 F2 — the regex-log relaunch signature).
1238
+ if (self._turn_budget_s
1239
+ and self._turn_budget_s >= guardrails.TURN_THINK_MIN_WALL_S
1240
+ and self.mode != "plan" and not read_only_intent
1207
1241
  and levers.enabled("turn_think_budget")):
1208
1242
  _tt_budget = guardrails.turn_think_budget(self._turn_budget_s, self.tok_per_s)
1243
+ turn_think_budget_now = _tt_budget
1209
1244
  _tt_decision, turn_think_half_fired, turn_think_exhausted = (
1210
1245
  guardrails.turn_think_budget_check(
1211
1246
  turn_think_tokens, _tt_budget,
@@ -1217,10 +1252,11 @@ class Agent:
1217
1252
  "content": guardrails.TURN_THINK_BUDGET_STEER})
1218
1253
  elif _tt_decision == "exhausted":
1219
1254
  log.info("THINK-BUDGET exhausted at step %d: %d/%d cumulative think "
1220
- "tok this turn — no-think for the rest of the turn", step,
1221
- turn_think_tokens, _tt_budget)
1255
+ "tok this turn — throttling <think> (one action step per "
1256
+ "%d further think tok)", step, turn_think_tokens,
1257
+ _tt_budget, guardrails.TURN_THINK_REARM_TOK)
1222
1258
  self._emit("info", " [reasoning budget exhausted for this turn — "
1223
- "acting without further <think> for the rest of it]")
1259
+ "throttling further <think>]")
1224
1260
 
1225
1261
  log.info("step %d: %d tok @ %.1f tok/s | prefill %d new + %d cached | "
1226
1262
  "accept %.2f", step, stats.generated_tokens, stats.tok_per_s,
@@ -1277,7 +1313,7 @@ class Agent:
1277
1313
  # above) with its tokens in the KV cache. Force ONE time-boxed, no-think
1278
1314
  # landing turn so the model writes its best artifacts to the exact paths before
1279
1315
  # the harness kills the turn at the wall — instead of dying mid-token with
1280
- # nothing landed. turn_think_exhausted makes the landing (and any step after it)
1316
+ # nothing landed. landing_no_think makes the landing (and any step after it)
1281
1317
  # no-think so it can't re-open a spiral; the latch disarms _deadline_stop, so
1282
1318
  # the landing generation runs to its token box, not another abort. deadline_fired
1283
1319
  # is exclusive with rep_fired / the think-cap below (it is checked first in
@@ -1286,7 +1322,7 @@ class Agent:
1286
1322
  # there is rarely one, and partial-tool-call surgery is not worth its risk here.
1287
1323
  if deadline_fired[0] and not hard_wrapup_fired:
1288
1324
  hard_wrapup_fired = True
1289
- turn_think_exhausted = True
1325
+ landing_no_think = True
1290
1326
  _remaining = self._turn_budget_s - (time.monotonic() - turn_start)
1291
1327
  log.info("HARD-WRAPUP abort at step %d: %.0fs left, gen was %d tok",
1292
1328
  step, _remaining, stats.generated_tokens)
@@ -1371,12 +1407,33 @@ class Agent:
1371
1407
  _stripped_for_markers = strip_think(text)
1372
1408
  garbled = ("<tool_call>" in _stripped_for_markers
1373
1409
  or "</function>" in _stripped_for_markers
1374
- or "<function=" in _stripped_for_markers)
1410
+ or "<function=" in _stripped_for_markers
1411
+ or _has_open_tool_call(text))
1412
+ if garbled:
1413
+ consecutive_garbles += 1
1414
+ # From the 2nd consecutive garble, scrub the PREVIOUS garbled
1415
+ # assistant body: the wrong dialect must not stay in context as a
1416
+ # few-shot example (repeated garbles in a row each condition on the
1417
+ # last). Costs one prefix-cache invalidation on this rare path.
1418
+ if (levers.enabled("garble_never_final")
1419
+ and consecutive_garbles >= 2 and last_garble_idx is not None):
1420
+ self.messages[last_garble_idx]["content"] = \
1421
+ guardrails.GARBLE_SCRUBBED
1422
+ log.info("GARBLE scrub at step %d: previous garbled message "
1423
+ "body removed from context (consecutive=%d)",
1424
+ step, consecutive_garbles)
1425
+ last_garble_idx = len(self.messages) - 1
1426
+ else:
1427
+ consecutive_garbles = 0
1375
1428
  kind, nudge = guardrails.nudge_for_no_calls(
1376
1429
  text, hit_cap, made_edit, unverified_edit, read_only_intent,
1377
- action_task, truncation_nudges, answer_nudges, verify_nudges,
1378
- _has_open_tool_call(text), garbled_call=garbled)
1379
- if kind == "truncated":
1430
+ action_task or run_task, truncation_nudges, answer_nudges,
1431
+ verify_nudges, _has_open_tool_call(text), garbled_call=garbled,
1432
+ garble_nudges=garble_nudges,
1433
+ consecutive_garbles=consecutive_garbles)
1434
+ if kind == "garble":
1435
+ garble_nudges += 1
1436
+ elif kind == "truncated":
1380
1437
  truncation_nudges += 1
1381
1438
  elif kind == "no-edit":
1382
1439
  answer_nudges += 1
@@ -1384,9 +1441,25 @@ class Agent:
1384
1441
  verify_nudges += 1
1385
1442
  if nudge:
1386
1443
  log.info("END-ANSWER rejected step %d: %s (hit_cap=%s, has_code=%s, "
1387
- "action_task=%s)", step, kind, hit_cap, has_code, action_task)
1444
+ "action_task=%s, run_task=%s)", step, kind, hit_cap,
1445
+ has_code, action_task, run_task)
1388
1446
  self.messages.append({"role": "tool", "name": "edit", "content": nudge})
1389
1447
  continue
1448
+ # Invariant: a step whose text contains tool-call markers is NEVER a
1449
+ # final answer — the model was trying to act, not to finish. With the
1450
+ # nudge budget spent, hard-stop with a banked note instead of accepting
1451
+ # the garble (otherwise a run of garbles can burn the shared counter and
1452
+ # the audit latch, and the last one ships as the answer with most of the
1453
+ # wall budget still unspent).
1454
+ if garbled and levers.enabled("garble_never_final"):
1455
+ self.budget_note = guardrails.progress_note(self.messages)
1456
+ log.info("END step %d: GARBLE hard-stop (%d garble nudges spent) — "
1457
+ "a garbled tool call is never a final answer",
1458
+ step, garble_nudges)
1459
+ self._emit("info", " [turn stopped: repeated malformed tool calls "
1460
+ "— progress note banked; say 'continue' to retry]")
1461
+ return ("[stopped: the model kept emitting malformed tool calls "
1462
+ "— say 'continue' to resume]")
1390
1463
  # Iter-3 did-nothing gate: in auto/headless mode (every benchmark run), a
1391
1464
  # turn that ends having executed ZERO real tools is never a legitimate
1392
1465
  # completion — the keyword intent classifier misses tasks like "extract the
@@ -1405,7 +1478,9 @@ class Agent:
1405
1478
  # and end as a hard stop, so --auto-continue (headless) or the
1406
1479
  # user's 'continue' (TUI) relaunches a fresh attempt with the
1407
1480
  # note instead of silently shipping nothing.
1408
- self.budget_note = guardrails.progress_note(self.messages)
1481
+ self.budget_note = guardrails.progress_note(
1482
+ self.messages,
1483
+ rejected_claim=strip_think(text).strip())
1409
1484
  log.info("END step %d: FINAL ANSWER blocked by no-empty-diff gate "
1410
1485
  "(made_edit=%s, unverified_edit=%s) — progress note banked",
1411
1486
  step, made_edit, unverified_edit)
@@ -1433,6 +1508,8 @@ class Agent:
1433
1508
  })
1434
1509
  if audit:
1435
1510
  done_audit_fired = True
1511
+ done_audit_bounces += 1
1512
+ audit_absent_list = guardrails.audit_absent_paths(audit_task)
1436
1513
  _runway = ((self._turn_budget_s
1437
1514
  - (time.monotonic() - turn_start))
1438
1515
  if self._turn_budget_s else float("inf"))
@@ -1442,6 +1519,26 @@ class Agent:
1442
1519
  self.messages.append({"role": "tool", "name": "edit",
1443
1520
  "content": audit})
1444
1521
  continue
1522
+ # Absent-path re-bounce: the first audit stat'ed task-named paths as
1523
+ # ABSENT and promised acceptance anyway; if they are STILL absent with
1524
+ # runway to spare, bounce one final time (cap 2/turn) — otherwise a
1525
+ # task-named deliverable the audit already flagged absent can go
1526
+ # unwritten and the accept still goes through.
1527
+ if self.mode != "plan" and not self._subagent \
1528
+ and done_audit_fired and done_audit_bounces < 2 \
1529
+ and audit_absent_list:
1530
+ _runway = ((self._turn_budget_s
1531
+ - (time.monotonic() - turn_start))
1532
+ if self._turn_budget_s else None)
1533
+ rebounce = guardrails.audit_rebounce(audit_absent_list, _runway)
1534
+ if rebounce:
1535
+ done_audit_bounces += 1
1536
+ audit_absent_list = []
1537
+ log.info("DONE-AUDIT rebounce (final-answer): still-absent "
1538
+ "task paths, runway=%s", _runway)
1539
+ self.messages.append({"role": "tool", "name": "edit",
1540
+ "content": rebounce})
1541
+ continue
1445
1542
  log.info("END step %d: model produced a FINAL ANSWER, no tool calls "
1446
1543
  "(did_work=%s, made_edit=%s, unverified_edit=%s)",
1447
1544
  step, did_work, made_edit, unverified_edit)
@@ -1451,6 +1548,8 @@ class Agent:
1451
1548
  # The step acted — the spiral broke on its own, so clear the no-think escalation
1452
1549
  # streak: escalation is only for consecutive capped/salvaged stalls.
1453
1550
  capped_stall_streak = 0
1551
+ consecutive_garbles = 0 # a parsed call ends the garble streak
1552
+ last_garble_idx = None
1454
1553
 
1455
1554
  # Terminal tool -> end the turn cleanly, but enforce verify-before-done
1456
1555
  # (forge's prerequisite idea): if files were changed and nothing has been
@@ -1497,7 +1596,9 @@ class Agent:
1497
1596
  # nudges ran out) becomes a resumable hard stop, not a success
1498
1597
  # (matplotlib-25332 r3: done accepted at 84s with edits in tree
1499
1598
  # and zero successful post-edit commands).
1500
- self.budget_note = guardrails.progress_note(self.messages)
1599
+ self.budget_note = guardrails.progress_note(
1600
+ self.messages,
1601
+ rejected_claim=str(terminal.get("summary") or ""))
1501
1602
  log.info("END step %d: DONE blocked by no-empty-diff gate "
1502
1603
  "(made_edit=%s, unverified_edit=%s) — progress note banked",
1503
1604
  step, made_edit, unverified_edit)
@@ -1540,6 +1641,8 @@ class Agent:
1540
1641
  })
1541
1642
  if audit:
1542
1643
  done_audit_fired = True
1644
+ done_audit_bounces += 1
1645
+ audit_absent_list = guardrails.audit_absent_paths(audit_task)
1543
1646
  _runway = ((self._turn_budget_s
1544
1647
  - (time.monotonic() - turn_start))
1545
1648
  if self._turn_budget_s else float("inf"))
@@ -1548,6 +1651,22 @@ class Agent:
1548
1651
  self.messages.append({"role": "tool", "name": "done",
1549
1652
  "content": audit})
1550
1653
  continue
1654
+ # Absent-path re-bounce (see the final-answer twin above): a
1655
+ # still-absent task-named path at accept time gets ONE more bounce,
1656
+ # then the next done is accepted unconditionally.
1657
+ if done_audit_fired and done_audit_bounces < 2 and audit_absent_list:
1658
+ _runway = ((self._turn_budget_s
1659
+ - (time.monotonic() - turn_start))
1660
+ if self._turn_budget_s else None)
1661
+ rebounce = guardrails.audit_rebounce(audit_absent_list, _runway)
1662
+ if rebounce:
1663
+ done_audit_bounces += 1
1664
+ audit_absent_list = []
1665
+ log.info("DONE-AUDIT rebounce: still-absent task paths, "
1666
+ "runway=%s", _runway)
1667
+ self.messages.append({"role": "tool", "name": "done",
1668
+ "content": rebounce})
1669
+ continue
1551
1670
  # Iter-3 deliverable recheck (levers.done_spec_recheck): one last self-check
1552
1671
  # that the required outputs actually exist at the right path/format before
1553
1672
  # we accept done — the hidden container-end-state verifier gives no second
@@ -1820,7 +1939,18 @@ class Agent:
1820
1939
  if made_edit and not _gov_prev_made:
1821
1940
  readonly_streak = 0
1822
1941
  elif did_work and not made_edit:
1823
- readonly_streak += 1
1942
+ # A bash step that isn't provably read-only is ACTION, not
1943
+ # investigation — `git merge`, `apt-get install`, redirects are ops
1944
+ # progress. Otherwise the gate can count an entire git/ops workflow as
1945
+ # "investigation" and demand an edit at a decision point where there is
1946
+ # nothing to edit yet.
1947
+ if levers.enabled("gate_ops_exempt") and any(
1948
+ n == "bash"
1949
+ and not guardrails.is_readonly_bash(str(a.get("command", "")))
1950
+ for n, a in calls):
1951
+ readonly_streak = 0
1952
+ else:
1953
+ readonly_streak += 1
1824
1954
  if not read_only_intent:
1825
1955
  gate = guardrails.investigation_gate(readonly_streak, made_edit, gate_nudges)
1826
1956
  if gate:
@@ -637,9 +637,24 @@ def main():
637
637
  # ships as an empty diff (the NIGHT-7 bail signature).
638
638
  continues = args.auto_continue if args.auto_continue is not None \
639
639
  else (2 if run_mode == "auto" else 0)
640
- while agent.budget_note and continues > 0:
640
+ used_continues = 0
641
+ while agent.budget_note:
642
+ # Base allowance first; past it, keep granting fresh attempts while most
643
+ # of the task wall is still unspent (bounded by AUTO_CONTINUE_TOTAL_CAP) —
644
+ # the fixed base is wall-blind: build-pov-ray (TB2.1 v1.0.0) gave up after
645
+ # 3 step-capped turns with 94.7% of a 12000s budget unused (plan 107 F3).
646
+ if continues > 0:
647
+ continues -= 1
648
+ elif (args.auto_continue is None and args.turn_budget_s
649
+ and guardrails.replenish_continue(
650
+ args.turn_budget_s, time.monotonic() - task_start,
651
+ used_continues)):
652
+ sys.stderr.write("[governor] wall budget mostly unspent — granting an "
653
+ "extra continue\n")
654
+ else:
655
+ break
656
+ used_continues += 1
641
657
  note = agent.budget_note
642
- continues -= 1
643
658
  # The wall budget is a TASK-level deadline (the harness SIGKILLs the whole chad
644
659
  # process), not a per-turn one: a relaunch must inherit only the wall time that
645
660
  # REMAINS, or the governor / wrap-up / hard-abort windows (measured from each