@khoavu882/doflow 2.0.0-beta.5 → 2.0.0

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 (471) hide show
  1. package/README.md +12 -11
  2. package/bin/doflow.js +21 -11
  3. package/core/.antigravity-plugin/plugin.json +1 -2
  4. package/core/.claude-plugin/plugin.json +1 -1
  5. package/core/.codex-plugin/plugin.json +1 -1
  6. package/core/.plugin/plugin.json +1 -1
  7. package/core/harnesses/claude/hooks/lib.sh +43 -24
  8. package/core/harnesses/claude/hooks/permission-audit.sh +3 -0
  9. package/core/harnesses/claude/hooks/post-compact.sh +35 -32
  10. package/core/harnesses/claude/hooks/post-edit-lint.sh +2 -0
  11. package/core/harnesses/claude/hooks/post-shell-observe.sh +48 -0
  12. package/core/harnesses/claude/hooks/post-tool-failure.sh +3 -0
  13. package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
  14. package/core/harnesses/claude/hooks/session-end.sh +23 -10
  15. package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
  16. package/core/harnesses/claude/settings/settings.json +18 -0
  17. package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
  18. package/core/harnesses/codex/agents/core-implementer.toml +1 -0
  19. package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
  20. package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
  21. package/core/harnesses/codex/agents/research-writer.toml +1 -1
  22. package/core/harnesses/codex/agents/reviewer.toml +10 -0
  23. package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
  24. package/core/harnesses/codex/agents/system-architect.toml +1 -1
  25. package/core/harnesses/codex/hooks/lib.sh +44 -25
  26. package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
  27. package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
  28. package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
  29. package/core/harnesses/codex/hooks/subagent-audit.sh +1 -0
  30. package/core/harnesses/copilot/hooks/doflow-lib.sh +93 -0
  31. package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
  32. package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +9 -0
  33. package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
  34. package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
  35. package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
  36. package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
  37. package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
  38. package/core/harnesses/copilot/hooks/doflow.json +57 -0
  39. package/core/harnesses/kiro/agents/doflow.json +1 -1
  40. package/core/harnesses/kiro/hooks/lib.sh +26 -6
  41. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +1 -0
  42. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +1 -0
  43. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +1 -0
  44. package/core/harnesses/kiro/hooks/session-start.sh +1 -0
  45. package/core/harnesses/kiro/hooks/stop-check.sh +1 -0
  46. package/core/harnesses/pi/extensions/doflow-guard.js +8 -0
  47. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +0 -1
  48. package/core/harnesses/shared/hooks/bridge/bridge.sh +102 -9
  49. package/core/harnesses/shared/hooks/bridge/envelope.jq +87 -18
  50. package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
  51. package/core/harnesses/shared/hooks/bridge/respond.sh +10 -2
  52. package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +28 -14
  53. package/core/harnesses/shared/hooks/policies/capture-failure.sh +48 -15
  54. package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
  55. package/core/harnesses/shared/hooks/policies/contract.sh +3 -0
  56. package/core/harnesses/shared/hooks/policies/deny-json.sh +2 -11
  57. package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
  58. package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
  59. package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
  60. package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
  61. package/core/harnesses/shared/hooks/policies/lib.sh +63 -40
  62. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +3 -2
  63. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +213 -12
  64. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +114 -38
  65. package/core/harnesses/shared/hooks/policies/session-context.sh +30 -1
  66. package/core/harnesses/shared/hooks/policies/stop-check.sh +20 -9
  67. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +5 -2
  68. package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
  69. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +60 -155
  70. package/core/harnesses/shared/hooks/stream-hook-runner.js +47 -8
  71. package/core/harnesses/shared/locator/doflow-run +2 -3
  72. package/core/registry/advice.json +3 -3
  73. package/core/registry/assets.json +55 -82
  74. package/core/registry/capabilities.json +12 -0
  75. package/core/registry/contracts.json +58 -31
  76. package/core/registry/contribution-points.json +32 -0
  77. package/core/registry/gates.json +32 -0
  78. package/core/registry/guidance-caps.json +40 -0
  79. package/core/registry/harnesses.json +1442 -363
  80. package/core/registry/lifecycle.json +485 -304
  81. package/core/registry/mcp.json +5 -1
  82. package/core/registry/models.json +7 -1
  83. package/core/registry/package-templates/README.md +19 -0
  84. package/core/registry/package-templates/agent/files/agent.md +9 -0
  85. package/core/registry/package-templates/agent/template.json +4 -0
  86. package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
  87. package/core/registry/package-templates/capability-provider/template.json +11 -0
  88. package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
  89. package/core/registry/package-templates/debate-preset/template.json +4 -0
  90. package/core/registry/package-templates/review-preset/files/preset.json +30 -0
  91. package/core/registry/package-templates/review-preset/template.json +4 -0
  92. package/core/registry/package-templates/skill/files/SKILL.md +8 -0
  93. package/core/registry/package-templates/skill/template.json +4 -0
  94. package/core/registry/package-templates/worker/files/worker.json +92 -0
  95. package/core/registry/package-templates/worker/template.json +11 -0
  96. package/core/registry/packages/doflow/doflow-package.json +15 -0
  97. package/core/registry/packages/doflow/presets/debate-3.json +26 -0
  98. package/core/registry/protocols.json +61 -0
  99. package/core/registry/routes.json +17 -0
  100. package/core/registry/rules.json +431 -0
  101. package/core/registry/settings.json +276 -0
  102. package/core/registry/storage.json +679 -31
  103. package/core/registry/telemetry.json +236 -0
  104. package/core/registry/transcripts.json +142 -0
  105. package/core/registry/verification.json +76 -1
  106. package/core/registry/workflows.json +55 -0
  107. package/core/shared/agent-specs/core-implementer.md +3 -0
  108. package/core/shared/agent-specs/delivery-lead.md +68 -0
  109. package/core/shared/agent-specs/quality-guardian.md +5 -1
  110. package/core/shared/agent-specs/research-writer.md +10 -3
  111. package/core/shared/agent-specs/reviewer.md +53 -0
  112. package/core/shared/agent-specs/spec-analyst.md +5 -6
  113. package/core/shared/agent-specs/system-architect.md +5 -1
  114. package/core/shared/guidance/DOFLOW_CORE.md +1 -5
  115. package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
  116. package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
  117. package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
  118. package/core/shared/guidance/pointers/codex.md +1 -4
  119. package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
  120. package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
  121. package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
  122. package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
  123. package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
  124. package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
  125. package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
  126. package/core/shared/guidance/references/ORCHESTRATION.md +1 -2
  127. package/core/shared/guidance/rules/RULES.md +45 -0
  128. package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
  129. package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
  130. package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
  131. package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
  132. package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
  133. package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
  134. package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
  135. package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
  136. package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
  137. package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
  138. package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
  139. package/core/shared/scripts/doflow/bash/validate-artifacts.sh +48 -4
  140. package/core/shared/scripts/doflow/bin/doflow-run +149 -34
  141. package/core/shared/skills/do/SKILL.md +12 -10
  142. package/core/shared/skills/do/references/advice_calls.md +26 -0
  143. package/core/shared/skills/do/references/pm_routing.md +1 -2
  144. package/core/shared/skills/do/references/program_mode.md +31 -0
  145. package/core/shared/skills/do-brainstorm/SKILL.md +20 -74
  146. package/core/shared/skills/do-brainstorm/references/handoff_branches.md +35 -0
  147. package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
  148. package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
  149. package/core/shared/skills/do-code-review/SKILL.md +41 -144
  150. package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
  151. package/core/shared/skills/do-code-review/references/tools.md +90 -0
  152. package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
  153. package/core/shared/skills/do-design/SKILL.md +18 -17
  154. package/core/shared/skills/do-diagnose/SKILL.md +14 -4
  155. package/core/shared/skills/do-document/SKILL.md +11 -1
  156. package/core/shared/skills/do-document/references/deep_research.md +1 -2
  157. package/core/shared/skills/do-document/references/embedded-research.md +18 -6
  158. package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
  159. package/core/shared/skills/do-execute-plan/SKILL.md +15 -58
  160. package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
  161. package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
  162. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +26 -1
  163. package/core/shared/skills/do-execute-plan/references/run_position.md +13 -1
  164. package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
  165. package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
  166. package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
  167. package/core/shared/skills/do-flow/SKILL.md +8 -9
  168. package/core/shared/skills/do-flow/references/task_classes.md +2 -2
  169. package/core/shared/skills/do-git/SKILL.md +4 -0
  170. package/core/shared/skills/do-implement/SKILL.md +4 -7
  171. package/core/shared/skills/do-plan/SKILL.md +26 -15
  172. package/core/shared/skills/do-test/SKILL.md +7 -4
  173. package/core/shared/templates/doflow/constitution-template.md +9 -3
  174. package/core/shared/templates/doflow/data-model-template.md +1 -1
  175. package/core/shared/templates/doflow/design-template.md +113 -16
  176. package/core/shared/templates/doflow/plan-template.md +11 -10
  177. package/core/shared/templates/doflow/question-log-template.md +14 -2
  178. package/core/shared/templates/doflow/requirement-template.md +1 -1
  179. package/core/worker-evidence/antigravity/checks.json +14 -0
  180. package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
  181. package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
  182. package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
  183. package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
  184. package/core/worker-evidence/codex/checks.json +38 -0
  185. package/package.json +10 -6
  186. package/src/adapters/antigravity/index.js +143 -66
  187. package/src/adapters/claude/index.js +24 -4
  188. package/src/adapters/codex/agents.js +25 -15
  189. package/src/adapters/codex/index.js +48 -13
  190. package/src/adapters/codex/native-projection.js +6 -2
  191. package/src/adapters/copilot/index.js +58 -23
  192. package/src/adapters/copy-tree.js +290 -44
  193. package/src/adapters/guidance-manifest.js +71 -0
  194. package/src/adapters/guidance-pointer.js +19 -0
  195. package/src/adapters/hook-commands.js +6 -10
  196. package/src/adapters/index.js +1 -0
  197. package/src/adapters/kiro/index.js +23 -12
  198. package/src/adapters/opencode/index.js +8 -8
  199. package/src/adapters/pi/hooks.js +2 -1
  200. package/src/adapters/pi/index.js +14 -7
  201. package/src/adapters/resolve-source.js +26 -2
  202. package/src/cli/commands/install.js +2 -1
  203. package/src/cli/commands/reconcile.js +53 -9
  204. package/src/cli/commands/remove.js +2 -2
  205. package/src/cli/commands/rollback.js +6 -3
  206. package/src/cli/commands/self-update.js +18 -9
  207. package/src/cli/commands/status.js +21 -2
  208. package/src/cli/commands/ui.js +99 -0
  209. package/src/cli/commands/update.js +2 -1
  210. package/src/cli/index.js +322 -29
  211. package/src/cli/runtime-commands.js +209 -57
  212. package/src/cli/shared.js +0 -2
  213. package/src/helper/git.js +1 -1
  214. package/src/helper/guardrails.js +1 -1
  215. package/src/helper/marker-merge.js +31 -13
  216. package/src/helper/package-ids.js +37 -0
  217. package/src/helper/semver.js +94 -0
  218. package/src/install/backup.js +105 -15
  219. package/src/install/context.js +1 -1
  220. package/src/install/targets.js +31 -16
  221. package/src/lifecycle/coverage.js +20 -3
  222. package/src/lifecycle/hook-tier.js +160 -0
  223. package/src/lifecycle/index.js +412 -78
  224. package/src/lifecycle/retired-harnesses.js +318 -0
  225. package/src/lifecycle/view.js +35 -15
  226. package/src/registry/index.js +551 -26
  227. package/src/registry/worker-ids.js +45 -0
  228. package/src/release/build-package.js +0 -3
  229. package/src/release/docs-blocks.js +492 -0
  230. package/src/release/preflight.js +425 -0
  231. package/src/release/rehearse.js +401 -0
  232. package/src/release/release-workflow.js +733 -0
  233. package/src/release/sync-plugin-versions.js +43 -7
  234. package/src/runtime/advice/bench-gate.js +25 -0
  235. package/src/runtime/advice/cli.js +59 -16
  236. package/src/runtime/advice/client.js +32 -9
  237. package/src/runtime/advice/config.js +98 -133
  238. package/src/runtime/advice/doctor.js +1 -1
  239. package/src/runtime/advice/elevation.js +97 -0
  240. package/src/runtime/advice/endpoint-guard.js +9 -3
  241. package/src/runtime/advice/mode.js +37 -0
  242. package/src/runtime/advice/record.js +18 -5
  243. package/src/runtime/advice/registry.js +4 -1
  244. package/src/runtime/advice/shadow-config.js +229 -0
  245. package/src/runtime/advice/shadow-dispatcher.js +276 -0
  246. package/src/runtime/brief-rules.js +81 -0
  247. package/src/runtime/brief-tools.js +111 -34
  248. package/src/runtime/candidate.js +20 -11
  249. package/src/runtime/capabilities/executor.js +450 -0
  250. package/src/runtime/capabilities/loopback.js +41 -0
  251. package/src/runtime/capabilities/sanitizer.js +88 -0
  252. package/src/runtime/capability-router.js +135 -3
  253. package/src/runtime/checkouts.js +73 -11
  254. package/src/runtime/claims.js +13 -7
  255. package/src/runtime/cli.js +196 -19
  256. package/src/runtime/config-home.js +42 -0
  257. package/src/runtime/context/arbitrate.js +767 -0
  258. package/src/runtime/context/delivery-store.js +4 -6
  259. package/src/runtime/context/emission-rules.js +118 -0
  260. package/src/runtime/context/identity.js +227 -0
  261. package/src/runtime/context/primary-command.js +117 -0
  262. package/src/runtime/context/primary.js +344 -0
  263. package/src/runtime/context/summary-store.js +655 -0
  264. package/src/runtime/context-pack.js +154 -78
  265. package/src/runtime/decision-register.js +13 -4
  266. package/src/runtime/dispatch/admission.js +392 -0
  267. package/src/runtime/dispatch/attempt-end.js +274 -0
  268. package/src/runtime/dispatch/command.js +628 -0
  269. package/src/runtime/dispatch/controls.js +904 -0
  270. package/src/runtime/dispatch/envelope.js +276 -0
  271. package/src/runtime/dispatch/grants.js +310 -0
  272. package/src/runtime/dispatch/integrate.js +617 -0
  273. package/src/runtime/dispatch/loop.js +1835 -0
  274. package/src/runtime/dispatch/store.js +352 -0
  275. package/src/runtime/dispatch/verify-run.js +201 -0
  276. package/src/runtime/evidence-ledger.js +34 -7
  277. package/src/runtime/failure/capture.js +3 -1
  278. package/src/runtime/failure/cli.js +25 -9
  279. package/src/runtime/failure/home.js +57 -30
  280. package/src/runtime/failure/store.js +7 -2
  281. package/src/runtime/feature-pointer.js +174 -0
  282. package/src/runtime/feature-resolve.js +13 -2
  283. package/src/runtime/fleet/command.js +77 -0
  284. package/src/runtime/fleet/projects.js +178 -0
  285. package/src/runtime/fleet/tree.js +383 -0
  286. package/src/runtime/freshness.js +33 -3
  287. package/src/runtime/git-memo.js +46 -0
  288. package/src/runtime/guidance/ablation.js +427 -0
  289. package/src/runtime/health.js +565 -17
  290. package/src/runtime/history-compactor.js +13 -3
  291. package/src/runtime/implementation-gate.js +277 -22
  292. package/src/runtime/inventory/coverage.js +1 -1
  293. package/src/runtime/inventory/index.js +1 -1
  294. package/src/runtime/inventory/precedence.js +2 -2
  295. package/src/runtime/inventory/siblings.js +6 -4
  296. package/src/runtime/knowledge/code-corpus.js +277 -0
  297. package/src/runtime/knowledge/code-lexical.js +130 -0
  298. package/src/runtime/knowledge/code-retrieval.js +307 -0
  299. package/src/runtime/knowledge/code-validation.js +106 -0
  300. package/src/runtime/knowledge/gp3-gate.js +74 -0
  301. package/src/runtime/knowledge/graph-adapter.js +288 -0
  302. package/src/runtime/knowledge/graph-fallback.js +394 -0
  303. package/src/runtime/knowledge/hybrid.js +136 -8
  304. package/src/runtime/knowledge/retrieval.js +34 -1
  305. package/src/runtime/knowledge/rtk-compress.js +77 -0
  306. package/src/runtime/knowledge/semble-bridge.py +106 -0
  307. package/src/runtime/knowledge/semble-lifecycle.js +114 -0
  308. package/src/runtime/knowledge/semble-provider.js +365 -0
  309. package/src/runtime/lifecycle/cli.js +16 -6
  310. package/src/runtime/lifecycle/event-store.js +8 -6
  311. package/src/runtime/lifecycle/followup.js +6 -1
  312. package/src/runtime/lifecycle/overview.js +24 -1
  313. package/src/runtime/lifecycle/report-store.js +8 -10
  314. package/src/runtime/lifecycle/retention.js +95 -31
  315. package/src/runtime/lifecycle/root.js +9 -1
  316. package/src/runtime/lifecycle/status.js +34 -17
  317. package/src/runtime/lifecycle/store-upkeep.js +56 -7
  318. package/src/runtime/mask.js +134 -7
  319. package/src/runtime/mcp-consultations.js +40 -6
  320. package/src/runtime/model-router.js +79 -7
  321. package/src/runtime/models/catalogue.js +191 -0
  322. package/src/runtime/models/command.js +153 -0
  323. package/src/runtime/off-words.js +12 -0
  324. package/src/runtime/outcome.js +2 -2
  325. package/src/runtime/packages/command.js +1009 -0
  326. package/src/runtime/packages/contributions.js +116 -0
  327. package/src/runtime/packages/digest.js +132 -0
  328. package/src/runtime/packages/fetch.js +182 -0
  329. package/src/runtime/packages/grants.js +192 -0
  330. package/src/runtime/packages/index-file.js +134 -0
  331. package/src/runtime/packages/instruction.js +90 -0
  332. package/src/runtime/packages/manifest.js +331 -0
  333. package/src/runtime/packages/points.js +477 -0
  334. package/src/runtime/packages/programs.js +216 -0
  335. package/src/runtime/packages/scaffold.js +277 -0
  336. package/src/runtime/packages/source.js +90 -0
  337. package/src/runtime/packages/state.js +164 -0
  338. package/src/runtime/packages/store.js +449 -0
  339. package/src/runtime/packages/tar.js +106 -0
  340. package/src/runtime/packages/view.js +406 -0
  341. package/src/runtime/plan-check.js +59 -0
  342. package/src/runtime/plan-content.js +324 -0
  343. package/src/runtime/policy-init.js +118 -0
  344. package/src/runtime/profiles/doctor.js +96 -0
  345. package/src/runtime/profiles/resolve.js +244 -0
  346. package/src/runtime/profiles/rollback.js +254 -0
  347. package/src/runtime/profiles/sampler.js +41 -0
  348. package/src/runtime/readiness-record.js +16 -11
  349. package/src/runtime/readiness.js +46 -5
  350. package/src/runtime/research-request.js +50 -25
  351. package/src/runtime/retrieval-plan.js +10 -30
  352. package/src/runtime/retrieval-record.js +75 -5
  353. package/src/runtime/review/command.js +246 -0
  354. package/src/runtime/review/dominance.js +68 -0
  355. package/src/runtime/review/findings.js +434 -0
  356. package/src/runtime/review/identity.js +255 -0
  357. package/src/runtime/review/protocol-command.js +109 -0
  358. package/src/runtime/review/protocols.js +405 -0
  359. package/src/runtime/review/resolve-protocol.js +302 -0
  360. package/src/runtime/review/start.js +530 -0
  361. package/src/runtime/roadmap/facts.js +479 -0
  362. package/src/runtime/roadmap/gates.js +270 -0
  363. package/src/runtime/roadmap/loop.js +1636 -0
  364. package/src/runtime/roadmap/merge.js +381 -0
  365. package/src/runtime/roadmap/rows.js +1113 -0
  366. package/src/runtime/roadmap/seed.js +951 -0
  367. package/src/runtime/rules-report.js +336 -0
  368. package/src/runtime/scaffold/artifacts.js +60 -9
  369. package/src/runtime/scaffold/generate.js +21 -9
  370. package/src/runtime/settings/autopilot.js +106 -0
  371. package/src/runtime/settings/cli.js +280 -0
  372. package/src/runtime/settings/config-write.js +314 -0
  373. package/src/runtime/settings/files.js +271 -0
  374. package/src/runtime/settings/legacy.js +154 -0
  375. package/src/runtime/settings/lexical.js +56 -0
  376. package/src/runtime/settings/prepared-run.js +269 -0
  377. package/src/runtime/settings/project-layers.js +118 -0
  378. package/src/runtime/settings/refs.js +288 -0
  379. package/src/runtime/settings/registry.js +553 -0
  380. package/src/runtime/settings/resolve.js +713 -0
  381. package/src/runtime/settings/shared.js +37 -0
  382. package/src/runtime/settings/worker-billing.js +107 -0
  383. package/src/runtime/settings/write.js +269 -0
  384. package/src/runtime/slot-command.js +78 -0
  385. package/src/runtime/slots.js +671 -0
  386. package/src/runtime/source-version.js +1 -0
  387. package/src/runtime/state-root.js +217 -0
  388. package/src/runtime/storage/cli.js +12 -5
  389. package/src/runtime/storage/engine.js +9 -2
  390. package/src/runtime/storage/registry.js +14 -1
  391. package/src/runtime/storage/roots.js +66 -16
  392. package/src/runtime/storage/rules.js +82 -10
  393. package/src/runtime/storage/upkeep.js +10 -2
  394. package/src/runtime/storage/work-status.js +245 -26
  395. package/src/runtime/task-attempts.js +589 -69
  396. package/src/runtime/task-classifier.js +47 -11
  397. package/src/runtime/task-graph.js +373 -0
  398. package/src/runtime/task-run.js +778 -0
  399. package/src/runtime/task-scope.js +176 -55
  400. package/src/runtime/task-sources.js +147 -0
  401. package/src/runtime/task-state.js +374 -64
  402. package/src/runtime/team/admission.js +301 -0
  403. package/src/runtime/team/envelope.js +324 -0
  404. package/src/runtime/team/review-guard.js +156 -0
  405. package/src/runtime/trace/analyze.js +427 -0
  406. package/src/runtime/trace/baseline.js +770 -0
  407. package/src/runtime/trace/ledger.js +63 -140
  408. package/src/runtime/trace/otlp-export.js +568 -0
  409. package/src/runtime/trace/otlp-map.js +481 -0
  410. package/src/runtime/trace/spans.js +745 -0
  411. package/src/runtime/trace/telemetry.js +225 -0
  412. package/src/runtime/trace/views.js +1 -0
  413. package/src/runtime/transcripts/command.js +98 -0
  414. package/src/runtime/transcripts/cursor.js +326 -0
  415. package/src/runtime/transcripts/read.js +126 -0
  416. package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
  417. package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
  418. package/src/runtime/transcripts/readers/copilot-events.js +90 -0
  419. package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
  420. package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
  421. package/src/runtime/transcripts/registry.js +161 -0
  422. package/src/runtime/transcripts/stats.js +161 -0
  423. package/src/runtime/verification/check-key.js +628 -0
  424. package/src/runtime/verification/contract-runner.js +546 -11
  425. package/src/runtime/verification/engine.js +499 -9
  426. package/src/runtime/verification/impact.js +617 -0
  427. package/src/runtime/verification/inflight.js +205 -0
  428. package/src/runtime/verification/registry.js +176 -0
  429. package/src/runtime/verification/reuse.js +184 -0
  430. package/src/runtime/verification/telemetry-emit.js +254 -0
  431. package/src/runtime/verification/test-selection.js +67 -7
  432. package/src/runtime/worker/active.js +90 -0
  433. package/src/runtime/worker/cancel.js +222 -0
  434. package/src/runtime/worker/catalogue.js +122 -0
  435. package/src/runtime/worker/command.js +677 -0
  436. package/src/runtime/worker/evidence.js +288 -0
  437. package/src/runtime/worker/launch.js +288 -0
  438. package/src/runtime/worker/models.js +137 -0
  439. package/src/runtime/worker/parsers.js +218 -0
  440. package/src/runtime/worker/probes/index.js +706 -0
  441. package/src/runtime/worker/process-tree.js +415 -0
  442. package/src/runtime/worker/quota.js +43 -0
  443. package/src/runtime/worker/readiness.js +713 -0
  444. package/src/runtime/worker/run-store.js +166 -0
  445. package/src/runtime/worker/supervisor.js +541 -0
  446. package/src/runtime/worker/worktree-check.js +252 -0
  447. package/src/runtime/workflow-engine.js +5 -1
  448. package/src/runtime/workflow-orchestrator.js +578 -63
  449. package/src/runtime/worktree.js +149 -22
  450. package/src/state/index.js +1 -1
  451. package/src/state/lockfile.js +64 -10
  452. package/src/state/run-lock.js +1 -1
  453. package/types/node-commonjs.d.ts +106 -7
  454. package/core/harnesses/gemini/hooks/hooks.json +0 -77
  455. package/core/harnesses/gemini/hooks/lib.sh +0 -302
  456. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -10
  457. package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
  458. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -10
  459. package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
  460. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -10
  461. package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
  462. package/core/harnesses/gemini/hooks/session-end.sh +0 -5
  463. package/core/harnesses/gemini/hooks/session-start.sh +0 -7
  464. package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
  465. package/core/shared/guidance/FLAGS.md +0 -25
  466. package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
  467. package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
  468. package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
  469. package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
  470. package/src/adapters/gemini/hooks.js +0 -251
  471. package/src/adapters/gemini/index.js +0 -429
@@ -0,0 +1,778 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ // Task run records (feature 080, design C4): the run key, who may act on a run, the run record,
6
+ // its totals, the aggregate budget check and the resume epoch.
7
+ //
8
+ // `runKeyFor` is one probe here: it reads the environment and walks up to `.git`, so it runs
9
+ // before the ledger lock. `resumeRun` runs the resume probes (source re-measure, scopeContext, git
10
+ // branch) before its lock. Every other function is pure over the parsed ledger object and is called
11
+ // inside `mutate()`'s `apply`: none reads a file, starts a process or reads the clock (`now` is
12
+ // passed in). A refusal is returned as `{ reason, ...fields }` or `null`; the caller wraps it in
13
+ // its own `{ write: false, result: { disposition: 'refused', ... } }`.
14
+
15
+ const { cwdHash, resolveIdentity, ID_PATTERN } = require('./context/identity.js');
16
+ const { findWorkTreeTop } = require('./source-version.js');
17
+ const crypto = require('node:crypto');
18
+ const { taskAttemptLimit, graphDigest, graphTuples, deriveTasks, isGraphPlan, overlaps } = require('./task-graph.js');
19
+ const { measureSources, compareSources } = require('./task-sources.js');
20
+
21
+
22
+
23
+ /** IC-009: `null` attempts means the limit derived from the graph; `null` tokens means no limit. */
24
+
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+ /** `traceIds` is whether this caller's environment lets a run mint trace ids (088, IC-010): the
33
+ * switch is read where the environment is, because `startRun` is handed only the key. A key built
34
+ * by hand leaves it out and `traceIdsOn` reads the process environment. */
35
+
36
+
37
+ /**
38
+ * IC-011: the caller's run key. `top` is the worktree top above the identity's worktree (or the
39
+ * worktree itself outside git), so every subdirectory of one worktree reaches the same run. No
40
+ * flag sets or overrides it. The raw `identity.worktree` stays on `identity` for `scopeContext`.
41
+ */
42
+ function runKeyFor(env , cwd ) {
43
+ const identity = resolveIdentity(env, { cwd, sessionId: '' });
44
+ const top = findWorkTreeTop(identity.worktree).top ?? identity.worktree;
45
+ return { runKey: `${cwdHash(top)}/${identity.run}/${identity.lineage}`, top, identity, traceIds: traceSwitchOn(env) };
46
+ }
47
+
48
+ /** 088 DD-5: the telemetry switch read through its one reader. `telemetry.ts` is required here, not
49
+ * at load, so a module user that never reaches a run key does not load the trace modules. A fault
50
+ * reading the switch means no ids. */
51
+ function traceSwitchOn(env ) {
52
+ try { return !require('./trace/telemetry.js').telemetryOff(env); } catch { return false; }
53
+ }
54
+
55
+ /** Whether `key` may mint trace ids: its own flag, else the given environment. */
56
+ function traceIdsOn(key , env = process.env) {
57
+ return typeof key.traceIds === 'boolean' ? key.traceIds : traceSwitchOn(env);
58
+ }
59
+
60
+ /** 088 IC-010: a fresh 128-bit id as 32 lowercase hex, or null when telemetry is off or the id
61
+ * source faults. Minting never fails the mutation that calls it. */
62
+ function mintTraceId(on ) {
63
+ if (!on) return null;
64
+ try {
65
+ const id = crypto.randomBytes(16).toString('hex');
66
+ return /^[0-9a-f]{32}$/.test(id) ? id : null;
67
+ } catch { return null; }
68
+ }
69
+
70
+ /** IC-011: a `solo` lineage may not start, pause, cancel, resume or reopen a run, because every
71
+ * solo session would share one. */
72
+ function refuseSolo(identity ) {
73
+ return identity && identity.lineage === 'solo' ? { reason: 'identity-solo' } : null;
74
+ }
75
+
76
+ /** IC-026 / DEC-006: a worker session claims nothing in a graph plan and opens no run; it finishes
77
+ * with the token it was handed. */
78
+ function refuseWorker(identity ) {
79
+ return identity && identity.role === 'worker' ? { reason: 'worker-session' } : null;
80
+ }
81
+
82
+ function normalizeBudget(budget ) {
83
+ const pick = (v ) => (Number.isInteger(v) && v >= 0 ? v : null);
84
+ return { attempts: pick(budget && budget.attempts), tokens: pick(budget && budget.tokens) };
85
+ }
86
+
87
+ function emptyTotals() {
88
+ return { attempts: 0, tokens: { measured: 0, estimated: 0, reserved: 0 }, unreported: 0 };
89
+ }
90
+
91
+ /**
92
+ * IC-010: a new run record for `key` (the `runKeyFor` result), `control: 'running'`, with an empty
93
+ * history. `epoch` is 1 for `start` and for a resume that creates the run (IC-025); a legacy-plan
94
+ * claim passes the epoch it records. The caller refreshes `graphDigest`, `graphTasks` and `totals`
95
+ * and appends history.
96
+ */
97
+ function createRun(key , budget , now , { epoch = 1 } = {}) {
98
+ if (!Number.isInteger(epoch) || epoch < 0) throw new TypeError(`createRun: epoch must be a non-negative integer, got ${epoch}`);
99
+ const { identity } = key;
100
+ return {
101
+ runKey: key.runKey,
102
+ worktreeKey: key.runKey.split('/')[0],
103
+ run: identity.run,
104
+ lineage: identity.lineage,
105
+ worktree: key.top,
106
+ createdAt: now.toISOString(),
107
+ control: 'running',
108
+ epoch,
109
+ budget: normalizeBudget(budget),
110
+ totals: emptyTotals(),
111
+ graphDigest: null ,
112
+ graphTasks: null ,
113
+ history: [] ,
114
+ };
115
+ }
116
+
117
+ function reported(a ) {
118
+ return Boolean(a.usage) && Number.isInteger(a.usage.tokens) && a.usage.tokens >= 0;
119
+ }
120
+
121
+ /**
122
+ * IC-009: totals over every attempt carrying `runKey`, recomputed from the attempt list (never
123
+ * from the stored copy). Reported usage adds to its basis, including a reported 0; an active
124
+ * attempt's reservation is `reserved`; a cancelled attempt cannot report, so its reservation is
125
+ * `estimated`; a finished, released or superseded attempt with no usage is `unreported`
126
+ * (DEC-017 superseded-unreported: a reopen does not launder an unreported attempt out of DEC-011).
127
+ */
128
+ function computeTotals(store , runKey ) {
129
+ const totals = emptyTotals();
130
+ const tasks = store && store.tasks && typeof store.tasks === 'object' ? store.tasks : {};
131
+ for (const entry of Object.values (tasks)) {
132
+ const attempts = entry && Array.isArray(entry.attempts) ? entry.attempts : [];
133
+ for (const a of attempts) {
134
+ if (a.runKey !== runKey) continue;
135
+ totals.attempts += 1;
136
+ const reserve = a.reserve && Number.isInteger(a.reserve.tokens) ? a.reserve.tokens : 0;
137
+ if (reported(a)) {
138
+ if (a.usage.basis === 'measured') totals.tokens.measured += a.usage.tokens;
139
+ else totals.tokens.estimated += a.usage.tokens;
140
+ } else if (a.state === 'active') totals.tokens.reserved += reserve;
141
+ else if (a.state === 'cancelled') totals.tokens.estimated += reserve;
142
+ else if (a.state === 'finished' || a.state === 'released' || a.state === 'superseded') totals.unreported += 1;
143
+ }
144
+ }
145
+ return totals;
146
+ }
147
+
148
+ /** IC-009: the run's attempt limit (its own, else the sum of the graph's task limits) and token
149
+ * limit (its own, else none). `run` may be absent: a legacy-plan claim before its record exists. */
150
+ function effectiveLimits(run , graph ) {
151
+ const budget = normalizeBudget(run && run.budget);
152
+ const attempts = budget.attempts ?? (graph && Array.isArray(graph.tasks)
153
+ ? graph.tasks.reduce((sum , t ) => sum + taskAttemptLimit(t).limit, 0)
154
+ : 0);
155
+ return { attempts, tokens: budget.tokens };
156
+ }
157
+
158
+ /**
159
+ * IC-006 step 12, the aggregate half, in IC-009's order. With a token limit: no reservation,
160
+ * then unreported usage (DEC-011), then the token limit. With or without one: the attempt limit.
161
+ */
162
+ function aggregateRefusal(run , totals , graph , reserveTokens ) {
163
+ const limits = effectiveLimits(run, graph);
164
+ const { measured, estimated, reserved } = totals.tokens;
165
+ if (limits.tokens !== null) {
166
+ const reserve = Number.isInteger(reserveTokens) ? (reserveTokens ) : 0;
167
+ if (reserve <= 0) return { reason: 'reservation-required', limit: limits.tokens };
168
+ if (totals.unreported > 0) return { reason: 'aggregate-unaccounted', unreported: totals.unreported };
169
+ if (measured + estimated + reserved + reserve > limits.tokens) {
170
+ return {
171
+ reason: 'aggregate-exhausted', scope: 'run', measure: 'tokens', limit: limits.tokens,
172
+ used: measured + estimated, reserved, basis: { measured, estimated },
173
+ };
174
+ }
175
+ }
176
+ if (totals.attempts + 1 > limits.attempts) {
177
+ return {
178
+ reason: 'aggregate-exhausted', scope: 'run', measure: 'attempts', limit: limits.attempts,
179
+ used: totals.attempts, reserved: 0, basis: null,
180
+ };
181
+ }
182
+ return null;
183
+ }
184
+
185
+ /** IC-006 step 7: a paused or cancelled run refuses claims. An absent run is running. */
186
+ function controlRefusal(run , runKey ) {
187
+ if (run && run.control === 'paused') return { reason: 'run-paused', runKey };
188
+ if (run && run.control === 'cancelled') return { reason: 'run-cancelled', runKey };
189
+ return null;
190
+ }
191
+
192
+ /** IC-025 / DEC-007, IC-006 step 8 (graph plans only): the run must exist and `epoch` must equal
193
+ * its epoch. Absent, old or future epochs are all `resume-required`, naming the current one.
194
+ * DEC-019 epoch-zero-never-matches: an epoch-0 run (created by a legacy claim or a control) has
195
+ * never been started or resumed, so it matches no epoch. */
196
+ function epochRefusal(run , runKey , epoch ) {
197
+ if (run && Number.isInteger(run.epoch) && run.epoch >= 1 && Number.isInteger(epoch) && epoch === run.epoch) return null;
198
+ return { reason: 'resume-required', runKey, epoch: run ? run.epoch : null };
199
+ }
200
+
201
+ /** IC-025: the epoch an acknowledged resume issues: `epoch + 1`, or 1 when it creates the run. */
202
+ function nextEpoch(run ) {
203
+ return run && Number.isInteger(run.epoch) ? run.epoch + 1 : 1;
204
+ }
205
+
206
+ // ── run controls (IC-012, IC-026; DEC-005) ────────────────────────────────────────────────────
207
+ //
208
+ // Each is an `apply` for `mutate()`: it takes the parsed store and the inputs resolved before the
209
+ // lock, and returns `{ write, result }`. A refusal returns `write: false`, so it changes nothing.
210
+ // None kills a process: ending a worker is 081's.
211
+
212
+ /** IC-004: above this many tasks a run stores `graphTasks: null` and `graphTruncated: true`. */
213
+ const GRAPH_TASKS_LIMIT = 500;
214
+
215
+ /** IC-004 and IC-009: every run write stores the recorded graph and totals recomputed from the
216
+ * attempt list. */
217
+ function refreshRun(store , run , graph ) {
218
+ run.graphDigest = graphDigest(graph);
219
+ if (graph.tasks.length > GRAPH_TASKS_LIMIT) {
220
+ run.graphTasks = null;
221
+ run.graphTruncated = true;
222
+ } else {
223
+ run.graphTasks = graphTuples(graph);
224
+ delete run.graphTruncated;
225
+ }
226
+ run.totals = computeTotals(store, run.runKey);
227
+ }
228
+
229
+ function ownRun(store , runKey ) {
230
+ return store.runs && Object.prototype.hasOwnProperty.call(store.runs, runKey) ? store.runs[runKey] : null;
231
+ }
232
+
233
+ /** The caller's run, created when absent. Only start and an acknowledged resume issue an epoch
234
+ * (DEC-007), so a run another control creates has epoch 0, like a legacy claim's (C.2). */
235
+ function ensureRun(store , key , now ) {
236
+ if (!store.runs) store.runs = {};
237
+ let run = ownRun(store, key.runKey);
238
+ if (!run) {
239
+ run = createRun(key, null, now, { epoch: 0 });
240
+ store.runs[key.runKey] = run;
241
+ }
242
+ return run;
243
+ }
244
+
245
+ /** IC-011, IC-026: a solo lineage or a worker session may not act on a run. */
246
+ function identityRefusal(identity ) {
247
+ return refuseSolo(identity) || refuseWorker(identity);
248
+ }
249
+
250
+ function refusedControl(reason , runKey , extra = {}) {
251
+ return { write: false, result: { disposition: 'refused', reason, runKey, ...extra } };
252
+ }
253
+
254
+ /** IC-012 `start`: creates the caller's run with `epoch: 1` and history `start`. `budget` is
255
+ * `{ attempts, tokens }`, each a whole number or null. */
256
+ function startRun(store , { key, graph, by, budget, now } ) {
257
+ const who = identityRefusal(key.identity);
258
+ if (who) return refusedControl(who.reason, key.runKey);
259
+ if (ownRun(store, key.runKey)) return refusedControl('run-exists', key.runKey);
260
+ if (!store.runs) store.runs = {};
261
+ const run = createRun(key, budget, now, { epoch: 1 });
262
+ store.runs[key.runKey] = run;
263
+ const entry = { at: now.toISOString(), action: 'start', by, reason: null, epoch: 1 };
264
+ const traceId = mintTraceId(traceIdsOn(key));
265
+ if (traceId) { run.traceId = traceId; entry.traceId = traceId; }
266
+ run.history.push(entry);
267
+ refreshRun(store, run, graph);
268
+ return { write: true, result: { disposition: 'started', runKey: key.runKey, epoch: 1, budget: run.budget, revision: store.revision } };
269
+ }
270
+
271
+ /** IC-012 `pause`: the run, created if absent, becomes `paused`; active attempts are untouched and
272
+ * may still finish (IC-007). */
273
+ function pauseRun(store , { key, graph, by, reason, now } ) {
274
+ const who = identityRefusal(key.identity);
275
+ if (who) return refusedControl(who.reason, key.runKey);
276
+ const existing = ownRun(store, key.runKey);
277
+ if (existing && existing.control === 'paused') return refusedControl('control-unchanged', key.runKey, { control: 'paused' });
278
+ if (existing && existing.control === 'cancelled') return refusedControl('run-cancelled', key.runKey);
279
+ const run = ensureRun(store, key, now);
280
+ run.control = 'paused';
281
+ run.history.push({ at: now.toISOString(), action: 'pause', by, reason });
282
+ refreshRun(store, run, graph);
283
+ return { write: true, result: { disposition: 'paused', runKey: key.runKey, revision: store.revision } };
284
+ }
285
+
286
+ function activeAttempts(store ) {
287
+ const out = [];
288
+ for (const [taskId, entry] of Object.entries (store.tasks || {})) {
289
+ for (const attempt of entry.attempts) if (attempt.state === 'active') out.push({ taskId, attempt });
290
+ }
291
+ return out;
292
+ }
293
+
294
+ /**
295
+ * IC-012 run `cancel`: every active attempt carrying this run key becomes `cancelled`. With
296
+ * `adoptUnowned`, an active attempt with no run key is first given this one and cancelled with
297
+ * `adopted: true` (critic A m7). Active attempts of other runs, and runKey-less ones without
298
+ * `adoptUnowned`, are listed in `notCancelled` and left alone. `cancelled` names every attempt
299
+ * this call cancelled, adopted ones included; `adopted` is that subset.
300
+ */
301
+ function cancelRun(store , { key, graph, by, reason, adoptUnowned = false, now } ) {
302
+ const who = identityRefusal(key.identity);
303
+ if (who) return refusedControl(who.reason, key.runKey);
304
+ const existing = ownRun(store, key.runKey);
305
+ if (existing && existing.control === 'cancelled') return refusedControl('control-unchanged', key.runKey, { control: 'cancelled' });
306
+ const run = ensureRun(store, key, now);
307
+ const at = now.toISOString();
308
+ const cancelled = [];
309
+ const adopted = [];
310
+ const notCancelled = [];
311
+ for (const { taskId, attempt } of activeAttempts(store)) {
312
+ const owned = attempt.runKey === key.runKey;
313
+ const adopt = !attempt.runKey && adoptUnowned;
314
+ if (!owned && !adopt) {
315
+ notCancelled.push({ taskId, token: attempt.token, runKey: attempt.runKey || null });
316
+ continue;
317
+ }
318
+ if (adopt) {
319
+ attempt.runKey = key.runKey;
320
+ adopted.push({ taskId, token: attempt.token });
321
+ }
322
+ attempt.state = 'cancelled';
323
+ attempt.cancel = adopt ? { at, by, reason, adopted: true } : { at, by, reason };
324
+ cancelled.push({ taskId, token: attempt.token });
325
+ }
326
+ run.control = 'cancelled';
327
+ run.history.push({ at, action: 'cancel', by, reason, cancelled });
328
+ refreshRun(store, run, graph);
329
+ return {
330
+ write: true,
331
+ result: { disposition: 'cancelled', runKey: key.runKey, cancelled, adopted, notCancelled, revision: store.revision },
332
+ };
333
+ }
334
+
335
+ /** IC-012 attempt `cancel`: the task's active attempt holding `token`, which must carry this run
336
+ * key, becomes `cancelled`; the run's control is unchanged. */
337
+ function cancelAttempt(store , { key, graph, task, token, by, reason, now } ) {
338
+ const who = identityRefusal(key.identity);
339
+ if (who) return refusedControl(who.reason, key.runKey, { taskId: task, token });
340
+ const entry = store.tasks && store.tasks[task];
341
+ const active = entry ? entry.attempts.find((a ) => a.state === 'active') || null : null;
342
+ if (!active) return refusedControl('no-active-attempt', key.runKey, { taskId: task, token });
343
+ if (active.token !== token) return refusedControl('token-mismatch', key.runKey, { taskId: task, token, activeToken: active.token });
344
+ if (active.runKey !== key.runKey) {
345
+ return { write: false, result: { disposition: 'refused', reason: 'foreign-run', taskId: task, token, runKey: active.runKey || null } };
346
+ }
347
+ const run = ensureRun(store, key, now);
348
+ const at = now.toISOString();
349
+ active.state = 'cancelled';
350
+ active.cancel = { at, by, reason };
351
+ const cancelled = [{ taskId: task, token }];
352
+ run.history.push({ at, action: 'cancel', by, reason, cancelled });
353
+ refreshRun(store, run, graph);
354
+ return {
355
+ write: true,
356
+ result: { disposition: 'cancelled', runKey: key.runKey, taskId: task, token, control: run.control, cancelled, revision: store.revision },
357
+ };
358
+ }
359
+
360
+ // ── resume (IC-015, IC-025; DEC-004, DEC-007, DEC-010) ────────────────────────────────────────
361
+ //
362
+ // The probes (IC-013 re-measure, IC-014 scopeContext and the git branch) run before any lock in
363
+ // `resumeRun`; everything else here is pure over the parsed store, so the locked `apply` can
364
+ // rebuild the ledger-derived half of the report and recompute the digest against what is on disk.
365
+
366
+ /**
367
+ * IC-005 row 4: the cancelled attempts a resume has cleared. A cancel is cleared when the run its
368
+ * attempt's `runKey` names has a `resume` history entry that lists it in `cleared`. A `resume`
369
+ * entry that carries a `cleared` list is authoritative: its stamp is never compared, since a stamp
370
+ * from another clock can sort either side of a resume. Only a run none of whose resume entries
371
+ * carries a `cleared` list (a record written before the list existed) clears by a resume being
372
+ * later than `cancel.at`. Pure; an
373
+ * empty list for a store with no cancelled attempt (every `8ce5ad7` store). Each entry also
374
+ * carries the cleared `attempt` object itself, so a caller can
375
+ * pair by identity when tokens are duplicated or missing (a malformed ledger).
376
+ */
377
+ function clearedCancels(store ) {
378
+ const out = [];
379
+ const runs = store && store.runs && typeof store.runs === 'object' ? store.runs : null;
380
+ if (!runs) return out;
381
+ for (const [taskId, entry] of Object.entries (store.tasks || {})) {
382
+ const attempts = entry && Array.isArray(entry.attempts) ? entry.attempts : [];
383
+ for (const a of attempts) {
384
+ if (!a || a.state !== 'cancelled' || !a.runKey) continue;
385
+ const run = ownRun(store, a.runKey);
386
+ const history = run && Array.isArray(run.history) ? run.history : [];
387
+ const at = a.cancel && typeof a.cancel.at === 'string' ? a.cancel.at : null;
388
+ const resumes = history.filter((h ) => h && h.action === 'resume');
389
+ // Legacy fallback: only a run none of whose resumes carries a `cleared` list clears by stamp.
390
+ const listed = resumes.some((h ) => Array.isArray(h.cleared));
391
+ const cleared = resumes.some((h ) => Array.isArray(h.cleared)
392
+ ? h.cleared.some((c ) => c && c.taskId === taskId && c.token === a.token)
393
+ : (!listed && at !== null && typeof h.at === 'string' && h.at > at));
394
+ if (cleared) out.push({ taskId, token: a.token, attempt: a });
395
+ }
396
+ }
397
+ return out;
398
+ }
399
+
400
+ /** Ids in plan order, then any ledger-only ids sorted, so a report's order never depends on the
401
+ * order keys were written. */
402
+ function taskOrder(store , graph ) {
403
+ const planned = graph.tasks.map((t ) => t.id);
404
+ const extra = Object.keys(store.tasks || {}).filter((id) => !planned.includes(id)).sort();
405
+ return [...planned, ...extra];
406
+ }
407
+
408
+ /**
409
+ * IC-013 at resume: the attempt each task's source comparison reads. A SUCCEEDED task compares
410
+ * its done attempt (listed under `doneChanged`); any other task its highest-token attempt. A task
411
+ * with no attempt, or whose attempt recorded no sources and whose plan line declares no files,
412
+ * has nothing to compare. `measure` is false when the comparison is unverifiable without a disk
413
+ * access (no `sources`, or `sourcesTruncated`).
414
+ */
415
+ function resumeTargets(store , graph , derived ) {
416
+ const targets = [];
417
+ for (const record of derived) {
418
+ const entry = store.tasks && store.tasks[record.id];
419
+ const attempts = entry && Array.isArray(entry.attempts) ? entry.attempts : [];
420
+ if (!attempts.length) continue;
421
+ const done = record.state === 'SUCCEEDED';
422
+ const attempt = done
423
+ ? attempts.find((a ) => a.token === record.doneToken)
424
+ : attempts.reduce((a , b ) => (b.token > a.token ? b : a));
425
+ if (!attempt) continue;
426
+ const task = graph.tasks.find((t ) => t.id === record.id);
427
+ const declared = task && Array.isArray(task.files) ? task.files.length : 0;
428
+ const recorded = Array.isArray(attempt.sources);
429
+ if (!recorded && declared === 0) continue;
430
+ targets.push({ taskId: record.id, token: attempt.token, done, attempt, measure: recorded && !attempt.sourcesTruncated, declared });
431
+ }
432
+ return targets;
433
+ }
434
+
435
+ const targetKey = (taskId , token ) => `${taskId}#${token}`;
436
+
437
+ /** The resume probes' source half (IC-013): one re-measure per target that can be compared, of
438
+ * the entries its attempt recorded, under its own `execWorktree` and then the plan roots. Reads
439
+ * the filesystem; runs before any lock. */
440
+ function probeSources(targets , roots ) {
441
+ const probes = new Map ();
442
+ for (const t of targets) {
443
+ if (!t.measure) continue;
444
+ const files = t.attempt.sources.map((s ) => s.file);
445
+ probes.set(targetKey(t.taskId, t.token), measureSources({ files }, t.attempt.execWorktree ?? null, roots));
446
+ }
447
+ return probes;
448
+ }
449
+
450
+ /** `{ taskId, token, count }` for an unverifiable task: `count` is the entries that could not be
451
+ * compared, every recorded (or declared) entry when the cause is the whole task. */
452
+ function unverifiableCount(target , after ) {
453
+ const recorded = Array.isArray(target.attempt.sources) ? target.attempt.sources : [];
454
+ const whole = recorded.length || target.declared;
455
+ if (!target.measure || !after || after.execWorktreeMissing || after.sourcesTruncated) return whole;
456
+ const now = new Map (after.sources.map((s ) => [s.file, s]));
457
+ const opaque = (s ) => !s || s.kind === 'unhashable' || s.kind === 'unverifiable';
458
+ const missing = recorded.some((s ) => !now.has(s.file));
459
+ if (missing) return whole;
460
+ return recorded.filter((s ) => opaque(s) || opaque(now.get(s.file))).length;
461
+ }
462
+
463
+ /** IC-004 at resume: the recorded digest against the current one and, when the run stored its
464
+ * tuple list, the ids added, removed and changed. */
465
+ function graphComparison(run , graph ) {
466
+ const current = graphDigest(graph);
467
+ const recorded = run && typeof run.graphDigest === 'string' ? run.graphDigest : null;
468
+ const truncated = Boolean(run && run.graphTruncated);
469
+ const out = { recorded, current, changed: recorded !== null && recorded !== current, added: [] , removed: [] , changedIds: [] , truncated };
470
+ if (!run || !Array.isArray(run.graphTasks)) return out;
471
+ const before = new Map (run.graphTasks.map((tuple ) => [String(tuple[0]), JSON.stringify(tuple)]));
472
+ const after = new Map (graphTuples(graph).map((tuple ) => [String(tuple[0]), JSON.stringify(tuple)]));
473
+ for (const [id, tuple] of after) {
474
+ if (!before.has(id)) out.added.push(id);
475
+ else if (before.get(id) !== tuple) out.changedIds.push(id);
476
+ }
477
+ for (const id of before.keys()) if (!after.has(id)) out.removed.push(id);
478
+ return out;
479
+ }
480
+
481
+ /**
482
+ * IC-015: the resume report from the store (the ledger-derived half) and the probe results
483
+ * (`probes` from `probeSources`, `staleContext` from `compareScopeContext`). Pure: called once
484
+ * outside the lock for the report, and again inside it to recompute the digest. A target with no
485
+ * probe (an attempt claimed after the probes ran) is unverifiable, so the digest moves.
486
+ */
487
+ function buildResumeReport(store , { key, graph, hashes = null, probes, staleContext, now } ) {
488
+ const run = ownRun(store, key.runKey);
489
+ const derived = deriveTasks(graph, store, { hashes, clearedCancels: clearedCancels(store) });
490
+ const activeAttempts = [];
491
+ for (const taskId of taskOrder(store, graph)) {
492
+ const entry = store.tasks[taskId];
493
+ const attempts = entry && Array.isArray(entry.attempts) ? entry.attempts : [];
494
+ for (const a of attempts.filter((x ) => x.state === 'active').sort((x , y ) => x.token - y.token)) {
495
+ const started = Date.parse(a.startedAt);
496
+ activeAttempts.push({
497
+ taskId, token: a.token, owner: a.owner, mode: a.mode, startedAt: a.startedAt,
498
+ ageSeconds: Number.isFinite(started) ? Math.max(0, Math.floor((now.getTime() - started) / 1000)) : 0,
499
+ runKey: a.runKey || null, execWorktree: a.execWorktree || null,
500
+ });
501
+ }
502
+ }
503
+ const changedSources = [];
504
+ const doneChanged = [];
505
+ const unverifiable = [];
506
+ for (const target of resumeTargets(store, graph, derived)) {
507
+ const after = probes.get(targetKey(target.taskId, target.token)) || null;
508
+ const compared = target.measure && after ? compareSources(target.attempt, after) : { unverifiable: true, files: [] };
509
+ if (compared.unverifiable) {
510
+ unverifiable.push({ taskId: target.taskId, token: target.token, count: unverifiableCount(target, after) });
511
+ continue;
512
+ }
513
+ const into = target.done ? doneChanged : changedSources;
514
+ for (const f of compared.files) {
515
+ if (f.change !== 'unchanged') into.push({ taskId: target.taskId, token: target.token, file: f.file, change: f.change });
516
+ }
517
+ }
518
+ return {
519
+ runKey: key.runKey,
520
+ control: run ? run.control : 'none',
521
+ epoch: run ? run.epoch : null,
522
+ activeAttempts,
523
+ changedSources,
524
+ doneChanged,
525
+ unverifiable,
526
+ staleContext: staleContext.map((s ) => ({ kind: s.kind, path: s.path, reasons: [...s.reasons] })),
527
+ graph: graphComparison(run, graph),
528
+ };
529
+ }
530
+
531
+ /** JSON with object keys sorted at every level. */
532
+ function canonicalJson(value ) {
533
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
534
+ if (value && typeof value === 'object') {
535
+ return `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${canonicalJson(value[k])}`).join(',')}}`;
536
+ }
537
+ return JSON.stringify(value === undefined ? null : value);
538
+ }
539
+
540
+ /** IC-015: `sha256:` over the report's canonical JSON with every `ageSeconds` removed, so an
541
+ * acknowledgement a few seconds later still matches. */
542
+ function resumeDigest(report ) {
543
+ const stable = { ...report, activeAttempts: report.activeAttempts.map(({ ageSeconds, ...rest } ) => rest) };
544
+ return `sha256:${crypto.createHash('sha256').update(canonicalJson(stable)).digest('hex')}`;
545
+ }
546
+
547
+ /** The current branch of `worktree` (`git symbolic-ref --short -q HEAD`), and, when HEAD is
548
+ * detached or unreadable, its commit (`git rev-parse HEAD`). 2 s per call; a failure is null. */
549
+ function probeGit(worktree , gitRunner ) {
550
+ const run = gitRunner || ((args , cwd ) => {
551
+ const { execFileSync } = require('node:child_process');
552
+ try {
553
+ return execFileSync('git', args, { cwd, encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'] });
554
+ } catch {
555
+ return null;
556
+ }
557
+ });
558
+ const clean = (out ) => (typeof out === 'string' && out.trim() ? out.trim() : null);
559
+ const branch = clean(run(['symbolic-ref', '--short', '-q', 'HEAD'], worktree));
560
+ const commit = branch ? null : clean(run(['rev-parse', 'HEAD'], worktree));
561
+ return { branch, commit };
562
+ }
563
+
564
+ /**
565
+ * IC-015 / IC-025: the resume. `io.read()` returns the store without a lock (`readTaskState`);
566
+ * `io.mutate(apply)` runs `apply(store)` inside the ledger lock and returns its result. The
567
+ * caller has loaded the plan (`graph`, `hashes`) and resolved `key` (`runKeyFor`) and checked the
568
+ * flags.
569
+ *
570
+ * Order: identity refusals (`identity-solo`; `worker-session` in a graph plan); then every probe,
571
+ * before any lock: the store is read, each target's sources re-measured, `scopeContext` called
572
+ * with the raw identity worktree and compared with the branch and commit probed in the run's
573
+ * worktree. Without `ack`: `ack-required` with the report and digest, nothing written. With
574
+ * `ack`: one `mutate()` that rebuilds the ledger-derived half against the disk, recomputes the
575
+ * digest and either refuses `report-changed` (nothing written) or sets `control: 'running'`,
576
+ * issues `nextEpoch`, lists the run's cancelled attempts in the history entry's `cleared` and
577
+ * appends history `resume`. `summary-store.ts` and the git process are loaded here only (IC-022).
578
+ */
579
+ function resumeRun(
580
+ io ,
581
+ { key, graph, hashes = null, roots = [], ack = null, by = null, reason = null, now, env = process.env, gitRunner = null }
582
+ ,
583
+ ) {
584
+ const refusal = refuseSolo(key.identity) || (isGraphPlan(graph) ? refuseWorker(key.identity) : null);
585
+ if (refusal) return { disposition: 'refused', reason: refusal.reason, runKey: key.runKey };
586
+
587
+ // Probes, before any lock.
588
+ const seen = io.read();
589
+ const targets = resumeTargets(seen, graph, deriveTasks(graph, seen, { hashes, clearedCancels: clearedCancels(seen) }));
590
+ const probes = probeSources(targets, roots);
591
+ const { scopeContext, compareScopeContext } = require('./context/summary-store.js');
592
+ const context = scopeContext(key.identity.worktree, key.identity.run, key.identity.lineage, env);
593
+ const seenRun = ownRun(seen, key.runKey);
594
+ const { branch, commit } = probeGit(seenRun && seenRun.worktree ? seenRun.worktree : key.top, gitRunner);
595
+ const staleContext = compareScopeContext(context, { worktree: key.identity.worktree, branch, commit });
596
+ const inputs = { key, graph, hashes, probes, staleContext, now };
597
+
598
+ if (!ack) {
599
+ const report = buildResumeReport(seen, inputs);
600
+ return { disposition: 'ack-required', report, digest: resumeDigest(report) };
601
+ }
602
+
603
+ return io.mutate((store ) => {
604
+ const report = buildResumeReport(store, inputs);
605
+ const digest = resumeDigest(report);
606
+ if (digest !== ack) return { write: false, result: { disposition: 'refused', reason: 'report-changed', report, digest } };
607
+ if (!store.runs) store.runs = {};
608
+ let run = ownRun(store, key.runKey);
609
+ const epoch = nextEpoch(run);
610
+ if (!run) {
611
+ run = createRun(key, null, now, { epoch });
612
+ store.runs[key.runKey] = run;
613
+ }
614
+ run.control = 'running';
615
+ run.epoch = epoch;
616
+ const cleared = [];
617
+ for (const [taskId, entry] of Object.entries (store.tasks || {})) {
618
+ for (const a of entry.attempts) if (a.state === 'cancelled' && a.runKey === key.runKey) cleared.push({ taskId, token: a.token });
619
+ }
620
+ const entry = { at: now.toISOString(), action: 'resume', by, reason: reason ?? null, digest, epoch, cleared };
621
+ const traceId = mintTraceId(traceIdsOn(key, env));
622
+ if (traceId) {
623
+ entry.traceId = traceId;
624
+ entry.traceLink = typeof run.traceId === 'string' ? run.traceId : null;
625
+ run.traceId = traceId;
626
+ }
627
+ run.history.push(entry);
628
+ refreshRun(store, run, graph);
629
+ return { write: true, result: { disposition: 'resumed', report, digest, epoch, revision: store.revision } };
630
+ });
631
+ }
632
+
633
+ // ── inspect (IC-016, IC-009 `totalsMismatch`; critic B m2, m4, m6) ────────────────────────────
634
+ //
635
+ // Pure over a store the caller read with `readTaskState` (no lock, no write, no git process).
636
+
637
+ /** IC-009: the stored totals equal the recomputed ones. Key order is not compared. */
638
+ function sameTotals(stored , computed ) {
639
+ return canonicalJson(stored === undefined ? null : stored) === canonicalJson(computed);
640
+ }
641
+
642
+ /**
643
+ * Critic B m6: tasks per phase and `[P]` fan-out (the `[P]` tasks of a phase, which may run
644
+ * together). Advisory only: no threshold, never a refusal.
645
+ */
646
+ function decompositionAdvisory(graph ) {
647
+ const phases = graph.phases.map((phase ) => {
648
+ const tasks = graph.tasks.filter((t ) => t.phase === phase);
649
+ return { phase, tasks: tasks.length, parallel: tasks.filter((t ) => t.parallel).length };
650
+ });
651
+ return {
652
+ advisoryOnly: true ,
653
+ phases,
654
+ maxTasksPerPhase: phases.reduce((m , p ) => Math.max(m, p.tasks), 0),
655
+ maxParallelFanOut: phases.reduce((m , p ) => Math.max(m, p.parallel), 0),
656
+ };
657
+ }
658
+
659
+ /** 088 OI-1: a history entry as it was before trace ids, so `inspect` output does not depend on the
660
+ * telemetry switch (FR-002). */
661
+ function withoutTrace(entry ) {
662
+ if (!entry || typeof entry !== 'object' || (!('traceId' in entry) && !('traceLink' in entry))) return entry;
663
+ const { traceId, traceLink, ...rest } = entry;
664
+ return rest;
665
+ }
666
+
667
+ /** 093 IC-018 c: `worktreeKey`, `run` and `lineage` split from a stored `runKey`
668
+ * (`<cwdHash>/<run>/<lineage>`, `runKeyFor`); all three null when the record has none or another shape. */
669
+ function runKeyParts(runKey ) {
670
+ const parts = typeof runKey === 'string' ? runKey.split('/') : [];
671
+ if (parts.length === 3 && /^[0-9a-f]{16}$/.test(parts[0]) && ID_PATTERN.test(parts[1]) && ID_PATTERN.test(parts[2])) {
672
+ return { worktreeKey: parts[0], run: parts[1], lineage: parts[2] };
673
+ }
674
+ return { worktreeKey: null, run: null, lineage: null };
675
+ }
676
+
677
+ /** 093 IC-013: a task's attempts in ledger order, metadata only (no reason text, evidence id,
678
+ * worktree, usage or trace). `endedAt` is `finishedAt` (finished; superseded, which is always a
679
+ * finished attempt), else `endedAt` (released), else `cancel.at` (cancelled); `result` is kept
680
+ * only when it is one of RESULTS. IC-018 c appends `worktreeKey`, `run` and `lineage` (`runKeyParts`). */
681
+ function attemptsView(store , taskId ) {
682
+ // Required here, not at the top: task-attempts.ts requires this module while it loads.
683
+ const { RESULTS } = require('./task-attempts.js');
684
+ const entry = store && store.tasks ? store.tasks[taskId] : null;
685
+ const attempts = entry && Array.isArray(entry.attempts) ? entry.attempts : [];
686
+ return attempts.map((a ) => ({
687
+ token: a.token, owner: a.owner ?? null, mode: a.mode ?? null, state: a.state,
688
+ startedAt: a.startedAt ?? null, endedAt: a.finishedAt ?? a.endedAt ?? (a.cancel && a.cancel.at) ?? null,
689
+ result: RESULTS.includes(a.result) ? a.result : null,
690
+ ...runKeyParts(a.runKey),
691
+ }));
692
+ }
693
+
694
+ /**
695
+ * IC-016: the inspect view of the caller's run. `store` is a plain read (`readTaskState`);
696
+ * `graph`, `hashes` and `warnings` come from `loadPlan`; `key` from `runKeyFor`. Pure: nothing is
697
+ * written, no lock is taken and no process runs. `totals` is recomputed from the attempt list and
698
+ * `totalsMismatch` says whether the stored copy differs (IC-009); the stored copy is returned as
699
+ * `storedTotals` only then. `defaults` lists every budget limit that is a default rather than
700
+ * declared (critic B m2), so a caller sees nominal limits without reading each row.
701
+ */
702
+ function inspectRun(store , { slug, key, graph, hashes = null, warnings = [] } ) {
703
+ const run = ownRun(store, key.runKey);
704
+ const tasks = deriveTasks(graph, store, { hashes, clearedCancels: clearedCancels(store) });
705
+ const limits = effectiveLimits(run, graph);
706
+ const totals = computeTotals(store, key.runKey);
707
+ const totalsMismatch = Boolean(run) && !sameTotals(run.totals, totals);
708
+ const runView = {
709
+ runKey: key.runKey,
710
+ exists: Boolean(run),
711
+ control: run ? run.control : 'none',
712
+ epoch: run ? run.epoch : null,
713
+ budget: { attempts: limits.attempts, tokens: limits.tokens, source: normalizeBudget(run && run.budget).attempts === null ? 'default' : 'run' },
714
+ totals,
715
+ totalsMismatch,
716
+ history: run && Array.isArray(run.history) ? run.history.map(withoutTrace) : [],
717
+ };
718
+ if (totalsMismatch) runView.storedTotals = run.totals === undefined ? null : run.totals;
719
+ return {
720
+ slug,
721
+ revision: Number.isInteger(store.revision) ? store.revision : 0,
722
+ graph: {
723
+ digest: graphDigest(graph),
724
+ graphPlan: isGraphPlan(graph),
725
+ warnings,
726
+ tasks: graph.tasks.map((t ) => ({
727
+ id: t.id, phase: t.phase, owner: t.owner ?? null, taskDeps: t.taskDeps, parallel: t.parallel,
728
+ acceptance: t.acceptance, files: t.files || [],
729
+ })),
730
+ overlaps: overlaps(graph),
731
+ },
732
+ tasks: tasks.map((r ) => ({ ...r, attempts: attemptsView(store, r.id) })),
733
+ run: runView,
734
+ defaults: {
735
+ runAttempts: runView.budget.source === 'default',
736
+ tasks: tasks.filter((r ) => r.budget.source === 'default').map((r ) => r.id),
737
+ },
738
+ advisory: { decomposition: decompositionAdvisory(graph) },
739
+ };
740
+ }
741
+
742
+ /**
743
+ * IC-016 text: one line per task (`id state reason budget used/limit`, `(default)` on a default
744
+ * limit), then control, epoch, limits with their source and totals with each token figure
745
+ * labelled by basis. Measured and estimated are never summed.
746
+ */
747
+ function formatInspect(view ) {
748
+ const lines = [];
749
+ const r = view.run;
750
+ if (view.defaults.runAttempts || view.defaults.tasks.length) {
751
+ const what = [
752
+ ...(view.defaults.runAttempts ? ['run attempt limit'] : []),
753
+ ...(view.defaults.tasks.length ? [`task limits of ${view.defaults.tasks.join(', ')}`] : []),
754
+ ];
755
+ lines.push(`DEFAULT BUDGETS: ${what.join('; ')} are defaults, not declared limits`);
756
+ }
757
+ lines.push(`Task attempts for ${view.slug} (revision ${view.revision}, ${view.graph.graphPlan ? 'graph plan' : 'legacy plan'})`);
758
+ for (const t of view.tasks) {
759
+ lines.push(` ${t.id} ${t.state} ${t.reason} budget ${t.budget.used}/${t.budget.limit}${t.budget.source === 'default' ? ' (default)' : ''}`);
760
+ }
761
+ lines.push(`Run ${r.runKey}: control ${r.control}, epoch ${r.epoch === null ? 'none' : r.epoch}`);
762
+ lines.push(` limits: attempts ${r.budget.attempts} (${r.budget.source}), tokens ${r.budget.tokens === null ? 'none' : `${r.budget.tokens} (run)`}`);
763
+ const tk = r.totals.tokens;
764
+ lines.push(` totals: attempts ${r.totals.attempts}; tokens measured ${tk.measured}, estimated ${tk.estimated}, reserved ${tk.reserved}; unreported ${r.totals.unreported}`);
765
+ if (r.totalsMismatch) lines.push(' totals mismatch: the stored totals differ from the attempt list; the figures above are recomputed');
766
+ for (const w of view.graph.warnings) lines.push(`Warning: ${w.kind} ${w.ids.join(' ')}${w.value !== undefined ? ` '${w.value}'` : ''}`);
767
+ for (const o of view.graph.overlaps) lines.push(`Overlap: ${o.a} and ${o.b} both declare ${o.files.join(', ')}`);
768
+ const d = view.advisory.decomposition;
769
+ lines.push(`Advisory (decomposition): ${d.phases.map((p ) => `${p.phase} ${p.tasks} task${p.tasks === 1 ? '' : 's'}, ${p.parallel} [P]`).join('; ')}`);
770
+ return lines;
771
+ }
772
+
773
+ module.exports = {
774
+ runKeyFor, refuseSolo, refuseWorker, createRun, computeTotals, effectiveLimits,
775
+ aggregateRefusal, controlRefusal, epochRefusal, nextEpoch,
776
+ GRAPH_TASKS_LIMIT, refreshRun, ensureRun, identityRefusal, startRun, pauseRun, cancelRun, cancelAttempt,
777
+ clearedCancels, buildResumeReport, resumeDigest, resumeRun, inspectRun, formatInspect,
778
+ };