chad-code 1.0.9__tar.gz → 1.12.0__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 (119) hide show
  1. {chad_code-1.0.9 → chad_code-1.12.0}/PKG-INFO +1 -1
  2. {chad_code-1.0.9 → chad_code-1.12.0}/pyproject.toml +1 -1
  3. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/__init__.py +1 -1
  4. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/agent.py +76 -5
  5. chad_code-1.12.0/src/chad/ambient.py +453 -0
  6. chad_code-1.12.0/src/chad/checkpoint.py +150 -0
  7. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/cli.py +5 -4
  8. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/compaction.py +4 -0
  9. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/guardrails.py +38 -7
  10. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/levers.py +180 -30
  11. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/profiles.py +1 -5
  12. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/prompt.py +20 -0
  13. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/render.py +3 -1
  14. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/repomap.py +100 -0
  15. chad_code-1.12.0/src/chad/seatbelt.py +255 -0
  16. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/symbols.py +4 -1
  17. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/syntaxgate.py +7 -0
  18. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/tools.py +87 -8
  19. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/tui.py +30 -1
  20. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad_code.egg-info/PKG-INFO +1 -1
  21. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad_code.egg-info/SOURCES.txt +7 -0
  22. chad_code-1.12.0/tests/test_ambient.py +277 -0
  23. chad_code-1.12.0/tests/test_checkpoint.py +132 -0
  24. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_lever_bite.py +118 -0
  25. chad_code-1.12.0/tests/test_lever_instrumentation.py +95 -0
  26. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_levers.py +37 -11
  27. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_repomap.py +87 -0
  28. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_repomap_polyglot.py +31 -0
  29. chad_code-1.12.0/tests/test_seatbelt.py +336 -0
  30. {chad_code-1.0.9 → chad_code-1.12.0}/LICENSE +0 -0
  31. {chad_code-1.0.9 → chad_code-1.12.0}/README.md +0 -0
  32. {chad_code-1.0.9 → chad_code-1.12.0}/setup.cfg +0 -0
  33. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/atif.py +0 -0
  34. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/base_engine.py +0 -0
  35. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/bench.py +0 -0
  36. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/completion_engine.py +0 -0
  37. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/config.py +0 -0
  38. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/diag.py +0 -0
  39. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/engine.py +0 -0
  40. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/ignore.py +0 -0
  41. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/lsp.py +0 -0
  42. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/lspclient.py +0 -0
  43. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/lspservers.py +0 -0
  44. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/mcp.py +0 -0
  45. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/mcp_oauth.py +0 -0
  46. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/mlx_fastpath.py +0 -0
  47. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/mlx_moe_fused.py +0 -0
  48. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/mlx_qsdpa.py +0 -0
  49. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/parakeet/LICENSE +0 -0
  50. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/parakeet/__init__.py +0 -0
  51. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/parakeet/alignment.py +0 -0
  52. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/parakeet/attention.py +0 -0
  53. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/parakeet/audio.py +0 -0
  54. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/parakeet/cache.py +0 -0
  55. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/parakeet/conformer.py +0 -0
  56. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/parakeet/ctc.py +0 -0
  57. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/parakeet/parakeet.py +0 -0
  58. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/parakeet/rnnt.py +0 -0
  59. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/parakeet/tokenizer.py +0 -0
  60. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/parakeet/utils.py +0 -0
  61. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/prove.py +0 -0
  62. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/serve.py +0 -0
  63. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/session.py +0 -0
  64. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/skills.py +0 -0
  65. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/speech.py +0 -0
  66. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/toolcall_parse.py +0 -0
  67. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad/validate.py +0 -0
  68. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad_code.egg-info/dependency_links.txt +0 -0
  69. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad_code.egg-info/entry_points.txt +0 -0
  70. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad_code.egg-info/requires.txt +0 -0
  71. {chad_code-1.0.9 → chad_code-1.12.0}/src/chad_code.egg-info/top_level.txt +0 -0
  72. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_agent.py +0 -0
  73. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_agent_e2e.py +0 -0
  74. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_agent_guards.py +0 -0
  75. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_atif.py +0 -0
  76. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_bench.py +0 -0
  77. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_cli.py +0 -0
  78. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_compact_notice.py +0 -0
  79. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_compaction.py +0 -0
  80. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_completion_engine.py +0 -0
  81. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_config.py +0 -0
  82. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_confirm_preview.py +0 -0
  83. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_done_audit.py +0 -0
  84. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_drift_warn.py +0 -0
  85. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_edit.py +0 -0
  86. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_edit_corruption.py +0 -0
  87. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_engine.py +0 -0
  88. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_engine_kvquant.py +0 -0
  89. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_engine_pld_hybrid.py +0 -0
  90. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_feel_pack.py +0 -0
  91. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_garble_invariant.py +0 -0
  92. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_gate.py +0 -0
  93. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_ignore.py +0 -0
  94. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_intent.py +0 -0
  95. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_log_redaction.py +0 -0
  96. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_lsp.py +0 -0
  97. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_lsp_live.py +0 -0
  98. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_lspclient.py +0 -0
  99. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_mcp.py +0 -0
  100. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_mcp_oauth.py +0 -0
  101. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_mlx_fastpath.py +0 -0
  102. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_mlx_moe_fused.py +0 -0
  103. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_mlx_qsdpa.py +0 -0
  104. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_plan_review.py +0 -0
  105. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_prove.py +0 -0
  106. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_render.py +0 -0
  107. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_replace_lines.py +0 -0
  108. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_serve.py +0 -0
  109. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_session.py +0 -0
  110. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_skills.py +0 -0
  111. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_speech.py +0 -0
  112. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_speech_tui.py +0 -0
  113. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_subagent.py +0 -0
  114. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_symbols.py +0 -0
  115. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_syntaxgate.py +0 -0
  116. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_toolcall_parse.py +0 -0
  117. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_tools.py +0 -0
  118. {chad_code-1.0.9 → chad_code-1.12.0}/tests/test_tui.py +0 -0
  119. {chad_code-1.0.9 → chad_code-1.12.0}/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.9
3
+ Version: 1.12.0
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
@@ -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.9"
7
+ version = "1.12.0"
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,7 +4,7 @@ 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.9"
7
+ __version__ = "1.12.0"
8
8
 
9
9
  # chad sets no MLX_* runtime vars. MLX_METAL_FAST_SYNCH, MLX_MAX_OPS_PER_BUFFER
10
10
  # and MLX_MAX_MB_PER_BUFFER were each measured end-to-end on the 35B and every
@@ -14,7 +14,18 @@ import re
14
14
  import sys
15
15
  import time
16
16
 
17
- from . import atif, compaction, config, guardrails, levers, session, syntaxgate
17
+ from . import (
18
+ ambient,
19
+ atif,
20
+ checkpoint,
21
+ compaction,
22
+ config,
23
+ guardrails,
24
+ levers,
25
+ seatbelt,
26
+ session,
27
+ syntaxgate,
28
+ )
18
29
  from .base_engine import BackendError, BaseEngine
19
30
  from .diag import args_preview, log, redact, result_preview
20
31
  from .prompt import build_subagent_prompt, build_system_prompt, classify_intent
@@ -47,7 +58,7 @@ SUBAGENT_MAX_STEPS = 24
47
58
  SUBAGENT_CTX_LIMIT = 32000
48
59
  SUBAGENT_READ_ONLY = {
49
60
  "read", "grep", "glob", "repo_map", "overview", "view_symbol",
50
- "find_symbol", "find_refs", "done", "finish", "stop",
61
+ "find_symbol", "definition", "find_refs", "done", "finish", "stop",
51
62
  }
52
63
  # `write_todos` is deliberately absent: a sub-agent that plans its own work would mutate
53
64
  # the process-global `_TODOS` and clobber the parent's pinned todo panel (and `_sub_emit`
@@ -346,6 +357,8 @@ class Agent:
346
357
  skills.reset_session()
347
358
  from . import mcp
348
359
  mcp.reset_session()
360
+ ambient.reset() # fresh session-state ledger; sub-agents share the
361
+ # parent's and never feed it (see run_turn)
349
362
  self.mode = mode or ("yolo" if yolo else "normal")
350
363
  self.thinking = thinking # Ornith is a reasoning model; toggles <think> blocks
351
364
  # Steps per WINDOW, not a hard kill: a window that landed+verified a change
@@ -625,6 +638,7 @@ class Agent:
625
638
  # measured trace: a model re-phrasing the same delete 30 times). The
626
639
  # guard names itself and the fix — narrow the target.
627
640
  if levers.enabled("scoped_destructive_guard"):
641
+ levers.fired("scoped_destructive_guard", deny_text=True)
628
642
  self._deny_reason = (
629
643
  "[blocked by the destructive-command guard, not by a person: "
630
644
  "the command matches a catastrophic pattern (recursive delete "
@@ -722,6 +736,7 @@ class Agent:
722
736
  note = sub.budget_note or guardrails.progress_note(sub.messages)
723
737
  if not note:
724
738
  return res
739
+ levers.fired("subagent_budget_note")
725
740
  return (res.rstrip() + "\n[sub-agent progress before it "
726
741
  f"stopped: {note}]").strip()
727
742
  if sub.interrupted:
@@ -1196,6 +1211,7 @@ class Agent:
1196
1211
  or self._backend_retries >= _MAX_BACKEND_RETRIES):
1197
1212
  raise
1198
1213
  self._backend_retries += 1
1214
+ levers.fired("backend_retry", attempt=self._backend_retries)
1199
1215
  log.warning("backend error (retry %d/%d): %s",
1200
1216
  self._backend_retries, _MAX_BACKEND_RETRIES, e)
1201
1217
  self._emit("status", f"Backend error; retrying "
@@ -1285,6 +1301,8 @@ class Agent:
1285
1301
  # the first branch already credits; an unclosed generation that
1286
1302
  # stopped SHORT of the cap is a truncation of some other kind, not
1287
1303
  # the reasoning overspend this counts.
1304
+ levers.fired("capped_think_credit", step=step,
1305
+ tokens=stats.generated_tokens)
1288
1306
  _think_delta = stats.generated_tokens
1289
1307
  else:
1290
1308
  _think_delta = 0
@@ -1312,6 +1330,7 @@ class Agent:
1312
1330
  if _tt_decision == "half":
1313
1331
  log.info("THINK-BUDGET half at step %d: %d/%d cumulative think tok "
1314
1332
  "this turn", step, turn_think_tokens, _tt_budget)
1333
+ levers.fired("turn_think_budget", step=step, decision="half")
1315
1334
  self.messages.append({"role": "tool", "name": "steer",
1316
1335
  "content": guardrails.TURN_THINK_BUDGET_STEER})
1317
1336
  elif _tt_decision == "exhausted":
@@ -1319,6 +1338,7 @@ class Agent:
1319
1338
  "tok this turn — throttling <think> (one action step per "
1320
1339
  "%d further think tok)", step, turn_think_tokens,
1321
1340
  _tt_budget, guardrails.TURN_THINK_REARM_TOK)
1341
+ levers.fired("turn_think_budget", step=step, decision="exhausted")
1322
1342
  self._emit("info", " [reasoning budget exhausted for this turn — "
1323
1343
  "throttling further <think>]")
1324
1344
 
@@ -1388,6 +1408,8 @@ class Agent:
1388
1408
  hard_wrapup_fired = True
1389
1409
  landing_no_think = True
1390
1410
  _remaining = self._turn_budget_s - (time.monotonic() - turn_start)
1411
+ levers.fired("hard_wrapup", step=step,
1412
+ remaining_s=int(_remaining))
1391
1413
  log.info("HARD-WRAPUP abort at step %d: %.0fs left, gen was %d tok",
1392
1414
  step, _remaining, stats.generated_tokens)
1393
1415
  self._emit("info", " [wall deadline reached — landing the best answer now]")
@@ -1452,6 +1474,7 @@ class Agent:
1452
1474
  capped_stall_streak += 1
1453
1475
  if (self.think_ceiling and self.thinking and capped_stall_streak >= 2
1454
1476
  and levers.enabled("no_think_escalation")):
1477
+ levers.fired("no_think_escalation", step=step)
1455
1478
  no_think_next = True
1456
1479
  else:
1457
1480
  capped_stall_streak = 0
@@ -1481,6 +1504,7 @@ class Agent:
1481
1504
  # last). Costs one prefix-cache invalidation on this rare path.
1482
1505
  if (levers.enabled("garble_never_final")
1483
1506
  and consecutive_garbles >= 2 and last_garble_idx is not None):
1507
+ levers.fired("garble_never_final", step=step, action="scrub")
1484
1508
  self.messages[last_garble_idx]["content"] = \
1485
1509
  guardrails.GARBLE_SCRUBBED
1486
1510
  log.info("GARBLE scrub at step %d: previous garbled message "
@@ -1516,6 +1540,7 @@ class Agent:
1516
1540
  # the audit latch, and the last one ships as the answer with most of the
1517
1541
  # wall budget still unspent).
1518
1542
  if garbled and levers.enabled("garble_never_final"):
1543
+ levers.fired("garble_never_final", step=step, action="hard-stop")
1519
1544
  self.budget_note = guardrails.progress_note(self.messages)
1520
1545
  log.info("END step %d: GARBLE hard-stop (%d garble nudges spent) — "
1521
1546
  "a garbled tool call is never a final answer",
@@ -1558,6 +1583,8 @@ class Agent:
1558
1583
  if audit:
1559
1584
  done_audit_fired = True
1560
1585
  done_audit_bounces += 1
1586
+ levers.fired("done_audit", step=step, entry="churn-handoff")
1587
+ levers.fired("audit_churn_handoff", step=step)
1561
1588
  audit_absent_list = guardrails.audit_absent_paths(audit_task)
1562
1589
  _runway = ((self._turn_budget_s
1563
1590
  - (time.monotonic() - turn_start))
@@ -1607,6 +1634,7 @@ class Agent:
1607
1634
  if audit:
1608
1635
  done_audit_fired = True
1609
1636
  done_audit_bounces += 1
1637
+ levers.fired("done_audit", step=step, entry="final-answer")
1610
1638
  audit_absent_list = guardrails.audit_absent_paths(audit_task)
1611
1639
  _runway = ((self._turn_budget_s
1612
1640
  - (time.monotonic() - turn_start))
@@ -1681,10 +1709,15 @@ class Agent:
1681
1709
  log.info("DONE rejected: edits not verified -> nudge #%d", verify_nudges)
1682
1710
  self.messages.append({
1683
1711
  "role": "tool", "name": "done",
1712
+ # The session-ledger facts (what changed, what last ran) ride
1713
+ # the bounce when the lever is on — the model's stale state
1714
+ # model IS the wrong-verify bug.
1684
1715
  "content": "[not done yet: you changed files but have not run anything "
1685
1716
  "to verify them. Run the project's tests (or the code) with "
1686
1717
  "bash, check the output is correct, then call done. If a test "
1687
- "fails, fix the code first.]",
1718
+ "fails, fix the code first.]"
1719
+ + ("" if self._subagent else
1720
+ ambient.ledger_suffix(step=step, force=True)),
1688
1721
  })
1689
1722
  continue
1690
1723
  if action_task and not read_only_intent and self.mode != "plan" \
@@ -1706,6 +1739,9 @@ class Agent:
1706
1739
  if audit:
1707
1740
  done_audit_fired = True
1708
1741
  done_audit_bounces += 1
1742
+ levers.fired("done_audit", step=step,
1743
+ entry="churn-handoff-done")
1744
+ levers.fired("audit_churn_handoff", step=step)
1709
1745
  audit_absent_list = guardrails.audit_absent_paths(audit_task)
1710
1746
  _runway = ((self._turn_budget_s
1711
1747
  - (time.monotonic() - turn_start))
@@ -1768,14 +1804,18 @@ class Agent:
1768
1804
  if audit:
1769
1805
  done_audit_fired = True
1770
1806
  done_audit_bounces += 1
1807
+ levers.fired("done_audit", step=step, entry="done")
1771
1808
  audit_absent_list = guardrails.audit_absent_paths(audit_task)
1772
1809
  _runway = ((self._turn_budget_s
1773
1810
  - (time.monotonic() - turn_start))
1774
1811
  if self._turn_budget_s else float("inf"))
1775
1812
  log.info("DONE-AUDIT bounce: paths=%s runway=%.0fs",
1776
1813
  guardrails.audit_extract_paths(audit_task), _runway)
1777
- self.messages.append({"role": "tool", "name": "done",
1778
- "content": audit})
1814
+ self.messages.append({
1815
+ "role": "tool", "name": "done",
1816
+ # Session-ledger facts ride the audit bounce
1817
+ "content": audit
1818
+ + ambient.ledger_suffix(step=step, force=True)})
1779
1819
  continue
1780
1820
  # Absent-path re-bounce (see the final-answer twin above): a
1781
1821
  # still-absent task-named path at accept time gets ONE more bounce,
@@ -1802,6 +1842,7 @@ class Agent:
1802
1842
  and guardrails.done_spec_recheck(
1803
1843
  did_work, unverified_edit, done_recheck_done, read_only_intent):
1804
1844
  done_recheck_done = True
1845
+ levers.fired("done_spec_recheck", step=step)
1805
1846
  log.info("DONE deferred for deliverable recheck (step %d)", step)
1806
1847
  self.messages.append({"role": "tool", "name": "done",
1807
1848
  "content": guardrails.DONE_SPEC_RECHECK})
@@ -1897,6 +1938,7 @@ class Agent:
1897
1938
  _sub_sig = (str(args.get("description", "")).strip(),
1898
1939
  str(args.get("prompt", "")).strip())
1899
1940
  if _sub_sig in subagent_sigs and levers.enabled("subagent_no_respawn"):
1941
+ levers.fired("subagent_no_respawn", step=step)
1900
1942
  result = ("[you already ran this exact sub-agent this turn — do NOT "
1901
1943
  "re-run it. Use what it returned, or do the work yourself "
1902
1944
  "now with grep/read to locate the code and edit to change "
@@ -1940,12 +1982,27 @@ class Agent:
1940
1982
  else:
1941
1983
  _t0 = time.perf_counter()
1942
1984
  self.tool_dispatches += 1
1985
+ # A snapshot happens AFTER approval, immediately before the tool
1986
+ # runs — a denied edit must not leave a checkpoint claiming it ran.
1987
+ if (name in AUTO_EDIT_TOOLS
1988
+ and levers.enabled("edit_checkpoint")):
1989
+ _ref = checkpoint.snapshot(
1990
+ os.getcwd(), f"before {name} {args.get('path', '')}".strip())
1991
+ if _ref:
1992
+ levers.fired("edit_checkpoint", step=step, ref=_ref)
1993
+ # Seatbelt context is scoped to exactly this dispatch: `active`
1994
+ # reflects the mode of the agent actually executing, so a
1995
+ # sub-agent's yolo loop is confined inside a normal-mode parent
1996
+ # turn, and the context can't leak to the `!cmd` passthrough.
1997
+ seatbelt.set_context(self.mode == "yolo", os.getcwd())
1943
1998
  try:
1944
1999
  result = fn(args, self._should_stop)
1945
2000
  if plan_write and result.startswith("[wrote"):
1946
2001
  self.last_plan_path = os.path.abspath(args["path"])
1947
2002
  except Exception as e: # noqa: BLE001 - surface tool errors to model
1948
2003
  result = f"[tool error: {type(e).__name__}: {e}]"
2004
+ finally:
2005
+ seatbelt.set_context(False, None)
1949
2006
  _tool_s = time.perf_counter() - _t0
1950
2007
  # Backstop: bound the prefill from any tool, AND from the step as a
1951
2008
  # whole — several calls in one step stack into one prefill, so later
@@ -1960,6 +2017,16 @@ class Agent:
1960
2017
  log.info("TOOL %s duplicate result elided (%d chars)",
1961
2018
  name, len(result))
1962
2019
  result = _elided
2020
+ # Ambient state: fold this result into the session
2021
+ # ledger and append whatever fact lines the enabled levers owe it
2022
+ # (session_ledger on landed mutations, bash_read_skeleton on file
2023
+ # content surfacing through bash/read). After the clip cap on
2024
+ # purpose — a clipped result must still carry its facts; the
2025
+ # appended text is bounded in ambient.py. Main agent only: a
2026
+ # sub-agent's transcript folds away and must not pollute the
2027
+ # parent's ledger.
2028
+ if not self._subagent:
2029
+ result = ambient.annotate(name, args, result, step=step)
1963
2030
  step_tool_chars += len(result)
1964
2031
  if _PREFILL_TRACE:
1965
2032
  self._trace_tools_pending.append([name, round(_tool_s, 4)])
@@ -1977,6 +2044,7 @@ class Agent:
1977
2044
  if (plan_write and result.startswith("[wrote") and not plan_reviews
1978
2045
  and levers.enabled("plan_review")):
1979
2046
  plan_reviews += 1
2047
+ levers.fired("plan_review", step=step)
1980
2048
  log.info("PLAN REVIEW nudge for %s", self.last_plan_path)
1981
2049
  self.messages.append({"role": "tool", "name": "read", "content": (
1982
2050
  f"[plan written to {self.last_plan_path}. Before you call `done`, "
@@ -2087,6 +2155,9 @@ class Agent:
2087
2155
  n == "bash"
2088
2156
  and not guardrails.is_readonly_bash(str(a.get("command", "")))
2089
2157
  for n, a in calls):
2158
+ if readonly_streak: # the exemption actually reset a live streak
2159
+ levers.fired("gate_ops_exempt", step=step,
2160
+ streak=readonly_streak)
2090
2161
  readonly_streak = 0
2091
2162
  else:
2092
2163
  readonly_streak += 1