@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,338 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+ const crypto = require('node:crypto');
8
+ const { REPO_ROOT } = require('../helper/repo-root.ts');
9
+ const { updateTaskState, readTaskState, mergeRecords } = require('./task-state.ts');
10
+ const { taskStoreDir } = require('./task-scope.ts');
11
+
12
+ const VALID_EVIDENCE_KINDS = new Set([
13
+ 'exact-search',
14
+ 'semantic-retrieval',
15
+ 'structural',
16
+ 'historical',
17
+ 'documentation',
18
+ 'test-result',
19
+ 'runtime-observation',
20
+ 'user-statement',
21
+ 'diff',
22
+ 'generated-analysis',
23
+ ]);
24
+
25
+ const VALID_PROVENANCE = new Set(['extracted', 'inferred', 'asserted']);
26
+
27
+ /** An item's lifecycle status, distinct from `freshness.status` (a snapshot judgment about the
28
+ * file the item points at, which a later edit can only ever move toward STALE, never back).
29
+ * 'active' is the only non-terminal value; 'superseded' is terminal and permanent — mirrors
30
+ * `claims.js`'s TERMINAL_CLAIM_STATUSES so a fresher record can retire an item without deleting
31
+ * it (NFR-003 applies here the same way it does to claims). */
32
+ const EVIDENCE_STATUSES = new Set(['active', 'superseded']);
33
+
34
+ /**
35
+ * Gate for a status arriving from disk. The set above was declared and exported while nothing read
36
+ * it, so the "mirrors claims.js" claim in the comment held for the declaration and not for the use.
37
+ *
38
+ * This throws where `claims.js` normalizes, and the asymmetry is deliberate: an unrecognized claim
39
+ * status falls back to `'hypothesis'`, the weakest state, so the worst case is a claim having to
40
+ * earn its support again. There is no equivalently safe default here. Treating an unreadable status
41
+ * as `'active'` would silently un-retire superseded evidence and feed it back into readiness — the
42
+ * exact failure `supersede` exists to prevent — and treating it as `'superseded'` would retire
43
+ * evidence nobody retired. With no safe direction, the honest move is to refuse the file and name
44
+ * the item, rather than pick one and be wrong in a way no output reveals.
45
+ *
46
+ * @param {object} item
47
+ * @param {string} file the ledger path, so the message says which file to look at
48
+ * @returns {object} the same item, once its status is one of the two
49
+ */
50
+ function assertKnownStatus(item: any, file: string) {
51
+ const status = item?.status;
52
+ if (!EVIDENCE_STATUSES.has(status)) {
53
+ throw new Error(
54
+ `Evidence '${item?.id ?? '(no id)'}' in ${file} has status '${status}', which is not one of: `
55
+ + `${[...EVIDENCE_STATUSES].join(', ')}. Refusing to guess whether it is active.`,
56
+ );
57
+ }
58
+ return item;
59
+ }
60
+
61
+ /** A task id becomes a filename inside the state directory, so it must not be able to name a path.
62
+ * `path.join(stateDir, `${taskId}.json`)` happily resolves `../../../etc/hosts` out of the state
63
+ * dir entirely — read-only today only because nothing calls save(), and an arbitrary file write
64
+ * the moment a writer is wired up. Constrain it to a flat, filename-safe token instead of trying
65
+ * to sanitize a traversal after the fact.
66
+ * @param {string} taskId
67
+ * @returns {string} the validated id, for use in an expression
68
+ */
69
+ function assertSafeTaskId(taskId: string): string {
70
+ if (typeof taskId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(taskId) || taskId.includes('..')) {
71
+ throw new Error(`Invalid task id '${taskId}': expected letters, digits, dot, dash or underscore, and no path separators`);
72
+ }
73
+ return taskId;
74
+ }
75
+
76
+ class EvidenceLedger {
77
+ declare fsImpl: any;
78
+ declare repoRoot: any;
79
+ declare fixedStateDir: any;
80
+ declare slug: any;
81
+ declare stateDir: any;
82
+ declare evidenceMap: Map<any, any>;
83
+ declare baseline: Map<any, any>;
84
+ declare seq: number;
85
+ /**
86
+ * @param {Object} [options]
87
+ * @param {string} [options.repoRoot]
88
+ * @param {string} [options.stateDir] a fixed directory; when given, no feature namespace applies
89
+ * @param {string} [options.slug] feature whose namespace the records live under (see task-scope.js)
90
+ * @param {Object} [options.fsImpl]
91
+ */
92
+ constructor(options: any = {}) {
93
+ this.fsImpl = options.fsImpl || fs;
94
+ this.repoRoot = options.repoRoot || REPO_ROOT;
95
+ this.fixedStateDir = options.stateDir || null;
96
+ this.slug = options.slug || null;
97
+ this.stateDir = this.fixedStateDir || path.join(this.repoRoot, '.doflow', 'state', 'evidence');
98
+ this.evidenceMap = new Map();
99
+ this.baseline = new Map();
100
+ this.seq = 0;
101
+ }
102
+
103
+ /** Where this task's evidence file lives: the feature's namespace when it has one (IC-002). */
104
+ fileFor(taskId: any) {
105
+ const dir = this.fixedStateDir
106
+ || taskStoreDir({ projectRoot: this.repoRoot, store: 'evidence', taskId, slug: this.slug });
107
+ return path.join(dir, `${assertSafeTaskId(taskId)}.json`);
108
+ }
109
+
110
+ /**
111
+ * Generates a unique evidence identifier. Uniqueness must hold ACROSS instances and processes,
112
+ * not just within one: ids key the concurrency-safe merge in save() (task-state.js), and two
113
+ * ledgers minting `ev_<ms>_1` in the same millisecond made the merge collapse two distinct
114
+ * observations into one — the very lost write R4 exists to prevent, reintroduced by an id
115
+ * scheme. The random component carries that burden; timestamp and sequence stay for readability.
116
+ * @returns {string}
117
+ */
118
+ generateId(): string {
119
+ this.seq += 1;
120
+ return `ev_${Date.now().toString(36)}_${crypto.randomBytes(4).toString('hex')}_${this.seq.toString(36)}`;
121
+ }
122
+
123
+ /**
124
+ * Validates and registers a new evidence item.
125
+ * @param {Object} item
126
+ * @returns {string} evidenceId
127
+ */
128
+ addEvidence(item: any): string {
129
+ if (!item || typeof item !== 'object') {
130
+ throw new Error('Evidence item must be an object');
131
+ }
132
+
133
+ if (!item.kind || !VALID_EVIDENCE_KINDS.has(item.kind)) {
134
+ throw new Error(`Invalid evidence kind '${item.kind}'. Allowed: ${[...VALID_EVIDENCE_KINDS].join(', ')}`);
135
+ }
136
+
137
+ const provenance = item.provenance || 'extracted';
138
+ if (!VALID_PROVENANCE.has(provenance)) {
139
+ throw new Error(`Invalid provenance '${provenance}'. Allowed: ${[...VALID_PROVENANCE].join(', ')}`);
140
+ }
141
+
142
+ const id = item.id || this.generateId();
143
+ const taskId = item.taskId || 'default';
144
+
145
+ const source = item.source || {
146
+ provider: 'unknown',
147
+ capability: 'general',
148
+ };
149
+
150
+ const freshness = {
151
+ gitCommit: item.freshness?.gitCommit || null,
152
+ fileHash: item.freshness?.fileHash || null,
153
+ observedAt: item.freshness?.observedAt || new Date().toISOString(),
154
+ status: item.freshness?.status || 'FRESH',
155
+ };
156
+
157
+ const evidenceRecord = {
158
+ id,
159
+ taskId,
160
+ kind: item.kind,
161
+ source,
162
+ locator: item.locator || {},
163
+ provenance,
164
+ content: item.content || null,
165
+ freshness,
166
+ // Which template requirements this item claims to establish. Readiness counts an item toward
167
+ // a requirement only when the writer named it here — kind-matching alone let an item saying
168
+ // "tests failed" satisfy a passing-baseline requirement (architecture review R1, P1).
169
+ establishes: Array.isArray(item.establishes) ? [...item.establishes] : [],
170
+ // Typed execution record for test-result / runtime-observation items: what ran and how it
171
+ // exited. Tree identity and time live in freshness (gitCommit, observedAt).
172
+ observation: item.observation
173
+ ? { command: item.observation.command, exitCode: item.observation.exitCode }
174
+ : null,
175
+ supports: Array.isArray(item.supports) ? [...item.supports] : [],
176
+ contradicts: Array.isArray(item.contradicts) ? [...item.contradicts] : [],
177
+ // Every item starts 'active'. Not settable through addEvidence's own input (EVIDENCE_ITEM_FIELDS
178
+ // in cli.js does not list it) — the only way to reach 'superseded' is supersedeEvidence below,
179
+ // the same boundary claims.js already draws around its own status field.
180
+ status: 'active',
181
+ supersededBy: null,
182
+ };
183
+
184
+ this.evidenceMap.set(id, evidenceRecord);
185
+ return id;
186
+ }
187
+
188
+ /**
189
+ * Retrieves an evidence item by ID.
190
+ * @param {string} id
191
+ * @returns {Object|null}
192
+ */
193
+ getEvidence(id: string) {
194
+ return this.evidenceMap.get(id) || null;
195
+ }
196
+
197
+ /**
198
+ * Queries evidence items matching criteria.
199
+ * @param {Object} [filter={}]
200
+ * @returns {Array<Object>}
201
+ */
202
+ queryEvidence(filter: any = {}) {
203
+ const results = [];
204
+ for (const item of this.evidenceMap.values()) {
205
+ if (filter.taskId && item.taskId !== filter.taskId) continue;
206
+ if (filter.kind && item.kind !== filter.kind) continue;
207
+ if (filter.provider && item.source.provider !== filter.provider) continue;
208
+ if (filter.capability && item.source.capability !== filter.capability) continue;
209
+ if (filter.file && item.locator.file !== filter.file) continue;
210
+ if (filter.status && item.freshness.status !== filter.status) continue;
211
+ results.push(item);
212
+ }
213
+ return results;
214
+ }
215
+
216
+ /**
217
+ * Records that this item's content no longer represents the tree as it stands, and points
218
+ * forward to the item that does. Nothing is deleted or rewritten: the record and its original
219
+ * freshness stay exactly as observed, only `status` changes — mirrors
220
+ * `ClaimsManager.supersedeClaim` field for field, including the checks and their order (the
221
+ * replacement is validated before the target, for the same reason: a typo in the target reads
222
+ * as "unknown evidence" either way, but a typo in the replacement should not be reported as a
223
+ * problem with the target instead).
224
+ * @param {string} evidenceId the item to retire
225
+ * @param {string} replacedBy the id of the item that supersedes it; must already exist
226
+ * @returns {string} the new status
227
+ */
228
+ supersedeEvidence(evidenceId: string, replacedBy: string): string {
229
+ if (!replacedBy) {
230
+ throw new Error('supersede requires the id of the evidence item that replaces this one');
231
+ }
232
+ if (replacedBy === evidenceId) {
233
+ throw new Error(`Evidence '${evidenceId}' cannot supersede itself`);
234
+ }
235
+ if (!this.evidenceMap.has(replacedBy)) {
236
+ throw new Error(`No evidence '${replacedBy}' is recorded — a forward pointer to nothing is worse `
237
+ + 'than no pointer. Record the replacing evidence first: doflow evidence --task-id <id> --action add ...');
238
+ }
239
+ const item = this.evidenceMap.get(evidenceId);
240
+ if (!item) {
241
+ throw new Error(`Unknown evidence '${evidenceId}'`);
242
+ }
243
+ if (item.status === 'superseded') {
244
+ throw new Error(`Evidence '${evidenceId}' is already superseded and cannot be superseded again`);
245
+ }
246
+ item.status = 'superseded';
247
+ item.supersededBy = replacedBy;
248
+ item.supersededAt = new Date().toISOString();
249
+ return item.status;
250
+ }
251
+
252
+ /**
253
+ * Marks all evidence items referencing modified files as STALE.
254
+ * @param {Array<string>} modifiedFiles
255
+ * @returns {number} count of invalidated items
256
+ */
257
+ invalidateFiles(modifiedFiles: any): number {
258
+ if (!Array.isArray(modifiedFiles) || modifiedFiles.length === 0) return 0;
259
+ const fileSet = new Set(modifiedFiles);
260
+ let count = 0;
261
+
262
+ for (const item of this.evidenceMap.values()) {
263
+ if (item.locator?.file && fileSet.has(item.locator.file)) {
264
+ item.freshness.status = 'STALE';
265
+ count += 1;
266
+ }
267
+ }
268
+ return count;
269
+ }
270
+
271
+ /**
272
+ * Returns all evidence records in memory.
273
+ * @returns {Array<Object>}
274
+ */
275
+ getAllEvidence() {
276
+ return Array.from(this.evidenceMap.values());
277
+ }
278
+
279
+ /**
280
+ * Persists evidence for a specific task to disk — locked, merged, revisioned (task-state.js).
281
+ * A concurrent writer's items that this instance never loaded survive the save: they are folded
282
+ * in from disk inside the lock, so overlapping sessions append rather than overwrite each other.
283
+ * @param {string} [taskId='default']
284
+ * @returns {string} filePath
285
+ */
286
+ save(taskId = 'default'): string {
287
+ const targetFile = this.fileFor(taskId);
288
+ let merged: any;
289
+ const file = updateTaskState({
290
+ fsImpl: this.fsImpl, file: targetFile,
291
+ build: (disk: any) => {
292
+ merged = mergeRecords(this.queryEvidence({ taskId }), disk?.evidence || [], this.baseline);
293
+ return { taskId, updatedAt: new Date().toISOString(), evidenceCount: merged.length, evidence: merged };
294
+ },
295
+ });
296
+ for (const item of merged) {
297
+ this.evidenceMap.set(item.id, item);
298
+ this.baseline.set(item.id, JSON.parse(JSON.stringify(item)));
299
+ }
300
+ return file;
301
+ }
302
+
303
+ /**
304
+ * Loads evidence for a task from disk into memory.
305
+ * @param {string} [taskId='default']
306
+ * @returns {number} count of loaded items
307
+ */
308
+ load(taskId = 'default'): number {
309
+ const targetFile = this.fileFor(taskId);
310
+ const data = readTaskState(this.fsImpl, targetFile);
311
+ if (data && Array.isArray(data.evidence)) {
312
+ for (const item of data.evidence) {
313
+ assertKnownStatus(item, targetFile);
314
+ this.evidenceMap.set(item.id, item);
315
+ this.baseline.set(item.id, JSON.parse(JSON.stringify(item)));
316
+ }
317
+ return data.evidence.length;
318
+ }
319
+ return 0;
320
+ }
321
+
322
+ /**
323
+ * Resets the in-memory ledger.
324
+ */
325
+ clear() {
326
+ this.evidenceMap.clear();
327
+ this.baseline.clear();
328
+ this.seq = 0;
329
+ }
330
+ }
331
+
332
+ module.exports = {
333
+ EvidenceLedger,
334
+ VALID_EVIDENCE_KINDS,
335
+ VALID_PROVENANCE,
336
+ EVIDENCE_STATUSES,
337
+ assertSafeTaskId,
338
+ };
@@ -0,0 +1,272 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ /**
6
+ * The Node failure writer (IC-011, IC-014, IC-015, DEC-019, DEC-020). Records one line per internal
7
+ * error in the machine-wide `events.jsonl`. Capture is a side channel and must never be noticed by
8
+ * the command it observes:
9
+ * - it writes no byte to stdout or stderr and never throws;
10
+ * - it is synchronous and takes no lock (one append of one line);
11
+ * - while capture is off it opens no file and creates no folder.
12
+ * Only the normalised, masked message is stored, never the raw message, argv, stdin or environment.
13
+ *
14
+ * Rotation by rename at 1 MiB is done here and by the `failure` verb only; the bash writers append.
15
+ */
16
+
17
+ const fs = require('node:fs');
18
+ const os = require('node:os');
19
+ const path = require('node:path');
20
+ const { REPO_ROOT } = require('../../helper/repo-root.ts');
21
+ const { normaliseMessage, maskLine } = require('../mask.ts');
22
+ const { isProgrammingError, errorKind } = require('./classifier.ts');
23
+ const { failureHome, captureIsOff, eventsPath } = require('./home.ts');
24
+
25
+ const MAX_LINE_BYTES = 2048;
26
+ const MAX_RAW_MESSAGE = 4096;
27
+ const ROTATE_AT_BYTES = 1048576;
28
+ const KEEP_ROTATED = 4;
29
+ const PARTIAL_KEEP_MS = 60 * 60 * 1000;
30
+ const ROTATED = /^events-\d{8}T\d{6}Z-\d+\.jsonl$/;
31
+ const COMMAND_NAME = /^[a-z][a-z0-9-]{0,39}$/;
32
+
33
+ /** Errors already recorded in this process, so one error caught at two points counts once. */
34
+ const recorded = new WeakSet();
35
+
36
+ let cachedVersion: string | null = null;
37
+ /** The package version; the projected runtime ships no package.json, so a tolerant read. */
38
+ function packageVersion(): string {
39
+ if (cachedVersion === null) {
40
+ try { cachedVersion = String(require('../../../package.json').version); } catch { cachedVersion = 'unknown'; }
41
+ }
42
+ return cachedVersion;
43
+ }
44
+
45
+ function harnessName(env: any): string {
46
+ const raw = env.DOFLOW_AGENT;
47
+ return typeof raw === 'string' && /^[A-Za-z0-9._-]{1,40}$/.test(raw) ? raw : 'none';
48
+ }
49
+
50
+ /** The absolute, non-root homes to compare a working directory with: HOME as given, its real path, and `os.homedir()`. */
51
+ function homeCandidates(env: any): string[] {
52
+ const out: string[] = [];
53
+ const add = (value: any) => {
54
+ if (typeof value !== 'string' || !path.isAbsolute(value)) return;
55
+ const home = value.replace(/\/+$/, '');
56
+ if (home.length > 1 && !out.includes(home)) out.push(home);
57
+ };
58
+ add(env.HOME);
59
+ if (typeof env.HOME === 'string' && path.isAbsolute(env.HOME)) { try { add(fs.realpathSync(env.HOME)); } catch { /* HOME does not exist */ } }
60
+ try { add(os.homedir()); add(fs.realpathSync(os.homedir())); } catch { /* no home known to the system */ }
61
+ return out;
62
+ }
63
+
64
+ /**
65
+ * The working directory as stored: the home prefix written as `~` whichever way home is spelled (a
66
+ * symlinked HOME makes `process.cwd()` the physical path), masked like any other free text. A
67
+ * directory under no known home, when `HOME` itself is not usable, is stored as its folder name only,
68
+ * so an absolute path with a user name in it never reaches the file.
69
+ */
70
+ function projectName(env: any): string {
71
+ let cwd: string;
72
+ try { cwd = process.cwd(); } catch { return ''; }
73
+ const home = homeCandidates(env).find((h) => cwd === h || cwd.startsWith(`${h}/`));
74
+ if (home) return maskLine(`~${cwd.slice(home.length)}`, { home: null }).text.slice(0, 200);
75
+ const homeGiven = typeof env.HOME === 'string' && path.isAbsolute(env.HOME);
76
+ if (!homeGiven) return maskLine(path.basename(cwd), { home: null }).text.slice(0, 200);
77
+ return maskLine(cwd, { home: env.HOME }).text.slice(0, 200);
78
+ }
79
+
80
+ function commandName(command: any): string {
81
+ return typeof command === 'string' && COMMAND_NAME.test(command) ? command : 'unknown';
82
+ }
83
+
84
+ /**
85
+ * The first stack frame inside DoFlow's own files as `<relative path>:<function>`, no line number
86
+ * (IC-011). Null when no frame is inside the package, which is also what keeps a frame from a
87
+ * dependency or a user's own file out of the record.
88
+ * @param {*} error
89
+ * @returns {string|null}
90
+ */
91
+ function doflowFrame(error: any): string | null {
92
+ let stack: string;
93
+ try { stack = typeof error.stack === 'string' ? error.stack : ''; } catch { return null; }
94
+ const root = REPO_ROOT + path.sep;
95
+ for (const line of stack.split('\n')) {
96
+ const match = /^\s*at (?:async )?(?:(.+?) \()?(.+?):\d+:\d+\)?$/.exec(line);
97
+ if (!match) continue;
98
+ const file = match[2].replace(/^file:\/\//, '');
99
+ if (!file.startsWith(root) || file.includes(`${path.sep}node_modules${path.sep}`)) continue;
100
+ const fn = (match[1] || '').replace(/^new /, '').split('.').pop()!.replace(/\s.*$/, '') || '<anonymous>';
101
+ return `${path.relative(REPO_ROOT, file).split(path.sep).join('/')}:${fn}`;
102
+ }
103
+ return null;
104
+ }
105
+
106
+ /** One line at or under MAX_LINE_BYTES including the newline: shorten `message`, then `project`. */
107
+ function buildLine(record: any): string | null {
108
+ const bytes = (r: any) => Buffer.byteLength(JSON.stringify(r), 'utf8') + 1;
109
+ const out = { ...record };
110
+ for (const field of ['message', 'project']) {
111
+ while (bytes(out) > MAX_LINE_BYTES && out[field].length > 0) {
112
+ out[field] = out[field].slice(0, Math.max(0, out[field].length - Math.max(8, bytes(out) - MAX_LINE_BYTES)));
113
+ }
114
+ }
115
+ return bytes(out) > MAX_LINE_BYTES ? null : `${JSON.stringify(out)}\n`;
116
+ }
117
+
118
+ const stamp = (date: Date): string => date.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
119
+
120
+ /** Rotated files with their size and modification time, oldest last-modified first (names alone tie inside one second). */
121
+ function rotatedFiles(home: string): Array<{ name: string; size: number; mtime: number }> {
122
+ return fs.readdirSync(home).filter((name: string) => ROTATED.test(name)).map((name: string) => {
123
+ let size = 0;
124
+ let mtime = 0;
125
+ try { const st = fs.statSync(path.join(home, name)); size = st.size; mtime = st.mtimeMs; } catch { /* gone */ }
126
+ return { name, size, mtime };
127
+ }).sort((a: any, b: any) => a.mtime - b.mtime || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
128
+ }
129
+
130
+ /**
131
+ * Removes rotated files beyond the newest four full ones, and partial ones (see rotateIfDue) once
132
+ * they are an hour old. A partial file never counts toward the four, so it cannot push a real one out.
133
+ */
134
+ function prune(home: string, now = Date.now()): void {
135
+ const files = rotatedFiles(home);
136
+ const full = files.filter((f) => f.size >= ROTATE_AT_BYTES);
137
+ const doomed = [
138
+ ...full.slice(0, Math.max(0, full.length - KEEP_ROTATED)),
139
+ ...files.filter((f) => f.size < ROTATE_AT_BYTES && now - f.mtime > PARTIAL_KEEP_MS),
140
+ ];
141
+ for (const f of doomed) {
142
+ try { fs.unlinkSync(path.join(home, f.name)); } catch { /* ignored */ }
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Rotation (IC-015): at 1 MiB the live file is renamed to `events-<UTC time>-<pid>.jsonl`, and the
148
+ * rotated files beyond the newest four are deleted. A failed rename or delete is ignored, so a
149
+ * process that lost the race simply appends to whatever is there.
150
+ *
151
+ * A process that measured the old live file may rename the fresh one another writer just created
152
+ * (it holds less than 1 MiB). That file is linked back as the live file when nobody has recreated it
153
+ * (same inode, so a writer still holding it loses nothing); when the live file exists again it stays
154
+ * as a small rotated file, which readers read like any other and which is never counted toward the
155
+ * four kept, so a real rotated file is never pushed out by one. Nothing is ever copied or deleted
156
+ * here except by prune, so no line is lost to the race.
157
+ * @param {string} home the failure home
158
+ * @returns {boolean} whether this call rotated a full file
159
+ */
160
+ function rotateIfDue(home: string): boolean {
161
+ const live = eventsPath(home);
162
+ try {
163
+ if (fs.statSync(live).size < ROTATE_AT_BYTES) return false;
164
+ } catch { return false; }
165
+ // The name carries the second and the pid, so a process that rotates twice inside one second would
166
+ // rename onto its own earlier file and destroy it: take the next free second instead.
167
+ let rotated = path.join(home, `events-${stamp(new Date())}-${process.pid}.jsonl`);
168
+ for (let i = 1; i < 120 && fs.existsSync(rotated); i++) {
169
+ rotated = path.join(home, `events-${stamp(new Date(Date.now() + i * 1000))}-${process.pid}.jsonl`);
170
+ }
171
+ try {
172
+ fs.renameSync(live, rotated);
173
+ } catch { return false; }
174
+ let size = 0;
175
+ try { size = fs.statSync(rotated).size; } catch { return false; }
176
+ if (size < ROTATE_AT_BYTES) {
177
+ try { fs.linkSync(rotated, live); fs.unlinkSync(rotated); } catch { /* the live file exists again: the small file stays */ }
178
+ return false;
179
+ }
180
+ try { prune(home); } catch { /* ignored */ }
181
+ return true;
182
+ }
183
+
184
+ /**
185
+ * Appends one line to `file` only when it is absent or a regular file. A FIFO, a device or a symlink
186
+ * there is skipped silently: opening a FIFO for writing blocks until something reads it, which would
187
+ * hang the command capture is meant to observe. `O_NONBLOCK` fails such an open at once and the
188
+ * descriptor is checked before the write, so a file swapped in after any earlier check is still safe.
189
+ * @returns {boolean} whether the line was written
190
+ */
191
+ function appendRegular(file: string, line: string): boolean {
192
+ const c = fs.constants;
193
+ let fd: number | undefined;
194
+ try {
195
+ fd = fs.openSync(file, c.O_WRONLY | c.O_APPEND | c.O_CREAT | (c.O_NOFOLLOW || 0) | (c.O_NONBLOCK || 0), 0o600);
196
+ if (!fs.fstatSync(fd).isFile()) return false;
197
+ fs.writeSync(fd, line);
198
+ return true;
199
+ } catch { return false; } finally {
200
+ if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* nothing more to do */ } }
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Appends one failure line. Every failure to do so is swallowed.
206
+ * @param {{source: string, command: string, kind: string, message?: string, frame?: string|null, exit?: number|null}} fields
207
+ * `message` must already be normalised (see captureError).
208
+ * @param {Object} [env] defaults to process.env
209
+ * @returns {boolean} whether a line was written
210
+ */
211
+ function captureFailure(fields: any, env = process.env): boolean {
212
+ try {
213
+ const home = failureHome(env);
214
+ if (!home || captureIsOff(home, env)) return false;
215
+ const line = buildLine({
216
+ v: 1,
217
+ at: new Date().toISOString(),
218
+ source: fields.source,
219
+ command: commandName(fields.command),
220
+ harness: harnessName(env),
221
+ version: packageVersion(),
222
+ project: projectName(env),
223
+ kind: String(fields.kind).slice(0, 80),
224
+ message: typeof fields.message === 'string' ? fields.message : '',
225
+ frame: typeof fields.frame === 'string' ? fields.frame : null,
226
+ exit: Number.isInteger(fields.exit) ? fields.exit : null,
227
+ });
228
+ if (line === null) return false;
229
+ fs.mkdirSync(home, { recursive: true, mode: 0o700 });
230
+ rotateIfDue(home);
231
+ return appendRegular(eventsPath(home), line);
232
+ } catch {
233
+ return false;
234
+ }
235
+ }
236
+
237
+ /** The error's message cut to 4096 characters before any masking, so no input makes the rules do unbounded work. */
238
+ function rawMessage(error: any): string {
239
+ try {
240
+ const message = error.message;
241
+ return typeof message === 'string' ? message.slice(0, MAX_RAW_MESSAGE) : '';
242
+ } catch { return ''; }
243
+ }
244
+
245
+ /**
246
+ * Records a caught error when it is a programming error (IC-010). The caller keeps doing whatever it
247
+ * did before: this returns a boolean and nothing else changes.
248
+ * @param {*} error
249
+ * @param {{command?: string, exit?: number|null, uncaught?: boolean}} [context]
250
+ * `uncaught` marks an error that escaped every handler (kind `uncaught:<kind>`).
251
+ * @param {Object} [env]
252
+ * @returns {boolean} whether a line was written
253
+ */
254
+ function captureError(error: any, context: any = {}, env = process.env): boolean {
255
+ try {
256
+ if (!isProgrammingError(error) || recorded.has(error)) return false;
257
+ recorded.add(error);
258
+ const kind = errorKind(error);
259
+ return captureFailure({
260
+ source: 'cli',
261
+ command: context.command,
262
+ kind: context.uncaught ? `uncaught:${kind}` : kind,
263
+ message: normaliseMessage(rawMessage(error), { home: env.HOME }),
264
+ frame: doflowFrame(error),
265
+ exit: context.exit,
266
+ }, env);
267
+ } catch {
268
+ return false;
269
+ }
270
+ }
271
+
272
+ module.exports = { appendRegular, captureFailure, captureError, rotateIfDue, doflowFrame, MAX_LINE_BYTES, ROTATE_AT_BYTES, KEEP_ROTATED };
@@ -0,0 +1,68 @@
1
+ 'use strict';
2
+
3
+ // TypeScript migration (agent-docs/BRIEF_GOAL_DOFLOW.md): run directly by Node native type-stripping.
4
+
5
+ /**
6
+ * The programming-error classifier (IC-010, DEC-013, DEC-031). Decides whether a caught error is a
7
+ * bug in DoFlow worth recording, as opposed to a finding, a refusal, a bad flag or a fault in the
8
+ * user's environment. Pure and total: it never throws, whatever it is handed.
9
+ *
10
+ * The same function is copied into core/harnesses/shared/hooks/stream-hook-runner.js, because hook
11
+ * files are installed without the runtime library; a test runs both copies over one fixture set.
12
+ */
13
+
14
+ const CLASS_NAMES = new Set(['TypeError', 'RangeError', 'ReferenceError']);
15
+
16
+ /** Reads one property without ever throwing: a throwing getter reads as undefined. */
17
+ function read(error: any, key: string): any {
18
+ try { return error[key]; } catch { return undefined; }
19
+ }
20
+
21
+ /** `error instanceof Class`, never throwing. */
22
+ function isA(error: any, Class: any): boolean {
23
+ try { return error instanceof Class; } catch { return false; }
24
+ }
25
+
26
+ /**
27
+ * The `instanceof` checks come first and every property read is guarded, so a getter that throws
28
+ * never drops a real TypeError.
29
+ * @param {*} error
30
+ * @returns {boolean}
31
+ */
32
+ function isProgrammingError(error: any): boolean {
33
+ try {
34
+ if (error === null || typeof error !== 'object') return false;
35
+ const code = read(error, 'code');
36
+ const name = read(error, 'name');
37
+ // A broken pipe on stdout or stderr is the reader going away, not a bug (DEC-031).
38
+ if (code === 'EPIPE') return false;
39
+ if (isA(error, SyntaxError) || name === 'SyntaxError') return false;
40
+ if (isA(error, TypeError) || isA(error, RangeError) || isA(error, ReferenceError)) return true;
41
+ if (CLASS_NAMES.has(name)) return true;
42
+ if (name === 'AssertionError' || code === 'ERR_ASSERTION') return true;
43
+ if (code === 'MODULE_NOT_FOUND') return true;
44
+ return typeof code === 'string' && (typeof read(error, 'syscall') === 'string' || typeof read(error, 'errno') === 'number');
45
+ } catch {
46
+ return false;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * The `kind` a captured error is recorded under: the class name for the programming-error classes,
52
+ * else the error's code (a system error or MODULE_NOT_FOUND), else its name.
53
+ * @param {*} error
54
+ * @returns {string}
55
+ */
56
+ function errorKind(error: any): string {
57
+ for (const Class of [TypeError, RangeError, ReferenceError]) {
58
+ if (isA(error, Class)) return Class.name;
59
+ }
60
+ const name = read(error, 'name');
61
+ const code = read(error, 'code');
62
+ if (CLASS_NAMES.has(name) || name === 'AssertionError') return name;
63
+ if (typeof code === 'string') return code;
64
+ if (typeof name === 'string' && name) return name;
65
+ return 'Error';
66
+ }
67
+
68
+ module.exports = { isProgrammingError, errorKind };