@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,178 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `fleet --action projects` (093 IC-019): the projects this machine knows, from two read-only
5
+ * stores: the user-state `upkeep-roots` index and the `cwd` of each hook project directory
6
+ * (`<config home>/doflow/session-env/projects/<hash>/meta.json`). A candidate is kept when it is an
7
+ * existing directory (not a symlink) whose git work-tree top holds `.doflow/` and is not under a
8
+ * temp folder; candidates that share a git common dir are one project. `root` (the main checkout)
9
+ * is for the Console server only. Nothing is written: no lock, no mkdir, no upkeep.
10
+ * @returns {{ok: false, error: 'cannot-list-projects'} | {ok: true, body: Object}}
11
+ */
12
+
13
+ const fs = require('node:fs');
14
+ const os = require('node:os');
15
+ const path = require('node:path');
16
+ const { spawnSync } = require('node:child_process');
17
+ const { cwdHash, stateDir } = require('../context/identity.js');
18
+ const { listCheckouts } = require('../checkouts.js');
19
+ const { resolveRoots } = require('../storage/roots.js');
20
+ const { loadStorageRegistry } = require('../storage/registry.js');
21
+ const { REPO_ROOT } = require('../../helper/repo-root.js');
22
+
23
+ const HOOK_DIR_LIMIT = 2000;
24
+ const PROBE_LIMIT = 500; // git probes per call: each is one process, and the Console polls this
25
+ const META_MAX_BYTES = 64 * 1024;
26
+ const LABEL = /^[A-Za-z0-9 ._:/@-]{1,64}$/;
27
+ const FIXED_TEMP_ROOTS = ['/private/tmp', '/private/var/folders'];
28
+
29
+
30
+
31
+ function realOrNull(p , fsImpl ) {
32
+ try { return fsImpl.realpathSync(p); } catch { return null; }
33
+ }
34
+
35
+ function under(p , root ) {
36
+ return p === root || p.startsWith(root.endsWith(path.sep) ? root : root + path.sep);
37
+ }
38
+
39
+ /** The label rule of IC-004: at most 64 characters of `[A-Za-z0-9 ._:/@-]`, else `(withheld)`. */
40
+ function label(name ) {
41
+ return LABEL.test(name) ? name : '(withheld)';
42
+ }
43
+
44
+ function iso(ms ) {
45
+ return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
46
+ }
47
+
48
+ function lstatOrNull(p , fsImpl ) {
49
+ try { return fsImpl.lstatSync(p); } catch { return null; }
50
+ }
51
+
52
+ function listProjects({
53
+ env = process.env, cwd = process.cwd(), exec = spawnSync, fsImpl = fs, tempRoots,
54
+ } = {}) {
55
+ const excluded = { missing: 0, temp: 0, notDoflow: 0, unreadable: 0 };
56
+ const body = { v: 1, store: 'absent', projects: [], excluded, truncated: false };
57
+
58
+ const userState = resolveRoots({ registry: loadStorageRegistry({ repoRoot: REPO_ROOT }), projectRoot: null, global: true, env })['user-state'];
59
+ const hooks = stateDir(env);
60
+ if (userState === null && hooks === null) return { ok: true, body };
61
+ body.store = 'present';
62
+
63
+ // Candidates keyed by the path as written; each keeps its sources and the newest last_active seen.
64
+ const candidates = new Map ();
65
+ const add = (dir , source , activeMs = -Infinity) => {
66
+ const c = candidates.get(dir) ?? { sources: new Set (), activeMs: -Infinity };
67
+ c.sources.add(source);
68
+ c.activeMs = Math.max(c.activeMs, activeMs);
69
+ candidates.set(dir, c);
70
+ };
71
+
72
+ if (userState !== null) {
73
+ const file = path.join(userState, 'upkeep-roots');
74
+ const info = lstatOrNull(file, fsImpl);
75
+ if (info && info.isFile()) {
76
+ let text = '';
77
+ try { text = fsImpl.readFileSync(file, 'utf8'); } catch { excluded.unreadable += 1; }
78
+ for (const line of text.split('\n')) {
79
+ if (line === '') continue;
80
+ if (path.isAbsolute(line)) add(line, 'upkeep-roots'); else excluded.unreadable += 1;
81
+ }
82
+ }
83
+ }
84
+
85
+ if (hooks !== null) {
86
+ const base = path.join(hooks, 'projects');
87
+ let dirs = [];
88
+ try {
89
+ for (const dirent of fsImpl.readdirSync(base, { withFileTypes: true })) {
90
+ if (!dirent.isDirectory()) continue;
91
+ const st = lstatOrNull(path.join(base, dirent.name), fsImpl);
92
+ if (st) dirs.push({ name: dirent.name, mtimeMs: st.mtimeMs });
93
+ }
94
+ } catch { /* no hook project directories */ }
95
+ dirs.sort((a, b) => b.mtimeMs - a.mtimeMs || (a.name < b.name ? -1 : 1));
96
+ if (dirs.length > HOOK_DIR_LIMIT) { body.truncated = true; dirs = dirs.slice(0, HOOK_DIR_LIMIT); }
97
+ for (const { name } of dirs) {
98
+ const file = path.join(base, name, 'meta.json');
99
+ const info = lstatOrNull(file, fsImpl);
100
+ if (!info) continue; // a directory with no meta.json is not a record
101
+ let meta ;
102
+ try {
103
+ if (!info.isFile() || info.size > META_MAX_BYTES) throw new Error('unreadable');
104
+ meta = JSON.parse(fsImpl.readFileSync(file, 'utf8'));
105
+ } catch { excluded.unreadable += 1; continue; }
106
+ if (meta === null || typeof meta !== 'object' || typeof meta.cwd !== 'string' || !path.isAbsolute(meta.cwd)) { excluded.unreadable += 1; continue; }
107
+ add(meta.cwd, 'hook-dirs', typeof meta.last_active === 'string' ? Date.parse(meta.last_active) : -Infinity);
108
+ }
109
+ }
110
+
111
+ const temp = [...(tempRoots ?? [realOrNull(os.tmpdir(), fsImpl) ?? os.tmpdir(), ...FIXED_TEMP_ROOTS])];
112
+ const isTemp = (p ) => temp.some((root) => under(p, root));
113
+
114
+ /** `git rev-parse` for one directory: its work-tree top and common git dir, or itself when outside git. */
115
+ const probe = (dir ) => {
116
+ let res ;
117
+ try {
118
+ res = exec('git', ['-C', dir, 'rev-parse', '--path-format=absolute', '--show-toplevel', '--git-common-dir'], { encoding: 'utf8', timeout: 10000 });
119
+ } catch (error ) { res = { error }; }
120
+ if (res && res.error && res.error.code === 'ENOENT') return 'git-missing';
121
+ const lines = res && !res.error && res.status === 0 ? String(res.stdout || '').split('\n') : [];
122
+ const top = lines[0] ? realOrNull(lines[0], fsImpl) : null;
123
+ const common = lines[1] ? realOrNull(lines[1], fsImpl) : null;
124
+ if (top !== null && common !== null) return { top, common };
125
+ const self = realOrNull(dir, fsImpl) ?? dir;
126
+ return { top: self, common: self };
127
+ };
128
+
129
+
130
+ const groups = new Map ();
131
+ let probes = 0;
132
+ for (const [dir, candidate] of candidates) {
133
+ const info = lstatOrNull(dir, fsImpl);
134
+ if (!info || info.isSymbolicLink() || !info.isDirectory()) { excluded.missing += 1; continue; }
135
+ const real = realOrNull(dir, fsImpl);
136
+ if (real === null) { excluded.missing += 1; continue; }
137
+ if (isTemp(real)) { excluded.temp += 1; continue; }
138
+ if (probes >= PROBE_LIMIT) { body.truncated = true; continue; }
139
+ probes += 1;
140
+ const found = probe(dir);
141
+ if (found === 'git-missing') return { ok: false, error: 'cannot-list-projects' };
142
+ const doflow = lstatOrNull(path.join(found.top, '.doflow'), fsImpl);
143
+ if (!doflow || !doflow.isDirectory()) { excluded.notDoflow += 1; continue; }
144
+ if (isTemp(found.top)) { excluded.temp += 1; continue; }
145
+ const due = lstatOrNull(path.join(found.top, '.doflow', 'state', 'upkeep-due'), fsImpl);
146
+ const g = groups.get(found.common) ?? { common: found.common, tops: new Set (), sources: new Set (), lastMs: -Infinity };
147
+ g.tops.add(found.top);
148
+ for (const s of candidate.sources) g.sources.add(s);
149
+ g.lastMs = Math.max(g.lastMs, candidate.activeMs, due && due.isFile() ? due.mtimeMs : -Infinity);
150
+ groups.set(found.common, g);
151
+ }
152
+
153
+ const here = groups.size > 0 ? probe(cwd) : null;
154
+ if (here === 'git-missing') return { ok: false, error: 'cannot-list-projects' };
155
+
156
+ const projects = [...groups.values()].map((g) => {
157
+ const first = [...g.tops].sort()[0];
158
+ const listed = listCheckouts({ cwd: first, exec, fsImpl });
159
+ const real = (p ) => (typeof p === 'string' ? realOrNull(p, fsImpl) : null);
160
+ const main = (listed.ok && real(listed.main)) || first;
161
+ const count = listed.ok ? listed.entries.filter((e ) => !e.sandbox).length : g.tops.size;
162
+ return {
163
+ projectKey: cwdHash(main),
164
+ root: main,
165
+ label: label(path.basename(main)),
166
+ current: here !== null && g.common === here.common,
167
+ checkouts: Math.max(count, 1),
168
+ sources: ['upkeep-roots', 'hook-dirs'].filter((s) => g.sources.has(s)),
169
+ lastRecordAt: iso(g.lastMs),
170
+ };
171
+ });
172
+ const time = (p ) => (p.lastRecordAt === null ? -Infinity : Date.parse(p.lastRecordAt));
173
+ projects.sort((a, b) => Number(b.current) - Number(a.current) || time(b) - time(a) || (a.projectKey < b.projectKey ? -1 : 1));
174
+ body.projects = projects;
175
+ return { ok: true, body };
176
+ }
177
+
178
+ module.exports = { listProjects };
@@ -0,0 +1,383 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `fleet --action tree` (093 IC-020): one project's agent hierarchy as nodes and edges, built from
5
+ * records that already exist and written to nowhere. Sources: IC-018 a sessions (hook observations
6
+ * joined to the session directory), the harnesses' own sub-agent records read through V.4's readers
7
+ * (`header`, and the metadata mode for the last record time), task attempts by run/lineage, worker runs
8
+ * (caller, `childOf`, binding, end, a bounded process check) and the dispatch `primary` snapshot.
9
+ * Every edge names the record it comes from; two nodes merge only on a shared key (a runKey, a run id,
10
+ * an agent key), never on a harness or a model. A label is the record's own role text, never invented.
11
+ * A transcript is joined by session id alone (gate-a Q4 A); nothing is written, no path, pid or raw
12
+ * session id is printed. `internal` (not part of the body) carries what V.6 needs per node: the file to open, and
13
+ * `records` (DoFlow's own records of the agent: type, time, task, result) and `holds` (the tasks it is working on).
14
+ * @returns {{ok: false, error: string} | {ok: true, body: Object, internal: Map<string, Object>}}
15
+ */
16
+
17
+ const crypto = require('node:crypto');
18
+ const fs = require('node:fs');
19
+ const os = require('node:os');
20
+ const path = require('node:path');
21
+ const { spawnSync } = require('node:child_process');
22
+ const { cwdHash, stateDir } = require('../context/identity.js');
23
+ const { observedSessions } = require('../context/primary.js');
24
+ const { listCheckouts } = require('../checkouts.js');
25
+ const { readTaskState } = require('../task-state.js');
26
+ const { readRuns } = require('../worker/run-store.js');
27
+ const processTree = require('../worker/process-tree.js');
28
+ const { loadTranscriptRegistry } = require('../transcripts/registry.js');
29
+ const { readJsonFile, label } = require('../transcripts/cursor.js');
30
+ const { isSafeSlug } = require('../task-scope.js');
31
+
32
+ const MAX_SESSIONS = 64;
33
+ const MAX_NODES = 256;
34
+ const MAX_CHECKS = 16;
35
+ const SCAN_FILES = 200;
36
+ const LIST_CAP = 2000;
37
+ const SUBAGENT_FILES = 64;
38
+ const NEEDLE_BYTES = 2 * 1024 * 1024;
39
+ const SESSION_DIR = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
40
+ const RUN_KEY = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
41
+
42
+ const sha16 = (text ) => crypto.createHash('sha256').update(text).digest('hex').slice(0, 16);
43
+ const READERS = {
44
+ 'claude-jsonl': () => require('../transcripts/readers/claude-jsonl.js'),
45
+ 'codex-rollout': () => require('../transcripts/readers/codex-rollout.js'),
46
+ 'kiro-jsonl': () => require('../transcripts/readers/kiro-jsonl.js'),
47
+ 'copilot-events': () => require('../transcripts/readers/copilot-events.js'),
48
+ 'pi-jsonl': () => require('../transcripts/readers/pi-jsonl.js'),
49
+ };
50
+ /** How each harness encodes a cwd into its transcript directory name (research section 3). */
51
+ const ENCODE = {
52
+ claude: (cwd) => cwd.replace(/[^A-Za-z0-9]/g, '-'),
53
+ pi: (cwd) => cwd.replace(/^[\\/]/, '').replace(/[\\/:]/g, '-'),
54
+ };
55
+
56
+ function regularFile(file , fsImpl ) {
57
+ try { return fsImpl.lstatSync(file).isFile(); } catch { return false; }
58
+ }
59
+
60
+ /** Files under `root` matching a glob whose only wildcard is `*` inside one path segment. */
61
+ function expandGlob(root , pattern , fsImpl ) {
62
+ let found = [root];
63
+ for (const seg of pattern.split('/')) {
64
+ const next = [];
65
+ if (!seg.includes('*')) for (const dir of found) next.push(path.join(dir, seg));
66
+ else {
67
+ const re = new RegExp(`^${seg.split('*').map((part) => part.replace(/[.+?^${}()|[\]\\]/g, '\\$&')).join('[^/]*')}$`);
68
+ for (const dir of found) {
69
+ let names = [];
70
+ try { names = fsImpl.readdirSync(dir); } catch { /* not a directory */ }
71
+ for (const name of names.slice(0, LIST_CAP)) if (re.test(name)) next.push(path.join(dir, name));
72
+ }
73
+ }
74
+ found = next.slice(0, LIST_CAP);
75
+ }
76
+ return found.filter((file) => regularFile(file, fsImpl)).sort();
77
+ }
78
+
79
+ /** The registry locations of `entry` for this platform, as `[root, glob]` pairs with the base resolved. */
80
+ function locations(entry , env , home , platform ) {
81
+ const out = [];
82
+ for (const loc of entry.locations) {
83
+ if (loc.os !== platform || loc.status === 'unknown') continue;
84
+ let base = null;
85
+ if (loc.base === 'home') base = home;
86
+ else if (loc.base === 'xdg-data') base = env.XDG_DATA_HOME || path.join(home, '.local', 'share');
87
+ else if (loc.base.startsWith('env:')) base = env[loc.base.slice(4)] || null;
88
+ if (base) out.push([path.join(base, ...loc.segments), loc.glob]);
89
+ }
90
+ return out;
91
+ }
92
+
93
+ /** Does `file` hold `needle` in its first 2 MiB? Only the yes or no leaves this function. */
94
+ function fileHas(file , needle , fsImpl ) {
95
+ try {
96
+ const fd = fsImpl.openSync(file, 'r');
97
+ try {
98
+ const buf = Buffer.alloc(Math.min(NEEDLE_BYTES, fsImpl.fstatSync(fd).size));
99
+ fsImpl.readSync(fd, buf, 0, buf.length, 0);
100
+ return buf.toString('latin1').includes(needle);
101
+ } finally { fsImpl.closeSync(fd); }
102
+ } catch { return false; }
103
+ }
104
+
105
+ function buildTree({
106
+ env = process.env, cwd = process.cwd(), exec = spawnSync, fsImpl = fs, platform = process.platform, home, alive = processTree.alive, now = Date.now,
107
+ } = {}) {
108
+ const observed = observedSessions({ env, cwd, exec, fsImpl, now });
109
+ if (!observed.ok) return { ok: false, error: observed.error };
110
+ const listed = listCheckouts({ cwd, exec, fsImpl });
111
+ const checkouts = listed.ok ? listed.entries.filter((e ) => !e.sandbox).map((e ) => e.path) : [];
112
+ const projectKey = cwdHash(listed.ok ? listed.main : cwd);
113
+ const homeDir = home ?? env.HOME ?? os.homedir();
114
+ const loaded = loadTranscriptRegistry();
115
+ const registry = {};
116
+ for (const entry of loaded.ok ? loaded.registry.entries : []) registry[entry.harness] = entry;
117
+
118
+ const nodes = new Map ();
119
+ const edges = new Map ();
120
+ const internal = new Map ();
121
+ const counts = { unreadable: 0, notChecked: 0 };
122
+ let checks = 0;
123
+
124
+ /** The node for `agentKey`, created once: a second source with the same key adds what the first lacked. */
125
+ const node = (agentKey , fields , intern ) => {
126
+ let n = nodes.get(agentKey);
127
+ if (!n) {
128
+ if (nodes.size >= MAX_NODES) { counts.notChecked += 1; return null; }
129
+ n = {
130
+ agentKey, kind: null, harness: null, model: null, modelBasis: null, label: null, depth: 0, worktreeKey: projectKey,
131
+ startedAt: null, lastRecordAt: null, liveness: 'not-checked', ended: null, transcript: { reader: null, linked: false, reason: 'no-reader' },
132
+ ...fields,
133
+ };
134
+ nodes.set(agentKey, n);
135
+ internal.set(agentKey, { kind: fields.kind, records: [], holds: [] });
136
+ } else {
137
+ for (const [key, value] of Object.entries(fields)) if (n[key] === null) n[key] = value;
138
+ }
139
+ Object.assign(internal.get(agentKey), intern ?? {});
140
+ return n;
141
+ };
142
+ /** One of DoFlow's own records of an agent (`records`) or a task it is working on (`holds`), kept for V.6. */
143
+ const note = (n , list , item ) => { if (n) internal.get(n.agentKey)[list].push(item); };
144
+ const slugOf = (store , name ) => (isSafeSlug(store?.slug) ? store.slug : name.replace(/\.json$/, ''));
145
+ const edge = (from , to , basis ) => {
146
+ if (from !== to) edges.set(`${from}|${to}|${basis}`, { from, to, basis }); // kept only when both ends are nodes, below
147
+ };
148
+ const harnessOf = (id ) => (typeof id !== 'string' ? null : registry[id] ? id : '(other)');
149
+ const transcriptOf = (harness , file ) => {
150
+ const entry = harness ? registry[harness] : null;
151
+ if (!entry) return { reader: null, linked: false, reason: 'no-reader' };
152
+ if (entry.join === 'none') return { reader: null, linked: false, reason: 'not-linked' };
153
+ if (entry.reader === null) return { reader: null, linked: false, reason: 'no-reader' };
154
+ return file ? { reader: entry.reader, linked: true, reason: null } : { reader: entry.reader, linked: false, reason: 'not-found' };
155
+ };
156
+ const lastAt = (harness , file , companion ) => {
157
+ try {
158
+ const out = READERS[registry[harness].reader]().read({ file, companion, mode: 'metadata', maxBytes: 32768, limit: 64 });
159
+ return [...out.events].reverse().find((e ) => e.at)?.at ?? null;
160
+ } catch { return null; }
161
+ };
162
+
163
+ // ---- main sessions (IC-018 a) and the transcript each one's id names ----------------------------------
164
+ const sessions = observed.body.sessions.filter((s ) => s.kind === 'hook');
165
+ counts.notChecked += Math.max(0, sessions.length - MAX_SESSIONS);
166
+ const root = stateDir(env);
167
+ const rawIds = new Map ();
168
+ if (root !== null) {
169
+ try { for (const name of fsImpl.readdirSync(path.join(root, 'sessions')).slice(0, LIST_CAP)) if (SESSION_DIR.test(name)) rawIds.set(sha16(name), name); } catch { /* none */ }
170
+ }
171
+ const scanned = new Map (); // harness -> [{file, companion, header}] of the bounded scan
172
+ const scan = (harness ) => {
173
+ if (scanned.has(harness)) return scanned.get(harness);
174
+ const entry = registry[harness];
175
+ const files = [];
176
+ if (entry && entry.reader) {
177
+ for (const [dir, glob] of locations(entry, env, homeDir, platform)) {
178
+ if (harness === 'pi') for (const c of checkouts) files.push(...expandGlob(dir, glob.replace('{cwdEncoded}', ENCODE.pi(c)), fsImpl));
179
+ else files.push(...expandGlob(dir, glob.replace('{sessionId}', '*').replace('{cwdEncoded}', '*'), fsImpl));
180
+ }
181
+ }
182
+ const newest = harness === 'kiro' || harness === 'pi'
183
+ ? files.map((file) => { try { return { file, t: fsImpl.lstatSync(file).mtimeMs }; } catch { return { file, t: 0 }; } }).sort((a, b) => b.t - a.t).map((x) => x.file)
184
+ : [...files].sort().reverse();
185
+ const out = [];
186
+ for (const file of [...new Set(newest)].slice(0, SCAN_FILES)) {
187
+ try { out.push({ file, header: READERS[entry.reader]().header({ file }) }); } catch { counts.unreadable += 1; }
188
+ }
189
+ scanned.set(harness, out);
190
+ return out;
191
+ };
192
+ const locate = (harness , id ) => {
193
+ const entry = registry[harness];
194
+ if (!entry || !entry.reader || entry.join === 'none' || !SESSION_DIR.test(id)) return null;
195
+ if (harness === 'pi') return scan('pi').find((x) => x.header.sessionId === id)?.file ?? null;
196
+ for (const [dir, glob] of locations(entry, env, homeDir, platform)) {
197
+ const hit = expandGlob(dir, glob.replace('{sessionId}', id).replace('{cwdEncoded}', '*'), fsImpl)[0];
198
+ if (hit) return hit;
199
+ }
200
+ return null;
201
+ };
202
+
203
+ const mains = [];
204
+ for (const s of sessions.slice(0, MAX_SESSIONS)) {
205
+ const harness = harnessOf(s.harness);
206
+ const rawId = rawIds.get(s.sessionKey) ?? null;
207
+ const file = harness && rawId ? locate(harness, rawId) : null;
208
+ const agentKey = sha16(`${harness ?? ''}:${rawId ?? s.sessionKey}`);
209
+ const companion = file && harness === 'kiro' ? file.replace(/\.jsonl$/, '.json') : undefined;
210
+ const n = node(agentKey, {
211
+ kind: 'main-session', harness, model: label(s.model), modelBasis: s.model ? 'hook-observation' : null, worktreeKey: s.worktreeKey,
212
+ startedAt: s.startedAt, lastRecordAt: (file && harness && lastAt(harness, file, companion)) || s.lastHookRecordAt,
213
+ transcript: transcriptOf(harness, file),
214
+ }, { harness, sessionId: rawId, file, companion });
215
+ if (n) {
216
+ mains.push({ n, harness, rawId, file });
217
+ if (s.startedAt) note(n, 'records', { type: 'hook-record', at: s.startedAt, task: null, result: null });
218
+ if (s.lastHookRecordAt && s.lastHookRecordAt !== s.startedAt) note(n, 'records', { type: 'hook-record', at: s.lastHookRecordAt, task: null, result: null });
219
+ }
220
+ }
221
+
222
+ // ---- harness sub-agent records (IC-020 sources), through the readers' header ---------------------------
223
+ const child = (parent , harness , idPart , header , file , companion ) => {
224
+ const agentKey = sha16(`${harness}:${idPart}`);
225
+ const depth = Number.isInteger(header.depth) ? header.depth : parent.depth + 1;
226
+ const n = node(agentKey, {
227
+ kind: 'harness-subagent', harness, model: header.model, modelBasis: header.model ? 'harness-record' : null, label: header.label, depth,
228
+ worktreeKey: parent.worktreeKey, lastRecordAt: file ? lastAt(harness, file, companion) : null, transcript: transcriptOf(harness, file),
229
+ }, { harness, sessionId: header.sessionId, agentId: header.agentId, file, companion });
230
+ if (n) edge(parent.agentKey, agentKey, 'harness-subagent-record');
231
+ };
232
+ for (const { n, harness, rawId, file } of mains) {
233
+ if (!harness || !rawId || !file || !registry[harness]) continue;
234
+ const reader = READERS[registry[harness].reader]();
235
+ try {
236
+ if (harness === 'claude') {
237
+ const dir = path.join(path.dirname(file), path.basename(file, '.jsonl'), 'subagents');
238
+ let names = [];
239
+ try { names = fsImpl.readdirSync(dir).filter((x ) => /^agent-.+\.jsonl$/.test(x)).sort().slice(0, SUBAGENT_FILES); } catch { /* none */ }
240
+ const subs = names.map((name) => {
241
+ const sub = path.join(dir, name);
242
+ const meta = readJsonFile(sub.replace(/\.jsonl$/, '.meta.json')) || {};
243
+ return { sub, header: reader.header({ file: sub }), toolUseId: typeof meta.toolUseId === 'string' ? meta.toolUseId : null };
244
+ });
245
+ const made = new Map ();
246
+ for (const s of subs.sort((a, b) => (a.header.depth ?? 1) - (b.header.depth ?? 1))) {
247
+ const depth = s.header.depth ?? 1;
248
+ // A depth-1 agent was spawned by the main session; deeper ones by the file that holds their tool_use id.
249
+ const candidates = depth <= 1 ? [{ file, node: n }] : made.get(depth - 1) ?? [];
250
+ const parent = depth <= 1 ? candidates[0] : candidates.find((c) => s.toolUseId !== null && fileHas(c.file, s.toolUseId, fsImpl));
251
+ const agentId = s.header.agentId ?? path.basename(s.sub, '.jsonl').replace(/^agent-/, '');
252
+ const agentKey = sha16(`claude:${rawId}:${agentId}`);
253
+ const made1 = node(agentKey, {
254
+ kind: 'harness-subagent', harness, model: s.header.model, modelBasis: s.header.model ? 'harness-record' : null, label: s.header.label, depth,
255
+ worktreeKey: n.worktreeKey, lastRecordAt: lastAt(harness, s.sub), transcript: transcriptOf(harness, s.sub),
256
+ }, { harness, sessionId: rawId, agentId, file: s.sub });
257
+ if (!made1) continue;
258
+ if (parent) edge(parent.node.agentKey, agentKey, 'harness-subagent-record');
259
+ made.set(depth, [...(made.get(depth) ?? []), { file: s.sub, node: made1 }]);
260
+ }
261
+ } else if (harness === 'copilot') {
262
+ for (const c of reader.header({ file }).children) child(n, harness, `${rawId}:${c.agentId}`, { ...c, model: null, sessionId: rawId }, null);
263
+ } else if (harness === 'codex' || harness === 'pi' || harness === 'kiro') {
264
+ // Children name their parent session id in their own header: follow the chain through the bounded scan.
265
+ const parents = new Map ([[rawId, n]]);
266
+ const headers = scan(harness);
267
+ for (let grew = true; grew;) {
268
+ grew = false;
269
+ for (const h of headers) {
270
+ const p = h.header.parentSessionId;
271
+ if (!p || !parents.has(p) || !h.header.sessionId || parents.has(h.header.sessionId)) continue;
272
+ const companion = harness === 'kiro' ? h.file.replace(/\.jsonl$/, '.json') : undefined;
273
+ child(parents.get(p), harness, h.header.sessionId, h.header, h.file, companion);
274
+ const made = nodes.get(sha16(`${harness}:${h.header.sessionId}`));
275
+ if (made) { parents.set(h.header.sessionId, made); grew = true; }
276
+ }
277
+ }
278
+ }
279
+ } catch { counts.unreadable += 1; }
280
+ }
281
+
282
+ // ---- DoFlow records: attempts, worker runs, dispatch --------------------------------------------------
283
+ const readStores = (checkout , dirName , read ) => {
284
+ const dir = path.join(checkout, '.doflow', 'state', dirName);
285
+ let names = [];
286
+ try { names = fsImpl.readdirSync(dir).filter((x ) => x.endsWith('.json')).sort(); } catch { return []; }
287
+ const out = [];
288
+ for (const name of names) {
289
+ try { const data = read(path.join(dir, name)); if (data) out.push([name, data]); } catch { counts.unreadable += 1; }
290
+ }
291
+ return out;
292
+ };
293
+ const runNode = (runKey , extra = {}) => {
294
+ if (!RUN_KEY.test(runKey)) return null;
295
+ return node(sha16(`doflow:${runKey}`), { kind: 'doflow-run', worktreeKey: runKey.split('/')[0], ...extra }, { runKey });
296
+ };
297
+ const attemptRuns = new Map (); // `task#token` -> runKey of the attempt
298
+ for (const checkout of checkouts) {
299
+ for (const [name, store] of readStores(checkout, 'task-attempts', (file) => readTaskState(fsImpl, file))) {
300
+ for (const [taskId, entry] of Object.entries(store.tasks ?? {}) ) {
301
+ for (const a of Array.isArray(entry?.attempts) ? entry.attempts : []) {
302
+ if (typeof a.runKey !== 'string') continue;
303
+ attemptRuns.set(`${taskId}#${a.token}`, a.runKey);
304
+ const at = [a.endedAt, a.finishedAt, a.startedAt].find((v) => typeof v === 'string') ?? null;
305
+ const n = runNode(a.runKey, { startedAt: typeof a.startedAt === 'string' ? a.startedAt : null, lastRecordAt: at });
306
+ if (n) {
307
+ if (typeof a.owner === 'string' && (n.label === null || (at ?? '') >= (n.lastRecordAt ?? ''))) n.label = label(a.owner);
308
+ if (at && (n.lastRecordAt === null || at > n.lastRecordAt)) n.lastRecordAt = at;
309
+ if (typeof a.startedAt === 'string' && (n.startedAt === null || a.startedAt < n.startedAt)) n.startedAt = a.startedAt;
310
+ const task = label(taskId);
311
+ if (typeof a.startedAt === 'string') note(n, 'records', { type: 'attempt-claim', at: a.startedAt, task, result: null });
312
+ const endedAt = [a.finishedAt, a.endedAt].find((v) => typeof v === 'string');
313
+ if (endedAt) note(n, 'records', { type: 'attempt-end', at: endedAt, task, result: typeof a.result === 'string' ? label(a.result) : null });
314
+ if (a.state === 'active') note(n, 'holds', { slug: slugOf(store, name), task: taskId });
315
+ }
316
+ }
317
+ }
318
+ }
319
+ }
320
+
321
+ const runs = new Map ();
322
+ for (const checkout of checkouts) {
323
+ for (const [name, byId] of readStores(checkout, 'worker-runs', (file) => readRuns(file, fsImpl))) {
324
+ for (const [runId, run] of Object.entries(byId) ) if (!runs.has(runId)) runs.set(runId, { run, checkout, slug: slugOf(null, name) });
325
+ }
326
+ }
327
+ const ordered = [...runs.entries()].sort((a, b) => String(b[1].run.startedAt ?? b[1].run.createdAt ?? '').localeCompare(String(a[1].run.startedAt ?? a[1].run.createdAt ?? '')) || (a[0] < b[0] ? -1 : 1));
328
+ const workerKey = (runId ) => sha16(`worker:${runId}`);
329
+ for (const [runId, { run, checkout, slug }] of ordered) {
330
+ const caller = run.caller && typeof run.caller.runKey === 'string' ? runNode(run.caller.runKey) : null;
331
+ const harness = harnessOf(run.harness);
332
+ const isChild = typeof run.childOf === 'string' && run.childOf !== '';
333
+ let liveness = 'not-checked';
334
+ if (run.phase !== 'ended') {
335
+ if (checks < MAX_CHECKS) {
336
+ checks += 1;
337
+ const seen = [alive(run.supervisor, { platform }), alive(run.worker, { platform })];
338
+ liveness = seen.includes(true) ? 'process-seen' : seen.every((v) => v === false) ? 'process-gone' : 'not-checked';
339
+ } else counts.notChecked += 1;
340
+ }
341
+ const n = node(workerKey(runId), {
342
+ kind: isChild ? 'child-worker-run' : 'worker-run', harness, model: label(run.binding?.model), modelBasis: run.binding?.model ? 'worker-binding' : null,
343
+ depth: isChild ? 2 : caller ? 1 : 0, worktreeKey: (run.caller?.runKey ?? '').split('/')[0] || cwdHash(checkout),
344
+ startedAt: run.startedAt ?? run.createdAt ?? null, lastRecordAt: run.end?.at ?? run.startedAt ?? run.createdAt ?? null, liveness,
345
+ ended: run.end && typeof run.end.at === 'string' ? { at: run.end.at, result: typeof run.end.state === 'string' ? run.end.state : null } : null,
346
+ transcript: transcriptOf(harness, null),
347
+ }, { harness, runId });
348
+ if (!n) continue;
349
+ const task = label(run.task);
350
+ const startedAt = run.startedAt ?? run.createdAt;
351
+ if (typeof startedAt === 'string') note(n, 'records', { type: 'worker-run-start', at: startedAt, task, result: null });
352
+ if (run.end && typeof run.end.at === 'string') note(n, 'records', { type: 'worker-run-end', at: run.end.at, task, result: typeof run.end.state === 'string' ? label(run.end.state) : null });
353
+ if (run.phase !== 'ended' && typeof run.task === 'string') note(n, 'holds', { slug, task: run.task });
354
+ if (isChild) edge(workerKey(run.childOf), n.agentKey, 'worker-childOf');
355
+ else if (caller) edge(caller.agentKey, n.agentKey, 'worker-caller');
356
+ else {
357
+ const viaAttempt = attemptRuns.get(`${run.task}#${run.token}`);
358
+ const owner = viaAttempt ? runNode(viaAttempt) : null;
359
+ if (owner) edge(owner.agentKey, n.agentKey, 'attempt-runKey');
360
+ }
361
+ }
362
+ for (const checkout of checkouts) {
363
+ for (const [, store] of readStores(checkout, 'dispatch', (file) => readTaskState(fsImpl, file))) {
364
+ for (const d of Object.values(store.dispatches ?? {}) ) {
365
+ if (typeof d?.parentRunKey !== 'string') continue;
366
+ const snap = d.primary && typeof d.primary === 'object' ? d.primary : null;
367
+ const owner = runNode(d.parentRunKey, snap ? { harness: harnessOf(snap.harness), model: label(typeof snap.model === 'string' ? snap.model : null), modelBasis: snap.model ? 'dispatch-snapshot' : null } : {});
368
+ const launched = [...runs.entries()].find(([, r]) => r.run.task === d.task && r.run.token === d.token && !r.run.childOf);
369
+ if (owner && launched) edge(owner.agentKey, workerKey(launched[0]), 'dispatch-claim');
370
+ }
371
+ }
372
+ }
373
+
374
+ // ---- what leaves: no internal field, kept nodes' edges only ---------------------------------------------
375
+ const keep = [...nodes.values()];
376
+ const body = {
377
+ v: 1, projectKey, nodes: keep, edges: [...edges.values()].filter((e ) => nodes.has(e.from) && nodes.has(e.to)),
378
+ limits: { nodes: MAX_NODES, notChecked: counts.notChecked }, unreadable: counts.unreadable,
379
+ };
380
+ return { ok: true, body, internal };
381
+ }
382
+
383
+ module.exports = { buildTree, READERS, MAX_SESSIONS, MAX_NODES, MAX_CHECKS };
@@ -21,13 +21,40 @@ function fileHash(root , locator ) {
21
21
  } catch { return null; }
22
22
  }
23
23
 
24
- function measureFreshness(root , locator , gitCommit ) {
25
- return { gitCommit, fileHash: fileHash(root, locator), observedAt: new Date().toISOString(), status: 'FRESH' };
24
+ /**
25
+ * IC-007: the root a relative locator.file is read in: sourceRoot, except for a locator under
26
+ * `agent-docs/doflow/<slug>/` whose folder is absent in sourceRoot and present in the state root
27
+ * (the IC-005 `feature_root` case), which resolves in the state root. Any other missing file stays
28
+ * missing in sourceRoot (=> STALE/missing as today for a non-redirected file).
29
+ */
30
+ function fileRootFor(sourceRoot , stateRoot , locator ) {
31
+ const file = locator?.file;
32
+ if (typeof file !== 'string' || file === '' || path.isAbsolute(file) || path.resolve(sourceRoot) === path.resolve(stateRoot)) return sourceRoot;
33
+ const normalized = path.normalize(file).replace(/\\/g, '/');
34
+ const match = normalized.match(/^agent-docs\/doflow\/([^/]+)\/.+/);
35
+ if (match) {
36
+ const slug = match[1];
37
+ const folder = path.join('agent-docs', 'doflow', slug);
38
+ const isDir = (root ) => {
39
+ try { return fs.statSync(path.resolve(root, folder)).isDirectory(); } catch { return false; }
40
+ };
41
+ if (!isDir(sourceRoot) && isDir(stateRoot)) {
42
+ return stateRoot;
43
+ }
44
+ }
45
+ return sourceRoot;
46
+ }
47
+
48
+ // IC-007: a relative locator.file is hashed in sourceRoot, the checkout the verb runs in; `root`
49
+ // (the record store) is only the default. An absolute locator.file is hashed as is.
50
+ function measureFreshness(root , locator , gitCommit , sourceRoot = root) {
51
+ return { gitCommit, fileHash: fileHash(fileRootFor(sourceRoot, root, locator), locator), observedAt: new Date().toISOString(), status: 'FRESH' };
26
52
  }
27
53
 
28
54
  class FreshnessValidator {
29
55
  repoRoot ;
30
56
  treeRoot ;
57
+ sourceRoot ;
31
58
  gitRunner ;
32
59
 
33
60
 
@@ -37,11 +64,13 @@ class FreshnessValidator {
37
64
  * @param {Object} [options]
38
65
  * @param {string} [options.repoRoot]
39
66
  * @param {string} [options.treeRoot]
67
+ * @param {string} [options.sourceRoot] checkout a relative locator.file is hashed in (default repoRoot)
40
68
  * @param {Function} [options.gitRunner]
41
69
  */
42
70
  constructor(options = {}) {
43
71
  this.repoRoot = options.repoRoot || REPO_ROOT;
44
72
  this.treeRoot = options.treeRoot || this.repoRoot;
73
+ this.sourceRoot = options.sourceRoot || this.repoRoot;
45
74
  this.gitRunner = options.gitRunner || this.defaultGitRunner.bind(this);
46
75
  this.validatingLedger = false;
47
76
  }
@@ -158,7 +187,7 @@ class FreshnessValidator {
158
187
  }
159
188
 
160
189
  if (item.freshness?.fileHash) {
161
- const currentHash = fileHash(this.repoRoot, item.locator);
190
+ const currentHash = fileHash(fileRootFor(this.sourceRoot, this.repoRoot, item.locator), item.locator);
162
191
  return currentHash === item.freshness.fileHash
163
192
  ? { status: 'FRESH' }
164
193
  : { status: 'STALE', reason: currentHash ? 'Observed file contents changed' : 'Observed file is missing or unreadable' };
@@ -210,5 +239,6 @@ class FreshnessValidator {
210
239
 
211
240
  module.exports = {
212
241
  measureFreshness,
242
+ fileRootFor,
213
243
  FreshnessValidator,
214
244
  };