@iowarp/clio-coder 0.3.4 → 0.3.6

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 (265) hide show
  1. package/CHANGELOG.md +37 -2
  2. package/CONTRIBUTING.md +6 -6
  3. package/README.md +2 -2
  4. package/dist/{acp-S5R4RR5B.js → acp-2BEHC4DL.js} +4 -4
  5. package/dist/{agents-P6DMMVZY.js → agents-LNNFTM53.js} +13 -11
  6. package/dist/assets/codewiki.json +1 -1
  7. package/dist/{auth-2XCZLPKS.js → auth-KXXFI2VS.js} +6 -6
  8. package/dist/{chunk-YCWGATWI.js → chunk-24I7BN55.js} +2 -2
  9. package/dist/{chunk-EKMEHE4H.js → chunk-33YXPOE3.js} +2 -3
  10. package/dist/chunk-3BPUFZDL.js +37 -0
  11. package/dist/{chunk-WPQLXFOZ.js → chunk-43AOLP7E.js} +2 -2
  12. package/dist/{chunk-N4CZJQRK.js → chunk-5JGRAMKL.js} +4 -4
  13. package/dist/{chunk-BRXQQJFP.js → chunk-6US73PDB.js} +568 -47
  14. package/dist/{chunk-K6WL7QZT.js → chunk-6XXKFVSN.js} +2 -2
  15. package/dist/{chunk-QQK64KLB.js → chunk-CJUB2JJ2.js} +138 -20
  16. package/dist/{chunk-HV5X7OR2.js → chunk-CKXWIANG.js} +12 -12
  17. package/dist/{chunk-UZHIZC5S.js → chunk-CYQKWTG3.js} +61 -76
  18. package/dist/{chunk-QWU7ZBO7.js → chunk-DJVECN66.js} +204 -45
  19. package/dist/{chunk-ZWMF7253.js → chunk-E2ER4LJF.js} +304 -9
  20. package/dist/{chunk-7RXG6QRZ.js → chunk-EKY57CSP.js} +2 -75
  21. package/dist/{chunk-EDRHSCIE.js → chunk-EYPA3EGJ.js} +10 -2
  22. package/dist/{chunk-TTNYS3EA.js → chunk-G7MUEIGA.js} +1 -1
  23. package/dist/{chunk-BPGS2WCQ.js → chunk-GEYXPTRF.js} +2 -1
  24. package/dist/{chunk-BEY543CS.js → chunk-GOXNB3AO.js} +5 -2
  25. package/dist/{chunk-G4BMMOKF.js → chunk-HVDIIIQW.js} +2 -2
  26. package/dist/chunk-HWUFFB6L.js +83 -0
  27. package/dist/{chunk-35MKKU5R.js → chunk-K7T3E2SR.js} +15 -8
  28. package/dist/{chunk-VAWWTKDP.js → chunk-KHSFENX2.js} +2 -2
  29. package/dist/chunk-LCGCVYZ4.js +57 -0
  30. package/dist/{chunk-X6COSD2O.js → chunk-LYF7OHWH.js} +41 -14
  31. package/dist/{chunk-POHLU5DW.js → chunk-M6L6IDJG.js} +3 -3
  32. package/dist/{chunk-X4RCMKVQ.js → chunk-NDINPTJ4.js} +2 -2
  33. package/dist/{chunk-5M54SPOL.js → chunk-ODFEOB4F.js} +161 -5
  34. package/dist/{chunk-3JLKSKD7.js → chunk-OH3TOQTB.js} +5 -1
  35. package/dist/{chunk-MEQ45TQ4.js → chunk-PBTHKCPN.js} +18 -4
  36. package/dist/{chunk-ED4KHGC3.js → chunk-PPAMZ32Z.js} +9 -2
  37. package/dist/{chunk-QQL5RT5M.js → chunk-QM3F2GKX.js} +94 -36
  38. package/dist/{chunk-A2GZF7DC.js → chunk-QNQHSOLF.js} +4 -4
  39. package/dist/{chunk-KRPY7NTG.js → chunk-R46L2BIR.js} +3 -3
  40. package/dist/{chunk-BP4OYD6A.js → chunk-RY3LY4J5.js} +20 -2
  41. package/dist/{chunk-34475P3I.js → chunk-TSHXZTOQ.js} +5 -4
  42. package/dist/{chunk-VJWL6YS5.js → chunk-UUVG37B4.js} +2 -2
  43. package/dist/{chunk-2TZWSW76.js → chunk-WHGPSPT5.js} +2 -2
  44. package/dist/{chunk-TW3WDMVS.js → chunk-WHJYKASB.js} +2 -2
  45. package/dist/{chunk-YHZX5GEU.js → chunk-XAKHZX5N.js} +2 -2
  46. package/dist/{chunk-HXG4IURW.js → chunk-XE2VEJHX.js} +2 -2
  47. package/dist/{chunk-3HZ5RWN2.js → chunk-XF5N4U5A.js} +7 -6
  48. package/dist/{chunk-ZYKPLLNQ.js → chunk-XXQNGV4M.js} +590 -32
  49. package/dist/{chunk-4JUF2NNX.js → chunk-XYDYPRZI.js} +4 -4
  50. package/dist/{chunk-VMNQ6OZA.js → chunk-ZRGEBJ4T.js} +971 -794
  51. package/dist/{chunk-2LZI5CAG.js → chunk-ZXF4XRKW.js} +75 -33
  52. package/dist/{chunk-VSNATDE6.js → chunk-ZZMN5OM4.js} +2 -2
  53. package/dist/cli/index.js +31 -31
  54. package/dist/{clio-J5JIOIDS.js → clio-M2KGYUFZ.js} +2 -2
  55. package/dist/{code-nav-AXCXSBHX.js → code-nav-GQNL7XA6.js} +5 -5
  56. package/dist/codewiki/build-worker.js +4 -4
  57. package/dist/{components-KELWS457.js → components-5TTYYX6G.js} +3 -3
  58. package/dist/{config-OEBMIN2U.js → config-XUUYQIWO.js} +27 -25
  59. package/dist/{configure-PUQOSIXQ.js → configure-IHJ7YOMV.js} +7 -7
  60. package/dist/{context-URSXPBCK.js → context-74JLXAWD.js} +12 -12
  61. package/dist/{context-MGSE4Z2T.js → context-75MIWW3U.js} +24 -22
  62. package/dist/{context-EKDCKUUZ.js → context-ZQ7SIFJV.js} +8 -7
  63. package/dist/{context-clear-KDAJRNUK.js → context-clear-GYKWNUML.js} +24 -22
  64. package/dist/{context-index-BZ4UYMTC.js → context-index-SSR5ECNE.js} +3 -3
  65. package/dist/{context-working-set-SBKMPPI2.js → context-working-set-UX5KEP4J.js} +11 -10
  66. package/dist/{dispatch-runner-MSWN72NK.js → dispatch-runner-GIJBHNFL.js} +21 -20
  67. package/dist/{docs-2C2LTVT2.js → docs-6FZSCG5B.js} +3 -3
  68. package/dist/{doctor-7BSE27PJ.js → doctor-SVJ5BZCW.js} +4 -4
  69. package/dist/{eval-IZGDOO4H.js → eval-CG6LLBLD.js} +47 -232
  70. package/dist/{evidence-SR7WXB5B.js → evidence-ZYFIEN42.js} +19 -18
  71. package/dist/{evolve-K7VE2CBX.js → evolve-QGEXEMDW.js} +19 -18
  72. package/dist/{extensions-QVDOHDGJ.js → extensions-ADGNCJJD.js} +3 -3
  73. package/dist/{fleet-7XMJNQNF.js → fleet-S5R4ZOQY.js} +49 -30
  74. package/dist/{fleet-preflight-AQNAH644.js → fleet-preflight-BHSNPBMH.js} +2 -2
  75. package/dist/{init-JGNPAYXT.js → init-5DRU55YR.js} +31 -29
  76. package/dist/memory-7YKKR6UC.js +467 -0
  77. package/dist/{models-ZMMLFJNN.js → models-ZPOLRU2C.js} +10 -10
  78. package/dist/{monitor-2F3T5KHP.js → monitor-US5F5YGZ.js} +33 -18
  79. package/dist/{orchestrator-ORHT43JB.js → orchestrator-E2AL4T5N.js} +1092 -659
  80. package/dist/{paths-UXLN5YYZ.js → paths-E7KYAQWE.js} +3 -3
  81. package/dist/{reset-NXGTYNUO.js → reset-KZ652EK6.js} +3 -3
  82. package/dist/{run-RF4WJGMT.js → run-SRNBKDWD.js} +52 -40
  83. package/dist/{share-UT3W6E4M.js → share-CGZE33UP.js} +3 -3
  84. package/dist/{skills-PSACKC5Q.js → skills-S2X4DLY5.js} +4 -4
  85. package/dist/{skills-eval-WJSI55RZ.js → skills-eval-W2GGIC4R.js} +19 -18
  86. package/dist/{targets-PIIRAOYS.js → targets-54SWINWB.js} +14 -12
  87. package/dist/{terminal-lease-ULWXWNVY.js → terminal-lease-SAIF2OGY.js} +5 -4
  88. package/dist/{uninstall-FZCQCDKC.js → uninstall-BVLWXKBT.js} +3 -3
  89. package/dist/{upgrade-346TZ6AV.js → upgrade-JKAR27XC.js} +8 -8
  90. package/dist/{usage-6KKXR32N.js → usage-MSAWCLX4.js} +60 -27
  91. package/dist/{verifiers-4UUM6TEE.js → verifiers-NCBTHHN2.js} +60 -54
  92. package/dist/{wiki-generate-7STOCIFZ.js → wiki-generate-GUSOQ6ZP.js} +30 -28
  93. package/dist/worker/entry.js +69 -58
  94. package/dist/{workspace-G4ZWUIPR.js → workspace-ZJ6BFM3Q.js} +4 -4
  95. package/docs/README.md +3 -3
  96. package/docs/acp.md +1 -1
  97. package/docs/alcf-provider.md +1 -1
  98. package/docs/architecture.md +2 -2
  99. package/docs/artifact-placement.md +1 -2
  100. package/docs/artifact-versions.md +1 -1
  101. package/docs/built-in-agents.md +1 -1
  102. package/docs/capacity-and-scheduling.md +1 -1
  103. package/docs/commands-and-modes.md +9 -7
  104. package/docs/configuration-and-targets.md +12 -1
  105. package/docs/context-engine.md +4 -2
  106. package/docs/context-working-set.md +4 -4
  107. package/docs/development-pipeline.md +1 -1
  108. package/docs/documentation-coverage.md +3 -3
  109. package/docs/documentation-guide.md +2 -2
  110. package/docs/eval-runner.md +1 -1
  111. package/docs/evals-internal.md +4 -45
  112. package/docs/evidence-and-memory.md +67 -7
  113. package/docs/evolution.md +1 -1
  114. package/docs/exit-codes-and-output.md +1 -1
  115. package/docs/extensions-and-sharing.md +2 -2
  116. package/docs/fleet-dispatch.md +28 -2
  117. package/docs/installation-and-lifecycle.md +2 -2
  118. package/docs/middleware-and-components.md +19 -2
  119. package/docs/model-catalog.md +1 -1
  120. package/docs/observability.md +3 -3
  121. package/docs/proactive-memory.md +26 -16
  122. package/docs/prompt-envelope-and-tools.md +4 -2
  123. package/docs/provider-adapter-cookbook.md +1 -1
  124. package/docs/release-cut-checklist.md +38 -35
  125. package/docs/safety-model.md +29 -7
  126. package/docs/scientific-validation.md +3 -3
  127. package/docs/session-lifecycle.md +1 -1
  128. package/docs/skills-marketplace.md +1 -1
  129. package/docs/tool-usage.md +2 -2
  130. package/docs/trace-store.md +1 -1
  131. package/docs/troubleshooting.md +1 -1
  132. package/docs/tui-design.md +38 -4
  133. package/docs/worker-dispatch-mechanics.md +1 -1
  134. package/package.json +7 -4
  135. package/src/cli/agents.ts +2 -3
  136. package/src/cli/argv.ts +14 -1
  137. package/src/cli/fleet.ts +15 -0
  138. package/src/cli/index.ts +1 -1
  139. package/src/cli/memory.ts +272 -10
  140. package/src/cli/modes/json-stream.ts +2 -2
  141. package/src/cli/modes/print.ts +12 -1
  142. package/src/cli/run.ts +22 -2
  143. package/src/cli/targets.ts +12 -3
  144. package/src/cli/usage.ts +55 -7
  145. package/src/core/bus-events.ts +3 -0
  146. package/src/core/response-model-id.ts +134 -0
  147. package/src/core/toml.ts +62 -0
  148. package/src/core/workspace-files.ts +0 -1
  149. package/src/domains/agents/builtins/architect.md +1 -1
  150. package/src/domains/agents/catalog.ts +5 -4
  151. package/src/domains/agents/recipe.ts +54 -14
  152. package/src/domains/agents/result-contract.ts +7 -4
  153. package/src/domains/context/bootstrap.ts +36 -27
  154. package/src/domains/context/project-metadata.ts +19 -63
  155. package/src/domains/context/prompt-context.ts +8 -0
  156. package/src/domains/context/working-set/policies/index.ts +3 -4
  157. package/src/domains/dispatch/budget-envelope.ts +396 -0
  158. package/src/domains/dispatch/contract.ts +2 -0
  159. package/src/domains/dispatch/extension.ts +81 -27
  160. package/src/domains/dispatch/orphan-recovery.ts +1 -0
  161. package/src/domains/dispatch/receipt-integrity.ts +4 -0
  162. package/src/domains/dispatch/state.ts +1 -0
  163. package/src/domains/dispatch/types.ts +10 -3
  164. package/src/domains/dispatch/validation.ts +14 -0
  165. package/src/domains/dispatch/worker-spawn.ts +14 -3
  166. package/src/domains/eval/metrics/evidence.ts +0 -116
  167. package/src/domains/eval/metrics/invariants.ts +1 -1
  168. package/src/domains/eval/runners/clio-run.ts +1 -10
  169. package/src/domains/eval/runners/external-command.ts +2 -29
  170. package/src/domains/eval/schema/suite.ts +0 -7
  171. package/src/domains/eval/suites/run.ts +1 -7
  172. package/src/domains/memory/index.ts +22 -0
  173. package/src/domains/memory/operations.ts +58 -1
  174. package/src/domains/memory/promotion.ts +281 -0
  175. package/src/domains/memory/prompt-section.ts +25 -5
  176. package/src/domains/memory/proposal.ts +51 -7
  177. package/src/domains/memory/task-bank.ts +3 -2
  178. package/src/domains/memory/task-memory-handoff.ts +181 -24
  179. package/src/domains/memory/task-memory-policy.ts +3 -1
  180. package/src/domains/memory/types.ts +37 -0
  181. package/src/domains/memory/validate.ts +178 -0
  182. package/src/domains/middleware/memory-intervention.ts +35 -25
  183. package/src/domains/middleware/runtime.ts +6 -0
  184. package/src/domains/middleware/skills-reminder.ts +19 -4
  185. package/src/domains/middleware/stalled-turn.ts +43 -1
  186. package/src/domains/middleware/types.ts +10 -0
  187. package/src/domains/observability/contract.ts +6 -1
  188. package/src/domains/observability/cost.ts +20 -4
  189. package/src/domains/observability/extension.ts +2 -2
  190. package/src/domains/providers/index.ts +3 -0
  191. package/src/domains/providers/model-discovery.ts +9 -0
  192. package/src/domains/providers/runtime-resolution.ts +38 -1
  193. package/src/domains/providers/runtimes/common/probe-helpers.ts +97 -16
  194. package/src/domains/providers/types/context-window-slots.ts +18 -0
  195. package/src/domains/providers/types/runtime-descriptor.ts +3 -1
  196. package/src/domains/safety/call-target.ts +211 -14
  197. package/src/domains/safety/decision-presentation.ts +268 -0
  198. package/src/domains/safety/redaction.ts +73 -0
  199. package/src/domains/session/context-ledger.ts +10 -1
  200. package/src/domains/session/decision-board.ts +4 -0
  201. package/src/domains/session/entries.ts +3 -0
  202. package/src/domains/session/history.ts +68 -19
  203. package/src/domains/session/usage.ts +24 -7
  204. package/src/engine/acp/event-mapper.ts +7 -0
  205. package/src/engine/acp/server.ts +29 -2
  206. package/src/engine/apis/lmstudio.ts +25 -4
  207. package/src/engine/apis/openai-completions.ts +147 -22
  208. package/src/engine/claude/sdk-runtime.ts +8 -2
  209. package/src/engine/claude/tool-safety.ts +13 -0
  210. package/src/engine/loop-guard.ts +27 -3
  211. package/src/engine/worker-events.ts +4 -3
  212. package/src/engine/worker-runtime.ts +59 -54
  213. package/src/entry/orchestrator.ts +18 -1
  214. package/src/interactive/chat-loop-messages.ts +22 -0
  215. package/src/interactive/chat-loop.ts +13 -0
  216. package/src/interactive/chat-renderer.ts +19 -3
  217. package/src/interactive/clio-editor.ts +44 -7
  218. package/src/interactive/context-overlay.ts +43 -5
  219. package/src/interactive/cost-overlay.ts +39 -8
  220. package/src/interactive/dispatch-board.ts +212 -35
  221. package/src/interactive/footer/widgets.ts +13 -0
  222. package/src/interactive/interactive-application.ts +6 -1
  223. package/src/interactive/interactive-input-runtime.ts +11 -1
  224. package/src/interactive/interactive-presentation.ts +11 -1
  225. package/src/interactive/memory-overlay.ts +89 -4
  226. package/src/interactive/overlay-ask-user-lifecycle.ts +1 -1
  227. package/src/interactive/overlay-frame.ts +5 -2
  228. package/src/interactive/overlay-general-openers.ts +40 -1
  229. package/src/interactive/overlay-key-routing.ts +41 -1
  230. package/src/interactive/overlay-lifecycle.ts +11 -4
  231. package/src/interactive/overlay-permission-lifecycle.ts +23 -8
  232. package/src/interactive/overlay-transitions.ts +11 -0
  233. package/src/interactive/overlays/ask-user.ts +74 -30
  234. package/src/interactive/overlays/decisions.ts +3 -1
  235. package/src/interactive/permission-hint.ts +35 -0
  236. package/src/interactive/permission-overlay.ts +95 -45
  237. package/src/interactive/renderers/tool-execution.ts +19 -49
  238. package/src/interactive/session-last-turn.ts +8 -1
  239. package/src/interactive/session-usage-reseed.ts +36 -10
  240. package/src/interactive/slash-commands.ts +2 -2
  241. package/src/interactive/status/summary.ts +5 -0
  242. package/src/interactive/status/types.ts +5 -0
  243. package/src/interactive/terminal-lease.ts +1 -0
  244. package/src/interactive/turn-context.ts +96 -23
  245. package/src/interactive/turn-middleware.ts +1 -0
  246. package/src/interactive/turn-runtime.ts +37 -8
  247. package/src/interactive/turn-state.ts +3 -0
  248. package/src/interactive/worker-progress.ts +440 -0
  249. package/src/interactive/worker-stream.ts +51 -110
  250. package/src/tools/agent-tools.ts +28 -3
  251. package/src/tools/ask-user.ts +21 -1
  252. package/src/tools/context/index.ts +2 -2
  253. package/src/tools/dispatch-arguments.ts +8 -0
  254. package/src/tools/dispatch-event-text.ts +19 -0
  255. package/src/tools/dispatch.ts +24 -1
  256. package/src/tools/monitor.ts +15 -0
  257. package/src/tools/registry.ts +15 -5
  258. package/src/tools/result-disposition.ts +156 -0
  259. package/src/tools/result-shaping.ts +59 -1
  260. package/src/tools/verify/authoring.ts +55 -54
  261. package/src/tools/worker-evidence.ts +19 -0
  262. package/src/worker/spec-contract.ts +43 -3
  263. package/dist/chunk-EFADSJET.js +0 -18
  264. package/dist/memory-4ALKDJ4Q.js +0 -246
  265. package/src/domains/eval/metrics/chaos-stream.ts +0 -93
@@ -19,6 +19,7 @@
19
19
  import { existsSync, readdirSync, readFileSync } from "node:fs";
20
20
  import { join } from "node:path";
21
21
  import { parse as parseYaml } from "yaml";
22
+ import { createTomlFileReader, type TomlFileReader, tomlTableAt } from "../../core/toml.js";
22
23
 
23
24
  /** Longest description worth carrying into a one-sentence identity line. */
24
25
  const MAX_DESCRIPTION_CHARS = 400;
@@ -39,7 +40,7 @@ interface MetadataFragment {
39
40
  description?: string | null;
40
41
  }
41
42
 
42
- type MetadataReader = (cwd: string) => { file: string; fragment: MetadataFragment } | null;
43
+ type MetadataReader = (cwd: string, tomlFiles: TomlFileReader) => { file: string; fragment: MetadataFragment } | null;
43
44
 
44
45
  function readText(cwd: string, relative: string): string | null {
45
46
  try {
@@ -83,54 +84,6 @@ function jsonRecord(cwd: string, relative: string): Record<string, unknown> | nu
83
84
  }
84
85
  }
85
86
 
86
- /**
87
- * One key from one TOML table, without a TOML parser.
88
- *
89
- * Deliberately narrow: it reads `key = "value"` (single-line, single- or
90
- * double-quoted) from the named top-level table and understands nothing else.
91
- * That is the entire shape `description` and `name` take in `pyproject.toml`
92
- * and `Cargo.toml`. A multi-line or otherwise exotic value reads as absent,
93
- * which costs a description and never produces a wrong one.
94
- */
95
- function tomlTableValue(raw: string, table: string, key: string): string | null {
96
- const lines = raw.split(/\r?\n/u);
97
- let inTable = false;
98
- for (const line of lines) {
99
- const trimmed = line.trim();
100
- if (trimmed.startsWith("[")) {
101
- inTable = trimmed === `[${table}]`;
102
- continue;
103
- }
104
- if (!inTable) continue;
105
- const match = /^([A-Za-z0-9_-]+)\s*=\s*(.*)$/u.exec(trimmed);
106
- if (!match || match[1] !== key) continue;
107
- const value = (match[2] ?? "").trim();
108
- const quoted = /^"((?:[^"\\]|\\.)*)"$/u.exec(value) ?? /^'([^']*)'$/u.exec(value);
109
- if (!quoted) return null;
110
- return (quoted[1] ?? "").replace(/\\"/gu, '"');
111
- }
112
- return null;
113
- }
114
-
115
- /**
116
- * Whether a TOML file declares the named top-level table, e.g. `tool.tox`.
117
- * A trimmed line must start with `[table]` and carry nothing after it but an
118
- * optional `# comment`, the only content TOML itself allows there. A raw
119
- * `raw.includes("[table]")` check would also fire on a comment mentioning the
120
- * table or a string value that happens to contain the same bracketed text;
121
- * anchoring to the start of the line rules both out without a TOML parser,
122
- * while still accepting the valid `[table] # comment` form that a plain
123
- * equality check would have missed.
124
- */
125
- export function hasTomlTable(raw: string, table: string): boolean {
126
- const header = `[${table}]`;
127
- return raw.split(/\r?\n/u).some((line) => {
128
- const trimmed = line.trim();
129
- if (!trimmed.startsWith(header)) return false;
130
- return /^\s*(?:#.*)?$/u.test(trimmed.slice(header.length));
131
- });
132
- }
133
-
134
87
  /** One `KEY = value` line from a Doxyfile or an INI-shaped config. */
135
88
  function keyEqualsValue(raw: string, key: string): string | null {
136
89
  const match = new RegExp(`^[ \\t]*${key}[ \\t]*(?:\\+?=)[ \\t]*(.+)$`, "mu").exec(raw);
@@ -182,27 +135,30 @@ const packageJson: MetadataReader = (cwd) => {
182
135
  return { file: "package.json", fragment: { name: name(record.name), description: description(record.description) } };
183
136
  };
184
137
 
185
- const pyprojectToml: MetadataReader = (cwd) => {
186
- const raw = readText(cwd, "pyproject.toml");
187
- if (raw === null) return null;
138
+ const pyprojectToml: MetadataReader = (_cwd, tomlFiles) => {
139
+ const document = tomlFiles.read("pyproject.toml");
140
+ if (document === null) return null;
141
+ const project = tomlTableAt(document, ["project"]);
142
+ const poetry = tomlTableAt(document, ["tool", "poetry"]);
188
143
  // PEP 621 `[project]` first, then Poetry's older table.
189
- const projectName = tomlTableValue(raw, "project", "name") ?? tomlTableValue(raw, "tool.poetry", "name");
190
- const projectDescription =
191
- tomlTableValue(raw, "project", "description") ?? tomlTableValue(raw, "tool.poetry", "description");
192
144
  return {
193
145
  file: "pyproject.toml",
194
- fragment: { name: name(projectName), description: description(projectDescription) },
146
+ fragment: {
147
+ name: name(project?.name) ?? name(poetry?.name),
148
+ description: description(project?.description) ?? description(poetry?.description),
149
+ },
195
150
  };
196
151
  };
197
152
 
198
- const cargoToml: MetadataReader = (cwd) => {
199
- const raw = readText(cwd, "Cargo.toml");
200
- if (raw === null) return null;
153
+ const cargoToml: MetadataReader = (_cwd, tomlFiles) => {
154
+ const document = tomlFiles.read("Cargo.toml");
155
+ if (document === null) return null;
156
+ const packageTable = tomlTableAt(document, ["package"]);
201
157
  return {
202
158
  file: "Cargo.toml",
203
159
  fragment: {
204
- name: name(tomlTableValue(raw, "package", "name")),
205
- description: description(tomlTableValue(raw, "package", "description")),
160
+ name: name(packageTable?.name),
161
+ description: description(packageTable?.description),
206
162
  },
207
163
  };
208
164
  };
@@ -488,13 +444,13 @@ function readmeTitle(content: string): string | null {
488
444
  * a CMake project that names itself but describes itself only in its README
489
445
  * should get both, not one or neither.
490
446
  */
491
- export function readProjectMetadata(cwd: string): ProjectMetadata {
447
+ export function readProjectMetadata(cwd: string, tomlFiles = createTomlFileReader(cwd)): ProjectMetadata {
492
448
  const out: ProjectMetadata = { name: null, description: null, nameSource: null, descriptionSource: null };
493
449
  for (const reader of METADATA_READERS) {
494
450
  if (out.name !== null && out.description !== null) break;
495
451
  let read: ReturnType<MetadataReader>;
496
452
  try {
497
- read = reader(cwd);
453
+ read = reader(cwd, tomlFiles);
498
454
  } catch {
499
455
  continue;
500
456
  }
@@ -20,6 +20,10 @@ import { wikiCompleteness, wikiStaleness } from "./wiki/staleness.js";
20
20
  * context-init preload reporting, and `clio-coder config inspect`, so every surface
21
21
  * measures the same text the session prompt would preload.
22
22
  */
23
+ /** The one line that stands in for a handbook this workspace does not have. */
24
+ export const HANDBOOK_ABSENT_FRAGMENT =
25
+ "<handbook>none: this workspace has no CLIO-CODER.md, so do not read one; learn the repository from its files, and the operator can run /context init to write a handbook</handbook>";
26
+
23
27
  export function renderPromptContext(cwd: string): ProjectPromptContext {
24
28
  const projectType = detectProjectType(cwd);
25
29
  const pieces = [renderProjectTypeFragment(projectType)];
@@ -30,6 +34,10 @@ export function renderPromptContext(cwd: string): ProjectPromptContext {
30
34
  for (const issue of loadedClioMd.errors) {
31
35
  warnings.push(`clio: malformed ${issue.path} ignored: ${issue.error}`);
32
36
  }
37
+ // Said where the handbook would have been: a model that sees no project
38
+ // context spends its first tool call reading CLIO-CODER.md and gets
39
+ // ENOENT, while the operator's header already says it is missing (#191).
40
+ if (loadedClioMd.files.length === 0 && loadedClioMd.errors.length === 0) pieces.push(HANDBOOK_ABSENT_FRAGMENT);
33
41
  const codewiki = readCodewiki(cwd);
34
42
  if (codewiki) {
35
43
  const state = readClioState(cwd);
@@ -3,10 +3,9 @@
3
3
  * replay-lite runner resolve the same object from the same settings value and
4
4
  * cannot drift into running different selections.
5
5
  *
6
- * `structural-v1` is the default; the replay tables under
7
- * benchmarks/results/context-replay/ put it ahead of `age-horizon` on
8
- * retention and ahead of the random control on precision. `age-horizon` stays
9
- * resolvable as the exact pre-layer selection.
6
+ * `structural-v1` is the default; internal replay studies put it ahead of
7
+ * `age-horizon` on retention and ahead of the random control on precision.
8
+ * `age-horizon` stays resolvable as the exact pre-layer selection.
10
9
  */
11
10
 
12
11
  import type { WorkingSetPolicy, WorkingSetPolicyId } from "../contract.js";
@@ -0,0 +1,396 @@
1
+ import type { WorkerBudget } from "../../worker/spec-contract.js";
2
+ import type { AgentBudget, AgentBudgetPhase } from "../agents/recipe.js";
3
+
4
+ /** Invocation-level phase requested by dispatch. */
5
+ export interface DispatchBudgetPhase {
6
+ toolCalls: number;
7
+ readReserve: number;
8
+ }
9
+
10
+ /** Typed invocation request. The ceiling preauthorizes growth on a later phase. */
11
+ export interface DispatchBudgetRequest extends DispatchBudgetPhase {
12
+ retryRevision?: DispatchBudgetPhase;
13
+ }
14
+
15
+ export type BudgetEnvelopeReasonCode =
16
+ | "global-cap-clamp"
17
+ | "read-reserve-unavailable-clamp"
18
+ | "read-reserve-cap-clamp"
19
+ | "retry-growth-authorized"
20
+ | "retry-growth-denied"
21
+ | "revision-growth-authorized"
22
+ | "revision-growth-denied";
23
+
24
+ export interface BudgetEnvelopeReason {
25
+ code: BudgetEnvelopeReasonCode;
26
+ detail: string;
27
+ }
28
+
29
+ export interface RunToolBudgetEnvelope {
30
+ version: 1;
31
+ policy: {
32
+ recipeId: string;
33
+ default: AgentBudgetPhase & { synthesis: boolean };
34
+ maximum: AgentBudgetPhase;
35
+ exact: boolean;
36
+ };
37
+ request: DispatchBudgetRequest | null;
38
+ effective: WorkerBudget;
39
+ reasons: ReadonlyArray<BudgetEnvelopeReason>;
40
+ }
41
+
42
+ export type BudgetAdmissionDenialCode =
43
+ | "exact-recipe-policy"
44
+ | "recipe-maximum"
45
+ | "global-cap"
46
+ | "ceiling-below-request";
47
+
48
+ export class BudgetAdmissionError extends Error {
49
+ readonly code: BudgetAdmissionDenialCode;
50
+
51
+ constructor(code: BudgetAdmissionDenialCode, message: string) {
52
+ super(`dispatch: budget admission denied (${code}): ${message}`);
53
+ this.name = "BudgetAdmissionError";
54
+ this.code = code;
55
+ }
56
+ }
57
+
58
+ function isRecord(value: unknown): value is Record<string, unknown> {
59
+ return typeof value === "object" && value !== null && !Array.isArray(value);
60
+ }
61
+
62
+ function clonePhase(value: unknown, prefix: string): DispatchBudgetPhase {
63
+ if (!isRecord(value)) throw new Error(`${prefix} must be an object`);
64
+ for (const key of Object.keys(value)) {
65
+ if (key !== "toolCalls" && key !== "readReserve") throw new Error(`${prefix}.${key} is unknown`);
66
+ }
67
+ for (const key of ["toolCalls", "readReserve"] as const) {
68
+ if (!Object.hasOwn(value, key)) throw new Error(`${prefix}.${key} is required`);
69
+ }
70
+ const toolCalls = value.toolCalls;
71
+ if (typeof toolCalls !== "number" || !Number.isSafeInteger(toolCalls) || toolCalls <= 0) {
72
+ throw new Error(`${prefix}.toolCalls must be a positive safe integer`);
73
+ }
74
+ const readReserve = value.readReserve;
75
+ if (typeof readReserve !== "number" || !Number.isSafeInteger(readReserve) || readReserve < 0) {
76
+ throw new Error(`${prefix}.readReserve must be a non-negative safe integer`);
77
+ }
78
+ if (readReserve >= toolCalls) throw new Error(`${prefix}.readReserve must be less than ${prefix}.toolCalls`);
79
+ return { toolCalls, readReserve };
80
+ }
81
+
82
+ /** Strict validation and cloning for model, CLI, and persisted request inputs. */
83
+ export function cloneDispatchBudgetRequest(value: unknown, prefix = "budget"): DispatchBudgetRequest {
84
+ if (!isRecord(value)) throw new Error(`${prefix} must be an object`);
85
+ for (const key of Object.keys(value)) {
86
+ if (key !== "toolCalls" && key !== "readReserve" && key !== "retryRevision") {
87
+ throw new Error(`${prefix}.${key} is unknown`);
88
+ }
89
+ }
90
+ const phase = clonePhase({ toolCalls: value.toolCalls, readReserve: value.readReserve }, prefix);
91
+ const retryRevision =
92
+ value.retryRevision === undefined ? undefined : clonePhase(value.retryRevision, `${prefix}.retryRevision`);
93
+ return { ...phase, ...(retryRevision === undefined ? {} : { retryRevision }) };
94
+ }
95
+
96
+ function freezeEnvelope(envelope: RunToolBudgetEnvelope): RunToolBudgetEnvelope {
97
+ Object.freeze(envelope.policy.default);
98
+ Object.freeze(envelope.policy.maximum);
99
+ Object.freeze(envelope.policy);
100
+ if (envelope.request !== null) {
101
+ if (envelope.request.retryRevision !== undefined) Object.freeze(envelope.request.retryRevision);
102
+ Object.freeze(envelope.request);
103
+ }
104
+ Object.freeze(envelope.effective);
105
+ for (const reason of envelope.reasons) Object.freeze(reason);
106
+ Object.freeze(envelope.reasons);
107
+ return Object.freeze(envelope);
108
+ }
109
+
110
+ function phaseWithin(candidate: DispatchBudgetPhase, maximum: AgentBudgetPhase): boolean {
111
+ return candidate.toolCalls <= maximum.toolCalls && candidate.readReserve <= maximum.readReserve;
112
+ }
113
+
114
+ function samePhase(left: DispatchBudgetPhase, right: AgentBudgetPhase): boolean {
115
+ return left.toolCalls === right.toolCalls && left.readReserve === right.readReserve;
116
+ }
117
+
118
+ function assertRequestAdmitted(input: { request: DispatchBudgetRequest; policy: AgentBudget; hardCap: number }): void {
119
+ const { request, policy, hardCap } = input;
120
+ const authoredMaximum = policy.maximum ?? policy;
121
+ if (policy.maximum === undefined && !samePhase(request, policy)) {
122
+ throw new BudgetAdmissionError(
123
+ "exact-recipe-policy",
124
+ `the recipe pins ${policy.toolCalls} tool calls with a read reserve of ${policy.readReserve}`,
125
+ );
126
+ }
127
+ if (!phaseWithin(request, authoredMaximum)) {
128
+ throw new BudgetAdmissionError(
129
+ "recipe-maximum",
130
+ `the requested phase ${request.toolCalls}/${request.readReserve} exceeds recipe maximum ${authoredMaximum.toolCalls}/${authoredMaximum.readReserve}`,
131
+ );
132
+ }
133
+ if (request.toolCalls > hardCap) {
134
+ throw new BudgetAdmissionError(
135
+ "global-cap",
136
+ `the requested ${request.toolCalls} tool calls exceed the operator cap of ${hardCap}`,
137
+ );
138
+ }
139
+ const ceiling = request.retryRevision;
140
+ if (ceiling === undefined) return;
141
+ if (ceiling.toolCalls < request.toolCalls || ceiling.readReserve < request.readReserve) {
142
+ throw new BudgetAdmissionError(
143
+ "ceiling-below-request",
144
+ `retryRevision ${ceiling.toolCalls}/${ceiling.readReserve} must not be smaller than the requested phase ${request.toolCalls}/${request.readReserve}`,
145
+ );
146
+ }
147
+ if (policy.maximum === undefined && !samePhase(ceiling, policy)) {
148
+ throw new BudgetAdmissionError(
149
+ "exact-recipe-policy",
150
+ `the recipe pins ${policy.toolCalls} tool calls with a read reserve of ${policy.readReserve}`,
151
+ );
152
+ }
153
+ if (!phaseWithin(ceiling, authoredMaximum)) {
154
+ throw new BudgetAdmissionError(
155
+ "recipe-maximum",
156
+ `retryRevision ${ceiling.toolCalls}/${ceiling.readReserve} exceeds recipe maximum ${authoredMaximum.toolCalls}/${authoredMaximum.readReserve}`,
157
+ );
158
+ }
159
+ if (ceiling.toolCalls > hardCap) {
160
+ throw new BudgetAdmissionError(
161
+ "global-cap",
162
+ `retryRevision ${ceiling.toolCalls} exceeds the operator cap of ${hardCap}`,
163
+ );
164
+ }
165
+ }
166
+
167
+ export interface ResolveToolBudgetEnvelopeInput {
168
+ recipeId: string;
169
+ policy: AgentBudget;
170
+ request?: DispatchBudgetRequest;
171
+ hardCap: number;
172
+ hasReadTool: boolean;
173
+ retry: boolean;
174
+ revision: boolean;
175
+ }
176
+
177
+ /** Resolve the one immutable envelope used by admission, enforcement, and evidence. */
178
+ export function resolveToolBudgetEnvelope(input: ResolveToolBudgetEnvelopeInput): RunToolBudgetEnvelope {
179
+ if (!Number.isSafeInteger(input.hardCap) || input.hardCap <= 0) {
180
+ throw new Error("dispatch: worker tool-call cap must be a positive safe integer");
181
+ }
182
+ if (input.request !== undefined)
183
+ assertRequestAdmitted({ request: input.request, policy: input.policy, hardCap: input.hardCap });
184
+
185
+ const request = input.request === undefined ? null : cloneDispatchBudgetRequest(input.request);
186
+ const base = request ?? input.policy;
187
+ const reasons: BudgetEnvelopeReason[] = [];
188
+ let selected: DispatchBudgetPhase = base;
189
+ const phaseKind = input.retry ? "retry" : input.revision ? "revision" : null;
190
+ if (phaseKind !== null) {
191
+ const ceiling = request?.retryRevision;
192
+ if (ceiling !== undefined && !samePhase(ceiling, base)) {
193
+ selected = ceiling;
194
+ reasons.push({
195
+ code: phaseKind === "retry" ? "retry-growth-authorized" : "revision-growth-authorized",
196
+ detail: `${phaseKind} phase grew from ${base.toolCalls}/${base.readReserve} to the preauthorized ceiling ${ceiling.toolCalls}/${ceiling.readReserve}`,
197
+ });
198
+ } else {
199
+ reasons.push({
200
+ code: phaseKind === "retry" ? "retry-growth-denied" : "revision-growth-denied",
201
+ detail:
202
+ ceiling === undefined
203
+ ? `${phaseKind} phase retained ${base.toolCalls}/${base.readReserve} because the original request declared no ceiling`
204
+ : `${phaseKind} phase retained ${base.toolCalls}/${base.readReserve} because the declared ceiling did not authorize growth`,
205
+ });
206
+ }
207
+ }
208
+ const revisionCeiling = phaseKind === null ? request?.retryRevision : undefined;
209
+ if (revisionCeiling !== undefined && !samePhase(revisionCeiling, selected)) {
210
+ reasons.push({
211
+ code: "revision-growth-authorized",
212
+ detail: `a result-contract revision may grow from ${selected.toolCalls}/${selected.readReserve} to the preauthorized ceiling ${revisionCeiling.toolCalls}/${revisionCeiling.readReserve}`,
213
+ });
214
+ }
215
+
216
+ let toolCalls = selected.toolCalls;
217
+ if (toolCalls > input.hardCap) {
218
+ reasons.push({
219
+ code: "global-cap-clamp",
220
+ detail: `tool calls were clamped from ${toolCalls} to the operator cap of ${input.hardCap}`,
221
+ });
222
+ toolCalls = input.hardCap;
223
+ }
224
+ let readReserve = selected.readReserve;
225
+ if (!input.hasReadTool && readReserve > 0) {
226
+ reasons.push({
227
+ code: "read-reserve-unavailable-clamp",
228
+ detail: `read reserve was clamped from ${readReserve} to 0 because the read tool is unavailable`,
229
+ });
230
+ readReserve = 0;
231
+ }
232
+ const maximumEffectiveReserve = Math.max(0, toolCalls - 1);
233
+ if (readReserve > maximumEffectiveReserve) {
234
+ reasons.push({
235
+ code: "read-reserve-cap-clamp",
236
+ detail: `read reserve was clamped from ${readReserve} to ${maximumEffectiveReserve} after the tool-call cap`,
237
+ });
238
+ readReserve = maximumEffectiveReserve;
239
+ }
240
+ let effectiveRevision: DispatchBudgetPhase | undefined;
241
+ if (revisionCeiling !== undefined && !samePhase(revisionCeiling, selected)) {
242
+ let revisionReadReserve = revisionCeiling.readReserve;
243
+ if (!input.hasReadTool && revisionReadReserve > 0) {
244
+ reasons.push({
245
+ code: "read-reserve-unavailable-clamp",
246
+ detail: `result-contract revision read reserve was clamped from ${revisionReadReserve} to 0 because the read tool is unavailable`,
247
+ });
248
+ revisionReadReserve = 0;
249
+ }
250
+ effectiveRevision = { toolCalls: revisionCeiling.toolCalls, readReserve: revisionReadReserve };
251
+ }
252
+
253
+ const authoredMaximum = input.policy.maximum ?? input.policy;
254
+ return freezeEnvelope({
255
+ version: 1,
256
+ policy: {
257
+ recipeId: input.recipeId,
258
+ default: {
259
+ toolCalls: input.policy.toolCalls,
260
+ readReserve: input.policy.readReserve,
261
+ synthesis: input.policy.synthesis,
262
+ },
263
+ maximum: { toolCalls: authoredMaximum.toolCalls, readReserve: authoredMaximum.readReserve },
264
+ exact: input.policy.maximum === undefined,
265
+ },
266
+ request,
267
+ effective: {
268
+ toolCalls,
269
+ readReserve,
270
+ synthesis: input.policy.synthesis,
271
+ hardCap: input.hardCap,
272
+ ...(effectiveRevision === undefined ? {} : { revision: effectiveRevision }),
273
+ },
274
+ reasons,
275
+ });
276
+ }
277
+
278
+ export function formatBudgetPolicy(envelope: RunToolBudgetEnvelope): string {
279
+ const policy = envelope.policy;
280
+ const range = policy.exact
281
+ ? `exact ${policy.default.toolCalls}/${policy.default.readReserve}`
282
+ : `default ${policy.default.toolCalls}/${policy.default.readReserve}, max ${policy.maximum.toolCalls}/${policy.maximum.readReserve}`;
283
+ return `${policy.recipeId} ${range}, synthesis=${policy.default.synthesis ? "on" : "off"}`;
284
+ }
285
+
286
+ export function formatBudgetRequest(envelope: RunToolBudgetEnvelope): string {
287
+ const request = envelope.request;
288
+ if (request === null) return "recipe default";
289
+ const ceiling = request.retryRevision;
290
+ return ceiling === undefined
291
+ ? `${request.toolCalls}/${request.readReserve}, no retry/revision growth`
292
+ : `${request.toolCalls}/${request.readReserve}, retry/revision ceiling ${ceiling.toolCalls}/${ceiling.readReserve}`;
293
+ }
294
+
295
+ export function formatEffectiveBudget(envelope: RunToolBudgetEnvelope): string {
296
+ const budget = envelope.effective;
297
+ const revision = budget.revision
298
+ ? `, result revision ${budget.revision.toolCalls}/${budget.revision.readReserve}`
299
+ : "";
300
+ return `${budget.toolCalls}/${budget.readReserve}${revision}, lifetime cap ${budget.hardCap}, synthesis=${budget.synthesis ? "on" : "off"}`;
301
+ }
302
+
303
+ export function formatBudgetReasons(envelope: RunToolBudgetEnvelope): string {
304
+ return envelope.reasons.length === 0 ? "none" : envelope.reasons.map((reason) => reason.code).join(",");
305
+ }
306
+
307
+ const BUDGET_REASON_CODES: ReadonlySet<string> = new Set<BudgetEnvelopeReasonCode>([
308
+ "global-cap-clamp",
309
+ "read-reserve-unavailable-clamp",
310
+ "read-reserve-cap-clamp",
311
+ "retry-growth-authorized",
312
+ "retry-growth-denied",
313
+ "revision-growth-authorized",
314
+ "revision-growth-denied",
315
+ ]);
316
+
317
+ /** Safely project an envelope that crossed a persistence or event boundary. */
318
+ export function cloneRunToolBudgetEnvelope(value: unknown): RunToolBudgetEnvelope | undefined {
319
+ try {
320
+ if (!isRecord(value) || value.version !== 1 || !isRecord(value.policy) || !isRecord(value.effective)) {
321
+ return undefined;
322
+ }
323
+ const recipeId = value.policy.recipeId;
324
+ if (typeof recipeId !== "string" || recipeId.length === 0 || typeof value.policy.exact !== "boolean") {
325
+ return undefined;
326
+ }
327
+ if (!isRecord(value.policy.default) || typeof value.policy.default.synthesis !== "boolean") return undefined;
328
+ const defaultPhase = clonePhase(
329
+ { toolCalls: value.policy.default.toolCalls, readReserve: value.policy.default.readReserve },
330
+ "budget.policy.default",
331
+ );
332
+ const maximum = clonePhase(value.policy.maximum, "budget.policy.maximum");
333
+ const request = value.request === null ? null : cloneDispatchBudgetRequest(value.request);
334
+ const effectivePhase = clonePhase(
335
+ { toolCalls: value.effective.toolCalls, readReserve: value.effective.readReserve },
336
+ "budget.effective",
337
+ );
338
+ const revision =
339
+ value.effective.revision === undefined
340
+ ? undefined
341
+ : clonePhase(value.effective.revision, "budget.effective.revision");
342
+ if (
343
+ typeof value.effective.synthesis !== "boolean" ||
344
+ typeof value.effective.hardCap !== "number" ||
345
+ !Number.isSafeInteger(value.effective.hardCap) ||
346
+ value.effective.hardCap <= 0 ||
347
+ !Array.isArray(value.reasons)
348
+ ) {
349
+ return undefined;
350
+ }
351
+ if (
352
+ maximum.toolCalls < defaultPhase.toolCalls ||
353
+ maximum.readReserve < defaultPhase.readReserve ||
354
+ (value.policy.exact && !samePhase(maximum, defaultPhase)) ||
355
+ effectivePhase.toolCalls > value.effective.hardCap ||
356
+ value.effective.synthesis !== value.policy.default.synthesis ||
357
+ (revision !== undefined &&
358
+ (revision.toolCalls < effectivePhase.toolCalls ||
359
+ revision.toolCalls > value.effective.hardCap ||
360
+ (revision.toolCalls === effectivePhase.toolCalls && revision.readReserve <= effectivePhase.readReserve)))
361
+ ) {
362
+ return undefined;
363
+ }
364
+ const reasons: BudgetEnvelopeReason[] = [];
365
+ for (const reason of value.reasons) {
366
+ if (
367
+ !isRecord(reason) ||
368
+ typeof reason.code !== "string" ||
369
+ !BUDGET_REASON_CODES.has(reason.code) ||
370
+ typeof reason.detail !== "string"
371
+ ) {
372
+ return undefined;
373
+ }
374
+ reasons.push({ code: reason.code as BudgetEnvelopeReasonCode, detail: reason.detail });
375
+ }
376
+ return freezeEnvelope({
377
+ version: 1,
378
+ policy: {
379
+ recipeId,
380
+ default: { ...defaultPhase, synthesis: value.policy.default.synthesis },
381
+ maximum,
382
+ exact: value.policy.exact,
383
+ },
384
+ request,
385
+ effective: {
386
+ ...effectivePhase,
387
+ synthesis: value.effective.synthesis,
388
+ hardCap: value.effective.hardCap,
389
+ ...(revision === undefined ? {} : { revision }),
390
+ },
391
+ reasons,
392
+ });
393
+ } catch {
394
+ return undefined;
395
+ }
396
+ }
@@ -6,6 +6,7 @@ import type { ProtectedArtifactState } from "../safety/protected-artifacts.js";
6
6
  import type { AssignmentId, DispatchAssignment } from "./assignment.js";
7
7
  import type { DurableAssignmentRecord } from "./assignment-store.js";
8
8
  import type { DetachedBatchRecord, RegisterDetachedBatchInput } from "./batch-store.js";
9
+ import type { RunToolBudgetEnvelope } from "./budget-envelope.js";
9
10
  import type { ExecutionRole } from "./execution-role.js";
10
11
  import type { DispatchReservationRecord, ReservationTopology } from "./reservation-store.js";
11
12
  import type { ApprovedAssignmentRoute } from "./route-approval.js";
@@ -111,6 +112,7 @@ export interface DispatchSnapshot {
111
112
  outcomePhase: string;
112
113
  heartbeat: "alive" | "stale" | "dead" | "n/a";
113
114
  lineage: RunLineage;
115
+ budget?: RunToolBudgetEnvelope;
114
116
  startedAt: string;
115
117
  /** Execution-only elapsed time retained for runtime monitoring. */
116
118
  elapsedMs: number;