@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,241 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ /**
6
+ * `lifecycle --action release` and `--action merged` (IC-020, IC-021, DEC-021, DEC-022, DEC-030,
7
+ * DEC-044). A release is previewed from git and recorded, on `--confirm`, as one local
8
+ * `release.recorded` event; nothing is written into the release commit, nothing is staged or
9
+ * committed, and no ignore rule is written. Each function returns a result object without printing.
10
+ *
11
+ * The integration ref is `facts.integration_ref` from `do-git-state.sh --lifecycle`, the very value
12
+ * the status deriver pins, so a preview before the tag and the overview agree about one ref and this
13
+ * module never resolves a ref a second way.
14
+ *
15
+ * Candidate choice reuses `deriveStatuses` with the release tags minus the tag being recorded: a
16
+ * feature then reads `finished` only when a record names it or an OTHER unrecorded `v*` tag contains
17
+ * its merge (DEC-030), which is the "ignore containment in `--tag`" rule without a change to status.js.
18
+ * A feature is dropped for exactly three reasons (IC-020): a release record names it, another
19
+ * unrecorded `v*` tag contains it, or no evidence of its merge reaches X.
20
+ *
21
+ * Evidence into X is looked for in two places: the derivation against the pinned integration ref
22
+ * (the normal case: features merge into develop, the tag is cut from it) and, when the tag exists, a
23
+ * second derivation against the tag's own first-parent chain (a hotfix merged only into the
24
+ * production branch). Each gives the full merge commit, which must be an ancestor of X. When a branch
25
+ * was merged more than once, the newest merge may lie after the tag; an earlier merge of the same
26
+ * branch (or of a merge naming the slug) that X contains then counts. The ancestry questions about X
27
+ * are answered from one graph read (`readHistory`) of the pinned ref, X and the evidence branch tips,
28
+ * so the preview costs a fixed number of git calls however many features and merges there are.
29
+ * Accepted ceiling: the search for an earlier merge walks, in memory, the integration ref's
30
+ * first-parent merges inside X once per feature that needs it.
31
+ *
32
+ * `evidence.commit` and the pinned sha come from `deriveStatuses` (`evidenceCommits`, `integrationSha`),
33
+ * so this module neither re-resolves the integration ref nor an abbreviated sha.
34
+ */
35
+
36
+ const nodeFs = require('node:fs');
37
+ const { spawnSync } = require('node:child_process');
38
+ const { isSafeSlug, invalidSlugRefusal } = require('../task-scope.ts');
39
+ const { appendEvents, readFold } = require('./event-store.ts');
40
+ const { deriveStatuses, readGitFacts, behindNote, subjectNames, readMerges, readBranchTips, readHistory } = require('./status.ts');
41
+ const { FollowupUsageError, oneLine, channelBy } = require('./followup.ts');
42
+
43
+ /** The release-tag pattern of IC-020, the one `git-state` filters by. */
44
+ const RELEASE_TAG = /^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$/;
45
+
46
+ const refusal = (action: any, finding: any, message: any) => ({ ok: false, action, finding, message });
47
+
48
+ /** Trimmed stdout of a git call, or null when git exits non-zero. */
49
+ function gitOut(root: any, args: any) {
50
+ const run = spawnSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] });
51
+ return run.status === 0 ? run.stdout.trim() : null;
52
+ }
53
+
54
+ function commitOf(root: any, ref: any) { return gitOut(root, ['rev-parse', '--verify', '--quiet', `${ref}^{commit}`]) || null; }
55
+
56
+ /** `a` is an ancestor of (or equal to) `b`. */
57
+ function isAncestor(root: any, a: any, b: any) {
58
+ return spawnSync('git', ['merge-base', '--is-ancestor', a, b], { cwd: root, stdio: 'ignore' }).status === 0;
59
+ }
60
+
61
+ /**
62
+ * Evidence of an earlier merge of a branch that was merged more than once: a merge X contains, committed
63
+ * since tracking, whose subject names the slug or whose non-first parent is an ancestor of the branch tip.
64
+ * `merges` are the pinned ref's first-parent merges inside X, newest first; `history` holds X and the tip.
65
+ */
66
+ function earlierMerge({ slug, evidence, tip, lowerBound, merges, bound, history }: any) {
67
+ if (tip && history.isAncestor(tip, bound)) return { ...evidence };
68
+ for (const merge of merges) {
69
+ if (merge.ct < lowerBound) continue;
70
+ // The merged side is this branch's own when it is an ancestor of the tip and the merge itself is not: a
71
+ // branch cut from the integration ref after that merge has the merge in its history and did not make it.
72
+ // A parent outside the first parent's history is one this merge introduced.
73
+ const own = (parent: any) => history.introducer(parent) === merge.sha && history.isAncestor(parent, tip);
74
+ if (tip && merge.parents.slice(1).some(own) && !history.isAncestor(merge.sha, tip)) return { ...evidence };
75
+ if (subjectNames(merge.subject, slug)) return { kind: 'merge-subject', ref: merge.sha.slice(0, 7) };
76
+ }
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * The tag a release follows (it only feeds the output): the base tag `git-state` proposes versions
82
+ * from, other than `tag`, when it is a release tag lying in X's history; else the newest release tag
83
+ * other than `tag` merged into X (git's version order). A `v*` tag that is not a release (`vnext`)
84
+ * never qualifies.
85
+ */
86
+ function previousTagOf(root: any, { tag, facts, bound }: any) {
87
+ const known = new Set(facts.release_tags);
88
+ if (facts.base_tag && facts.base_tag !== tag && known.has(facts.base_tag) && isAncestor(root, `refs/tags/${facts.base_tag}`, bound)) return facts.base_tag;
89
+ const merged = gitOut(root, ['tag', '--list', 'v*', '--sort=-v:refname', '--merged', bound]);
90
+ return (merged || '').split('\n').find((name: any) => name && name !== tag && known.has(name)) || null;
91
+ }
92
+
93
+ function slugList(raw: any, label: any) {
94
+ const list = [...new Set((Array.isArray(raw) ? raw : raw === undefined || raw === null ? [] : [raw]).map(String))];
95
+ for (const slug of list) if (!isSafeSlug(slug)) throw new FollowupUsageError(`${label}: ${invalidSlugRefusal(slug).message}`);
96
+ return list;
97
+ }
98
+
99
+ /**
100
+ * IC-020. Without `confirm` nothing is written; with it one `release.recorded` event is.
101
+ * @param {Object} options
102
+ * @param {string} options.root the IC-001 root
103
+ * @param {string} options.tag `vX.Y.Z[-pre]`
104
+ * @param {boolean} [options.confirm]
105
+ * @param {string[]} [options.features] `--feature` slugs to add
106
+ * @param {string[]} [options.exclude] `--exclude` slugs to leave out
107
+ * @param {string} [options.channel]
108
+ * @param {Object} [options.facts] `readGitFacts` output, for a caller that already has it
109
+ */
110
+ function releaseFeatures({ root, tag, confirm = false, features, exclude, channel, now = new Date(), fsImpl = nodeFs, facts: givenFacts }: any) {
111
+ if (typeof tag !== 'string' || !RELEASE_TAG.test(tag)) throw new FollowupUsageError(`--tag is required and must look like v1.2.3 or v1.2.3-rc.1 (got '${tag ?? ''}')`);
112
+ const added = slugList(features, '--feature');
113
+ const excluded = slugList(exclude, '--exclude');
114
+ const by = channelBy(channel);
115
+
116
+ const facts = givenFacts || readGitFacts(root);
117
+ if (facts.error) return refusal('release', 'no-integration-ref', `git facts are unavailable (${facts.error}), so no release can be previewed or recorded. Nothing was written.`);
118
+ if (!facts.integration_ref) return refusal('release', 'no-integration-ref', 'no integration ref resolves (develop, main, master, origin/HEAD), so no release can be previewed or recorded. Nothing was written.');
119
+
120
+ const fold = readFold(root, { fsImpl, now });
121
+ const trackedSlugs = new Set(fold.features.map((f: any) => f.slug));
122
+ // An exclusion is permanent for its tag, so a mistyped slug must not be recorded any more than a mistyped addition.
123
+ const untracked = [...new Set([...added, ...excluded])].filter((slug) => !trackedSlugs.has(slug));
124
+ if (untracked.length) {
125
+ return refusal('release', 'untracked-feature', `${untracked.join(', ')} ${untracked.length === 1 ? 'is' : 'are'} not tracked, so ${untracked.length === 1 ? 'it' : 'they'} cannot be added to or excluded from a release; run doflow-run lifecycle --action init --slug <slug> first. Nothing was written.`);
126
+ }
127
+
128
+ // DEC-030: containment in the tag being recorded never finishes a feature before its record exists.
129
+ const otherTags = { ...facts, release_tags: facts.release_tags.filter((name: any) => name !== tag) };
130
+ const derived = deriveStatuses({ root, fold, facts: otherTags });
131
+ if (derived.releaseMode === 'unknown' || !derived.integrationSha) return refusal('release', 'no-integration-ref', `${derived.reason || `${facts.integration_ref} does not resolve to a commit`}. Nothing was written.`);
132
+
133
+ // X: the tag's commit once it exists, else the commit the status view pinned the integration ref to (DEC-044).
134
+ const tagCommit = commitOf(root, `refs/tags/${tag}`);
135
+ const bound = tagCommit || derived.integrationSha;
136
+ const previousTag = previousTagOf(root, { tag, facts, bound });
137
+ // A hotfix merged only into the tag's own history has no evidence on the integration chain: derive against the tag as well.
138
+ const atTag = tagCommit && tagCommit !== derived.integrationSha ? deriveStatuses({ root, fold, facts: { ...otherTags, integration_ref: tagCommit } }) : null;
139
+
140
+ const recorded = new Set(fold.releases.flatMap((release: any) => release.features.map((f: any) => f.slug)));
141
+ const tagged = derived.releaseMode === 'tagged';
142
+ // While X is the pinned commit, every evidence commit is in its history. Once X is a tag elsewhere, one
143
+ // graph read of the pinned ref, X and the evidence branch tips answers every ancestry question about X.
144
+ let read: any = null;
145
+ const readOfX = () => {
146
+ if (read) return read;
147
+ const branchRefs = new Set(fold.features.map((f: any) => derived.statuses[f.slug].evidence).filter((e: any) => e && e.kind === 'branch').map((e: any) => e.ref));
148
+ // Local branches are listed first, so a ref resolves as refs/heads/<ref> before refs/remotes/<ref>.
149
+ const tips = new Map();
150
+ for (const t of readBranchTips(root)) if (branchRefs.has(t.short) && !tips.has(t.short)) tips.set(t.short, t.sha);
151
+ const history = readHistory(root, derived.integrationSha, [bound, ...tips.values()]);
152
+ const merges = readMerges(root, derived.integrationSha).filter((m: any) => history.isAncestor(m.sha, bound)).reverse();
153
+ read = { history, merges, tips };
154
+ return read;
155
+ };
156
+ const inX = (commit: any) => bound === derived.integrationSha || readOfX().history.isAncestor(commit, bound);
157
+ const candidates = [];
158
+ const notDetected = [];
159
+ // A git failure while reading X's history is an environment fault: a refusal, never a throw that would
160
+ // reach failure capture (NFR-005), as the status derivation reports its own as `unknown`.
161
+ try {
162
+ for (const feature of fold.features) {
163
+ const { slug } = feature;
164
+ if (recorded.has(slug)) continue;
165
+ const entry = derived.statuses[slug];
166
+ const hotfix = atTag && atTag.statuses[slug];
167
+ // With another v* tag in play, `finished` here means an unrecorded tag already contains the merge.
168
+ if (tagged && (entry.status === 'finished' || (hotfix && hotfix.status === 'finished'))) continue;
169
+ const evidence = entry.evidence;
170
+ let found = null;
171
+ if (evidence) {
172
+ const commit = derived.evidenceCommits[slug];
173
+ if (evidence.kind === 'confirmed' || (commit && inX(commit))) found = evidence;
174
+ }
175
+ if (!found && hotfix && hotfix.evidence) found = hotfix.evidence;
176
+ if (!found && evidence) {
177
+ const { history, merges, tips } = readOfX();
178
+ const tip = evidence.kind === 'branch' ? tips.get(evidence.ref) || null : null;
179
+ const lowerBound = Math.floor(Date.parse(feature.trackedAt) / 1000);
180
+ found = earlierMerge({ slug, evidence, tip, lowerBound, merges, bound, history });
181
+ }
182
+ if (found) candidates.push({ slug, evidence: found.kind, ref: found.ref }); else notDetected.push(slug);
183
+ }
184
+ } catch (error: any) {
185
+ return refusal('release', 'no-integration-ref', `git could not answer while reading the history of ${tagCommit ? tag : facts.integration_ref} (git-state-failed): ${String(error.message).split('\n')[0]}. Nothing was written.`);
186
+ }
187
+
188
+ const shipped = [...candidates];
189
+ for (const slug of added) if (!shipped.some((c) => c.slug === slug)) shipped.push({ slug, evidence: 'confirmed', ref: null });
190
+ const finishing = shipped.filter((c) => !excluded.includes(c.slug));
191
+ const finishingSlugs = new Set(finishing.map((c) => c.slug));
192
+ const followupsDone = fold.followups.filter((item: any) => item.state === 'taken' && finishingSlugs.has(item.takenBy)).map((item: any) => item.id);
193
+
194
+ const base: any = { ok: true, action: 'release', tag, previousTag, bound: tagCommit ? tag : facts.integration_ref, integrationRef: facts.integration_ref };
195
+ if (derived.integrationBehind > 0) base.note = behindNote(facts.integration_ref, derived.integrationBehind);
196
+
197
+ if (!confirm) {
198
+ const addHint = 'add --feature <slug> for a feature listed under notDetected that shipped';
199
+ return {
200
+ ...base, recorded: false, candidates, added, excluded, notDetected, followupsDone,
201
+ next: [tagCommit
202
+ ? `Record it: doflow-run lifecycle --action release --tag ${tag} --confirm (${addHint}; --exclude <slug> leaves one out)`
203
+ : `After git tag ${tag}: doflow-run lifecycle --action release --tag ${tag} --confirm (${addHint})`],
204
+ };
205
+ }
206
+ if (!tagCommit) return refusal('release', 'tag-missing', `${tag} does not exist; create the tag first, then confirm. Nothing was written.`);
207
+ const out = appendEvents(root, [{ type: 'release.recorded', by, data: { tag, commit: tagCommit, features: finishing, excluded } }], { now, fsImpl });
208
+ if (!out.ok) return refusal('release', out.finding, out.message);
209
+ return { ...base, recorded: true, candidates, added, excluded, notDetected, followupsDone, events: out.written.map((w: any) => w.file), next: [] };
210
+ }
211
+
212
+ /**
213
+ * IC-021 `merged`: the user's confirmation of a merge git cannot show (squash, rebase, fast-forward,
214
+ * cherry-pick). It records an observed fact after the merge, one `feature.merged` event, and the
215
+ * status stays derived from events on read, so nothing is written ahead of the fact (DEC-022).
216
+ */
217
+ function recordMerged({ root, slug, reason, channel, now = new Date(), fsImpl = nodeFs }: any) {
218
+ if (!slug) throw new FollowupUsageError('--slug is required for --action merged');
219
+ if (!isSafeSlug(slug)) throw new FollowupUsageError(invalidSlugRefusal(slug).message);
220
+ const problems: any[] = [];
221
+ const cleanReason = reason === undefined ? null : oneLine(reason, '--reason', problems);
222
+ if (!problems.length && !cleanReason) problems.push('--reason is required for --action merged: how the merge happened, for example "squash-merged by hand"');
223
+ if (problems.length) throw new FollowupUsageError(problems.join('; '));
224
+ const by = channelBy(channel);
225
+
226
+ const fold = readFold(root, { fsImpl, now });
227
+ if (!fold.features.some((f: any) => f.slug === slug)) {
228
+ return refusal('merged', 'untracked-feature', `${slug} is not a tracked feature, so no merge can be confirmed for it; run doflow-run lifecycle --action init --slug ${slug} first. Nothing was written.`);
229
+ }
230
+ const before = deriveStatuses({ root, fold }).statuses[slug];
231
+ if (before.status === 'unknown') return refusal('merged', 'no-integration-ref', `git cannot answer here, so a merge cannot be confirmed against an integration ref. Nothing was written.`);
232
+ if (before.evidence) {
233
+ return refusal('merged', 'already-merged', `${slug} already has merge evidence (${before.evidence.kind}${before.evidence.ref ? ` ${before.evidence.ref}` : ''}); nothing to confirm. Nothing was written.`);
234
+ }
235
+ const out = appendEvents(root, [{ type: 'feature.merged', by, data: { slug, reason: cleanReason } }], { now, fsImpl });
236
+ if (!out.ok) return refusal('merged', out.finding, out.message);
237
+ const after = deriveStatuses({ root, fold: readFold(root, { fsImpl, now }) }).statuses[slug];
238
+ return { ok: true, action: 'merged', slug, status: after.status, evidence: after.evidence, release: after.release, events: out.written.map((w: any) => w.file), next: [] };
239
+ }
240
+
241
+ module.exports = { releaseFeatures, recordMerged, RELEASE_TAG };
@@ -0,0 +1,221 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ /**
6
+ * The machine-local report body (IC-005, DEC-014). A report's text can hold a crash trace, a log or
7
+ * pasted output, so it never enters the repository: the body is masked with the body profile (IC-012),
8
+ * cut to at most 1 MiB and written once under the same per-machine home the failure store uses,
9
+ * `<XDG_CONFIG_HOME or $HOME/.config>/doflow/reports/<projectKey>/<FU-id>.txt`, folder 0700 and file
10
+ * 0600. The project event carries only a reference, the size and a masked 2 KB excerpt.
11
+ *
12
+ * `projectKey` is the first 12 hex characters of the SHA-256 of the IC-001 root's real path, so two
13
+ * clones of one project on one machine keep separate bodies, and a body is found again from any
14
+ * worktree that shares the root.
15
+ *
16
+ * The cut is by bytes and backs off to a UTF-8 boundary. Nothing here touches the network.
17
+ */
18
+
19
+ const nodeFs = require('node:fs');
20
+ const path = require('node:path');
21
+ const crypto = require('node:crypto');
22
+ const mask = require('../mask.ts');
23
+
24
+ /** A `--file` or `--stdin` source is read for at most this many bytes; the rest could not be kept anyway. */
25
+ const MAX_READ_BYTES = 16 * 1024 * 1024;
26
+ /** The most a stored body holds, marker line included. */
27
+ const MAX_BODY_BYTES = 1024 * 1024;
28
+ const TRUNCATED = '[truncated by doflow at 1 MiB]';
29
+ /** Masking sees at most this much of a long body: the limit plus a margin that masking can shrink back into it. */
30
+ const MASK_INPUT_BYTES = MAX_BODY_BYTES + 64 * 1024;
31
+ const FU_ID = /^FU-[0-9a-hjkmnp-tv-z]{6}$/;
32
+ /** CSI (`ESC [ ... final`) and OSC (`ESC ] ... BEL` or `ESC \`) sequences: terminal colour and title codes in a pasted log. */
33
+ const ANSI = /\u001b\[[0-?]*[ -/]*[@-~]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g;
34
+ /** The most of the masked body the project event carries. */
35
+ const EXCERPT_BYTES = 2048;
36
+
37
+ /** A caller mistake in the report input (exit 2); the verb layer maps it with the other usage errors. */
38
+ class ReportInputError extends Error {}
39
+
40
+ /**
41
+ * The folder holding every project's report bodies, or null when no home resolves. It resolves the
42
+ * base the way IC-011 resolves the failure home (`XDG_CONFIG_HOME` when set, non-empty and absolute,
43
+ * else `$HOME/.config` when `HOME` is absolute) and sits beside it, `doflow/reports`. The rule is
44
+ * repeated here rather than required from the failure module, so the follow-up service still loads in
45
+ * a build or an install where the failure modules cannot be loaded (IC-016).
46
+ */
47
+ function reportsHome(env: Record<string, string | undefined> = process.env): string | null {
48
+ const xdg = env.XDG_CONFIG_HOME;
49
+ if (typeof xdg === 'string' && xdg !== '') return path.isAbsolute(xdg) ? path.join(xdg, 'doflow', 'reports') : null;
50
+ const home = env.HOME;
51
+ return typeof home === 'string' && path.isAbsolute(home) ? path.join(home, '.config', 'doflow', 'reports') : null;
52
+ }
53
+
54
+ function realProjectRoot(root: string, fsImpl: any = nodeFs): string {
55
+ try { return fsImpl.realpathSync(root); } catch { return root; }
56
+ }
57
+
58
+ function projectKey(root: string, fsImpl: any = nodeFs): string {
59
+ return crypto.createHash('sha256').update(realProjectRoot(root, fsImpl)).digest('hex').slice(0, 12);
60
+ }
61
+
62
+ /** @returns {string|null} the body file of a report of this project, or null when no home resolves */
63
+ function bodyPath(root: string, id: string, { env = process.env, fsImpl = nodeFs }: any = {}): string | null {
64
+ const home = reportsHome(env);
65
+ return home === null ? null : path.join(home, projectKey(root, fsImpl), `${id}.txt`);
66
+ }
67
+
68
+ /** The `hasBody` the fold needs (IC-003): a report whose body file is on this machine. */
69
+ function bodyChecker(root: string, { env = process.env, fsImpl = nodeFs }: any = {}): (item: any) => boolean {
70
+ const home = reportsHome(env);
71
+ const folder = home === null ? null : path.join(home, projectKey(root, fsImpl));
72
+ return (item: any): boolean => {
73
+ // The id comes from an event file, so it is checked before it becomes part of a path.
74
+ if (folder === null || !FU_ID.test(String(item.id)) || item.bodyRef !== `local:${item.id}`) return false;
75
+ try { return fsImpl.statSync(path.join(folder, `${item.id}.txt`)).isFile(); } catch { return false; }
76
+ };
77
+ }
78
+
79
+ /** Reads at most `max` bytes from an open descriptor. */
80
+ function readCapped(fsImpl: any, fd: number, max: number): Buffer {
81
+ const chunks: Buffer[] = [];
82
+ let total = 0;
83
+ const chunk = Buffer.allocUnsafe(64 * 1024);
84
+ while (total < max) {
85
+ const n = fsImpl.readSync(fd, chunk, 0, Math.min(chunk.length, max - total), null);
86
+ if (n === 0) break;
87
+ chunks.push(Buffer.from(chunk.subarray(0, n)));
88
+ total += n;
89
+ }
90
+ return Buffer.concat(chunks);
91
+ }
92
+
93
+ /**
94
+ * The raw report text from exactly one of `--file`, `--stdin` or `--text`. A file is read as text, at
95
+ * most 16 MiB (`--file -` reads stdin); a binary one (a NUL byte) is refused.
96
+ * @param {{file?: string, stdin?: boolean, text?: string}} source
97
+ * @returns {string}
98
+ */
99
+ function readReportText({ file, stdin: stdinFlag, text }: { file?: string; stdin?: boolean; text?: string }, { fsImpl = nodeFs }: any = {}): string {
100
+ // `--file -` is stdin, as everywhere else a `-` stands for it.
101
+ const stdin = Boolean(stdinFlag) || file === '-';
102
+ if (file === '-') file = undefined;
103
+ const given = [file !== undefined, Boolean(stdin), text !== undefined].filter(Boolean).length;
104
+ if (given !== 1) throw new ReportInputError('give the report body with exactly one of --file <path>, --stdin or --text "<body>" (--file and --stdin also take text that starts with -)');
105
+ if (text !== undefined) return String(text);
106
+ let bytes: Buffer;
107
+ if (stdin) {
108
+ try { bytes = readCapped(fsImpl, 0, MAX_READ_BYTES); } catch (error: any) { throw new ReportInputError(`cannot read the report from stdin: ${error.message}`); }
109
+ } else {
110
+ const target = path.resolve(file as string);
111
+ let fd: number | undefined;
112
+ try {
113
+ if (!fsImpl.statSync(target).isFile()) throw new ReportInputError(`--file ${file} is not a regular file`);
114
+ fd = fsImpl.openSync(target, 'r');
115
+ bytes = readCapped(fsImpl, fd as number, MAX_READ_BYTES);
116
+ } catch (error: any) {
117
+ if (error instanceof ReportInputError) throw error;
118
+ throw new ReportInputError(`cannot read --file ${file}: ${error.code || error.message}`);
119
+ } finally {
120
+ if (fd !== undefined) { try { fsImpl.closeSync(fd); } catch { /* read already done */ } }
121
+ }
122
+ }
123
+ if (bytes.includes(0)) throw new ReportInputError('the report body is binary (it holds a NUL byte); give a text file or paste the text');
124
+ return bytes.toString('utf8');
125
+ }
126
+
127
+ /** The length of the longest prefix of `buffer` of at most `limit` bytes that ends on a UTF-8 boundary. */
128
+ function boundary(buffer: Buffer, limit: number): number {
129
+ let end = Math.min(limit, buffer.length);
130
+ if (end === buffer.length) return end;
131
+ while (end > 0 && (buffer[end] & 0xc0) === 0x80) end -= 1;
132
+ return end;
133
+ }
134
+
135
+ /**
136
+ * The text masking will see: ANSI sequences removed, then cut to the limit plus a margin so one very
137
+ * long input cannot make a mask rule recurse too deep. The cut is at the last line end in range, else the
138
+ * last whitespace, else nothing is kept (an unbroken run has no clean boundary), always on a UTF-8
139
+ * boundary; a token straddling the cut goes with the incomplete tail, never half-masked.
140
+ * @returns {{text: string, cut: boolean}}
141
+ */
142
+ function boundMaskInput(raw: string): { text: string; cut: boolean } {
143
+ const stripped = raw.replace(ANSI, '');
144
+ const whole = Buffer.from(stripped, 'utf8');
145
+ if (whole.length <= MASK_INPUT_BYTES) return { text: stripped, cut: false };
146
+ const head = whole.subarray(0, boundary(whole, MASK_INPUT_BYTES));
147
+ let end = head.lastIndexOf(0x0a);
148
+ if (end < 0) for (let i = head.length - 1; i >= 0; i -= 1) if ([0x20, 0x09, 0x0d, 0x0b, 0x0c].includes(head[i])) { end = i; break; }
149
+ return { text: end < 0 ? '' : head.subarray(0, end).toString('utf8'), cut: true };
150
+ }
151
+
152
+ /**
153
+ * Masks a report body with the body profile and keeps at most 1 MiB of it. A longer body is cut at a
154
+ * UTF-8 boundary and ends with the marker line, so the stored text is never over the limit. A body
155
+ * the mask cannot process is refused as a caller mistake, with one line and no echo of the input.
156
+ * @param {string} raw
157
+ * @param {{home?: string}} [options] the home directory the mask replaces with `~`
158
+ * @returns {{buffer: Buffer, masked: number, truncated: boolean}}
159
+ */
160
+ function prepareBody(raw: string, { home }: { home?: string } = {}): { buffer: Buffer; masked: number; truncated: boolean } {
161
+ const { text: bounded, cut } = boundMaskInput(raw);
162
+ let result: any;
163
+ try {
164
+ result = mask.maskBody(bounded, home === undefined ? undefined : { home });
165
+ } catch {
166
+ throw new ReportInputError('the report body could not be masked safely (its text is too irregular to process); cut it down or file the relevant part');
167
+ }
168
+ const whole = Buffer.from(result.text, 'utf8');
169
+ if (whole.length <= MAX_BODY_BYTES && !cut) return { buffer: whole, masked: result.masked, truncated: false };
170
+ const marker = Buffer.from(`\n${TRUNCATED}`, 'utf8');
171
+ const kept = whole.subarray(0, boundary(whole, MAX_BODY_BYTES - marker.length));
172
+ return { buffer: Buffer.concat([kept, marker]), masked: result.masked, truncated: true };
173
+ }
174
+
175
+ /**
176
+ * The first 2048 bytes of a stored body, cut at a UTF-8 boundary and, when a line end lies inside
177
+ * the limit, at that line end (DEC-014).
178
+ * @param {Buffer} body
179
+ * @returns {string}
180
+ */
181
+ function excerptOf(body: Buffer): string {
182
+ if (body.length <= EXCERPT_BYTES) return body.toString('utf8').replace(/\s+$/, '');
183
+ const head = body.subarray(0, boundary(body, EXCERPT_BYTES));
184
+ const lineEnd = head.lastIndexOf(0x0a);
185
+ return (lineEnd > 0 ? head.subarray(0, lineEnd) : head).toString('utf8').replace(/\s+$/, '');
186
+ }
187
+
188
+ /**
189
+ * Writes the body once: exclusive create, file 0600, folders 0700, never inside the repository.
190
+ * @returns {{ok: true, file: string}|{ok: false, reason: 'no-home'|'exists'|'error', message?: string}}
191
+ */
192
+ function writeBody(root: string, id: string, buffer: Buffer, { env = process.env, fsImpl = nodeFs }: any = {}): { ok: boolean; file?: string; reason?: string; message?: string } {
193
+ const file = bodyPath(root, id, { env, fsImpl });
194
+ if (file === null) return { ok: false, reason: 'no-home' };
195
+ const folder = path.dirname(file);
196
+ try {
197
+ fsImpl.mkdirSync(folder, { recursive: true, mode: 0o700 });
198
+ // `mkdir -p` applies the mode under the umask; the two folders that are ours are set outright.
199
+ fsImpl.chmodSync(folder, 0o700);
200
+ fsImpl.chmodSync(path.dirname(folder), 0o700);
201
+ try {
202
+ fsImpl.writeFileSync(path.join(folder, '.root'), `${realProjectRoot(root, fsImpl)}\n`, { flag: 'wx', mode: 0o600 });
203
+ } catch (error: any) {
204
+ if (error.code !== 'EEXIST') throw error;
205
+ }
206
+ fsImpl.writeFileSync(file, buffer, { flag: 'wx', mode: 0o600 });
207
+ return { ok: true, file };
208
+ } catch (error: any) {
209
+ return error.code === 'EEXIST' ? { ok: false, reason: 'exists' } : { ok: false, reason: 'error', message: error.code || error.message };
210
+ }
211
+ }
212
+
213
+ /** Removes a body this process just wrote, when the event that names it could not be written. */
214
+ function removeBody(file: string, fsImpl: any = nodeFs): void {
215
+ try { fsImpl.rmSync(file, { force: true }); } catch { /* the failure being reported stands */ }
216
+ }
217
+
218
+ module.exports = {
219
+ readReportText, prepareBody, excerptOf, writeBody, removeBody, bodyChecker, bodyPath, reportsHome, projectKey,
220
+ ReportInputError, MAX_READ_BYTES, MAX_BODY_BYTES, MASK_INPUT_BYTES, EXCERPT_BYTES, TRUNCATED,
221
+ };