@khoavu882/doflow 2.0.0-beta.5 → 2.0.1

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 (494) hide show
  1. package/README.md +16 -16
  2. package/bin/doflow.js +22 -12
  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/mcp-tool-guard.sh +4 -1
  9. package/core/harnesses/claude/hooks/permission-audit.sh +3 -0
  10. package/core/harnesses/claude/hooks/post-compact.sh +35 -32
  11. package/core/harnesses/claude/hooks/post-edit-lint.sh +2 -0
  12. package/core/harnesses/claude/hooks/post-shell-observe.sh +48 -0
  13. package/core/harnesses/claude/hooks/post-tool-failure.sh +3 -0
  14. package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
  15. package/core/harnesses/claude/hooks/pre-implement-gate.sh +4 -1
  16. package/core/harnesses/claude/hooks/session-end.sh +23 -10
  17. package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
  18. package/core/harnesses/claude/settings/settings.json +26 -9
  19. package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
  20. package/core/harnesses/codex/agents/core-implementer.toml +1 -0
  21. package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
  22. package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
  23. package/core/harnesses/codex/agents/research-writer.toml +1 -1
  24. package/core/harnesses/codex/agents/reviewer.toml +10 -0
  25. package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
  26. package/core/harnesses/codex/agents/system-architect.toml +1 -1
  27. package/core/harnesses/codex/hooks/hooks.json +0 -10
  28. package/core/harnesses/codex/hooks/lib.sh +44 -25
  29. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +4 -1
  30. package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
  31. package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
  32. package/core/harnesses/codex/hooks/pre-implement-gate.sh +4 -1
  33. package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
  34. package/core/harnesses/codex/hooks/subagent-audit.sh +1 -0
  35. package/core/harnesses/copilot/hooks/doflow-lib.sh +94 -0
  36. package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
  37. package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
  38. package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
  39. package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
  40. package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
  41. package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
  42. package/core/harnesses/copilot/hooks/doflow.json +51 -0
  43. package/core/harnesses/kiro/agents/doflow.json +1 -11
  44. package/core/harnesses/kiro/hooks/doflow.json +0 -10
  45. package/core/harnesses/kiro/hooks/lib.sh +26 -6
  46. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +8 -3
  47. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +8 -3
  48. package/core/harnesses/kiro/hooks/session-start.sh +1 -0
  49. package/core/harnesses/kiro/hooks/stop-check.sh +1 -0
  50. package/core/harnesses/pi/extensions/doflow-guard.js +11 -2
  51. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +1 -2
  52. package/core/harnesses/shared/hooks/adapters/antigravity.js +1 -1
  53. package/core/harnesses/shared/hooks/bridge/bridge.sh +126 -23
  54. package/core/harnesses/shared/hooks/bridge/envelope.jq +87 -18
  55. package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
  56. package/core/harnesses/shared/hooks/bridge/respond.sh +16 -2
  57. package/core/harnesses/shared/hooks/policies/capture-failure.sh +49 -16
  58. package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
  59. package/core/harnesses/shared/hooks/policies/contract.sh +3 -0
  60. package/core/harnesses/shared/hooks/policies/deny-json.sh +20 -7
  61. package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
  62. package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
  63. package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
  64. package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
  65. package/core/harnesses/shared/hooks/policies/lib.sh +63 -40
  66. package/core/harnesses/shared/hooks/policies/mcp-policy.conf +1 -1
  67. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +7 -8
  68. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +132 -57
  69. package/core/harnesses/shared/hooks/policies/session-context.sh +30 -1
  70. package/core/harnesses/shared/hooks/policies/stop-check.sh +27 -15
  71. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +6 -3
  72. package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
  73. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +60 -155
  74. package/core/harnesses/shared/hooks/stream-hook-runner.js +58 -20
  75. package/core/harnesses/shared/locator/doflow-run +2 -3
  76. package/core/registry/advice.json +3 -3
  77. package/core/registry/assets.json +55 -82
  78. package/core/registry/capabilities.json +12 -0
  79. package/core/registry/contracts.json +58 -31
  80. package/core/registry/contribution-points.json +35 -0
  81. package/core/registry/gates.json +32 -0
  82. package/core/registry/guidance-caps.json +40 -0
  83. package/core/registry/harnesses.json +1445 -366
  84. package/core/registry/intake.json +53 -0
  85. package/core/registry/lifecycle.json +599 -996
  86. package/core/registry/mcp.json +5 -1
  87. package/core/registry/models.json +7 -1
  88. package/core/registry/package-templates/README.md +19 -0
  89. package/core/registry/package-templates/agent/files/agent.md +9 -0
  90. package/core/registry/package-templates/agent/template.json +4 -0
  91. package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
  92. package/core/registry/package-templates/capability-provider/template.json +11 -0
  93. package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
  94. package/core/registry/package-templates/debate-preset/template.json +4 -0
  95. package/core/registry/package-templates/intake-category/files/category.json +8 -0
  96. package/core/registry/package-templates/intake-category/template.json +4 -0
  97. package/core/registry/package-templates/review-preset/files/preset.json +30 -0
  98. package/core/registry/package-templates/review-preset/template.json +4 -0
  99. package/core/registry/package-templates/skill/files/SKILL.md +8 -0
  100. package/core/registry/package-templates/skill/template.json +4 -0
  101. package/core/registry/package-templates/worker/files/worker.json +92 -0
  102. package/core/registry/package-templates/worker/template.json +11 -0
  103. package/core/registry/packages/doflow/doflow-package.json +15 -0
  104. package/core/registry/packages/doflow/presets/debate-3.json +26 -0
  105. package/core/registry/protocols.json +61 -0
  106. package/core/registry/routes.json +17 -0
  107. package/core/registry/rules.json +431 -0
  108. package/core/registry/settings.json +272 -0
  109. package/core/registry/storage.json +687 -31
  110. package/core/registry/telemetry.json +236 -0
  111. package/core/registry/transcripts.json +142 -0
  112. package/core/registry/verification.json +76 -6
  113. package/core/registry/workflows.json +55 -0
  114. package/core/shared/agent-specs/core-implementer.md +3 -0
  115. package/core/shared/agent-specs/delivery-lead.md +68 -0
  116. package/core/shared/agent-specs/quality-guardian.md +5 -1
  117. package/core/shared/agent-specs/research-writer.md +10 -3
  118. package/core/shared/agent-specs/reviewer.md +53 -0
  119. package/core/shared/agent-specs/spec-analyst.md +5 -6
  120. package/core/shared/agent-specs/system-architect.md +5 -1
  121. package/core/shared/guidance/DOFLOW_CORE.md +1 -5
  122. package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
  123. package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
  124. package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
  125. package/core/shared/guidance/pointers/codex.md +1 -4
  126. package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
  127. package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
  128. package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
  129. package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
  130. package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
  131. package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
  132. package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
  133. package/core/shared/guidance/references/ORCHESTRATION.md +3 -2
  134. package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +19 -1
  135. package/core/shared/guidance/rules/RULES.md +45 -0
  136. package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
  137. package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
  138. package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
  139. package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
  140. package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
  141. package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
  142. package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
  143. package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
  144. package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
  145. package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
  146. package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
  147. package/core/shared/scripts/doflow/bash/validate-artifacts.sh +49 -5
  148. package/core/shared/scripts/doflow/bin/doflow-run +152 -34
  149. package/core/shared/skills/do/SKILL.md +14 -12
  150. package/core/shared/skills/do/references/advice_calls.md +26 -0
  151. package/core/shared/skills/do/references/pm_routing.md +1 -2
  152. package/core/shared/skills/do/references/program_mode.md +31 -0
  153. package/core/shared/skills/do-brainstorm/SKILL.md +45 -97
  154. package/core/shared/skills/do-brainstorm/references/handoff_branches.md +36 -0
  155. package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
  156. package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
  157. package/core/shared/skills/do-code-review/SKILL.md +48 -148
  158. package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
  159. package/core/shared/skills/do-code-review/references/tools.md +90 -0
  160. package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
  161. package/core/shared/skills/do-design/SKILL.md +23 -20
  162. package/core/shared/skills/do-diagnose/SKILL.md +18 -6
  163. package/core/shared/skills/do-document/SKILL.md +12 -1
  164. package/core/shared/skills/do-document/references/deep_research.md +1 -2
  165. package/core/shared/skills/do-document/references/embedded-research.md +19 -7
  166. package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
  167. package/core/shared/skills/do-execute-plan/SKILL.md +19 -60
  168. package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
  169. package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
  170. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +26 -1
  171. package/core/shared/skills/do-execute-plan/references/run_position.md +16 -2
  172. package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
  173. package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
  174. package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
  175. package/core/shared/skills/do-flow/SKILL.md +10 -11
  176. package/core/shared/skills/do-flow/references/task_classes.md +5 -5
  177. package/core/shared/skills/do-git/SKILL.md +4 -0
  178. package/core/shared/skills/do-implement/SKILL.md +13 -14
  179. package/core/shared/skills/do-plan/SKILL.md +34 -20
  180. package/core/shared/skills/do-test/SKILL.md +7 -4
  181. package/core/shared/templates/doflow/constitution-template.md +9 -3
  182. package/core/shared/templates/doflow/data-model-template.md +1 -1
  183. package/core/shared/templates/doflow/design-template.md +113 -16
  184. package/core/shared/templates/doflow/plan-template.md +11 -10
  185. package/core/shared/templates/doflow/question-log-template.md +14 -2
  186. package/core/shared/templates/doflow/requirement-template.md +1 -1
  187. package/core/shared/workflows/do-flow-chain.md +2 -0
  188. package/core/worker-evidence/antigravity/checks.json +14 -0
  189. package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
  190. package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
  191. package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
  192. package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
  193. package/core/worker-evidence/codex/checks.json +38 -0
  194. package/package.json +10 -6
  195. package/src/adapters/antigravity/index.js +143 -66
  196. package/src/adapters/claude/index.js +46 -5
  197. package/src/adapters/codex/agents.js +25 -15
  198. package/src/adapters/codex/hooks.js +39 -3
  199. package/src/adapters/codex/index.js +61 -13
  200. package/src/adapters/codex/native-projection.js +6 -2
  201. package/src/adapters/copilot/index.js +58 -23
  202. package/src/adapters/copy-tree.js +291 -46
  203. package/src/adapters/guidance-manifest.js +71 -0
  204. package/src/adapters/guidance-pointer.js +19 -0
  205. package/src/adapters/hook-commands.js +6 -10
  206. package/src/adapters/index.js +1 -0
  207. package/src/adapters/kiro/index.js +23 -12
  208. package/src/adapters/opencode/index.js +8 -8
  209. package/src/adapters/pi/hooks.js +2 -1
  210. package/src/adapters/pi/index.js +14 -7
  211. package/src/adapters/resolve-source.js +26 -2
  212. package/src/cli/commands/install.js +2 -1
  213. package/src/cli/commands/reconcile.js +53 -9
  214. package/src/cli/commands/remove.js +2 -2
  215. package/src/cli/commands/rollback.js +6 -3
  216. package/src/cli/commands/self-update.js +18 -9
  217. package/src/cli/commands/status.js +22 -3
  218. package/src/cli/commands/ui.js +99 -0
  219. package/src/cli/commands/update.js +2 -1
  220. package/src/cli/index.js +337 -32
  221. package/src/cli/runtime-commands.js +220 -60
  222. package/src/cli/shared.js +0 -2
  223. package/src/helper/git.js +1 -1
  224. package/src/helper/guardrails.js +1 -1
  225. package/src/helper/marker-merge.js +31 -13
  226. package/src/helper/package-ids.js +37 -0
  227. package/src/helper/semver.js +94 -0
  228. package/src/install/backup.js +105 -15
  229. package/src/install/context.js +1 -1
  230. package/src/install/targets.js +31 -16
  231. package/src/lifecycle/coverage.js +4 -16
  232. package/src/lifecycle/hook-tier.js +160 -0
  233. package/src/lifecycle/index.js +412 -78
  234. package/src/lifecycle/retired-harnesses.js +318 -0
  235. package/src/lifecycle/view.js +35 -15
  236. package/src/registry/index.js +551 -26
  237. package/src/registry/worker-ids.js +45 -0
  238. package/src/release/build-package.js +0 -3
  239. package/src/release/docs-blocks.js +472 -0
  240. package/src/release/preflight.js +425 -0
  241. package/src/release/rehearse.js +401 -0
  242. package/src/release/release-workflow.js +733 -0
  243. package/src/release/sync-plugin-versions.js +43 -7
  244. package/src/runtime/advice/bench-gate.js +25 -0
  245. package/src/runtime/advice/cli.js +59 -16
  246. package/src/runtime/advice/client.js +32 -9
  247. package/src/runtime/advice/config.js +98 -133
  248. package/src/runtime/advice/doctor.js +1 -1
  249. package/src/runtime/advice/elevation.js +97 -0
  250. package/src/runtime/advice/endpoint-guard.js +9 -3
  251. package/src/runtime/advice/mode.js +37 -0
  252. package/src/runtime/advice/record.js +18 -5
  253. package/src/runtime/advice/registry.js +4 -1
  254. package/src/runtime/advice/shadow-config.js +229 -0
  255. package/src/runtime/advice/shadow-dispatcher.js +276 -0
  256. package/src/runtime/brief-rules.js +81 -0
  257. package/src/runtime/brief-tools.js +111 -34
  258. package/src/runtime/candidate.js +20 -11
  259. package/src/runtime/capabilities/executor.js +450 -0
  260. package/src/runtime/capabilities/loopback.js +41 -0
  261. package/src/runtime/capabilities/sanitizer.js +88 -0
  262. package/src/runtime/capability-router.js +135 -3
  263. package/src/runtime/checkouts.js +73 -11
  264. package/src/runtime/claims.js +13 -7
  265. package/src/runtime/cli.js +196 -19
  266. package/src/runtime/config-home.js +42 -0
  267. package/src/runtime/context/arbitrate.js +767 -0
  268. package/src/runtime/context/delivery-store.js +4 -6
  269. package/src/runtime/context/emission-rules.js +118 -0
  270. package/src/runtime/context/identity.js +227 -0
  271. package/src/runtime/context/pointer.js +67 -12
  272. package/src/runtime/context/primary-command.js +117 -0
  273. package/src/runtime/context/primary.js +344 -0
  274. package/src/runtime/context/summary-store.js +655 -0
  275. package/src/runtime/context-pack.js +158 -79
  276. package/src/runtime/decision-register.js +13 -4
  277. package/src/runtime/dispatch/admission.js +392 -0
  278. package/src/runtime/dispatch/attempt-end.js +274 -0
  279. package/src/runtime/dispatch/command.js +628 -0
  280. package/src/runtime/dispatch/controls.js +904 -0
  281. package/src/runtime/dispatch/envelope.js +276 -0
  282. package/src/runtime/dispatch/grants.js +310 -0
  283. package/src/runtime/dispatch/integrate.js +617 -0
  284. package/src/runtime/dispatch/loop.js +1835 -0
  285. package/src/runtime/dispatch/store.js +352 -0
  286. package/src/runtime/dispatch/verify-run.js +201 -0
  287. package/src/runtime/evidence-ledger.js +52 -7
  288. package/src/runtime/failure/capture.js +3 -1
  289. package/src/runtime/failure/cli.js +25 -9
  290. package/src/runtime/failure/home.js +57 -30
  291. package/src/runtime/failure/store.js +7 -2
  292. package/src/runtime/feature-pointer.js +174 -0
  293. package/src/runtime/feature-resolve.js +13 -2
  294. package/src/runtime/fleet/command.js +77 -0
  295. package/src/runtime/fleet/projects.js +178 -0
  296. package/src/runtime/fleet/tree.js +383 -0
  297. package/src/runtime/freshness.js +33 -3
  298. package/src/runtime/git-memo.js +46 -0
  299. package/src/runtime/guidance/ablation.js +427 -0
  300. package/src/runtime/health.js +624 -18
  301. package/src/runtime/history-compactor.js +13 -3
  302. package/src/runtime/implementation-gate.js +281 -26
  303. package/src/runtime/intake/engine.js +677 -0
  304. package/src/runtime/intake/index.js +136 -0
  305. package/src/runtime/intake/manifest.js +140 -0
  306. package/src/runtime/intake/taxonomy.js +206 -0
  307. package/src/runtime/inventory/coverage.js +1 -1
  308. package/src/runtime/inventory/index.js +1 -1
  309. package/src/runtime/inventory/precedence.js +2 -2
  310. package/src/runtime/inventory/siblings.js +6 -4
  311. package/src/runtime/knowledge/code-corpus.js +277 -0
  312. package/src/runtime/knowledge/code-lexical.js +130 -0
  313. package/src/runtime/knowledge/code-retrieval.js +307 -0
  314. package/src/runtime/knowledge/code-validation.js +106 -0
  315. package/src/runtime/knowledge/gp3-gate.js +74 -0
  316. package/src/runtime/knowledge/graph-adapter.js +288 -0
  317. package/src/runtime/knowledge/graph-fallback.js +394 -0
  318. package/src/runtime/knowledge/hybrid.js +136 -8
  319. package/src/runtime/knowledge/retrieval.js +34 -1
  320. package/src/runtime/knowledge/rtk-compress.js +77 -0
  321. package/src/runtime/knowledge/semble-bridge.py +106 -0
  322. package/src/runtime/knowledge/semble-lifecycle.js +114 -0
  323. package/src/runtime/knowledge/semble-provider.js +365 -0
  324. package/src/runtime/lifecycle/cli.js +88 -10
  325. package/src/runtime/lifecycle/event-store.js +8 -6
  326. package/src/runtime/lifecycle/followup.js +38 -7
  327. package/src/runtime/lifecycle/overview.js +26 -2
  328. package/src/runtime/lifecycle/report-store.js +8 -10
  329. package/src/runtime/lifecycle/retention.js +95 -31
  330. package/src/runtime/lifecycle/root.js +9 -1
  331. package/src/runtime/lifecycle/status.js +34 -17
  332. package/src/runtime/lifecycle/store-upkeep.js +56 -7
  333. package/src/runtime/mask.js +134 -7
  334. package/src/runtime/mcp-consultations.js +40 -6
  335. package/src/runtime/model-router.js +79 -7
  336. package/src/runtime/models/catalogue.js +191 -0
  337. package/src/runtime/models/command.js +153 -0
  338. package/src/runtime/off-words.js +12 -0
  339. package/src/runtime/outcome.js +2 -2
  340. package/src/runtime/packages/command.js +1009 -0
  341. package/src/runtime/packages/contributions.js +116 -0
  342. package/src/runtime/packages/digest.js +132 -0
  343. package/src/runtime/packages/fetch.js +182 -0
  344. package/src/runtime/packages/grants.js +192 -0
  345. package/src/runtime/packages/index-file.js +134 -0
  346. package/src/runtime/packages/instruction.js +90 -0
  347. package/src/runtime/packages/manifest.js +331 -0
  348. package/src/runtime/packages/points.js +487 -0
  349. package/src/runtime/packages/programs.js +216 -0
  350. package/src/runtime/packages/scaffold.js +277 -0
  351. package/src/runtime/packages/source.js +90 -0
  352. package/src/runtime/packages/state.js +164 -0
  353. package/src/runtime/packages/store.js +449 -0
  354. package/src/runtime/packages/tar.js +106 -0
  355. package/src/runtime/packages/view.js +406 -0
  356. package/src/runtime/plan-check.js +59 -0
  357. package/src/runtime/plan-content.js +324 -0
  358. package/src/runtime/policy-init.js +118 -0
  359. package/src/runtime/profiles/doctor.js +96 -0
  360. package/src/runtime/profiles/resolve.js +244 -0
  361. package/src/runtime/profiles/rollback.js +254 -0
  362. package/src/runtime/profiles/sampler.js +41 -0
  363. package/src/runtime/readiness-record.js +16 -11
  364. package/src/runtime/readiness.js +46 -5
  365. package/src/runtime/research-request.js +50 -25
  366. package/src/runtime/retrieval-plan.js +10 -30
  367. package/src/runtime/retrieval-record.js +75 -5
  368. package/src/runtime/review/command.js +246 -0
  369. package/src/runtime/review/dominance.js +68 -0
  370. package/src/runtime/review/findings.js +434 -0
  371. package/src/runtime/review/identity.js +255 -0
  372. package/src/runtime/review/protocol-command.js +109 -0
  373. package/src/runtime/review/protocols.js +405 -0
  374. package/src/runtime/review/resolve-protocol.js +302 -0
  375. package/src/runtime/review/start.js +530 -0
  376. package/src/runtime/roadmap/attendance.js +137 -0
  377. package/src/runtime/roadmap/facts.js +479 -0
  378. package/src/runtime/roadmap/gates.js +273 -0
  379. package/src/runtime/roadmap/loop.js +1719 -0
  380. package/src/runtime/roadmap/merge.js +390 -0
  381. package/src/runtime/roadmap/rows.js +1122 -0
  382. package/src/runtime/roadmap/seed.js +1063 -0
  383. package/src/runtime/roadmap/worktree-cleanup.js +147 -0
  384. package/src/runtime/rules-report.js +336 -0
  385. package/src/runtime/scaffold/artifacts.js +60 -9
  386. package/src/runtime/scaffold/generate.js +21 -9
  387. package/src/runtime/settings/autopilot.js +106 -0
  388. package/src/runtime/settings/cli.js +280 -0
  389. package/src/runtime/settings/config-write.js +314 -0
  390. package/src/runtime/settings/files.js +271 -0
  391. package/src/runtime/settings/legacy.js +154 -0
  392. package/src/runtime/settings/lexical.js +56 -0
  393. package/src/runtime/settings/prepared-run.js +269 -0
  394. package/src/runtime/settings/project-layers.js +118 -0
  395. package/src/runtime/settings/refs.js +288 -0
  396. package/src/runtime/settings/registry.js +553 -0
  397. package/src/runtime/settings/resolve.js +713 -0
  398. package/src/runtime/settings/shared.js +37 -0
  399. package/src/runtime/settings/worker-billing.js +107 -0
  400. package/src/runtime/settings/write.js +269 -0
  401. package/src/runtime/slot-command.js +78 -0
  402. package/src/runtime/slots.js +671 -0
  403. package/src/runtime/source-version.js +1 -0
  404. package/src/runtime/state-root.js +217 -0
  405. package/src/runtime/storage/cli.js +12 -5
  406. package/src/runtime/storage/engine.js +9 -2
  407. package/src/runtime/storage/registry.js +14 -1
  408. package/src/runtime/storage/roots.js +66 -16
  409. package/src/runtime/storage/rules.js +82 -10
  410. package/src/runtime/storage/upkeep.js +10 -2
  411. package/src/runtime/storage/work-status.js +245 -26
  412. package/src/runtime/task-attempts.js +589 -69
  413. package/src/runtime/task-classifier.js +85 -14
  414. package/src/runtime/task-graph.js +373 -0
  415. package/src/runtime/task-run.js +778 -0
  416. package/src/runtime/task-scope.js +176 -55
  417. package/src/runtime/task-sources.js +147 -0
  418. package/src/runtime/task-state.js +373 -64
  419. package/src/runtime/team/admission.js +301 -0
  420. package/src/runtime/team/envelope.js +324 -0
  421. package/src/runtime/team/review-guard.js +156 -0
  422. package/src/runtime/trace/analyze.js +427 -0
  423. package/src/runtime/trace/baseline.js +770 -0
  424. package/src/runtime/trace/ledger.js +63 -140
  425. package/src/runtime/trace/otlp-export.js +568 -0
  426. package/src/runtime/trace/otlp-map.js +481 -0
  427. package/src/runtime/trace/spans.js +745 -0
  428. package/src/runtime/trace/telemetry.js +225 -0
  429. package/src/runtime/trace/views.js +1 -0
  430. package/src/runtime/transcripts/command.js +98 -0
  431. package/src/runtime/transcripts/cursor.js +326 -0
  432. package/src/runtime/transcripts/read.js +126 -0
  433. package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
  434. package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
  435. package/src/runtime/transcripts/readers/copilot-events.js +90 -0
  436. package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
  437. package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
  438. package/src/runtime/transcripts/registry.js +161 -0
  439. package/src/runtime/transcripts/stats.js +161 -0
  440. package/src/runtime/verb-view.js +267 -0
  441. package/src/runtime/verification/check-key.js +628 -0
  442. package/src/runtime/verification/contract-runner.js +546 -11
  443. package/src/runtime/verification/engine.js +499 -54
  444. package/src/runtime/verification/impact.js +617 -0
  445. package/src/runtime/verification/inflight.js +205 -0
  446. package/src/runtime/verification/registry.js +176 -0
  447. package/src/runtime/verification/reuse.js +184 -0
  448. package/src/runtime/verification/telemetry-emit.js +254 -0
  449. package/src/runtime/verification/test-selection.js +67 -7
  450. package/src/runtime/worker/active.js +90 -0
  451. package/src/runtime/worker/cancel.js +222 -0
  452. package/src/runtime/worker/catalogue.js +122 -0
  453. package/src/runtime/worker/command.js +677 -0
  454. package/src/runtime/worker/evidence.js +288 -0
  455. package/src/runtime/worker/launch.js +256 -0
  456. package/src/runtime/worker/models.js +137 -0
  457. package/src/runtime/worker/parsers.js +218 -0
  458. package/src/runtime/worker/probes/index.js +706 -0
  459. package/src/runtime/worker/process-tree.js +415 -0
  460. package/src/runtime/worker/quota.js +43 -0
  461. package/src/runtime/worker/readiness.js +713 -0
  462. package/src/runtime/worker/run-store.js +166 -0
  463. package/src/runtime/worker/supervisor.js +541 -0
  464. package/src/runtime/worker/worktree-check.js +252 -0
  465. package/src/runtime/workflow-engine.js +5 -1
  466. package/src/runtime/workflow-orchestrator.js +610 -67
  467. package/src/runtime/worktree.js +149 -22
  468. package/src/state/index.js +1 -1
  469. package/src/state/lockfile.js +64 -10
  470. package/src/state/run-lock.js +1 -1
  471. package/types/node-commonjs.d.ts +106 -7
  472. package/core/harnesses/claude/hooks/pre-bash-guard.sh +0 -10
  473. package/core/harnesses/codex/hooks/pre-bash-guard.sh +0 -10
  474. package/core/harnesses/gemini/hooks/hooks.json +0 -77
  475. package/core/harnesses/gemini/hooks/lib.sh +0 -302
  476. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -10
  477. package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
  478. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -10
  479. package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
  480. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -10
  481. package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
  482. package/core/harnesses/gemini/hooks/session-end.sh +0 -5
  483. package/core/harnesses/gemini/hooks/session-start.sh +0 -7
  484. package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
  485. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +0 -24
  486. package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +0 -91
  487. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +0 -378
  488. package/core/shared/guidance/FLAGS.md +0 -25
  489. package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
  490. package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
  491. package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
  492. package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
  493. package/src/adapters/gemini/hooks.js +0 -251
  494. package/src/adapters/gemini/index.js +0 -429
@@ -0,0 +1,745 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ const nodeFs = require('node:fs');
6
+ const path = require('node:path');
7
+ const crypto = require('node:crypto');
8
+ const { resolveRunsLocation, normalizeRunRecord } = require('./ledger.js');
9
+ const { SAFE_TOKEN } = require('./telemetry.js');
10
+ const { finish } = require('./render.js');
11
+ const { REPO_ROOT } = require('../../helper/repo-root.js');
12
+
13
+ /**
14
+ * `trace spans` (088 IC-011, FR-005, DD-5): spans projected offline from the records that already
15
+ * exist under `<config>/state`: the run ledger (`runs/*.jsonl`), telemetry lines
16
+ * (`runs/telemetry/*.jsonl`), 080 task-run records (`task-attempts/*.json`), orchestration records
17
+ * (`orchestration/*.json`), worker runs (`worker-runs/*.json`) and 082 dispatch stores
18
+ * (`dispatch/*.json`, read only for when a run ended). There is no span store.
19
+ *
20
+ * A trace is one epoch segment of one 080 run: a `start` or `resume` history entry with a `traceId`
21
+ * opens `[at, next resume or closing cancel)`. The 080 store is per slug (`task-attempts/<slug>.json`)
22
+ * and its run key is per session, so one session working on two slugs has two segments under one
23
+ * run key; each segment carries its slug, the file stem (110). The 080 record itself has no
24
+ * completion marker, so a segment also closes, inclusive, at the latest end among the activities it
25
+ * owns that started inside it, once every one of them has ended: an orchestration run ends at its
26
+ * last entry after `complete-run` or a `reject` gate decision; a dispatch run at `completedAt` when
27
+ * `completed`, `endedAt` when `cancelled` (`blocked`, `resume-required` and `cancelling` can still
28
+ * move on). A dispatch run owns the segments of its `run.slug`; an orchestration run those of its
29
+ * `traceId`, else of its `featureSlug`; a record with neither, or a slug that owns no segment of the
30
+ * run key, owns every segment of its `runKey` as before 110. A dispatch record whose `run`/`lineage`
31
+ * disagree with its `runKey`, or with no end stamp, closes nothing.
32
+ *
33
+ * A worker run joins through its slug: `(slug, task, token)` -> that slug's attempt -> the attempt's
34
+ * segment, else the segment of its slug and caller run key that holds its start. A record with a run
35
+ * identity joins the one segment that holds it, or is a gap named by its locator: none holds it
36
+ * (`unmatched`) or more than one does (`ambiguous`, e.g. one session identity under two worktrees).
37
+ * Ids beat windows (110 F3): a hook line's `worker_run` follows that worker, a verify.run's
38
+ * `dispatch_run` goes to that dispatch run's segment that holds it (T1). An id that names no record,
39
+ * or a record with no placement, leaves the line unmatched/ambiguous; it never joins another run.
40
+ * A line that more than one segment holds joins one of them only through the single activity it
41
+ * fits, labelled `attrs.join: "activity"` (T2): a hook line with an attempt fits the one worker
42
+ * whose identity and lifetime match and that every hook line of its session resolves to; a
43
+ * `source: dispatch` verify.run fits the one dispatch run of its identity whose window holds it,
44
+ * and whose task or slug is the line's task when it names one. A line T2 cannot settle stays
45
+ * `ambiguous`, and a line one segment holds joins it unlabelled, as before 110 (T3).
46
+ * A line stamped to the whole second (doflow-run's ledger lines) is compared at that resolution:
47
+ * the segment start is floored to its second, so the `dispatch start` line of the run's own start
48
+ * second joins, and a second shared by two segments makes it `ambiguous`. A segment without a trace id
49
+ * (a run recorded before 088, or with telemetry off) holds its records as `ungrouped`, as do
50
+ * records with no identity, grouped by `session`, else by UTC day.
51
+ *
52
+ * Read only: every input is read through `fsImpl` read calls, nothing is written. The output has
53
+ * sorted keys and no wall-clock field, so the same stores give the same bytes. Attribute values are
54
+ * identifiers or numbers only (SAFE_TOKEN), so free text, paths and commands in a record never
55
+ * reach the output (NFR-004).
56
+ *
57
+ * Accepted ceiling: a history `cancel` entry does not say whether it cancelled the run or one
58
+ * attempt. The last cancel of a segment closes it only when no pause follows it and the run was
59
+ * resumed after it or is cancelled now; any other reading leaves the segment open to the next
60
+ * resume, and a misread shows up as located `unmatched` gaps, never as a wrong join. An activity
61
+ * that began before a resume counts for the segment it began in only.
62
+ */
63
+
64
+ const PARTITION = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
65
+ const STORE_FILE = /^[A-Za-z0-9][A-Za-z0-9._-]*\.json$/;
66
+ const NAMESPACE_DIR = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
67
+ const HEX16 = /^[0-9a-f]{16}$/;
68
+ const HEX32 = /^[0-9a-f]{32}$/;
69
+ const WORKER_RUN_ID = /^w-[0-9a-f]{12}$/;
70
+ const RUN_KEY = /^[A-Za-z0-9._:-]+\/[A-Za-z0-9._:-]+\/[A-Za-z0-9._:-]+$/;
71
+ /** `--run`: a run key or a run id. */
72
+ const RUN_ARG = /^[A-Za-z0-9][A-Za-z0-9._:-]*(?:\/[A-Za-z0-9._:-]+){0,2}$/;
73
+ /** UTC only: a timestamp without `Z` would parse in the local zone and break byte equality. */
74
+ const ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
75
+ /** A timestamp with no fraction: true only to the whole second. */
76
+ const WHOLE_SECOND = /:\d{2}Z$/;
77
+ const PRE_EVENTS = new Set(['PreToolUse', 'tool_call']);
78
+ const POST_EVENTS = new Set(['PostToolUse', 'PostToolUseFailure']);
79
+ const LEDGER_ATTRS = [['verb', 'verb'], ['outcome', 'outcome'], ['capability', 'capability'],
80
+ ['provider', 'provider'], ['stage', 'stage'], ['workflow', 'workflow'], ['taskClass', 'task_class'], ['exitCode', 'exit_code']];
81
+
82
+ let registryCache = null;
83
+ function telemetryKinds() {
84
+ if (!registryCache) {
85
+ try { registryCache = JSON.parse(nodeFs.readFileSync(path.join(REPO_ROOT, 'core', 'registry', 'telemetry.json'), 'utf8')).kinds || {}; } catch { registryCache = {}; }
86
+ }
87
+ return registryCache;
88
+ }
89
+
90
+ const sha16 = (text ) => crypto.createHash('sha256').update(text).digest('hex').slice(0, 16);
91
+ const str = (v ) => (typeof v === 'string' && v !== '' ? v : null);
92
+ const safe = (v ) => (v === null || typeof v === 'boolean' || (typeof v === 'number' && Number.isFinite(v))
93
+ || (typeof v === 'string' && SAFE_TOKEN.test(v)) ? v : undefined);
94
+ const iso = (v ) => (typeof v === 'string' && ISO_UTC.test(v) && Number.isFinite(Date.parse(v)) ? v : null);
95
+ const msOf = (v ) => (v === null ? null : Date.parse(v));
96
+ const runKeyOf = (v ) => (typeof v === 'string' && RUN_KEY.test(v) ? v : null);
97
+ /** A store file's slug: `task-attempts/<slug>.json` -> `<slug>`. */
98
+ const stemOf = (file ) => path.basename(file, '.json');
99
+
100
+ /** The allowlisted attributes of `source`, each an identifier or a number; anything else is left out. */
101
+ function pick(source , pairs ) {
102
+ const out = {};
103
+ for (const [from, to] of pairs) {
104
+ const value = source && typeof source === 'object' ? safe(source[from]) : undefined;
105
+ if (value !== undefined) out[to] = value;
106
+ }
107
+ return out;
108
+ }
109
+ const same = (keys ) => keys.map((k) => [k, k]);
110
+
111
+ const PACK_DIGEST = /^sha256:[0-9a-f]{64}$/;
112
+ /**
113
+ * Issue #9 stage 3: a stage span's pack reference, from its `complete-stage` entry's `packs`
114
+ * (oldest first): how many packs the stage was handed, and the task id and digest of the latest.
115
+ * A record written before `packs` existed, or a malformed item, adds nothing: absent is unknown.
116
+ */
117
+ function packAttrs(packs ) {
118
+ const valid = (Array.isArray(packs) ? packs : []).filter((p ) => p && typeof p === 'object'
119
+ && typeof p.taskId === 'string' && SAFE_TOKEN.test(p.taskId) && typeof p.digest === 'string' && PACK_DIGEST.test(p.digest));
120
+ if (!valid.length) return {};
121
+ const last = valid[valid.length - 1];
122
+ return { pack_count: valid.length, pack_task: last.taskId, pack_digest: last.digest };
123
+ }
124
+
125
+ /** A span in the making: output fields plus where it is placed and what it may parent to. */
126
+ function item(kind , name , locator , { start = null, end = null, durationMs = null, basis = 'recorded', attrs = {}, place = { type: 'none' }, session = null, link = {} } = {}) {
127
+ let stop = end;
128
+ let took = durationMs;
129
+ if (start !== null && stop !== null) took = Math.max(0, (msOf(stop) ) - (msOf(start) ));
130
+ else if (start !== null && Number.isInteger(took) && took >= 0) stop = new Date((msOf(start) ) + took).toISOString();
131
+ else took = null;
132
+ return { kind, name: safe(name) ?? null, locator, start, end: stop, durationMs: took, basis, attrs, place, session: safe(session) ?? null, link };
133
+ }
134
+
135
+ // ── reading ─────────────────────────────────────────────────────────────────────────────────────
136
+
137
+ function listDir(fsImpl , dir , re ) {
138
+ try { return fsImpl.readdirSync(dir).filter((name ) => re.test(name)).sort(); } catch { return []; }
139
+ }
140
+
141
+ /** Larger than any real partition or store; a bigger file is a gap, not a read. */
142
+ const MAX_STORE_BYTES = 64 * 1024 * 1024;
143
+
144
+ /**
145
+ * The text of a regular file, or null for anything else. A store directory is the project's, but a
146
+ * FIFO, socket, device, directory or symlink can be planted in it, and a plain read of a FIFO with no
147
+ * writer blocks the verb forever. So: lstat and skip what is not a regular file; open
148
+ * O_RDONLY|O_NOFOLLOW|O_NONBLOCK so a swap after the lstat fails instead of blocking or following;
149
+ * fstat the descriptor and check size before reading it.
150
+ */
151
+ function readRegular(fsImpl , file ) {
152
+ try {
153
+ if (!fsImpl.lstatSync(file).isFile()) return null;
154
+ const { O_RDONLY, O_NOFOLLOW = 0, O_NONBLOCK = 0 } = nodeFs.constants;
155
+ const fd = fsImpl.openSync(file, O_RDONLY | O_NOFOLLOW | O_NONBLOCK);
156
+ try {
157
+ const stat = fsImpl.fstatSync(fd);
158
+ if (!stat.isFile() || stat.size > MAX_STORE_BYTES) return null;
159
+ return fsImpl.readFileSync(fd, 'utf8');
160
+ } finally {
161
+ fsImpl.closeSync(fd);
162
+ }
163
+ } catch {
164
+ return null;
165
+ }
166
+ }
167
+
168
+ /** Each line of each partition as `{ locator, value }`, or a gap when it does not parse. */
169
+ function readLines(fsImpl , dir , rel , gaps , kind ) {
170
+ const out = [];
171
+ for (const name of listDir(fsImpl, dir, PARTITION)) {
172
+ const file = `${rel}/${name}`;
173
+ const text = readRegular(fsImpl, path.join(dir, name));
174
+ if (text === null) { gaps.push({ locator: file, reason: 'unparseable', kind }); continue; }
175
+ text.split('\n').forEach((line, index) => {
176
+ if (line.trim() === '') return;
177
+ const locator = `${file}:${index + 1}`;
178
+ try { out.push({ locator, value: JSON.parse(line) }); } catch { gaps.push({ locator, reason: 'unparseable', kind }); }
179
+ });
180
+ }
181
+ return out;
182
+ }
183
+
184
+ /** Each JSON store file of a directory as `{ file, value }`, or a gap when it does not parse. With
185
+ * `namespaced`, the files of each `<slug>/` directory come too (115: a run of a task id other than its
186
+ * feature's slug lives in `orchestration/<slug>/`); a flat file whose namespaced copy exists is that
187
+ * copy's seed, not a second record, and is left out. */
188
+ function readStores(fsImpl , stateDir , rel , gaps , kind , namespaced = false) {
189
+ const out = [];
190
+ const read = (file , name , dir , namespace ) => {
191
+ try {
192
+ const text = readRegular(fsImpl, path.join(dir, name));
193
+ if (text === null) throw new Error('not a readable regular file');
194
+ const value = JSON.parse(text);
195
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('not an object');
196
+ out.push({ file, value, name, namespace });
197
+ } catch { gaps.push({ locator: file, reason: 'unparseable', kind }); }
198
+ };
199
+ const base = path.join(stateDir, rel);
200
+ for (const name of listDir(fsImpl, base, STORE_FILE)) read(`${rel}/${name}`, name, base, '');
201
+ if (!namespaced) return out.map(({ file, value }) => ({ file, value }));
202
+ let entries = [];
203
+ try { entries = fsImpl.readdirSync(base, { withFileTypes: true }); } catch { entries = []; }
204
+ const namespaces = entries.filter((entry ) => entry.isDirectory() && NAMESPACE_DIR.test(entry.name) && !entry.name.endsWith('.lock'))
205
+ .map((entry ) => entry.name).sort();
206
+ for (const namespace of namespaces) {
207
+ const dir = path.join(base, namespace);
208
+ for (const name of listDir(fsImpl, dir, STORE_FILE)) read(`${rel}/${namespace}/${name}`, name, dir, namespace);
209
+ }
210
+ const copied = (flat ) => out.some((other) => other.namespace !== ''
211
+ && other.name === flat.name && (typeof flat.value.featureSlug !== 'string' || flat.value.featureSlug === other.namespace));
212
+ return out.filter((entry) => entry.namespace !== '' || !copied(entry)).map(({ file, value }) => ({ file, value }));
213
+ }
214
+
215
+ const objects = (v ) => (v && typeof v === 'object' && !Array.isArray(v)
216
+ ? Object.entries(v).filter(([, x]) => x && typeof x === 'object' && !Array.isArray(x)) : []);
217
+
218
+ // ── 080 segments ────────────────────────────────────────────────────────────────────────────────
219
+
220
+ /** The run's epoch segments and one item per history entry, placed in its segment by index. */
221
+ function segmentsOf(file , runKey , run , items ) {
222
+ const history = Array.isArray(run.history) ? run.history : [];
223
+ const [, runId, lineage] = runKey.split('/');
224
+ const segments = [];
225
+ const open = (entry , index ) => {
226
+ const traceId = entry && HEX32.test(entry.traceId) ? entry.traceId : null;
227
+ const from = entry ? iso(entry.at) : iso(run.createdAt);
228
+ segments.push({ file, slug: stemOf(file), runKey, run: runId, lineage, index, traceId,
229
+ traceLink: traceId && HEX32.test(entry.traceLink) ? entry.traceLink : null,
230
+ epoch: entry ? (Number.isInteger(entry.epoch) ? entry.epoch : null) : 0, from, to: null , closeMs: null , opener: null , entries: [] });
231
+ };
232
+ history.forEach((entry, index) => {
233
+ const opens = entry && (entry.action === 'start' || entry.action === 'resume');
234
+ if (opens) open(entry, index);
235
+ else if (!segments.length) open(null, -1); // records before any start: one untraced segment from createdAt
236
+ segments[segments.length - 1].entries.push(index);
237
+ });
238
+ segments.forEach((seg, n) => {
239
+ const next = segments[n + 1];
240
+ seg.to = next ? next.from : null;
241
+ const cancels = seg.entries.filter((i ) => history[i]?.action === 'cancel');
242
+ const last = cancels[cancels.length - 1];
243
+ const pausedAfter = seg.entries.some((i ) => i > last && history[i]?.action === 'pause');
244
+ if (last !== undefined && !pausedAfter && (next || run.control === 'cancelled')) seg.to = iso(history[last].at) ?? seg.to;
245
+ seg.fromMs = seg.from === null ? -Infinity : msOf(seg.from);
246
+ seg.toMs = seg.to === null ? null : msOf(seg.to);
247
+ for (const i of seg.entries) {
248
+ const entry = history[i] || {};
249
+ const start = iso(entry.at);
250
+ const opener = i === seg.index;
251
+ const it = item(opener ? 'run' : 'control', entry.action, `${file}#${runKey}/${i}`, {
252
+ start, end: opener ? seg.to : start, attrs: pick(entry, same(['action', 'epoch'])), place: { type: 'segment', seg },
253
+ });
254
+ if (opener) seg.opener = it;
255
+ items.push(it);
256
+ }
257
+ });
258
+ return segments;
259
+ }
260
+
261
+ /** Whether `seg` holds `t`; `coarse` (a whole-second stamp) compares against the start's second. */
262
+ const holds = (seg , t , coarse = false) => t !== null
263
+ && (coarse ? Math.floor(seg.fromMs / 1000) * 1000 : seg.fromMs) <= t
264
+ && (seg.toMs === null || t < seg.toMs) && (seg.closeMs === null || t <= seg.closeMs);
265
+
266
+ /**
267
+ * The segments of `runKey` that a record owned by `traceId`, else by `slug`, belongs to (110 F2).
268
+ * A trace id names its segment only. A slug that owns no segment of that run key (a store whose
269
+ * slug names no 080 record there) and a record with neither keep every segment of the run key, as
270
+ * before 110.
271
+ */
272
+ function owned(segments , runKey , { traceId = null, slug = null } = {}) {
273
+ const keyed = segments.filter((seg) => seg.runKey === runKey);
274
+ if (traceId) return keyed.filter((seg) => seg.traceId === traceId);
275
+ const mine = slug ? keyed.filter((seg) => seg.slug === slug) : [];
276
+ return mine.length ? mine : keyed;
277
+ }
278
+
279
+ /**
280
+ * Closes each segment whose activities (the orchestration and dispatch runs it owns, started
281
+ * inside it) have all ended, at the latest end among them, when that is inside the segment. One
282
+ * activity still going keeps it open.
283
+ * @param {Object[]} segments
284
+ * @param {Array<{runKey: string, traceId: string|null, slug: string|null, startMs: number|null, ended: boolean, last: string|null}>} runs
285
+ */
286
+ function closeEnded(segments , runs ) {
287
+ const scope = new Map(runs.map((o) => [o, new Set(owned(segments, o.runKey, o))]));
288
+ for (const seg of segments) {
289
+ const mine = runs.filter((o) => scope.get(o) .has(seg) && holds(seg, o.startMs));
290
+ if (!mine.length || !mine.every((o) => o.ended && o.last !== null)) continue;
291
+ const last = mine.map((o) => o.last).sort((a, b) => (msOf(a) ) - (msOf(b) )).pop();
292
+ const lastMs = msOf(last) ;
293
+ if (lastMs < seg.fromMs || (seg.toMs !== null && lastMs >= seg.toMs)) continue;
294
+ seg.closeMs = lastMs;
295
+ seg.to = last;
296
+ if (seg.opener) {
297
+ seg.opener.end = last;
298
+ seg.opener.durationMs = seg.opener.start === null ? null : Math.max(0, lastMs - (msOf(seg.opener.start) ));
299
+ }
300
+ }
301
+ }
302
+
303
+ // ── the projection ──────────────────────────────────────────────────────────────────────────────
304
+
305
+ /**
306
+ * The spans view of the stores under `stateDir`. Pure over what it reads.
307
+ * @param {Object} options
308
+ * @param {string} options.stateDir `<config>/state`
309
+ * @param {string|null} [options.run] keep the traces of this run key, else of this run id
310
+ * @param {Object} [options.fsImpl=fs] read calls only
311
+ * @returns {{v: 1, traces: Object[], ungrouped: Object[], gaps: Object[]}}
312
+ */
313
+ function buildSpans({ stateDir, run = null, fsImpl = nodeFs } ) {
314
+ const gaps = [];
315
+ const items = [];
316
+ const segments = [];
317
+ const aliases = []; // run-scoped worker identities -> the caller's run key
318
+ const attemptOf = new Map (); // `${slug}\n${task}\n${token}` -> the attempt's placement
319
+ const workers = []; // each worker run with the identity and lifetime its hook lines match
320
+ const dispatchRuns = []; // each 082 dispatch run tied to its run key
321
+
322
+ for (const { file, value } of readStores(fsImpl, stateDir, 'task-attempts', gaps, 'task-run')) {
323
+ const runs = objects(value.runs);
324
+ for (const [key, rec] of runs) {
325
+ const runKey = runKeyOf(rec.runKey) === key ? key : null;
326
+ if (runKey) segments.push(...segmentsOf(file, runKey, rec, items));
327
+ }
328
+ for (const [taskId, entry] of objects(value.tasks)) {
329
+ (Array.isArray(entry.attempts) ? entry.attempts : []).forEach((a , n ) => {
330
+ if (!a || typeof a !== 'object') return;
331
+ const runKey = runKeyOf(a.runKey);
332
+ const token = Number.isInteger(a.token) ? a.token : n + 1;
333
+ const place = runKey ? { type: 'attempt', file, runKey, epoch: a.epoch } : { type: 'none' };
334
+ if (runKey) attemptOf.set(`${stemOf(file)}\n${taskId}\n${token}`, place);
335
+ items.push(item('attempt', taskId, `${file}#${runKey ?? '-'}/${taskId}/${token}`, {
336
+ start: iso(a.startedAt), end: iso(a.finishedAt) ?? iso(a.endedAt) ?? iso(a.cancel?.at),
337
+ attrs: { ...pick(a, same(['token', 'state', 'mode', 'result', 'epoch'])), ...pick({ task: taskId }, same(['task'])) },
338
+ place,
339
+ link: { attemptOf: { task: taskId, token: String(token) } },
340
+ }));
341
+ });
342
+ }
343
+ }
344
+
345
+ for (const { file, value } of readStores(fsImpl, stateDir, 'worker-runs', gaps, 'worker')) {
346
+ for (const [runId, w] of objects(value.runs)) {
347
+ if (!WORKER_RUN_ID.test(runId)) continue;
348
+ const start = iso(w.startedAt) ?? iso(w.createdAt);
349
+ const callerKey = runKeyOf(w.caller?.runKey);
350
+ const end = iso(w.end?.at);
351
+ const token = Number.isInteger(w.token) ? String(w.token) : null;
352
+ const worker = item('worker', w.harness, `${file}#${runId}`, {
353
+ start, end,
354
+ attrs: { ...pick(w, same(['task', 'token', 'harness', 'phase'])), ...pick({ runId, endState: w.end?.state }, same(['runId', 'endState'])) },
355
+ place: callerKey ? { type: 'worker', slug: stemOf(file), task: str(w.task), token, runKey: callerKey, at: msOf(start) } : { type: 'none' },
356
+ link: { workerOf: { task: str(w.task), token } },
357
+ });
358
+ items.push(worker);
359
+ const id = w.identity;
360
+ workers.push({ item: worker, runId, run: str(id?.run), lineage: str(id?.lineage),
361
+ attempt: id?.attempt === null || id?.attempt === undefined ? null : String(id.attempt), startMs: msOf(start), endMs: msOf(end) });
362
+ if (callerKey && id && id.runSource === 'run-scoped' && str(id.run) && str(id.lineage)) aliases.push({ run: id.run, lineage: id.lineage, runKey: callerKey, worker });
363
+ }
364
+ }
365
+
366
+ const activities = [];
367
+ for (const { file, value } of readStores(fsImpl, stateDir, 'orchestration', gaps, 'orchestration', true)) {
368
+ const runKey = runKeyOf(value.runKey);
369
+ const history = Array.isArray(value.history) ? value.history : [];
370
+ let stageStart = iso(value.startedAt) ?? iso(history[0]?.at);
371
+ const ended = history.some((e) => e && (e.action === 'complete-run' || (e.action === 'decide-gate' && e.detail === 'reject')));
372
+ const stamps = history.map((e) => (e && typeof e === 'object' ? iso(e.at) : null)).filter((a) => a !== null);
373
+ const last = stamps.sort((a, b) => (msOf(a) ) - (msOf(b) )).pop() ?? null;
374
+ if (runKey) {
375
+ activities.push({ runKey, traceId: HEX32.test(value.traceId) ? value.traceId : null, slug: str(value.featureSlug), startMs: msOf(stageStart), ended, last });
376
+ }
377
+ history.forEach((entry, i) => {
378
+ if (!entry || typeof entry !== 'object') return;
379
+ const at = iso(entry.at);
380
+ const place = runKey ? { type: 'runKey', runKey, at: msOf(at) } : { type: 'none' };
381
+ const attrs = pick(entry, same(['action', 'node', 'outcome', 'executionStatus']));
382
+ const locator = `${file}#${runKey ?? '-'}/${i}`;
383
+ if (entry.action === 'complete-stage') {
384
+ Object.assign(attrs, packAttrs(entry.packs));
385
+ items.push(item('stage', entry.node, locator, { start: stageStart, end: at, basis: 'derived', attrs, place }));
386
+ stageStart = at;
387
+ } else {
388
+ items.push(item('orchestration', entry.action, locator, { start: at, end: at, attrs, place }));
389
+ }
390
+ });
391
+ }
392
+
393
+ for (const { file, value } of readStores(fsImpl, stateDir, 'dispatch', gaps, 'dispatch')) {
394
+ const d = value.run && typeof value.run === 'object' ? value.run : null;
395
+ const runKey = d ? runKeyOf(d.runKey) : null;
396
+ if (!runKey) continue;
397
+ const [, keyRun, keyLineage] = runKey.split('/');
398
+ if ((d.run != null && d.run !== keyRun) || (d.lineage != null && d.lineage !== keyLineage)) continue; // not tied: no closer, no join
399
+ const slug = str(d.slug) ?? stemOf(file);
400
+ const startMs = msOf(iso(d.createdAt));
401
+ const last = d.state === 'completed' ? iso(d.completedAt) : d.state === 'cancelled' ? iso(d.endedAt) : null;
402
+ activities.push({ runKey, traceId: null, slug, startMs, ended: last !== null, last });
403
+ const tasks = new Set(objects(value.dispatches).map(([, x]) => str(x.task)).filter((x) => x !== null));
404
+ dispatchRuns.push({ id: str(d.dispatchRunId), slug, runKey, run: keyRun, lineage: keyLineage, startMs,
405
+ endMs: msOf(iso(d.completedAt) ?? iso(d.endedAt)), tasks });
406
+ }
407
+ closeEnded(segments, activities);
408
+
409
+ const runsDir = path.join(stateDir, 'runs');
410
+ for (const { locator, value } of readLines(fsImpl, runsDir, 'runs', gaps, 'ledger')) {
411
+ const rec = normalizeRunRecord(value);
412
+ if (!rec) { gaps.push({ locator, reason: 'unparseable', kind: 'ledger' }); continue; }
413
+ const start = iso(rec.timestamp);
414
+ items.push(identityItem('ledger', rec.verb, locator, value, {
415
+ start, durationMs: rec.durationMs ?? null, attrs: pick(rec, LEDGER_ATTRS), session: rec.session,
416
+ }));
417
+ }
418
+ const kinds = telemetryKinds();
419
+ for (const { locator, value } of readLines(fsImpl, path.join(runsDir, 'telemetry'), 'runs/telemetry', gaps, 'telemetry')) {
420
+ const kind = value && typeof value === 'object' && !Array.isArray(value) ? value.kind : null;
421
+ if (typeof kind !== 'string' || !Object.hasOwn(kinds, kind)) { gaps.push({ locator, reason: 'unparseable', kind: 'telemetry' }); continue; }
422
+ const fields = Object.keys(kinds[kind].fields || {});
423
+ const isObserver = value.policy === 'post-shell-observe';
424
+ const durationMs = isObserver
425
+ ? (value.tool_duration_ms ?? value.duration_ms ?? null)
426
+ : (value.duration_ms ?? null);
427
+ items.push(identityItem(kind, value.policy ?? value.tier ?? kind, locator, value, {
428
+ start: iso(value.timestamp), durationMs, attrs: pick(value, same(fields)), session: value.session,
429
+ link: { verifyId: HEX16.test(value.verify_id) ? value.verify_id : null, event: str(value.event), toolCall: str(value.tool_call_id) },
430
+ }));
431
+ }
432
+
433
+ return assemble({ items, gaps, run, ctx: { segments, aliases, attemptOf, workers, dispatchRuns } });
434
+ }
435
+
436
+ /**
437
+ * A ledger or telemetry line: placed by `(run, lineage)` when it carries both. The scope ids it
438
+ * carries (110: hook `worker_run`, `role`, `session`; verify.run `dispatch_run`, `source`, `task`)
439
+ * are kept for placement only, read defensively: absent is null.
440
+ */
441
+ function identityItem(kind , name , locator , raw , opts ) {
442
+ const run = str(raw.run);
443
+ const lineage = str(raw.lineage);
444
+ const attempt = raw.attempt === null || raw.attempt === undefined ? null : String(raw.attempt); // telemetry uint, ledger string
445
+ const workerRun = typeof raw.worker_run === 'string' && WORKER_RUN_ID.test(raw.worker_run) ? raw.worker_run : null;
446
+ return item(kind, name, locator, {
447
+ ...opts,
448
+ place: run && lineage ? { type: 'identity', run, lineage, at: msOf(opts.start) } : { type: 'none' },
449
+ link: { ...(opts.link || {}), attempt,
450
+ scope: { workerRun, dispatchRun: str(raw.dispatch_run), role: str(raw.role), session: str(raw.session), source: str(raw.source), task: str(raw.task) } },
451
+ });
452
+ }
453
+
454
+ /** The segments that may hold `it`: one is a join, none `unmatched`, more `ambiguous`. */
455
+ function candidates(it , ctx ) {
456
+ const { segments, aliases, attemptOf } = ctx;
457
+ const p = it.place;
458
+ const coarse = typeof it.start === 'string' && WHOLE_SECOND.test(it.start);
459
+ const found = new Set ();
460
+ const byAttempt = (a ) => {
461
+ for (const seg of segments) if (seg.file === a.file && seg.runKey === a.runKey && seg.epoch === a.epoch) found.add(seg);
462
+ };
463
+ if (p.type === 'identity') {
464
+ const tiered = tiers(it, ctx, coarse);
465
+ if (tiered) return tiered;
466
+ for (const seg of segments) if (seg.run === p.run && seg.lineage === p.lineage && holds(seg, p.at, coarse)) found.add(seg);
467
+ for (const alias of aliases) {
468
+ if (alias.run !== p.run || alias.lineage !== p.lineage) continue;
469
+ for (const seg of segments) {
470
+ if (seg.runKey === alias.runKey && holds(seg, p.at, coarse)) { found.add(seg); it.link.viaWorker = alias.worker; }
471
+ }
472
+ }
473
+ if (found.size > 1) return activity(it, ctx, [...found]);
474
+ } else if (p.type === 'worker') {
475
+ // F1: (slug, task, token) -> that slug's attempt -> its segment; else run key + window, same slug only.
476
+ const a = p.task !== null && p.token !== null ? attemptOf.get(`${p.slug}\n${p.task}\n${p.token}`) : undefined;
477
+ if (a) byAttempt(a);
478
+ if (!found.size) for (const seg of segments) if (seg.slug === p.slug && seg.runKey === p.runKey && holds(seg, p.at, coarse)) found.add(seg);
479
+ } else if (p.type === 'runKey') {
480
+ for (const seg of segments) if (seg.runKey === p.runKey && holds(seg, p.at, coarse)) found.add(seg);
481
+ } else if (p.type === 'attempt') {
482
+ byAttempt(p);
483
+ }
484
+ return [...found];
485
+ }
486
+
487
+ /** Where a worker run is placed, computed once: hook lines that name or match it follow it. */
488
+ function placeOf(worker , ctx ) {
489
+ if (!ctx.placed.has(worker)) ctx.placed.set(worker, worker.place.type === 'none' ? [] : candidates(worker, ctx));
490
+ return ctx.placed.get(worker);
491
+ }
492
+
493
+ /** The segments of dispatch run `d` (its slug's, 110 F2) that hold `t`. */
494
+ const dispatchSegments = (d , t , coarse , ctx ) => owned(ctx.segments, d.runKey, { slug: d.slug }).filter((seg) => holds(seg, t, coarse));
495
+
496
+ /**
497
+ * T1 (110 F3): an id the line carries beats any window. A hook's `worker_run` follows that worker to
498
+ * its segment and parents to it; a verify.run's `dispatch_run` goes to the segment of that dispatch
499
+ * run's slug that holds the line. An id that names no record, or a record with no placement, leaves
500
+ * the line unmatched/ambiguous; it never joins another run.
501
+ */
502
+ function tiers(it , ctx , coarse ) {
503
+ const s = it.link.scope;
504
+ if (it.kind === 'hook' && s.workerRun) {
505
+ const named = ctx.workers.filter((w ) => w.runId === s.workerRun);
506
+ if (named.length === 1) it.link.viaWorker = named[0].item;
507
+ if (named.length) return [...new Set(named.flatMap((w ) => placeOf(w.item, ctx)))];
508
+ }
509
+ if (it.kind === 'verify.run' && s.dispatchRun) {
510
+ const named = ctx.dispatchRuns.filter((d ) => d.id === s.dispatchRun);
511
+ if (named.length) return [...new Set(named.flatMap((d ) => dispatchSegments(d, it.place.at, coarse, ctx)))];
512
+ }
513
+ return null;
514
+ }
515
+
516
+ /**
517
+ * T2 (110 F3), for a line T3 finds in more than one segment: it joins the one candidate of the
518
+ * single activity it fits, labelled `join: activity` because the fit is inferred. A hook line with
519
+ * an attempt (role worker or absent) fits the one worker whose identity and lifetime match it, and
520
+ * only when every hook line of its session resolves to that same worker. A `source: dispatch`
521
+ * verify.run fits the one dispatch run of its identity whose window holds it and, when it names a
522
+ * task, has that task or slug. Anything else stays with its candidates (`ambiguous`).
523
+ */
524
+ function activity(it , ctx , found ) {
525
+ const s = it.link.scope;
526
+ const coarse = typeof it.start === 'string' && WHOLE_SECOND.test(it.start);
527
+ let segs = [];
528
+ let via = null;
529
+ if (it.kind === 'hook') {
530
+ const w = s.resolved;
531
+ if (!w || (s.session !== null && ctx.sessions.get(s.session) !== w)) return found;
532
+ segs = placeOf(w.item, ctx);
533
+ via = w.item;
534
+ } else if (it.kind === 'verify.run' && s.source === 'dispatch') {
535
+ const p = it.place;
536
+ const fits = ctx.dispatchRuns.filter((d ) => d.run === p.run && d.lineage === p.lineage && p.at !== null && d.startMs !== null
537
+ && d.startMs <= p.at && (d.endMs === null || p.at <= d.endMs) && (s.task === null || d.tasks.has(s.task) || d.slug === s.task)
538
+ && (s.dispatchRun === null || d.id === s.dispatchRun));
539
+ if (fits.length !== 1) return found;
540
+ segs = dispatchSegments(fits[0], p.at, coarse, ctx);
541
+ } else return found;
542
+ const pick = segs.filter((seg) => found.includes(seg));
543
+ if (pick.length !== 1) return found;
544
+ if (via) it.link.viaWorker = via;
545
+ it.attrs = { ...it.attrs, join: 'activity' };
546
+ return pick;
547
+ }
548
+
549
+ /**
550
+ * The one worker each hook line resolves to (by its `worker_run`, else as the single worker whose
551
+ * identity `{run, lineage, attempt}` and lifetime `[start, end.at]` hold it), and per session the
552
+ * worker all its hook lines share, or null when any line resolves to none or to another worker.
553
+ */
554
+ function resolveSessions(items , ctx ) {
555
+ ctx.sessions = new Map ();
556
+ for (const it of items) {
557
+ if (it.kind !== 'hook' || !it.link.scope) continue;
558
+ const s = it.link.scope;
559
+ const p = it.place;
560
+ const t = msOf(it.start);
561
+ let hits = [];
562
+ if (s.workerRun) {
563
+ hits = ctx.workers.filter((w ) => w.runId === s.workerRun);
564
+ } else if (p.type === 'identity' && it.link.attempt !== null && (s.role === null || s.role === 'worker') && t !== null) {
565
+ hits = ctx.workers.filter((w ) => w.run === p.run && w.lineage === p.lineage && w.attempt === it.link.attempt
566
+ && w.startMs !== null && w.startMs <= t && (w.endMs === null || t <= w.endMs));
567
+ }
568
+ s.resolved = hits.length === 1 ? hits[0] : null;
569
+ if (s.session === null) continue;
570
+ const prior = ctx.sessions.has(s.session) ? ctx.sessions.get(s.session) : s.resolved;
571
+ ctx.sessions.set(s.session, prior === s.resolved ? prior : null);
572
+ }
573
+ }
574
+
575
+ function groupOf(it ) {
576
+ if (it.session) return `session:${it.session}`;
577
+ return `day:${it.start ? it.start.slice(0, 10) : 'undated'}`;
578
+ }
579
+
580
+ /** Placement, ids, parents and the sorted output. */
581
+ function assemble({ items, gaps, run, ctx } ) {
582
+ const { segments } = ctx;
583
+ ctx.placed = new Map ();
584
+ resolveSessions(items, ctx);
585
+ const traces = new Map ();
586
+ for (const seg of segments) if (seg.traceId) traces.set(seg, []);
587
+ const ungrouped = new Map ();
588
+ const loose = (it ) => {
589
+ const g = groupOf(it);
590
+ if (!ungrouped.has(g)) ungrouped.set(g, []);
591
+ ungrouped.get(g) .push(it);
592
+ };
593
+ const identityGaps = [];
594
+ for (const it of items) {
595
+ let target = null;
596
+ if (it.place.type === 'segment') target = it.place.seg;
597
+ else if (it.place.type !== 'none') {
598
+ const found = it.kind === 'worker' ? placeOf(it, ctx) : candidates(it, ctx);
599
+ if (found.length !== 1) {
600
+ const p = it.place;
601
+ const [, keyRun, keyLineage] = p.runKey ? p.runKey.split('/') : [];
602
+ identityGaps.push({ locator: it.locator, reason: found.length ? 'ambiguous' : 'unmatched', kind: it.kind,
603
+ runKey: p.runKey ?? null, run: p.run ?? keyRun ?? null, lineage: p.lineage ?? keyLineage ?? null });
604
+ continue;
605
+ }
606
+ target = found[0];
607
+ }
608
+ if (target && target.traceId) traces.get(target) .push(it);
609
+ else loose(it);
610
+ }
611
+
612
+ const out = (list , base , root ) => {
613
+ for (const it of list) it.spanId = sha16(base + it.locator);
614
+ link(list, root);
615
+ return list.sort(byStart).map((it) => ({
616
+ spanId: it.spanId, parentSpanId: it.parentSpanId, kind: it.kind, name: it.name, start: it.start, end: it.end,
617
+ durationMs: it.durationMs, basis: it.basis, locator: it.locator, attrs: it.attrs,
618
+ }));
619
+ };
620
+ let traceList = [...traces.entries()].map(([seg, list]) => {
621
+ const root = list.find((it) => it.kind === 'run' && it.locator === `${seg.file}#${seg.runKey}/${seg.index}`) || null;
622
+ return { traceId: seg.traceId, traceLink: seg.traceLink, runKey: seg.runKey, epoch: seg.epoch, window: { from: seg.from, to: seg.to },
623
+ spans: out(list, seg.traceId, root), fromMs: seg.fromMs };
624
+ }).sort((a, b) => a.fromMs - b.fromMs || cmp(a.traceId, b.traceId) || cmp(a.runKey, b.runKey))
625
+ .map(({ fromMs, ...trace }) => trace);
626
+ let groupList = [...ungrouped.entries()].sort(([a], [b]) => cmp(a, b))
627
+ .map(([group, list]) => ({ group, label: 'ungrouped', spans: out(list, group, null) }));
628
+ let gapList = [...gaps.map((g ) => ({ ...g, runKey: null, run: null, lineage: null })), ...identityGaps];
629
+
630
+ if (run !== null && run !== undefined) {
631
+ const byKey = traceList.some((tr) => tr.runKey === run);
632
+ const identity = byKey ? run.split('/').slice(1).join('/') : null;
633
+ traceList = traceList.filter((tr) => (byKey ? tr.runKey === run : tr.runKey.split('/')[1] === run));
634
+ groupList = [];
635
+ gapList = gapList.filter((g) => g.reason === 'unparseable'
636
+ || (byKey ? g.runKey === run || `${g.run}/${g.lineage}` === identity : g.run === run));
637
+ }
638
+ gapList = gapList.map(({ locator, reason, kind }) => ({ locator, reason, kind }))
639
+ .sort((a, b) => cmp(a.locator, b.locator) || cmp(a.reason, b.reason));
640
+ return { v: 1, traces: traceList, ungrouped: groupList, gaps: gapList };
641
+ }
642
+
643
+ const cmp = (a , b ) => (a < b ? -1 : a > b ? 1 : 0);
644
+ function byStart(a , b ) {
645
+ const x = a.start === null ? Infinity : msOf(a.start) ;
646
+ const y = b.start === null ? Infinity : msOf(b.start) ;
647
+ return x - y || cmp(a.spanId, b.spanId);
648
+ }
649
+
650
+ /** Parents inside one trace or group, and pre/post hook pairs folded into `test` spans (IC-005). */
651
+ function link(list , root ) {
652
+ const verifyRuns = new Map ();
653
+ for (const it of list) if (it.kind === 'verify.run' && it.link.verifyId) verifyRuns.set(it.link.verifyId, it);
654
+ const attempts = list.filter((it) => it.kind === 'attempt');
655
+ const within = (a , it ) => {
656
+ const t = msOf(it.start);
657
+ return t !== null && a.start !== null && (msOf(a.start) ) <= t && (a.end === null || t <= (msOf(a.end) ));
658
+ };
659
+ // Pairing first, by time, so a post line meets the latest pre line before it.
660
+ const pre = new Map ();
661
+ for (const it of [...list].sort(byStart)) {
662
+ if (it.kind !== 'hook' || !it.session || !it.link.toolCall) continue;
663
+ const key = `${it.session}\n${it.link.toolCall}`;
664
+ if (PRE_EVENTS.has(it.link.event)) pre.set(key, it);
665
+ else if (POST_EVENTS.has(it.link.event) && pre.has(key)) {
666
+ const opener = pre.get(key);
667
+ pre.delete(key);
668
+ const end = it.start;
669
+ it.kind = 'test';
670
+ it.name = 'test';
671
+ it.start = opener.start;
672
+ it.end = end;
673
+ it.durationMs = Math.max(0, (msOf(end) ) - (msOf(opener.start) ));
674
+ it.basis = 'derived';
675
+ it.attrs = { ...it.attrs, status: it.link.event === 'PostToolUseFailure' ? 'fail' : 'pass' };
676
+ }
677
+ }
678
+ for (const it of list) {
679
+ let parent = null;
680
+ if ((it.kind === 'verify.tier' || it.kind === 'verify.check') && verifyRuns.has(it.link.verifyId)) parent = verifyRuns.get(it.link.verifyId);
681
+ else if (it.link.viaWorker && list.includes(it.link.viaWorker)) parent = it.link.viaWorker;
682
+ else if (it.link.attempt !== undefined && it.link.attempt !== null) {
683
+ const hits = attempts.filter((a) => a.link.attemptOf.token === it.link.attempt && within(a, it));
684
+ if (hits.length === 1) parent = hits[0];
685
+ } else if (it.link.workerOf) {
686
+ const hits = attempts.filter((a) => a.link.attemptOf.task === it.link.workerOf.task && a.link.attemptOf.token === it.link.workerOf.token);
687
+ if (hits.length === 1) parent = hits[0];
688
+ }
689
+ if (!parent && root && it !== root) parent = root;
690
+ it.parentSpanId = parent && parent !== it ? parent.spanId : null;
691
+ }
692
+ }
693
+
694
+ // ── the verb ────────────────────────────────────────────────────────────────────────────────────
695
+
696
+ /**
697
+ * The spans view for a project, resolving `<config>` the way the ledger views do.
698
+ * @param {Object} options
699
+ * @param {string} [options.projectRoot] where to walk up from for `.doflow` (default: cwd)
700
+ * @param {string|null} [options.run]
701
+ * @param {boolean} [options.global=false]
702
+ * @param {Object} [options.env=process.env]
703
+ * @param {Object} [options.fsImpl=fs]
704
+ * @param {string} [options.homeDir]
705
+ */
706
+ function projectSpans({ projectRoot, run = null, global = false, env = process.env, fsImpl = nodeFs, homeDir } = {}) {
707
+ const location = resolveRunsLocation({ start: projectRoot, global, env, fsImpl, ...(homeDir ? { homeDir } : {}) });
708
+ return buildSpans({ stateDir: path.dirname(location.dir), run, fsImpl });
709
+ }
710
+
711
+ /** Object keys sorted at every level, so the JSON form is the same bytes for the same stores. */
712
+ function sorted(value ) {
713
+ if (Array.isArray(value)) return value.map(sorted);
714
+ if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort().map((k) => [k, sorted(value[k])]));
715
+ return value;
716
+ }
717
+
718
+ /**
719
+ * `doflow trace spans [--run <runKey|run>] [--json] [<project>]`. Exit 0 answered, 2 usage.
720
+ * @param {Object} options
721
+ * @param {string[]} [options.positional] the arguments after `spans`: at most the project root
722
+ */
723
+ function handleTraceSpans({ positional = [], run, json = false, global = false, env = process.env, fsImpl = nodeFs } = {}) {
724
+ const usage = (detail ) => {
725
+ if (json) console.log(JSON.stringify({ error: 'usage', detail }));
726
+ else console.error(`doflow trace spans: ${detail}`);
727
+ return finish(2);
728
+ };
729
+ if (positional.length > 1) return usage('usage: trace spans [--run <runKey|run>] [--json] [<project>]');
730
+ if (run !== undefined && run !== null && (typeof run !== 'string' || !RUN_ARG.test(run))) return usage('--run expects a run key (<hash>/<run>/<lineage>) or a run id');
731
+ const view = projectSpans({ projectRoot: global ? undefined : path.resolve(positional[0] || '.'), run: run ?? null, global, env, fsImpl });
732
+ if (json) {
733
+ console.log(JSON.stringify(sorted(view), null, 2));
734
+ return finish(0);
735
+ }
736
+ for (const tr of view.traces) {
737
+ console.log(`trace ${tr.traceId} ${tr.runKey} epoch ${tr.epoch} ${tr.window.from} .. ${tr.window.to ?? 'open'} spans ${tr.spans.length}${tr.traceLink ? ` link ${tr.traceLink}` : ''}`);
738
+ }
739
+ console.log(`ungrouped: ${view.ungrouped.length} group(s), ${view.ungrouped.reduce((n , g ) => n + g.spans.length, 0)} span(s)`);
740
+ const count = (reason ) => view.gaps.filter((g ) => g.reason === reason).length;
741
+ console.log(`gaps: ${view.gaps.length} (unmatched ${count('unmatched')}, ambiguous ${count('ambiguous')}, unparseable ${count('unparseable')})`);
742
+ return finish(0);
743
+ }
744
+
745
+ module.exports = { buildSpans, projectSpans, handleTraceSpans };