@khoavu882/doflow 2.0.0-beta.4 → 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 (497) hide show
  1. package/README.md +12 -11
  2. package/bin/doflow.js +21 -11
  3. package/core/.antigravity-plugin/plugin.json +1 -3
  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 +7 -17
  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-bash-guard.sh +7 -17
  15. package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
  16. package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
  17. package/core/harnesses/claude/hooks/session-end.sh +23 -10
  18. package/core/harnesses/claude/hooks/session-start.sh +5 -3
  19. package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
  20. package/core/harnesses/claude/hooks/stop-check.sh +5 -3
  21. package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
  22. package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
  23. package/core/harnesses/claude/settings/settings.json +18 -0
  24. package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
  25. package/core/harnesses/codex/agents/core-implementer.toml +1 -0
  26. package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
  27. package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
  28. package/core/harnesses/codex/agents/research-writer.toml +1 -1
  29. package/core/harnesses/codex/agents/reviewer.toml +10 -0
  30. package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
  31. package/core/harnesses/codex/agents/system-architect.toml +1 -1
  32. package/core/harnesses/codex/hooks/lib.sh +44 -25
  33. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
  34. package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
  35. package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
  36. package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
  37. package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
  38. package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
  39. package/core/harnesses/codex/hooks/session-start.sh +5 -3
  40. package/core/harnesses/codex/hooks/stop-check.sh +6 -5
  41. package/core/harnesses/codex/hooks/subagent-audit.sh +18 -6
  42. package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
  43. package/core/harnesses/copilot/hooks/doflow-lib.sh +93 -0
  44. package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
  45. package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +9 -0
  46. package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
  47. package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
  48. package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
  49. package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
  50. package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
  51. package/core/harnesses/copilot/hooks/doflow.json +57 -0
  52. package/core/harnesses/kiro/agents/doflow.json +1 -1
  53. package/core/harnesses/kiro/hooks/lib.sh +26 -6
  54. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +21 -5
  55. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +21 -5
  56. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +21 -5
  57. package/core/harnesses/kiro/hooks/session-start.sh +12 -5
  58. package/core/harnesses/kiro/hooks/stop-check.sh +22 -30
  59. package/core/harnesses/pi/extensions/doflow-guard.js +167 -0
  60. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -12
  61. package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
  62. package/core/harnesses/shared/hooks/bridge/bridge.sh +277 -0
  63. package/core/harnesses/shared/hooks/bridge/envelope.jq +129 -0
  64. package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
  65. package/core/harnesses/shared/hooks/bridge/respond.sh +70 -0
  66. package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +28 -14
  67. package/core/harnesses/shared/hooks/policies/capture-failure.sh +51 -16
  68. package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
  69. package/core/harnesses/shared/hooks/policies/contract.sh +78 -0
  70. package/core/harnesses/shared/hooks/policies/deny-json.sh +35 -9
  71. package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
  72. package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
  73. package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
  74. package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
  75. package/core/harnesses/shared/hooks/policies/lib.sh +65 -40
  76. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +48 -35
  77. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +284 -33
  78. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +180 -110
  79. package/core/harnesses/shared/hooks/policies/session-context.sh +66 -7
  80. package/core/harnesses/shared/hooks/policies/stop-check.sh +61 -23
  81. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +39 -6
  82. package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
  83. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +97 -159
  84. package/core/harnesses/shared/hooks/stream-hook-runner.js +114 -187
  85. package/core/harnesses/shared/locator/doflow-run +2 -3
  86. package/core/registry/advice.json +3 -3
  87. package/core/registry/assets.json +72 -80
  88. package/core/registry/capabilities.json +12 -0
  89. package/core/registry/contracts.json +60 -33
  90. package/core/registry/contribution-points.json +32 -0
  91. package/core/registry/gates.json +32 -0
  92. package/core/registry/guidance-caps.json +40 -0
  93. package/core/registry/harnesses.json +1542 -277
  94. package/core/registry/lifecycle.json +2837 -37
  95. package/core/registry/mcp.json +5 -1
  96. package/core/registry/models.json +7 -1
  97. package/core/registry/package-templates/README.md +19 -0
  98. package/core/registry/package-templates/agent/files/agent.md +9 -0
  99. package/core/registry/package-templates/agent/template.json +4 -0
  100. package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
  101. package/core/registry/package-templates/capability-provider/template.json +11 -0
  102. package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
  103. package/core/registry/package-templates/debate-preset/template.json +4 -0
  104. package/core/registry/package-templates/review-preset/files/preset.json +30 -0
  105. package/core/registry/package-templates/review-preset/template.json +4 -0
  106. package/core/registry/package-templates/skill/files/SKILL.md +8 -0
  107. package/core/registry/package-templates/skill/template.json +4 -0
  108. package/core/registry/package-templates/worker/files/worker.json +92 -0
  109. package/core/registry/package-templates/worker/template.json +11 -0
  110. package/core/registry/packages/doflow/doflow-package.json +15 -0
  111. package/core/registry/packages/doflow/presets/debate-3.json +26 -0
  112. package/core/registry/protocols.json +61 -0
  113. package/core/registry/routes.json +17 -0
  114. package/core/registry/rules.json +431 -0
  115. package/core/registry/settings.json +276 -0
  116. package/core/registry/storage.json +701 -30
  117. package/core/registry/telemetry.json +236 -0
  118. package/core/registry/transcripts.json +142 -0
  119. package/core/registry/verification.json +76 -1
  120. package/core/registry/workflows.json +55 -0
  121. package/core/shared/agent-specs/core-implementer.md +3 -0
  122. package/core/shared/agent-specs/delivery-lead.md +68 -0
  123. package/core/shared/agent-specs/quality-guardian.md +5 -1
  124. package/core/shared/agent-specs/research-writer.md +10 -3
  125. package/core/shared/agent-specs/reviewer.md +53 -0
  126. package/core/shared/agent-specs/spec-analyst.md +5 -6
  127. package/core/shared/agent-specs/system-architect.md +5 -1
  128. package/core/shared/guidance/DOFLOW_CORE.md +1 -5
  129. package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
  130. package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
  131. package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
  132. package/core/shared/guidance/pointers/codex.md +1 -4
  133. package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
  134. package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
  135. package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
  136. package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
  137. package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
  138. package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
  139. package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
  140. package/core/shared/guidance/references/ORCHESTRATION.md +1 -2
  141. package/core/shared/guidance/rules/RULES.md +45 -0
  142. package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
  143. package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
  144. package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
  145. package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
  146. package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
  147. package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
  148. package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
  149. package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
  150. package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
  151. package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
  152. package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
  153. package/core/shared/scripts/doflow/bash/validate-artifacts.sh +48 -4
  154. package/core/shared/scripts/doflow/bin/doflow-run +154 -35
  155. package/core/shared/skills/do/SKILL.md +12 -10
  156. package/core/shared/skills/do/references/advice_calls.md +26 -0
  157. package/core/shared/skills/do/references/pm_routing.md +1 -2
  158. package/core/shared/skills/do/references/program_mode.md +31 -0
  159. package/core/shared/skills/do-brainstorm/SKILL.md +20 -74
  160. package/core/shared/skills/do-brainstorm/references/handoff_branches.md +35 -0
  161. package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
  162. package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
  163. package/core/shared/skills/do-code-review/SKILL.md +41 -144
  164. package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
  165. package/core/shared/skills/do-code-review/references/tools.md +90 -0
  166. package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
  167. package/core/shared/skills/do-design/SKILL.md +24 -23
  168. package/core/shared/skills/do-diagnose/SKILL.md +14 -4
  169. package/core/shared/skills/do-document/SKILL.md +11 -1
  170. package/core/shared/skills/do-document/references/deep_research.md +1 -2
  171. package/core/shared/skills/do-document/references/embedded-research.md +18 -6
  172. package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
  173. package/core/shared/skills/do-execute-plan/SKILL.md +17 -59
  174. package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
  175. package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
  176. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +28 -1
  177. package/core/shared/skills/do-execute-plan/references/run_position.md +13 -1
  178. package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
  179. package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
  180. package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
  181. package/core/shared/skills/do-flow/SKILL.md +8 -9
  182. package/core/shared/skills/do-flow/references/task_classes.md +2 -2
  183. package/core/shared/skills/do-git/SKILL.md +4 -0
  184. package/core/shared/skills/do-implement/SKILL.md +7 -9
  185. package/core/shared/skills/do-plan/SKILL.md +29 -19
  186. package/core/shared/skills/do-test/SKILL.md +7 -4
  187. package/core/shared/templates/doflow/constitution-template.md +9 -3
  188. package/core/shared/templates/doflow/data-model-template.md +1 -1
  189. package/core/shared/templates/doflow/design-template.md +113 -16
  190. package/core/shared/templates/doflow/plan-template.md +11 -10
  191. package/core/shared/templates/doflow/question-log-template.md +14 -2
  192. package/core/shared/templates/doflow/requirement-template.md +1 -1
  193. package/core/worker-evidence/antigravity/checks.json +14 -0
  194. package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
  195. package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
  196. package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
  197. package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
  198. package/core/worker-evidence/codex/checks.json +38 -0
  199. package/package.json +10 -6
  200. package/src/adapters/antigravity/index.js +226 -113
  201. package/src/adapters/claude/index.js +39 -5
  202. package/src/adapters/codex/agents.js +25 -15
  203. package/src/adapters/codex/hooks.js +40 -5
  204. package/src/adapters/codex/index.js +100 -22
  205. package/src/adapters/codex/native-projection.js +6 -2
  206. package/src/adapters/copilot/index.js +58 -23
  207. package/src/adapters/copy-tree.js +294 -48
  208. package/src/adapters/guidance-manifest.js +71 -0
  209. package/src/adapters/guidance-pointer.js +19 -0
  210. package/src/adapters/hook-commands.js +6 -10
  211. package/src/adapters/index.js +1 -0
  212. package/src/adapters/kiro/index.js +38 -13
  213. package/src/adapters/opencode/index.js +8 -8
  214. package/src/adapters/pi/hooks.js +120 -0
  215. package/src/adapters/pi/index.js +48 -16
  216. package/src/adapters/resolve-source.js +26 -2
  217. package/src/cli/commands/install.js +3 -2
  218. package/src/cli/commands/reconcile.js +53 -9
  219. package/src/cli/commands/remove.js +2 -2
  220. package/src/cli/commands/rollback.js +6 -3
  221. package/src/cli/commands/self-update.js +18 -9
  222. package/src/cli/commands/status.js +21 -2
  223. package/src/cli/commands/ui.js +99 -0
  224. package/src/cli/commands/update.js +3 -2
  225. package/src/cli/index.js +338 -33
  226. package/src/cli/runtime-commands.js +243 -52
  227. package/src/cli/shared.js +0 -2
  228. package/src/helper/git.js +1 -1
  229. package/src/helper/guardrails.js +1 -1
  230. package/src/helper/marker-merge.js +31 -13
  231. package/src/helper/package-ids.js +37 -0
  232. package/src/helper/semver.js +94 -0
  233. package/src/install/backup.js +105 -15
  234. package/src/install/context.js +1 -1
  235. package/src/install/targets.js +31 -16
  236. package/src/lifecycle/coverage.js +74 -0
  237. package/src/lifecycle/hook-step.js +61 -0
  238. package/src/lifecycle/hook-tier.js +160 -0
  239. package/src/lifecycle/index.js +434 -86
  240. package/src/lifecycle/retired-harnesses.js +318 -0
  241. package/src/lifecycle/view.js +35 -15
  242. package/src/registry/index.js +686 -32
  243. package/src/registry/worker-ids.js +45 -0
  244. package/src/release/build-package.js +0 -3
  245. package/src/release/docs-blocks.js +492 -0
  246. package/src/release/preflight.js +425 -0
  247. package/src/release/rehearse.js +401 -0
  248. package/src/release/release-workflow.js +733 -0
  249. package/src/release/sync-plugin-versions.js +43 -7
  250. package/src/runtime/advice/bench-gate.js +25 -0
  251. package/src/runtime/advice/cli.js +59 -16
  252. package/src/runtime/advice/client.js +32 -9
  253. package/src/runtime/advice/config.js +98 -133
  254. package/src/runtime/advice/doctor.js +1 -1
  255. package/src/runtime/advice/elevation.js +97 -0
  256. package/src/runtime/advice/endpoint-guard.js +9 -3
  257. package/src/runtime/advice/mode.js +37 -0
  258. package/src/runtime/advice/record.js +18 -5
  259. package/src/runtime/advice/registry.js +4 -1
  260. package/src/runtime/advice/shadow-config.js +229 -0
  261. package/src/runtime/advice/shadow-dispatcher.js +276 -0
  262. package/src/runtime/brief-rules.js +81 -0
  263. package/src/runtime/brief-tools.js +111 -34
  264. package/src/runtime/candidate.js +514 -0
  265. package/src/runtime/capabilities/executor.js +450 -0
  266. package/src/runtime/capabilities/loopback.js +41 -0
  267. package/src/runtime/capabilities/sanitizer.js +88 -0
  268. package/src/runtime/capability-router.js +135 -3
  269. package/src/runtime/checkouts.js +73 -11
  270. package/src/runtime/claims.js +27 -12
  271. package/src/runtime/cli-result.js +11 -4
  272. package/src/runtime/cli.js +221 -277
  273. package/src/runtime/config-home.js +42 -0
  274. package/src/runtime/context/arbitrate.js +767 -0
  275. package/src/runtime/context/budget.js +203 -0
  276. package/src/runtime/context/compose.js +1367 -0
  277. package/src/runtime/context/delivery-store.js +100 -0
  278. package/src/runtime/context/emission-rules.js +118 -0
  279. package/src/runtime/context/identity.js +227 -0
  280. package/src/runtime/context/mandatory.js +594 -0
  281. package/src/runtime/context/pointer.js +142 -0
  282. package/src/runtime/context/primary-command.js +117 -0
  283. package/src/runtime/context/primary.js +344 -0
  284. package/src/runtime/context/summary-store.js +655 -0
  285. package/src/runtime/context-pack.js +1106 -245
  286. package/src/runtime/decision-register.js +13 -4
  287. package/src/runtime/dispatch/admission.js +392 -0
  288. package/src/runtime/dispatch/attempt-end.js +274 -0
  289. package/src/runtime/dispatch/command.js +628 -0
  290. package/src/runtime/dispatch/controls.js +904 -0
  291. package/src/runtime/dispatch/envelope.js +276 -0
  292. package/src/runtime/dispatch/grants.js +310 -0
  293. package/src/runtime/dispatch/integrate.js +617 -0
  294. package/src/runtime/dispatch/loop.js +1835 -0
  295. package/src/runtime/dispatch/store.js +352 -0
  296. package/src/runtime/dispatch/verify-run.js +201 -0
  297. package/src/runtime/evidence-fields.js +39 -0
  298. package/src/runtime/evidence-ledger.js +41 -7
  299. package/src/runtime/failure/capture.js +3 -1
  300. package/src/runtime/failure/cli.js +25 -9
  301. package/src/runtime/failure/home.js +57 -30
  302. package/src/runtime/failure/store.js +7 -2
  303. package/src/runtime/feature-pointer.js +174 -0
  304. package/src/runtime/feature-resolve.js +13 -2
  305. package/src/runtime/fleet/command.js +77 -0
  306. package/src/runtime/fleet/projects.js +178 -0
  307. package/src/runtime/fleet/tree.js +383 -0
  308. package/src/runtime/freshness.js +77 -10
  309. package/src/runtime/git-memo.js +46 -0
  310. package/src/runtime/guidance/ablation.js +427 -0
  311. package/src/runtime/health.js +565 -17
  312. package/src/runtime/history-compactor.js +13 -3
  313. package/src/runtime/implementation-gate.js +277 -22
  314. package/src/runtime/inventory/coverage.js +1 -1
  315. package/src/runtime/inventory/index.js +1 -1
  316. package/src/runtime/inventory/precedence.js +2 -2
  317. package/src/runtime/inventory/siblings.js +6 -4
  318. package/src/runtime/knowledge/code-corpus.js +277 -0
  319. package/src/runtime/knowledge/code-lexical.js +130 -0
  320. package/src/runtime/knowledge/code-retrieval.js +307 -0
  321. package/src/runtime/knowledge/code-validation.js +106 -0
  322. package/src/runtime/knowledge/gp3-gate.js +74 -0
  323. package/src/runtime/knowledge/graph-adapter.js +288 -0
  324. package/src/runtime/knowledge/graph-fallback.js +394 -0
  325. package/src/runtime/knowledge/hybrid.js +136 -8
  326. package/src/runtime/knowledge/retrieval.js +34 -1
  327. package/src/runtime/knowledge/rtk-compress.js +77 -0
  328. package/src/runtime/knowledge/semble-bridge.py +106 -0
  329. package/src/runtime/knowledge/semble-lifecycle.js +114 -0
  330. package/src/runtime/knowledge/semble-provider.js +365 -0
  331. package/src/runtime/lifecycle/cli.js +16 -6
  332. package/src/runtime/lifecycle/event-store.js +8 -6
  333. package/src/runtime/lifecycle/followup.js +6 -1
  334. package/src/runtime/lifecycle/overview.js +24 -1
  335. package/src/runtime/lifecycle/report-store.js +8 -10
  336. package/src/runtime/lifecycle/retention.js +95 -31
  337. package/src/runtime/lifecycle/root.js +9 -1
  338. package/src/runtime/lifecycle/status.js +34 -17
  339. package/src/runtime/lifecycle/store-upkeep.js +56 -7
  340. package/src/runtime/mask.js +134 -7
  341. package/src/runtime/mcp-consultations.js +40 -6
  342. package/src/runtime/model-router.js +79 -7
  343. package/src/runtime/models/catalogue.js +191 -0
  344. package/src/runtime/models/command.js +153 -0
  345. package/src/runtime/off-words.js +12 -0
  346. package/src/runtime/outcome.js +3 -3
  347. package/src/runtime/packages/command.js +1009 -0
  348. package/src/runtime/packages/contributions.js +116 -0
  349. package/src/runtime/packages/digest.js +132 -0
  350. package/src/runtime/packages/fetch.js +182 -0
  351. package/src/runtime/packages/grants.js +192 -0
  352. package/src/runtime/packages/index-file.js +134 -0
  353. package/src/runtime/packages/instruction.js +90 -0
  354. package/src/runtime/packages/manifest.js +331 -0
  355. package/src/runtime/packages/points.js +477 -0
  356. package/src/runtime/packages/programs.js +216 -0
  357. package/src/runtime/packages/scaffold.js +277 -0
  358. package/src/runtime/packages/source.js +90 -0
  359. package/src/runtime/packages/state.js +164 -0
  360. package/src/runtime/packages/store.js +449 -0
  361. package/src/runtime/packages/tar.js +106 -0
  362. package/src/runtime/packages/view.js +406 -0
  363. package/src/runtime/plan-check.js +59 -0
  364. package/src/runtime/plan-content.js +324 -0
  365. package/src/runtime/policy-init.js +118 -0
  366. package/src/runtime/profiles/doctor.js +96 -0
  367. package/src/runtime/profiles/resolve.js +244 -0
  368. package/src/runtime/profiles/rollback.js +254 -0
  369. package/src/runtime/profiles/sampler.js +41 -0
  370. package/src/runtime/readiness-record.js +16 -11
  371. package/src/runtime/readiness.js +127 -16
  372. package/src/runtime/research-request.js +50 -25
  373. package/src/runtime/retrieval-plan.js +402 -73
  374. package/src/runtime/retrieval-record.js +293 -0
  375. package/src/runtime/review/command.js +246 -0
  376. package/src/runtime/review/dominance.js +68 -0
  377. package/src/runtime/review/findings.js +434 -0
  378. package/src/runtime/review/identity.js +255 -0
  379. package/src/runtime/review/protocol-command.js +109 -0
  380. package/src/runtime/review/protocols.js +405 -0
  381. package/src/runtime/review/resolve-protocol.js +302 -0
  382. package/src/runtime/review/start.js +530 -0
  383. package/src/runtime/roadmap/facts.js +479 -0
  384. package/src/runtime/roadmap/gates.js +270 -0
  385. package/src/runtime/roadmap/loop.js +1636 -0
  386. package/src/runtime/roadmap/merge.js +381 -0
  387. package/src/runtime/roadmap/rows.js +1113 -0
  388. package/src/runtime/roadmap/seed.js +951 -0
  389. package/src/runtime/rules-report.js +336 -0
  390. package/src/runtime/scaffold/artifacts.js +63 -9
  391. package/src/runtime/scaffold/generate.js +21 -9
  392. package/src/runtime/settings/autopilot.js +106 -0
  393. package/src/runtime/settings/cli.js +280 -0
  394. package/src/runtime/settings/config-write.js +314 -0
  395. package/src/runtime/settings/files.js +271 -0
  396. package/src/runtime/settings/legacy.js +154 -0
  397. package/src/runtime/settings/lexical.js +56 -0
  398. package/src/runtime/settings/prepared-run.js +269 -0
  399. package/src/runtime/settings/project-layers.js +118 -0
  400. package/src/runtime/settings/refs.js +288 -0
  401. package/src/runtime/settings/registry.js +553 -0
  402. package/src/runtime/settings/resolve.js +713 -0
  403. package/src/runtime/settings/shared.js +37 -0
  404. package/src/runtime/settings/worker-billing.js +107 -0
  405. package/src/runtime/settings/write.js +269 -0
  406. package/src/runtime/slot-command.js +78 -0
  407. package/src/runtime/slots.js +671 -0
  408. package/src/runtime/source-version.js +290 -0
  409. package/src/runtime/state-root.js +217 -0
  410. package/src/runtime/storage/cli.js +12 -5
  411. package/src/runtime/storage/engine.js +9 -2
  412. package/src/runtime/storage/registry.js +14 -1
  413. package/src/runtime/storage/roots.js +66 -16
  414. package/src/runtime/storage/rules.js +82 -10
  415. package/src/runtime/storage/upkeep.js +10 -2
  416. package/src/runtime/storage/work-status.js +245 -26
  417. package/src/runtime/task-attempts.js +589 -69
  418. package/src/runtime/task-classifier.js +47 -11
  419. package/src/runtime/task-graph.js +373 -0
  420. package/src/runtime/task-run.js +778 -0
  421. package/src/runtime/task-scope.js +202 -56
  422. package/src/runtime/task-sources.js +147 -0
  423. package/src/runtime/task-state.js +440 -65
  424. package/src/runtime/team/admission.js +301 -0
  425. package/src/runtime/team/envelope.js +324 -0
  426. package/src/runtime/team/review-guard.js +156 -0
  427. package/src/runtime/trace/analyze.js +427 -0
  428. package/src/runtime/trace/baseline.js +770 -0
  429. package/src/runtime/trace/ledger.js +63 -140
  430. package/src/runtime/trace/otlp-export.js +568 -0
  431. package/src/runtime/trace/otlp-map.js +481 -0
  432. package/src/runtime/trace/spans.js +745 -0
  433. package/src/runtime/trace/telemetry.js +225 -0
  434. package/src/runtime/trace/views.js +1 -0
  435. package/src/runtime/transcripts/command.js +98 -0
  436. package/src/runtime/transcripts/cursor.js +326 -0
  437. package/src/runtime/transcripts/read.js +126 -0
  438. package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
  439. package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
  440. package/src/runtime/transcripts/readers/copilot-events.js +90 -0
  441. package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
  442. package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
  443. package/src/runtime/transcripts/registry.js +161 -0
  444. package/src/runtime/transcripts/stats.js +161 -0
  445. package/src/runtime/verification/check-key.js +628 -0
  446. package/src/runtime/verification/contract-runner.js +546 -11
  447. package/src/runtime/verification/engine.js +499 -9
  448. package/src/runtime/verification/impact.js +617 -0
  449. package/src/runtime/verification/inflight.js +205 -0
  450. package/src/runtime/verification/registry.js +176 -0
  451. package/src/runtime/verification/reuse.js +184 -0
  452. package/src/runtime/verification/telemetry-emit.js +254 -0
  453. package/src/runtime/verification/test-selection.js +67 -7
  454. package/src/runtime/worker/active.js +90 -0
  455. package/src/runtime/worker/cancel.js +222 -0
  456. package/src/runtime/worker/catalogue.js +122 -0
  457. package/src/runtime/worker/command.js +677 -0
  458. package/src/runtime/worker/evidence.js +288 -0
  459. package/src/runtime/worker/launch.js +288 -0
  460. package/src/runtime/worker/models.js +137 -0
  461. package/src/runtime/worker/parsers.js +218 -0
  462. package/src/runtime/worker/probes/index.js +706 -0
  463. package/src/runtime/worker/process-tree.js +415 -0
  464. package/src/runtime/worker/quota.js +43 -0
  465. package/src/runtime/worker/readiness.js +713 -0
  466. package/src/runtime/worker/run-store.js +166 -0
  467. package/src/runtime/worker/supervisor.js +541 -0
  468. package/src/runtime/worker/worktree-check.js +252 -0
  469. package/src/runtime/workflow-engine.js +5 -1
  470. package/src/runtime/workflow-orchestrator.js +578 -63
  471. package/src/runtime/worktree.js +149 -22
  472. package/src/state/index.js +1 -1
  473. package/src/state/lockfile.js +64 -10
  474. package/src/state/run-lock.js +1 -1
  475. package/types/node-commonjs.d.ts +106 -7
  476. package/core/.antigravity-plugin/hooks.json +0 -39
  477. package/core/harnesses/gemini/hooks/hooks.json +0 -77
  478. package/core/harnesses/gemini/hooks/lib.sh +0 -302
  479. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -21
  480. package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
  481. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -21
  482. package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
  483. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -22
  484. package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
  485. package/core/harnesses/gemini/hooks/session-end.sh +0 -5
  486. package/core/harnesses/gemini/hooks/session-start.sh +0 -5
  487. package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
  488. package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
  489. package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
  490. package/core/harnesses/shared/hooks/hooks.json +0 -47
  491. package/core/shared/guidance/FLAGS.md +0 -25
  492. package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
  493. package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
  494. package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
  495. package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
  496. package/src/adapters/gemini/hooks.js +0 -210
  497. package/src/adapters/gemini/index.js +0 -390
@@ -0,0 +1,628 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ const crypto = require('node:crypto');
6
+ const nodeFs = require('node:fs');
7
+ const path = require('node:path');
8
+ const { spawnSync } = require('node:child_process');
9
+
10
+ /**
11
+ * Check key and tree digest (088 E.2, IC-009, DD-2; 089 IC-001, DD-1).
12
+ *
13
+ * Computes a deterministic 16-hex key representing a check's execution identity.
14
+ * Two check invocations with equal check keys can safely share results in in-flight
15
+ * deduplication (P5.4, G.4, IC-017) and, when enabled, reuse a recorded PASS (089).
16
+ *
17
+ * Key (payload version 2) = sha16(JSON.stringify([
18
+ * 2,
19
+ * command,
20
+ * timeoutMs,
21
+ * realpath(cwd),
22
+ * HEAD,
23
+ * treeDigest, modified/untracked files, ignored entries removed (unchanged from version 1)
24
+ * ignoredDigest, sorted names of ignored entries (`!!`), `none` when there are none
25
+ * inputsDigest, bytes of the registry's policy.reuse.ignoredInputs, `absent` when missing
26
+ * linksDigest, bytes of the OUT-OF-REPO targets of tracked symlinks (mode 120000), sha256('none') when none
27
+ * toolDigest, realpath/size/mtime/inode of `node` and of the command's first word on env.PATH
28
+ * process.version,
29
+ * platform,
30
+ * arch,
31
+ * envDigest
32
+ * ]))
33
+ *
34
+ * `checkKeyV2` also returns the full sha256 of the same payload (`payloadDigest`) for the
35
+ * reuse store's trust check; `checkKey` returns the key only. One `git status` call yields
36
+ * both the tree digest and the ignored set (`treeState`).
37
+ *
38
+ * If git faults, more than 500 files or 50 MB are modified/untracked, or a declared input is a
39
+ * symlink, a non-file, unreadable or over 1 MB, the key is null (unkeyable, runs unshared).
40
+ * REUSE-UNKEYABLE (089 review C-3, C-4): when a tracked symlink points at a directory or an
41
+ * unreadable/oversized target outside the repository (or there are over 500 links), or the
42
+ * command's first word cannot be parsed safely (quotes, escapes, `$`, backtick, `~`, a glob, an
43
+ * inline `PATH=`), `checkKeyV2` still returns `key` (in-flight sharing and telemetry use it) but
44
+ * `payloadDigest` is null, so the check is neither looked up nor stored.
45
+ * Residual (design RK-1): an ignored file outside the declared list that changes content, and
46
+ * a tool behind a wrapper, are not seen.
47
+ */
48
+
49
+ const MAX_UNCOMMITTED_FILES = 500;
50
+ const MAX_UNCOMMITTED_BYTES = 50 * 1024 * 1024; // 50 MB
51
+ const MAX_INPUT_BYTES = 1024 * 1024; // 1 MB per declared ignored input
52
+ const PAYLOAD_VERSION = 2;
53
+ /** Stands for an ignored-entry digest that could not be read; never equals a sha256 hex digest. */
54
+ const IGNORED_UNAVAILABLE = 'unavailable';
55
+ /** Tracked symlinks are not read (the caller asked for no link read; telemetry/in-flight keys only). */
56
+ const LINKS_SKIPPED = 'skipped';
57
+ /** A tracked symlink whose out-of-repo target cannot be keyed: the check is reuse-unkeyable. */
58
+ const LINKS_UNKEYABLE = 'unkeyable';
59
+ const MAX_TRACKED_LINKS = 500;
60
+
61
+ /** First words that are shell syntax, not a program on PATH (IC-001 toolDigest). */
62
+ const SHELL_KEYWORDS = new Set([
63
+ 'if', 'then', 'else', 'elif', 'fi', 'for', 'while', 'until', 'do', 'done', 'case', 'esac',
64
+ 'select', 'function', 'time', 'coproc', '!', '{', '}', '[[', ']]', '(', ')',
65
+ ]);
66
+
67
+ function sha256(text ) {
68
+ return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
69
+ }
70
+
71
+ /** Registry-owned inputs of the key; a load fault makes a check unkeyable at the caller. */
72
+ function keyPolicy() {
73
+ const { loadVerificationRegistry } = require('./registry.js');
74
+ const policy = loadVerificationRegistry().policy;
75
+ const exclude = policy?.dedup?.envExclude;
76
+ if (!Array.isArray(exclude) || !exclude.every((key ) => typeof key === 'string')) {
77
+ throw new Error('Invalid dedup environment exclusions');
78
+ }
79
+ const inputs = policy?.reuse?.ignoredInputs;
80
+ if (!Array.isArray(inputs) || !inputs.every((entry ) => typeof entry === 'string')) {
81
+ throw new Error('Invalid reuse ignored inputs');
82
+ }
83
+ return { envExclude: exclude, ignoredInputs: inputs };
84
+ }
85
+
86
+ function envExclude() {
87
+ return keyPolicy().envExclude;
88
+ }
89
+
90
+ /**
91
+ * Computes sha256 of sorted NAME=VALUE lines minus excluded environment variables.
92
+ * @param {Record<string, string | undefined>} [env=process.env]
93
+ * @param {readonly string[]} [exclude=registry policy.dedup.envExclude]
94
+ * @returns {string} sha256 hex digest
95
+ */
96
+ function envDigest(
97
+ env = process.env,
98
+ exclude = envExclude(),
99
+ ) {
100
+ const excludeSet = new Set(exclude);
101
+ const lines = [];
102
+ for (const [key, value] of Object.entries(env)) {
103
+ if (value === undefined || excludeSet.has(key)) continue;
104
+ // Raw NAME=VALUE lines must be unambiguous. Multiline environments run unshared
105
+ // rather than collide with a second environment containing an additional key.
106
+ if (key.includes('=') || key.includes('\n') || value.includes('\n')) {
107
+ throw new Error('Unkeyable environment');
108
+ }
109
+ lines.push(`${key}=${value}`);
110
+ }
111
+ lines.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
112
+ return crypto.createHash('sha256').update(lines.join('\n'), 'utf8').digest('hex');
113
+ }
114
+
115
+ /**
116
+ * The HEAD commit hash of the git repository at root, or null on git fault.
117
+ * @param {string} root
118
+ * @param {{ spawnSyncImpl?: typeof spawnSync }} [options]
119
+ * @returns {string | null} 40- or 64-character hex commit, or null
120
+ */
121
+ function headCommit(root , { spawnSyncImpl = spawnSync } = {}) {
122
+ try {
123
+ const res = spawnSyncImpl('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8' });
124
+ if (res.status !== 0 || typeof res.stdout !== 'string') return null;
125
+ const trimmed = res.stdout.trim();
126
+ return /^[0-9a-f]{40,64}$/.test(trimmed) ? trimmed : null;
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+
132
+ /** What one `git status` call says about the working tree (089 IC-001). */
133
+
134
+
135
+
136
+
137
+
138
+
139
+
140
+
141
+
142
+
143
+
144
+ /** `links: false` skips the `git ls-files` spawn (reuse off: nothing reads the link digest). */
145
+
146
+
147
+
148
+
149
+
150
+
151
+
152
+
153
+
154
+ /**
155
+ * Runs the ONE `git status --porcelain=v1 -z --untracked-files=all --ignored=matching` call and
156
+ * splits it: modified/untracked entries (runtime-state directories left out) and the digest of the
157
+ * ignored (`!!`) entry names. Null on a git fault or a malformed rename entry.
158
+ */
159
+ function readStatus(
160
+ root ,
161
+ { spawnSyncImpl = spawnSync, fsImpl = nodeFs, configDir, links: readLinks = true } ,
162
+ ) {
163
+ const top = spawnSyncImpl('git', ['rev-parse', '--show-toplevel'], { cwd: root, encoding: 'utf8' });
164
+ if (top.status !== 0 || typeof top.stdout !== 'string' || !path.isAbsolute(top.stdout.trim())) return null;
165
+ const gitRoot = fsImpl.realpathSync(top.stdout.trim());
166
+ // 089 review C-5: runtime state is left out, tracked project config is not. Under `.doflow` and
167
+ // the config dir, untracked (`??`) and ignored (`!!`) entries and everything in `state/` are
168
+ // excluded; a tracked entry (any other status) is an input and enters the tree digest.
169
+ const excluded = ['.doflow'];
170
+ if (configDir) {
171
+ // A first run may not have created its config directory yet. Resolve its nearest
172
+ // existing ancestor so a symlinked parent has the same identity after creation.
173
+ let ancestor = path.resolve(configDir);
174
+ const missing = [];
175
+ let realConfig ;
176
+ for (;;) {
177
+ try { realConfig = path.join(fsImpl.realpathSync(ancestor), ...missing); break; }
178
+ catch (error ) {
179
+ if (error.code !== 'ENOENT' || path.dirname(ancestor) === ancestor) throw error;
180
+ missing.unshift(path.basename(ancestor));
181
+ ancestor = path.dirname(ancestor);
182
+ }
183
+ }
184
+ const relative = path.relative(gitRoot, realConfig );
185
+ if (relative && relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative)) {
186
+ excluded.push(relative.split(path.sep).join('/'));
187
+ }
188
+ }
189
+ const res = spawnSyncImpl(
190
+ 'git',
191
+ ['status', '--porcelain=v1', '-z', '--untracked-files=all', '--ignored=matching'],
192
+ { cwd: root, encoding: 'utf8' },
193
+ );
194
+ if (res.status !== 0 || typeof res.stdout !== 'string') return null;
195
+
196
+ const raw = res.stdout;
197
+ const tokens = raw.split('\0');
198
+ // The trailing null produces an empty token at the end
199
+ if (tokens.length > 0 && tokens[tokens.length - 1] === '') {
200
+ tokens.pop();
201
+ }
202
+
203
+ const entries = [];
204
+ const ignoredPaths = [];
205
+ for (let i = 0; i < tokens.length; i += 1) {
206
+ const token = tokens[i];
207
+ if (!token) continue;
208
+ const status = token.slice(0, 2);
209
+ const relPath = token.slice(3);
210
+ let entry = token;
211
+ // Both names are part of a rename/copy entry; dropping the source can give two
212
+ // different uncommitted trees the same digest when their destination bytes match.
213
+ if (/[RC]/.test(status)) {
214
+ const source = tokens[++i];
215
+ if (!source) return null;
216
+ entry += `\0${source}`;
217
+ }
218
+
219
+ // Only runtime state is excluded; a tracked file under .doflow or the config dir is an input.
220
+ if (excluded.some((dir) => (relPath === dir || relPath.startsWith(`${dir}/`))
221
+ && (status === '??' || status === '!!' || relPath === `${dir}/state` || relPath.startsWith(`${dir}/state/`)))) {
222
+ continue;
223
+ }
224
+ // Ignored entries (`!!`) are named in the key, never read: the tree digest stays what it was.
225
+ if (status === '!!') {
226
+ ignoredPaths.push(relPath);
227
+ continue;
228
+ }
229
+ entries.push({ entry, relPath });
230
+ }
231
+
232
+ ignoredPaths.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
233
+ const links = readLinks ? linksDigest(gitRoot, { spawnSyncImpl, fsImpl }) : LINKS_SKIPPED;
234
+ return { gitRoot, entries, ignored: sha256(ignoredPaths.length === 0 ? 'none' : ignoredPaths.join('\0')), links };
235
+ }
236
+
237
+ function insideRoot(root , target ) {
238
+ const relative = path.relative(root, target);
239
+ return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
240
+ }
241
+
242
+ /**
243
+ * Digest of the OUT-OF-REPO targets of tracked symlinks (089 review C-3). One `git ls-files -s -z`;
244
+ * entries of mode 120000. A target inside the git root is covered by the tree digest or the
245
+ * ignored set and skipped. A target outside is a regular file of at most 1 MB (`target\0sha256`)
246
+ * or missing (`target\0absent`); a directory, another type, an unreadable or oversized target,
247
+ * more than 500 links or a git fault gives LINKS_UNKEYABLE (the check is not reused).
248
+ */
249
+ function linksDigest(gitRoot , { spawnSyncImpl = spawnSync, fsImpl = nodeFs } ) {
250
+ try {
251
+ const res = spawnSyncImpl('git', ['ls-files', '-s', '-z'], { cwd: gitRoot, encoding: 'utf8' });
252
+ if (res.status !== 0 || typeof res.stdout !== 'string') return LINKS_UNKEYABLE;
253
+ const links = new Set ();
254
+ for (const token of res.stdout.split('\0')) {
255
+ const match = /^120000 [0-9a-f]+ \d\t([\s\S]+)$/.exec(token);
256
+ if (match) links.add(match[1]);
257
+ }
258
+ if (links.size === 0) return sha256('none');
259
+ if (links.size > MAX_TRACKED_LINKS) return LINKS_UNKEYABLE;
260
+ const lines = [];
261
+ for (const rel of links) {
262
+ const full = path.resolve(gitRoot, rel);
263
+ let lstat ;
264
+ try { lstat = fsImpl.lstatSync(full); } catch (error ) {
265
+ if (error.code === 'ENOENT') continue; // removed: the status entry covers it
266
+ return LINKS_UNKEYABLE;
267
+ }
268
+ if (!lstat.isSymbolicLink()) continue; // replaced by a file: the status entry covers it
269
+ let real ;
270
+ try { real = fsImpl.realpathSync(full); } catch (error ) {
271
+ if (error.code !== 'ENOENT') return LINKS_UNKEYABLE;
272
+ const lexical = path.resolve(path.dirname(full), fsImpl.readlinkSync(full));
273
+ if (!insideRoot(gitRoot, lexical)) lines.push(`${lexical}\0absent`);
274
+ continue;
275
+ }
276
+ if (insideRoot(gitRoot, real)) continue;
277
+ const stat = fsImpl.statSync(real);
278
+ if (!stat.isFile() || stat.size > MAX_INPUT_BYTES) return LINKS_UNKEYABLE;
279
+ const content = fsImpl.readFileSync(real);
280
+ if (content.length > MAX_INPUT_BYTES) return LINKS_UNKEYABLE;
281
+ lines.push(`${real}\0${crypto.createHash('sha256').update(content).digest('hex')}`);
282
+ }
283
+ if (lines.length === 0) return sha256('none');
284
+ lines.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
285
+ return sha256(lines.join('\n'));
286
+ } catch {
287
+ return LINKS_UNKEYABLE;
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Digest of the modified/untracked entries: reads each file's bytes. Null when more than 500
293
+ * files or 50 MB are dirty, or an entry is a symlink, a non-file or unreadable.
294
+ */
295
+ function hashEntries(
296
+ gitRoot ,
297
+ entries ,
298
+ fsImpl ,
299
+ ) {
300
+ if (entries.length === 0) {
301
+ return crypto.createHash('sha256').update('clean', 'utf8').digest('hex');
302
+ }
303
+
304
+ if (entries.length > MAX_UNCOMMITTED_FILES) {
305
+ return null; // More than 500 uncommitted files -> unkeyable
306
+ }
307
+
308
+ // Sort entries deterministically
309
+ entries.sort((a, b) => (a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0));
310
+
311
+ const hasher = crypto.createHash('sha256');
312
+ let totalBytes = 0;
313
+
314
+ for (const item of entries) {
315
+ const fullPath = path.resolve(gitRoot, item.relPath);
316
+ let fileHash = '';
317
+ try {
318
+ if (typeof fsImpl.lstatSync === 'function' && fsImpl.lstatSync(fullPath).isSymbolicLink()) return null;
319
+ const stat = fsImpl.statSync(fullPath);
320
+ if (!stat.isFile()) return null; // directory/submodule contents are not represented by porcelain
321
+ if (stat.isFile()) {
322
+ totalBytes += stat.size;
323
+ if (totalBytes > MAX_UNCOMMITTED_BYTES) {
324
+ return null; // More than 50 MB total dirty content -> unkeyable
325
+ }
326
+ const content = fsImpl.readFileSync(fullPath);
327
+ fileHash = crypto.createHash('sha256').update(content).digest('hex');
328
+ }
329
+ } catch (error ) {
330
+ // A deletion has no content; an unreadable live file cannot be safely keyed.
331
+ if (error.code !== 'ENOENT' || !item.entry.slice(0, 2).includes('D')) return null;
332
+ }
333
+ hasher.update(item.entry, 'utf8');
334
+ hasher.update('\0', 'utf8');
335
+ hasher.update(fileHash, 'utf8');
336
+ hasher.update('\0', 'utf8');
337
+ }
338
+
339
+ return hasher.digest('hex');
340
+ }
341
+
342
+ /**
343
+ * Computes the tree digest and the ignored-entry digest from ONE git status call.
344
+ * Returns null if not in a git repository, or if > 500 files or > 50 MB are dirty.
345
+ * @param {string} root
346
+ * @param {{ spawnSyncImpl?: typeof spawnSync, fsImpl?: typeof nodeFs, configDir?: string }} [options]
347
+ * @returns {TreeState | null}
348
+ */
349
+ function treeState(root , options = {}) {
350
+ try {
351
+ const status = readStatus(root, options);
352
+ if (!status) return null;
353
+ const tree = hashEntries(status.gitRoot, status.entries, options.fsImpl || nodeFs);
354
+ return tree === null ? null : { tree, ignored: status.ignored, gitRoot: status.gitRoot, links: status.links };
355
+ } catch {
356
+ return null;
357
+ }
358
+ }
359
+
360
+ /**
361
+ * IC-001 compatibility seam for tests; the runner uses checkKeyV2/treeState.
362
+ * Computes a digest of all modified and untracked files in the working tree.
363
+ * Returns null if not in a git repository, or if > 500 files or > 50 MB are dirty.
364
+ * Values for trees without ignored entries are those of payload version 1.
365
+ * @param {string} root
366
+ * @param {{ spawnSyncImpl?: typeof spawnSync, fsImpl?: typeof nodeFs, configDir?: string }} [options]
367
+ * @returns {string | null}
368
+ */
369
+ function treeDigest(root , options = {}) {
370
+ return treeState(root, options)?.tree ?? null;
371
+ }
372
+
373
+ /**
374
+ * Digest of the declared ignored inputs (089 IC-001, DA-1): `path\0sha256(bytes)` or
375
+ * `path\0absent` per path, in registry order, relative to the git root. A symlink, a
376
+ * non-file, a read fault other than ENOENT or a file over 1 MB makes the key null.
377
+ * @returns {string | null}
378
+ */
379
+ function inputsDigest(gitRoot , inputs , fsImpl = nodeFs) {
380
+ try {
381
+ const lines = [];
382
+ for (const rel of inputs) {
383
+ const full = path.resolve(gitRoot, rel);
384
+ let stat ;
385
+ try { stat = fsImpl.lstatSync(full); }
386
+ catch (error ) {
387
+ if (error.code === 'ENOENT') { lines.push(`${rel}\0absent`); continue; }
388
+ return null;
389
+ }
390
+ if (stat.isSymbolicLink() || !stat.isFile() || stat.size > MAX_INPUT_BYTES) return null;
391
+ const content = fsImpl.readFileSync(full);
392
+ if (content.length > MAX_INPUT_BYTES) return null;
393
+ lines.push(`${rel}\0${crypto.createHash('sha256').update(content).digest('hex')}`);
394
+ }
395
+ return sha256(lines.join('\n'));
396
+ } catch {
397
+ return null;
398
+ }
399
+ }
400
+
401
+ /**
402
+ * First word of a shell command, past leading NAME=value assignments and quotes, and whether the
403
+ * parser can trust it (089 review C-4). Not reusable: the first word or a leading assignment holds
404
+ * a quote, a backslash, `$` or a backtick (the shell expands or joins what the whitespace split
405
+ * cut), a leading assignment sets PATH (the tool resolves on another PATH), or the first word
406
+ * starts with `~` or holds `*`, `?` or `[` (the shell expands it). `word` is still the key's tool.
407
+ */
408
+ function firstShellWord(command ) {
409
+ let reusable = true;
410
+ for (const word of command.trim().split(/\s+/)) {
411
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(word)) {
412
+ if (/["'\\$`]/.test(word) || word.startsWith('PATH=')) reusable = false;
413
+ continue;
414
+ }
415
+ if (/["'\\$`*?[]/.test(word) || word.startsWith('~')) reusable = false;
416
+ return { word: word.replace(/^["']+|["']+$/g, ''), reusable };
417
+ }
418
+ return { word: '', reusable };
419
+ }
420
+
421
+ /**
422
+ * Identity of one program: its first match on PATH (PATHEXT on win32), realpath, size,
423
+ * mtimeMs and inode, or `absent`. A word that is a shell keyword or contains a slash is
424
+ * taken as a path relative to cwd. No process is spawned.
425
+ */
426
+ function toolIdentity(
427
+ word ,
428
+ { cwd, env, fsImpl, platform } ,
429
+ ) {
430
+ if (word === '') return `${word}\0absent`;
431
+ const win = platform === 'win32';
432
+ const direct = SHELL_KEYWORDS.has(word) || word.includes('/') || (win && word.includes('\\'));
433
+ const extensions = win
434
+ ? ['', ...String(env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)]
435
+ : [''];
436
+ const candidates = [];
437
+ if (direct) {
438
+ for (const ext of extensions) candidates.push(path.resolve(cwd, word + ext));
439
+ } else if (typeof env.PATH === 'string') {
440
+ for (const dir of env.PATH.split(win ? ';' : ':')) {
441
+ for (const ext of extensions) candidates.push(path.resolve(cwd, dir === '' ? '.' : dir, word + ext));
442
+ }
443
+ }
444
+ for (const candidate of candidates) {
445
+ let isFile = false;
446
+ try { isFile = fsImpl.statSync(candidate).isFile(); } catch { isFile = false; }
447
+ if (!isFile) continue;
448
+ const real = fsImpl.realpathSync(candidate);
449
+ const stat = fsImpl.statSync(real);
450
+ return [word, real, stat.size, stat.mtimeMs, stat.ino].join('\0');
451
+ }
452
+ return `${word}\0absent`;
453
+ }
454
+
455
+ /**
456
+ * Digest of the identity of `node` and of the command's first word (089 IC-001, DA-2).
457
+ * @returns {string}
458
+ */
459
+ function toolDigest(
460
+ command ,
461
+ { cwd, env = process.env, fsImpl = nodeFs, platform = process.platform }
462
+ ,
463
+ ) {
464
+ const io = { cwd, env, fsImpl, platform };
465
+ return sha256([toolIdentity('node', io), toolIdentity(firstShellWord(command).word, io)].join('\n'));
466
+ }
467
+
468
+ /** The version 2 payload (089 IC-001); the order of the entries is part of the key's identity. */
469
+ function payloadOf(parts
470
+
471
+
472
+ ) {
473
+ const { command, timeoutMs, realCwd, head, tree, ignoredDig, inputsDig, linksDig, toolDig, envDig } = parts;
474
+ const payload = [
475
+ PAYLOAD_VERSION,
476
+ command,
477
+ timeoutMs,
478
+ realCwd,
479
+ head,
480
+ tree,
481
+ ignoredDig,
482
+ inputsDig,
483
+ linksDig,
484
+ toolDig,
485
+ process.version,
486
+ process.platform,
487
+ (process ).arch,
488
+ envDig,
489
+ ];
490
+ return payload;
491
+ }
492
+
493
+
494
+
495
+
496
+
497
+
498
+
499
+
500
+
501
+
502
+
503
+
504
+
505
+
506
+
507
+
508
+
509
+
510
+
511
+
512
+ /**
513
+ * Computes the version 2 check key and the full payload digest for a command run in cwd
514
+ * (089 IC-001). Returns null if unkeyable; any thrown error is null.
515
+ * @param {Object} input
516
+ * @param {string} input.command
517
+ * @param {number} input.timeoutMs
518
+ * @param {string} input.cwd
519
+ * @param {string | null} [input.head] HEAD commit; resolved from cwd if omitted
520
+ * @param {string | null} [input.tree] treeDigest; resolved from cwd if omitted
521
+ * @param {string | null} [input.ignored] ignored-entry digest from treeState; resolved with one
522
+ * status call when omitted (also resolves `tree` when that is omitted too)
523
+ * @param {Record<string, string | undefined>} [input.env=process.env]
524
+ * @returns {{ key: string, payloadDigest: string | null } | null} `payloadDigest` is null when the
525
+ * check is reuse-unkeyable (unparseable first word, unkeyable or unread tracked symlinks); the
526
+ * `key` is still returned for in-flight sharing and telemetry.
527
+ */
528
+ function checkKeyV2({
529
+ command,
530
+ timeoutMs,
531
+ cwd,
532
+ head,
533
+ tree,
534
+ ignored,
535
+ gitRoot,
536
+ links,
537
+ configDir,
538
+ env = process.env,
539
+ fsImpl = nodeFs,
540
+ spawnSyncImpl = spawnSync,
541
+ platform = process.platform,
542
+ } ) {
543
+ try {
544
+ if (typeof command !== 'string' || !Number.isFinite(timeoutMs) || typeof cwd !== 'string') {
545
+ return null;
546
+ }
547
+
548
+ const realCwd = fsImpl.realpathSync(cwd);
549
+
550
+ let resolvedHead = head;
551
+ if (resolvedHead === undefined) {
552
+ resolvedHead = headCommit(realCwd, { spawnSyncImpl });
553
+ }
554
+
555
+ let resolvedTree = tree;
556
+ let resolvedIgnored = ignored;
557
+ let resolvedRoot = gitRoot;
558
+ let resolvedLinks = links;
559
+ if (resolvedTree === undefined) {
560
+ // Nothing precomputed: one status call supplies the tree, the ignored set and the root.
561
+ const state = treeState(realCwd, { spawnSyncImpl, fsImpl, configDir });
562
+ if (!state) return null;
563
+ resolvedTree = state.tree;
564
+ if (resolvedIgnored === undefined) resolvedIgnored = state.ignored;
565
+ if (resolvedRoot === undefined) resolvedRoot = state.gitRoot;
566
+ if (resolvedLinks === undefined) resolvedLinks = state.links;
567
+ } else if (resolvedIgnored === undefined || resolvedRoot === undefined || resolvedLinks === undefined) {
568
+ // A tree without its ignored set: one status call, without reading any dirty file again.
569
+ // The caller already read the tree, so a fault here means this directory is not a git
570
+ // checkout (a test seam); the key then carries IGNORED_UNAVAILABLE, which no healthy
571
+ // digest equals, and the declared inputs are read from cwd.
572
+ let status = null;
573
+ try { status = readStatus(realCwd, { spawnSyncImpl, fsImpl, configDir }); } catch { status = null; }
574
+ if (resolvedIgnored === undefined) resolvedIgnored = status ? status.ignored : IGNORED_UNAVAILABLE;
575
+ if (resolvedRoot === undefined) resolvedRoot = status ? status.gitRoot : realCwd;
576
+ if (resolvedLinks === undefined) resolvedLinks = status ? status.links : LINKS_UNKEYABLE;
577
+ }
578
+
579
+ if (!resolvedHead || !resolvedTree || !resolvedIgnored || !resolvedRoot) {
580
+ return null;
581
+ }
582
+ // A caller that supplied tree/ignored/root without links gets one read; null means not read.
583
+ const linksDig = typeof resolvedLinks === 'string' ? resolvedLinks : LINKS_SKIPPED;
584
+
585
+ const policy = keyPolicy();
586
+ const envDig = envDigest(env, policy.envExclude);
587
+ const ignoredDig = resolvedIgnored;
588
+ const inputsDig = inputsDigest(resolvedRoot, policy.ignoredInputs, fsImpl);
589
+ if (inputsDig === null) return null;
590
+ const toolDig = toolDigest(command, { cwd: realCwd, env, fsImpl, platform });
591
+ const payload = payloadOf({
592
+ command, timeoutMs, realCwd, head: resolvedHead, tree: resolvedTree,
593
+ ignoredDig, inputsDig, linksDig, toolDig, envDig,
594
+ });
595
+
596
+ const payloadDigest = sha256(JSON.stringify(payload));
597
+ // Reuse-unkeyable (089 review C-3, C-4): the key is kept, the stored-PASS identity is not.
598
+ const reusable = linksDig !== LINKS_UNKEYABLE && linksDig !== LINKS_SKIPPED
599
+ && firstShellWord(command).reusable;
600
+ return { key: payloadDigest.slice(0, 16), payloadDigest: reusable ? payloadDigest : null };
601
+ } catch {
602
+ return null;
603
+ }
604
+ }
605
+
606
+ /**
607
+ * IC-001 compatibility seam for tests; the runner uses checkKeyV2/treeState.
608
+ * Computes checkKey for a command run in cwd.
609
+ * Returns 16 hex chars, or null if unkeyable.
610
+ * @param {Object} input see checkKeyV2
611
+ * @returns {string | null}
612
+ */
613
+ function checkKey(input ) {
614
+ return checkKeyV2(input)?.key ?? null;
615
+ }
616
+
617
+ // `checkKey` and `treeDigest` are IC-001 compatibility seams for tests; the runner uses
618
+ // checkKeyV2/treeState.
619
+ module.exports = {
620
+ envDigest,
621
+ headCommit,
622
+ treeDigest,
623
+ treeState,
624
+ inputsDigest,
625
+ toolDigest,
626
+ checkKey,
627
+ checkKeyV2,
628
+ };