chad-code 1.0.8__tar.gz → 1.11.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 (115) hide show
  1. {chad_code-1.0.8 → chad_code-1.11.0}/PKG-INFO +1 -1
  2. {chad_code-1.0.8 → chad_code-1.11.0}/pyproject.toml +5 -5
  3. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/__init__.py +1 -1
  4. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/agent.py +50 -5
  5. chad_code-1.11.0/src/chad/ambient.py +453 -0
  6. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/cli.py +5 -4
  7. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/compaction.py +4 -0
  8. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/guardrails.py +38 -7
  9. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/levers.py +136 -30
  10. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/mlx_fastpath.py +55 -4
  11. chad_code-1.11.0/src/chad/mlx_moe_fused.py +657 -0
  12. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/profiles.py +1 -5
  13. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/prompt.py +20 -0
  14. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/render.py +3 -1
  15. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/repomap.py +100 -0
  16. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/symbols.py +4 -1
  17. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/syntaxgate.py +7 -0
  18. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/tools.py +41 -3
  19. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad_code.egg-info/PKG-INFO +1 -1
  20. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad_code.egg-info/SOURCES.txt +5 -0
  21. chad_code-1.11.0/tests/test_ambient.py +277 -0
  22. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_lever_bite.py +47 -0
  23. chad_code-1.11.0/tests/test_lever_instrumentation.py +95 -0
  24. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_levers.py +37 -11
  25. chad_code-1.11.0/tests/test_mlx_moe_fused.py +354 -0
  26. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_repomap.py +87 -0
  27. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_repomap_polyglot.py +31 -0
  28. {chad_code-1.0.8 → chad_code-1.11.0}/LICENSE +0 -0
  29. {chad_code-1.0.8 → chad_code-1.11.0}/README.md +0 -0
  30. {chad_code-1.0.8 → chad_code-1.11.0}/setup.cfg +0 -0
  31. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/atif.py +0 -0
  32. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/base_engine.py +0 -0
  33. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/bench.py +0 -0
  34. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/completion_engine.py +0 -0
  35. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/config.py +0 -0
  36. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/diag.py +0 -0
  37. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/engine.py +0 -0
  38. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/ignore.py +0 -0
  39. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/lsp.py +0 -0
  40. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/lspclient.py +0 -0
  41. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/lspservers.py +0 -0
  42. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/mcp.py +0 -0
  43. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/mcp_oauth.py +0 -0
  44. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/mlx_qsdpa.py +0 -0
  45. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/parakeet/LICENSE +0 -0
  46. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/parakeet/__init__.py +0 -0
  47. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/parakeet/alignment.py +0 -0
  48. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/parakeet/attention.py +0 -0
  49. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/parakeet/audio.py +0 -0
  50. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/parakeet/cache.py +0 -0
  51. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/parakeet/conformer.py +0 -0
  52. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/parakeet/ctc.py +0 -0
  53. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/parakeet/parakeet.py +0 -0
  54. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/parakeet/rnnt.py +0 -0
  55. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/parakeet/tokenizer.py +0 -0
  56. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/parakeet/utils.py +0 -0
  57. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/prove.py +0 -0
  58. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/serve.py +0 -0
  59. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/session.py +0 -0
  60. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/skills.py +0 -0
  61. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/speech.py +0 -0
  62. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/toolcall_parse.py +0 -0
  63. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/tui.py +0 -0
  64. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad/validate.py +0 -0
  65. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad_code.egg-info/dependency_links.txt +0 -0
  66. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad_code.egg-info/entry_points.txt +0 -0
  67. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad_code.egg-info/requires.txt +0 -0
  68. {chad_code-1.0.8 → chad_code-1.11.0}/src/chad_code.egg-info/top_level.txt +0 -0
  69. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_agent.py +0 -0
  70. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_agent_e2e.py +0 -0
  71. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_agent_guards.py +0 -0
  72. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_atif.py +0 -0
  73. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_bench.py +0 -0
  74. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_cli.py +0 -0
  75. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_compact_notice.py +0 -0
  76. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_compaction.py +0 -0
  77. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_completion_engine.py +0 -0
  78. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_config.py +0 -0
  79. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_confirm_preview.py +0 -0
  80. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_done_audit.py +0 -0
  81. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_drift_warn.py +0 -0
  82. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_edit.py +0 -0
  83. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_edit_corruption.py +0 -0
  84. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_engine.py +0 -0
  85. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_engine_kvquant.py +0 -0
  86. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_engine_pld_hybrid.py +0 -0
  87. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_feel_pack.py +0 -0
  88. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_garble_invariant.py +0 -0
  89. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_gate.py +0 -0
  90. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_ignore.py +0 -0
  91. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_intent.py +0 -0
  92. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_log_redaction.py +0 -0
  93. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_lsp.py +0 -0
  94. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_lsp_live.py +0 -0
  95. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_lspclient.py +0 -0
  96. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_mcp.py +0 -0
  97. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_mcp_oauth.py +0 -0
  98. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_mlx_fastpath.py +0 -0
  99. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_mlx_qsdpa.py +0 -0
  100. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_plan_review.py +0 -0
  101. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_prove.py +0 -0
  102. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_render.py +0 -0
  103. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_replace_lines.py +0 -0
  104. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_serve.py +0 -0
  105. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_session.py +0 -0
  106. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_skills.py +0 -0
  107. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_speech.py +0 -0
  108. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_speech_tui.py +0 -0
  109. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_subagent.py +0 -0
  110. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_symbols.py +0 -0
  111. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_syntaxgate.py +0 -0
  112. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_toolcall_parse.py +0 -0
  113. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_tools.py +0 -0
  114. {chad_code-1.0.8 → chad_code-1.11.0}/tests/test_tui.py +0 -0
  115. {chad_code-1.0.8 → chad_code-1.11.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.8
3
+ Version: 1.11.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.8"
7
+ version = "1.11.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"
@@ -174,10 +174,10 @@ module = "chad.parakeet.*"
174
174
  ignore_errors = true
175
175
 
176
176
  [[tool.mypy.overrides]]
177
- # mlx_fastpath patches mlx_lm classes that only exist on macOS: with mlx installed
178
- # (dev Macs, the macos CI jobs) its `type: ignore[method-assign]` comments are load-
177
+ # These modules patch mlx_lm classes that only exist on macOS: with mlx installed
178
+ # (dev Macs, the macos CI jobs) their `type: ignore[method-assign]` comments are load-
179
179
  # bearing; on the Linux lint runner mlx_lm is absent, the patched classes type as Any,
180
180
  # and warn_unused_ignores flags those same comments — unfixable in the code for both
181
- # platforms at once, so the unused-ignore warning alone is relaxed for this module.
182
- module = "chad.mlx_fastpath"
181
+ # platforms at once, so the unused-ignore warning alone is relaxed for them.
182
+ module = ["chad.mlx_fastpath", "chad.mlx_moe_fused"]
183
183
  warn_unused_ignores = false
@@ -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.8"
7
+ __version__ = "1.11.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,7 @@ 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 ambient, atif, compaction, config, guardrails, levers, session, syntaxgate
18
18
  from .base_engine import BackendError, BaseEngine
19
19
  from .diag import args_preview, log, redact, result_preview
20
20
  from .prompt import build_subagent_prompt, build_system_prompt, classify_intent
@@ -47,7 +47,7 @@ SUBAGENT_MAX_STEPS = 24
47
47
  SUBAGENT_CTX_LIMIT = 32000
48
48
  SUBAGENT_READ_ONLY = {
49
49
  "read", "grep", "glob", "repo_map", "overview", "view_symbol",
50
- "find_symbol", "find_refs", "done", "finish", "stop",
50
+ "find_symbol", "definition", "find_refs", "done", "finish", "stop",
51
51
  }
52
52
  # `write_todos` is deliberately absent: a sub-agent that plans its own work would mutate
53
53
  # the process-global `_TODOS` and clobber the parent's pinned todo panel (and `_sub_emit`
@@ -346,6 +346,8 @@ class Agent:
346
346
  skills.reset_session()
347
347
  from . import mcp
348
348
  mcp.reset_session()
349
+ ambient.reset() # fresh session-state ledger; sub-agents share the
350
+ # parent's and never feed it (see run_turn)
349
351
  self.mode = mode or ("yolo" if yolo else "normal")
350
352
  self.thinking = thinking # Ornith is a reasoning model; toggles <think> blocks
351
353
  # Steps per WINDOW, not a hard kill: a window that landed+verified a change
@@ -625,6 +627,7 @@ class Agent:
625
627
  # measured trace: a model re-phrasing the same delete 30 times). The
626
628
  # guard names itself and the fix — narrow the target.
627
629
  if levers.enabled("scoped_destructive_guard"):
630
+ levers.fired("scoped_destructive_guard", deny_text=True)
628
631
  self._deny_reason = (
629
632
  "[blocked by the destructive-command guard, not by a person: "
630
633
  "the command matches a catastrophic pattern (recursive delete "
@@ -722,6 +725,7 @@ class Agent:
722
725
  note = sub.budget_note or guardrails.progress_note(sub.messages)
723
726
  if not note:
724
727
  return res
728
+ levers.fired("subagent_budget_note")
725
729
  return (res.rstrip() + "\n[sub-agent progress before it "
726
730
  f"stopped: {note}]").strip()
727
731
  if sub.interrupted:
@@ -1196,6 +1200,7 @@ class Agent:
1196
1200
  or self._backend_retries >= _MAX_BACKEND_RETRIES):
1197
1201
  raise
1198
1202
  self._backend_retries += 1
1203
+ levers.fired("backend_retry", attempt=self._backend_retries)
1199
1204
  log.warning("backend error (retry %d/%d): %s",
1200
1205
  self._backend_retries, _MAX_BACKEND_RETRIES, e)
1201
1206
  self._emit("status", f"Backend error; retrying "
@@ -1285,6 +1290,8 @@ class Agent:
1285
1290
  # the first branch already credits; an unclosed generation that
1286
1291
  # stopped SHORT of the cap is a truncation of some other kind, not
1287
1292
  # the reasoning overspend this counts.
1293
+ levers.fired("capped_think_credit", step=step,
1294
+ tokens=stats.generated_tokens)
1288
1295
  _think_delta = stats.generated_tokens
1289
1296
  else:
1290
1297
  _think_delta = 0
@@ -1312,6 +1319,7 @@ class Agent:
1312
1319
  if _tt_decision == "half":
1313
1320
  log.info("THINK-BUDGET half at step %d: %d/%d cumulative think tok "
1314
1321
  "this turn", step, turn_think_tokens, _tt_budget)
1322
+ levers.fired("turn_think_budget", step=step, decision="half")
1315
1323
  self.messages.append({"role": "tool", "name": "steer",
1316
1324
  "content": guardrails.TURN_THINK_BUDGET_STEER})
1317
1325
  elif _tt_decision == "exhausted":
@@ -1319,6 +1327,7 @@ class Agent:
1319
1327
  "tok this turn — throttling <think> (one action step per "
1320
1328
  "%d further think tok)", step, turn_think_tokens,
1321
1329
  _tt_budget, guardrails.TURN_THINK_REARM_TOK)
1330
+ levers.fired("turn_think_budget", step=step, decision="exhausted")
1322
1331
  self._emit("info", " [reasoning budget exhausted for this turn — "
1323
1332
  "throttling further <think>]")
1324
1333
 
@@ -1388,6 +1397,8 @@ class Agent:
1388
1397
  hard_wrapup_fired = True
1389
1398
  landing_no_think = True
1390
1399
  _remaining = self._turn_budget_s - (time.monotonic() - turn_start)
1400
+ levers.fired("hard_wrapup", step=step,
1401
+ remaining_s=int(_remaining))
1391
1402
  log.info("HARD-WRAPUP abort at step %d: %.0fs left, gen was %d tok",
1392
1403
  step, _remaining, stats.generated_tokens)
1393
1404
  self._emit("info", " [wall deadline reached — landing the best answer now]")
@@ -1452,6 +1463,7 @@ class Agent:
1452
1463
  capped_stall_streak += 1
1453
1464
  if (self.think_ceiling and self.thinking and capped_stall_streak >= 2
1454
1465
  and levers.enabled("no_think_escalation")):
1466
+ levers.fired("no_think_escalation", step=step)
1455
1467
  no_think_next = True
1456
1468
  else:
1457
1469
  capped_stall_streak = 0
@@ -1481,6 +1493,7 @@ class Agent:
1481
1493
  # last). Costs one prefix-cache invalidation on this rare path.
1482
1494
  if (levers.enabled("garble_never_final")
1483
1495
  and consecutive_garbles >= 2 and last_garble_idx is not None):
1496
+ levers.fired("garble_never_final", step=step, action="scrub")
1484
1497
  self.messages[last_garble_idx]["content"] = \
1485
1498
  guardrails.GARBLE_SCRUBBED
1486
1499
  log.info("GARBLE scrub at step %d: previous garbled message "
@@ -1516,6 +1529,7 @@ class Agent:
1516
1529
  # the audit latch, and the last one ships as the answer with most of the
1517
1530
  # wall budget still unspent).
1518
1531
  if garbled and levers.enabled("garble_never_final"):
1532
+ levers.fired("garble_never_final", step=step, action="hard-stop")
1519
1533
  self.budget_note = guardrails.progress_note(self.messages)
1520
1534
  log.info("END step %d: GARBLE hard-stop (%d garble nudges spent) — "
1521
1535
  "a garbled tool call is never a final answer",
@@ -1558,6 +1572,8 @@ class Agent:
1558
1572
  if audit:
1559
1573
  done_audit_fired = True
1560
1574
  done_audit_bounces += 1
1575
+ levers.fired("done_audit", step=step, entry="churn-handoff")
1576
+ levers.fired("audit_churn_handoff", step=step)
1561
1577
  audit_absent_list = guardrails.audit_absent_paths(audit_task)
1562
1578
  _runway = ((self._turn_budget_s
1563
1579
  - (time.monotonic() - turn_start))
@@ -1607,6 +1623,7 @@ class Agent:
1607
1623
  if audit:
1608
1624
  done_audit_fired = True
1609
1625
  done_audit_bounces += 1
1626
+ levers.fired("done_audit", step=step, entry="final-answer")
1610
1627
  audit_absent_list = guardrails.audit_absent_paths(audit_task)
1611
1628
  _runway = ((self._turn_budget_s
1612
1629
  - (time.monotonic() - turn_start))
@@ -1681,10 +1698,15 @@ class Agent:
1681
1698
  log.info("DONE rejected: edits not verified -> nudge #%d", verify_nudges)
1682
1699
  self.messages.append({
1683
1700
  "role": "tool", "name": "done",
1701
+ # The session-ledger facts (what changed, what last ran) ride
1702
+ # the bounce when the lever is on — the model's stale state
1703
+ # model IS the wrong-verify bug.
1684
1704
  "content": "[not done yet: you changed files but have not run anything "
1685
1705
  "to verify them. Run the project's tests (or the code) with "
1686
1706
  "bash, check the output is correct, then call done. If a test "
1687
- "fails, fix the code first.]",
1707
+ "fails, fix the code first.]"
1708
+ + ("" if self._subagent else
1709
+ ambient.ledger_suffix(step=step, force=True)),
1688
1710
  })
1689
1711
  continue
1690
1712
  if action_task and not read_only_intent and self.mode != "plan" \
@@ -1706,6 +1728,9 @@ class Agent:
1706
1728
  if audit:
1707
1729
  done_audit_fired = True
1708
1730
  done_audit_bounces += 1
1731
+ levers.fired("done_audit", step=step,
1732
+ entry="churn-handoff-done")
1733
+ levers.fired("audit_churn_handoff", step=step)
1709
1734
  audit_absent_list = guardrails.audit_absent_paths(audit_task)
1710
1735
  _runway = ((self._turn_budget_s
1711
1736
  - (time.monotonic() - turn_start))
@@ -1768,14 +1793,18 @@ class Agent:
1768
1793
  if audit:
1769
1794
  done_audit_fired = True
1770
1795
  done_audit_bounces += 1
1796
+ levers.fired("done_audit", step=step, entry="done")
1771
1797
  audit_absent_list = guardrails.audit_absent_paths(audit_task)
1772
1798
  _runway = ((self._turn_budget_s
1773
1799
  - (time.monotonic() - turn_start))
1774
1800
  if self._turn_budget_s else float("inf"))
1775
1801
  log.info("DONE-AUDIT bounce: paths=%s runway=%.0fs",
1776
1802
  guardrails.audit_extract_paths(audit_task), _runway)
1777
- self.messages.append({"role": "tool", "name": "done",
1778
- "content": audit})
1803
+ self.messages.append({
1804
+ "role": "tool", "name": "done",
1805
+ # Session-ledger facts ride the audit bounce
1806
+ "content": audit
1807
+ + ambient.ledger_suffix(step=step, force=True)})
1779
1808
  continue
1780
1809
  # Absent-path re-bounce (see the final-answer twin above): a
1781
1810
  # still-absent task-named path at accept time gets ONE more bounce,
@@ -1802,6 +1831,7 @@ class Agent:
1802
1831
  and guardrails.done_spec_recheck(
1803
1832
  did_work, unverified_edit, done_recheck_done, read_only_intent):
1804
1833
  done_recheck_done = True
1834
+ levers.fired("done_spec_recheck", step=step)
1805
1835
  log.info("DONE deferred for deliverable recheck (step %d)", step)
1806
1836
  self.messages.append({"role": "tool", "name": "done",
1807
1837
  "content": guardrails.DONE_SPEC_RECHECK})
@@ -1897,6 +1927,7 @@ class Agent:
1897
1927
  _sub_sig = (str(args.get("description", "")).strip(),
1898
1928
  str(args.get("prompt", "")).strip())
1899
1929
  if _sub_sig in subagent_sigs and levers.enabled("subagent_no_respawn"):
1930
+ levers.fired("subagent_no_respawn", step=step)
1900
1931
  result = ("[you already ran this exact sub-agent this turn — do NOT "
1901
1932
  "re-run it. Use what it returned, or do the work yourself "
1902
1933
  "now with grep/read to locate the code and edit to change "
@@ -1960,6 +1991,16 @@ class Agent:
1960
1991
  log.info("TOOL %s duplicate result elided (%d chars)",
1961
1992
  name, len(result))
1962
1993
  result = _elided
1994
+ # Ambient state: fold this result into the session
1995
+ # ledger and append whatever fact lines the enabled levers owe it
1996
+ # (session_ledger on landed mutations, bash_read_skeleton on file
1997
+ # content surfacing through bash/read). After the clip cap on
1998
+ # purpose — a clipped result must still carry its facts; the
1999
+ # appended text is bounded in ambient.py. Main agent only: a
2000
+ # sub-agent's transcript folds away and must not pollute the
2001
+ # parent's ledger.
2002
+ if not self._subagent:
2003
+ result = ambient.annotate(name, args, result, step=step)
1963
2004
  step_tool_chars += len(result)
1964
2005
  if _PREFILL_TRACE:
1965
2006
  self._trace_tools_pending.append([name, round(_tool_s, 4)])
@@ -1977,6 +2018,7 @@ class Agent:
1977
2018
  if (plan_write and result.startswith("[wrote") and not plan_reviews
1978
2019
  and levers.enabled("plan_review")):
1979
2020
  plan_reviews += 1
2021
+ levers.fired("plan_review", step=step)
1980
2022
  log.info("PLAN REVIEW nudge for %s", self.last_plan_path)
1981
2023
  self.messages.append({"role": "tool", "name": "read", "content": (
1982
2024
  f"[plan written to {self.last_plan_path}. Before you call `done`, "
@@ -2087,6 +2129,9 @@ class Agent:
2087
2129
  n == "bash"
2088
2130
  and not guardrails.is_readonly_bash(str(a.get("command", "")))
2089
2131
  for n, a in calls):
2132
+ if readonly_streak: # the exemption actually reset a live streak
2133
+ levers.fired("gate_ops_exempt", step=step,
2134
+ streak=readonly_streak)
2090
2135
  readonly_streak = 0
2091
2136
  else:
2092
2137
  readonly_streak += 1