@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,66 @@
1
+ #!/usr/bin/env bash
2
+ # sync-context.sh — idempotently write a marker-delimited block into an agent context
3
+ # file (CLAUDE.md / AGENTS.md / .github/copilot-instructions.md). Used by /do-constitution
4
+ # to propagate a pointer to the active constitution without rewriting the whole file.
5
+ #
6
+ # The block body is read from stdin; markers are added by this script. Re-running with new
7
+ # content REPLACES the previous block (idempotent — never duplicates). Deterministic so the
8
+ # model never does between-marker text surgery itself.
9
+ #
10
+ # Exit codes follow the dispatcher's uniform contract (design.md §4.2): 0 success, 1 the write
11
+ # did not land, 2 a usage error. This script used to exit 0 on a missing --file and on a failed
12
+ # write, which made every failure indistinguishable from success to its one caller
13
+ # (/do-constitution) — the skill is responsible for reporting whether the pointer landed, and it
14
+ # cannot report what it cannot observe.
15
+ #
16
+ # Usage: printf '%s' "<body>" | sync-context.sh --file <context-file>
17
+
18
+ set -uo pipefail
19
+
20
+ START="<!-- DOFLOW START -->"
21
+ END="<!-- DOFLOW END -->"
22
+
23
+ file=""
24
+ while [ $# -gt 0 ]; do
25
+ case "$1" in
26
+ # `shift 2` fails when --file is the last argument, leaving $# unchanged and the loop
27
+ # spinning forever; consume the flag first so the value shift is always safe.
28
+ --file) shift; file="${1:-}"; [ $# -gt 0 ] && shift ;;
29
+ *) shift ;;
30
+ esac
31
+ done
32
+ [ -n "$file" ] || { echo "sync-context: --file required" >&2; exit 2; }
33
+
34
+ content="$(cat)"
35
+ block="$START
36
+ $content
37
+ $END"
38
+
39
+ if [ ! -f "$file" ]; then
40
+ printf '%s\n' "$block" > "$file" || { echo "sync-context: could not create $file" >&2; exit 1; }
41
+ echo "sync-context: created $file"
42
+ exit 0
43
+ fi
44
+
45
+ if grep -qF "$START" "$file" && grep -qF "$END" "$file"; then
46
+ # $block contains embedded newlines; passing it via awk -v triggers "newline in string" on
47
+ # BSD awk (macOS). Route it through ENVIRON instead — env-var access bypasses -v's
48
+ # argument-parsing grammar on both GNU and BSD awk.
49
+ export SYNC_CONTEXT_BLOCK="$block"
50
+ awk -v s="$START" -v e="$END" '
51
+ $0==s {
52
+ n = split(ENVIRON["SYNC_CONTEXT_BLOCK"], lines, "\n")
53
+ for (i = 1; i <= n; i++) print lines[i]
54
+ skip=1; next
55
+ }
56
+ $0==e {skip=0; next}
57
+ skip!=1 {print}
58
+ ' "$file" > "$file.tmp" && mv "$file.tmp" "$file" ||
59
+ { rm -f "$file.tmp"; echo "sync-context: could not update the block in $file" >&2; exit 1; }
60
+ echo "sync-context: updated block in $file"
61
+ else
62
+ printf '\n%s\n' "$block" >> "$file" ||
63
+ { echo "sync-context: could not append the block to $file" >&2; exit 1; }
64
+ echo "sync-context: appended block to $file"
65
+ fi
66
+ exit 0
@@ -0,0 +1,526 @@
1
+ #!/usr/bin/env bash
2
+ # validate-artifacts.sh — advisory consistency checker for doflow chain artifacts.
3
+ #
4
+ # Checks that an artifact's index tables agree with the detail beside them. This is a CONSISTENCY
5
+ # checker, not a CONFORMANCE checker: it never asks whether an index had to exist, only whether one
6
+ # that does exist matches its detail. That is what lets artifacts written before this convention —
7
+ # which have no index tables at all — pass without any version marker or detection logic.
8
+ #
9
+ # Rules:
10
+ # parity index IDs and Detail IDs match, in both directions
11
+ # status every Status is "Live" or "Superseded -> <ref>"
12
+ # supersede an ID-shaped <ref> resolves to an ID present in the same artifact
13
+ # history a superseded ID has an entry in the History section (or in the archive file that
14
+ # the History section's "Earlier entries:" pointer names)
15
+ # stale a line outside History and HTML comments cites a superseded DEC-### from the
16
+ # feature's decision register without also naming a decision that replaced it. Inert
17
+ # when the feature has no decisions/register.json.
18
+ # unknown a line outside History, comments and code fences cites a DEC-### that is not in the
19
+ # feature's decision register. Inert without a register.
20
+ # rollup plan.md's phase rollup counts match its task checklist
21
+ #
22
+ # Grammar (see guidance/references/ARTIFACT_FORMAT.md) — deliberately prefix-agnostic, so
23
+ # FR/NFR/C/D/A and any future prefix work with no change here:
24
+ # indexed section a table whose first header cell is "ID", followed in the same "## " section
25
+ # by a "**Detail**" marker
26
+ # index ID first cell of each table body row
27
+ # Status the cell under the header column literally named "Status" (located by name,
28
+ # never by position, so a column added to the right cannot shift the check)
29
+ # detail entry either of two forms, both valid in the same artifact, neither deprecated:
30
+ # bullet form a line starting "- **<ID>", where <ID> is the leading token
31
+ # inside the bold; "- **FR-001:**" and
32
+ # "- **NFR-001 (qualifier):**" both parse
33
+ # heading form a line matching "#### <ID>: <text>", counted as a detail entry
34
+ # only when <ID> also appears in the first column of that
35
+ # section's index table. An ID-shaped heading with no matching
36
+ # index row is not a detail entry and is ignored, so an unrelated
37
+ # heading cannot invent a parity finding of its own
38
+ #
39
+ # Usage: validate-artifacts.sh [--json] [--slug=<slug>] [<path>...]
40
+ # No <path> → resolve the active feature via do-paths.sh and check every artifact present.
41
+ #
42
+ # Exit: 0 = clean, 1 = violations found (ADVISORY — no hook consumes this exit code),
43
+ # 0 + printed note = could not run (fail-open, matching do-prereqs.sh).
44
+
45
+ set -uo pipefail
46
+
47
+ emit_json=false
48
+ slug_override=""
49
+ targets=()
50
+ for a in "$@"; do
51
+ case "$a" in
52
+ --json) emit_json=true ;;
53
+ --slug=*) slug_override="${a#--slug=}" ;;
54
+ -*) ;; # unknown flag: ignore rather than fail the caller
55
+ *) targets+=("$a") ;;
56
+ esac
57
+ done
58
+
59
+ # Fail-open: always say why nothing was checked — an unrunnable checker must never be mistaken for
60
+ # a clean bill of health — but never obstruct the caller.
61
+ note() {
62
+ if [ "$emit_json" = true ]; then
63
+ printf '{"ok":true,"note":"%s","findings":[]}\n' "$1"
64
+ else
65
+ printf 'validate-artifacts: %s (nothing checked)\n' "$1"
66
+ fi
67
+ exit 0
68
+ }
69
+
70
+ # The resolver refuses a slug that could name a path (IC-009). That refusal is the caller's mistake,
71
+ # not an unrunnable checker, so it is passed on as the refusal it is — exit 2 with the resolver's
72
+ # own error object — rather than folded into the fail-open notes above.
73
+ refuse_invalid_slug() {
74
+ [ "$(printf '%s' "$1" | jq -r '.error // empty' 2>/dev/null)" = "invalid-slug" ] || return 0
75
+ if [ "$emit_json" = true ]; then
76
+ printf '%s\n' "$1"
77
+ else
78
+ printf 'validate-artifacts: %s\n' "$(printf '%s' "$1" | jq -r '.message')" >&2
79
+ fi
80
+ exit 2
81
+ }
82
+
83
+ # ── locate targets ────────────────────────────────────────────────────────────────────────────
84
+ if [ "${#targets[@]}" -eq 0 ]; then
85
+ command -v jq >/dev/null 2>&1 || note "jq-absent"
86
+
87
+ script_dir="$(cd "$(dirname "$0")" && pwd)"
88
+ RESOLVER="$script_dir/do-paths.sh"
89
+ [ -f "$RESOLVER" ] || RESOLVER="${DOFLOW_CONFIG_DIR:+$DOFLOW_CONFIG_DIR/scripts/doflow/bash/do-paths.sh}"
90
+ [ -f "$RESOLVER" ] || note "resolver-absent"
91
+
92
+ resolver_args=(--json)
93
+ [ -n "$slug_override" ] && resolver_args+=("--slug=$slug_override")
94
+ rc=0; json=$(bash "$RESOLVER" "${resolver_args[@]}" 2>/dev/null) || rc=$?
95
+ [ "$rc" -eq 2 ] && refuse_invalid_slug "$json"
96
+ [ "$rc" -eq 0 ] || note "resolver-error"
97
+
98
+ root=$(printf '%s' "$json" | jq -r '.repo_root // empty')
99
+ [ -n "$(printf '%s' "$json" | jq -r '.feature_slug // empty')" ] || note "no-active-feature"
100
+
101
+ # data-model.md is a default target only for a feature that has a decision register, so a folder
102
+ # from before the register validates exactly as it always did (FR-016).
103
+ keys="requirement design specs plan"
104
+ [ "$(printf '%s' "$json" | jq -r '.has_decisions // false')" = "true" ] && keys="requirement design specs data_model plan"
105
+ for key in $keys; do
106
+ p=$(printf '%s' "$json" | jq -r ".$key // empty")
107
+ [ -n "$p" ] && [ -f "$root/$p" ] && targets+=("$root/$p")
108
+ done
109
+ [ "${#targets[@]}" -gt 0 ] || note "no-artifacts-present"
110
+ elif command -v jq >/dev/null 2>&1; then
111
+ # Explicit paths skip discovery, but the stale rule still needs the feature's register. Resolve
112
+ # quietly: any failure here only leaves the rule inert, never changes what was asked for.
113
+ script_dir="$(cd "$(dirname "$0")" && pwd)"
114
+ RESOLVER="$script_dir/do-paths.sh"
115
+ [ -f "$RESOLVER" ] || RESOLVER="${DOFLOW_CONFIG_DIR:+$DOFLOW_CONFIG_DIR/scripts/doflow/bash/do-paths.sh}"
116
+ json=""
117
+ if [ -f "$RESOLVER" ]; then
118
+ resolver_args=(--json)
119
+ [ -n "$slug_override" ] && resolver_args+=("--slug=$slug_override")
120
+ rc=0; json=$(bash "$RESOLVER" "${resolver_args[@]}" 2>/dev/null) || rc=$?
121
+ [ "$rc" -eq 2 ] && refuse_invalid_slug "$json"
122
+ [ "$rc" -eq 0 ] || json=""
123
+ fi
124
+ root=$(printf '%s' "$json" | jq -r '.repo_root // empty' 2>/dev/null)
125
+ fi
126
+
127
+ # ── decision register (stale rule) ──────────────────────────────────────────────────────────────
128
+ # stale_map is "<id>:<successor>,<successor>;..." — every superseded decision with its chain, the
129
+ # live end last. Empty when there is no register, so the rule is inert; a register jq cannot read
130
+ # also leaves it empty (fail-open).
131
+ stale_map=""; known_ids=""; reg_ok=0; feat_abs=""
132
+ # The feature folder is canonicalised physically: the resolver reports a logical root in a non-git
133
+ # directory, while each file below is compared by its physical path. A symlink or /tmp vs
134
+ # /private/tmp on one side only would make the containment test silently fail.
135
+ if [ -n "${json:-}" ] && [ -n "$(printf '%s' "$json" | jq -r '.feature_dir // empty' 2>/dev/null)" ]; then
136
+ feat_abs=$(cd "$root/$(printf '%s' "$json" | jq -r '.feature_dir')" 2>/dev/null && pwd -P) || feat_abs=""
137
+ fi
138
+ if [ -n "$feat_abs" ] && [ "$(printf '%s' "$json" | jq -r '.has_decisions // false' 2>/dev/null)" = "true" ]; then
139
+ reg_file="$root/$(printf '%s' "$json" | jq -r '.decisions_register')"
140
+ # limit() bounds each chain at the decision count, so a hand-edited register whose supersededBy
141
+ # links form a cycle still terminates; a decision is never its own successor.
142
+ stale_map=$(jq -r '
143
+ .decisions as $d
144
+ | ($d | map({key: .id, value: .supersededBy}) | from_entries) as $next
145
+ | [ $d[] | select(.status == "superseded") | .id as $id
146
+ | {id: $id, chain: ([ limit($d | length; $id | recurse($next[.] // empty)) ] | .[1:] | map(select(. != $id)))} ]
147
+ | map(select(.chain | length > 0) | .id + ":" + (.chain | join(",")))
148
+ | join(";")' "$reg_file" 2>/dev/null) || stale_map=""
149
+ # Every id the register holds, as numbers ("1,2,3"): the unknown rule needs the whole set, and is
150
+ # active only when the register could be read.
151
+ if known_ids=$(jq -r '[.decisions[].id | ltrimstr("DEC-") | tonumber] | join(",")' "$reg_file" 2>/dev/null); then
152
+ reg_ok=1
153
+ else
154
+ known_ids=""
155
+ fi
156
+ fi
157
+
158
+ # ── check each target ─────────────────────────────────────────────────────────────────────────
159
+ # Findings accumulate as: <file>\t<rule>\t<id>\t<message>
160
+ findings=""
161
+ for f in "${targets[@]}"; do
162
+ if [ ! -f "$f" ]; then
163
+ findings="${findings}${f}"$'\t'"io"$'\t'"-"$'\t'"file not found"$'\n'
164
+ continue
165
+ fi
166
+ # The register belongs to one feature: an explicit path outside that feature folder must not be
167
+ # checked against it.
168
+ file_stale_map=""; file_reg_ok=0; file_known_ids=""; hist_root=""
169
+ if [ -n "$feat_abs" ]; then
170
+ case "$(cd "$(dirname "$f")" 2>/dev/null && pwd -P)/" in
171
+ "$feat_abs"/*)
172
+ file_stale_map="$stale_map"; file_reg_ok="$reg_ok"; file_known_ids="$known_ids"
173
+ # The only place a History pointer may lead: the feature's own archive directory.
174
+ hist_root=$(cd "$feat_abs/decisions/history" 2>/dev/null && pwd -P) || hist_root=""
175
+ ;;
176
+ esac
177
+ fi
178
+ # Paths reach awk through the environment: -v would process backslash escapes inside them.
179
+ out=$(DF_ART_DIR="$(dirname "$f")" DF_HIST_ROOT="$hist_root" awk -v is_plan="$([ "$(basename "$f")" = "plan.md" ] && echo 1 || echo 0)" \
180
+ -v stale_map="$file_stale_map" -v reg_ok="$file_reg_ok" -v known_ids="$file_known_ids" -v sq="'" '
181
+ # Inline markup is presentation, not value: "**Superseded → X**" and "`Live`" mean the same as
182
+ # their bare forms, so emphasis is stripped before any comparison.
183
+ function trim(s) { gsub(/[`*]/, "", s); gsub(/^[ \t]+|[ \t]+$/, "", s); return s }
184
+ function is_id(s) { return s ~ /^[A-Za-z]+-?[0-9]+$/ }
185
+ function finding(rule, id, msg) { print rule "\t" id "\t" msg }
186
+
187
+ # Blanks the parts of a line that sit inside an HTML comment, tracking comments that span
188
+ # lines, so a comment block is never read as artifact text. Mirrors the compactor: only a
189
+ # comment opening at the start of a line continues past it.
190
+ function strip_comments(s, out, p, rest) {
191
+ out = ""
192
+ while (s != "") {
193
+ if (in_comment) {
194
+ p = index(s, "-->")
195
+ if (!p) return out
196
+ s = substr(s, p + 3); in_comment = 0
197
+ } else {
198
+ p = index(s, "<!--")
199
+ if (!p) return out s
200
+ rest = substr(s, p + 4)
201
+ if (out == "" && substr(s, 1, p - 1) ~ /^[ \t]*$/) {
202
+ # A comment that starts the line may run on over later lines.
203
+ s = rest; in_comment = 1
204
+ } else if (index(rest, "-->")) {
205
+ # Mid-line, it counts only when it also closes on this line; a lone `<!--` quoted in
206
+ # prose is text and must not hide the rest of the file.
207
+ out = out substr(s, 1, p - 1); s = substr(rest, index(rest, "-->") + 3)
208
+ } else return out s
209
+ }
210
+ }
211
+ return out
212
+ }
213
+
214
+ # One `unknown` finding per DEC-### the register does not hold, then one `stale` finding per
215
+ # superseded DEC-### on the line, unless the same line also names a decision
216
+ # later in that decision chain (the line is then about the change, not a stale statement).
217
+ # awk has no \b, so a token counts only when no word character touches either end.
218
+ function check_stale(text, lineno, pos, s, l, tok, before, after, num, cn, i, j, order, cited, named, flagged, ok, word, ws, we) {
219
+ split("", order); split("", cited); split("", named); split("", flagged); cn = 0; pos = 1
220
+ while (pos <= length(text) && match(substr(text, pos), /DEC-[0-9]+/)) {
221
+ s = pos + RSTART - 1; l = RLENGTH
222
+ tok = substr(text, s, l)
223
+ before = (s > 1) ? substr(text, s - 1, 1) : ""
224
+ after = substr(text, s + l, 1)
225
+ pos = s + l
226
+ if (before ~ /[A-Za-z0-9_]/ || after ~ /[A-Za-z_]/) continue
227
+ # A token in a URL, a file path or a file name is not prose and cites nothing, so neither
228
+ # `stale` nor `unknown` reports it. Three shapes: its whitespace-delimited word holds "://"
229
+ # (https://x.test?id=DEC-095); a letter follows a "." (DEC-094.md, [n](DEC-097.md)); or a "/"
230
+ # precedes it, unless the segment before that slash is itself a DEC token (DEC-001/DEC-002 is
231
+ # prose and every half is checked). A sentence-final "DEC-099." is still a citation.
232
+ if (before == "/") {
233
+ word = substr(text, 1, s - 2); sub(/^.*[\/ \t]/, "", word); sub(/^[(\[{"`<*_]+/, "", word)
234
+ if (word !~ /^DEC-[0-9]+$/) continue
235
+ }
236
+ if (after == "." && substr(text, s + l + 1, 1) ~ /[A-Za-z]/) continue
237
+ ws = s; while (ws > 1 && substr(text, ws - 1, 1) !~ /[ \t]/) ws--
238
+ we = s + l; while (we <= length(text) && substr(text, we, 1) !~ /[ \t]/) we++
239
+ if (index(substr(text, ws, we - ws), "://")) continue
240
+ num = substr(tok, 5) + 0
241
+ named[num] = 1
242
+ if (reg_ok && !(num in known) && !(num in flagged)) {
243
+ flagged[num] = 1
244
+ finding("unknown", tok, "line " lineno " cites " tok ", which is not in the decision register")
245
+ }
246
+ if ((num in chain_end) && !(num in cited)) { cited[num] = tok; order[++cn] = num }
247
+ }
248
+ for (i = 1; i <= cn; i++) {
249
+ num = order[i]; ok = 0
250
+ for (j = 1; j <= chain_n[num]; j++) if (chain_m[num, j] in named) ok = 1
251
+ if (!ok) finding("stale", cited[num], "line " lineno " cites " cited[num] ", superseded by " chain_end[num])
252
+ }
253
+ }
254
+
255
+ # The pointer line in History names an archive file (relative to this artifact). IDs that
256
+ # moved there still count as History entries for the history rule.
257
+ # True when the directory holding `path` resolves, physically, inside <feature>/decisions/history.
258
+ # A pointer is data from the artifact; it must not make the validator read arbitrary files.
259
+ function under_hist_root(path, d, cmd, real) {
260
+ if (hist_root == "" || index(path, sq)) return 0
261
+ d = path; sub(/\/[^\/]*$/, "", d)
262
+ if (d == "") d = "/"
263
+ cmd = "cd " sq d sq " 2>/dev/null && pwd -P"
264
+ real = ""
265
+ cmd | getline real
266
+ close(cmd)
267
+ return real != "" && index(real "/", hist_root "/") == 1
268
+ }
269
+
270
+ function load_archive(line, target, path, l, c, first) {
271
+ if (!match(line, /\]\([^)]+\)/)) return
272
+ target = substr(line, RSTART + 2, RLENGTH - 3)
273
+ path = (target ~ /^\//) ? target : dir "/" target
274
+ if (!under_hist_root(path)) return
275
+ while ((getline l < path) > 0) {
276
+ if (l ~ /^[ \t]*\|/) {
277
+ if (l ~ /^[ \t]*\|[ :|-]*$/) continue
278
+ split(l, c, "|"); first = trim(c[2])
279
+ if (is_id(first)) hist[first] = 1
280
+ } else if (l ~ /^- \*\*[A-Za-z]+-?[0-9]+/) {
281
+ sub(/^- \*\*/, "", l)
282
+ if (match(l, /^[A-Za-z]+-?[0-9]+/)) hist[substr(l, 1, RLENGTH)] = 1
283
+ }
284
+ }
285
+ close(path)
286
+ }
287
+
288
+ BEGIN {
289
+ dir = ENVIRON["DF_ART_DIR"]; hist_root = ENVIRON["DF_HIST_ROOT"]
290
+ sec = 0; in_table = 0; in_rollup = 0; status_col = 0; phase = ""; in_comment = 0; cur_hist = 0; fence_ch = ""; fence_len = 0
291
+ if (reg_ok) {
292
+ nk = split(known_ids, kn, ",")
293
+ for (k = 1; k <= nk; k++) if (kn[k] != "") known[kn[k] + 0] = 1
294
+ }
295
+ if (stale_map != "") {
296
+ nrec = split(stale_map, recs, ";")
297
+ for (r = 1; r <= nrec; r++) {
298
+ split(recs[r], kv, ":"); num = substr(kv[1], 5) + 0
299
+ nc = split(kv[2], ch, ",")
300
+ chain_end[num] = ch[nc]; chain_n[num] = nc
301
+ for (j = 1; j <= nc; j++) chain_m[num, j] = substr(ch[j], 5) + 0
302
+ }
303
+ }
304
+ }
305
+
306
+ # Runs on every line, before the structural rules below (which consume lines with "next").
307
+ # Fenced code is an example, not artifact text: it neither opens a section nor cites anything.
308
+ # Fence rules follow the compactor: a run of 3+ backticks or tildes, up to 3 spaces in, closed by
309
+ # a bare run of the same character at least as long.
310
+ {
311
+ in_fence = 0
312
+ t = $0; sub(/[ \t]+$/, "", t)
313
+ if (fence_ch != "") {
314
+ in_fence = 1
315
+ if (t ~ /^ ? ? ?(`+|~+)$/) {
316
+ sub(/^ +/, "", t)
317
+ if (substr(t, 1, 1) == fence_ch && length(t) >= fence_len) fence_ch = ""
318
+ }
319
+ } else if (t ~ /^ ? ? ?(```|~~~)/) {
320
+ in_fence = 1
321
+ sub(/^ +/, "", t); fence_ch = substr(t, 1, 1); fence_len = 0
322
+ while (substr(t, fence_len + 1, 1) == fence_ch) fence_len++
323
+ }
324
+ if (!in_fence) {
325
+ vis = strip_comments($0)
326
+ if ($0 ~ /^## /) cur_hist = ($0 ~ /[Hh]istory/) ? 1 : 0
327
+ else if (cur_hist && vis ~ /^Earlier entries: \[decisions\/history\/[a-z-]+\.md\]/) load_archive(vis)
328
+ if ((stale_map != "" || reg_ok) && !cur_hist && vis != "") check_stale(vis, NR)
329
+ }
330
+ }
331
+
332
+ # With a register the fence state above gates every structural rule below: a heading, table row,
333
+ # detail entry or checklist line inside a fenced example is an example, so a quoted
334
+ # "## 9. History" or "### Phase A" cannot open a section or change a count. The fenced line still
335
+ # ends a table. Without a register the structural tracking is exactly what it always was.
336
+ reg_ok && in_fence { in_table = 0; in_rollup = 0; next }
337
+
338
+ # ── section boundaries ───────────────────────────────────────────────────────────────────
339
+ /^## / {
340
+ sec++
341
+ in_table = 0; in_rollup = 0; status_col = 0
342
+ is_hist[sec] = ($0 ~ /[Hh]istory/) ? 1 : 0
343
+ next
344
+ }
345
+
346
+ # ── "### Phase X" headings drive the rollup task count; any other ### clears the phase, so
347
+ # checkboxes under "Completion criteria" are never counted as tasks ──────────────────────
348
+ # "### Task Summary" holds the rollup table and is not a phase; "Summary" is carved out
349
+ # explicitly so a heading written as "### Phase Summary" cannot be read as a phase named
350
+ # "Summary".
351
+ /^### / {
352
+ in_table = 0; in_rollup = 0
353
+ if ($0 ~ /^### Phase / && $3 != "Summary") { phase = $3; phase_seen[phase] = 1 } else { phase = "" }
354
+ next
355
+ }
356
+
357
+ # ── tables ───────────────────────────────────────────────────────────────────────────────
358
+ /^[ \t]*\|/ {
359
+ n = split($0, cell, "|")
360
+ if ($0 ~ /^[ \t]*\|[ :|-]*$/) next # separator row
361
+
362
+ if (!in_table && !in_rollup) {
363
+ first = trim(cell[2])
364
+ if (first == "ID") {
365
+ in_table = 1; has_index[sec] = 1; status_col = 0
366
+ for (i = 2; i <= n; i++) if (trim(cell[i]) == "Status") status_col = i
367
+ next
368
+ }
369
+ if (is_plan && first == "Phase") {
370
+ rollup_col = 0
371
+ for (i = 2; i <= n; i++) if (trim(cell[i]) == "Tasks") rollup_col = i
372
+ if (rollup_col) { in_rollup = 1; saw_rollup = 1 }
373
+ next
374
+ }
375
+ next
376
+ }
377
+
378
+ if (in_rollup) {
379
+ p = trim(cell[2]); c = trim(cell[rollup_col])
380
+ if (p != "" && c ~ /^[0-9]+$/) { rollup[p] = c + 0; rollup_seen[p] = 1 }
381
+ next
382
+ }
383
+
384
+ id = trim(cell[2])
385
+ if (id == "") next
386
+ idx[sec, id] = 1; ids[sec] = ids[sec] " " id; all[id] = 1
387
+ if (status_col) status[sec, id] = trim(cell[status_col])
388
+ if (is_hist[sec]) hist[id] = 1
389
+ next
390
+ }
391
+ { in_table = 0; in_rollup = 0 } # any non-table line ends a table
392
+
393
+ /^\*\*Detail\*\*/ { has_detail[sec] = 1; next }
394
+
395
+ # The ID is the LEADING token inside the bold, not the whole of it. Real entries look like
396
+ # "- **FR-001:** ..." and "- **NFR-001 (No new hard gate — CRITICAL):** ..." — so match the ID
397
+ # prefix and ignore whatever qualifier follows it.
398
+ /^- \*\*[A-Za-z]+-?[0-9]+/ {
399
+ line = $0; sub(/^- \*\*/, "", line)
400
+ if (match(line, /^[A-Za-z]+-?[0-9]+/)) {
401
+ id = substr(line, 1, RLENGTH)
402
+ det[sec, id] = 1; dids[sec] = dids[sec] " " id; all[id] = 1
403
+ if (is_hist[sec]) hist[id] = 1
404
+ }
405
+ next
406
+ }
407
+
408
+ # The heading form of the same entry: "#### IC-002: <summary>". It counts only when the ID is
409
+ # already an index row of this section, which is decidable here because a section states its
410
+ # index table before its "**Detail**" marker, so idx is populated by the time a heading under
411
+ # that marker is read. Everything else that looks ID-shaped — "#### C4 Level 1: System Context",
412
+ # "#### Family: Checking" — matches no index row and is ignored outright: it neither satisfies a
413
+ # row nor reports an orphan of its own. Section tracking is untouched, since sec advances on
414
+ # "## " headings only.
415
+ /^#### [A-Za-z]+-?[0-9]+:/ {
416
+ line = $0; sub(/^#### /, "", line)
417
+ if (match(line, /^[A-Za-z]+-?[0-9]+/)) {
418
+ id = substr(line, 1, RLENGTH)
419
+ # Only an id the index already carries counts. Two statements the bullet rule needs are
420
+ # deliberately absent here: "all[id] = 1" is already set for every indexed id, and
421
+ # "hist[id] = 1" is redundant here in both shapes a History section can take. Under the
422
+ # documented Date-first table its ids never enter idx, so this branch is not reached at all;
423
+ # under an ID-first table the index rule above has already set hist[id] from the row. Either
424
+ # way the heading form is not the form to use in a History section -- see ARTIFACT_FORMAT.md
425
+ # section 1, which says History detail uses the bullet form.
426
+ if ((sec, id) in idx) { det[sec, id] = 1; dids[sec] = dids[sec] " " id }
427
+ }
428
+ next
429
+ }
430
+
431
+ # ── task checklist lines feed the rollup comparison ──────────────────────────────────────
432
+ is_plan && phase != "" && /^- \[[ xX]\] / { actual[phase]++ }
433
+
434
+ END {
435
+ for (s = 1; s <= sec; s++) {
436
+ if (!has_index[s] || !has_detail[s]) continue # not an indexed section — skip
437
+
438
+ ni = split(ids[s], a, " ")
439
+ for (i = 1; i <= ni; i++) {
440
+ id = a[i]; if (id == "") continue
441
+ if (!((s, id) in det)) finding("parity", id, "appears in the index but has no Detail entry")
442
+
443
+ st = status[s, id]
444
+ if (st == "" || st == "Live") continue
445
+ if (st ~ /^Superseded/) {
446
+ ref = st; sub(/^Superseded/, "", ref); gsub(/^[^A-Za-z0-9]+/, "", ref); ref = trim(ref)
447
+ sup[id] = 1
448
+ if (ref == "") finding("supersede", id, "is Superseded but names no replacement")
449
+ else if (is_id(ref) && !(ref in all)) finding("supersede", id, "is Superseded -> " ref ", which does not exist in this artifact")
450
+ } else {
451
+ finding("status", id, "Status \"" st "\" is not Live or Superseded -> <ref>")
452
+ }
453
+ }
454
+ nd = split(dids[s], b, " ")
455
+ for (i = 1; i <= nd; i++) {
456
+ id = b[i]; if (id == "") continue
457
+ if (!((s, id) in idx)) finding("parity", id, "appears in Detail but has no index row")
458
+ }
459
+ }
460
+
461
+ for (id in sup) if (!(id in hist)) finding("history", id, "is superseded but has no History entry")
462
+
463
+ # Consistency, not conformance: a plan with no rollup table at all has nothing to be
464
+ # inconsistent with, so the check is skipped rather than reported. This is the same rule that
465
+ # lets indexless sections pass, applied to the rollup.
466
+ if (is_plan && saw_rollup) {
467
+ for (p in rollup_seen) {
468
+ if (!(p in phase_seen)) { finding("rollup", "Phase " p, "is in the rollup but has no checklist section"); continue }
469
+ got = (p in actual) ? actual[p] : 0
470
+ if (got != rollup[p]) finding("rollup", "Phase " p, "rollup states " rollup[p] " tasks; the checklist has " got)
471
+ }
472
+ for (p in phase_seen) if (!(p in rollup_seen)) finding("rollup", "Phase " p, "has a checklist section but no rollup row")
473
+ }
474
+ }
475
+ ' "$f" 2>/dev/null)
476
+ awk_status=$?
477
+ # A failed parse must never look like a clean file. Empty output is indistinguishable from "no
478
+ # violations", so the exit status is what separates "checked and clean" from "never checked" —
479
+ # for instance if this runs under an awk that rejects a construct used above.
480
+ if [ "$awk_status" -ne 0 ]; then
481
+ findings="${findings}${f}"$'\t'"io"$'\t'"-"$'\t'"could not be parsed (awk exit ${awk_status}) — this file was NOT checked"$'\n'
482
+ continue
483
+ fi
484
+ if [ -n "$out" ]; then
485
+ while IFS= read -r line; do
486
+ [ -n "$line" ] && findings="${findings}${f}"$'\t'"${line}"$'\n'
487
+ done <<< "$out"
488
+ fi
489
+ done
490
+
491
+ # ── report ────────────────────────────────────────────────────────────────────────────────────
492
+ if [ -z "$findings" ]; then
493
+ if [ "$emit_json" = true ]; then
494
+ printf '{"ok":true,"findings":[]}\n'
495
+ else
496
+ printf 'validate-artifacts: %d artifact(s) checked, no violations\n' "${#targets[@]}"
497
+ fi
498
+ exit 0
499
+ fi
500
+
501
+ if [ "$emit_json" = true ]; then
502
+ # Escape backslash then double quote, in that order — reversing it would re-escape the
503
+ # backslashes this step introduces. Findings legitimately contain quotes (a status message
504
+ # names the offending value), and a path may contain either.
505
+ json_escape() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'; }
506
+ printf '{"ok":false,"findings":['
507
+ first=1
508
+ while IFS=$'\t' read -r file rule id msg; do
509
+ [ -z "$file" ] && continue
510
+ [ $first -eq 0 ] && printf ','
511
+ printf '{"file":"%s","rule":"%s","id":"%s","message":"%s"}' \
512
+ "$(json_escape "$file")" "$rule" "$(json_escape "$id")" "$(json_escape "$msg")"
513
+ first=0
514
+ done <<< "$findings"
515
+ printf ']}\n'
516
+ else
517
+ last=""
518
+ while IFS=$'\t' read -r file rule id msg; do
519
+ [ -z "$file" ] && continue
520
+ if [ "$file" != "$last" ]; then printf '%s\n' "$file"; last="$file"; fi
521
+ printf ' %-10s %s %s\n' "$rule" "$id" "$msg"
522
+ done <<< "$findings"
523
+ printf '\n%d finding(s) — advisory; the chain continues regardless.\n' \
524
+ "$(printf '%s' "$findings" | grep -c .)"
525
+ fi
526
+ exit 1