@khoavu882/doflow 0.0.0-stage → 2.0.0-beta.4

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 (409) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +132 -2
  3. package/bin/doflow.js +44 -0
  4. package/core/.antigravity-plugin/hooks.json +39 -0
  5. package/core/.antigravity-plugin/mcp_config.json +18 -0
  6. package/core/.antigravity-plugin/plugin.json +31 -0
  7. package/core/.claude-plugin/marketplace.json +20 -0
  8. package/core/.claude-plugin/plugin.json +21 -0
  9. package/core/.codex-plugin/marketplace.json +20 -0
  10. package/core/.codex-plugin/plugin.json +35 -0
  11. package/core/.plugin/plugin.json +19 -0
  12. package/core/harnesses/antigravity/hooks/pre-implementation-gate.sh +9 -0
  13. package/core/harnesses/antigravity/hooks/stop-check.sh +10 -0
  14. package/core/harnesses/claude/hooks/lib.sh +304 -0
  15. package/core/harnesses/claude/hooks/mcp-tool-guard.sh +20 -0
  16. package/core/harnesses/claude/hooks/permission-audit.sh +47 -0
  17. package/core/harnesses/claude/hooks/post-compact.sh +74 -0
  18. package/core/harnesses/claude/hooks/post-edit-lint.sh +36 -0
  19. package/core/harnesses/claude/hooks/post-tool-failure.sh +47 -0
  20. package/core/harnesses/claude/hooks/pre-bash-guard.sh +20 -0
  21. package/core/harnesses/claude/hooks/pre-compact.sh +43 -0
  22. package/core/harnesses/claude/hooks/pre-implement-gate.sh +21 -0
  23. package/core/harnesses/claude/hooks/session-end.sh +89 -0
  24. package/core/harnesses/claude/hooks/session-start.sh +5 -0
  25. package/core/harnesses/claude/hooks/skill-config-audit.sh +39 -0
  26. package/core/harnesses/claude/hooks/stop-check.sh +5 -0
  27. package/core/harnesses/claude/hooks/subagent-audit.sh +7 -0
  28. package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -0
  29. package/core/harnesses/claude/settings/keybindings.json +20 -0
  30. package/core/harnesses/claude/settings/settings.json +201 -0
  31. package/core/harnesses/claude/statusline/doflow-statusline.sh +49 -0
  32. package/core/harnesses/codex/agents/core-implementer.toml +7 -0
  33. package/core/harnesses/codex/agents/quality-guardian.toml +8 -0
  34. package/core/harnesses/codex/agents/research-writer.toml +8 -0
  35. package/core/harnesses/codex/agents/spec-analyst.toml +8 -0
  36. package/core/harnesses/codex/agents/system-architect.toml +9 -0
  37. package/core/harnesses/codex/config/config.toml +10 -0
  38. package/core/harnesses/codex/hooks/hooks.json +122 -0
  39. package/core/harnesses/codex/hooks/lib.sh +335 -0
  40. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +18 -0
  41. package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +52 -0
  42. package/core/harnesses/codex/hooks/post-edit-lint.sh +6 -0
  43. package/core/harnesses/codex/hooks/pre-bash-guard.sh +18 -0
  44. package/core/harnesses/codex/hooks/pre-compact.impl.sh +46 -0
  45. package/core/harnesses/codex/hooks/pre-compact.sh +9 -0
  46. package/core/harnesses/codex/hooks/pre-implement-gate.sh +19 -0
  47. package/core/harnesses/codex/hooks/session-end.impl.sh +92 -0
  48. package/core/harnesses/codex/hooks/session-end.sh +5 -0
  49. package/core/harnesses/codex/hooks/session-start.sh +5 -0
  50. package/core/harnesses/codex/hooks/stop-check.sh +8 -0
  51. package/core/harnesses/codex/hooks/subagent-audit.sh +8 -0
  52. package/core/harnesses/codex/hooks/user-prompt-submit.sh +6 -0
  53. package/core/harnesses/gemini/hooks/hooks.json +77 -0
  54. package/core/harnesses/gemini/hooks/lib.sh +302 -0
  55. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +21 -0
  56. package/core/harnesses/gemini/hooks/post-edit-lint.sh +39 -0
  57. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +21 -0
  58. package/core/harnesses/gemini/hooks/pre-compact.sh +38 -0
  59. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +22 -0
  60. package/core/harnesses/gemini/hooks/session-end.impl.sh +89 -0
  61. package/core/harnesses/gemini/hooks/session-end.sh +5 -0
  62. package/core/harnesses/gemini/hooks/session-start.sh +5 -0
  63. package/core/harnesses/gemini/settings/adapter-defaults.json +4 -0
  64. package/core/harnesses/kiro/agents/doflow.json +58 -0
  65. package/core/harnesses/kiro/hooks/doflow.json +53 -0
  66. package/core/harnesses/kiro/hooks/lib.sh +324 -0
  67. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +9 -0
  68. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +9 -0
  69. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +9 -0
  70. package/core/harnesses/kiro/hooks/session-start.sh +9 -0
  71. package/core/harnesses/kiro/hooks/stop-check.sh +43 -0
  72. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +39 -0
  73. package/core/harnesses/shared/hooks/adapters/antigravity.js +111 -0
  74. package/core/harnesses/shared/hooks/adapters/arg-fields.js +19 -0
  75. package/core/harnesses/shared/hooks/adapters/gemini.js +114 -0
  76. package/core/harnesses/shared/hooks/hooks.json +47 -0
  77. package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +91 -0
  78. package/core/harnesses/shared/hooks/policies/capture-failure.sh +97 -0
  79. package/core/harnesses/shared/hooks/policies/deny-json.sh +29 -0
  80. package/core/harnesses/shared/hooks/policies/lib.sh +501 -0
  81. package/core/harnesses/shared/hooks/policies/mcp-policy.conf +21 -0
  82. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +91 -0
  83. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +328 -0
  84. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +496 -0
  85. package/core/harnesses/shared/hooks/policies/prompt-nudge.jq +131 -0
  86. package/core/harnesses/shared/hooks/policies/session-context.sh +141 -0
  87. package/core/harnesses/shared/hooks/policies/stop-check.sh +264 -0
  88. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +46 -0
  89. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +235 -0
  90. package/core/harnesses/shared/hooks/stream-hook-runner.js +590 -0
  91. package/core/harnesses/shared/locator/doflow-run +87 -0
  92. package/core/registry/advice.json +101 -0
  93. package/core/registry/assets.json +792 -0
  94. package/core/registry/capabilities.json +120 -0
  95. package/core/registry/contracts.json +189 -0
  96. package/core/registry/external-tools.json +191 -0
  97. package/core/registry/harnesses.json +2062 -0
  98. package/core/registry/lifecycle.json +231 -0
  99. package/core/registry/mcp.json +18 -0
  100. package/core/registry/models.json +79 -0
  101. package/core/registry/readiness-templates.json +118 -0
  102. package/core/registry/routes.json +47 -0
  103. package/core/registry/storage.json +1061 -0
  104. package/core/registry/verification.json +258 -0
  105. package/core/registry/workflows.json +790 -0
  106. package/core/shared/agent-specs/core-implementer.md +73 -0
  107. package/core/shared/agent-specs/quality-guardian.md +28 -0
  108. package/core/shared/agent-specs/research-writer.md +31 -0
  109. package/core/shared/agent-specs/spec-analyst.md +33 -0
  110. package/core/shared/agent-specs/system-architect.md +29 -0
  111. package/core/shared/guidance/DOFLOW_CORE.md +8 -0
  112. package/core/shared/guidance/FLAGS.md +25 -0
  113. package/core/shared/guidance/PRINCIPLES.md +61 -0
  114. package/core/shared/guidance/VERSION +1 -0
  115. package/core/shared/guidance/mcp/MCP_Context7.md +18 -0
  116. package/core/shared/guidance/mcp/MCP_Sequential.md +17 -0
  117. package/core/shared/guidance/modes/MODE_Brainstorming.md +46 -0
  118. package/core/shared/guidance/modes/MODE_DeepResearch.md +69 -0
  119. package/core/shared/guidance/modes/MODE_Introspection.md +33 -0
  120. package/core/shared/guidance/modes/MODE_Orchestration.md +57 -0
  121. package/core/shared/guidance/modes/MODE_Task_Management.md +74 -0
  122. package/core/shared/guidance/pointers/claude-gemini.md +2 -0
  123. package/core/shared/guidance/pointers/codex.md +6 -0
  124. package/core/shared/guidance/references/ARTIFACT_FORMAT.md +373 -0
  125. package/core/shared/guidance/references/ARTIFACT_VOICE.md +104 -0
  126. package/core/shared/guidance/references/CONSTITUTION_BASE.md +47 -0
  127. package/core/shared/guidance/references/DIAGRAM_GUIDE.md +161 -0
  128. package/core/shared/guidance/references/DOFLOW_CHAIN.md +87 -0
  129. package/core/shared/guidance/references/EVIDENCE_LEDGER.md +93 -0
  130. package/core/shared/guidance/references/MODEL_SELECTION.md +74 -0
  131. package/core/shared/guidance/references/ORCHESTRATION.md +36 -0
  132. package/core/shared/guidance/references/RESEARCH_CONFIG.md +21 -0
  133. package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +81 -0
  134. package/core/shared/guidance/rules/RULE_01_SAFETY.md +25 -0
  135. package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +23 -0
  136. package/core/shared/guidance/rules/RULE_03_QUALITY.md +14 -0
  137. package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +46 -0
  138. package/core/shared/scripts/doflow/assets/mermaid-theme.json +46 -0
  139. package/core/shared/scripts/doflow/bash/do-exec-paths.sh +167 -0
  140. package/core/shared/scripts/doflow/bash/do-git-state.sh +546 -0
  141. package/core/shared/scripts/doflow/bash/do-parallel-check.sh +208 -0
  142. package/core/shared/scripts/doflow/bash/do-paths.sh +347 -0
  143. package/core/shared/scripts/doflow/bash/do-prereqs.sh +66 -0
  144. package/core/shared/scripts/doflow/bash/do-review-package.sh +127 -0
  145. package/core/shared/scripts/doflow/bash/do-task-brief.sh +698 -0
  146. package/core/shared/scripts/doflow/bash/render-audit.sh +224 -0
  147. package/core/shared/scripts/doflow/bash/render-diagrams.sh +177 -0
  148. package/core/shared/scripts/doflow/bash/render-puml.sh +223 -0
  149. package/core/shared/scripts/doflow/bash/sync-context.sh +66 -0
  150. package/core/shared/scripts/doflow/bash/validate-artifacts.sh +526 -0
  151. package/core/shared/scripts/doflow/bin/doflow-run +592 -0
  152. package/core/shared/skills/do/SKILL.md +128 -0
  153. package/core/shared/skills/do/references/estimation.md +13 -0
  154. package/core/shared/skills/do/references/pm_routing.md +25 -0
  155. package/core/shared/skills/do/references/tool_matrix.md +52 -0
  156. package/core/shared/skills/do-brainstorm/SKILL.md +252 -0
  157. package/core/shared/skills/do-code-review/README.md +49 -0
  158. package/core/shared/skills/do-code-review/SKILL.md +333 -0
  159. package/core/shared/skills/do-code-review/assets/sample_c_clean.c +71 -0
  160. package/core/shared/skills/do-code-review/assets/sample_c_smells.c +66 -0
  161. package/core/shared/skills/do-code-review/assets/sample_cpp_shift.cpp +19 -0
  162. package/core/shared/skills/do-code-review/assets/sample_csharp_clean.cs +100 -0
  163. package/core/shared/skills/do-code-review/assets/sample_csharp_smells.cs +78 -0
  164. package/core/shared/skills/do-code-review/assets/sample_java_clean.java +55 -0
  165. package/core/shared/skills/do-code-review/assets/sample_java_smells.java +56 -0
  166. package/core/shared/skills/do-code-review/assets/sample_json_clean.json +7 -0
  167. package/core/shared/skills/do-code-review/assets/sample_markdown_clean.md +30 -0
  168. package/core/shared/skills/do-code-review/assets/sample_markdown_smells.md +29 -0
  169. package/core/shared/skills/do-code-review/assets/sample_shell_heredoc.sh +38 -0
  170. package/core/shared/skills/do-code-review/assets/sample_shell_smells.sh +15 -0
  171. package/core/shared/skills/do-code-review/assets/sample_typescript_strings.ts +36 -0
  172. package/core/shared/skills/do-code-review/assets/sample_yaml_smells.yaml +6 -0
  173. package/core/shared/skills/do-code-review/content-types/config.md +83 -0
  174. package/core/shared/skills/do-code-review/content-types/markdown.md +132 -0
  175. package/core/shared/skills/do-code-review/expected_outputs/sample_c_clean_quality.json +79 -0
  176. package/core/shared/skills/do-code-review/expected_outputs/sample_c_smells_quality.json +169 -0
  177. package/core/shared/skills/do-code-review/expected_outputs/sample_cpp_shift_quality.json +49 -0
  178. package/core/shared/skills/do-code-review/expected_outputs/sample_csharp_clean_quality.json +149 -0
  179. package/core/shared/skills/do-code-review/expected_outputs/sample_csharp_smells_quality.json +157 -0
  180. package/core/shared/skills/do-code-review/expected_outputs/sample_java_clean_quality.json +67 -0
  181. package/core/shared/skills/do-code-review/expected_outputs/sample_java_smells_quality.json +97 -0
  182. package/core/shared/skills/do-code-review/expected_outputs/sample_json_clean_quality.json +33 -0
  183. package/core/shared/skills/do-code-review/expected_outputs/sample_markdown_clean_quality.json +1 -0
  184. package/core/shared/skills/do-code-review/expected_outputs/sample_markdown_smells_quality.json +16 -0
  185. package/core/shared/skills/do-code-review/expected_outputs/sample_shell_heredoc_quality.json +54 -0
  186. package/core/shared/skills/do-code-review/expected_outputs/sample_shell_smells_quality.json +91 -0
  187. package/core/shared/skills/do-code-review/expected_outputs/sample_typescript_strings_quality.json +60 -0
  188. package/core/shared/skills/do-code-review/expected_outputs/sample_yaml_smells_quality.json +46 -0
  189. package/core/shared/skills/do-code-review/languages/c.md +103 -0
  190. package/core/shared/skills/do-code-review/languages/cpp.md +103 -0
  191. package/core/shared/skills/do-code-review/languages/csharp.md +97 -0
  192. package/core/shared/skills/do-code-review/languages/dart.md +108 -0
  193. package/core/shared/skills/do-code-review/languages/go.md +92 -0
  194. package/core/shared/skills/do-code-review/languages/java.md +103 -0
  195. package/core/shared/skills/do-code-review/languages/kotlin.md +88 -0
  196. package/core/shared/skills/do-code-review/languages/php.md +109 -0
  197. package/core/shared/skills/do-code-review/languages/python.md +94 -0
  198. package/core/shared/skills/do-code-review/languages/ruby.md +105 -0
  199. package/core/shared/skills/do-code-review/languages/rust.md +99 -0
  200. package/core/shared/skills/do-code-review/languages/shell.md +90 -0
  201. package/core/shared/skills/do-code-review/languages/swift.md +88 -0
  202. package/core/shared/skills/do-code-review/languages/typescript.md +97 -0
  203. package/core/shared/skills/do-code-review/review-policy.json +47 -0
  204. package/core/shared/skills/do-code-review/rules/universal.md +49 -0
  205. package/core/shared/skills/do-code-review/scripts/code_quality_checker.py +1696 -0
  206. package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +532 -0
  207. package/core/shared/skills/do-code-review/scripts/pr_analyzer.py +620 -0
  208. package/core/shared/skills/do-code-review/scripts/review_report_generator.py +505 -0
  209. package/core/shared/skills/do-constitution/SKILL.md +72 -0
  210. package/core/shared/skills/do-design/SKILL.md +212 -0
  211. package/core/shared/skills/do-diagnose/SKILL.md +156 -0
  212. package/core/shared/skills/do-diagnose/references/code_audit.md +15 -0
  213. package/core/shared/skills/do-diagnose/references/refactoring.md +12 -0
  214. package/core/shared/skills/do-diagnose/references/root_cause.md +17 -0
  215. package/core/shared/skills/do-document/SKILL.md +81 -0
  216. package/core/shared/skills/do-document/references/api-reference.md +42 -0
  217. package/core/shared/skills/do-document/references/deep_research.md +18 -0
  218. package/core/shared/skills/do-document/references/embedded-research.md +11 -0
  219. package/core/shared/skills/do-document/references/implementation-flow.md +105 -0
  220. package/core/shared/skills/do-document/references/index_generator.md +9 -0
  221. package/core/shared/skills/do-document/references/user-guide.md +22 -0
  222. package/core/shared/skills/do-execute-plan/SKILL.md +261 -0
  223. package/core/shared/skills/do-execute-plan/references/handoff_record.md +57 -0
  224. package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +92 -0
  225. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +155 -0
  226. package/core/shared/skills/do-execute-plan/references/run_position.md +63 -0
  227. package/core/shared/skills/do-execute-plan/references/scaffold.md +441 -0
  228. package/core/shared/skills/do-execute-plan/references/task_attempts.md +41 -0
  229. package/core/shared/skills/do-flow/SKILL.md +186 -0
  230. package/core/shared/skills/do-flow/references/task_classes.md +134 -0
  231. package/core/shared/skills/do-git/SKILL.md +115 -0
  232. package/core/shared/skills/do-git/references/hotfix-ritual.md +44 -0
  233. package/core/shared/skills/do-git/references/lifecycle-policy.md +54 -0
  234. package/core/shared/skills/do-git/references/release-ritual.md +64 -0
  235. package/core/shared/skills/do-implement/SKILL.md +222 -0
  236. package/core/shared/skills/do-plan/SKILL.md +219 -0
  237. package/core/shared/skills/do-test/SKILL.md +113 -0
  238. package/core/shared/templates/doflow/constitution-template.md +28 -0
  239. package/core/shared/templates/doflow/data-model-template.md +169 -0
  240. package/core/shared/templates/doflow/design-template.md +193 -0
  241. package/core/shared/templates/doflow/external-contract-template.md +35 -0
  242. package/core/shared/templates/doflow/intent-template.md +45 -0
  243. package/core/shared/templates/doflow/plan-template.md +151 -0
  244. package/core/shared/templates/doflow/question-log-template.md +51 -0
  245. package/core/shared/templates/doflow/requirement-template.md +126 -0
  246. package/core/shared/templates/doflow/specs-template.md +55 -0
  247. package/core/shared/templates/doflow/state-template.md +59 -0
  248. package/core/shared/workflows/do-flow-chain.md +17 -0
  249. package/package.json +48 -4
  250. package/src/adapters/antigravity/index.js +631 -0
  251. package/src/adapters/claude/index.js +548 -0
  252. package/src/adapters/claude/mcp.js +94 -0
  253. package/src/adapters/codex/agents.js +208 -0
  254. package/src/adapters/codex/config.js +272 -0
  255. package/src/adapters/codex/hooks.js +203 -0
  256. package/src/adapters/codex/index.js +707 -0
  257. package/src/adapters/codex/mcp.js +271 -0
  258. package/src/adapters/codex/native-projection.js +34 -0
  259. package/src/adapters/copilot/index.js +413 -0
  260. package/src/adapters/copy-tree.js +620 -0
  261. package/src/adapters/gemini/hooks.js +210 -0
  262. package/src/adapters/gemini/index.js +390 -0
  263. package/src/adapters/hook-commands.js +60 -0
  264. package/src/adapters/index.js +106 -0
  265. package/src/adapters/kiro/index.js +310 -0
  266. package/src/adapters/mcp-entries.js +331 -0
  267. package/src/adapters/opencode/index.js +466 -0
  268. package/src/adapters/pi/index.js +279 -0
  269. package/src/adapters/pi/json-members.js +185 -0
  270. package/src/adapters/pi/mcp.js +373 -0
  271. package/src/adapters/resolve-source.js +19 -0
  272. package/src/cli/commands/install.js +218 -0
  273. package/src/cli/commands/list-backups.js +21 -0
  274. package/src/cli/commands/reconcile.js +120 -0
  275. package/src/cli/commands/remove.js +82 -0
  276. package/src/cli/commands/rollback.js +172 -0
  277. package/src/cli/commands/self-update.js +33 -0
  278. package/src/cli/commands/status.js +138 -0
  279. package/src/cli/commands/tools.js +92 -0
  280. package/src/cli/commands/update.js +162 -0
  281. package/src/cli/index.js +568 -0
  282. package/src/cli/runtime-commands.js +281 -0
  283. package/src/cli/shared.js +218 -0
  284. package/src/helper/bash-helper.js +34 -0
  285. package/src/helper/git.js +24 -0
  286. package/src/helper/guardrails.js +49 -0
  287. package/src/helper/harness-paths.js +140 -0
  288. package/src/helper/marker-merge.js +116 -0
  289. package/src/helper/prompt.js +61 -0
  290. package/src/helper/repo-root.js +29 -0
  291. package/src/helper/settings-merge.js +151 -0
  292. package/src/helper/settings-scope.js +33 -0
  293. package/src/helper/toml.js +244 -0
  294. package/src/install/backup-set.js +149 -0
  295. package/src/install/backup.js +724 -0
  296. package/src/install/context.js +58 -0
  297. package/src/install/manifest.js +145 -0
  298. package/src/install/mcp.js +254 -0
  299. package/src/install/paths.js +84 -0
  300. package/src/install/targets.js +90 -0
  301. package/src/install/tool-lifecycle.js +151 -0
  302. package/src/lifecycle/bash-availability.js +25 -0
  303. package/src/lifecycle/index.js +812 -0
  304. package/src/lifecycle/mcp-index.js +28 -0
  305. package/src/lifecycle/policies.js +73 -0
  306. package/src/lifecycle/view.js +175 -0
  307. package/src/registry/index.js +611 -0
  308. package/src/release/build-package.js +91 -0
  309. package/src/release/changelog-section.js +70 -0
  310. package/src/release/sync-plugin-versions.js +66 -0
  311. package/src/runtime/advice/cli.js +283 -0
  312. package/src/runtime/advice/client.js +324 -0
  313. package/src/runtime/advice/config.js +189 -0
  314. package/src/runtime/advice/doctor.js +169 -0
  315. package/src/runtime/advice/endpoint-guard.js +134 -0
  316. package/src/runtime/advice/kinds.js +154 -0
  317. package/src/runtime/advice/providers/ollama-systemone.js +209 -0
  318. package/src/runtime/advice/record.js +166 -0
  319. package/src/runtime/advice/registry.js +170 -0
  320. package/src/runtime/advice/strength.js +66 -0
  321. package/src/runtime/brief-tools.js +342 -0
  322. package/src/runtime/c4-project.js +371 -0
  323. package/src/runtime/capability-router.js +484 -0
  324. package/src/runtime/checkouts.js +170 -0
  325. package/src/runtime/claims.js +468 -0
  326. package/src/runtime/cli-result.js +55 -0
  327. package/src/runtime/cli.js +678 -0
  328. package/src/runtime/command-detect.js +516 -0
  329. package/src/runtime/context-pack.js +492 -0
  330. package/src/runtime/decision-register.js +493 -0
  331. package/src/runtime/evidence-ledger.js +338 -0
  332. package/src/runtime/failure/capture.js +272 -0
  333. package/src/runtime/failure/classifier.js +68 -0
  334. package/src/runtime/failure/cli.js +178 -0
  335. package/src/runtime/failure/home.js +64 -0
  336. package/src/runtime/failure/store.js +226 -0
  337. package/src/runtime/feature-resolve.js +79 -0
  338. package/src/runtime/freshness.js +177 -0
  339. package/src/runtime/health.js +863 -0
  340. package/src/runtime/history-compactor.js +237 -0
  341. package/src/runtime/implementation-gate.js +321 -0
  342. package/src/runtime/inventory/coverage.js +218 -0
  343. package/src/runtime/inventory/identity.js +65 -0
  344. package/src/runtime/inventory/index.js +705 -0
  345. package/src/runtime/inventory/precedence.js +134 -0
  346. package/src/runtime/inventory/read-scopes.js +131 -0
  347. package/src/runtime/inventory/siblings.js +192 -0
  348. package/src/runtime/knowledge/chunker.js +124 -0
  349. package/src/runtime/knowledge/hybrid.js +149 -0
  350. package/src/runtime/knowledge/index-store.js +145 -0
  351. package/src/runtime/knowledge/retrieval.js +194 -0
  352. package/src/runtime/leak-scan.js +185 -0
  353. package/src/runtime/lifecycle/cli.js +276 -0
  354. package/src/runtime/lifecycle/event-store.js +340 -0
  355. package/src/runtime/lifecycle/fold.js +353 -0
  356. package/src/runtime/lifecycle/followup.js +432 -0
  357. package/src/runtime/lifecycle/goal.js +249 -0
  358. package/src/runtime/lifecycle/intent-writer.js +86 -0
  359. package/src/runtime/lifecycle/overview.js +292 -0
  360. package/src/runtime/lifecycle/release.js +241 -0
  361. package/src/runtime/lifecycle/report-store.js +221 -0
  362. package/src/runtime/lifecycle/retention.js +212 -0
  363. package/src/runtime/lifecycle/root.js +71 -0
  364. package/src/runtime/lifecycle/status.js +353 -0
  365. package/src/runtime/lifecycle/store-upkeep.js +393 -0
  366. package/src/runtime/locator-resolve.js +158 -0
  367. package/src/runtime/mask.js +210 -0
  368. package/src/runtime/mcp-consultations.js +412 -0
  369. package/src/runtime/mcp-reach.js +157 -0
  370. package/src/runtime/model-router.js +153 -0
  371. package/src/runtime/outcome.js +467 -0
  372. package/src/runtime/reach.js +148 -0
  373. package/src/runtime/readiness-record.js +150 -0
  374. package/src/runtime/readiness.js +435 -0
  375. package/src/runtime/recovery.js +282 -0
  376. package/src/runtime/research-request.js +231 -0
  377. package/src/runtime/retrieval-plan.js +578 -0
  378. package/src/runtime/scaffold/artifacts.js +217 -0
  379. package/src/runtime/scaffold/fingerprint.js +56 -0
  380. package/src/runtime/scaffold/generate.js +911 -0
  381. package/src/runtime/scaffold/languages.js +245 -0
  382. package/src/runtime/storage/cli.js +64 -0
  383. package/src/runtime/storage/engine.js +249 -0
  384. package/src/runtime/storage/registry.js +130 -0
  385. package/src/runtime/storage/roots.js +63 -0
  386. package/src/runtime/storage/rules.js +282 -0
  387. package/src/runtime/storage/upkeep.js +243 -0
  388. package/src/runtime/storage/work-status.js +326 -0
  389. package/src/runtime/task-attempts.js +415 -0
  390. package/src/runtime/task-classifier.js +550 -0
  391. package/src/runtime/task-scope.js +130 -0
  392. package/src/runtime/task-state.js +176 -0
  393. package/src/runtime/trace/indicators.js +235 -0
  394. package/src/runtime/trace/ledger.js +626 -0
  395. package/src/runtime/trace/render.js +76 -0
  396. package/src/runtime/trace/views.js +459 -0
  397. package/src/runtime/verification/contract-runner.js +341 -0
  398. package/src/runtime/verification/engine.js +1461 -0
  399. package/src/runtime/verification/registry.js +130 -0
  400. package/src/runtime/verification/scope-bound.js +418 -0
  401. package/src/runtime/verification/test-selection.js +172 -0
  402. package/src/runtime/workflow-engine.js +588 -0
  403. package/src/runtime/workflow-orchestrator.js +953 -0
  404. package/src/runtime/worktree.js +340 -0
  405. package/src/state/index.js +193 -0
  406. package/src/state/lockfile.js +136 -0
  407. package/src/state/run-lock.js +408 -0
  408. package/types/node-commonjs.d.ts +68 -0
  409. package/types/runtime.d.ts +91 -0
@@ -0,0 +1,324 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ const fs = require('node:fs');
6
+ const http = require('node:http');
7
+ const https = require('node:https');
8
+ const { resolveSetting } = require('./config.js');
9
+ const { checkEndpoint } = require('./endpoint-guard.js');
10
+ const { strengthOf } = require('./strength.js');
11
+ const { validateQuestionSet, validateStaticAnswer } = require('./kinds.js');
12
+ const { appendPick, appendMiss } = require('./record.js');
13
+
14
+ // Keep this map literal: module reachability must be auditable without dynamic require paths.
15
+ const ADAPTERS = Object.freeze({
16
+ 'ollama-systemone': require('./providers/ollama-systemone.js'),
17
+ });
18
+
19
+ function durationSince(startedAt ) {
20
+ return Math.max(0, Date.now() - startedAt);
21
+ }
22
+
23
+ function versionTuple(version ) {
24
+ const match = typeof version === 'string' && version.match(/^v?(\d+)\.(\d+)\.(\d+)/);
25
+ return match ? match.slice(1).map(Number) : null;
26
+ }
27
+
28
+ function versionAtLeast(version , minimum ) {
29
+ const actual = versionTuple(version);
30
+ const required = versionTuple(minimum);
31
+ if (!actual || !required) return false;
32
+ for (let index = 0; index < 3; index += 1) {
33
+ if (actual[index] > required[index]) return true;
34
+ if (actual[index] < required[index]) return false;
35
+ }
36
+ return true;
37
+ }
38
+
39
+ function networkReason(error ) {
40
+ return error?.code === 'ADVICE_TIMEOUT' || error?.code === 'ETIMEDOUT' || error?.code === 'ESOCKETTIMEDOUT'
41
+ ? 'timeout' : 'unreachable';
42
+ }
43
+
44
+ function requestAction({ endpoint, action, pinnedLookup, timeoutMs, httpModule = http, httpsModule = https } ) {
45
+ return new Promise ((resolve, reject) => {
46
+ let request ;
47
+ let settled = false;
48
+ const finish = (error , response ) => {
49
+ if (settled) return;
50
+ settled = true;
51
+ clearTimeout(timer);
52
+ if (error) reject(error);
53
+ else resolve(response);
54
+ };
55
+ const timer = setTimeout(() => {
56
+ const error = new Error(`decision model request timed out after ${timeoutMs}ms`);
57
+ error.code = 'ADVICE_TIMEOUT';
58
+ request?.destroy(error);
59
+ finish(error);
60
+ }, timeoutMs);
61
+
62
+ try {
63
+ const url = new URL(endpoint);
64
+ const transport = url.protocol === 'https:' ? httpsModule : httpModule;
65
+ const prefix = url.pathname.replace(/\/+$/, '');
66
+ const requestPath = `${prefix}${action.path}` || action.path;
67
+ const body = action.body === undefined ? undefined : JSON.stringify(action.body);
68
+ const options = {
69
+ protocol: url.protocol,
70
+ hostname: url.hostname.replace(/^\[|\]$/g, ''),
71
+ port: url.port ? Number(url.port) : undefined,
72
+ path: requestPath,
73
+ method: action.method,
74
+ agent: false,
75
+ lookup: pinnedLookup,
76
+ headers: body === undefined ? undefined : {
77
+ 'content-type': 'application/json',
78
+ 'content-length': Buffer.byteLength(body),
79
+ },
80
+ };
81
+ request = transport.request(options, (response ) => {
82
+ const chunks = [];
83
+ response.on('data', (chunk ) => chunks.push(Buffer.from(chunk)));
84
+ response.on('error', (error ) => finish(error));
85
+ response.on('end', () => finish(null, {
86
+ status: response.statusCode || 0,
87
+ bodyText: Buffer.concat(chunks).toString('utf8'),
88
+ }));
89
+ });
90
+ request.on('error', (error ) => finish(error));
91
+ if (body === undefined) request.end();
92
+ else request.end(body);
93
+ } catch (error ) {
94
+ finish(error);
95
+ }
96
+ });
97
+ }
98
+
99
+ function emptyAdviceResult(status , reason , detail , setting , durationMs ) {
100
+ return {
101
+ status, reason, detail: detail ?? null,
102
+ provider: setting?.provider ?? null,
103
+ model: setting?.model ?? null,
104
+ endpoint: setting?.endpoint ?? null,
105
+ decidedBy: setting?.decidedBy ?? { user: null, project: null },
106
+ durationMs,
107
+ answers: [],
108
+ };
109
+ }
110
+
111
+ function createAdviceClient({
112
+ adviceRegistry,
113
+ roots,
114
+ fsImpl = fs,
115
+ lookup,
116
+ httpModule = http,
117
+ httpsModule = https,
118
+ clock = () => new Date(),
119
+ } = {}) {
120
+ async function ask({ questionSet, kind, staticAnswer, caller, taskId = null, record = true } = {}) {
121
+ const startedAt = Date.now();
122
+ let setting = null;
123
+ const effectiveKind = typeof kind === 'string' && kind ? kind : 'adhoc';
124
+ const shouldRecord = record !== false;
125
+
126
+ function unavailable(reason , detail ) {
127
+ const durationMs = durationSince(startedAt);
128
+ if (shouldRecord && setting?.state === 'on') {
129
+ try { appendMiss({ roots, now: clock, fsImpl, kind: effectiveKind, caller, reason, model: setting.model, durationMs }); }
130
+ catch { /* recording failure does not change an AdviceResult */ }
131
+ }
132
+ return emptyAdviceResult('unavailable', reason, detail, setting, durationMs);
133
+ }
134
+
135
+ try {
136
+ setting = resolveSetting({ adviceRegistry, roots, fsImpl });
137
+ if (setting.state !== 'on') {
138
+ return emptyAdviceResult('off', setting.reason, setting.notes?.[0] || null, setting, durationSince(startedAt));
139
+ }
140
+
141
+ const provider = adviceRegistry?.providers?.find((entry ) => entry.id === setting.provider);
142
+ const adapter = provider && ADAPTERS[provider.adapter];
143
+ if (!provider || !adapter) return unavailable('config-invalid', `provider adapter '${provider?.adapter || setting.provider}' is unavailable`);
144
+
145
+ if (effectiveKind !== 'adhoc') {
146
+ const namedKind = adviceRegistry.kinds?.find((entry ) => entry.id === effectiveKind);
147
+ if (!namedKind || questionSet?.questions?.length !== 1 || questionSet.questions[0]?.name !== effectiveKind) {
148
+ return unavailable('config-invalid', `question set does not match kind '${effectiveKind}'`);
149
+ }
150
+ }
151
+ const questionValidation = validateQuestionSet(questionSet, { adviceRegistry });
152
+ if (!questionValidation.ok) return unavailable('config-invalid', questionValidation.detail);
153
+ if (staticAnswer !== undefined && staticAnswer !== null) {
154
+ const staticValidation = validateStaticAnswer(questionSet.questions[0], staticAnswer);
155
+ if (!staticValidation.ok) return unavailable('config-invalid', staticValidation.detail);
156
+ }
157
+
158
+ const stateBytes = Buffer.byteLength(questionSet.state, 'utf8');
159
+ if (stateBytes > adviceRegistry.limits.maxStateBytes) {
160
+ return unavailable('over-limit', `state is ${stateBytes} bytes; the limit is ${adviceRegistry.limits.maxStateBytes} (core/registry/advice.json limits.maxStateBytes)`);
161
+ }
162
+ const request = adapter.buildRequest(questionSet, { model: setting.model });
163
+ const bodyText = JSON.stringify(request.body);
164
+ const bodyBytes = Buffer.byteLength(bodyText, 'utf8');
165
+ if (bodyBytes > adviceRegistry.limits.maxBodyBytes) {
166
+ return unavailable('over-limit', `request body is ${bodyBytes} bytes; the limit is ${adviceRegistry.limits.maxBodyBytes} (core/registry/advice.json limits.maxBodyBytes)`);
167
+ }
168
+
169
+ const endpointCheck = await checkEndpoint(setting.endpoint, {
170
+ lookup,
171
+ file: setting.decidedBy?.user || 'provider default',
172
+ timeoutMs: Math.max(0, adviceRegistry.timeoutMs - durationSince(startedAt)),
173
+ });
174
+ if (!endpointCheck.ok) return unavailable(endpointCheck.reason || 'non-loopback', endpointCheck.message);
175
+ const remainingMs = adviceRegistry.timeoutMs - durationSince(startedAt);
176
+ if (remainingMs <= 0) return unavailable('timeout', 'decision model request budget exhausted');
177
+
178
+ let response;
179
+ try {
180
+ response = await requestAction({
181
+ endpoint: setting.endpoint,
182
+ action: request,
183
+ pinnedLookup: endpointCheck.pinnedLookup,
184
+ timeoutMs: remainingMs,
185
+ httpModule,
186
+ httpsModule,
187
+ });
188
+ } catch (error ) {
189
+ const reason = networkReason(error);
190
+ return unavailable(reason, error.message);
191
+ }
192
+
193
+ let parsed;
194
+ try { parsed = adapter.parseReply(response.status, response.bodyText, questionSet); }
195
+ catch (error ) { return unavailable('malformed-reply', error.message); }
196
+ if (!parsed?.ok) return unavailable(parsed?.reason || 'malformed-reply', parsed?.detail || 'reply could not be parsed');
197
+
198
+ const answers = parsed.answers.map((answer , index ) => {
199
+ const strength = strengthOf(answer, adviceRegistry.strength);
200
+ const answerStatic = questionSet.questions.length === 1 ? staticAnswer ?? null : null;
201
+ const result = {
202
+ name: answer.name,
203
+ kind: effectiveKind,
204
+ type: answer.type,
205
+ pick: answer.pick,
206
+ strength,
207
+ probabilities: answer.probabilities,
208
+ confidence: answer.confidence ?? null,
209
+ staticAnswer: answerStatic,
210
+ agrees: answerStatic === null ? null : answer.pick === answerStatic,
211
+ pickId: null,
212
+ };
213
+ if (answer.type === 'score') {
214
+ result.pickIndex = answer.pickIndex;
215
+ result.score = answer.score;
216
+ result.criteria = answer.criteria;
217
+ }
218
+ if (shouldRecord) {
219
+ const written = appendPick({
220
+ roots, now: clock, fsImpl, kind: effectiveKind,
221
+ answer: { ...answer, strength },
222
+ question: questionSet.questions[index],
223
+ staticAnswer: answerStatic,
224
+ provider: setting.provider,
225
+ model: setting.model,
226
+ state: questionSet.state,
227
+ caller,
228
+ taskId,
229
+ durationMs: durationSince(startedAt),
230
+ });
231
+ if (written) result.pickId = written.pickId;
232
+ }
233
+ return result;
234
+ });
235
+ return {
236
+ status: 'answered', reason: null, detail: null,
237
+ provider: setting.provider, model: setting.model, endpoint: setting.endpoint,
238
+ decidedBy: setting.decidedBy, durationMs: durationSince(startedAt), answers,
239
+ };
240
+ } catch (error ) {
241
+ return unavailable('config-invalid', error?.message || 'advice request failed');
242
+ }
243
+ }
244
+
245
+ async function inspect({ endpoint, model, provider: providerId = 'ollama', file, minVersion, timeoutMs } = {}) {
246
+ const unavailable = (state , message , version = null, versionOk = false) => ({
247
+ endpoint: { state, version, versionOk, message: message ?? null },
248
+ models: [],
249
+ model: { installed: false, decisionCapable: false },
250
+ });
251
+ try {
252
+ const provider = adviceRegistry?.providers?.find((entry ) => entry.id === providerId);
253
+ const adapter = provider && ADAPTERS[provider.adapter];
254
+ if (!provider || !adapter) return unavailable('refused', `provider '${providerId}' is unavailable`);
255
+ const probeLimit = Number.isFinite(timeoutMs) ? timeoutMs : adviceRegistry.probeTimeoutMs;
256
+ const budgetMs = Number.isFinite(adviceRegistry.doctorBudgetMs) ? adviceRegistry.doctorBudgetMs : probeLimit * 4;
257
+ const startedAt = Date.now();
258
+ const remaining = () => Math.max(0, budgetMs - (Date.now() - startedAt));
259
+ const endpointCheck = await checkEndpoint(endpoint, { lookup, file, timeoutMs: remaining() });
260
+ if (!endpointCheck.ok) return unavailable(endpointCheck.reason === 'timeout' ? 'unreachable' : 'refused', endpointCheck.message);
261
+ const requestProbe = (action ) => {
262
+ const left = remaining();
263
+ if (left <= 0) throw Object.assign(new Error('decision model probe budget exhausted'), { code: 'ADVICE_TIMEOUT' });
264
+ return requestAction({
265
+ endpoint, action, pinnedLookup: endpointCheck.pinnedLookup,
266
+ timeoutMs: Math.min(probeLimit, left), httpModule, httpsModule,
267
+ });
268
+ };
269
+
270
+ let versionResponse;
271
+ try { versionResponse = await requestProbe(adapter.versionRequest()); }
272
+ catch (error ) { return unavailable('unreachable', error.message); }
273
+ const version = adapter.parseVersion(versionResponse.status, versionResponse.bodyText)?.version ?? null;
274
+ const minimum = minVersion || provider.minVersion;
275
+ const versionOk = version ? versionAtLeast(version, minimum) : false;
276
+ const endpointResult = {
277
+ state: 'reached',
278
+ version,
279
+ versionOk,
280
+ message: version ? null : `Ollama version endpoint returned HTTP ${versionResponse.status}`,
281
+ };
282
+
283
+ let models = [];
284
+ try {
285
+ const listResponse = await requestProbe(adapter.listRequest());
286
+ models = adapter.parseList(listResponse.status, listResponse.bodyText);
287
+ } catch {
288
+ return { endpoint: endpointResult, models, model: { installed: false, decisionCapable: false } };
289
+ }
290
+
291
+ const configuredIndex = models.findIndex((entry ) => adapter.modelMatches(model, entry.name));
292
+ const fallbackIndexes = models.map((entry , index ) => index)
293
+ .filter((index ) => !Array.isArray(models[index].capabilities));
294
+ if (configuredIndex >= 0 && fallbackIndexes.includes(configuredIndex)) {
295
+ fallbackIndexes.splice(fallbackIndexes.indexOf(configuredIndex), 1);
296
+ fallbackIndexes.unshift(configuredIndex);
297
+ }
298
+ for (const index of fallbackIndexes.slice(0, 8)) {
299
+ try {
300
+ const showResponse = await requestProbe(adapter.showRequest(models[index].name));
301
+ const shown = adapter.parseShow(showResponse.status, showResponse.bodyText);
302
+ if (shown) models[index] = { ...models[index], capabilities: shown.capabilities };
303
+ } catch {
304
+ // Keep capabilities unknown when the read-only fallback is unavailable.
305
+ }
306
+ }
307
+ const match = models.find((entry ) => adapter.modelMatches(model, entry.name));
308
+ return {
309
+ endpoint: endpointResult,
310
+ models,
311
+ model: {
312
+ installed: Boolean(match),
313
+ decisionCapable: Boolean(match?.capabilities?.includes(provider.capability)),
314
+ },
315
+ };
316
+ } catch (error ) {
317
+ return unavailable('unreachable', error?.message || 'decision model inspection failed');
318
+ }
319
+ }
320
+
321
+ return { ask, inspect };
322
+ }
323
+
324
+ module.exports = { createAdviceClient, versionAtLeast };
@@ -0,0 +1,189 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
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
+ }
16
+
17
+ function offResult(reason , decidedBy , notes = [], setting = {}) {
18
+ return {
19
+ state: 'off', reason,
20
+ provider: setting.provider ?? null,
21
+ endpoint: setting.endpoint ?? null,
22
+ model: setting.model ?? null,
23
+ decidedBy,
24
+ modelFrom: setting.model ? setting.modelFrom ?? null : null,
25
+ notes,
26
+ };
27
+ }
28
+
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
+ }
37
+ }
38
+
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' };
59
+ }
60
+ if (value.enabled && (typeof value.model !== 'string' || !value.model.trim())) {
61
+ return { error: 'user setting model is required when enabled is true' };
62
+ }
63
+
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
+ };
71
+ }
72
+
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' };
78
+
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 };
88
+ }
89
+
90
+ function resolveSetting({
91
+ adviceRegistry,
92
+ roots,
93
+ storageRegistry,
94
+ projectRoot,
95
+ global = false,
96
+ env = process.env,
97
+ fsImpl = fs,
98
+ } = {}) {
99
+ const notes = [];
100
+ let resolvedRoots = roots;
101
+ try {
102
+ if (!resolvedRoots) resolvedRoots = resolveRoots({ registry: storageRegistry, projectRoot, global, env });
103
+ } catch (error ) {
104
+ return offResult('config-invalid', { user: null, project: null }, [`storage roots could not be resolved: ${error.message}`]);
105
+ }
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}`]);
113
+ }
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);
121
+
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
+ }
144
+ }
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
+
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
+ }
167
+ }
168
+
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 */ }
182
+ }
183
+ return { ok: true, file, setting: output };
184
+ } catch (error ) {
185
+ return { ok: false, reason: 'config-invalid', detail: `${file}: ${error.message}` };
186
+ }
187
+ }
188
+
189
+ module.exports = { resolveSetting, writeSetting };