@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,532 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Doc Quality Checker
4
+
5
+ Analyzes markdown/prose content (SKILL.md files, agent specs, guidance-tree
6
+ references) for structural health issues: skill-creator's progressive-disclosure
7
+ length guideline, presence of an explicit Boundaries (Will / Will Not) section,
8
+ triggering-description strength, dangling references/modes/ cross-references, and
9
+ staleness markers.
10
+
11
+ This is deliberately NOT a code-smell checker: markdown/prose has no "language" in
12
+ the sense `code_quality_checker.py`'s LANGUAGE_EXTENSIONS assumes, so there is no
13
+ complexity scoring, no SOLID-violation detection, and no aggregate quality score —
14
+ per this repo's Professional Honesty principle, inventing a single quality number
15
+ for prose would claim more precision than a pattern match can support. Every
16
+ finding here is deterministic and cite-the-match: it names the exact line, phrase,
17
+ or reference that triggered it.
18
+
19
+ See `core/shared/skills/do-code-review/content-types/markdown.md` for the full
20
+ rationale behind each check.
21
+
22
+ Usage:
23
+ python doc_quality_checker.py /path/to/SKILL.md
24
+ python doc_quality_checker.py /path/to/directory
25
+ python doc_quality_checker.py . --json
26
+ """
27
+
28
+ import argparse
29
+ import json
30
+ import os
31
+ import re
32
+ import sys
33
+ from pathlib import Path
34
+ from typing import Dict, List, Optional, Tuple
35
+
36
+
37
+ # Structural thresholds, named the same way code_quality_checker.py's THRESHOLDS dict is.
38
+ THRESHOLDS = {
39
+ "skill_md_max_lines": 500,
40
+ "description_min_words": 40,
41
+ }
42
+
43
+
44
+ def read_file_content(filepath: Path) -> str:
45
+ """Read file content safely."""
46
+ try:
47
+ with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
48
+ return f.read()
49
+ except Exception:
50
+ return ""
51
+
52
+
53
+ def display_path(path: Path) -> str:
54
+ """The form a path takes in emitted output: relative to the working directory.
55
+
56
+ Mirrors code_quality_checker.py's own `display_path` exactly, so findings from both
57
+ checkers can be merged by review_report_generator.py without reconciling two path
58
+ conventions. os.path.relpath rather than Path.relative_to: the latter raises ValueError
59
+ for a target outside the working directory, which is a supported input here.
60
+ """
61
+ return os.path.relpath(str(path), os.getcwd())
62
+
63
+
64
+ def is_skill_md(path: Path) -> bool:
65
+ """True for any `.../skills/<skill-name>/SKILL.md` — matches the dispatch pattern
66
+ `*/skills/*/SKILL.md`, regardless of what precedes `skills/` in the path."""
67
+ return path.name == "SKILL.md" and path.parent.parent.name == "skills"
68
+
69
+
70
+ _TOP_LEVEL_FRONTMATTER_KEY_RE = re.compile(r"^([A-Za-z][A-Za-z0-9_-]*)\s*:")
71
+
72
+
73
+ def frontmatter_keys(lines: List[str]) -> List[str]:
74
+ """Top-level YAML frontmatter keys only — values are irrelevant here, and parsing them
75
+ would take on a YAML dependency this repo deliberately does not have (mirrors
76
+ test/guards/_shared.js's frontmatterKeys(), same reasoning)."""
77
+ if not lines or lines[0].strip() != "---":
78
+ return []
79
+ keys = []
80
+ for line in lines[1:]:
81
+ if line.strip() == "---":
82
+ break
83
+ if line[:1].isspace() or not line.strip():
84
+ continue # nested value, not a top-level key
85
+ m = _TOP_LEVEL_FRONTMATTER_KEY_RE.match(line)
86
+ if m:
87
+ keys.append(m.group(1))
88
+ return keys
89
+
90
+
91
+ def is_agent_spec(path: Path, lines: Optional[List[str]] = None) -> bool:
92
+ """True for a specialist agent-spec file. `agent-specs` is this repo's own SOURCE-TREE
93
+ directory name — every harness projects it to a differently-named directory on install
94
+ (`agents/` for claude/codex/gemini/copilot/kiro; never `agent-specs/`), so a real installed
95
+ copy would silently never match a directory-name-only check. Since DoFlow is designed to be
96
+ installed anywhere, this checks the installed-shape name too, but path name alone is too
97
+ generic (an unrelated project can have its own unrelated `agents/` folder) — content shape
98
+ is the tie-breaker: an agent-spec's frontmatter has `model:` and either `tools:` (the legacy
99
+ shape) or `effort:` (the current shared shape), but never `argument-hint:` (that key belongs
100
+ to a skill's own invocation contract, never an agent's). Its `tools:` field is optional in
101
+ the current shape; omitting it lets the host provide the agent's available tools."""
102
+ if path.suffix.lower() != ".md" or path.parent.name not in ("agent-specs", "agents"):
103
+ return False
104
+ if lines is None:
105
+ lines = read_file_content(path).splitlines()
106
+ keys = set(frontmatter_keys(lines))
107
+ return (
108
+ "model" in keys
109
+ and ("tools" in keys or "effort" in keys)
110
+ and "argument-hint" not in keys
111
+ )
112
+
113
+
114
+ def find_guidance_dir(start: Path) -> Optional[Path]:
115
+ """Walk up from `start` (a file or directory) looking for an ancestor that contains
116
+ `core/shared/guidance`. Self-contained — does not depend on the caller's cwd, so this
117
+ check works standalone on a single file passed from anywhere on disk."""
118
+ for parent in [start if start.is_dir() else start.parent, *start.parents]:
119
+ candidate = parent / "core" / "shared" / "guidance"
120
+ if candidate.is_dir():
121
+ return candidate
122
+ return None
123
+
124
+
125
+ def make_finding(path: Path, line: int, rule: str, severity: str, message: str) -> Dict:
126
+ """One finding, shaped `{file, line, rule, message}` at minimum (plus `severity`, the
127
+ same triage field code_quality_checker.py's `smells` already carry) — the schema
128
+ `review_report_generator.py` can consume from either checker without a new aggregation
129
+ path. `line` is 0 for a file-level finding with no single matching line (an absence,
130
+ like a missing section) — the same "no specific line" sentinel
131
+ review_report_generator.py's own `issue.get("line", 0)` already treats as the default.
132
+ """
133
+ return {
134
+ "file": display_path(path),
135
+ "line": line,
136
+ "rule": rule,
137
+ "severity": severity,
138
+ "message": message,
139
+ }
140
+
141
+
142
+ # --------------------------------------------------------------------------------------
143
+ # Check 1: SKILL.md length vs. skill-creator's progressive-disclosure guideline
144
+ # --------------------------------------------------------------------------------------
145
+
146
+ def check_length(path: Path, lines: List[str]) -> List[Dict]:
147
+ if not is_skill_md(path):
148
+ return []
149
+ total = len(lines)
150
+ # A file ending in a single trailing newline splits into one extra empty element —
151
+ # that is not a line of content, so it should not count toward the guideline.
152
+ if total and lines[-1] == "":
153
+ total -= 1
154
+ limit = THRESHOLDS["skill_md_max_lines"]
155
+ if total <= limit:
156
+ return []
157
+ return [make_finding(
158
+ path, total, "skill_md_length", "medium",
159
+ f"SKILL.md has {total} lines, exceeding skill-creator's ~{limit}-line "
160
+ "progressive-disclosure guideline — split overflow into a references/ file so this "
161
+ "stays a dispatch/overview surface."
162
+ )]
163
+
164
+
165
+ # --------------------------------------------------------------------------------------
166
+ # Check 2: Boundaries (Will / Will Not) section, for SKILL.md and agent-spec files
167
+ # --------------------------------------------------------------------------------------
168
+
169
+ _BOUNDARIES_HEADING_RE = re.compile(r'^#{1,6}\s*Boundaries\b', re.IGNORECASE)
170
+ _HEADING_RE = re.compile(r'^#{1,6}\s')
171
+ _WILL_RE = re.compile(r'\*\*Will:?\*\*', re.IGNORECASE)
172
+ _WILL_NOT_RE = re.compile(r'\*\*Will Not:?\*\*', re.IGNORECASE)
173
+
174
+
175
+ def _find_boundaries_section(lines: List[str]) -> Tuple[Optional[int], str]:
176
+ """Return (0-based heading line index or None, section text from the heading to the
177
+ next heading or EOF). section text is "" when no heading is found."""
178
+ heading_idx = None
179
+ for i, line in enumerate(lines):
180
+ if _BOUNDARIES_HEADING_RE.match(line.strip()):
181
+ heading_idx = i
182
+ break
183
+ if heading_idx is None:
184
+ return None, ""
185
+ end_idx = len(lines)
186
+ for j in range(heading_idx + 1, len(lines)):
187
+ if _HEADING_RE.match(lines[j]):
188
+ end_idx = j
189
+ break
190
+ return heading_idx, "\n".join(lines[heading_idx:end_idx])
191
+
192
+
193
+ def check_boundaries(path: Path, lines: List[str]) -> List[Dict]:
194
+ if not (is_skill_md(path) or is_agent_spec(path, lines)):
195
+ return []
196
+
197
+ heading_idx, section_text = _find_boundaries_section(lines)
198
+
199
+ if heading_idx is not None:
200
+ if _WILL_RE.search(section_text) and _WILL_NOT_RE.search(section_text):
201
+ return []
202
+ return [make_finding(
203
+ path, heading_idx + 1, "missing_boundaries", "medium",
204
+ "Has a '## Boundaries' heading but no explicit '**Will:**' / '**Will Not:**' "
205
+ "framing within it — scope is headed but not actually stated."
206
+ )]
207
+
208
+ # No heading at all — an equivalent Will/Will-Not framing elsewhere in the file still
209
+ # counts, per this check's own "or equivalent framing" allowance.
210
+ full_text = "\n".join(lines)
211
+ if _WILL_RE.search(full_text) and _WILL_NOT_RE.search(full_text):
212
+ return []
213
+ return [make_finding(
214
+ path, 0, "missing_boundaries", "medium",
215
+ "No '## Boundaries' heading or equivalent '**Will:**' / '**Will Not:**' framing "
216
+ "found — this file's scope is not explicit."
217
+ )]
218
+
219
+
220
+ # --------------------------------------------------------------------------------------
221
+ # Check 3: triggering description strength, SKILL.md frontmatter only
222
+ # --------------------------------------------------------------------------------------
223
+
224
+ # Same shape as G8's own `argument-hint` extraction regex (test/guards/_shared.js), applied
225
+ # to `description:` instead — an optional wrapping quote, non-greedy body, trailing quote.
226
+ _FRONTMATTER_DESCRIPTION_RE = re.compile(r'^description:\s*"?(.*?)"?\s*$')
227
+
228
+ _TRIGGER_SIGNAL_RE = re.compile(
229
+ r"\buse\s+(when|whenever|if)\b|\bfor\s+example\b|\be\.g\.|['\"][^'\"]{6,}['\"]",
230
+ re.IGNORECASE,
231
+ )
232
+
233
+
234
+ def _parse_frontmatter_description(lines: List[str]) -> Optional[Dict]:
235
+ """Return {"description": str, "line": 1-based line number} if the file opens with a
236
+ YAML frontmatter block (`---` ... `---`) containing a `description:` field, else None."""
237
+ if not lines or lines[0].strip() != "---":
238
+ return None
239
+ closing = None
240
+ for i in range(1, len(lines)):
241
+ if lines[i].strip() == "---":
242
+ closing = i
243
+ break
244
+ if closing is None:
245
+ return None
246
+ for i in range(1, closing):
247
+ m = _FRONTMATTER_DESCRIPTION_RE.match(lines[i])
248
+ if m:
249
+ return {"description": m.group(1), "line": i + 1}
250
+ return None
251
+
252
+
253
+ def check_description(path: Path, lines: List[str]) -> List[Dict]:
254
+ if not is_skill_md(path):
255
+ return []
256
+ fm = _parse_frontmatter_description(lines)
257
+ if fm is None:
258
+ return []
259
+
260
+ desc = fm["description"]
261
+ word_count = len(desc.split())
262
+ problems = []
263
+
264
+ min_words = THRESHOLDS["description_min_words"]
265
+ if word_count < min_words:
266
+ problems.append(f"only {word_count} words (skill-creator's guidance wants roughly {min_words}+)")
267
+ if not _TRIGGER_SIGNAL_RE.search(desc):
268
+ problems.append('no concrete trigger phrase or example (e.g. "Use when...", a quoted user phrase)')
269
+
270
+ if not problems:
271
+ return []
272
+ return [make_finding(
273
+ path, fm["line"], "weak_trigger_description", "low",
274
+ "Description is not sufficiently 'pushy' per skill-creator's guidance: "
275
+ + "; ".join(problems) + "."
276
+ )]
277
+
278
+
279
+ # --------------------------------------------------------------------------------------
280
+ # Check 4: dangling references/X.md or modes/X.md cross-references
281
+ # --------------------------------------------------------------------------------------
282
+
283
+ # Same match shape as G3's own dangling-reference regex (test/guards/consumers.test.js).
284
+ _REFERENCE_MENTION_RE = re.compile(r'`?((?:modes|references)/[A-Za-z0-9_.-]+\.md)`?')
285
+
286
+
287
+ def _unresolved_reference_message(ref: str, bases: List[Path]) -> Optional[str]:
288
+ """None if `ref` resolves under any candidate base; otherwise the human-readable
289
+ "does not resolve" message citing every base that was tried."""
290
+ if any((base / ref).is_file() for base in bases):
291
+ return None
292
+ base_desc = " or ".join(display_path(b) for b in bases)
293
+ return f"'{ref}' does not resolve to a real file relative to {base_desc}."
294
+
295
+
296
+ def check_dangling_references(path: Path, lines: List[str]) -> List[Dict]:
297
+ findings: List[Dict] = []
298
+ guidance_dir = find_guidance_dir(path)
299
+
300
+ # Two candidate resolution roots: the file's own directory (a skill's or an agent
301
+ # spec's own references/modes subdirectory) and the shared guidance tree (what most
302
+ # SKILL.md `references/X.md` mentions actually point at). A reference is only dangling
303
+ # if it resolves under NEITHER — checking both, rather than picking one based on which
304
+ # tree the file lives in, avoids a false "dangling" result for a guidance-tree file that
305
+ # also happens to keep a mention relative to its own subdirectory, and vice versa.
306
+ bases = [path.parent]
307
+ if guidance_dir is not None and guidance_dir != path.parent:
308
+ bases.append(guidance_dir)
309
+
310
+ in_fence = False
311
+ for i, line in enumerate(lines):
312
+ stripped = line.strip()
313
+ if stripped.startswith("```") or stripped.startswith("~~~"):
314
+ in_fence = not in_fence
315
+ continue
316
+ if in_fence:
317
+ continue
318
+ for m in _REFERENCE_MENTION_RE.finditer(line):
319
+ message = _unresolved_reference_message(m.group(1), bases)
320
+ if message:
321
+ findings.append(make_finding(path, i + 1, "dangling_reference", "high", message))
322
+ return findings
323
+
324
+
325
+ # --------------------------------------------------------------------------------------
326
+ # Check 5: staleness markers (deprecated/legacy/no longer/outdated/obsolete)
327
+ # --------------------------------------------------------------------------------------
328
+
329
+ _STALE_KEYWORD_RE = re.compile(r"\b(deprecated|legacy|no longer|outdated|obsolete)\b", re.IGNORECASE)
330
+
331
+ # Any of these appearing in the same paragraph is a strong signal the paragraph is
332
+ # *describing a detection rule or an instruction about reviewed code* — e.g. "flag TODO/
333
+ # deprecated comments" — not asserting that this file's own content is stale. This feature's
334
+ # own design-phase investigation already found a naive keyword grep over-triggers on exactly
335
+ # this pattern; excluding whole paragraphs on this signal is deliberately conservative.
336
+ _SIGNAL_WORDS_RE = re.compile(
337
+ r"\b(flags?|flagged|flagging|detects?|detecting|pattern|patterns|listing|quotes?|"
338
+ r"quoting|prune|pruned|remove|removed|identify|consult|consider|searches?)\b",
339
+ re.IGNORECASE,
340
+ )
341
+
342
+ # A paragraph opening with a conditional/temporal connector ("When an item is superseded,
343
+ # ...") is describing a general mechanism, not asserting the current file's state.
344
+ _CONDITIONAL_START_RE = re.compile(r"^[^a-zA-Z]{0,10}(when|if|once|after|before)\b", re.IGNORECASE)
345
+
346
+
347
+ def _iter_paragraphs(lines: List[str]):
348
+ """Yield (0-based start line index, paragraph lines) for each blank-line-delimited
349
+ block of prose, skipping fenced code blocks entirely."""
350
+ in_fence = False
351
+ start = None
352
+ block: List[str] = []
353
+ for i, line in enumerate(lines):
354
+ stripped = line.strip()
355
+ if stripped.startswith("```") or stripped.startswith("~~~"):
356
+ if block:
357
+ yield start, block
358
+ block, start = [], None
359
+ in_fence = not in_fence
360
+ continue
361
+ if in_fence:
362
+ continue
363
+ if not stripped:
364
+ if block:
365
+ yield start, block
366
+ block, start = [], None
367
+ continue
368
+ if start is None:
369
+ start = i
370
+ block.append(line)
371
+ if block:
372
+ yield start, block
373
+
374
+
375
+ def _offset_to_line(start_idx: int, para_lines: List[str], offset: int) -> int:
376
+ """Map a character offset in `" ".join(para_lines)` back to a 1-based source line."""
377
+ consumed = 0
378
+ for j, l in enumerate(para_lines):
379
+ seg_len = len(l) + (1 if j < len(para_lines) - 1 else 0) # +1 for the joining space
380
+ if offset < consumed + seg_len:
381
+ return start_idx + j + 1
382
+ consumed += seg_len
383
+ return start_idx + len(para_lines)
384
+
385
+
386
+ def check_staleness(path: Path, lines: List[str]) -> List[Dict]:
387
+ findings: List[Dict] = []
388
+ for start_idx, para_lines in _iter_paragraphs(lines):
389
+ para_text = " ".join(para_lines)
390
+
391
+ if _CONDITIONAL_START_RE.match(para_text.strip()):
392
+ continue
393
+ # A paragraph carrying inline code (backtick-wrapped identifiers) is virtually
394
+ # always technical prose naming actual fields/functions/paths, not a claim that
395
+ # this document's own content is stale — every real example this check was
396
+ # calibrated against (e.g. "the legacy `overlaps`/`serialize` fields") is this shape.
397
+ if "`" in para_text:
398
+ continue
399
+ if _SIGNAL_WORDS_RE.search(para_text):
400
+ continue
401
+
402
+ for m in _STALE_KEYWORD_RE.finditer(para_text):
403
+ start, end = m.span()
404
+ before = para_text[start - 1] if start > 0 else ""
405
+ after = para_text[end] if end < len(para_text) else ""
406
+ if before in "\"'/" or after in "\"'/":
407
+ # The keyword itself is quoted or slash-enumerated ("deprecated / legacy /
408
+ # outdated") — the word is the referent, not a claim about this file.
409
+ continue
410
+ line_no = _offset_to_line(start_idx, para_lines, start)
411
+ findings.append(make_finding(
412
+ path, line_no, "staleness_marker", "low",
413
+ f"Possible staleness marker '{m.group(0)}' — confirm this prose actually "
414
+ "asserts the surrounding content is outdated, not a rule/instruction "
415
+ "quoting the word as a detection target."
416
+ ))
417
+ return findings
418
+
419
+
420
+ # --------------------------------------------------------------------------------------
421
+ # File / directory orchestration
422
+ # --------------------------------------------------------------------------------------
423
+
424
+ def analyze_file(filepath: Path) -> List[Dict]:
425
+ """Analyze a single markdown file. Returns [] for a non-.md file or an unreadable one —
426
+ the same quiet-skip behavior code_quality_checker.py's directory walk relies on."""
427
+ if filepath.suffix.lower() != ".md":
428
+ return []
429
+ content = read_file_content(filepath)
430
+ if not content:
431
+ return []
432
+ lines = content.split("\n")
433
+
434
+ findings: List[Dict] = []
435
+ findings.extend(check_length(filepath, lines))
436
+ findings.extend(check_boundaries(filepath, lines))
437
+ findings.extend(check_description(filepath, lines))
438
+ findings.extend(check_dangling_references(filepath, lines))
439
+ findings.extend(check_staleness(filepath, lines))
440
+ return findings
441
+
442
+
443
+ def analyze_directory(dir_path: Path, recursive: bool = True) -> List[Dict]:
444
+ """Analyze every .md file under a directory."""
445
+ findings: List[Dict] = []
446
+ pattern = "**/*.md" if recursive else "*.md"
447
+ for filepath in sorted(dir_path.glob(pattern)):
448
+ if "node_modules" in str(filepath) or ".git" in str(filepath):
449
+ continue
450
+ findings.extend(analyze_file(filepath))
451
+ return findings
452
+
453
+
454
+ def print_report(findings: List[Dict], target: Path) -> None:
455
+ """Print human-readable analysis report."""
456
+ print("=" * 60)
457
+ print("DOC QUALITY REPORT")
458
+ print("=" * 60)
459
+ print(f"\nTarget: {display_path(target)}")
460
+ print(f"Findings: {len(findings)}")
461
+
462
+ if not findings:
463
+ print("\nNo findings.")
464
+ print("\n" + "=" * 60)
465
+ return
466
+
467
+ by_file: Dict[str, List[Dict]] = {}
468
+ for finding in findings:
469
+ by_file.setdefault(finding["file"], []).append(finding)
470
+
471
+ for file, file_findings in by_file.items():
472
+ print(f"\n{file}")
473
+ for finding in file_findings:
474
+ print(f" [{finding['severity'].upper()}] line {finding['line']} "
475
+ f"({finding['rule']}): {finding['message']}")
476
+
477
+ print("\n" + "=" * 60)
478
+
479
+
480
+ def main():
481
+ parser = argparse.ArgumentParser(
482
+ description="Analyze markdown/prose content quality (structural + staleness checks)"
483
+ )
484
+ parser.add_argument(
485
+ "path",
486
+ help="File or directory to analyze"
487
+ )
488
+ parser.add_argument(
489
+ "--recursive", "-r",
490
+ action="store_true",
491
+ default=True,
492
+ help="Recursively analyze directories for .md files (default: true)"
493
+ )
494
+ parser.add_argument(
495
+ "--json",
496
+ action="store_true",
497
+ help="Output in JSON format"
498
+ )
499
+ parser.add_argument(
500
+ "--output", "-o",
501
+ help="Write output to file"
502
+ )
503
+
504
+ args = parser.parse_args()
505
+
506
+ target = Path(args.path).resolve()
507
+
508
+ if not target.exists():
509
+ print(f"Error: Path does not exist: {target}", file=sys.stderr)
510
+ sys.exit(1)
511
+
512
+ if target.is_file():
513
+ findings = analyze_file(target)
514
+ else:
515
+ findings = analyze_directory(target, args.recursive)
516
+ if not findings and not any(target.glob("**/*.md" if args.recursive else "*.md")):
517
+ print(f"Warning: no .md files found under {display_path(target)}", file=sys.stderr)
518
+
519
+ if args.json:
520
+ output = json.dumps(findings, indent=2, default=str)
521
+ if args.output:
522
+ with open(args.output, "w") as f:
523
+ f.write(output)
524
+ print(f"Results written to {args.output}")
525
+ else:
526
+ print(output)
527
+ else:
528
+ print_report(findings, target)
529
+
530
+
531
+ if __name__ == "__main__":
532
+ main()