@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
@@ -15,6 +15,7 @@
15
15
  const fs = require('node:fs');
16
16
  const path = require('node:path');
17
17
  const { REPO_ROOT } = require('../helper/repo-root.js');
18
+ const { contentDigest } = require('../runtime/packages/digest.js');
18
19
 
19
20
  /** Every distribution manifest that repeats the release version, relative to the repo root. */
20
21
  const PLUGIN_MANIFESTS = [
@@ -23,15 +24,50 @@ const PLUGIN_MANIFESTS = [
23
24
  'core/.plugin/plugin.json',
24
25
  ];
25
26
 
27
+ /** The point registry that lists the bundled first-party packages (122 IC-015), relative to the repo root. */
28
+ const POINTS_FILE = 'core/registry/contribution-points.json';
29
+
30
+ /**
31
+ * 122 PN-8: a bundled package's manifest repeats the release version (`version`, and `doflow` as the exact range
32
+ * `=<version>`), and its directory's content digest is pinned in `contribution-points.json`. A bump changes the
33
+ * manifest bytes, so the three move together: rewrite the manifest, hash the directory, pin the digest. Returns the
34
+ * files it rewrote. Targeted replacement keeps the formatting of both files.
35
+ */
36
+ function syncBundledPackages(version , repoRoot , fsImpl ) {
37
+ const pointsFile = path.join(repoRoot, POINTS_FILE);
38
+ let pointsText = fsImpl.readFileSync(pointsFile, 'utf8');
39
+ const points = JSON.parse(pointsText);
40
+ const rewritten = [];
41
+ for (const entry of points.bundled) {
42
+ const manifestRel = path.posix.join(entry.dir, 'doflow-package.json');
43
+ const manifestFile = path.join(repoRoot, manifestRel);
44
+ const text = fsImpl.readFileSync(manifestFile, 'utf8');
45
+ const next = text
46
+ .replace(/("version":\s*")[^"]*"/, (_m , head ) => `${head}${version}"`)
47
+ .replace(/("doflow":\s*")[^"]*"/, (_m , head ) => `${head}=${version}"`);
48
+ if (next !== text) {
49
+ fsImpl.writeFileSync(manifestFile, next, 'utf8');
50
+ rewritten.push(manifestRel);
51
+ }
52
+ const digest = contentDigest(path.join(repoRoot, entry.dir), { limits: points.limits, fsImpl });
53
+ if (digest !== entry.contentDigest) {
54
+ pointsText = pointsText.replace(entry.contentDigest, digest);
55
+ rewritten.push(POINTS_FILE);
56
+ }
57
+ }
58
+ if (rewritten.includes(POINTS_FILE)) fsImpl.writeFileSync(pointsFile, pointsText, 'utf8');
59
+ return [...new Set(rewritten)];
60
+ }
61
+
26
62
  /**
27
63
  * Rewrites each plugin manifest's version to package.json's, preserving formatting by targeted
28
- * string replacement rather than re-serializing the whole file.
64
+ * string replacement rather than re-serializing the whole file. The bundled packages follow (`bundled`).
29
65
  * @param {Object} [options]
30
66
  * @param {string} [options.repoRoot]
31
67
  * @param {Object} [options.fsImpl]
32
- * @returns {{version: string, updated: Array<string>, unchanged: Array<string>}}
68
+ * @returns {{version: string, updated: Array<string>, unchanged: Array<string>, bundled: Array<string>}}
33
69
  */
34
- function syncPluginVersions({ repoRoot = REPO_ROOT, fsImpl = fs } = {}) {
70
+ function syncPluginVersions({ repoRoot = REPO_ROOT, fsImpl = fs } = {}) {
35
71
  const pkg = JSON.parse(fsImpl.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
36
72
  if (typeof pkg.version !== 'string' || pkg.version.trim() === '') {
37
73
  throw new Error('package.json declares no version to project');
@@ -54,13 +90,13 @@ function syncPluginVersions({ repoRoot = REPO_ROOT, fsImpl = fs }
54
90
  fsImpl.writeFileSync(file, next, 'utf8');
55
91
  updated.push(rel);
56
92
  }
57
- return { version: pkg.version, updated, unchanged };
93
+ return { version: pkg.version, updated, unchanged, bundled: syncBundledPackages(pkg.version, repoRoot, fsImpl) };
58
94
  }
59
95
 
60
96
  if (require.main === module) {
61
97
  const result = syncPluginVersions();
62
- for (const rel of result.updated) console.log(`synced ${rel} -> ${result.version}`);
63
- if (result.updated.length === 0) console.log(`all plugin manifests already at ${result.version}`);
98
+ for (const rel of [...result.updated, ...result.bundled]) console.log(`synced ${rel} -> ${result.version}`);
99
+ if (result.updated.length === 0 && result.bundled.length === 0) console.log(`all plugin manifests and bundled packages already at ${result.version}`);
64
100
  }
65
101
 
66
- module.exports = { syncPluginVersions, PLUGIN_MANIFESTS };
102
+ module.exports = { syncPluginVersions, PLUGIN_MANIFESTS, POINTS_FILE };
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ // The bench bar that act mode requires. User decision D3 (2026-10-26): act is unreachable in 084. The bar may only be
4
+ // met by a DoFlow-owned scored bench corpus, none ships yet, and a file in the caller's project is never read, so the
5
+ // bar is always unmet. Producing that corpus is the future row "trusted bench scores" (not yet registered).
6
+
7
+
8
+
9
+
10
+
11
+
12
+
13
+
14
+ function evaluateBenchBar(kind , model ) {
15
+ return {
16
+ kind,
17
+ model,
18
+ passed: false,
19
+ reason: `no trusted bench scores ship for kind '${kind}' and model '${model}'`,
20
+ };
21
+ }
22
+
23
+ module.exports = {
24
+ evaluateBenchBar,
25
+ };
@@ -9,11 +9,14 @@ const { REPO_ROOT } = require('../../helper/repo-root.js');
9
9
  const { loadAdviceRegistry } = require('./registry.js');
10
10
  const { loadStorageRegistry } = require('../storage/registry.js');
11
11
  const { resolveRoots } = require('../storage/roots.js');
12
- const { resolveSetting, writeSetting } = require('./config.js');
12
+ const { resolveSetting } = require('./config.js');
13
+ const { loadSettings } = require('../settings/resolve.js');
14
+ const { writeLayer } = require('../settings/write.js');
13
15
  const { createAdviceClient } = require('./client.js');
14
16
  const { materializeKind, validateQuestionSet, validateStaticAnswer } = require('./kinds.js');
15
17
  const { findPick, appendOutcome } = require('./record.js');
16
18
  const { strengthWords } = require('./strength.js');
19
+ const { projectLayerSource } = require('../settings/project-layers.js');
17
20
 
18
21
  function loadSources(repoRoot ) {
19
22
  const read = (name ) => JSON.parse(fs.readFileSync(path.join(repoRoot, 'core/registry', name), 'utf8'));
@@ -28,11 +31,39 @@ function loadRuntime({ projectRoot, global = false } = {}) {
28
31
  const advice = loadAdviceRegistry({ repoRoot: REPO_ROOT });
29
32
  const storage = loadStorageRegistry({ repoRoot: REPO_ROOT });
30
33
  const roots = resolveRoots({ registry: storage, projectRoot, global });
34
+ const projectLayers = projectLayerSource(projectRoot);
31
35
  const sources = loadSources(REPO_ROOT);
32
36
  const adviceRegistry = advice.ok ? advice.registry : null;
33
- return { adviceRegistry, adviceError: advice.ok ? null : advice.detail, storage, roots, sources };
37
+ return { adviceRegistry, adviceError: advice.ok ? null : advice.detail, storage, roots, sources, projectLayers };
34
38
  }
35
39
 
40
+ function emitDeprecatedNotes(runtime , loadSettingsImpl = loadSettings) {
41
+ emitDeprecatedNotesByCode(runtime, loadSettingsImpl);
42
+ }
43
+
44
+ function emitDeprecatedNotesByCode(runtime , loadSettingsImpl = loadSettings) {
45
+ if (!runtime.adviceRegistry) return;
46
+ const roots = runtime.roots || {};
47
+ const projectRoot = roots['project-doflow'] ? path.dirname(roots['project-doflow']) : null;
48
+
49
+ try {
50
+ const view = loadSettingsImpl({ projectRoot, fsImpl: fs });
51
+ const enabled = view.get('advice.enabled', 'global');
52
+ const canReadProject = enabled.value === true && enabled.state === 'ok' && enabled.origin.via === null;
53
+ for (const note of view.notes || []) {
54
+ if (note.code !== 'deprecated') continue;
55
+ const sourceLayer = view.files.find((file ) => file.path === note.source)?.layer;
56
+ if (!note.key || !['user', 'project'].includes(sourceLayer)) continue;
57
+ if (sourceLayer === 'project' && !canReadProject) continue;
58
+ let resolution;
59
+ try { resolution = view.get(note.key, sourceLayer === 'user' ? 'global' : 'project'); } catch { continue; }
60
+ if (!resolution.origin.legacy || resolution.origin.source !== note.source) continue;
61
+ console.error('note: ' + note.text);
62
+ }
63
+ } catch {
64
+ // Advice remains usable when legacy-source reporting fails.
65
+ }
66
+ }
36
67
  function emitResult(result , { json, kind } ) {
37
68
  if (result.reason === 'over-limit') {
38
69
  return usageError('advise', result.detail || 'request exceeds an advice limit', json);
@@ -141,7 +172,7 @@ function writeFailure(message , json ) {
141
172
  }
142
173
 
143
174
  async function handleConfigure(options , runtime ) {
144
- const { adviceRegistry, adviceError, roots, sources } = runtime;
175
+ const { adviceRegistry, adviceError, roots, sources, projectLayers } = runtime;
145
176
  const { json, project, global, force } = options;
146
177
  const hasModel = typeof options.model === 'string' && options.model.length > 0;
147
178
  const hasOff = options.off === true;
@@ -152,17 +183,28 @@ async function handleConfigure(options , runtime ) {
152
183
  if (project && (global || !options.projectRoot)) return usageError('advise', '--project requires a project root', json);
153
184
  if (adviceError || !adviceRegistry) return writeFailure(`advice registry is invalid: ${adviceError || 'missing registry'}`, json);
154
185
 
186
+ if (!project && !roots['config-home']) return usageError('advise', 'no config home resolves; set an absolute HOME or XDG_CONFIG_HOME, or use --project', json);
187
+ emitDeprecatedNotes(runtime);
188
+
155
189
  const file = project
156
- ? path.join(roots['project-doflow'], 'decision-model.json')
157
- : path.join(roots['config-home'], 'decision-model.json');
190
+ ? path.join(projectLayers.layersFrom || roots['project-doflow'], 'settings.json')
191
+ : path.join(roots['config-home'], 'settings.json');
158
192
  const fsImpl = fs;
193
+ const inheritedFromMain = !!project && projectLayers.checkout === 'main';
194
+ const discloseWrite = (setting ) => {
195
+ const result = { written: file, setting };
196
+ if (inheritedFromMain) result.inheritedFromMainCheckout = true;
197
+ if (json) console.log(JSON.stringify(result, null, 2));
198
+ else if (inheritedFromMain) console.log(`(inherited from the main checkout)`);
199
+ };
159
200
  if (hasOff) {
160
- const setting = project ? { schema: 1, enabled: false } : { schema: 1, enabled: false };
161
- const written = writeSetting({ file, level: project ? 'project' : 'user', setting, force, fsImpl });
201
+ const written = writeLayer({ file, set: { 'advice.enabled': false }, force, fsImpl });
162
202
  if (!written.ok) return writeFailure(written.detail, json);
163
203
  const effective = resolveSetting({ adviceRegistry, roots, fsImpl });
164
- if (json) console.log(JSON.stringify({ written: file, setting: effective }, null, 2));
165
- else console.log(project ? `Decision model for this project: off (wrote ${file}).` : `Decision model off (wrote ${file}).`);
204
+ if (json) discloseWrite(effective);
205
+ else console.log(project
206
+ ? `Decision model for this project: off (wrote ${file}${inheritedFromMain ? ' (inherited from the main checkout)' : ''}).`
207
+ : `Decision model off (wrote ${file}).`);
166
208
  return finishRuntime(0);
167
209
  }
168
210
 
@@ -172,7 +214,7 @@ async function handleConfigure(options , runtime ) {
172
214
  if (project && userSetting.state !== 'on') {
173
215
  return writeFailure('turn the decision model on at user level first; a project cannot enable it', json);
174
216
  }
175
- const providerId = userSetting.provider || 'ollama';
217
+ const providerId = userSetting.provider ?? loadSettings({ global: true }).get('advice.provider', 'global').value;
176
218
  const provider = adviceRegistry.providers.find((entry ) => entry.id === providerId);
177
219
  if (!provider) return writeFailure(`provider '${providerId}' is not in core/registry/advice.json`, json);
178
220
  const endpoint = project
@@ -185,14 +227,14 @@ async function handleConfigure(options , runtime ) {
185
227
  if (failure) return writeFailure(failure, json);
186
228
 
187
229
  const setting = project
188
- ? { schema: 1, model: options.model }
189
- : { schema: 1, enabled: true, provider: provider.id, endpoint, model: options.model };
190
- const written = writeSetting({ file, level: project ? 'project' : 'user', setting, force, fsImpl });
230
+ ? { 'advice.model': options.model }
231
+ : { 'advice.enabled': true, 'advice.provider': provider.id, 'advice.endpoint': endpoint, 'advice.model': options.model };
232
+ const written = writeLayer({ file, set: setting, force, fsImpl });
191
233
  if (!written.ok) return writeFailure(written.detail, json);
192
234
  const effective = resolveSetting({ adviceRegistry, roots, fsImpl });
193
- if (json) console.log(JSON.stringify({ written: file, setting: effective }, null, 2));
235
+ if (json) discloseWrite(effective);
194
236
  else console.log(project
195
- ? `Decision model for this project: ${options.model} (wrote ${file}).`
237
+ ? `Decision model for this project: ${options.model} (wrote ${file}${inheritedFromMain ? ' (inherited from the main checkout)' : ''}).`
196
238
  : `Decision model on: ${options.model} at ${endpoint} (wrote ${file}).`);
197
239
  return finishRuntime(0);
198
240
  }
@@ -274,10 +316,11 @@ async function handleAdviseCommand(options = {}) {
274
316
  const shaped = { ...options, projectRoot };
275
317
  if (action === 'configure') return await handleConfigure(shaped, runtime);
276
318
  if (action === 'outcome') return handleOutcome(shaped, runtime);
319
+ emitDeprecatedNotes(runtime);
277
320
  return await handleAsk(shaped, runtime);
278
321
  } catch (error ) {
279
322
  return writeFailure(error?.message || 'advice command failed', options.json);
280
323
  }
281
324
  }
282
325
 
283
- module.exports = { handleAdviseCommand };
326
+ module.exports = { handleAdviseCommand, emitDeprecatedNotes };
@@ -6,6 +6,7 @@ const fs = require('node:fs');
6
6
  const http = require('node:http');
7
7
  const https = require('node:https');
8
8
  const { resolveSetting } = require('./config.js');
9
+ const { loadSettings } = require('../settings/resolve.js');
9
10
  const { checkEndpoint } = require('./endpoint-guard.js');
10
11
  const { strengthOf } = require('./strength.js');
11
12
  const { validateQuestionSet, validateStaticAnswer } = require('./kinds.js');
@@ -41,7 +42,14 @@ function networkReason(error ) {
41
42
  ? 'timeout' : 'unreachable';
42
43
  }
43
44
 
44
- function requestAction({ endpoint, action, pinnedLookup, timeoutMs, httpModule = http, httpsModule = https } ) {
45
+ function timeoutError(message ) {
46
+ const error = new Error(message);
47
+ error.code = 'ADVICE_TIMEOUT';
48
+ return error;
49
+ }
50
+
51
+ /** `signal`, when given, aborts the request the way its own timeout does: the request is destroyed and the timer cleared. */
52
+ function requestAction({ endpoint, action, pinnedLookup, timeoutMs, signal, httpModule = http, httpsModule = https } ) {
45
53
  return new Promise ((resolve, reject) => {
46
54
  let request ;
47
55
  let settled = false;
@@ -49,15 +57,18 @@ function requestAction({ endpoint, action, pinnedLookup, timeoutMs, httpModule =
49
57
  if (settled) return;
50
58
  settled = true;
51
59
  clearTimeout(timer);
60
+ signal?.removeEventListener?.('abort', onAbort);
52
61
  if (error) reject(error);
53
62
  else resolve(response);
54
63
  };
55
- const timer = setTimeout(() => {
56
- const error = new Error(`decision model request timed out after ${timeoutMs}ms`);
57
- error.code = 'ADVICE_TIMEOUT';
64
+ const stop = (error ) => {
58
65
  request?.destroy(error);
59
66
  finish(error);
60
- }, timeoutMs);
67
+ };
68
+ const onAbort = () => stop(timeoutError('decision model request aborted by the caller'));
69
+ const timer = setTimeout(() => stop(timeoutError(`decision model request timed out after ${timeoutMs}ms`)), timeoutMs);
70
+ if (signal?.aborted) { onAbort(); return; }
71
+ signal?.addEventListener?.('abort', onAbort, { once: true });
61
72
 
62
73
  try {
63
74
  const url = new URL(endpoint);
@@ -117,7 +128,11 @@ function createAdviceClient({
117
128
  httpsModule = https,
118
129
  clock = () => new Date(),
119
130
  } = {}) {
120
- async function ask({ questionSet, kind, staticAnswer, caller, taskId = null, record = true } = {}) {
131
+ /**
132
+ * `model`, when given, replaces the setting's model for this call: it is the model asked, recorded and reported.
133
+ * `signal`, when given, ends the call early: an in-flight request is destroyed and the call is unavailable with 'timeout'.
134
+ */
135
+ async function ask({ questionSet, kind, staticAnswer, caller, taskId = null, record = true, model, signal } = {}) {
121
136
  const startedAt = Date.now();
122
137
  let setting = null;
123
138
  const effectiveKind = typeof kind === 'string' && kind ? kind : 'adhoc';
@@ -137,6 +152,7 @@ function createAdviceClient({
137
152
  if (setting.state !== 'on') {
138
153
  return emptyAdviceResult('off', setting.reason, setting.notes?.[0] || null, setting, durationSince(startedAt));
139
154
  }
155
+ if (typeof model === 'string' && model.trim()) setting = { ...setting, model: model.trim() };
140
156
 
141
157
  const provider = adviceRegistry?.providers?.find((entry ) => entry.id === setting.provider);
142
158
  const adapter = provider && ADAPTERS[provider.adapter];
@@ -166,14 +182,17 @@ function createAdviceClient({
166
182
  return unavailable('over-limit', `request body is ${bodyBytes} bytes; the limit is ${adviceRegistry.limits.maxBodyBytes} (core/registry/advice.json limits.maxBodyBytes)`);
167
183
  }
168
184
 
185
+ if (signal?.aborted) return unavailable('timeout', 'decision model request aborted by the caller');
169
186
  const endpointCheck = await checkEndpoint(setting.endpoint, {
170
187
  lookup,
171
188
  file: setting.decidedBy?.user || 'provider default',
172
189
  timeoutMs: Math.max(0, adviceRegistry.timeoutMs - durationSince(startedAt)),
190
+ signal,
173
191
  });
174
192
  if (!endpointCheck.ok) return unavailable(endpointCheck.reason || 'non-loopback', endpointCheck.message);
175
193
  const remainingMs = adviceRegistry.timeoutMs - durationSince(startedAt);
176
194
  if (remainingMs <= 0) return unavailable('timeout', 'decision model request budget exhausted');
195
+ if (signal?.aborted) return unavailable('timeout', 'decision model request aborted by the caller');
177
196
 
178
197
  let response;
179
198
  try {
@@ -182,6 +201,7 @@ function createAdviceClient({
182
201
  action: request,
183
202
  pinnedLookup: endpointCheck.pinnedLookup,
184
203
  timeoutMs: remainingMs,
204
+ signal,
185
205
  httpModule,
186
206
  httpsModule,
187
207
  });
@@ -242,16 +262,19 @@ function createAdviceClient({
242
262
  }
243
263
  }
244
264
 
245
- async function inspect({ endpoint, model, provider: providerId = 'ollama', file, minVersion, timeoutMs } = {}) {
265
+ async function inspect({ endpoint, model, provider: providerId, file, minVersion, timeoutMs } = {}) {
246
266
  const unavailable = (state , message , version = null, versionOk = false) => ({
247
267
  endpoint: { state, version, versionOk, message: message ?? null },
248
268
  models: [],
249
269
  model: { installed: false, decisionCapable: false },
250
270
  });
251
271
  try {
252
- const provider = adviceRegistry?.providers?.find((entry ) => entry.id === providerId);
272
+ const effectiveProviderId = providerId
273
+ ?? resolveSetting({ adviceRegistry, roots, fsImpl }).provider
274
+ ?? loadSettings({ global: true, env: {}, fsImpl }).get('advice.provider', 'global').value;
275
+ const provider = adviceRegistry?.providers?.find((entry ) => entry.id === effectiveProviderId);
253
276
  const adapter = provider && ADAPTERS[provider.adapter];
254
- if (!provider || !adapter) return unavailable('refused', `provider '${providerId}' is unavailable`);
277
+ if (!provider || !adapter) return unavailable('refused', `provider '${effectiveProviderId}' is unavailable`);
255
278
  const probeLimit = Number.isFinite(timeoutMs) ? timeoutMs : adviceRegistry.probeTimeoutMs;
256
279
  const budgetMs = Number.isFinite(adviceRegistry.doctorBudgetMs) ? adviceRegistry.doctorBudgetMs : probeLimit * 4;
257
280
  const startedAt = Date.now();
@@ -5,14 +5,7 @@
5
5
  const fs = require('node:fs');
6
6
  const path = require('node:path');
7
7
  const { resolveRoots } = require('../storage/roots.js');
8
-
9
- const SETTING_FILE = 'decision-model.json';
10
- const PROJECT_NOTE = 'a project cannot enable the decision model or choose its endpoint';
11
- const KNOWN_KEYS = new Set(['schema', 'enabled', 'provider', 'endpoint', 'model']);
12
-
13
- function isObject(value ) {
14
- return value !== null && typeof value === 'object' && !Array.isArray(value);
15
- }
8
+ const { loadSettings } = require('../settings/resolve.js');
16
9
 
17
10
  function offResult(reason , decidedBy , notes = [], setting = {}) {
18
11
  return {
@@ -26,65 +19,58 @@ function offResult(reason , decidedBy , notes = [], setting
26
19
  };
27
20
  }
28
21
 
29
- function readJson(file , fsImpl ) {
30
- if (!file) return { exists: false };
31
- try {
32
- return { exists: true, value: JSON.parse(fsImpl.readFileSync(file, 'utf8')) };
33
- } catch (error ) {
34
- if (error?.code === 'ENOENT') return { exists: false };
35
- return { exists: true, error };
36
- }
22
+ function projectLayer(layer ) {
23
+ return layer === 'project' || layer === 'local';
37
24
  }
38
25
 
39
- function parseUserSetting(value , adviceRegistry ) {
40
- if (!isObject(value)) return { error: 'user setting must be a JSON object' };
41
- if (value.schema !== 1) return { error: 'user setting schema must be 1' };
42
- if (typeof value.enabled !== 'boolean') return { error: 'user setting enabled must be a boolean' };
43
-
44
- const providers = Array.isArray(adviceRegistry?.providers) ? adviceRegistry.providers : [];
45
- const providerId = value.provider === undefined ? 'ollama' : value.provider;
46
- const provider = providers.find((item ) => item?.id === providerId);
47
- if (typeof providerId !== 'string' || !provider) return { error: `user setting provider '${providerId}' is not in advice.json providers` };
48
-
49
- let endpoint = provider.defaultEndpoint;
50
- if (value.endpoint !== undefined) {
51
- if (typeof value.endpoint !== 'string' || !value.endpoint.trim()) return { error: 'user setting endpoint must be a URL string' };
52
- try { new URL(value.endpoint); } catch { return { error: 'user setting endpoint must be a URL string' }; }
53
- endpoint = value.endpoint;
54
- }
55
- if (typeof endpoint !== 'string' || !endpoint) return { error: `providers.${providerId}.defaultEndpoint is missing` };
56
-
57
- if (value.model !== undefined && (typeof value.model !== 'string' || !value.model.trim())) {
58
- return { error: 'user setting model must be a non-empty string' };
26
+ function errorNote(error , offBy = null) {
27
+ if (error.legacy && error.layer === 'user') {
28
+ if (error.code === 'unreadable' || error.code === 'not-file'
29
+ || (error.code === 'unparseable' && error.reason.startsWith('not valid JSON'))) {
30
+ return `user setting at ${error.source} could not be read: ${error.reason}`;
31
+ }
32
+ return `${error.reason} (${error.source})`;
59
33
  }
60
- if (value.enabled && (typeof value.model !== 'string' || !value.model.trim())) {
61
- return { error: 'user setting model is required when enabled is true' };
34
+ if (projectLayer(error.layer)) {
35
+ if (offBy && error.source === offBy) return `project setting at ${error.source} is invalid: ${error.reason}; advice is off`;
36
+ return `project setting at ${error.source} ignored: ${error.reason}`;
62
37
  }
38
+ return `user setting at ${error.source} is invalid${error.key ? ` for '${error.key}'` : ''}: ${error.reason}`;
39
+ }
63
40
 
64
- const notes = Object.keys(value)
65
- .filter((key) => !KNOWN_KEYS.has(key))
66
- .map((key) => `unknown user key '${key}' ignored`);
67
- return {
68
- value: { enabled: value.enabled, provider: providerId, endpoint, model: value.model ?? null },
69
- notes,
70
- };
41
+ function settingNotes(view , includeProject , includeDeprecated = false, offBy = null) {
42
+ const notes = view.notes
43
+ .filter((note ) => {
44
+ if (note.code !== 'deprecated' || !includeDeprecated) return note.code !== 'deprecated';
45
+ const sourceLayer = view.files.find((file ) => file.path === note.source)?.layer;
46
+ if (!note.key || !['user', 'project'].includes(sourceLayer)) return false;
47
+ const resolution = view.get(note.key, sourceLayer === 'user' ? 'global' : 'project');
48
+ return resolution.origin.legacy && resolution.origin.source === note.source;
49
+ })
50
+ .filter((note ) => includeProject || !projectLayer(view.files.find((file ) => file.path === note.source)?.layer))
51
+ .map((note ) => note.text);
52
+ const seen = new Set ();
53
+ for (const error of view.errors) {
54
+ if (!includeProject && projectLayer(error.layer)) continue;
55
+ const key = error.legacy
56
+ ? `${error.layer}\0${error.source}`
57
+ : `${error.layer}\0${error.source}\0${error.key}\0${error.code}`;
58
+ if (seen.has(key)) continue;
59
+ seen.add(key);
60
+ notes.push(errorNote(error, offBy));
61
+ }
62
+ return notes;
71
63
  }
72
64
 
73
- function parseProjectSetting(value ) {
74
- if (!isObject(value)) return { error: 'project setting must be a JSON object' };
75
- if (value.schema !== 1) return { error: 'project setting schema must be 1' };
76
- if (value.enabled !== undefined && typeof value.enabled !== 'boolean') return { error: 'project setting enabled must be a boolean' };
77
- if (value.model !== undefined && (typeof value.model !== 'string' || !value.model.trim())) return { error: 'project setting model must be a non-empty string' };
65
+ function modelSource(setting ) {
66
+ if (setting.value === null || setting.value === undefined) return null;
67
+ if (setting.origin.layer === 'user') return 'user';
68
+ return projectLayer(setting.origin.layer) ? 'project' : null;
69
+ }
78
70
 
79
- const notes = [];
80
- if (value.enabled === true) notes.push(`project key 'enabled' ignored: ${PROJECT_NOTE}`);
81
- for (const key of ['endpoint', 'provider']) {
82
- if (Object.hasOwn(value, key)) notes.push(`project key '${key}' ignored: ${PROJECT_NOTE}`);
83
- }
84
- for (const key of Object.keys(value)) {
85
- if (!['schema', 'enabled', 'model', 'endpoint', 'provider'].includes(key)) notes.push(`unknown project key '${key}' ignored`);
86
- }
87
- return { value: { disabled: value.enabled === false, model: value.model ?? null }, notes };
71
+ function decidedSource(setting , layer ) {
72
+ if (layer === 'user') return setting.origin.layer === 'user' ? setting.origin.source : null;
73
+ return projectLayer(setting.origin.layer) ? setting.origin.source : null;
88
74
  }
89
75
 
90
76
  function resolveSetting({
@@ -95,95 +81,74 @@ function resolveSetting({
95
81
  global = false,
96
82
  env = process.env,
97
83
  fsImpl = fs,
84
+ includeDeprecated = false,
98
85
  } = {}) {
99
- const notes = [];
100
86
  let resolvedRoots = roots;
101
87
  try {
102
88
  if (!resolvedRoots) resolvedRoots = resolveRoots({ registry: storageRegistry, projectRoot, global, env });
103
89
  } catch (error ) {
104
90
  return offResult('config-invalid', { user: null, project: null }, [`storage roots could not be resolved: ${error.message}`]);
105
91
  }
106
- const userFile = resolvedRoots?.['config-home'] ? path.join(resolvedRoots['config-home'], SETTING_FILE) : null;
107
- const projectFile = resolvedRoots?.['project-doflow'] ? path.join(resolvedRoots['project-doflow'], SETTING_FILE) : null;
108
- const decidedBy = { user: null, project: null };
109
- const userRead = readJson(userFile, fsImpl);
110
- if (!userRead.exists) return offResult('not-configured', decidedBy, notes);
111
- if (userRead.error) {
112
- return offResult('config-invalid', { user: userFile, project: null }, [`user setting at ${userFile} could not be read: ${userRead.error.message}`]);
92
+ const resolverEnv = { ...env };
93
+ if (resolvedRoots?.['config-home']) {
94
+ resolverEnv.XDG_CONFIG_HOME = path.dirname(resolvedRoots['config-home']);
95
+ } else {
96
+ delete resolverEnv.XDG_CONFIG_HOME;
97
+ delete resolverEnv.HOME;
113
98
  }
114
- const user = parseUserSetting(userRead.value, adviceRegistry);
115
- if (user.error) return offResult('config-invalid', { user: userFile, project: null }, [`${user.error} (${userFile})`]);
116
- decidedBy.user = userFile;
117
- notes.push(...user.notes);
118
-
119
- const base = { ...user.value, modelFrom: user.value.model ? 'user' : null };
120
- if (!user.value.enabled) return offResult('user-disabled', decidedBy, notes, base);
99
+ const resolverProjectRoot = !global && resolvedRoots?.['project-doflow']
100
+ ? path.dirname(resolvedRoots['project-doflow'])
101
+ : null;
121
102
 
122
- const projectRead = readJson(projectFile, fsImpl);
123
- if (projectRead.exists) {
124
- if (projectRead.error) {
125
- notes.push(`project setting at ${projectFile} ignored: ${projectRead.error.message}`);
126
- } else {
127
- const project = parseProjectSetting(projectRead.value);
128
- if (project.error) {
129
- notes.push(`project setting at ${projectFile} ignored: ${project.error}`);
130
- } else {
131
- notes.push(...project.notes);
132
- if (project.value.disabled) {
133
- decidedBy.project = projectFile;
134
- return offResult('project-disabled', decidedBy, notes, base);
135
- }
136
- if (project.value.model) {
137
- decidedBy.project = projectFile;
138
- return {
139
- state: 'on', reason: null, provider: base.provider, endpoint: base.endpoint,
140
- model: project.value.model, decidedBy, modelFrom: 'project', notes,
141
- };
142
- }
143
- }
103
+ let view ;
104
+ try {
105
+ view = loadSettings({ projectRoot: resolverProjectRoot, global, env: resolverEnv, fsImpl });
106
+ const enabledGlobal = view.get('advice.enabled', 'global');
107
+ const canReadProject = !global && enabledGlobal.value === true
108
+ && enabledGlobal.state === 'ok' && enabledGlobal.origin.via === null;
109
+ const enabled = canReadProject ? view.get('advice.enabled', 'project') : enabledGlobal;
110
+ const provider = view.get('advice.provider', 'global');
111
+ const endpoint = view.get('advice.endpoint', 'global');
112
+ const modelGlobal = view.get('advice.model', 'global');
113
+ const model = canReadProject && enabled.value === true ? view.get('advice.model', 'project') : modelGlobal;
114
+ const decidedBy = {
115
+ user: decidedSource(enabledGlobal, 'user'),
116
+ project: decidedSource(enabled, 'project') || decidedSource(model, 'project'),
117
+ };
118
+ let reason = null;
119
+ if (enabled.state === 'invalid' || enabled.origin.via === 'fail-safe' || enabled.origin.via === 'cross-key') {
120
+ reason = 'config-invalid';
121
+ } else if (enabled.value !== true) {
122
+ if (enabled.origin.layer === 'default' || enabled.origin.via === 'no-config-home') reason = 'not-configured';
123
+ else if (projectLayer(enabled.origin.layer)) reason = 'project-disabled';
124
+ else reason = 'user-disabled';
144
125
  }
145
- }
146
- return {
147
- state: 'on', reason: null, provider: base.provider, endpoint: base.endpoint,
148
- model: base.model, decidedBy, modelFrom: base.modelFrom, notes,
149
- };
150
- }
151
126
 
152
- function writeSetting({ file, setting, level = 'user', force = false, fsImpl = fs } = {}) {
153
- if (typeof file !== 'string' || !file || !isObject(setting)) {
154
- return { ok: false, reason: 'config-invalid', detail: 'file and setting object are required' };
155
- }
156
- const directory = path.dirname(file);
157
- let existing = {};
158
- try {
159
- if (fsImpl.existsSync(file)) {
160
- try {
161
- const parsed = JSON.parse(fsImpl.readFileSync(file, 'utf8'));
162
- if (!isObject(parsed)) throw new Error('setting must be a JSON object');
163
- existing = parsed;
164
- } catch (error ) {
165
- if (!force) return { ok: false, reason: 'config-invalid', detail: `${file}: ${error.message}` };
166
- }
127
+ // A project file that turned advice off (fail-safe, reason config-invalid) is not "ignored": say advice is off.
128
+ const notes = settingNotes(view, canReadProject, includeDeprecated, reason === 'config-invalid' ? decidedBy.project : null);
129
+ if (reason === 'not-configured') return offResult(reason, decidedBy, notes);
130
+ if (reason === 'config-invalid' && (enabledGlobal.state === 'invalid'
131
+ || enabledGlobal.origin.via === 'fail-safe' || enabledGlobal.origin.via === 'cross-key')) {
132
+ return offResult(reason, decidedBy, notes);
167
133
  }
168
134
 
169
- const preserved = Object.fromEntries(Object.entries(existing).filter(([key]) => !KNOWN_KEYS.has(key)));
170
- const retainedUserFields = level === 'user' && setting.enabled === false
171
- ? Object.fromEntries(['provider', 'endpoint', 'model'].filter((key) => existing[key] !== undefined).map((key) => [key, existing[key]]))
172
- : {};
173
- const output = { ...preserved, ...retainedUserFields, ...setting };
174
- fsImpl.mkdirSync(directory, { recursive: true });
175
- const suffix = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
176
- const temporary = path.join(directory, `.${path.basename(file)}.${suffix}.tmp`);
177
- try {
178
- fsImpl.writeFileSync(temporary, `${JSON.stringify(output, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
179
- fsImpl.renameSync(temporary, file);
180
- } finally {
181
- try { if (fsImpl.existsSync(temporary)) fsImpl.unlinkSync(temporary); } catch { /* keep the primary result */ }
135
+ const effectiveModel = reason === 'config-invalid' ? modelGlobal : model;
136
+ const effectiveModelFrom = modelSource(effectiveModel);
137
+ if (reason) {
138
+ return offResult(reason, decidedBy, notes, {
139
+ provider: provider.value,
140
+ endpoint: endpoint.value,
141
+ model: effectiveModel.value,
142
+ modelFrom: effectiveModelFrom,
143
+ });
182
144
  }
183
- return { ok: true, file, setting: output };
145
+ return {
146
+ state: 'on', reason: null, provider: provider.value, endpoint: endpoint.value,
147
+ model: effectiveModel.value, decidedBy, modelFrom: effectiveModelFrom, notes,
148
+ };
184
149
  } catch (error ) {
185
- return { ok: false, reason: 'config-invalid', detail: `${file}: ${error.message}` };
150
+ return offResult('config-invalid', { user: null, project: null }, [`settings resolver could not be loaded: ${error.message}`]);
186
151
  }
187
152
  }
188
153
 
189
- module.exports = { resolveSetting, writeSetting };
154
+ module.exports = { resolveSetting };