@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,164 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * The DoFlow-written package state file (122 IC-009, PN-1): `~/.doflow/packages/state.json`, atomic writes,
5
+ * owner-only mode 0600. It holds narrowing facts only (`revoked`, `suspended`, `audit`, `removed`), never a
6
+ * widening one. A missing file is empty state; a malformed one counts as every package revoked (`unreadable`).
7
+ * The writers read, change and rewrite the file: a caller that writes takes the scope's `run.lock` first (IC-007).
8
+ */
9
+
10
+ const fs = require('node:fs');
11
+ const os = require('node:os');
12
+ const path = require('node:path');
13
+ const { atomicJsonWrite } = require('../../state/index.js');
14
+
15
+ const STATE_VERSION = 1;
16
+ const AUDIT_MAX = 64;
17
+ const TOP_KEYS = ['state', 'revoked', 'suspended', 'audit', 'removed'];
18
+ const DIGEST = /^[0-9a-f]{64}$/;
19
+
20
+ const isObject = (value ) => value !== null && typeof value === 'object' && !Array.isArray(value);
21
+ const isText = (value ) => typeof value === 'string' && value !== '';
22
+ const isTime = (value ) => isText(value) && !Number.isNaN(Date.parse(value));
23
+
24
+ const SHAPES = {
25
+ revoked: (r) => isObject(r) && isText(r.package) && DIGEST.test(r.contentDigest) && isTime(r.at),
26
+ suspended: (s) => isObject(s) && isText(s.package) && isText(s.reason) && isTime(s.since) && typeof s.projected === 'boolean',
27
+ audit: (a) => isObject(a) && isTime(a.at) && isText(a.verb) && isText(a.package) && isText(a.result),
28
+ removed: (r) => isObject(r) && isText(r.package) && Array.isArray(r.qualifiedIds) && r.qualifiedIds.every(isText) && isTime(r.at),
29
+ };
30
+
31
+ const emptyState = () => ({ state: STATE_VERSION, revoked: [], suspended: [], audit: [], removed: [] });
32
+
33
+ /** `~/.doflow/packages/state.json` (home from `homeDir`, else `os.homedir()`). */
34
+ const statePath = (homeDir ) => path.join(homeDir ?? os.homedir(), '.doflow', 'packages', 'state.json');
35
+
36
+ function stateProblem(value ) {
37
+ if (!isObject(value)) return 'not a JSON object';
38
+ if (Object.keys(value).some((key) => !TOP_KEYS.includes(key))) return 'unknown key';
39
+ if (value.state !== STATE_VERSION) return 'unsupported state version';
40
+ for (const [key, valid] of Object.entries(SHAPES)) {
41
+ if (value[key] !== undefined && !(Array.isArray(value[key]) && value[key].every(valid))) return `bad ${key}`;
42
+ }
43
+ return null;
44
+ }
45
+
46
+ /**
47
+ * Read the state. Returns `{ path, exists, unreadable, state, findings }`. A missing file: empty `state`, no
48
+ * finding. A malformed file: `unreadable` true, empty `state` and one `state-unreadable` finding; callers treat
49
+ * `unreadable` as every package revoked (`grantRevoked`). Never throws.
50
+ */
51
+ function readState({ homeDir, fsImpl = fs } = {}) {
52
+ const file = statePath(homeDir);
53
+ const result = { path: file, exists: false, unreadable: false, state: emptyState(), findings: [] };
54
+ let text ;
55
+ try {
56
+ text = String(fsImpl.readFileSync(file, 'utf8'));
57
+ result.exists = true;
58
+ } catch (error ) {
59
+ if (error?.code === 'ENOENT') return result;
60
+ text = '';
61
+ result.exists = true;
62
+ }
63
+ let problem ;
64
+ let parsed ;
65
+ try { parsed = JSON.parse(text); problem = stateProblem(parsed); } catch { problem = 'not valid JSON'; }
66
+ if (problem !== null) {
67
+ result.unreadable = true;
68
+ result.findings.push({ package: null, field: 'state.json', code: 'state-unreadable', severity: 'error',
69
+ message: `the package state file is unreadable (${problem}); every package counts as revoked until verify --repair`, allowed: 'a state.json DoFlow wrote' });
70
+ return result;
71
+ }
72
+ result.state = { ...emptyState(), ...parsed, audit: (parsed.audit ?? []).slice(-AUDIT_MAX) };
73
+ return result;
74
+ }
75
+
76
+ /** Write the state atomically with mode 0600 (the temp file is created 0600 and renamed over the target). */
77
+ function writeState(state , { homeDir, fsImpl = fs } = {}) {
78
+ // The file is created 0600: `atomicJsonWrite` (the declared writer) gets an fs whose file writes carry that mode.
79
+ const owned = new Proxy(fsImpl, {
80
+ get: (target , name ) => (name === 'writeFileSync'
81
+ ? (file , data , options ) => target[name](file, data, { ...options, mode: 0o600 })
82
+ : (typeof target[name] === 'function' ? target[name].bind(target) : target[name])),
83
+ });
84
+ return atomicJsonWrite(statePath(homeDir), { ...state, audit: state.audit.slice(-AUDIT_MAX) }, { fsImpl: owned });
85
+ }
86
+
87
+ /** Read, apply `change` to the state, write. A malformed file is replaced only through `repairState`, so this refuses it. */
88
+ function update(options , change ) {
89
+ const read = readState(options);
90
+ if (read.unreadable) throw new Error('the package state file is unreadable; run verify --repair first');
91
+ change(read.state);
92
+ writeState(read.state, options);
93
+ return read.state;
94
+ }
95
+
96
+ /** Revoke trust for `packageId` at `contentDigest`: a trust grant counts only when its `at` is later than `at`. */
97
+ function recordRevocation(options , { packageId, contentDigest, at = new Date().toISOString() } ) {
98
+ return update(options, (state) => {
99
+ state.revoked = state.revoked.filter((r ) => !(r.package === packageId && r.contentDigest === contentDigest));
100
+ state.revoked.push({ package: packageId, contentDigest, at });
101
+ });
102
+ }
103
+
104
+ /** The last detected suspension for `packageId` (one per package); `projected` says its files are still on disk. */
105
+ function recordSuspension(options , { packageId, reason, projected = false, since = new Date().toISOString() } ) {
106
+ return update(options, (state) => {
107
+ state.suspended = state.suspended.filter((s ) => s.package !== packageId);
108
+ state.suspended.push({ package: packageId, reason, since, projected });
109
+ });
110
+ }
111
+
112
+ /** Drop the suspension record of `packageId` (it is intact again). */
113
+ function clearSuspension(options , { packageId } ) {
114
+ return update(options, (state) => { state.suspended = state.suspended.filter((s ) => s.package !== packageId); });
115
+ }
116
+
117
+ /** PN-1 tombstone: `remove` names the package's worker qualified ids; one entry per package, the latest. */
118
+ function recordTombstone(options , { packageId, qualifiedIds, at = new Date().toISOString() } ) {
119
+ return update(options, (state) => {
120
+ state.removed = state.removed.filter((r ) => r.package !== packageId);
121
+ state.removed.push({ package: packageId, qualifiedIds: [...qualifiedIds], at });
122
+ });
123
+ }
124
+
125
+ /** Append one package verb result; only the latest 64 entries are kept, newest last. */
126
+ function appendAudit(options , { verb, packageId, result, at = new Date().toISOString() } ) {
127
+ return update(options, (state) => { state.audit.push({ at, verb, package: packageId, result }); });
128
+ }
129
+
130
+ /**
131
+ * `verify --repair` (IC-009): rewrite the state with one revocation per grant line present now (`grantEntries` is
132
+ * `readGrants(...).entries`; a line with a package and a content digest names its revocation), `at` the repair
133
+ * time. Suspensions, tombstones and audit are dropped with the unreadable file. Repair never widens: every
134
+ * grant read before `at` stays void until the person adds a line printed after it. A `fetch` line names no
135
+ * package, so it has no revocation record to hold; it widens network access only, and its result still needs trust.
136
+ */
137
+ function repairState(options , { grantEntries = [], at = new Date().toISOString() } = {}) {
138
+ const state = emptyState();
139
+ for (const entry of grantEntries) {
140
+ if (!isText(entry?.package) || !DIGEST.test(entry?.contentDigest)) continue;
141
+ if (!state.revoked.some((r ) => r.package === entry.package && r.contentDigest === entry.contentDigest)) {
142
+ state.revoked.push({ package: entry.package, contentDigest: entry.contentDigest, at });
143
+ }
144
+ }
145
+ state.audit.push({ at, verb: 'verify --repair', package: '*', result: `ok: ${state.revoked.length} revoked` });
146
+ writeState(state, options);
147
+ return state;
148
+ }
149
+
150
+ /**
151
+ * Whether a `trust` or `exempt` grant is revoked by `read` (a `readState` result): always when the state is
152
+ * unreadable; else when a revocation names the same package and content digest and the grant's `at` is not
153
+ * later than the revocation's.
154
+ */
155
+ function grantRevoked(read , grant ) {
156
+ if (read.unreadable) return true;
157
+ return read.state.revoked.some((r ) => r.package === grant.package && r.contentDigest === grant.contentDigest
158
+ && !(Date.parse(grant.at) > Date.parse(r.at)));
159
+ }
160
+
161
+ module.exports = {
162
+ statePath, readState, writeState, recordRevocation, recordSuspension, clearSuspension, recordTombstone,
163
+ appendAudit, repairState, grantRevoked, AUDIT_MAX,
164
+ };
@@ -0,0 +1,449 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Plans, the package store and the apply step (122 IC-006, IC-007, IC-012 `add` and `update` for a local
5
+ * source, IC-024 storage). A plan is a file under `~/.doflow/packages/plans/` that changes no record and
6
+ * activates nothing; applying one takes the global scope's `run.lock`, re-snapshots the source, recomputes both
7
+ * digests, stages a copy, re-hashes it, renames it into `store/<id>/<contentDigest first 16 hex>/`, writes the
8
+ * `.installed-from.json` marker beside the tree and writes the lock row last. A failure after the rename removes
9
+ * the entry before the lock is touched. Nothing here writes the person grant file.
10
+ *
11
+ * The plan's `projection` (IC-006) names, per harness of the global lock's `targets`, the skills and agents the
12
+ * package would project there and the listing bytes they add to that harness's always-loaded context; the
13
+ * projection itself is the lifecycle's (IC-017, `src/lifecycle/index.js`), which asks `projectionReach` here which
14
+ * harness can take which point. Remote plans retain a fetched snapshot and apply it without fetching again.
15
+ */
16
+
17
+ const fs = require('node:fs');
18
+ const os = require('node:os');
19
+ const path = require('node:path');
20
+ const crypto = require('node:crypto');
21
+ const { REPO_ROOT } = require('../../helper/repo-root.js');
22
+ const { isPackageId, projectedName } = require('../../helper/package-ids.js');
23
+ const { checkPackage, MANIFEST_FILE } = require('./manifest.js');
24
+ const { loadPoints } = require('./points.js');
25
+ const { walkTree, digestFiles, canonicalJson } = require('./digest.js');
26
+ const { hostVersion } = require('./view.js');
27
+ const { readLock, writeLock, defaultLock, lockPackages } = require('../../state/lockfile.js');
28
+ const { atomicJsonWrite } = require('../../state/index.js');
29
+ const { acquireRunLock } = require('../../state/run-lock.js');
30
+ const { pinSource, fetchSource } = require('./source.js');
31
+ const { pinnedSourceFindings } = require('./index-file.js');
32
+
33
+ const PLAN_VERSION = 1;
34
+ const PLAN_ID = /^[0-9a-f]{16}$/;
35
+ const DIGEST = /^[0-9a-f]{64}$/;
36
+ const MARKER_SUFFIX = '.installed-from.json';
37
+ const TIER_ORDER = ['inert', 'instruction', 'executable'];
38
+
39
+ const sha256 = (data ) => crypto.createHash('sha256').update(data).digest('hex');
40
+ const isObject = (value ) => value !== null && typeof value === 'object' && !Array.isArray(value);
41
+
42
+ /** `~/.doflow/packages` and its parts for a home directory. */
43
+ function packagePaths(homeDir ) {
44
+ const base = path.join(homeDir, '.doflow', 'packages');
45
+ return { base, plans: path.join(base, 'plans'), store: path.join(base, 'store'), fetched: path.join(base, 'fetched') };
46
+ }
47
+
48
+ function finding(field , code , message , allowed , pkg = null) {
49
+ return { package: pkg, field, code, severity: 'error', message, allowed };
50
+ }
51
+
52
+ /** The ISO 8601 UTC time of a grant line: whole seconds, as IC-008 prints it. */
53
+ const grantTime = (when ) => new Date(when).toISOString().replace(/\.\d+Z$/, 'Z');
54
+
55
+ /**
56
+ * Check a local package directory and describe what it would add. `{ ok: false, findings }` when the checker
57
+ * reports an error; otherwise the manifest, both digests, the inventory (`point`, `qualifiedId`, `tier`, sorted)
58
+ * and the trust policy version the digests were taken under.
59
+ */
60
+ function analyze(dir , { repoRoot = REPO_ROOT, fsImpl = fs } = {}) {
61
+ const loaded = loadPoints({ repoRoot });
62
+ const checked = checkPackage(dir, { repoRoot, hostVersion: hostVersion(repoRoot), bundled: [], fsImpl, ...(loaded.ok ? { pointsRegistry: loaded.registry } : {}) });
63
+ if (!checked.ok) return { ok: false, findings: checked.findings };
64
+ const manifest = JSON.parse(String(fsImpl.readFileSync(path.join(dir, MANIFEST_FILE), 'utf8')));
65
+ const tiers = Object.fromEntries(loaded.registry.points.map((p ) => [p.id, p.tier]));
66
+ const inventory = (manifest.contributions ?? [])
67
+ .map((c ) => ({ point: c.point, qualifiedId: `${manifest.id}.${c.id}`, tier: tiers[c.point] }))
68
+ .sort((a , b ) => (a.point < b.point ? -1 : a.point > b.point ? 1 : a.qualifiedId < b.qualifiedId ? -1 : a.qualifiedId > b.qualifiedId ? 1 : 0));
69
+ const programs = [...(manifest.programs ?? [])].sort((a , b ) => (String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0));
70
+ return {
71
+ ok: true, findings: checked.findings, manifest, inventory, programs, contentDigest: checked.contentDigest, capabilityDigest: checked.capabilityDigest,
72
+ trustPolicyVersion: loaded.registry.trustPolicyVersion, limits: loaded.registry.limits,
73
+ };
74
+ }
75
+
76
+ // ── projection reach and the plan's `projection` (IC-017, IC-006) ───────────────────────────────────
77
+
78
+ /** The built-in asset whose per-harness projection (renderer, transform, layout, destination) a package
79
+ * contribution of each instruction point borrows, so it lands where and how DoFlow's own skills and agents do. */
80
+ const BORROWED_ASSET = { skill: 'skills.doflow', agent: 'agents.shared' };
81
+ const CAPABILITY = { skill: 'skills', agent: 'agents' };
82
+ const INSTRUCTION_POINTS = Object.keys(BORROWED_ASSET);
83
+
84
+ /**
85
+ * Whether `harnessId` can take a package contribution of `point` (IC-017 harness reach): `{ ok: true, projection,
86
+ * nativeDir }` with the borrowed asset's projection for that harness, else `{ ok: false, reason }`. The capability
87
+ * is the harness's `skills` or `agents` capability in `harnesses.json`; `unavailable` is the gap NFR-003 names.
88
+ */
89
+ function projectionReach(registry , harnessId , point ) {
90
+ const harness = registry.harnesses.find((h ) => h.id === harnessId);
91
+ if (!harness) return { ok: false, reason: 'it is not a harness this DoFlow knows' };
92
+ const name = CAPABILITY[point];
93
+ if (!name) return { ok: false, reason: `${point} is not an instruction point` };
94
+ const capability = harness.capabilities?.[name];
95
+ if (!capability || capability.status === 'unavailable') return { ok: false, reason: `its ${name} capability is unavailable` };
96
+ const asset = registry.assets.find((a ) => a.id === BORROWED_ASSET[point]);
97
+ const projection = asset?.projection?.[harnessId];
98
+ if (!projection) return { ok: false, reason: `DoFlow projects no ${name} into it (capability ${capability.status})` };
99
+ return { ok: true, projection, nativeDir: asset.nativeDir?.[harnessId] ?? '' };
100
+ }
101
+
102
+ /** The `name: value` pairs of a Markdown file's frontmatter, quotes stripped; empty when there is none. */
103
+ function frontmatterValues(text ) {
104
+ const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
105
+ const values = {};
106
+ for (const line of match ? match[1].split(/\r?\n/) : []) {
107
+ const item = /^([A-Za-z][A-Za-z0-9_-]*):[ \t]*(.*)$/.exec(line);
108
+ if (item) values[item[1]] = item[2].trim().replace(/^"(.*)"$/, '$1');
109
+ }
110
+ return values;
111
+ }
112
+
113
+ /** The bytes a contribution adds to a harness's always-loaded listing: its name and description, plus a skill's
114
+ * `when_to_use` (what G22 counts for DoFlow's own skills). 0 when the file cannot be read. */
115
+ function listingBytes(file , fsImpl ) {
116
+ try {
117
+ const values = frontmatterValues(String(fsImpl.readFileSync(file, 'utf8')));
118
+ return ['name', 'description', 'when_to_use'].reduce((sum, key) => sum + Buffer.byteLength(values[key] ?? '', 'utf8'), 0);
119
+ } catch { return 0; }
120
+ }
121
+
122
+ /** The instruction contributions of a manifest: `{ point, qualifiedId, name, path }`, `path` relative to the package root. */
123
+ function instructionEntries(manifest ) {
124
+ return (Array.isArray(manifest?.contributions) ? manifest.contributions : [])
125
+ .filter((c ) => c && INSTRUCTION_POINTS.includes(c.point) && c.version === 1 && typeof c.id === 'string' && typeof (c.point === 'skill' ? c.dir : c.file) === 'string')
126
+ .map((c ) => ({ point: c.point, qualifiedId: `${manifest.id}.${c.id}`, name: projectedName(`${manifest.id}.${c.id}`) ?? `${manifest.id}-${c.id}`, path: c.point === 'skill' ? c.dir : c.file }));
127
+ }
128
+
129
+ /**
130
+ * The plan's `projection` (IC-006): per harness of the global lock's `targets`, the projected names by point and
131
+ * the listing bytes they add there; a contribution the harness cannot take is named under `unreachable` and in
132
+ * `warnings`. `{}` for a package with no skill or agent, or when no harness is installed at global scope.
133
+ */
134
+ function projectionPreview(dir , manifest , lock , { repoRoot = REPO_ROOT, registry, fsImpl = fs } = {}) {
135
+ const entries = instructionEntries(manifest);
136
+ const harnesses = (lock?.targets ?? []).map((t ) => t?.harness).filter((h ) => typeof h === 'string');
137
+ const projection = {};
138
+ const warnings = [];
139
+ if (entries.length === 0 || harnesses.length === 0) return { projection, warnings };
140
+ const reg = registry ?? require('../../registry/index.js').loadRegistry({ repoRoot });
141
+ for (const harness of harnesses) {
142
+ const row = { listingBytes: 0 };
143
+ for (const entry of entries) {
144
+ const reach = projectionReach(reg, harness, entry.point);
145
+ if (!reach.ok) {
146
+ (row.unreachable ??= []).push(entry.name);
147
+ warnings.push(`${harness} cannot take ${entry.point} ${entry.qualifiedId}: ${reach.reason}`);
148
+ continue;
149
+ }
150
+ (row[entry.point] ??= []).push(entry.name);
151
+ row.listingBytes += listingBytes(path.join(dir, ...entry.path.split('/'), ...(entry.point === 'skill' ? ['SKILL.md'] : [])), fsImpl);
152
+ }
153
+ projection[harness] = row;
154
+ }
155
+ return { projection, warnings };
156
+ }
157
+
158
+ /** The highest tier in an inventory (`inert` for none). */
159
+ const tierMax = (inventory ) => inventory.reduce((top , c ) => (TIER_ORDER.indexOf(c.tier) > TIER_ORDER.indexOf(top) ? c.tier : top), 'inert');
160
+
161
+ /**
162
+ * The plan id: the first 16 hex of sha256(content NUL capability NUL canonical source) (IC-006). The storage
163
+ * engine reads a name that starts with 13 decimal digits as a millisecond timestamp, which would let the
164
+ * plans store prune a fresh plan at once (about 1 id in 450), so such a candidate is rerolled by hashing
165
+ * the same text with `NUL <n>` appended, n = 1, 2, ... The result stays 16 hex and is still a function of the
166
+ * inputs. `hash` is a test seam.
167
+ */
168
+ function planIdFor(text , hash = sha256) {
169
+ for (let n = 0; ; n += 1) {
170
+ const candidate = hash(n === 0 ? text : `${text}\0${n}`).slice(0, 16);
171
+ if (!/^[0-9]{13}/.test(candidate)) return candidate;
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Check a directory and write its plan. Local calls write only the plan. Remote calls supply a pinnedSource
177
+ * and move their checked staging tree into fetched/<planId> before publishing the plan.
178
+ */
179
+ function planPackage({ action, source, id, pinnedSource, expected, homeDir = os.homedir(), repoRoot = REPO_ROOT, now = Date.now(), hash, registry, fsImpl = fs } ) {
180
+ if (pinnedSource) {
181
+ const findings = pinnedSourceFindings(pinnedSource);
182
+ if (findings.length) return { ok: false, findings };
183
+ }
184
+ const lock = readLock({ scope: 'global', homeDir }, { fsImpl });
185
+ const rows = lockPackages(lock);
186
+ let sourcePath = source;
187
+ if (action === 'update') {
188
+ const row = rows.find((r ) => r.id === id);
189
+ if (!row) return { ok: false, findings: [finding('id', 'bad-id', `no package with id '${id}' is recorded`, 'the id of a recorded package', id)] };
190
+ if (row.source?.type !== 'local') return { ok: false, findings: [finding('source', 'bad-type', `package '${id}' has a remote source; remove it and use add --source or add --index with a new spec`, 'a local source for update, or a new add after remove', id)] };
191
+ sourcePath = row.source.path;
192
+ }
193
+ const analysis = analyze(sourcePath, { repoRoot, fsImpl });
194
+ if (!analysis.ok) return { ok: false, findings: analysis.findings };
195
+ const { manifest } = analysis;
196
+ if (expected && (manifest.id !== expected.id || manifest.version !== expected.version)) {
197
+ return { ok: false, findings: [finding('source', 'bad-id', `fetched package ${manifest.id} ${manifest.version} does not match index selection ${expected.id} ${expected.version}`, expected)] };
198
+ }
199
+ if (action === 'add' && rows.some((r ) => r.id === manifest.id)) {
200
+ return { ok: false, findings: [finding('id', 'collision-package', `package '${manifest.id}' is already recorded; use update`, 'an id no recorded package uses', manifest.id)] };
201
+ }
202
+ if (action === 'update' && manifest.id !== id) {
203
+ return { ok: false, findings: [finding('id', 'bad-id', `the manifest at ${sourcePath} names '${manifest.id}', not '${id}'`, id, id)] };
204
+ }
205
+ const source_ = pinnedSource ?? { type: 'local', path: sourcePath };
206
+ const planId = planIdFor(`${analysis.contentDigest}\0${analysis.capabilityDigest}\0${canonicalJson(source_)}`, hash);
207
+ const grantLines = {};
208
+ if (tierMax(analysis.inventory) !== 'inert') {
209
+ grantLines.trust = JSON.stringify({
210
+ grant: 'trust', package: manifest.id, version: manifest.version, contentDigest: analysis.contentDigest,
211
+ capabilityDigest: analysis.capabilityDigest, policy: analysis.trustPolicyVersion, at: grantTime(now),
212
+ });
213
+ }
214
+ const preview = projectionPreview(sourcePath, manifest, lock, { repoRoot, registry, fsImpl });
215
+ const plan = {
216
+ plan: PLAN_VERSION, planId, createdAt: new Date(now).toISOString(), action, source: source_,
217
+ package: { id: manifest.id, version: manifest.version, doflow: manifest.doflow },
218
+ contentDigest: analysis.contentDigest, capabilityDigest: analysis.capabilityDigest, trustPolicyVersion: analysis.trustPolicyVersion,
219
+ inventory: analysis.inventory, programs: analysis.programs, projection: preview.projection, findings: analysis.findings, grantLines,
220
+ };
221
+ const paths = packagePaths(homeDir);
222
+ const planFile = path.join(paths.plans, `${planId}.json`);
223
+ if (pinnedSource) {
224
+ const snapshot = path.join(paths.fetched, planId);
225
+ fsImpl.mkdirSync(paths.fetched, { recursive: true });
226
+ if (fsImpl.existsSync(snapshot)) {
227
+ if (digestOf(snapshot, analysis.limits, fsImpl) !== analysis.contentDigest) {
228
+ return { ok: false, findings: [finding('contentDigest', 'digest-mismatch', `retained snapshot for plan ${planId} has changed`, analysis.contentDigest, manifest.id)] };
229
+ }
230
+ } else {
231
+ fsImpl.renameSync(sourcePath, snapshot);
232
+ }
233
+ fsImpl.utimesSync(snapshot, new Date(now), new Date(now));
234
+ }
235
+ atomicJsonWrite(planFile, plan, { fsImpl });
236
+ return { ok: true, plan, planFile, tierMax: tierMax(analysis.inventory), projectionWarnings: preview.warnings, instructions: instructionEntries(manifest).length };
237
+ }
238
+
239
+ /** Pin, fetch and retain a checked remote tree. The command has already checked the fetch grant. */
240
+ function planRemote({ source, pinned, expected, homeDir = os.homedir(), repoRoot = REPO_ROOT, env = process.env, now = Date.now(), registry, fsImpl = fs } ) {
241
+ if (!pinned) {
242
+ const pin = pinSource(source, { env });
243
+ if (!pin.ok) return { ok: false, findings: [pin.finding] };
244
+ pinned = pin.pinned;
245
+ }
246
+ const findings = pinnedSourceFindings(pinned);
247
+ if (findings.length) return { ok: false, findings };
248
+ const loaded = loadPoints({ repoRoot });
249
+ if (!loaded.ok) return { ok: false, findings: [finding('points', 'state-unreadable', loaded.errors.join('; '), 'a valid point registry')] };
250
+ const staging = path.join(packagePaths(homeDir).fetched, `.staging-${crypto.randomBytes(8).toString('hex')}`);
251
+ fsImpl.mkdirSync(path.dirname(staging), { recursive: true });
252
+ fsImpl.mkdirSync(staging);
253
+ try {
254
+ const fetched = fetchSource(pinned, { stagingDir: staging, env, limits: loaded.registry.limits });
255
+ if (!fetched.ok) return fetched;
256
+ const tree = path.join(staging, 'tree');
257
+ fsImpl.mkdirSync(tree);
258
+ for (const file of fetched.files) {
259
+ const target = path.join(tree, ...file.path.split('/'));
260
+ fsImpl.mkdirSync(path.dirname(target), { recursive: true });
261
+ fsImpl.writeFileSync(target, file.data);
262
+ fsImpl.chmodSync(target, file.mode);
263
+ }
264
+ return planPackage({ action: 'add', source: tree, pinnedSource: pinned, expected, homeDir, repoRoot, now, registry, fsImpl });
265
+ } finally {
266
+ fsImpl.rmSync(staging, { recursive: true, force: true });
267
+ }
268
+ }
269
+
270
+ /** A plan file's content when it is the IC-006 shape with a local or pinned remote source, else a finding. */
271
+ function readPlan(planId , homeDir , fsImpl ) {
272
+ const planFile = path.join(packagePaths(homeDir).plans, `${planId}.json`);
273
+ let text ;
274
+ try { text = String(fsImpl.readFileSync(planFile, 'utf8')); } catch (error ) {
275
+ if (error?.code === 'ENOENT') return { finding: finding('plan', 'file-missing', `no plan ${planId}; a plan is kept for 24 hours`, 'a plan id printed by add or update') };
276
+ return { finding: finding('plan', 'state-unreadable', `plan ${planId} cannot be read: ${error?.message}`, 'a readable plan file') };
277
+ }
278
+ let plan ;
279
+ try { plan = JSON.parse(text); } catch { plan = null; }
280
+ const ok = isObject(plan) && plan.plan === PLAN_VERSION && plan.planId === planId && (plan.action === 'add' || plan.action === 'update')
281
+ && isObject(plan.source) && ((plan.source.type === 'local' && typeof plan.source.path === 'string' && path.isAbsolute(plan.source.path))
282
+ || (plan.action === 'add' && pinnedSourceFindings(plan.source).length === 0))
283
+ && isObject(plan.package) && isPackageId(plan.package.id) && DIGEST.test(plan.contentDigest) && DIGEST.test(plan.capabilityDigest);
284
+ if (!ok) return { finding: finding('plan', 'state-unreadable', `plan ${planId} is not a plan this DoFlow wrote`, 'a plan written by package add or update') };
285
+ return { plan };
286
+ }
287
+
288
+ /** Copy the regular files of `files` under `dest`, normalising the mode to the execute bit alone. */
289
+ function copyFiles(files , dest , fsImpl ) {
290
+ for (const file of files) {
291
+ const target = path.join(dest, ...file.rel.split('/'));
292
+ fsImpl.mkdirSync(path.dirname(target), { recursive: true });
293
+ fsImpl.copyFileSync(file.abs, target);
294
+ fsImpl.chmodSync(target, file.exec ? 0o755 : 0o644);
295
+ }
296
+ }
297
+
298
+ /** The IC-005 content digest of a directory, or null when its tree breaks a tree rule. */
299
+ function digestOf(dir , limits , fsImpl ) {
300
+ const walked = walkTree(dir, limits, { fsImpl });
301
+ return walked.findings.length > 0 ? null : digestFiles(walked.files, fsImpl);
302
+ }
303
+
304
+ /** Remove a store entry's tree and marker, and the id directory when that leaves it empty. Never throws. */
305
+ function removeEntry(idDir , digest16 , fsImpl ) {
306
+ try {
307
+ fsImpl.rmSync(path.join(idDir, digest16), { recursive: true, force: true });
308
+ fsImpl.rmSync(path.join(idDir, `${digest16}${MARKER_SUFFIX}`), { force: true });
309
+ if (fsImpl.readdirSync(idDir).length === 0) fsImpl.rmdirSync(idDir);
310
+ return true;
311
+ } catch { return false; }
312
+ }
313
+
314
+ /**
315
+ * Storage upkeep for the package stores. `upkeepAfterVerb` visits the state and config roots only (its
316
+ * `MOMENT_ROOTS`), so the declared rows `packages-plans` and `packages-staging` carry no moment and the plans
317
+ * (24 hours) and stale staging directories under `~/.doflow/packages` are pruned here, by the engine, whenever
318
+ * a plan is applied. It honours the prune setting and `DOFLOW_UPKEEP=off`, and never throws.
319
+ */
320
+ function pruneStale({ homeDir, env, now, fsImpl } ) {
321
+ if (env.DOFLOW_UPKEEP === 'off') return;
322
+ try {
323
+ const { loadStorageRegistry } = require('../storage/registry.js');
324
+ const { resolveRoots, readPruneSetting } = require('../storage/roots.js');
325
+ const { runPass } = require('../storage/engine.js');
326
+ const registry = loadStorageRegistry({ repoRoot: REPO_ROOT });
327
+ const roots = resolveRoots({ registry, global: true, env: { ...env, HOME: homeDir } });
328
+ const { setting } = readPruneSetting({ roots, rootId: 'user-doflow', fsImpl });
329
+ runPass({ registry, roots, stores: ['packages-plans', 'packages-staging', 'packages-fetched', 'packages-fetch-staging'], mode: 'prune', setting: { 'user-doflow': setting }, now, env, fsImpl });
330
+ if (setting === 'on') {
331
+ const paths = packagePaths(homeDir);
332
+ if (fsImpl.existsSync(paths.fetched)) {
333
+ for (const name of fsImpl.readdirSync(paths.fetched)) {
334
+ if (PLAN_ID.test(name) && !fsImpl.existsSync(path.join(paths.plans, `${name}.json`))) {
335
+ fsImpl.rmSync(path.join(paths.fetched, name), { recursive: true, force: true });
336
+ }
337
+ }
338
+ }
339
+ }
340
+ } catch { /* upkeep must never affect the apply */ }
341
+ }
342
+
343
+ /**
344
+ * Apply a plan (`add --plan` or `update --plan`). `action` names the verb the caller ran: a plan of the other
345
+ * kind is refused. Returns `{ ok: true, ... }` or `{ ok: false, findings }` (a refusal writes no store entry and
346
+ * leaves the lock byte-identical); an unexpected failure throws after the entry it made is removed and the run
347
+ * lock released. `hooks` are test seams: `afterCopy(stagingDir)` runs after the copy and before the re-hash,
348
+ * `afterRename(entryDir)` runs right after the rename.
349
+ */
350
+ function applyPlan({ planId, action = null, homeDir = os.homedir(), repoRoot = REPO_ROOT, env = process.env, now = Date.now(), hooks = {}, fsImpl = fs } ) {
351
+ const read = readPlan(planId, homeDir, fsImpl);
352
+ if (read.finding) return { ok: false, findings: [read.finding] };
353
+ const { plan } = read;
354
+ if (action !== null && plan.action !== action) {
355
+ return { ok: false, findings: [finding('action', 'bad-type', `plan ${planId} is ${plan.action === 'add' ? 'an add' : 'an update'} plan`, `a plan made by ${action}`, plan.package.id)] };
356
+ }
357
+ const hold = acquireRunLock({ scopeRoot: homeDir, scope: 'global', command: `package ${plan.action}` });
358
+ try {
359
+ pruneStale({ homeDir, env, now, fsImpl });
360
+ return applyLocked({ plan, homeDir, repoRoot, now, hooks, fsImpl });
361
+ } finally {
362
+ hold.release();
363
+ }
364
+ }
365
+
366
+ function applyLocked({ plan, homeDir, repoRoot, now, hooks, fsImpl } ) {
367
+ const id = plan.package.id;
368
+ const lock = readLock({ scope: 'global', homeDir }, { fsImpl });
369
+ const rows = lockPackages(lock);
370
+ const previous = rows.find((r ) => r.id === id) ?? null;
371
+ if (plan.action === 'add' && previous) {
372
+ return { ok: false, findings: [finding('id', 'collision-package', `package '${id}' is already recorded; use update`, 'an id no recorded package uses', id)] };
373
+ }
374
+ if (plan.action === 'update' && !previous) {
375
+ return { ok: false, findings: [finding('id', 'bad-id', `no package with id '${id}' is recorded`, 'the id of a recorded package', id)] };
376
+ }
377
+ const remote = plan.source.type !== 'local';
378
+ const sourceDir = remote ? path.join(packagePaths(homeDir).fetched, plan.planId) : plan.source.path;
379
+ if (remote) {
380
+ const loaded = loadPoints({ repoRoot });
381
+ if (!loaded.ok) return { ok: false, findings: [finding('points', 'state-unreadable', loaded.errors.join('; '), 'a valid point registry', id)] };
382
+ if (digestOf(sourceDir, loaded.registry.limits, fsImpl) !== plan.contentDigest) {
383
+ return { ok: false, findings: [finding('contentDigest', 'digest-mismatch', `fetched snapshot for plan ${plan.planId} is missing or changed; plan again`, plan.contentDigest, id)] };
384
+ }
385
+ }
386
+ const analysis = analyze(sourceDir, { repoRoot, fsImpl });
387
+ if (!analysis.ok) return { ok: false, findings: analysis.findings };
388
+ const changed = [
389
+ ['contentDigest', analysis.contentDigest, plan.contentDigest], ['capabilityDigest', analysis.capabilityDigest, plan.capabilityDigest],
390
+ ['id', analysis.manifest.id, id],
391
+ ].filter(([, now_, planned]) => now_ !== planned);
392
+ if (changed.length > 0) {
393
+ return { ok: false, findings: changed.map(([field]) => finding(field, 'digest-mismatch', `${field} of ${sourceDir} differs from plan ${plan.planId}; plan again`, `the ${field} the plan recorded`, id)) };
394
+ }
395
+
396
+ const { store } = packagePaths(homeDir);
397
+ const digest16 = plan.contentDigest.slice(0, 16);
398
+ const staging = path.join(store, `.staging-${plan.planId}`);
399
+ const idDir = path.join(store, id);
400
+ const entry = path.join(idDir, digest16);
401
+ fsImpl.mkdirSync(store, { recursive: true });
402
+ fsImpl.rmSync(staging, { recursive: true, force: true });
403
+ const mismatch = (message ) => {
404
+ fsImpl.rmSync(staging, { recursive: true, force: true });
405
+ return { ok: false, findings: [finding('contentDigest', 'digest-mismatch', message, `the contentDigest the plan recorded`, id)] };
406
+ };
407
+ try {
408
+ const source = walkTree(sourceDir, analysis.limits, { fsImpl });
409
+ if (source.findings.length > 0) return mismatch(`${sourceDir} no longer passes the tree rules (${source.findings.map((f ) => f.code).join(', ')}); plan again`);
410
+ copyFiles(source.files, staging, fsImpl);
411
+ if (typeof hooks.afterCopy === 'function') hooks.afterCopy(staging);
412
+ if (digestOf(staging, analysis.limits, fsImpl) !== plan.contentDigest) return mismatch(`the staged copy of ${sourceDir} differs from plan ${plan.planId}; plan again`);
413
+
414
+ fsImpl.mkdirSync(idDir, { recursive: true });
415
+ let created = false;
416
+ if (fsImpl.existsSync(entry)) {
417
+ // A leftover of an interrupted run, or an update that changed capabilities only: reuse it when it holds the planned bytes.
418
+ fsImpl.rmSync(staging, { recursive: true, force: true });
419
+ if (digestOf(entry, analysis.limits, fsImpl) !== plan.contentDigest) {
420
+ return { ok: false, findings: [finding('store', 'integrity-mismatch', `the store entry for ${id} ${digest16} does not hold the planned bytes`, 'an intact store entry', id)] };
421
+ }
422
+ } else {
423
+ fsImpl.renameSync(staging, entry);
424
+ created = true;
425
+ }
426
+ const row = {
427
+ id, version: analysis.manifest.version, source: plan.source, contentDigest: plan.contentDigest, capabilityDigest: plan.capabilityDigest,
428
+ enabled: previous ? previous.enabled : false, addedAt: previous ? previous.addedAt : new Date(now).toISOString(),
429
+ };
430
+ try {
431
+ if (typeof hooks.afterRename === 'function') hooks.afterRename(entry);
432
+ atomicJsonWrite(`${entry}${MARKER_SUFFIX}`, {
433
+ marker: 1, planId: plan.planId, action: plan.action, source: plan.source, contentDigest: plan.contentDigest,
434
+ capabilityDigest: plan.capabilityDigest, installedAt: new Date(now).toISOString(),
435
+ }, { fsImpl });
436
+ writeLock({ scope: 'global', homeDir }, { ...(lock ?? defaultLock({ scope: 'global', scopeRoot: homeDir })), packages: [...rows.filter((r ) => r.id !== id), row] }, { fsImpl });
437
+ } catch (error) {
438
+ if (created) removeEntry(idDir, digest16, fsImpl);
439
+ throw error;
440
+ }
441
+ const oldDigest16 = previous ? previous.contentDigest.slice(0, 16) : null;
442
+ const leftover = oldDigest16 !== null && oldDigest16 !== digest16 && !removeEntry(idDir, oldDigest16, fsImpl);
443
+ return { ok: true, action: plan.action, id, version: row.version, enabled: row.enabled, storeDir: entry, contentDigest: row.contentDigest, capabilityDigest: row.capabilityDigest, leftover };
444
+ } finally {
445
+ fsImpl.rmSync(staging, { recursive: true, force: true });
446
+ }
447
+ }
448
+
449
+ module.exports = { planIdFor, planPackage, planRemote, applyPlan, packagePaths, removeEntry, projectionReach, instructionEntries, PLAN_ID, MARKER_SUFFIX };