@khoavu882/doflow 2.0.0-beta.5 → 2.0.1

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 (494) hide show
  1. package/README.md +16 -16
  2. package/bin/doflow.js +22 -12
  3. package/core/.antigravity-plugin/plugin.json +1 -2
  4. package/core/.claude-plugin/plugin.json +1 -1
  5. package/core/.codex-plugin/plugin.json +1 -1
  6. package/core/.plugin/plugin.json +1 -1
  7. package/core/harnesses/claude/hooks/lib.sh +43 -24
  8. package/core/harnesses/claude/hooks/mcp-tool-guard.sh +4 -1
  9. package/core/harnesses/claude/hooks/permission-audit.sh +3 -0
  10. package/core/harnesses/claude/hooks/post-compact.sh +35 -32
  11. package/core/harnesses/claude/hooks/post-edit-lint.sh +2 -0
  12. package/core/harnesses/claude/hooks/post-shell-observe.sh +48 -0
  13. package/core/harnesses/claude/hooks/post-tool-failure.sh +3 -0
  14. package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
  15. package/core/harnesses/claude/hooks/pre-implement-gate.sh +4 -1
  16. package/core/harnesses/claude/hooks/session-end.sh +23 -10
  17. package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
  18. package/core/harnesses/claude/settings/settings.json +26 -9
  19. package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
  20. package/core/harnesses/codex/agents/core-implementer.toml +1 -0
  21. package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
  22. package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
  23. package/core/harnesses/codex/agents/research-writer.toml +1 -1
  24. package/core/harnesses/codex/agents/reviewer.toml +10 -0
  25. package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
  26. package/core/harnesses/codex/agents/system-architect.toml +1 -1
  27. package/core/harnesses/codex/hooks/hooks.json +0 -10
  28. package/core/harnesses/codex/hooks/lib.sh +44 -25
  29. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +4 -1
  30. package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
  31. package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
  32. package/core/harnesses/codex/hooks/pre-implement-gate.sh +4 -1
  33. package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
  34. package/core/harnesses/codex/hooks/subagent-audit.sh +1 -0
  35. package/core/harnesses/copilot/hooks/doflow-lib.sh +94 -0
  36. package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
  37. package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
  38. package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
  39. package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
  40. package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
  41. package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
  42. package/core/harnesses/copilot/hooks/doflow.json +51 -0
  43. package/core/harnesses/kiro/agents/doflow.json +1 -11
  44. package/core/harnesses/kiro/hooks/doflow.json +0 -10
  45. package/core/harnesses/kiro/hooks/lib.sh +26 -6
  46. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +8 -3
  47. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +8 -3
  48. package/core/harnesses/kiro/hooks/session-start.sh +1 -0
  49. package/core/harnesses/kiro/hooks/stop-check.sh +1 -0
  50. package/core/harnesses/pi/extensions/doflow-guard.js +11 -2
  51. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +1 -2
  52. package/core/harnesses/shared/hooks/adapters/antigravity.js +1 -1
  53. package/core/harnesses/shared/hooks/bridge/bridge.sh +126 -23
  54. package/core/harnesses/shared/hooks/bridge/envelope.jq +87 -18
  55. package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
  56. package/core/harnesses/shared/hooks/bridge/respond.sh +16 -2
  57. package/core/harnesses/shared/hooks/policies/capture-failure.sh +49 -16
  58. package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
  59. package/core/harnesses/shared/hooks/policies/contract.sh +3 -0
  60. package/core/harnesses/shared/hooks/policies/deny-json.sh +20 -7
  61. package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
  62. package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
  63. package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
  64. package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
  65. package/core/harnesses/shared/hooks/policies/lib.sh +63 -40
  66. package/core/harnesses/shared/hooks/policies/mcp-policy.conf +1 -1
  67. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +7 -8
  68. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +132 -57
  69. package/core/harnesses/shared/hooks/policies/session-context.sh +30 -1
  70. package/core/harnesses/shared/hooks/policies/stop-check.sh +27 -15
  71. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +6 -3
  72. package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
  73. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +60 -155
  74. package/core/harnesses/shared/hooks/stream-hook-runner.js +58 -20
  75. package/core/harnesses/shared/locator/doflow-run +2 -3
  76. package/core/registry/advice.json +3 -3
  77. package/core/registry/assets.json +55 -82
  78. package/core/registry/capabilities.json +12 -0
  79. package/core/registry/contracts.json +58 -31
  80. package/core/registry/contribution-points.json +35 -0
  81. package/core/registry/gates.json +32 -0
  82. package/core/registry/guidance-caps.json +40 -0
  83. package/core/registry/harnesses.json +1445 -366
  84. package/core/registry/intake.json +53 -0
  85. package/core/registry/lifecycle.json +599 -996
  86. package/core/registry/mcp.json +5 -1
  87. package/core/registry/models.json +7 -1
  88. package/core/registry/package-templates/README.md +19 -0
  89. package/core/registry/package-templates/agent/files/agent.md +9 -0
  90. package/core/registry/package-templates/agent/template.json +4 -0
  91. package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
  92. package/core/registry/package-templates/capability-provider/template.json +11 -0
  93. package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
  94. package/core/registry/package-templates/debate-preset/template.json +4 -0
  95. package/core/registry/package-templates/intake-category/files/category.json +8 -0
  96. package/core/registry/package-templates/intake-category/template.json +4 -0
  97. package/core/registry/package-templates/review-preset/files/preset.json +30 -0
  98. package/core/registry/package-templates/review-preset/template.json +4 -0
  99. package/core/registry/package-templates/skill/files/SKILL.md +8 -0
  100. package/core/registry/package-templates/skill/template.json +4 -0
  101. package/core/registry/package-templates/worker/files/worker.json +92 -0
  102. package/core/registry/package-templates/worker/template.json +11 -0
  103. package/core/registry/packages/doflow/doflow-package.json +15 -0
  104. package/core/registry/packages/doflow/presets/debate-3.json +26 -0
  105. package/core/registry/protocols.json +61 -0
  106. package/core/registry/routes.json +17 -0
  107. package/core/registry/rules.json +431 -0
  108. package/core/registry/settings.json +272 -0
  109. package/core/registry/storage.json +687 -31
  110. package/core/registry/telemetry.json +236 -0
  111. package/core/registry/transcripts.json +142 -0
  112. package/core/registry/verification.json +76 -6
  113. package/core/registry/workflows.json +55 -0
  114. package/core/shared/agent-specs/core-implementer.md +3 -0
  115. package/core/shared/agent-specs/delivery-lead.md +68 -0
  116. package/core/shared/agent-specs/quality-guardian.md +5 -1
  117. package/core/shared/agent-specs/research-writer.md +10 -3
  118. package/core/shared/agent-specs/reviewer.md +53 -0
  119. package/core/shared/agent-specs/spec-analyst.md +5 -6
  120. package/core/shared/agent-specs/system-architect.md +5 -1
  121. package/core/shared/guidance/DOFLOW_CORE.md +1 -5
  122. package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
  123. package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
  124. package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
  125. package/core/shared/guidance/pointers/codex.md +1 -4
  126. package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
  127. package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
  128. package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
  129. package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
  130. package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
  131. package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
  132. package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
  133. package/core/shared/guidance/references/ORCHESTRATION.md +3 -2
  134. package/core/shared/guidance/references/WORKFLOW_HANDOFF.md +19 -1
  135. package/core/shared/guidance/rules/RULES.md +45 -0
  136. package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
  137. package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
  138. package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
  139. package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
  140. package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
  141. package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
  142. package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
  143. package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
  144. package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
  145. package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
  146. package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
  147. package/core/shared/scripts/doflow/bash/validate-artifacts.sh +49 -5
  148. package/core/shared/scripts/doflow/bin/doflow-run +152 -34
  149. package/core/shared/skills/do/SKILL.md +14 -12
  150. package/core/shared/skills/do/references/advice_calls.md +26 -0
  151. package/core/shared/skills/do/references/pm_routing.md +1 -2
  152. package/core/shared/skills/do/references/program_mode.md +31 -0
  153. package/core/shared/skills/do-brainstorm/SKILL.md +45 -97
  154. package/core/shared/skills/do-brainstorm/references/handoff_branches.md +36 -0
  155. package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
  156. package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
  157. package/core/shared/skills/do-code-review/SKILL.md +48 -148
  158. package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
  159. package/core/shared/skills/do-code-review/references/tools.md +90 -0
  160. package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
  161. package/core/shared/skills/do-design/SKILL.md +23 -20
  162. package/core/shared/skills/do-diagnose/SKILL.md +18 -6
  163. package/core/shared/skills/do-document/SKILL.md +12 -1
  164. package/core/shared/skills/do-document/references/deep_research.md +1 -2
  165. package/core/shared/skills/do-document/references/embedded-research.md +19 -7
  166. package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
  167. package/core/shared/skills/do-execute-plan/SKILL.md +19 -60
  168. package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
  169. package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
  170. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +26 -1
  171. package/core/shared/skills/do-execute-plan/references/run_position.md +16 -2
  172. package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
  173. package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
  174. package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
  175. package/core/shared/skills/do-flow/SKILL.md +10 -11
  176. package/core/shared/skills/do-flow/references/task_classes.md +5 -5
  177. package/core/shared/skills/do-git/SKILL.md +4 -0
  178. package/core/shared/skills/do-implement/SKILL.md +13 -14
  179. package/core/shared/skills/do-plan/SKILL.md +34 -20
  180. package/core/shared/skills/do-test/SKILL.md +7 -4
  181. package/core/shared/templates/doflow/constitution-template.md +9 -3
  182. package/core/shared/templates/doflow/data-model-template.md +1 -1
  183. package/core/shared/templates/doflow/design-template.md +113 -16
  184. package/core/shared/templates/doflow/plan-template.md +11 -10
  185. package/core/shared/templates/doflow/question-log-template.md +14 -2
  186. package/core/shared/templates/doflow/requirement-template.md +1 -1
  187. package/core/shared/workflows/do-flow-chain.md +2 -0
  188. package/core/worker-evidence/antigravity/checks.json +14 -0
  189. package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
  190. package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
  191. package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
  192. package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
  193. package/core/worker-evidence/codex/checks.json +38 -0
  194. package/package.json +10 -6
  195. package/src/adapters/antigravity/index.js +143 -66
  196. package/src/adapters/claude/index.js +46 -5
  197. package/src/adapters/codex/agents.js +25 -15
  198. package/src/adapters/codex/hooks.js +39 -3
  199. package/src/adapters/codex/index.js +61 -13
  200. package/src/adapters/codex/native-projection.js +6 -2
  201. package/src/adapters/copilot/index.js +58 -23
  202. package/src/adapters/copy-tree.js +291 -46
  203. package/src/adapters/guidance-manifest.js +71 -0
  204. package/src/adapters/guidance-pointer.js +19 -0
  205. package/src/adapters/hook-commands.js +6 -10
  206. package/src/adapters/index.js +1 -0
  207. package/src/adapters/kiro/index.js +23 -12
  208. package/src/adapters/opencode/index.js +8 -8
  209. package/src/adapters/pi/hooks.js +2 -1
  210. package/src/adapters/pi/index.js +14 -7
  211. package/src/adapters/resolve-source.js +26 -2
  212. package/src/cli/commands/install.js +2 -1
  213. package/src/cli/commands/reconcile.js +53 -9
  214. package/src/cli/commands/remove.js +2 -2
  215. package/src/cli/commands/rollback.js +6 -3
  216. package/src/cli/commands/self-update.js +18 -9
  217. package/src/cli/commands/status.js +22 -3
  218. package/src/cli/commands/ui.js +99 -0
  219. package/src/cli/commands/update.js +2 -1
  220. package/src/cli/index.js +337 -32
  221. package/src/cli/runtime-commands.js +220 -60
  222. package/src/cli/shared.js +0 -2
  223. package/src/helper/git.js +1 -1
  224. package/src/helper/guardrails.js +1 -1
  225. package/src/helper/marker-merge.js +31 -13
  226. package/src/helper/package-ids.js +37 -0
  227. package/src/helper/semver.js +94 -0
  228. package/src/install/backup.js +105 -15
  229. package/src/install/context.js +1 -1
  230. package/src/install/targets.js +31 -16
  231. package/src/lifecycle/coverage.js +4 -16
  232. package/src/lifecycle/hook-tier.js +160 -0
  233. package/src/lifecycle/index.js +412 -78
  234. package/src/lifecycle/retired-harnesses.js +318 -0
  235. package/src/lifecycle/view.js +35 -15
  236. package/src/registry/index.js +551 -26
  237. package/src/registry/worker-ids.js +45 -0
  238. package/src/release/build-package.js +0 -3
  239. package/src/release/docs-blocks.js +472 -0
  240. package/src/release/preflight.js +425 -0
  241. package/src/release/rehearse.js +401 -0
  242. package/src/release/release-workflow.js +733 -0
  243. package/src/release/sync-plugin-versions.js +43 -7
  244. package/src/runtime/advice/bench-gate.js +25 -0
  245. package/src/runtime/advice/cli.js +59 -16
  246. package/src/runtime/advice/client.js +32 -9
  247. package/src/runtime/advice/config.js +98 -133
  248. package/src/runtime/advice/doctor.js +1 -1
  249. package/src/runtime/advice/elevation.js +97 -0
  250. package/src/runtime/advice/endpoint-guard.js +9 -3
  251. package/src/runtime/advice/mode.js +37 -0
  252. package/src/runtime/advice/record.js +18 -5
  253. package/src/runtime/advice/registry.js +4 -1
  254. package/src/runtime/advice/shadow-config.js +229 -0
  255. package/src/runtime/advice/shadow-dispatcher.js +276 -0
  256. package/src/runtime/brief-rules.js +81 -0
  257. package/src/runtime/brief-tools.js +111 -34
  258. package/src/runtime/candidate.js +20 -11
  259. package/src/runtime/capabilities/executor.js +450 -0
  260. package/src/runtime/capabilities/loopback.js +41 -0
  261. package/src/runtime/capabilities/sanitizer.js +88 -0
  262. package/src/runtime/capability-router.js +135 -3
  263. package/src/runtime/checkouts.js +73 -11
  264. package/src/runtime/claims.js +13 -7
  265. package/src/runtime/cli.js +196 -19
  266. package/src/runtime/config-home.js +42 -0
  267. package/src/runtime/context/arbitrate.js +767 -0
  268. package/src/runtime/context/delivery-store.js +4 -6
  269. package/src/runtime/context/emission-rules.js +118 -0
  270. package/src/runtime/context/identity.js +227 -0
  271. package/src/runtime/context/pointer.js +67 -12
  272. package/src/runtime/context/primary-command.js +117 -0
  273. package/src/runtime/context/primary.js +344 -0
  274. package/src/runtime/context/summary-store.js +655 -0
  275. package/src/runtime/context-pack.js +158 -79
  276. package/src/runtime/decision-register.js +13 -4
  277. package/src/runtime/dispatch/admission.js +392 -0
  278. package/src/runtime/dispatch/attempt-end.js +274 -0
  279. package/src/runtime/dispatch/command.js +628 -0
  280. package/src/runtime/dispatch/controls.js +904 -0
  281. package/src/runtime/dispatch/envelope.js +276 -0
  282. package/src/runtime/dispatch/grants.js +310 -0
  283. package/src/runtime/dispatch/integrate.js +617 -0
  284. package/src/runtime/dispatch/loop.js +1835 -0
  285. package/src/runtime/dispatch/store.js +352 -0
  286. package/src/runtime/dispatch/verify-run.js +201 -0
  287. package/src/runtime/evidence-ledger.js +52 -7
  288. package/src/runtime/failure/capture.js +3 -1
  289. package/src/runtime/failure/cli.js +25 -9
  290. package/src/runtime/failure/home.js +57 -30
  291. package/src/runtime/failure/store.js +7 -2
  292. package/src/runtime/feature-pointer.js +174 -0
  293. package/src/runtime/feature-resolve.js +13 -2
  294. package/src/runtime/fleet/command.js +77 -0
  295. package/src/runtime/fleet/projects.js +178 -0
  296. package/src/runtime/fleet/tree.js +383 -0
  297. package/src/runtime/freshness.js +33 -3
  298. package/src/runtime/git-memo.js +46 -0
  299. package/src/runtime/guidance/ablation.js +427 -0
  300. package/src/runtime/health.js +624 -18
  301. package/src/runtime/history-compactor.js +13 -3
  302. package/src/runtime/implementation-gate.js +281 -26
  303. package/src/runtime/intake/engine.js +677 -0
  304. package/src/runtime/intake/index.js +136 -0
  305. package/src/runtime/intake/manifest.js +140 -0
  306. package/src/runtime/intake/taxonomy.js +206 -0
  307. package/src/runtime/inventory/coverage.js +1 -1
  308. package/src/runtime/inventory/index.js +1 -1
  309. package/src/runtime/inventory/precedence.js +2 -2
  310. package/src/runtime/inventory/siblings.js +6 -4
  311. package/src/runtime/knowledge/code-corpus.js +277 -0
  312. package/src/runtime/knowledge/code-lexical.js +130 -0
  313. package/src/runtime/knowledge/code-retrieval.js +307 -0
  314. package/src/runtime/knowledge/code-validation.js +106 -0
  315. package/src/runtime/knowledge/gp3-gate.js +74 -0
  316. package/src/runtime/knowledge/graph-adapter.js +288 -0
  317. package/src/runtime/knowledge/graph-fallback.js +394 -0
  318. package/src/runtime/knowledge/hybrid.js +136 -8
  319. package/src/runtime/knowledge/retrieval.js +34 -1
  320. package/src/runtime/knowledge/rtk-compress.js +77 -0
  321. package/src/runtime/knowledge/semble-bridge.py +106 -0
  322. package/src/runtime/knowledge/semble-lifecycle.js +114 -0
  323. package/src/runtime/knowledge/semble-provider.js +365 -0
  324. package/src/runtime/lifecycle/cli.js +88 -10
  325. package/src/runtime/lifecycle/event-store.js +8 -6
  326. package/src/runtime/lifecycle/followup.js +38 -7
  327. package/src/runtime/lifecycle/overview.js +26 -2
  328. package/src/runtime/lifecycle/report-store.js +8 -10
  329. package/src/runtime/lifecycle/retention.js +95 -31
  330. package/src/runtime/lifecycle/root.js +9 -1
  331. package/src/runtime/lifecycle/status.js +34 -17
  332. package/src/runtime/lifecycle/store-upkeep.js +56 -7
  333. package/src/runtime/mask.js +134 -7
  334. package/src/runtime/mcp-consultations.js +40 -6
  335. package/src/runtime/model-router.js +79 -7
  336. package/src/runtime/models/catalogue.js +191 -0
  337. package/src/runtime/models/command.js +153 -0
  338. package/src/runtime/off-words.js +12 -0
  339. package/src/runtime/outcome.js +2 -2
  340. package/src/runtime/packages/command.js +1009 -0
  341. package/src/runtime/packages/contributions.js +116 -0
  342. package/src/runtime/packages/digest.js +132 -0
  343. package/src/runtime/packages/fetch.js +182 -0
  344. package/src/runtime/packages/grants.js +192 -0
  345. package/src/runtime/packages/index-file.js +134 -0
  346. package/src/runtime/packages/instruction.js +90 -0
  347. package/src/runtime/packages/manifest.js +331 -0
  348. package/src/runtime/packages/points.js +487 -0
  349. package/src/runtime/packages/programs.js +216 -0
  350. package/src/runtime/packages/scaffold.js +277 -0
  351. package/src/runtime/packages/source.js +90 -0
  352. package/src/runtime/packages/state.js +164 -0
  353. package/src/runtime/packages/store.js +449 -0
  354. package/src/runtime/packages/tar.js +106 -0
  355. package/src/runtime/packages/view.js +406 -0
  356. package/src/runtime/plan-check.js +59 -0
  357. package/src/runtime/plan-content.js +324 -0
  358. package/src/runtime/policy-init.js +118 -0
  359. package/src/runtime/profiles/doctor.js +96 -0
  360. package/src/runtime/profiles/resolve.js +244 -0
  361. package/src/runtime/profiles/rollback.js +254 -0
  362. package/src/runtime/profiles/sampler.js +41 -0
  363. package/src/runtime/readiness-record.js +16 -11
  364. package/src/runtime/readiness.js +46 -5
  365. package/src/runtime/research-request.js +50 -25
  366. package/src/runtime/retrieval-plan.js +10 -30
  367. package/src/runtime/retrieval-record.js +75 -5
  368. package/src/runtime/review/command.js +246 -0
  369. package/src/runtime/review/dominance.js +68 -0
  370. package/src/runtime/review/findings.js +434 -0
  371. package/src/runtime/review/identity.js +255 -0
  372. package/src/runtime/review/protocol-command.js +109 -0
  373. package/src/runtime/review/protocols.js +405 -0
  374. package/src/runtime/review/resolve-protocol.js +302 -0
  375. package/src/runtime/review/start.js +530 -0
  376. package/src/runtime/roadmap/attendance.js +137 -0
  377. package/src/runtime/roadmap/facts.js +479 -0
  378. package/src/runtime/roadmap/gates.js +273 -0
  379. package/src/runtime/roadmap/loop.js +1719 -0
  380. package/src/runtime/roadmap/merge.js +390 -0
  381. package/src/runtime/roadmap/rows.js +1122 -0
  382. package/src/runtime/roadmap/seed.js +1063 -0
  383. package/src/runtime/roadmap/worktree-cleanup.js +147 -0
  384. package/src/runtime/rules-report.js +336 -0
  385. package/src/runtime/scaffold/artifacts.js +60 -9
  386. package/src/runtime/scaffold/generate.js +21 -9
  387. package/src/runtime/settings/autopilot.js +106 -0
  388. package/src/runtime/settings/cli.js +280 -0
  389. package/src/runtime/settings/config-write.js +314 -0
  390. package/src/runtime/settings/files.js +271 -0
  391. package/src/runtime/settings/legacy.js +154 -0
  392. package/src/runtime/settings/lexical.js +56 -0
  393. package/src/runtime/settings/prepared-run.js +269 -0
  394. package/src/runtime/settings/project-layers.js +118 -0
  395. package/src/runtime/settings/refs.js +288 -0
  396. package/src/runtime/settings/registry.js +553 -0
  397. package/src/runtime/settings/resolve.js +713 -0
  398. package/src/runtime/settings/shared.js +37 -0
  399. package/src/runtime/settings/worker-billing.js +107 -0
  400. package/src/runtime/settings/write.js +269 -0
  401. package/src/runtime/slot-command.js +78 -0
  402. package/src/runtime/slots.js +671 -0
  403. package/src/runtime/source-version.js +1 -0
  404. package/src/runtime/state-root.js +217 -0
  405. package/src/runtime/storage/cli.js +12 -5
  406. package/src/runtime/storage/engine.js +9 -2
  407. package/src/runtime/storage/registry.js +14 -1
  408. package/src/runtime/storage/roots.js +66 -16
  409. package/src/runtime/storage/rules.js +82 -10
  410. package/src/runtime/storage/upkeep.js +10 -2
  411. package/src/runtime/storage/work-status.js +245 -26
  412. package/src/runtime/task-attempts.js +589 -69
  413. package/src/runtime/task-classifier.js +85 -14
  414. package/src/runtime/task-graph.js +373 -0
  415. package/src/runtime/task-run.js +778 -0
  416. package/src/runtime/task-scope.js +176 -55
  417. package/src/runtime/task-sources.js +147 -0
  418. package/src/runtime/task-state.js +373 -64
  419. package/src/runtime/team/admission.js +301 -0
  420. package/src/runtime/team/envelope.js +324 -0
  421. package/src/runtime/team/review-guard.js +156 -0
  422. package/src/runtime/trace/analyze.js +427 -0
  423. package/src/runtime/trace/baseline.js +770 -0
  424. package/src/runtime/trace/ledger.js +63 -140
  425. package/src/runtime/trace/otlp-export.js +568 -0
  426. package/src/runtime/trace/otlp-map.js +481 -0
  427. package/src/runtime/trace/spans.js +745 -0
  428. package/src/runtime/trace/telemetry.js +225 -0
  429. package/src/runtime/trace/views.js +1 -0
  430. package/src/runtime/transcripts/command.js +98 -0
  431. package/src/runtime/transcripts/cursor.js +326 -0
  432. package/src/runtime/transcripts/read.js +126 -0
  433. package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
  434. package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
  435. package/src/runtime/transcripts/readers/copilot-events.js +90 -0
  436. package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
  437. package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
  438. package/src/runtime/transcripts/registry.js +161 -0
  439. package/src/runtime/transcripts/stats.js +161 -0
  440. package/src/runtime/verb-view.js +267 -0
  441. package/src/runtime/verification/check-key.js +628 -0
  442. package/src/runtime/verification/contract-runner.js +546 -11
  443. package/src/runtime/verification/engine.js +499 -54
  444. package/src/runtime/verification/impact.js +617 -0
  445. package/src/runtime/verification/inflight.js +205 -0
  446. package/src/runtime/verification/registry.js +176 -0
  447. package/src/runtime/verification/reuse.js +184 -0
  448. package/src/runtime/verification/telemetry-emit.js +254 -0
  449. package/src/runtime/verification/test-selection.js +67 -7
  450. package/src/runtime/worker/active.js +90 -0
  451. package/src/runtime/worker/cancel.js +222 -0
  452. package/src/runtime/worker/catalogue.js +122 -0
  453. package/src/runtime/worker/command.js +677 -0
  454. package/src/runtime/worker/evidence.js +288 -0
  455. package/src/runtime/worker/launch.js +256 -0
  456. package/src/runtime/worker/models.js +137 -0
  457. package/src/runtime/worker/parsers.js +218 -0
  458. package/src/runtime/worker/probes/index.js +706 -0
  459. package/src/runtime/worker/process-tree.js +415 -0
  460. package/src/runtime/worker/quota.js +43 -0
  461. package/src/runtime/worker/readiness.js +713 -0
  462. package/src/runtime/worker/run-store.js +166 -0
  463. package/src/runtime/worker/supervisor.js +541 -0
  464. package/src/runtime/worker/worktree-check.js +252 -0
  465. package/src/runtime/workflow-engine.js +5 -1
  466. package/src/runtime/workflow-orchestrator.js +610 -67
  467. package/src/runtime/worktree.js +149 -22
  468. package/src/state/index.js +1 -1
  469. package/src/state/lockfile.js +64 -10
  470. package/src/state/run-lock.js +1 -1
  471. package/types/node-commonjs.d.ts +106 -7
  472. package/core/harnesses/claude/hooks/pre-bash-guard.sh +0 -10
  473. package/core/harnesses/codex/hooks/pre-bash-guard.sh +0 -10
  474. package/core/harnesses/gemini/hooks/hooks.json +0 -77
  475. package/core/harnesses/gemini/hooks/lib.sh +0 -302
  476. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -10
  477. package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
  478. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -10
  479. package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
  480. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -10
  481. package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
  482. package/core/harnesses/gemini/hooks/session-end.sh +0 -5
  483. package/core/harnesses/gemini/hooks/session-start.sh +0 -7
  484. package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
  485. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +0 -24
  486. package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +0 -91
  487. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +0 -378
  488. package/core/shared/guidance/FLAGS.md +0 -25
  489. package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
  490. package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
  491. package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
  492. package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
  493. package/src/adapters/gemini/hooks.js +0 -251
  494. package/src/adapters/gemini/index.js +0 -429
@@ -91,7 +91,7 @@ def frontmatter_keys(lines: List[str]) -> List[str]:
91
91
  def is_agent_spec(path: Path, lines: Optional[List[str]] = None) -> bool:
92
92
  """True for a specialist agent-spec file. `agent-specs` is this repo's own SOURCE-TREE
93
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
94
+ (`agents/` for claude/codex/copilot/kiro; never `agent-specs/`), so a real installed
95
95
  copy would silently never match a directory-name-only check. Since DoFlow is designed to be
96
96
  installed anywhere, this checks the installed-shape name too, but path name alone is too
97
97
  generic (an unrelated project can have its own unrelated `agents/` folder) — content shape
@@ -417,13 +417,524 @@ def check_staleness(path: Path, lines: List[str]) -> List[Dict]:
417
417
  return findings
418
418
 
419
419
 
420
+ # --------------------------------------------------------------------------------------
421
+ # Check 6: uncited vendor claims (Feature 124, IC-004)
422
+ # --------------------------------------------------------------------------------------
423
+
424
+ _VENDOR_KW_RE = re.compile(
425
+ r"\b(library|libraries|framework|frameworks|runtime|runtimes|sdk|sdks|package|packages|dependency|dependencies|vendor|vendors|api|apis)\b",
426
+ re.IGNORECASE,
427
+ )
428
+ _BEHAVIOR_KW_RE = re.compile(
429
+ r"\b(returns|throws|behaves|defaults to|implements|requires|supports)\b",
430
+ re.IGNORECASE,
431
+ )
432
+ _CITATION_RE = re.compile(
433
+ r"\b(context7|c7|mcp__context7__)\b|https?://|\bUNVERIFIED\b",
434
+ re.IGNORECASE,
435
+ )
436
+ _SENTENCE_SPLIT_RE = re.compile(r"(?<=[.!?])\s+")
437
+
438
+
439
+ def check_uncited_vendor_claims(path: Path, lines: List[str]) -> List[Dict]:
440
+ findings: List[Dict] = []
441
+ # Skip YAML frontmatter
442
+ fm_end = 0
443
+ if lines and lines[0].strip() == "---":
444
+ for i in range(1, len(lines)):
445
+ if lines[i].strip() == "---":
446
+ fm_end = i + 1
447
+ break
448
+
449
+ for start_idx, para_lines in _iter_paragraphs(lines):
450
+ if start_idx < fm_end:
451
+ continue
452
+ para_text = " ".join(para_lines)
453
+ offset = 0
454
+ for sent in _SENTENCE_SPLIT_RE.split(para_text):
455
+ sent_clean = sent.strip()
456
+ if not sent_clean:
457
+ offset += len(sent) + 1
458
+ continue
459
+ if _VENDOR_KW_RE.search(sent_clean) and _BEHAVIOR_KW_RE.search(sent_clean):
460
+ if not _CITATION_RE.search(sent_clean):
461
+ sent_pos = para_text.find(sent, offset)
462
+ if sent_pos == -1:
463
+ sent_pos = offset
464
+ line_no = _offset_to_line(start_idx, para_lines, sent_pos)
465
+ findings.append(make_finding(
466
+ path, line_no, "uncited_vendor_claim", "low",
467
+ "States library/vendor fact without citation: use context7 first, else an HTTPS URL, else tag as UNVERIFIED."
468
+ ))
469
+ offset += len(sent) + 1
470
+ return findings
471
+
472
+
473
+ # --------------------------------------------------------------------------------------
474
+ # Check 7: voice tells (opt-in `--voice`; row 132)
475
+ # --------------------------------------------------------------------------------------
476
+ #
477
+ # Advisory and deterministic: it finds phrases the guidance tree's `ARTIFACT_VOICE.md` asks
478
+ # writers to avoid. Every finding quotes the match; there is no score, percentage or grade, and
479
+ # the check never changes this script's exit status. The word lists below are this repo's own.
480
+ #
481
+ # A tell is STRONG (reported on its own) or WEAK (a word or mark that is often fine alone: one
482
+ # dash, one bold word, "key"). A weak tell is reported only when at least two other tells share
483
+ # its paragraph, so a plain sentence with one dash stays silent.
484
+
485
+ _V_STRONG_WORDS = [
486
+ ("overused_word", r"delv(?:e|es|ed|ing)|tapestry|pivotal|testament|intricate|interplay|"
487
+ r"foster(?:s|ed|ing)?|underscor(?:e|es|ed|ing)|showcas(?:e|es|ed|ing)|vibrant"),
488
+ ("sales_word", r"boasts?|seamless(?:ly)?|cutting-edge|game-chang(?:er|ing)|groundbreaking|"
489
+ r"revolutionary|world-class|best-in-class|state-of-the-art|unparalleled"),
490
+ ("chatbot_residue", r"i hope this helps|hope that helps|great question|good question|"
491
+ r"i'?d be happy to|happy to help|as an ai(?: language model)?|"
492
+ r"let me know if you|feel free to (?:ask|reach)|"
493
+ r"(?<![\w'])(?:certainly|absolutely|of course)(?=!)"),
494
+ ("knowledge_limit", r"as of my (?:last|latest) (?:update|training)|as of my knowledge|"
495
+ r"knowledge cut-?off|my training data|up to my last (?:update|training)|"
496
+ r"i (?:don'?t|do not) have (?:access to )?real-time"),
497
+ ("serves_as", r"serv(?:es|e|ing) as|stands? as|standing as"),
498
+ ]
499
+ _V_WEAK_WORDS = [
500
+ ("overused_word", r"actually|additionally|crucial|enhanc(?:e|es|ed|ing)|highlight(?:s|ed|ing)?|"
501
+ r"key|landscape|valuable"),
502
+ ("sales_word", r"robust|powerful"),
503
+ ]
504
+ _V_STRONG_RES = [(k, re.compile(r"\b(?:" + p + r")\b" if k != "chatbot_residue" else "(?:" + p + ")",
505
+ re.IGNORECASE)) for k, p in _V_STRONG_WORDS]
506
+ _V_WEAK_RES = [(k, re.compile(r"\b(?:" + p + r")\b", re.IGNORECASE)) for k, p in _V_WEAK_WORDS]
507
+
508
+ _V_DASH_RE = re.compile("[—–]")
509
+ _V_CURLY_RE = re.compile("[‘’“”]")
510
+ _V_BOLD_RE = re.compile(r"\*\*[^*\n]+?\*\*|__[^_\n]+?__")
511
+ _V_LIST_RE = re.compile(r"^\s*(?:[-*+]|\d+[.)])\s+")
512
+ _V_HEADING_LINE_RE = re.compile(r"^\s{0,3}(#{1,6})\s+(.*?)\s*#*\s*$")
513
+ _V_HEADING_ID_RE = re.compile(r"^(?:\d+(?:\.\d+)*\.?|[A-Z]{1,4}-?\d+:?)\s+")
514
+ _V_MINOR = {"a", "an", "the", "and", "or", "of", "to", "in", "on", "for", "with", "by", "at", "as",
515
+ "vs", "via", "nor", "but", "from", "into", "is", "are"}
516
+
517
+ _FENCE_OPEN_RE = re.compile(r"^ {0,3}(`{3,}|~{3,})")
518
+ _INLINE_CODE_RE = re.compile(r"(`+)(?:(?!\1).)+?\1", re.DOTALL)
519
+ _HTML_COMMENT_RE = re.compile(r"<!--.*?-->", re.DOTALL)
520
+ _REF_USE_RE = re.compile(r"(\]\[)[^\]\n]*(\])")
521
+ _AUTOLINK_RE = re.compile(r"<(?:https?|mailto):[^>\n]*>|https?://[^\s)>\]]+")
522
+ _REFDEF_RE = re.compile(r"^ {0,3}\[[^\]\n]+\]:[ \t]*\S.*$")
523
+ _THEMATIC_RE = re.compile(r"^ {0,3}([-*_])(?:[ \t]*\1){2,}[ \t]*$")
524
+
525
+
526
+ def _blank(match: "re.Match") -> str:
527
+ return " " * len(match.group(0))
528
+
529
+
530
+ def _spaces(text: str) -> str:
531
+ """`text` with every character but newlines replaced by a space (offsets stay valid)."""
532
+ return re.sub(r"[^\n]", " ", text)
533
+
534
+
535
+ def _mask_destinations(text: str) -> str:
536
+ """Blank the `(...)` of every `](...)`, matching nested parentheses."""
537
+ out = list(text)
538
+ pos = 0
539
+ while True:
540
+ k = text.find("](", pos)
541
+ if k < 0:
542
+ return "".join(out)
543
+ depth, m = 1, k + 2
544
+ while m < len(text) and depth:
545
+ depth += {"(": 1, ")": -1}.get(text[m], 0)
546
+ m += 1
547
+ end = m if depth == 0 else k + 2
548
+ for q in range(k + 2, max(end - 1, k + 2)):
549
+ if text[q] != "\n":
550
+ out[q] = " "
551
+ pos = max(end, k + 2)
552
+
553
+
554
+ def _mask_inline(text: str) -> str:
555
+ """Blank inline code (also over line breaks), HTML comments, link destinations and
556
+ identifiers, and autolinks in a block of joined lines."""
557
+ text = _INLINE_CODE_RE.sub(lambda m: _spaces(m.group(0)), text)
558
+ text = _HTML_COMMENT_RE.sub(lambda m: _spaces(m.group(0)), text)
559
+ text = _mask_destinations(text)
560
+ text = re.sub(r"\[[^\]\n]*\]\[\]", lambda m: _spaces(m.group(0)), text) # collapsed [text][]
561
+ text = _REF_USE_RE.sub(lambda m: m.group(1) + _spaces(m.group(0))[2:-1] + m.group(2), text)
562
+ return _AUTOLINK_RE.sub(lambda m: _spaces(m.group(0)), text)
563
+
564
+
565
+ def mask_prose(lines: List[str]) -> List[str]:
566
+ """Return `lines` with every region a voice check must not read replaced by spaces, so
567
+ offsets and line numbers stay valid. A line removed whole becomes ""; a line with only part
568
+ masked keeps its length, so a paragraph is never split by masking. Regions: YAML front
569
+ matter; fenced code (closed only by the same character, at least as long); indented code
570
+ outside a list; block quotes including lazy continuation lines; HTML comments; inline code
571
+ (over line breaks too); link destinations, reference identifiers and definitions; autolinks."""
572
+ out: List[str] = []
573
+ i = 0
574
+ if lines and lines[0].strip() == "---":
575
+ for j in range(1, len(lines)):
576
+ if lines[j].strip() == "---":
577
+ out = [""] * (j + 1)
578
+ i = j + 1
579
+ break
580
+ fence = None # (char, length) of the open fence
581
+ in_comment = in_quote = in_list = False
582
+ prev = "blank" # blank | code | para: what the previous line was
583
+ for line in lines[i:]:
584
+ stripped = line.strip()
585
+ if fence:
586
+ if re.match(r"^ {0,3}" + re.escape(fence[0]) + "{" + str(fence[1]) + r",}[ \t]*$", line):
587
+ fence = None
588
+ out.append("")
589
+ prev = "code"
590
+ continue
591
+ if in_comment:
592
+ in_comment = "-->" not in line
593
+ out.append("")
594
+ continue
595
+ fm = _FENCE_OPEN_RE.match(line)
596
+ if fm:
597
+ fence = (fm.group(1)[0], len(fm.group(1)))
598
+ out.append("")
599
+ in_quote = False
600
+ prev = "code"
601
+ continue
602
+ if stripped.startswith("<!--"):
603
+ in_comment = "-->" not in line
604
+ out.append("")
605
+ continue
606
+ if not stripped:
607
+ out.append("")
608
+ in_quote = False
609
+ prev = "blank"
610
+ continue
611
+ is_list = bool(_V_LIST_RE.match(line)) and not line.startswith((" ", "\t"))
612
+ if stripped.startswith(">"):
613
+ in_quote = True
614
+ out.append("")
615
+ prev = "para"
616
+ continue
617
+ if in_quote and not (_V_HEADING_LINE_RE.match(line) or is_list or _THEMATIC_RE.match(line)):
618
+ out.append("") # lazy continuation of a block quote
619
+ prev = "para"
620
+ continue
621
+ in_quote = False
622
+ indented = line.startswith((" ", "\t"))
623
+ if indented and not in_list and prev != "para":
624
+ out.append("")
625
+ prev = "code"
626
+ continue
627
+ if is_list:
628
+ in_list = True
629
+ elif not indented:
630
+ in_list = False
631
+ prev = "heading" if _V_HEADING_LINE_RE.match(line) else "para"
632
+ out.append(line)
633
+
634
+ # Second pass: inline masks, over each run of consecutive kept lines so a span may cross a break.
635
+ k = 0
636
+ while k < len(out):
637
+ if out[k] == "" or not lines[k].strip():
638
+ k += 1
639
+ continue
640
+ m = k
641
+ while m < len(out) and out[m] != "" and lines[m].strip():
642
+ m += 1
643
+ block = _mask_inline("\n".join(out[k:m])).split("\n")
644
+ for n, text in enumerate(block):
645
+ out[k + n] = _REFDEF_RE.sub(lambda r: " " * len(r.group(0)), text)
646
+ k = m
647
+ return out
648
+
649
+
650
+ def _voice_paragraphs(masked: List[str], lines: List[str]) -> List[Tuple[int, List[str]]]:
651
+ """Paragraphs of masked lines as (0-based start index, lines). A line that is empty in the
652
+ source, or removed whole by masking (""), ends a paragraph; so do a heading and each list
653
+ item. A line that only had inline code masked stays in its paragraph."""
654
+ paras: List[Tuple[int, List[str]]] = []
655
+ start, block = None, []
656
+
657
+ def flush():
658
+ nonlocal start, block
659
+ if block:
660
+ paras.append((start, block))
661
+ start, block = None, []
662
+
663
+ for i, line in enumerate(masked):
664
+ if line == "" or not lines[i].strip():
665
+ flush()
666
+ continue
667
+ if _V_HEADING_LINE_RE.match(line):
668
+ flush()
669
+ paras.append((i, [line]))
670
+ continue
671
+ if _V_LIST_RE.match(line):
672
+ flush()
673
+ if start is None:
674
+ start = i
675
+ block.append(line)
676
+ flush()
677
+ return paras
678
+
679
+
680
+ def _quote(text: str, limit: int = 60) -> str:
681
+ text = " ".join(text.split())
682
+ return text if len(text) <= limit else text[: limit - 3] + "..."
683
+
684
+
685
+ def _is_title_case(heading: str) -> bool:
686
+ heading = _V_HEADING_ID_RE.sub("", heading) # "3.", "R1:" and "FR-001:" are identifiers, not words
687
+ words = re.findall(r"[A-Za-z][A-Za-z'-]*", heading)
688
+ rest = [w for w in words[1:] if w.lower() not in _V_MINOR and len(w) >= 3]
689
+ return len(rest) >= 2 and all(w[0].isupper() for w in rest)
690
+
691
+
692
+ def check_voice(path: Path, lines: List[str]) -> List[Dict]:
693
+ findings: List[Dict] = []
694
+ masked = mask_prose(lines)
695
+ for start_idx, para in _voice_paragraphs(masked, lines):
696
+ strong: List[Tuple[int, str, str]] = [] # (1-based line, kind, quoted text)
697
+ weak: List[Tuple[int, str, str]] = []
698
+ dashes: List[Tuple[int, int]] = []
699
+ curly: List[Tuple[int, int]] = []
700
+ bold: List[Tuple[int, str]] = []
701
+
702
+ for off, line in enumerate(para):
703
+ ln = start_idx + off + 1
704
+ text = line
705
+ hm = _V_HEADING_LINE_RE.match(line)
706
+ if hm and _is_title_case(hm.group(2)):
707
+ strong.append((ln, "title_case_heading", _quote(hm.group(2))))
708
+ for kind, rx in _V_STRONG_RES:
709
+ for m in rx.finditer(text):
710
+ strong.append((ln, kind, _quote(m.group(0))))
711
+ for kind, rx in _V_WEAK_RES:
712
+ for m in rx.finditer(text):
713
+ weak.append((ln, kind, _quote(m.group(0))))
714
+ for m in _V_DASH_RE.finditer(text):
715
+ dashes.append((ln, m.start()))
716
+ for m in _V_CURLY_RE.finditer(text):
717
+ curly.append((ln, m.start()))
718
+ lead = _V_LIST_RE.match(text)
719
+ body_start = lead.end() if lead else len(text) - len(text.lstrip())
720
+ for m in _V_BOLD_RE.finditer(text):
721
+ inner = m.group(0)[2:-2].strip()
722
+ if m.start() <= body_start or inner.endswith(":"):
723
+ continue # a label or a defined term opening the item, not emphasis
724
+ bold.append((ln, m.group(0)))
725
+
726
+ def around(ln: int, col: int) -> str:
727
+ src = para[ln - start_idx - 1]
728
+ return _quote(src[max(0, col - 20): col + 20])
729
+
730
+ if len(dashes) >= 2:
731
+ strong.append((dashes[0][0], "dash_habit", around(*dashes[0])))
732
+ elif dashes:
733
+ weak.append((dashes[0][0], "dash_habit", around(*dashes[0])))
734
+ if curly:
735
+ weak.append((curly[0][0], "curly_quote", around(*curly[0])))
736
+ if len(bold) >= 3:
737
+ strong.append((bold[0][0], "bold_density", _quote(" ".join(b[1] for b in bold[:3]))))
738
+ elif bold:
739
+ weak.append((bold[0][0], "bold_density", _quote(bold[0][1])))
740
+
741
+ if not (para and _V_LIST_RE.match(para[0]) or para[0].lstrip().startswith("|")):
742
+ joined = " ".join(p.strip() for p in para)
743
+ sents = [s for s in _SENTENCE_SPLIT_RE.split(joined) if s.strip()]
744
+ openers = []
745
+ for s in sents:
746
+ om = re.match(r"[A-Za-z][A-Za-z'-]*", s.lstrip(" \"'(*_"))
747
+ openers.append(om.group(0) if om else None)
748
+ for i in range(len(openers) - 2):
749
+ a, b, c = openers[i:i + 3]
750
+ if a and b and c and len(a) >= 2 and a.lower() == b.lower() == c.lower():
751
+ pos = joined.find(sents[i])
752
+ strong.append((_offset_to_line(start_idx, para, max(pos, 0)),
753
+ "repeated_opener", f"{a} (opens 3 consecutive sentences)"))
754
+ break
755
+
756
+ tells = len(strong) + len(weak)
757
+ reported = list(strong) + (list(weak) if tells >= 3 else [])
758
+ for ln, kind, quoted in sorted(reported, key=lambda t: (t[0], t[1])):
759
+ is_weak = (ln, kind, quoted) in weak
760
+ findings.append(make_finding(
761
+ path, ln, f"voice_{kind}", "low",
762
+ f"{kind}: \"{quoted}\"" + (" (weak tell, shown because the paragraph has others)"
763
+ if is_weak else "")))
764
+ return findings
765
+
766
+
767
+ # --------------------------------------------------------------------------------------
768
+ # Facts diff: what a rewrite must not change (opt-in `--facts-diff BEFORE AFTER`; row 132)
769
+ # --------------------------------------------------------------------------------------
770
+ #
771
+ # Extracts numbers, capitalised names, dates, URLs and citation markers from two versions of a
772
+ # file. A fact only in AFTER is an error (the rewrite added it); a fact only in BEFORE is a
773
+ # warning (the rewrite dropped or changed it). Presence is compared, not counts. Dates are
774
+ # compared as ISO dates ("March 14, 2026" and "14 March 2026" are one fact). Lines wrapped
775
+ # inside a paragraph are joined first, so a name split over two lines is one name.
776
+ #
777
+ # Names are a heuristic. A run of capitalised words is a name. A single capitalised word that
778
+ # opens a sentence is a name only when the same word is capitalised mid-sentence in either
779
+ # version, or has an inner capital or digit ("PostgreSQL"): so a name that moves to or from a
780
+ # sentence start still matches itself, and "Run" turned into "Execute" is not an added fact.
781
+ # Words in `_F_STOP_START` are never names.
782
+
783
+ _MONTHS = (r"(?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|"
784
+ r"Aug(?:ust)?|Sep(?:t(?:ember)?)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?)\.?")
785
+ _MONTH_NUMBER = {m: n for n, m in enumerate(
786
+ "jan feb mar apr may jun jul aug sep oct nov dec".split(), 1)}
787
+ _F_URL_RE = re.compile(r"https?://[^\s)>\]\"'`]+")
788
+ _F_DATE_RE = re.compile(
789
+ rf"\b\d{{4}}-\d{{2}}-\d{{2}}\b|\b{_MONTHS}\s+\d{{1,2}}(?:st|nd|rd|th)?,?\s+\d{{4}}\b|"
790
+ rf"\b\d{{1,2}}(?:st|nd|rd|th)?\s+{_MONTHS}\s+\d{{4}}\b|\b{_MONTHS}\s+\d{{4}}\b")
791
+ _F_DATE_FORMS = [
792
+ (re.compile(r"(\d{4})-(\d{2})-(\d{2})"), lambda g: f"{g[0]}-{g[1]}-{g[2]}"),
793
+ (re.compile(rf"({_MONTHS})\s+(\d{{1,2}})(?:st|nd|rd|th)?,?\s+(\d{{4}})"),
794
+ lambda g: f"{g[2]}-{_month(g[0]):02d}-{int(g[1]):02d}"),
795
+ (re.compile(rf"(\d{{1,2}})(?:st|nd|rd|th)?\s+({_MONTHS})\s+(\d{{4}})"),
796
+ lambda g: f"{g[2]}-{_month(g[1]):02d}-{int(g[0]):02d}"),
797
+ (re.compile(rf"({_MONTHS})\s+(\d{{4}})"), lambda g: f"{g[1]}-{_month(g[0]):02d}"),
798
+ ]
799
+ _F_CITE_RE = re.compile(
800
+ r"\[\^[^\]\s]+\]|\[@[^\]\s]+\]|\[\d+(?:\s*[,–-]\s*\d+)*\]|"
801
+ r"\([A-Z][A-Za-z-]+(?: et al\.| (?:and|&) [A-Z][A-Za-z-]+)?,? \d{4}[a-z]?\)")
802
+ _F_NUM_RE = re.compile(r"(?<![\w.])\d[\d,]*(?:\.\d+)*%?(?![\w])")
803
+ _F_STOP_START = {
804
+ "The", "A", "An", "This", "That", "These", "Those", "It", "Its", "We", "Our", "You", "Your", "In",
805
+ "On", "At", "For", "If", "When", "While", "Then", "There", "Here", "And", "But", "Or", "So",
806
+ "As", "By", "With", "Without", "To", "Of", "Each", "Every", "Some", "Any", "All", "No", "Not",
807
+ "See", "Note", "Use", "Run", "Read", "Set", "Add", "Make", "Keep", "Do", "Does", "Is", "Are",
808
+ "Was", "Were", "Be", "After", "Before", "Once", "Since", "Because", "However", "Also", "Both",
809
+ "Most", "Many", "One", "Two", "Three", "Only", "Even", "Still", "Yet", "Now", "Today",
810
+ }
811
+ _F_NAME_RUN_RE = re.compile(r"[A-Z][\w'-]*(?:[ \t]+[A-Z][\w'-]*)*")
812
+ _F_LAYOUT_PREFIX_RE = re.compile(r"[\s\-+*>|]*(?:\d+[.)])?[\s\-+*>|]*")
813
+ _F_BLOCK_START_RE = re.compile(r"^\s*(?:[-*+>|#]|\d+[.)]|`{3,}|~{3,})")
814
+
815
+
816
+ def _month(name: str) -> int:
817
+ return _MONTH_NUMBER[name.lower()[:3]]
818
+
819
+
820
+ def _iso_date(text: str) -> str:
821
+ for rx, fmt in _F_DATE_FORMS:
822
+ m = rx.fullmatch(text)
823
+ if m:
824
+ return fmt(m.groups())
825
+ return text
826
+
827
+
828
+ def _join_wrapped(text: str) -> str:
829
+ """Replace the line break inside a paragraph with a space (same length, so offsets hold)."""
830
+ lines = text.split("\n")
831
+ out = []
832
+ for idx, line in enumerate(lines):
833
+ out.append(line)
834
+ if idx < len(lines) - 1:
835
+ nxt = lines[idx + 1]
836
+ joinable = bool(line.strip() and nxt.strip() and not _F_BLOCK_START_RE.match(nxt)
837
+ and not line.lstrip().startswith(("|", "```", "~~~")))
838
+ out.append(" " if joinable else "\n")
839
+ return "".join(out)
840
+
841
+
842
+ def _extract_facts(text: str) -> Tuple[List[Tuple[str, str, int]], List[Tuple[str, int]]]:
843
+ """Return (facts, initials). facts are (kind, normalised value, 1-based line); initials are
844
+ the single capitalised words that open a sentence, which `facts_diff` resolves."""
845
+ facts: List[Tuple[str, str, int]] = []
846
+ initials: List[Tuple[str, int]] = []
847
+ line_starts: List[int] = []
848
+ pos = 0
849
+ for line in text.split("\n"):
850
+ line_starts.append(pos)
851
+ pos += len(line) + 1
852
+
853
+ def line_of(offset: int) -> int:
854
+ lo, hi = 0, len(line_starts) - 1
855
+ while lo < hi:
856
+ mid = (lo + hi + 1) // 2
857
+ if line_starts[mid] <= offset:
858
+ lo = mid
859
+ else:
860
+ hi = mid - 1
861
+ return lo + 1
862
+
863
+ work = list(text)
864
+
865
+ def take(rx: "re.Pattern", kind: str, norm=lambda s: s):
866
+ for m in rx.finditer("".join(work)):
867
+ value = m.group(0)
868
+ if kind == "url":
869
+ value = value.rstrip(".,;:!?")
870
+ facts.append((kind, norm(value), line_of(m.start())))
871
+ for k in range(m.start(), m.end()):
872
+ if work[k] != "\n":
873
+ work[k] = " "
874
+
875
+ take(_F_URL_RE, "url")
876
+ take(_F_DATE_RE, "date", lambda s: _iso_date(" ".join(s.split())))
877
+ take(_F_CITE_RE, "citation", lambda s: re.sub(r"\s+", "", s))
878
+ # ordered-list markers are layout, not facts
879
+ body = re.sub(r"(?m)^(\s*)(\d+[.)])(\s)", lambda m: m.group(1) + " " * len(m.group(2)) + m.group(3),
880
+ "".join(work))
881
+ for m in _F_NUM_RE.finditer(body):
882
+ facts.append(("number", m.group(0).replace(",", ""), line_of(m.start())))
883
+ # headings are layout for names; blank them in place so later offsets keep their lines
884
+ name_body = "\n".join(" " * len(l) if re.match(r"^\s{0,3}#{1,6}\s", l) else l for l in body.split("\n"))
885
+ name_body = _join_wrapped(_INLINE_CODE_RE.sub(lambda m: _spaces(m.group(0)), name_body))
886
+ for m in _F_NAME_RUN_RE.finditer(name_body):
887
+ words = m.group(0).split()
888
+ while words and words[0] in _F_STOP_START:
889
+ words.pop(0)
890
+ if not words or (len(words) == 1 and len(words[0]) < 2):
891
+ continue
892
+ prefix = name_body[name_body.rfind("\n", 0, m.start()) + 1:m.start()]
893
+ opener = prefix.rstrip(" \t*_\"'([")
894
+ sentence_start = bool(_F_LAYOUT_PREFIX_RE.fullmatch(prefix)) or opener == "" or opener[-1] in ".!?:"
895
+ if len(words) == 1 and sentence_start and not re.search(r"[A-Z].*[A-Z]|[a-z]\d|\d", words[0]):
896
+ initials.append((words[0], line_of(m.start())))
897
+ continue
898
+ facts.append(("name", " ".join(words), line_of(m.start())))
899
+ return facts, initials
900
+
901
+
902
+ def facts_diff(before: Path, after: Path) -> List[Dict]:
903
+ """Compare the facts of two versions. Added facts are errors, dropped ones warnings."""
904
+ b, b_initials = _extract_facts(read_file_content(before))
905
+ a, a_initials = _extract_facts(read_file_content(after))
906
+ confirmed = {v for k, v, _ in b + a if k == "name"}
907
+ b += [("name", w, ln) for w, ln in b_initials if w in confirmed]
908
+ a += [("name", w, ln) for w, ln in a_initials if w in confirmed]
909
+ b_keys = {(k, v) for k, v, _ in b}
910
+ a_keys = {(k, v) for k, v, _ in a}
911
+ findings: List[Dict] = []
912
+ done = set()
913
+ for kind, value, line in a:
914
+ if (kind, value) not in b_keys and (kind, value) not in done:
915
+ done.add((kind, value))
916
+ findings.append(make_finding(
917
+ after, line, f"fact_added_{kind}", "error",
918
+ f"{kind} \"{_quote(value)}\" is in the rewrite but not in the source: "
919
+ "remove it, or restore the source's version."))
920
+ for kind, value, line in b:
921
+ if (kind, value) not in a_keys and (kind, value) not in done:
922
+ done.add((kind, value))
923
+ findings.append(make_finding(
924
+ before, line, f"fact_dropped_{kind}", "warning",
925
+ f"{kind} \"{_quote(value)}\" is in the source but not in the rewrite: "
926
+ "confirm the removal was intended."))
927
+ return findings
928
+
929
+
420
930
  # --------------------------------------------------------------------------------------
421
931
  # File / directory orchestration
422
932
  # --------------------------------------------------------------------------------------
423
933
 
424
- def analyze_file(filepath: Path) -> List[Dict]:
934
+ def analyze_file(filepath: Path, voice: bool = False) -> List[Dict]:
425
935
  """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."""
936
+ the same quiet-skip behavior code_quality_checker.py's directory walk relies on. `voice`
937
+ adds the opt-in voice-tell check (Check 7); without it the output is unchanged."""
427
938
  if filepath.suffix.lower() != ".md":
428
939
  return []
429
940
  content = read_file_content(filepath)
@@ -437,17 +948,20 @@ def analyze_file(filepath: Path) -> List[Dict]:
437
948
  findings.extend(check_description(filepath, lines))
438
949
  findings.extend(check_dangling_references(filepath, lines))
439
950
  findings.extend(check_staleness(filepath, lines))
951
+ findings.extend(check_uncited_vendor_claims(filepath, lines))
952
+ if voice:
953
+ findings.extend(check_voice(filepath, lines))
440
954
  return findings
441
955
 
442
956
 
443
- def analyze_directory(dir_path: Path, recursive: bool = True) -> List[Dict]:
957
+ def analyze_directory(dir_path: Path, recursive: bool = True, voice: bool = False) -> List[Dict]:
444
958
  """Analyze every .md file under a directory."""
445
959
  findings: List[Dict] = []
446
960
  pattern = "**/*.md" if recursive else "*.md"
447
961
  for filepath in sorted(dir_path.glob(pattern)):
448
962
  if "node_modules" in str(filepath) or ".git" in str(filepath):
449
963
  continue
450
- findings.extend(analyze_file(filepath))
964
+ findings.extend(analyze_file(filepath, voice))
451
965
  return findings
452
966
 
453
967
 
@@ -483,8 +997,21 @@ def main():
483
997
  )
484
998
  parser.add_argument(
485
999
  "path",
1000
+ nargs="?",
486
1001
  help="File or directory to analyze"
487
1002
  )
1003
+ parser.add_argument(
1004
+ "--voice",
1005
+ action="store_true",
1006
+ help="Also report advisory voice tells (overused and sales words, chatbot residue, ...)"
1007
+ )
1008
+ parser.add_argument(
1009
+ "--facts-diff",
1010
+ nargs=2,
1011
+ metavar=("BEFORE", "AFTER"),
1012
+ help="Compare the numbers, names, dates, URLs and citations of two versions of a file: "
1013
+ "a fact only in AFTER is an error (exit 1), one only in BEFORE a warning"
1014
+ )
488
1015
  parser.add_argument(
489
1016
  "--recursive", "-r",
490
1017
  action="store_true",
@@ -503,6 +1030,21 @@ def main():
503
1030
 
504
1031
  args = parser.parse_args()
505
1032
 
1033
+ if args.facts_diff:
1034
+ before, after = (Path(p).resolve() for p in args.facts_diff)
1035
+ for p in (before, after):
1036
+ if not p.is_file():
1037
+ print(f"Error: Not a file: {p}", file=sys.stderr)
1038
+ sys.exit(1)
1039
+ findings = facts_diff(before, after)
1040
+ if args.json:
1041
+ print(json.dumps(findings, indent=2, default=str))
1042
+ else:
1043
+ print_report(findings, after)
1044
+ sys.exit(1 if any(f["severity"] == "error" for f in findings) else 0)
1045
+
1046
+ if not args.path:
1047
+ parser.error("a path is required unless --facts-diff is given")
506
1048
  target = Path(args.path).resolve()
507
1049
 
508
1050
  if not target.exists():
@@ -510,9 +1052,9 @@ def main():
510
1052
  sys.exit(1)
511
1053
 
512
1054
  if target.is_file():
513
- findings = analyze_file(target)
1055
+ findings = analyze_file(target, args.voice)
514
1056
  else:
515
- findings = analyze_directory(target, args.recursive)
1057
+ findings = analyze_directory(target, args.recursive, args.voice)
516
1058
  if not findings and not any(target.glob("**/*.md" if args.recursive else "*.md")):
517
1059
  print(f"Warning: no .md files found under {display_path(target)}", file=sys.stderr)
518
1060