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

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 (407) 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 +1057 -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 +46 -4
  250. package/src/adapters/antigravity/index.ts +631 -0
  251. package/src/adapters/claude/index.ts +548 -0
  252. package/src/adapters/claude/mcp.ts +94 -0
  253. package/src/adapters/codex/agents.ts +208 -0
  254. package/src/adapters/codex/config.ts +272 -0
  255. package/src/adapters/codex/hooks.ts +203 -0
  256. package/src/adapters/codex/index.ts +707 -0
  257. package/src/adapters/codex/mcp.ts +271 -0
  258. package/src/adapters/codex/native-projection.ts +34 -0
  259. package/src/adapters/copilot/index.ts +413 -0
  260. package/src/adapters/copy-tree.ts +620 -0
  261. package/src/adapters/gemini/hooks.ts +210 -0
  262. package/src/adapters/gemini/index.ts +390 -0
  263. package/src/adapters/hook-commands.ts +60 -0
  264. package/src/adapters/index.ts +106 -0
  265. package/src/adapters/kiro/index.ts +310 -0
  266. package/src/adapters/mcp-entries.ts +331 -0
  267. package/src/adapters/opencode/index.ts +466 -0
  268. package/src/adapters/pi/index.ts +279 -0
  269. package/src/adapters/pi/json-members.ts +185 -0
  270. package/src/adapters/pi/mcp.ts +373 -0
  271. package/src/adapters/resolve-source.ts +19 -0
  272. package/src/cli/commands/install.ts +218 -0
  273. package/src/cli/commands/list-backups.ts +21 -0
  274. package/src/cli/commands/reconcile.ts +120 -0
  275. package/src/cli/commands/remove.ts +82 -0
  276. package/src/cli/commands/rollback.ts +172 -0
  277. package/src/cli/commands/self-update.ts +33 -0
  278. package/src/cli/commands/status.ts +138 -0
  279. package/src/cli/commands/tools.ts +92 -0
  280. package/src/cli/commands/update.ts +162 -0
  281. package/src/cli/index.ts +568 -0
  282. package/src/cli/runtime-commands.ts +281 -0
  283. package/src/cli/shared.ts +218 -0
  284. package/src/helper/bash-helper.ts +34 -0
  285. package/src/helper/git.ts +24 -0
  286. package/src/helper/guardrails.ts +49 -0
  287. package/src/helper/harness-paths.ts +140 -0
  288. package/src/helper/marker-merge.ts +116 -0
  289. package/src/helper/prompt.ts +61 -0
  290. package/src/helper/repo-root.ts +29 -0
  291. package/src/helper/settings-merge.ts +151 -0
  292. package/src/helper/settings-scope.ts +33 -0
  293. package/src/helper/toml.ts +244 -0
  294. package/src/install/backup-set.ts +149 -0
  295. package/src/install/backup.ts +724 -0
  296. package/src/install/context.ts +58 -0
  297. package/src/install/manifest.ts +145 -0
  298. package/src/install/mcp.ts +254 -0
  299. package/src/install/paths.ts +84 -0
  300. package/src/install/targets.ts +90 -0
  301. package/src/install/tool-lifecycle.ts +151 -0
  302. package/src/lifecycle/bash-availability.ts +25 -0
  303. package/src/lifecycle/index.ts +812 -0
  304. package/src/lifecycle/mcp-index.ts +28 -0
  305. package/src/lifecycle/policies.ts +73 -0
  306. package/src/lifecycle/view.ts +175 -0
  307. package/src/registry/index.ts +611 -0
  308. package/src/release/sync-plugin-versions.ts +66 -0
  309. package/src/runtime/advice/cli.ts +283 -0
  310. package/src/runtime/advice/client.ts +324 -0
  311. package/src/runtime/advice/config.ts +189 -0
  312. package/src/runtime/advice/doctor.ts +169 -0
  313. package/src/runtime/advice/endpoint-guard.ts +134 -0
  314. package/src/runtime/advice/kinds.ts +154 -0
  315. package/src/runtime/advice/providers/ollama-systemone.ts +209 -0
  316. package/src/runtime/advice/record.ts +166 -0
  317. package/src/runtime/advice/registry.ts +170 -0
  318. package/src/runtime/advice/strength.ts +66 -0
  319. package/src/runtime/brief-tools.ts +342 -0
  320. package/src/runtime/c4-project.ts +371 -0
  321. package/src/runtime/capability-router.ts +484 -0
  322. package/src/runtime/checkouts.ts +170 -0
  323. package/src/runtime/claims.ts +468 -0
  324. package/src/runtime/cli-result.ts +55 -0
  325. package/src/runtime/cli.ts +678 -0
  326. package/src/runtime/command-detect.ts +516 -0
  327. package/src/runtime/context-pack.ts +492 -0
  328. package/src/runtime/decision-register.ts +493 -0
  329. package/src/runtime/evidence-ledger.ts +338 -0
  330. package/src/runtime/failure/capture.ts +272 -0
  331. package/src/runtime/failure/classifier.ts +68 -0
  332. package/src/runtime/failure/cli.ts +178 -0
  333. package/src/runtime/failure/home.ts +64 -0
  334. package/src/runtime/failure/store.ts +226 -0
  335. package/src/runtime/feature-resolve.ts +79 -0
  336. package/src/runtime/freshness.ts +177 -0
  337. package/src/runtime/health.ts +863 -0
  338. package/src/runtime/history-compactor.ts +237 -0
  339. package/src/runtime/implementation-gate.ts +321 -0
  340. package/src/runtime/inventory/coverage.ts +218 -0
  341. package/src/runtime/inventory/identity.ts +65 -0
  342. package/src/runtime/inventory/index.ts +705 -0
  343. package/src/runtime/inventory/precedence.ts +134 -0
  344. package/src/runtime/inventory/read-scopes.ts +131 -0
  345. package/src/runtime/inventory/siblings.ts +192 -0
  346. package/src/runtime/knowledge/chunker.ts +124 -0
  347. package/src/runtime/knowledge/hybrid.ts +149 -0
  348. package/src/runtime/knowledge/index-store.ts +145 -0
  349. package/src/runtime/knowledge/retrieval.ts +194 -0
  350. package/src/runtime/leak-scan.ts +185 -0
  351. package/src/runtime/lifecycle/cli.ts +276 -0
  352. package/src/runtime/lifecycle/event-store.ts +340 -0
  353. package/src/runtime/lifecycle/fold.ts +353 -0
  354. package/src/runtime/lifecycle/followup.ts +432 -0
  355. package/src/runtime/lifecycle/goal.ts +249 -0
  356. package/src/runtime/lifecycle/intent-writer.ts +86 -0
  357. package/src/runtime/lifecycle/overview.ts +292 -0
  358. package/src/runtime/lifecycle/release.ts +241 -0
  359. package/src/runtime/lifecycle/report-store.ts +221 -0
  360. package/src/runtime/lifecycle/retention.ts +212 -0
  361. package/src/runtime/lifecycle/root.ts +71 -0
  362. package/src/runtime/lifecycle/status.ts +353 -0
  363. package/src/runtime/lifecycle/store-upkeep.ts +393 -0
  364. package/src/runtime/locator-resolve.ts +158 -0
  365. package/src/runtime/mask.ts +210 -0
  366. package/src/runtime/mcp-consultations.ts +412 -0
  367. package/src/runtime/mcp-reach.ts +157 -0
  368. package/src/runtime/model-router.ts +153 -0
  369. package/src/runtime/outcome.ts +467 -0
  370. package/src/runtime/reach.ts +148 -0
  371. package/src/runtime/readiness-record.ts +150 -0
  372. package/src/runtime/readiness.ts +435 -0
  373. package/src/runtime/recovery.ts +282 -0
  374. package/src/runtime/research-request.ts +231 -0
  375. package/src/runtime/retrieval-plan.ts +578 -0
  376. package/src/runtime/scaffold/artifacts.ts +217 -0
  377. package/src/runtime/scaffold/fingerprint.ts +56 -0
  378. package/src/runtime/scaffold/generate.ts +911 -0
  379. package/src/runtime/scaffold/languages.ts +245 -0
  380. package/src/runtime/storage/cli.ts +64 -0
  381. package/src/runtime/storage/engine.ts +249 -0
  382. package/src/runtime/storage/registry.ts +130 -0
  383. package/src/runtime/storage/roots.ts +63 -0
  384. package/src/runtime/storage/rules.ts +282 -0
  385. package/src/runtime/storage/upkeep.ts +243 -0
  386. package/src/runtime/storage/work-status.ts +326 -0
  387. package/src/runtime/task-attempts.ts +415 -0
  388. package/src/runtime/task-classifier.ts +550 -0
  389. package/src/runtime/task-scope.ts +130 -0
  390. package/src/runtime/task-state.ts +176 -0
  391. package/src/runtime/trace/indicators.ts +235 -0
  392. package/src/runtime/trace/ledger.ts +626 -0
  393. package/src/runtime/trace/render.ts +76 -0
  394. package/src/runtime/trace/views.ts +459 -0
  395. package/src/runtime/verification/contract-runner.ts +341 -0
  396. package/src/runtime/verification/engine.ts +1461 -0
  397. package/src/runtime/verification/registry.ts +130 -0
  398. package/src/runtime/verification/scope-bound.ts +418 -0
  399. package/src/runtime/verification/test-selection.ts +172 -0
  400. package/src/runtime/workflow-engine.ts +588 -0
  401. package/src/runtime/workflow-orchestrator.ts +953 -0
  402. package/src/runtime/worktree.ts +340 -0
  403. package/src/state/index.ts +193 -0
  404. package/src/state/lockfile.ts +136 -0
  405. package/src/state/run-lock.ts +408 -0
  406. package/types/node-commonjs.d.ts +68 -0
  407. package/types/runtime.d.ts +91 -0
@@ -0,0 +1,911 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ /**
6
+ * Scaffold generator — turns `requirement.md`, `design.md` and `plan.md` into a reviewable code
7
+ * scaffold under the active feature directory (plan task C.8, design component C16, FR-023).
8
+ *
9
+ * The question it answers is "what shape does my plan imply, before any of it reaches my code?".
10
+ * That is why nothing here ever writes outside `agent-docs/doflow/<slug>/scaffold/`: the output is
11
+ * something to read and argue with, not something to run. Promoting any of it into the source tree
12
+ * stays a deliberate human act, or `/do-execute-plan`'s ordinary job.
13
+ *
14
+ * Four properties are load-bearing, and each one is why some piece of this module looks the way it
15
+ * does:
16
+ *
17
+ * 1. **No implementation logic.** Every emitted body is one pinned "not implemented" signal, in
18
+ * the same grammar `core/shared/skills/do-execute-plan/references/scaffold.md` pins for
19
+ * contract frames. Generated logic nobody has verified is exactly the false confidence this
20
+ * feature exists to remove.
21
+ * 2. **No invented interfaces.** When the artifacts declare a shape, it is carried through
22
+ * verbatim as a quoted comment. When they do not, the file gets one clearly-labelled
23
+ * placeholder signature and the gap is named in `MANIFEST.md` — never a plausible-looking
24
+ * API nobody wrote down.
25
+ * 3. **Byte-identical re-runs.** Nothing generated carries a timestamp, and `MANIFEST.md`
26
+ * reports each file's *state* (generated / preserved / skipped) rather than whether this
27
+ * particular run happened to touch it. A "generated at" line would make every second run a
28
+ * diff, and idempotency is the property that makes reviewing the scaffold in git worthwhile.
29
+ * 4. **Human edits survive.** Every file carries a fingerprint of its own body. A body that no
30
+ * longer hashes to its header is human-authored from this module's point of view and is never
31
+ * rewritten — including `MANIFEST.md` itself, which is why there is no "except the report"
32
+ * carve-out below.
33
+ *
34
+ * The failure posture is the one this feature keeps re-learning the hard way — a verification check
35
+ * with no command reporting PASS, an empty contract reporting PASS over zero evidence, readiness
36
+ * grading the wrong task on a missing id. **A gate must never answer confidently about something it
37
+ * did not evaluate.** So an artifact that could not be read or parsed does not degrade into a thin
38
+ * scaffold that reads as complete: it is named in `MANIFEST.md` and the result carries a non-zero
39
+ * exit code. What was skipped is reported as prominently as what was produced.
40
+ *
41
+ * Scope boundary against the external-dependency case: this module generates the *in-scope* side —
42
+ * the components and tasks this plan owns. A `depends-on:` value with no owning task is partitioned
43
+ * here and reported, but its contract frame is produced by the walk-up algorithm in
44
+ * `references/scaffold.md`, which the model executes. That algorithm handles nested `.git`
45
+ * boundaries, non-local vendors and `external-contract:` targets; reimplementing its partition-and-walk
46
+ * here would give the framework two implementations of one behaviour, which is the thing FR-022
47
+ * guards against.
48
+ *
49
+ * Three modules split out alongside this one, each required below and named for what it contains:
50
+ * - `scaffold-artifacts.js` — parses `requirement.md`, `design.md` and `plan.md` into structures.
51
+ * - `scaffold-languages.js` — per-extension language selection, identifier casing, the language
52
+ * emitters, and `repoLanguage()`.
53
+ * - `scaffold-fingerprint.js` — the sha256 fingerprint header that makes property 4 (human edits
54
+ * survive) possible: `compose()` writes it, `inspectExisting()` reads it back.
55
+ * What remains here is orchestration: `generateScaffold()` (itself decomposed into named steps —
56
+ * see the "Generator" section below), `renderManifest()`, and the `doflow scaffold` CLI handler.
57
+ */
58
+
59
+ const nodeFs = require('node:fs');
60
+ const path = require('node:path');
61
+ const { finishRuntime } = require('../cli-result.ts');
62
+ const { resolveActiveFeature } = require('../feature-resolve.ts');
63
+ const { parseRequirement, parseDesign, parsePlan } = require('./artifacts.ts');
64
+ const {
65
+ EXTENSION_LANGUAGE,
66
+ PROSE_EXTENSIONS,
67
+ UNEMITTED_SOURCE_EXTENSIONS,
68
+ words,
69
+ camel,
70
+ pascal,
71
+ snake,
72
+ EMITTERS,
73
+ repoLanguage,
74
+ } = require('./languages.ts');
75
+ const {
76
+ FINGERPRINT_MARKER,
77
+ FINGERPRINT_RE,
78
+ sha256,
79
+ compose,
80
+ inspectExisting,
81
+ } = require('./fingerprint.ts');
82
+
83
+ // `handleScaffoldCommand` reads through the plain `fs` binding it had when it lived in bin/doflow.js;
84
+ // aliased to the `node:fs` module this file already imports as `nodeFs`.
85
+ const fs = nodeFs;
86
+
87
+ /** Output root, relative to the feature directory. Fixed by design §4.5. */
88
+ const SCAFFOLD_DIR_NAME = 'scaffold';
89
+
90
+ /** The run report. Regenerated like any other file, and protected like any other file. */
91
+ const MANIFEST_NAME = 'MANIFEST.md';
92
+
93
+ /**
94
+ * Appended to every emitted file except the manifest.
95
+ *
96
+ * Not cosmetic. `node --test` collects *any* `.js` under a directory named `test`, and pytest and
97
+ * `go test ./...` sweep just as broadly — so a scaffold that emitted `test/FR-001.test.js` would
98
+ * quietly join the host project's own test run and fail it. That was observed, not theorised: the
99
+ * first version of this module turned this repository's green suite red the moment it generated
100
+ * against its own feature directory.
101
+ *
102
+ * One suffix on everything rather than a per-toolchain exclusion list, because the invariant worth
103
+ * having is stronger than "no runner we currently know about picks this up": nothing in the
104
+ * scaffold is loadable by any toolchain until a human deliberately drops the suffix. The mirrored
105
+ * layout stays legible either way — `src/runtime/scaffold/generate.ts.stub` reads as exactly what it is.
106
+ *
107
+ * Part 2's `contracts/` frames deliberately do not carry it: `default/` exists to be compiled
108
+ * against, and its tree contains no directory a test runner sweeps.
109
+ */
110
+ const STUB_SUFFIX = '.stub';
111
+
112
+ /** The three chain artifacts, in the order a reader meets them. All three are required: a scaffold
113
+ * missing one of them is a scaffold with no traceability, no shape, or no file list. */
114
+ const ARTIFACTS: readonly string[] = Object.freeze(['requirement.md', 'design.md', 'plan.md']);
115
+
116
+ /**
117
+ * Uniform exit codes, matching design §4.2's contract for every DoFlow verb.
118
+ * `1` is "a finding the caller must act on" — which includes an unparseable artifact, so a caller
119
+ * that only checks for a crash still sees the difference between a full and a partial scaffold.
120
+ */
121
+ const EXIT: Readonly<Record<string, number>> = Object.freeze({ OK: 0, FINDING: 1, USAGE: 2 });
122
+
123
+ const STATUS: Readonly<Record<string, string>> = Object.freeze({
124
+ COMPLETE: 'COMPLETE',
125
+ INCOMPLETE: 'INCOMPLETE',
126
+ BLOCKED: 'BLOCKED',
127
+ USAGE: 'USAGE',
128
+ });
129
+
130
+ // ---------------------------------------------------------------------------------------------
131
+ // Small helpers
132
+ // ---------------------------------------------------------------------------------------------
133
+ //
134
+ // `sha256`, `compose`, `inspectExisting` (fingerprinting), the identifier-casing helpers, the
135
+ // language tables and emitters, and the artifact parsers now live in `scaffold-fingerprint.js`,
136
+ // `scaffold-languages.js` and `scaffold-artifacts.js` respectively — required above. What is left
137
+ // here is generic and used only by the generator below.
138
+
139
+ function readIfPresent(file: string, fsImpl: any): string | null {
140
+ try {
141
+ return fsImpl.readFileSync(file, 'utf8');
142
+ } catch (error: any) {
143
+ if (error && ['ENOENT', 'ENOTDIR', 'EISDIR'].includes(error.code)) return null;
144
+ throw error;
145
+ }
146
+ }
147
+
148
+ function isDirectory(file: string, fsImpl: any): boolean {
149
+ try {
150
+ return fsImpl.statSync(file).isDirectory();
151
+ } catch {
152
+ return false;
153
+ }
154
+ }
155
+
156
+ /** Table cells are pipe-delimited; an unescaped pipe in a task description would shear the row. */
157
+ const cell = (text: any): string => String(text ?? '').replace(/\|/g, '\\|').replace(/\r?\n/g, ' ').trim();
158
+
159
+ /** Sorts by a string key without depending on locale collation, which varies by platform and would
160
+ * make "re-running produces no diff" false across machines. */
161
+ const byKey = (key: (a: any) => any) => (a: any, b: any) => (key(a) < key(b) ? -1 : key(a) > key(b) ? 1 : 0);
162
+
163
+ // ---------------------------------------------------------------------------------------------
164
+ // Generator
165
+ // ---------------------------------------------------------------------------------------------
166
+ //
167
+ // `generateScaffold` used to be one ~380-line function. It is now an orchestrator over the named
168
+ // steps below, each lifted out verbatim and given the name of the one thing it does. Two arrays —
169
+ // `notEvaluated` and `skipped` — accumulate across several of these steps in the same order the
170
+ // original inline code built them in; every consumer of either array sorts it before use (see
171
+ // `buildResult`), so the merge order between steps only has to match the original for `blocked`,
172
+ // which is computed immediately after the two steps that can populate it (`readArtifacts`,
173
+ // `parseArtifacts`) and before any later step's entries are merged in.
174
+
175
+ /**
176
+ * Reads the three chain artifacts from the feature directory.
177
+ * @param {string} featureDir
178
+ * @param {Object} fsImpl
179
+ * @returns {{ sources: Array<Object>, notEvaluated: Array<Object>, texts: Object }}
180
+ */
181
+ function readArtifacts(featureDir: string, fsImpl: any): { sources: any[]; notEvaluated: any[]; texts: Record<string, string | null> } {
182
+ const sources: any[] = [];
183
+ const notEvaluated: any[] = [];
184
+ const texts: Record<string, string | null> = {};
185
+ for (const name of ARTIFACTS) {
186
+ const text = readIfPresent(path.join(featureDir, name), fsImpl);
187
+ texts[name] = text;
188
+ sources.push({ name, present: text !== null, sha256: text === null ? null : sha256(text) });
189
+ if (text === null) {
190
+ notEvaluated.push({ what: name, why: 'artifact not found in the feature directory' });
191
+ }
192
+ }
193
+ return { sources, notEvaluated, texts };
194
+ }
195
+
196
+ /**
197
+ * Parses whichever artifact texts are present into `requirement.md` / `design.md` / `plan.md`
198
+ * structures, and collects each parser's own gaps.
199
+ * @param {Object} texts keyed by artifact name, from `readArtifacts`
200
+ * @returns {{ requirement: Object, design: Object, plan: Object, notEvaluated: Array<Object> }}
201
+ */
202
+ function parseArtifacts(texts: Record<string, string | null>): { requirement: any; design: any; plan: any; notEvaluated: any[] } {
203
+ const requirement = texts['requirement.md'] ? parseRequirement(texts['requirement.md']) : { requirements: new Map(), criteria: [], gaps: [] };
204
+ const design = texts['design.md'] ? parseDesign(texts['design.md']) : { components: new Map(), gaps: [] };
205
+ const plan = texts['plan.md'] ? parsePlan(texts['plan.md']) : { tasks: [], gaps: [] };
206
+ const notEvaluated: any[] = [];
207
+ for (const gap of [...requirement.gaps, ...design.gaps, ...plan.gaps]) {
208
+ notEvaluated.push({ what: `${gap.artifact} → ${gap.what}`, why: gap.why });
209
+ }
210
+ return { requirement, design, plan, notEvaluated };
211
+ }
212
+
213
+ /**
214
+ * Builds the story → requirement → component chain that lets a generated file's header name its
215
+ * own component without anyone restating the mapping in a fourth place.
216
+ * @param {Object} requirement from `parseArtifacts`
217
+ * @param {Object} design from `parseArtifacts`
218
+ * @returns {{ requirementsForStory: Map, componentsForRequirement: Map, traceTask: Function }}
219
+ */
220
+ function buildTraceabilityIndex(requirement: any, design: any): { requirementsForStory: Map<string, any[]>; componentsForRequirement: Map<string, any[]>; traceTask: (task: any) => any } {
221
+ const requirementsForStory = new Map<string, any[]>();
222
+ for (const req of requirement.requirements.values()) {
223
+ for (const story of req.stories) {
224
+ if (!requirementsForStory.has(story)) requirementsForStory.set(story, []);
225
+ requirementsForStory.get(story)!.push(req);
226
+ }
227
+ }
228
+ const componentsForRequirement = new Map<string, any[]>();
229
+ for (const component of design.components.values()) {
230
+ for (const id of component.serves) {
231
+ if (!componentsForRequirement.has(id)) componentsForRequirement.set(id, []);
232
+ componentsForRequirement.get(id)!.push(component);
233
+ }
234
+ }
235
+
236
+ function traceTask(task: any): any {
237
+ const reqs = [...new Set(task.stories.flatMap((s: string) => requirementsForStory.get(s) || []))]
238
+ .sort(byKey((r: any) => r.id));
239
+ const comps = [...new Set(reqs.flatMap((r: any) => componentsForRequirement.get(r.id) || []))]
240
+ .sort(byKey((c: any) => c.id));
241
+ return { requirements: reqs, components: comps };
242
+ }
243
+
244
+ return { requirementsForStory, componentsForRequirement, traceTask };
245
+ }
246
+
247
+ /**
248
+ * Groups plan tasks by the file each one names, so a file with several owning tasks gets one frame.
249
+ * @param {Array<Object>} tasks from `plan.tasks`
250
+ * @returns {{ byPath: Map<string, Array<Object>>, skipped: Array<Object> }}
251
+ */
252
+ function groupTasksByFile(tasks: any[]): { byPath: Map<string, any[]>; skipped: any[] } {
253
+ const byPath = new Map<string, any[]>();
254
+ const skipped: any[] = [];
255
+ for (const task of tasks) {
256
+ if (task.files.length === 0) {
257
+ skipped.push({ item: `task ${task.id}`, why: 'no `files:` field — nothing to mirror', benign: false });
258
+ continue;
259
+ }
260
+ for (const file of task.files) {
261
+ if (!byPath.has(file)) byPath.set(file, []);
262
+ byPath.get(file)!.push(task);
263
+ }
264
+ }
265
+ return { byPath, skipped };
266
+ }
267
+
268
+ /**
269
+ * src/ — one frame per source file the plan intends to create or change.
270
+ * @param {Object} options
271
+ * @param {Map<string, Array<Object>>} options.byPath from `groupTasksByFile`
272
+ * @param {string} options.scaffoldDir
273
+ * @param {string} options.repoRoot
274
+ * @param {Object} options.fsImpl
275
+ * @param {string} options.slug
276
+ * @param {string} options.message the pinned "not implemented" signal
277
+ * @param {Function} options.traceTask from `buildTraceabilityIndex`
278
+ * @returns {{ planned: Map<string, Object>, skipped: Array<Object>, shapeUndeclared: Array<Object> }}
279
+ */
280
+ function planSourceStubs({ byPath, scaffoldDir, repoRoot, fsImpl, slug, message, traceTask }: any): { planned: Map<string, any>; skipped: any[]; shapeUndeclared: any[] } {
281
+ const planned = new Map<string, any>(); // relative-to-scaffoldDir path → { body, header, kind, from }
282
+ const skipped: any[] = [];
283
+ const shapeUndeclared: any[] = [];
284
+
285
+ for (const [file, owners] of [...byPath.entries()].sort(byKey((e: any) => e[0]))) {
286
+ const ext = path.extname(file).toLowerCase();
287
+ const label = `${file} (${owners.map((t: any) => t.id).join(', ')})`;
288
+
289
+ if (file.endsWith('/') || isDirectory(path.join(repoRoot, file), fsImpl)) {
290
+ skipped.push({ item: label, why: 'names a directory, not a file — no single signature to emit', benign: true });
291
+ continue;
292
+ }
293
+ if (PROSE_EXTENSIONS.has(ext)) {
294
+ skipped.push({ item: label, why: `${ext || 'no extension'} is content, not code — nothing to scaffold`, benign: true });
295
+ continue;
296
+ }
297
+ const unemitted = UNEMITTED_SOURCE_EXTENSIONS[ext];
298
+ if (unemitted) {
299
+ // Two different gaps, kept distinct because the reader's next move differs: one is covered by
300
+ // the model-executed grammar table in references/scaffold.md, the other is covered nowhere.
301
+ skipped.push({
302
+ item: label,
303
+ why: unemitted.inProseGrammar
304
+ ? `no scaffold emitter for ${unemitted.name}; the Default-Implementation Grammar in references/scaffold.md covers it for contract frames, this generator does not`
305
+ : `no scaffold emitter for ${unemitted.name}, and it is not in the Default-Implementation Grammar either — write this file by hand`,
306
+ benign: false,
307
+ });
308
+ continue;
309
+ }
310
+ const languageId = EXTENSION_LANGUAGE[ext];
311
+ if (!languageId) {
312
+ skipped.push({ item: label, why: `unrecognized file type '${ext || path.basename(file)}' — no language to emit`, benign: false });
313
+ continue;
314
+ }
315
+
316
+ const target = path.join(scaffoldDir, 'src', file);
317
+ if (path.relative(scaffoldDir, target).startsWith('..')) {
318
+ // A `files:` value is artifact text, and artifact text is not trusted with a write path.
319
+ skipped.push({ item: label, why: 'path escapes the scaffold directory — refused', benign: false });
320
+ continue;
321
+ }
322
+
323
+ const emitter = EMITTERS[languageId];
324
+ const trace = owners.map((t: any) => ({ task: t, ...traceTask(t) }));
325
+ const reqs = [...new Set(trace.flatMap((t: any) => t.requirements))].sort(byKey((r: any) => r.id));
326
+ const comps = [...new Set(trace.flatMap((t: any) => t.components))].sort(byKey((c: any) => c.id));
327
+ const declared = comps.flatMap((c: any) => c.declared.map((d: any) => ({ ...d, component: c.id })));
328
+
329
+ const header = [
330
+ `doflow scaffold — generated from ${slug}'s requirement.md, design.md and plan.md.`,
331
+ 'Signatures and stubs only. Do not implement here; this tree is never promoted automatically.',
332
+ '',
333
+ `intended path: ${file}`,
334
+ ...owners.map((t: any) => `task: ${t.id} — ${t.description}`),
335
+ ...(reqs.length
336
+ ? reqs.map((r: any) => `requirement: ${r.id} — ${r.summary}`)
337
+ : ['requirement: none — no user story on the owning task resolves to a requirement']),
338
+ ...(comps.length
339
+ ? comps.map((c: any) => `component: ${c.id} — ${c.name} (design.md §3)`)
340
+ : ['component: none — no design component serves this task\'s requirements']),
341
+ ];
342
+
343
+ const bodyParts: string[] = [''];
344
+ if (declared.length) {
345
+ header.push(`shape: declared in design.md §4 (${[...new Set(declared.map((d: any) => d.component))].join(', ')}), quoted below`);
346
+ for (const decl of declared) {
347
+ const lines = decl.block.split('\n');
348
+ const shown = lines.slice(0, 40);
349
+ bodyParts.push([
350
+ emitter.line(`declared interface — design.md "${decl.heading}", verbatim:`),
351
+ ...shown.map((l: string) => emitter.line(` ${l}`.replace(/\s+$/, ''))),
352
+ ...(lines.length > shown.length ? [emitter.line(` … ${lines.length - shown.length} more lines in design.md`)] : []),
353
+ '',
354
+ ].join('\n'));
355
+ }
356
+ } else {
357
+ header.push('shape: not declared in design.md §4 — placeholder signature below');
358
+ // Not a skip — the file was emitted. Conflating the two would let 29 "we wrote it, but the
359
+ // design never said what it should look like" rows hide inside a skip count, which is the
360
+ // opposite of what the manifest is for.
361
+ shapeUndeclared.push({ path: path.join('src', file) + STUB_SUFFIX, why: 'design.md §4 declares no interface for this file\'s component — the signature below is a placeholder, not a contract' });
362
+ }
363
+
364
+ const pkg = snake(words(path.basename(path.dirname(file)) || 'scaffold')).replace(/_/g, '');
365
+ const symbol = languageId === 'go'
366
+ ? pascal(words(path.basename(file)))
367
+ : languageId === 'python' || languageId === 'rust'
368
+ ? snake(words(path.basename(file)))
369
+ : camel(words(path.basename(file)));
370
+
371
+ bodyParts.push(emitter.module({ symbol, message, pkg: pkg || 'scaffold' }));
372
+ planned.set(path.join('src', file) + STUB_SUFFIX, {
373
+ content: compose(emitter, header, `${bodyParts.join('\n')}\n`),
374
+ from: owners.map((t: any) => t.id).join(', '),
375
+ shape: declared.length ? 'declared interface + placeholder signature' : 'placeholder signature',
376
+ });
377
+ }
378
+
379
+ return { planned, skipped, shapeUndeclared };
380
+ }
381
+
382
+ /**
383
+ * test/ — one stub file per requirement that has acceptance criteria.
384
+ * @param {Object} options
385
+ * @param {Object} options.requirement from `parseArtifacts`
386
+ * @param {Map} options.componentsForRequirement from `buildTraceabilityIndex`
387
+ * @param {Object} options.language from `repoLanguage`
388
+ * @param {string} options.slug
389
+ * @param {string} options.message the pinned "not implemented" signal
390
+ * @returns {{ planned: Map<string, Object>, skipped: Array<Object>, notEvaluated: Array<Object> }}
391
+ */
392
+ function planTestStubs({ requirement, componentsForRequirement, language, slug, message }: any): { planned: Map<string, any>; skipped: any[]; notEvaluated: any[] } {
393
+ const planned = new Map<string, any>();
394
+ const skipped: any[] = [];
395
+ const notEvaluated: any[] = [];
396
+
397
+ if (requirement.criteria.length > 0) {
398
+ if (!language.id) {
399
+ notEvaluated.push({
400
+ what: 'test stubs',
401
+ why: `acceptance criteria exist but the repository language is unresolved (${language.why}) — no stub language to emit in`,
402
+ });
403
+ } else {
404
+ const emitter = EMITTERS[language.id];
405
+ const grouped = new Map<string, any[]>();
406
+ for (const criterion of requirement.criteria) {
407
+ const ids = criterion.requirements.length ? criterion.requirements : ['unattributed'];
408
+ for (const id of ids) {
409
+ if (!grouped.has(id)) grouped.set(id, []);
410
+ grouped.get(id)!.push(criterion);
411
+ }
412
+ if (criterion.requirements.length === 0) {
413
+ skipped.push({ item: `acceptance criterion "${criterion.statement}"`, why: 'names no FR/NFR — stubbed under "unattributed", traceability incomplete', benign: false });
414
+ }
415
+ }
416
+ for (const [id, cases] of [...grouped.entries()].sort(byKey((e: any) => e[0]))) {
417
+ const req = requirement.requirements.get(id);
418
+ const comps = (componentsForRequirement.get(id) || []).sort(byKey((c: any) => c.id));
419
+ const header = [
420
+ `doflow scaffold — test stubs generated from ${slug}'s requirement.md acceptance criteria.`,
421
+ 'Each stub fails until implemented, on purpose: a stub that passes asserts nothing and',
422
+ 'reads as coverage. Replace the body, do not delete the failure.',
423
+ '',
424
+ `requirement: ${id}${req ? ` — ${req.summary}` : ' — not present in the requirement index'}`,
425
+ ...(comps.length ? comps.map((c: any) => `component: ${c.id} — ${c.name} (design.md §3)`) : []),
426
+ `criteria: ${cases.length} from requirement.md §6`,
427
+ `language: ${language.id} (detected from ${language.signal} by command-detect.ts)`,
428
+ ];
429
+ const name = emitter.testName(id);
430
+ planned.set(path.join('test', name) + STUB_SUFFIX, {
431
+ content: compose(emitter, header, `\n${emitter.test({ id, cases, message, pkg: 'scaffold' })}\n`),
432
+ from: `${id} acceptance criteria`,
433
+ shape: `${cases.length} failing test stub${cases.length === 1 ? '' : 's'}`,
434
+ });
435
+ }
436
+ }
437
+ }
438
+
439
+ return { planned, skipped, notEvaluated };
440
+ }
441
+
442
+ /**
443
+ * contracts/ — partitioned here, generated by the prose algorithm. See the module header for why
444
+ * the walk-up is not reimplemented in code.
445
+ * @param {Array<Object>} tasks from `plan.tasks`
446
+ * @param {Set<string>} ownedPaths every path a task in this plan already owns
447
+ * @returns {Array<Object>} externalDependencies
448
+ */
449
+ function partitionExternalDependencies(tasks: any[], ownedPaths: Set<string>): any[] {
450
+ const externalDependencies: any[] = [];
451
+ const dependencyTasks = new Map<string, any[]>();
452
+ for (const task of tasks) {
453
+ for (const dep of task.dependsOn) {
454
+ if (!dependencyTasks.has(dep)) dependencyTasks.set(dep, []);
455
+ dependencyTasks.get(dep)!.push(task);
456
+ }
457
+ }
458
+ for (const [dep, owners] of [...dependencyTasks.entries()].sort(byKey((e: any) => e[0]))) {
459
+ const inScope = [...ownedPaths].some((p) => p === dep || p.startsWith(`${dep.replace(/\/$/, '')}/`));
460
+ const docs = [...new Set(owners.map((t: any) => t.externalContract).filter(Boolean))];
461
+ externalDependencies.push({
462
+ dependency: dep,
463
+ tasks: owners.map((t: any) => t.id).sort(),
464
+ disposition: inScope
465
+ ? 'in scope — a task in this plan owns it, no contract frame needed'
466
+ : docs.length === 1
467
+ ? `external, documented — no local repo to scan; frame from ${docs[0]}`
468
+ : docs.length > 1
469
+ ? `conflicting external-contract: values (${docs.join(', ')}) — reconcile before generating`
470
+ : 'external, undocumented — walk-up derivation in references/scaffold.md decides',
471
+ });
472
+ }
473
+ return externalDependencies;
474
+ }
475
+
476
+ /**
477
+ * Writes every planned file that isn't blocked, preserving anything hand-edited.
478
+ * @param {Object} options
479
+ * @param {Map<string, Object>} options.planned
480
+ * @param {Array<Object>} options.blocked a non-empty list suppresses every write
481
+ * @param {string} options.scaffoldDir
482
+ * @param {Object} options.fsImpl
483
+ * @returns {{ written: Array<string>, unchanged: Array<string>, preserved: Array<Object> }}
484
+ */
485
+ function applyPlannedFiles({ planned, blocked, scaffoldDir, fsImpl }: any): { written: string[]; unchanged: string[]; preserved: any[] } {
486
+ const written: string[] = [];
487
+ const unchanged: string[] = [];
488
+ const preserved: any[] = [];
489
+
490
+ function place(relative: string, content: string): void {
491
+ const target = path.join(scaffoldDir, relative);
492
+ const existing = readIfPresent(target, fsImpl);
493
+ if (existing !== null) {
494
+ const { claimed, actual } = inspectExisting(existing);
495
+ if (claimed === null || claimed !== actual) {
496
+ // No fingerprint, or a body that no longer matches one: human-authored as far as this
497
+ // module can tell. Failing closed here is the whole point — the cost of a wrong guess is
498
+ // someone's work, and the cost of preserving a file nobody edited is one manifest row.
499
+ preserved.push({ path: relative, why: claimed === null ? 'no scaffold fingerprint — treated as hand-authored' : 'body no longer matches its fingerprint — edited by hand' });
500
+ return;
501
+ }
502
+ if (existing === content) { unchanged.push(relative); return; }
503
+ }
504
+ fsImpl.mkdirSync(path.dirname(target), { recursive: true });
505
+ fsImpl.writeFileSync(target, content, 'utf8');
506
+ written.push(relative);
507
+ }
508
+
509
+ if (blocked.length === 0) {
510
+ for (const [relative, entry] of [...planned.entries()].sort(byKey((e: any) => e[0]))) {
511
+ place(relative, entry.content);
512
+ }
513
+ }
514
+
515
+ return { written, unchanged, preserved };
516
+ }
517
+
518
+ /**
519
+ * Files this generator wrote on an earlier run and no longer implies. Reported, never deleted:
520
+ * removing a file a human may have started working in is a worse failure than a stale one.
521
+ * @param {Object} options
522
+ * @param {string} options.scaffoldDir
523
+ * @param {Map<string, Object>} options.planned
524
+ * @param {Object} options.fsImpl
525
+ * @returns {Array<string>} orphans
526
+ */
527
+ function findOrphans({ scaffoldDir, planned, fsImpl }: any): string[] {
528
+ const orphans: string[] = [];
529
+ (function scan(dir: string, prefix: string) {
530
+ let entries: any[];
531
+ try {
532
+ entries = fsImpl.readdirSync(dir, { withFileTypes: true });
533
+ } catch { return; }
534
+ for (const entry of entries.sort(byKey((e: any) => e.name))) {
535
+ const relative = prefix ? path.join(prefix, entry.name) : entry.name;
536
+ if (entry.isDirectory()) { scan(path.join(dir, entry.name), relative); continue; }
537
+ if (relative === MANIFEST_NAME || planned.has(relative)) continue;
538
+ const text = readIfPresent(path.join(dir, entry.name), fsImpl);
539
+ if (text !== null && FINGERPRINT_RE.test(text)) orphans.push(relative);
540
+ }
541
+ }(scaffoldDir, ''));
542
+ return orphans;
543
+ }
544
+
545
+ /**
546
+ * Determines COMPLETE / INCOMPLETE / BLOCKED and, for the empty-scaffold case, records why: zero
547
+ * files is not evidence the plan implies no shape, it is evidence nothing was evaluated — the
548
+ * empty-contract defect wearing a different hat. Mutates `notEvaluated` in place for that one case,
549
+ * exactly as the original inline code did.
550
+ * @param {Object} options
551
+ * @param {Array<Object>} options.blocked
552
+ * @param {Array<Object>} options.notEvaluated mutated when the scaffold is empty and not blocked
553
+ * @param {Array<Object>} options.skipped
554
+ * @param {number} options.plannedSize
555
+ * @returns {{ status: string }}
556
+ */
557
+ function classifyRunStatus({ blocked, notEvaluated, skipped, plannedSize }: any): { status: string } {
558
+ const realGaps = [...notEvaluated, ...skipped.filter((s: any) => !s.benign)];
559
+ const status = blocked.length
560
+ ? STATUS.BLOCKED
561
+ : realGaps.length || plannedSize === 0
562
+ ? STATUS.INCOMPLETE
563
+ : STATUS.COMPLETE;
564
+
565
+ // An empty scaffold reported as complete is the empty-contract defect wearing a different hat:
566
+ // zero files is not evidence that a plan implies no shape, it is evidence nothing was evaluated.
567
+ if (plannedSize === 0 && blocked.length === 0) {
568
+ notEvaluated.push({ what: 'the whole scaffold', why: 'no task named a file this generator can emit — an empty scaffold is not a verdict that the plan implies no code' });
569
+ }
570
+
571
+ return { status };
572
+ }
573
+
574
+ /**
575
+ * Two summaries on purpose. `summary` is what the caller prints, so it may say what this run did.
576
+ * `stateSummary` goes into the manifest and describes the scaffold, not the run — "58 written" is
577
+ * true once and false every time after, and a manifest that changes on the second identical run
578
+ * is a manifest nobody can review as a diff.
579
+ * @param {Object} options
580
+ * @returns {{ stateSummary: string, summary: string }}
581
+ */
582
+ function buildSummaries({ status, blocked, planned, shapeUndeclared, preserved, skipped, notEvaluated, externalDependencies, written, unchanged }: any): { stateSummary: string; summary: string } {
583
+ const blockedWhy = blocked.map((b: any) => b.what).join(', ');
584
+
585
+ const stateSummary = status === STATUS.BLOCKED
586
+ ? `BLOCKED — ${blockedWhy} could not be read or parsed; no scaffold written`
587
+ : `${status} — ${planned.size} file(s), ${shapeUndeclared.length} of them with no shape declared in design.md, `
588
+ + `${preserved.length} preserved (hand-edited), ${skipped.length} skipped `
589
+ + `(${skipped.filter((s: any) => !s.benign).length} of them a real gap), ${notEvaluated.length} not evaluated, `
590
+ + `${externalDependencies.length} dependency reference(s) routed to the contract-frame case`;
591
+
592
+ const summary = status === STATUS.BLOCKED
593
+ ? stateSummary
594
+ : `${stateSummary}; this run wrote ${written.length} and left ${unchanged.length} already current`;
595
+
596
+ return { stateSummary, summary };
597
+ }
598
+
599
+ /**
600
+ * Assembles the result object `generateScaffold` returns (before the manifest is written).
601
+ * @param {Object} options
602
+ * @returns {Object} result
603
+ */
604
+ function buildResult({ status, slug, featureDir, scaffoldDir, repoRoot, language, sources, planned, written, unchanged, preserved, skipped, shapeUndeclared, notEvaluated, externalDependencies, orphans, stateSummary, summary }: any): any {
605
+ return {
606
+ status,
607
+ exitCode: status === STATUS.COMPLETE ? EXIT.OK : EXIT.FINDING,
608
+ ok: status === STATUS.COMPLETE,
609
+ slug,
610
+ featureDir,
611
+ scaffoldDir,
612
+ repoRoot,
613
+ manifestPath: path.join(scaffoldDir, MANIFEST_NAME),
614
+ language,
615
+ sources,
616
+ generated: [...planned.keys()].sort(),
617
+ written: written.sort(),
618
+ unchanged: unchanged.sort(),
619
+ preserved: preserved.sort(byKey((p: any) => p.path)),
620
+ skipped: skipped.sort(byKey((s: any) => s.item)),
621
+ shapeUndeclared: shapeUndeclared.sort(byKey((s: any) => s.path)),
622
+ notEvaluated: notEvaluated.sort(byKey((n: any) => n.what)),
623
+ externalDependencies,
624
+ orphans: orphans.sort(),
625
+ stateSummary,
626
+ summary,
627
+ };
628
+ }
629
+
630
+ /**
631
+ * Renders and, unless hand-edited, writes `MANIFEST.md` — the report — and returns the final
632
+ * result. The report obeys the same edit-preservation rule as everything else: a caller that wants
633
+ * it refreshed deletes it; silently overwriting the one file a reviewer is most likely to annotate
634
+ * would trade the invariant for convenience.
635
+ * @param {Object} options
636
+ * @param {Object} options.result from `buildResult`, mutated on the hand-edited branch
637
+ * @param {Map<string, Object>} options.planned
638
+ * @param {string} options.scaffoldDir
639
+ * @param {Object} options.fsImpl
640
+ * @returns {Object} result
641
+ */
642
+ function writeManifest({ result, planned, scaffoldDir, fsImpl }: any): any {
643
+ const manifest = renderManifest(result, planned);
644
+ const manifestTarget = path.join(scaffoldDir, MANIFEST_NAME);
645
+ const existingManifest = readIfPresent(manifestTarget, fsImpl);
646
+ if (existingManifest !== null) {
647
+ const { claimed, actual } = inspectExisting(existingManifest);
648
+ if (claimed === null || claimed !== actual) {
649
+ result.preserved.push({ path: MANIFEST_NAME, why: claimed === null ? 'no scaffold fingerprint — treated as hand-authored' : 'body no longer matches its fingerprint — edited by hand' });
650
+ result.notEvaluated.push({ what: MANIFEST_NAME, why: 'hand-edited, so this run could not refresh it — delete it to regenerate' });
651
+ result.status = STATUS.INCOMPLETE;
652
+ result.exitCode = EXIT.FINDING;
653
+ result.ok = false;
654
+ return result;
655
+ }
656
+ if (existingManifest === manifest) { result.unchanged.push(MANIFEST_NAME); return result; }
657
+ }
658
+ fsImpl.mkdirSync(scaffoldDir, { recursive: true });
659
+ fsImpl.writeFileSync(manifestTarget, manifest, 'utf8');
660
+ result.written.push(MANIFEST_NAME);
661
+ result.written.sort();
662
+ return result;
663
+ }
664
+
665
+ /**
666
+ * Generates the scaffold for one feature directory.
667
+ *
668
+ * @param {Object} options
669
+ * @param {string} options.featureDir directory holding `requirement.md`, `design.md`, `plan.md`
670
+ * @param {string} [options.repoRoot] repository the plan's `files:` paths are relative to;
671
+ * defaults to the working directory. Read-only — used for language detection, never written to.
672
+ * @param {Object} [options.fsImpl] injection seam for tests; same contract as `node:fs`
673
+ * @returns {Object} result — see `summary`, `status` and `exitCode`
674
+ */
675
+ function generateScaffold(options: any = {}): any {
676
+ const fsImpl = options.fsImpl || nodeFs;
677
+ const repoRoot = path.resolve(options.repoRoot || process.cwd());
678
+ const featureDir = options.featureDir ? path.resolve(options.featureDir) : null;
679
+
680
+ const usage = (message: string): any => ({
681
+ status: STATUS.USAGE, exitCode: EXIT.USAGE, ok: false, summary: message,
682
+ featureDir, scaffoldDir: null, manifestPath: null,
683
+ generated: [], written: [], unchanged: [], preserved: [], skipped: [], notEvaluated: [],
684
+ externalDependencies: [], orphans: [],
685
+ });
686
+
687
+ if (!featureDir) return usage('no feature directory given — pass featureDir');
688
+ if (!isDirectory(featureDir, fsImpl)) return usage(`not a directory: ${featureDir}`);
689
+
690
+ const slug = path.basename(featureDir);
691
+ const scaffoldDir = path.join(featureDir, SCAFFOLD_DIR_NAME);
692
+ const message = `${slug} scaffold — not implemented`;
693
+
694
+ const { sources, notEvaluated: artifactsNotEvaluated, texts } = readArtifacts(featureDir, fsImpl);
695
+ const { requirement, design, plan, notEvaluated: parseGapsNotEvaluated } = parseArtifacts(texts);
696
+ const notEvaluated = [...artifactsNotEvaluated, ...parseGapsNotEvaluated];
697
+
698
+ const blocked = notEvaluated.filter(
699
+ (n: any) => ARTIFACTS.includes(n.what) || n.what.startsWith('plan.md → task list'),
700
+ );
701
+
702
+ const { requirementsForStory, componentsForRequirement, traceTask } = buildTraceabilityIndex(requirement, design);
703
+
704
+ const language = repoLanguage(repoRoot, fsImpl);
705
+ const { byPath, skipped: groupSkipped } = groupTasksByFile(plan.tasks);
706
+ const { planned: sourcePlanned, skipped: sourceSkipped, shapeUndeclared } = planSourceStubs({
707
+ byPath, scaffoldDir, repoRoot, fsImpl, slug, message, traceTask,
708
+ });
709
+ const { planned: testPlanned, skipped: testSkipped, notEvaluated: testNotEvaluated } = planTestStubs({
710
+ requirement, componentsForRequirement, language, slug, message,
711
+ });
712
+ notEvaluated.push(...testNotEvaluated);
713
+ const skipped = [...groupSkipped, ...sourceSkipped, ...testSkipped];
714
+ const planned = new Map([...sourcePlanned, ...testPlanned]);
715
+
716
+ const ownedPaths = new Set([...byPath.keys()]);
717
+ const externalDependencies = partitionExternalDependencies(plan.tasks, ownedPaths);
718
+
719
+ const { written, unchanged, preserved } = applyPlannedFiles({ planned, blocked, scaffoldDir, fsImpl });
720
+
721
+ const orphans = findOrphans({ scaffoldDir, planned, fsImpl });
722
+
723
+ const { status } = classifyRunStatus({ blocked, notEvaluated, skipped, plannedSize: planned.size });
724
+
725
+ const { stateSummary, summary } = buildSummaries({
726
+ status, blocked, planned, shapeUndeclared, preserved, skipped, notEvaluated, externalDependencies, written, unchanged,
727
+ });
728
+
729
+ const result = buildResult({
730
+ status, slug, featureDir, scaffoldDir, repoRoot, language, sources, planned,
731
+ written, unchanged, preserved, skipped, shapeUndeclared, notEvaluated, externalDependencies, orphans,
732
+ stateSummary, summary,
733
+ });
734
+
735
+ return writeManifest({ result, planned, scaffoldDir, fsImpl });
736
+ }
737
+
738
+ /**
739
+ * Renders `MANIFEST.md`.
740
+ *
741
+ * Deliberately reports each file's *state* rather than what this run did to it. "Written" versus
742
+ * "already current" is a property of run order, not of the scaffold, and recording it would make
743
+ * the second run of an unchanged plan produce a diff — losing the idempotency the whole artifact is
744
+ * reviewed on.
745
+ */
746
+ function renderManifest(result: any, planned: Map<string, any>): string {
747
+ const lines: string[] = [];
748
+ const table = (headers: string[], rows: any[][], empty: string) => {
749
+ if (rows.length === 0) { lines.push(empty, ''); return; }
750
+ lines.push(`| ${headers.join(' | ')} |`, `|${headers.map(() => '---').join('|')}|`);
751
+ for (const r of rows) lines.push(`| ${r.map(cell).join(' | ')} |`);
752
+ lines.push('');
753
+ };
754
+
755
+ lines.push(`# Scaffold manifest — ${result.slug}`, '');
756
+ lines.push(
757
+ 'Generated by DoFlow\'s scaffold generator (`src/runtime/scaffold/generate.ts`) from this feature\'s own',
758
+ '`requirement.md`, `design.md` and `plan.md`. Signatures, types and failing stubs only — no',
759
+ 'implementation logic, by design. Nothing here is written into the source tree; promoting any of',
760
+ 'it is a deliberate act.',
761
+ '',
762
+ 'Every file below carries a fingerprint of its own body. Edit one and this generator will report',
763
+ 'it and leave it alone rather than overwrite it — including this manifest, which must be deleted',
764
+ 'to be refreshed once edited.',
765
+ '',
766
+ );
767
+ lines.push(`**Status:** ${result.stateSummary}`, '');
768
+
769
+ lines.push('## Source artifacts', '');
770
+ table(['Artifact', 'Read', 'SHA-256'],
771
+ result.sources.map((s: any) => [s.name, s.present ? 'yes' : 'NOT FOUND', s.sha256 || '—']),
772
+ '_None read._');
773
+
774
+ lines.push('## Generated', '');
775
+ table(['Path', 'Derived from', 'Shape'],
776
+ result.generated.map((p: string) => [p, planned.get(p).from, planned.get(p).shape]),
777
+ '_Nothing generated._');
778
+
779
+ lines.push('## Emitted without a declared shape', '');
780
+ lines.push('These files exist and are traceable, but `design.md` §4 never declared an interface for',
781
+ 'them, so their signature is a placeholder rather than a contract. Reviewing the placeholder as',
782
+ 'if it were the designed API is the mistake this section exists to prevent.', '');
783
+ table(['Path', 'Why'], result.shapeUndeclared.map((s: any) => [s.path, s.why]),
784
+ '_Every emitted file has a declared shape behind it._');
785
+
786
+ lines.push('## Preserved — edited by hand, not overwritten', '');
787
+ table(['Path', 'Why'], result.preserved.map((p: any) => [p.path, p.why]),
788
+ '_No hand-edited files detected._');
789
+
790
+ lines.push('## Skipped', '');
791
+ lines.push('What a scaffold leaves out is as much a part of it as what it contains: a reader who',
792
+ 'cannot see the gaps will read the tree as the whole shape of the plan.',
793
+ '',
794
+ '"Gap" separates the two kinds. A markdown file has no signature to emit and skipping it costs',
795
+ 'nothing; a source file in a language this generator cannot emit is work that silently did not',
796
+ 'happen, and it is what holds the status below `COMPLETE`.', '');
797
+ table(['Item', 'Gap', 'Why'], result.skipped.map((s: any) => [s.item, s.benign ? 'no' : 'YES', s.why]),
798
+ '_Nothing skipped._');
799
+
800
+ lines.push('## Not evaluated', '');
801
+ lines.push('Things this run could not judge either way. These, together with every skip marked as a',
802
+ 'gap above, are why the status is not `COMPLETE`. None of them is a claim that the underlying',
803
+ 'item is fine.', '');
804
+ table(['What', 'Why'], result.notEvaluated.map((n: any) => [n.what, n.why]),
805
+ '_Everything the artifacts declared was evaluated._');
806
+
807
+ lines.push('## External dependencies', '');
808
+ lines.push('`depends-on:` values, partitioned. Frames for the external ones are produced by the',
809
+ 'walk-up algorithm in `core/shared/skills/do-execute-plan/references/scaffold.md`, which handles',
810
+ 'nested `.git` boundaries, non-local vendors and `external-contract:` targets; this generator reports',
811
+ 'the partition rather than deriving the boundaries a second way.', '');
812
+ table(['Dependency', 'Named by', 'Disposition'],
813
+ result.externalDependencies.map((d: any) => [d.dependency, d.tasks.join(', '), d.disposition]),
814
+ '_No task declares a `depends-on:` value._');
815
+
816
+ if (result.orphans.length) {
817
+ lines.push('## Orphaned', '');
818
+ lines.push('Generated by an earlier run and no longer implied by the plan. Reported, never deleted.', '');
819
+ table(['Path'], result.orphans.map((p: string) => [p]), '');
820
+ }
821
+
822
+ const body = `\n${lines.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd()}\n`;
823
+ return `<!-- ${FINGERPRINT_MARKER} sha256:${sha256(body)} -->\n${body}`;
824
+ }
825
+
826
+ // ── the `doflow scaffold` verb handler (moved from bin/doflow.js, FR-023, plan task C.11) ──────
827
+ //
828
+ // This library takes an already-resolved feature directory. The handler below is the only thing
829
+ // between it and the seam: it answers "which feature" and turns the library's result into the
830
+ // uniform report contract (design §4.2). It deliberately owns no generation logic of its own — a
831
+ // second opinion about what a scaffold contains is exactly the duplication FR-005 exists to
832
+ // prevent.
833
+
834
+ /**
835
+ * Handles `doflow scaffold` — turn the active feature's `requirement.md`, `design.md` and
836
+ * `plan.md` into a reviewable scaffold under that feature's own directory (FR-023).
837
+ *
838
+ * Exit codes come from the generator and are surfaced, never reinterpreted: `BLOCKED` and
839
+ * `INCOMPLETE` are findings the caller must act on, so they exit 1 even though a run that reports
840
+ * what it could not read has done its job. Reporting a partial scaffold as success is the precise
841
+ * failure this feature keeps correcting.
842
+ *
843
+ * @param {Object} options
844
+ * @param {boolean} [options.json=false]
845
+ * @param {string} [options.projectRoot] project whose active feature is scaffolded
846
+ * @param {string|null} [options.slug] explicit feature override
847
+ * @returns {number} process exit code
848
+ */
849
+ function handleScaffoldCommand({ json = false, projectRoot, slug = null }: any = {}): number {
850
+ const root = projectRoot || process.cwd();
851
+ const feature = resolveActiveFeature({ projectRoot: root, slug });
852
+ if (feature.error) {
853
+ if (json) console.log(JSON.stringify({ ok: false, status: 'USAGE', exitCode: 2, error: feature.error, summary: feature.message }, null, 2));
854
+ else console.error(`doflow scaffold: ${feature.message}`);
855
+ return finishRuntime(2);
856
+ }
857
+
858
+ const result = generateScaffold({ featureDir: feature.featureDir, repoRoot: feature.repoRoot, fsImpl: fs });
859
+
860
+ if (json) {
861
+ console.log(JSON.stringify(result, null, 2));
862
+ return finishRuntime(result.exitCode);
863
+ }
864
+
865
+ console.log(`\nDoFlow Scaffold [${result.status}]:`);
866
+ console.log('═'.repeat(78));
867
+ console.log(`Feature: ${result.featureDir || feature.featureDir}`);
868
+ if (result.scaffoldDir) console.log(`Output: ${result.scaffoldDir}`);
869
+ console.log(`Summary: ${result.summary}`);
870
+
871
+ // A reader who cannot see the gaps reads the tree as the whole shape of the plan, so the
872
+ // negative space is printed at the same level as the counts rather than left to the manifest.
873
+ const sections: [string, string[]][] = [
874
+ ['Skipped', (result.skipped || []).map((s: any) => `${s.item}${s.benign ? '' : ' [GAP]'} — ${s.why}`)],
875
+ ['Not evaluated', (result.notEvaluated || []).map((n: any) => `${n.what} — ${n.why}`)],
876
+ ['Preserved (hand-edited, not overwritten)', (result.preserved || []).map((p: any) => `${p.path} — ${p.why}`)],
877
+ ['Orphaned (no longer implied by the plan)', result.orphans || []],
878
+ ];
879
+ for (const [title, lines] of sections) {
880
+ if (!lines.length) continue;
881
+ console.log('─'.repeat(78));
882
+ console.log(`${title}:`);
883
+ for (const line of lines) console.log(` ${line}`);
884
+ }
885
+ console.log('═'.repeat(78) + '\n');
886
+
887
+ return finishRuntime(result.exitCode);
888
+ }
889
+
890
+ module.exports = {
891
+ generateScaffold,
892
+ renderManifest,
893
+ parseRequirement,
894
+ parseDesign,
895
+ parsePlan,
896
+ repoLanguage,
897
+ inspectExisting,
898
+ SCAFFOLD_DIR_NAME,
899
+ MANIFEST_NAME,
900
+ STUB_SUFFIX,
901
+ ARTIFACTS,
902
+ FINGERPRINT_MARKER,
903
+ FINGERPRINT_RE,
904
+ EXTENSION_LANGUAGE,
905
+ PROSE_EXTENSIONS,
906
+ UNEMITTED_SOURCE_EXTENSIONS,
907
+ EMITTERS,
908
+ EXIT,
909
+ STATUS,
910
+ handleScaffoldCommand,
911
+ };