@khoavu882/doflow 2.0.0-beta.4 → 2.0.0

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 (497) hide show
  1. package/README.md +12 -11
  2. package/bin/doflow.js +21 -11
  3. package/core/.antigravity-plugin/plugin.json +1 -3
  4. package/core/.claude-plugin/plugin.json +1 -1
  5. package/core/.codex-plugin/plugin.json +1 -1
  6. package/core/.plugin/plugin.json +1 -1
  7. package/core/harnesses/claude/hooks/lib.sh +43 -24
  8. package/core/harnesses/claude/hooks/mcp-tool-guard.sh +7 -17
  9. package/core/harnesses/claude/hooks/permission-audit.sh +3 -0
  10. package/core/harnesses/claude/hooks/post-compact.sh +35 -32
  11. package/core/harnesses/claude/hooks/post-edit-lint.sh +2 -0
  12. package/core/harnesses/claude/hooks/post-shell-observe.sh +48 -0
  13. package/core/harnesses/claude/hooks/post-tool-failure.sh +3 -0
  14. package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
  15. package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
  16. package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
  17. package/core/harnesses/claude/hooks/session-end.sh +23 -10
  18. package/core/harnesses/claude/hooks/session-start.sh +5 -3
  19. package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
  20. package/core/harnesses/claude/hooks/stop-check.sh +5 -3
  21. package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
  22. package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
  23. package/core/harnesses/claude/settings/settings.json +18 -0
  24. package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
  25. package/core/harnesses/codex/agents/core-implementer.toml +1 -0
  26. package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
  27. package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
  28. package/core/harnesses/codex/agents/research-writer.toml +1 -1
  29. package/core/harnesses/codex/agents/reviewer.toml +10 -0
  30. package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
  31. package/core/harnesses/codex/agents/system-architect.toml +1 -1
  32. package/core/harnesses/codex/hooks/lib.sh +44 -25
  33. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
  34. package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
  35. package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
  36. package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
  37. package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
  38. package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
  39. package/core/harnesses/codex/hooks/session-start.sh +5 -3
  40. package/core/harnesses/codex/hooks/stop-check.sh +6 -5
  41. package/core/harnesses/codex/hooks/subagent-audit.sh +18 -6
  42. package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
  43. package/core/harnesses/copilot/hooks/doflow-lib.sh +93 -0
  44. package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
  45. package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +9 -0
  46. package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
  47. package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
  48. package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
  49. package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
  50. package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
  51. package/core/harnesses/copilot/hooks/doflow.json +57 -0
  52. package/core/harnesses/kiro/agents/doflow.json +1 -1
  53. package/core/harnesses/kiro/hooks/lib.sh +26 -6
  54. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +21 -5
  55. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +21 -5
  56. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +21 -5
  57. package/core/harnesses/kiro/hooks/session-start.sh +12 -5
  58. package/core/harnesses/kiro/hooks/stop-check.sh +22 -30
  59. package/core/harnesses/pi/extensions/doflow-guard.js +167 -0
  60. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -12
  61. package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
  62. package/core/harnesses/shared/hooks/bridge/bridge.sh +277 -0
  63. package/core/harnesses/shared/hooks/bridge/envelope.jq +129 -0
  64. package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
  65. package/core/harnesses/shared/hooks/bridge/respond.sh +70 -0
  66. package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +28 -14
  67. package/core/harnesses/shared/hooks/policies/capture-failure.sh +51 -16
  68. package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
  69. package/core/harnesses/shared/hooks/policies/contract.sh +78 -0
  70. package/core/harnesses/shared/hooks/policies/deny-json.sh +35 -9
  71. package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
  72. package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
  73. package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
  74. package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
  75. package/core/harnesses/shared/hooks/policies/lib.sh +65 -40
  76. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +48 -35
  77. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +284 -33
  78. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +180 -110
  79. package/core/harnesses/shared/hooks/policies/session-context.sh +66 -7
  80. package/core/harnesses/shared/hooks/policies/stop-check.sh +61 -23
  81. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +39 -6
  82. package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
  83. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +97 -159
  84. package/core/harnesses/shared/hooks/stream-hook-runner.js +114 -187
  85. package/core/harnesses/shared/locator/doflow-run +2 -3
  86. package/core/registry/advice.json +3 -3
  87. package/core/registry/assets.json +72 -80
  88. package/core/registry/capabilities.json +12 -0
  89. package/core/registry/contracts.json +60 -33
  90. package/core/registry/contribution-points.json +32 -0
  91. package/core/registry/gates.json +32 -0
  92. package/core/registry/guidance-caps.json +40 -0
  93. package/core/registry/harnesses.json +1542 -277
  94. package/core/registry/lifecycle.json +2837 -37
  95. package/core/registry/mcp.json +5 -1
  96. package/core/registry/models.json +7 -1
  97. package/core/registry/package-templates/README.md +19 -0
  98. package/core/registry/package-templates/agent/files/agent.md +9 -0
  99. package/core/registry/package-templates/agent/template.json +4 -0
  100. package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
  101. package/core/registry/package-templates/capability-provider/template.json +11 -0
  102. package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
  103. package/core/registry/package-templates/debate-preset/template.json +4 -0
  104. package/core/registry/package-templates/review-preset/files/preset.json +30 -0
  105. package/core/registry/package-templates/review-preset/template.json +4 -0
  106. package/core/registry/package-templates/skill/files/SKILL.md +8 -0
  107. package/core/registry/package-templates/skill/template.json +4 -0
  108. package/core/registry/package-templates/worker/files/worker.json +92 -0
  109. package/core/registry/package-templates/worker/template.json +11 -0
  110. package/core/registry/packages/doflow/doflow-package.json +15 -0
  111. package/core/registry/packages/doflow/presets/debate-3.json +26 -0
  112. package/core/registry/protocols.json +61 -0
  113. package/core/registry/routes.json +17 -0
  114. package/core/registry/rules.json +431 -0
  115. package/core/registry/settings.json +276 -0
  116. package/core/registry/storage.json +701 -30
  117. package/core/registry/telemetry.json +236 -0
  118. package/core/registry/transcripts.json +142 -0
  119. package/core/registry/verification.json +76 -1
  120. package/core/registry/workflows.json +55 -0
  121. package/core/shared/agent-specs/core-implementer.md +3 -0
  122. package/core/shared/agent-specs/delivery-lead.md +68 -0
  123. package/core/shared/agent-specs/quality-guardian.md +5 -1
  124. package/core/shared/agent-specs/research-writer.md +10 -3
  125. package/core/shared/agent-specs/reviewer.md +53 -0
  126. package/core/shared/agent-specs/spec-analyst.md +5 -6
  127. package/core/shared/agent-specs/system-architect.md +5 -1
  128. package/core/shared/guidance/DOFLOW_CORE.md +1 -5
  129. package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
  130. package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
  131. package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
  132. package/core/shared/guidance/pointers/codex.md +1 -4
  133. package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
  134. package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
  135. package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
  136. package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
  137. package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
  138. package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
  139. package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
  140. package/core/shared/guidance/references/ORCHESTRATION.md +1 -2
  141. package/core/shared/guidance/rules/RULES.md +45 -0
  142. package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
  143. package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
  144. package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
  145. package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
  146. package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
  147. package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
  148. package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
  149. package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
  150. package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
  151. package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
  152. package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
  153. package/core/shared/scripts/doflow/bash/validate-artifacts.sh +48 -4
  154. package/core/shared/scripts/doflow/bin/doflow-run +154 -35
  155. package/core/shared/skills/do/SKILL.md +12 -10
  156. package/core/shared/skills/do/references/advice_calls.md +26 -0
  157. package/core/shared/skills/do/references/pm_routing.md +1 -2
  158. package/core/shared/skills/do/references/program_mode.md +31 -0
  159. package/core/shared/skills/do-brainstorm/SKILL.md +20 -74
  160. package/core/shared/skills/do-brainstorm/references/handoff_branches.md +35 -0
  161. package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
  162. package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
  163. package/core/shared/skills/do-code-review/SKILL.md +41 -144
  164. package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
  165. package/core/shared/skills/do-code-review/references/tools.md +90 -0
  166. package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
  167. package/core/shared/skills/do-design/SKILL.md +24 -23
  168. package/core/shared/skills/do-diagnose/SKILL.md +14 -4
  169. package/core/shared/skills/do-document/SKILL.md +11 -1
  170. package/core/shared/skills/do-document/references/deep_research.md +1 -2
  171. package/core/shared/skills/do-document/references/embedded-research.md +18 -6
  172. package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
  173. package/core/shared/skills/do-execute-plan/SKILL.md +17 -59
  174. package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
  175. package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
  176. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +28 -1
  177. package/core/shared/skills/do-execute-plan/references/run_position.md +13 -1
  178. package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
  179. package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
  180. package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
  181. package/core/shared/skills/do-flow/SKILL.md +8 -9
  182. package/core/shared/skills/do-flow/references/task_classes.md +2 -2
  183. package/core/shared/skills/do-git/SKILL.md +4 -0
  184. package/core/shared/skills/do-implement/SKILL.md +7 -9
  185. package/core/shared/skills/do-plan/SKILL.md +29 -19
  186. package/core/shared/skills/do-test/SKILL.md +7 -4
  187. package/core/shared/templates/doflow/constitution-template.md +9 -3
  188. package/core/shared/templates/doflow/data-model-template.md +1 -1
  189. package/core/shared/templates/doflow/design-template.md +113 -16
  190. package/core/shared/templates/doflow/plan-template.md +11 -10
  191. package/core/shared/templates/doflow/question-log-template.md +14 -2
  192. package/core/shared/templates/doflow/requirement-template.md +1 -1
  193. package/core/worker-evidence/antigravity/checks.json +14 -0
  194. package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
  195. package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
  196. package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
  197. package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
  198. package/core/worker-evidence/codex/checks.json +38 -0
  199. package/package.json +10 -6
  200. package/src/adapters/antigravity/index.js +226 -113
  201. package/src/adapters/claude/index.js +39 -5
  202. package/src/adapters/codex/agents.js +25 -15
  203. package/src/adapters/codex/hooks.js +40 -5
  204. package/src/adapters/codex/index.js +100 -22
  205. package/src/adapters/codex/native-projection.js +6 -2
  206. package/src/adapters/copilot/index.js +58 -23
  207. package/src/adapters/copy-tree.js +294 -48
  208. package/src/adapters/guidance-manifest.js +71 -0
  209. package/src/adapters/guidance-pointer.js +19 -0
  210. package/src/adapters/hook-commands.js +6 -10
  211. package/src/adapters/index.js +1 -0
  212. package/src/adapters/kiro/index.js +38 -13
  213. package/src/adapters/opencode/index.js +8 -8
  214. package/src/adapters/pi/hooks.js +120 -0
  215. package/src/adapters/pi/index.js +48 -16
  216. package/src/adapters/resolve-source.js +26 -2
  217. package/src/cli/commands/install.js +3 -2
  218. package/src/cli/commands/reconcile.js +53 -9
  219. package/src/cli/commands/remove.js +2 -2
  220. package/src/cli/commands/rollback.js +6 -3
  221. package/src/cli/commands/self-update.js +18 -9
  222. package/src/cli/commands/status.js +21 -2
  223. package/src/cli/commands/ui.js +99 -0
  224. package/src/cli/commands/update.js +3 -2
  225. package/src/cli/index.js +338 -33
  226. package/src/cli/runtime-commands.js +243 -52
  227. package/src/cli/shared.js +0 -2
  228. package/src/helper/git.js +1 -1
  229. package/src/helper/guardrails.js +1 -1
  230. package/src/helper/marker-merge.js +31 -13
  231. package/src/helper/package-ids.js +37 -0
  232. package/src/helper/semver.js +94 -0
  233. package/src/install/backup.js +105 -15
  234. package/src/install/context.js +1 -1
  235. package/src/install/targets.js +31 -16
  236. package/src/lifecycle/coverage.js +74 -0
  237. package/src/lifecycle/hook-step.js +61 -0
  238. package/src/lifecycle/hook-tier.js +160 -0
  239. package/src/lifecycle/index.js +434 -86
  240. package/src/lifecycle/retired-harnesses.js +318 -0
  241. package/src/lifecycle/view.js +35 -15
  242. package/src/registry/index.js +686 -32
  243. package/src/registry/worker-ids.js +45 -0
  244. package/src/release/build-package.js +0 -3
  245. package/src/release/docs-blocks.js +492 -0
  246. package/src/release/preflight.js +425 -0
  247. package/src/release/rehearse.js +401 -0
  248. package/src/release/release-workflow.js +733 -0
  249. package/src/release/sync-plugin-versions.js +43 -7
  250. package/src/runtime/advice/bench-gate.js +25 -0
  251. package/src/runtime/advice/cli.js +59 -16
  252. package/src/runtime/advice/client.js +32 -9
  253. package/src/runtime/advice/config.js +98 -133
  254. package/src/runtime/advice/doctor.js +1 -1
  255. package/src/runtime/advice/elevation.js +97 -0
  256. package/src/runtime/advice/endpoint-guard.js +9 -3
  257. package/src/runtime/advice/mode.js +37 -0
  258. package/src/runtime/advice/record.js +18 -5
  259. package/src/runtime/advice/registry.js +4 -1
  260. package/src/runtime/advice/shadow-config.js +229 -0
  261. package/src/runtime/advice/shadow-dispatcher.js +276 -0
  262. package/src/runtime/brief-rules.js +81 -0
  263. package/src/runtime/brief-tools.js +111 -34
  264. package/src/runtime/candidate.js +514 -0
  265. package/src/runtime/capabilities/executor.js +450 -0
  266. package/src/runtime/capabilities/loopback.js +41 -0
  267. package/src/runtime/capabilities/sanitizer.js +88 -0
  268. package/src/runtime/capability-router.js +135 -3
  269. package/src/runtime/checkouts.js +73 -11
  270. package/src/runtime/claims.js +27 -12
  271. package/src/runtime/cli-result.js +11 -4
  272. package/src/runtime/cli.js +221 -277
  273. package/src/runtime/config-home.js +42 -0
  274. package/src/runtime/context/arbitrate.js +767 -0
  275. package/src/runtime/context/budget.js +203 -0
  276. package/src/runtime/context/compose.js +1367 -0
  277. package/src/runtime/context/delivery-store.js +100 -0
  278. package/src/runtime/context/emission-rules.js +118 -0
  279. package/src/runtime/context/identity.js +227 -0
  280. package/src/runtime/context/mandatory.js +594 -0
  281. package/src/runtime/context/pointer.js +142 -0
  282. package/src/runtime/context/primary-command.js +117 -0
  283. package/src/runtime/context/primary.js +344 -0
  284. package/src/runtime/context/summary-store.js +655 -0
  285. package/src/runtime/context-pack.js +1106 -245
  286. package/src/runtime/decision-register.js +13 -4
  287. package/src/runtime/dispatch/admission.js +392 -0
  288. package/src/runtime/dispatch/attempt-end.js +274 -0
  289. package/src/runtime/dispatch/command.js +628 -0
  290. package/src/runtime/dispatch/controls.js +904 -0
  291. package/src/runtime/dispatch/envelope.js +276 -0
  292. package/src/runtime/dispatch/grants.js +310 -0
  293. package/src/runtime/dispatch/integrate.js +617 -0
  294. package/src/runtime/dispatch/loop.js +1835 -0
  295. package/src/runtime/dispatch/store.js +352 -0
  296. package/src/runtime/dispatch/verify-run.js +201 -0
  297. package/src/runtime/evidence-fields.js +39 -0
  298. package/src/runtime/evidence-ledger.js +41 -7
  299. package/src/runtime/failure/capture.js +3 -1
  300. package/src/runtime/failure/cli.js +25 -9
  301. package/src/runtime/failure/home.js +57 -30
  302. package/src/runtime/failure/store.js +7 -2
  303. package/src/runtime/feature-pointer.js +174 -0
  304. package/src/runtime/feature-resolve.js +13 -2
  305. package/src/runtime/fleet/command.js +77 -0
  306. package/src/runtime/fleet/projects.js +178 -0
  307. package/src/runtime/fleet/tree.js +383 -0
  308. package/src/runtime/freshness.js +77 -10
  309. package/src/runtime/git-memo.js +46 -0
  310. package/src/runtime/guidance/ablation.js +427 -0
  311. package/src/runtime/health.js +565 -17
  312. package/src/runtime/history-compactor.js +13 -3
  313. package/src/runtime/implementation-gate.js +277 -22
  314. package/src/runtime/inventory/coverage.js +1 -1
  315. package/src/runtime/inventory/index.js +1 -1
  316. package/src/runtime/inventory/precedence.js +2 -2
  317. package/src/runtime/inventory/siblings.js +6 -4
  318. package/src/runtime/knowledge/code-corpus.js +277 -0
  319. package/src/runtime/knowledge/code-lexical.js +130 -0
  320. package/src/runtime/knowledge/code-retrieval.js +307 -0
  321. package/src/runtime/knowledge/code-validation.js +106 -0
  322. package/src/runtime/knowledge/gp3-gate.js +74 -0
  323. package/src/runtime/knowledge/graph-adapter.js +288 -0
  324. package/src/runtime/knowledge/graph-fallback.js +394 -0
  325. package/src/runtime/knowledge/hybrid.js +136 -8
  326. package/src/runtime/knowledge/retrieval.js +34 -1
  327. package/src/runtime/knowledge/rtk-compress.js +77 -0
  328. package/src/runtime/knowledge/semble-bridge.py +106 -0
  329. package/src/runtime/knowledge/semble-lifecycle.js +114 -0
  330. package/src/runtime/knowledge/semble-provider.js +365 -0
  331. package/src/runtime/lifecycle/cli.js +16 -6
  332. package/src/runtime/lifecycle/event-store.js +8 -6
  333. package/src/runtime/lifecycle/followup.js +6 -1
  334. package/src/runtime/lifecycle/overview.js +24 -1
  335. package/src/runtime/lifecycle/report-store.js +8 -10
  336. package/src/runtime/lifecycle/retention.js +95 -31
  337. package/src/runtime/lifecycle/root.js +9 -1
  338. package/src/runtime/lifecycle/status.js +34 -17
  339. package/src/runtime/lifecycle/store-upkeep.js +56 -7
  340. package/src/runtime/mask.js +134 -7
  341. package/src/runtime/mcp-consultations.js +40 -6
  342. package/src/runtime/model-router.js +79 -7
  343. package/src/runtime/models/catalogue.js +191 -0
  344. package/src/runtime/models/command.js +153 -0
  345. package/src/runtime/off-words.js +12 -0
  346. package/src/runtime/outcome.js +3 -3
  347. package/src/runtime/packages/command.js +1009 -0
  348. package/src/runtime/packages/contributions.js +116 -0
  349. package/src/runtime/packages/digest.js +132 -0
  350. package/src/runtime/packages/fetch.js +182 -0
  351. package/src/runtime/packages/grants.js +192 -0
  352. package/src/runtime/packages/index-file.js +134 -0
  353. package/src/runtime/packages/instruction.js +90 -0
  354. package/src/runtime/packages/manifest.js +331 -0
  355. package/src/runtime/packages/points.js +477 -0
  356. package/src/runtime/packages/programs.js +216 -0
  357. package/src/runtime/packages/scaffold.js +277 -0
  358. package/src/runtime/packages/source.js +90 -0
  359. package/src/runtime/packages/state.js +164 -0
  360. package/src/runtime/packages/store.js +449 -0
  361. package/src/runtime/packages/tar.js +106 -0
  362. package/src/runtime/packages/view.js +406 -0
  363. package/src/runtime/plan-check.js +59 -0
  364. package/src/runtime/plan-content.js +324 -0
  365. package/src/runtime/policy-init.js +118 -0
  366. package/src/runtime/profiles/doctor.js +96 -0
  367. package/src/runtime/profiles/resolve.js +244 -0
  368. package/src/runtime/profiles/rollback.js +254 -0
  369. package/src/runtime/profiles/sampler.js +41 -0
  370. package/src/runtime/readiness-record.js +16 -11
  371. package/src/runtime/readiness.js +127 -16
  372. package/src/runtime/research-request.js +50 -25
  373. package/src/runtime/retrieval-plan.js +402 -73
  374. package/src/runtime/retrieval-record.js +293 -0
  375. package/src/runtime/review/command.js +246 -0
  376. package/src/runtime/review/dominance.js +68 -0
  377. package/src/runtime/review/findings.js +434 -0
  378. package/src/runtime/review/identity.js +255 -0
  379. package/src/runtime/review/protocol-command.js +109 -0
  380. package/src/runtime/review/protocols.js +405 -0
  381. package/src/runtime/review/resolve-protocol.js +302 -0
  382. package/src/runtime/review/start.js +530 -0
  383. package/src/runtime/roadmap/facts.js +479 -0
  384. package/src/runtime/roadmap/gates.js +270 -0
  385. package/src/runtime/roadmap/loop.js +1636 -0
  386. package/src/runtime/roadmap/merge.js +381 -0
  387. package/src/runtime/roadmap/rows.js +1113 -0
  388. package/src/runtime/roadmap/seed.js +951 -0
  389. package/src/runtime/rules-report.js +336 -0
  390. package/src/runtime/scaffold/artifacts.js +63 -9
  391. package/src/runtime/scaffold/generate.js +21 -9
  392. package/src/runtime/settings/autopilot.js +106 -0
  393. package/src/runtime/settings/cli.js +280 -0
  394. package/src/runtime/settings/config-write.js +314 -0
  395. package/src/runtime/settings/files.js +271 -0
  396. package/src/runtime/settings/legacy.js +154 -0
  397. package/src/runtime/settings/lexical.js +56 -0
  398. package/src/runtime/settings/prepared-run.js +269 -0
  399. package/src/runtime/settings/project-layers.js +118 -0
  400. package/src/runtime/settings/refs.js +288 -0
  401. package/src/runtime/settings/registry.js +553 -0
  402. package/src/runtime/settings/resolve.js +713 -0
  403. package/src/runtime/settings/shared.js +37 -0
  404. package/src/runtime/settings/worker-billing.js +107 -0
  405. package/src/runtime/settings/write.js +269 -0
  406. package/src/runtime/slot-command.js +78 -0
  407. package/src/runtime/slots.js +671 -0
  408. package/src/runtime/source-version.js +290 -0
  409. package/src/runtime/state-root.js +217 -0
  410. package/src/runtime/storage/cli.js +12 -5
  411. package/src/runtime/storage/engine.js +9 -2
  412. package/src/runtime/storage/registry.js +14 -1
  413. package/src/runtime/storage/roots.js +66 -16
  414. package/src/runtime/storage/rules.js +82 -10
  415. package/src/runtime/storage/upkeep.js +10 -2
  416. package/src/runtime/storage/work-status.js +245 -26
  417. package/src/runtime/task-attempts.js +589 -69
  418. package/src/runtime/task-classifier.js +47 -11
  419. package/src/runtime/task-graph.js +373 -0
  420. package/src/runtime/task-run.js +778 -0
  421. package/src/runtime/task-scope.js +202 -56
  422. package/src/runtime/task-sources.js +147 -0
  423. package/src/runtime/task-state.js +440 -65
  424. package/src/runtime/team/admission.js +301 -0
  425. package/src/runtime/team/envelope.js +324 -0
  426. package/src/runtime/team/review-guard.js +156 -0
  427. package/src/runtime/trace/analyze.js +427 -0
  428. package/src/runtime/trace/baseline.js +770 -0
  429. package/src/runtime/trace/ledger.js +63 -140
  430. package/src/runtime/trace/otlp-export.js +568 -0
  431. package/src/runtime/trace/otlp-map.js +481 -0
  432. package/src/runtime/trace/spans.js +745 -0
  433. package/src/runtime/trace/telemetry.js +225 -0
  434. package/src/runtime/trace/views.js +1 -0
  435. package/src/runtime/transcripts/command.js +98 -0
  436. package/src/runtime/transcripts/cursor.js +326 -0
  437. package/src/runtime/transcripts/read.js +126 -0
  438. package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
  439. package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
  440. package/src/runtime/transcripts/readers/copilot-events.js +90 -0
  441. package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
  442. package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
  443. package/src/runtime/transcripts/registry.js +161 -0
  444. package/src/runtime/transcripts/stats.js +161 -0
  445. package/src/runtime/verification/check-key.js +628 -0
  446. package/src/runtime/verification/contract-runner.js +546 -11
  447. package/src/runtime/verification/engine.js +499 -9
  448. package/src/runtime/verification/impact.js +617 -0
  449. package/src/runtime/verification/inflight.js +205 -0
  450. package/src/runtime/verification/registry.js +176 -0
  451. package/src/runtime/verification/reuse.js +184 -0
  452. package/src/runtime/verification/telemetry-emit.js +254 -0
  453. package/src/runtime/verification/test-selection.js +67 -7
  454. package/src/runtime/worker/active.js +90 -0
  455. package/src/runtime/worker/cancel.js +222 -0
  456. package/src/runtime/worker/catalogue.js +122 -0
  457. package/src/runtime/worker/command.js +677 -0
  458. package/src/runtime/worker/evidence.js +288 -0
  459. package/src/runtime/worker/launch.js +288 -0
  460. package/src/runtime/worker/models.js +137 -0
  461. package/src/runtime/worker/parsers.js +218 -0
  462. package/src/runtime/worker/probes/index.js +706 -0
  463. package/src/runtime/worker/process-tree.js +415 -0
  464. package/src/runtime/worker/quota.js +43 -0
  465. package/src/runtime/worker/readiness.js +713 -0
  466. package/src/runtime/worker/run-store.js +166 -0
  467. package/src/runtime/worker/supervisor.js +541 -0
  468. package/src/runtime/worker/worktree-check.js +252 -0
  469. package/src/runtime/workflow-engine.js +5 -1
  470. package/src/runtime/workflow-orchestrator.js +578 -63
  471. package/src/runtime/worktree.js +149 -22
  472. package/src/state/index.js +1 -1
  473. package/src/state/lockfile.js +64 -10
  474. package/src/state/run-lock.js +1 -1
  475. package/types/node-commonjs.d.ts +106 -7
  476. package/core/.antigravity-plugin/hooks.json +0 -39
  477. package/core/harnesses/gemini/hooks/hooks.json +0 -77
  478. package/core/harnesses/gemini/hooks/lib.sh +0 -302
  479. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -21
  480. package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
  481. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -21
  482. package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
  483. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -22
  484. package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
  485. package/core/harnesses/gemini/hooks/session-end.sh +0 -5
  486. package/core/harnesses/gemini/hooks/session-start.sh +0 -5
  487. package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
  488. package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
  489. package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
  490. package/core/harnesses/shared/hooks/hooks.json +0 -47
  491. package/core/shared/guidance/FLAGS.md +0 -25
  492. package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
  493. package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
  494. package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
  495. package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
  496. package/src/adapters/gemini/hooks.js +0 -210
  497. package/src/adapters/gemini/index.js +0 -390
@@ -9,18 +9,17 @@
9
9
  * joined by any event that names more than one, and a unit goes only as a whole: when every item in
10
10
  * it is settled in the fold (a follow-up done or dismissed, a goal done) and its newest event is
11
11
  * older than the window. A unit whose removal would change anything else the fold shows (another
12
- * item, a feature, a release, a conflict) is kept.
12
+ * item, a feature, a release, a conflict) is kept, and so is a unit that an event of a type this
13
+ * version does not know (written by a newer DoFlow) names by id.
13
14
  *
14
- * `parseWindow` remains pure. `retentionWindow` composes its unset case with the declared storage
15
- * default and the project's prune setting; explicit environment values retain their existing meaning.
15
+ * `parseWindow` remains pure. `retentionWindow` composes the resolver-owned default with the project's
16
+ * prune setting; explicit values retain their existing meaning while pruning is off.
16
17
  */
17
18
 
18
19
  const nodeFs = require('node:fs');
19
20
  const { isDeepStrictEqual } = require('node:util');
20
- const { REPO_ROOT } = require('../../helper/repo-root.js');
21
- const { loadStorageRegistry } = require('../storage/registry.js');
22
- const { resolveRoots, readPruneSetting } = require('../storage/roots.js');
23
- const { foldInto, finalize } = require('./fold.js');
21
+ const { loadSettings } = require('../settings/resolve.js');
22
+ const { foldInto, finalize, EVENT_TYPES } = require('./fold.js');
24
23
 
25
24
  const HOUR_MS = 3600000;
26
25
 
@@ -35,23 +34,46 @@ function parseWindow(env = process.env)
35
34
  return { state: 'invalid', raw: String(raw) };
36
35
  }
37
36
 
38
- /** Composes IC-013's explicit environment override with the declared lifecycle default. */
39
- function retentionWindow({ env = process.env, projectRoot, registry, fsImpl = nodeFs } = {}) {
37
+ function invalidRaw(resolved , env ) {
38
+ for (let i = resolved.candidates.length - 1; i >= 0; i -= 1) {
39
+ const candidate = resolved.candidates[i];
40
+ if (candidate.status !== 'invalid') continue;
41
+ if (candidate.layer === 'env') return String(env.DOFLOW_RETENTION_HOURS ?? '');
42
+ try {
43
+ const raw = JSON.parse(candidate.display);
44
+ if (typeof raw === 'string' || typeof raw === 'number') return String(raw);
45
+ } catch { /* a malformed whole-file candidate has no display value */ }
46
+ return candidate.display || candidate.reason || candidate.code || '';
47
+ }
48
+ return '';
49
+ }
50
+
51
+ /** Composes the retention setting and prune gate through the shared settings resolver. */
52
+ function retentionWindow({ env = process.env, projectRoot, fsImpl = nodeFs } = {}) {
40
53
  const parsed = parseWindow(env);
41
54
  if (parsed.state === 'invalid') return parsed;
42
- if (parsed.state === 'on') return { ...parsed, source: 'env' };
43
- let activeRegistry ;
44
55
  try {
45
- activeRegistry = registry || loadStorageRegistry({ repoRoot: REPO_ROOT, fsImpl });
56
+ let settingsRegistry;
57
+ if (fsImpl !== nodeFs) {
58
+ const { loadSettingsRegistry } = require('../settings/registry.js');
59
+ const loaded = loadSettingsRegistry({ fsImpl });
60
+ if (!loaded.ok) throw new Error(loaded.detail);
61
+ settingsRegistry = loaded.registry;
62
+ }
63
+ const view = loadSettings({ projectRoot, env, fsImpl, ...(settingsRegistry ? { registry: settingsRegistry } : {}) });
64
+ const retention = view.get('storage.retentionHours', 'project');
65
+ if (retention.state === 'invalid') return { state: 'invalid', raw: invalidRaw(retention, env) };
66
+ if (retention.explicit) {
67
+ const hours = retention.value;
68
+ return { state: 'on', hours, ms: hours * HOUR_MS, source: retention.origin.layer };
69
+ }
70
+ if (view.get('storage.prune', 'project').value !== 'on') return { state: 'off' };
71
+ const hours = retention.value;
72
+ return { state: 'on', hours, ms: hours * HOUR_MS, source: 'default' };
46
73
  } catch {
47
- return { state: 'off' };
74
+ // Preserve an explicit environment window when the resolver registry is unavailable.
75
+ return parsed.state === 'on' ? { ...parsed, source: 'env' } : { state: 'off' };
48
76
  }
49
- const roots = resolveRoots({ registry: activeRegistry, projectRoot, env });
50
- if (readPruneSetting({ registry: activeRegistry, roots, rootId: 'project-state', fsImpl }).setting !== 'on') return { state: 'off' };
51
- const lifecycleStore = activeRegistry.stores.find((store ) => store.id === 'lifecycle-events');
52
- if (!lifecycleStore) throw new Error('Storage registry has no lifecycle-events store');
53
- const hours = lifecycleStore.bound.defaultHours;
54
- return { state: 'on', hours, ms: hours * HOUR_MS, source: 'default' };
55
77
  }
56
78
 
57
79
  function isText(value ) { return typeof value === 'string' && value.trim() !== ''; }
@@ -113,6 +135,34 @@ function unitsOf(events , shouldStop ) {
113
135
  return [...units.values()];
114
136
  }
115
137
 
138
+ /**
139
+ * Every string value and every object key in the data of the events whose type this version's fold
140
+ * does not know (a newer DoFlow wrote them). The fold ignores them, so retention cannot tell what they change; a unit that
141
+ * one of them names by id is kept whole rather than deleted from under it. Exact matches only:
142
+ * keeping too little is a lost record, keeping too much only a file that stays.
143
+ * @returns {Set<string>|null} null when `shouldStop` fired
144
+ */
145
+ function unknownTypeStrings(events , shouldStop ) {
146
+ const known = new Set (EVENT_TYPES);
147
+ const found = new Set ();
148
+ for (const event of events) {
149
+ if (shouldStop?.()) return null;
150
+ if (known.has(event.type)) continue;
151
+ const pending = [event.data];
152
+ while (pending.length > 0) {
153
+ if (shouldStop?.()) return null;
154
+ const value = pending.pop();
155
+ if (typeof value === 'string') found.add(value);
156
+ else if (Array.isArray(value)) pending.push(...value);
157
+ else if (value && typeof value === 'object') {
158
+ for (const key of Object.keys(value)) found.add(key);
159
+ pending.push(...Object.values(value));
160
+ }
161
+ }
162
+ }
163
+ return found;
164
+ }
165
+
116
166
  function isSettled(state , key ) {
117
167
  const [kind, id] = [key.slice(0, key.indexOf(':')), key.slice(key.indexOf(':') + 1)];
118
168
  if (kind === 'followup') {
@@ -173,6 +223,12 @@ function selectExpired(events , { now, windowMs, shouldStop } = {})
173
223
  }
174
224
  const named = unitsOf(events, shouldStop);
175
225
  if (!named || shouldStop?.()) return incomplete();
226
+ const mentioned = unknownTypeStrings(events, shouldStop);
227
+ if (!mentioned || shouldStop?.()) return incomplete();
228
+ // Selected exactly as 5bc9407b did (every settled, expired, unserved unit is a candidate), and only
229
+ // then are the units an unknown-type event names dropped. Protection removes units from that
230
+ // selection; it never changes which of the others are picked, so what is deleted is always a subset
231
+ // of what base would delete on the same events.
176
232
  const candidates = [];
177
233
  for (const unit of named) {
178
234
  if (shouldStop?.()) return incomplete();
@@ -186,20 +242,28 @@ function selectExpired(events , { now, windowMs, shouldStop } = {})
186
242
  if (eligible) candidates.push({ ...unit, newestAt: newest });
187
243
  }
188
244
  const full = finalize(state);
189
- let selected = candidates;
190
- const allSafe = refoldEqual(events, full, candidates, now, shouldStop);
191
- if (allSafe === null || shouldStop?.()) return incomplete();
192
- if (!allSafe) {
193
- selected = [];
194
- const ordered = [...candidates].sort((a , b ) => Date.parse(a.newestAt) - Date.parse(b.newestAt));
195
- if (shouldStop?.()) return incomplete();
245
+ /** The refold-safe subset of `list`: all of it when that holds, else the oldest-first greedy pass. */
246
+ const safeSubset = (list ) => {
247
+ const allSafe = refoldEqual(events, full, list, now, shouldStop);
248
+ if (allSafe === null || shouldStop?.()) return null;
249
+ if (allSafe) return list;
250
+ const chosen = [];
251
+ const ordered = [...list].sort((a , b ) => Date.parse(a.newestAt) - Date.parse(b.newestAt));
252
+ if (shouldStop?.()) return null;
196
253
  for (const unit of ordered) {
197
- if (shouldStop?.()) return incomplete();
198
- const safe = refoldEqual(events, full, [...selected, unit], now, shouldStop);
199
- if (safe === null) return incomplete();
200
- if (safe) selected.push(unit);
254
+ if (shouldStop?.()) return null;
255
+ const safe = refoldEqual(events, full, [...chosen, unit], now, shouldStop);
256
+ if (safe === null) return null;
257
+ if (safe) chosen.push(unit);
201
258
  }
202
- }
259
+ return chosen;
260
+ };
261
+ const baseSelected = safeSubset(candidates);
262
+ if (!baseSelected) return incomplete();
263
+ const unprotected = baseSelected.filter((unit ) => ![...unit.items].some((key ) => mentioned.has(key.slice(key.indexOf(':') + 1))));
264
+ // Dropping units can leave a set the refold no longer accepts; check it again, inside the subset.
265
+ const selected = unprotected.length === baseSelected.length ? baseSelected : safeSubset(unprotected);
266
+ if (!selected) return incomplete();
203
267
  const units = [];
204
268
  for (const unit of selected) {
205
269
  if (shouldStop?.()) return incomplete();
@@ -10,6 +10,7 @@
10
10
  const fs = require('node:fs');
11
11
  const path = require('node:path');
12
12
  const { execFileSync } = require('node:child_process');
13
+ const { memoGit } = require('../git-memo.js');
13
14
 
14
15
 
15
16
 
@@ -61,11 +62,18 @@ function showToplevel(cwd ) {
61
62
  * @returns {string} an absolute, real path
62
63
  */
63
64
  function projectRoot(cwd = process.cwd()) {
65
+ // Inside a call scope (git-memo.ts) the answer git gave for this path is kept for the call. The
66
+ // fallback `real(cwd)` is not git's answer, so it is never kept: the path may become a repository.
67
+ return memoGit(`projectRoot:${path.resolve(cwd)}`, () => gitProjectRoot(cwd)) ?? real(cwd);
68
+ }
69
+
70
+ /** The root git names for `cwd`, or undefined when git names none (outside a repository). */
71
+ function gitProjectRoot(cwd ) {
64
72
  let entries = [];
65
73
  try { entries = worktreeEntries(git(cwd, ['worktree', 'list', '--porcelain'])); } catch { /* not a repository */ }
66
74
  const first = entries[0];
67
75
  if (first && !first.bare && isRealWorkTree(first)) return real(first.path);
68
- return showToplevel(cwd) || real(cwd);
76
+ return showToplevel(cwd) || undefined;
69
77
  }
70
78
 
71
79
  module.exports = { projectRoot, worktreeEntries };
@@ -11,19 +11,20 @@
11
11
  * the release tags and the base tag) come from `do-git-state.sh --lifecycle`, so bash and Node never
12
12
  * derive them twice. The rest is a fixed number of plain `git` calls, however many features are
13
13
  * tracked: one `rev-parse` pins the ref, one `for-each-ref` lists the branches, one
14
- * `git log --first-parent --merges` lists the merges, one `git tag --list` (tagged mode) gives the
15
- * release tags' commits, and one `git rev-list --parents` reads the commit graph of the pinned ref
16
- * and those tags. Every ancestry question (which merge brought a branch tip in, which tag contains a
17
- * merge) is then answered from that graph in memory, not by a git call per feature.
14
+ * `git log --first-parent --merges` lists the merges and their `DoFlow-Row` trailers, one
15
+ * `git tag --list` (tagged mode) gives the release tags' commits, and one `git rev-list --parents`
16
+ * reads the commit graph of the pinned ref and those tags. Every ancestry question (which merge
17
+ * brought a branch tip in, which tag contains a merge) is then answered from that graph in memory,
18
+ * not by a git call per feature.
18
19
  *
19
20
  * Nothing here writes, fetches or reaches a network.
20
21
  *
21
22
  * Accepted ceilings, stated rather than hidden: a feature's merge evidence is only as good as the
22
- * merge commit's subject and the branch name (a squash, a rebase and a fast-forward leave none,
23
- * so they reach the user through `notDetected` and `lifecycle --action merged`); the merge log and
24
- * the graph are read in full, without `--since` or a lower cut (a skewed clock must not hide a
25
- * merge), so their cost grows with the history of the integration ref, once per call; git dates
26
- * have one-second resolution, so the lower bound is floored to the second.
23
+ * merge commit's `DoFlow-Row` trailer or subject and the branch name (a squash, a rebase and a
24
+ * fast-forward leave none, so they reach the user through `notDetected` and `lifecycle --action
25
+ * merged`); the merge log and the graph are read in full, without `--since` or a lower cut (a skewed
26
+ * clock must not hide a merge), so their cost grows with the history of the integration ref, once
27
+ * per call; git dates have one-second resolution, so the lower bound is floored to the second.
27
28
  */
28
29
 
29
30
  const fs = require('node:fs');
@@ -69,13 +70,18 @@ function subjectNames(subject , slug ) {
69
70
  return new RegExp(`(?:^|[^a-z0-9-])${escapeRegExp(slug)}(?:$|[^a-z0-9-])`).test(subject);
70
71
  }
71
72
 
72
- /** First-parent merges of `ref`, oldest first. */
73
- function readMerges(root , ref ) {
74
- const out = git(root, ['log', '--first-parent', '--merges', '--format=%H%x09%P%x09%ct%x09%s', ref]);
75
- return out.split('\n').filter(Boolean).map((line) => {
76
- const [sha, parents, ct, ...subject] = line.split('\t');
77
- return { sha, parents: parents.split(' ').filter(Boolean), ct: Number(ct), subject: subject.join('\t') };
78
- }).reverse();
73
+ /** First-parent merges of `ref`, oldest first, with any `DoFlow-Row` trailer. */
74
+ function readMerges(root , ref ) {
75
+ const out = git(root, ['log', '--first-parent', '--merges', '--format=%H%x00%P%x00%ct%x00%s%x00%(trailers:key=DoFlow-Row,separator=%x1f)%x00', ref]);
76
+ const fields = out.split('\0');
77
+ const merges = [];
78
+ for (let i = 0; i + 4 < fields.length; i += 5) {
79
+ const [rawSha, parents, ct, subject, rowTrailer] = fields.slice(i, i + 5);
80
+ const sha = rawSha.trim();
81
+ if (!sha) continue;
82
+ merges.push({ sha, parents: parents.split(' ').filter(Boolean), ct: Number(ct), subject, rowTrailer: rowTrailer.trim() });
83
+ }
84
+ return merges.reverse();
79
85
  }
80
86
 
81
87
  /** Branch refs by short name (`feat/x`, `origin/feat/x`) with their tips; local branches first. */
@@ -221,7 +227,18 @@ function findEvidence({ slug, lowerBound, merges, mergeBySha, newestCt, history,
221
227
  if (merge) return { kind: 'branch', ref: tip.short, commit: merge.sha };
222
228
  }
223
229
  for (let i = merges.length - 1; i >= 0; i -= 1) {
224
- if (merges[i].ct >= lowerBound && subjectNames(merges[i].subject, slug)) return { kind: 'merge-subject', ref: merges[i].sha.slice(0, 7), commit: merges[i].sha };
230
+ const merge = merges[i];
231
+ if (merge.ct < lowerBound) continue;
232
+ if (merge.rowTrailer) {
233
+ // A present DoFlow-Row is authoritative: it can match only on an exact two-parent merge,
234
+ // and a mismatch must not fall through to a subject that names a different row.
235
+ const match = /^DoFlow-Row:\s*(.*?)\s*$/i.exec(merge.rowTrailer);
236
+ if (merge.parents.length === 2 && match && match[1] === slug) {
237
+ return { kind: 'trailer', ref: merge.sha.slice(0, 7), commit: merge.sha };
238
+ }
239
+ continue;
240
+ }
241
+ if (subjectNames(merge.subject, slug)) return { kind: 'merge-subject', ref: merge.sha.slice(0, 7), commit: merge.sha };
225
242
  }
226
243
  if (confirmed) return { kind: 'confirmed', ref: null, commit: null };
227
244
  return null;
@@ -36,8 +36,18 @@ const LEGACY_EVENTS_REL = path.join(LEGACY_LIFECYCLE_REL, 'events');
36
36
  const MARKER_REL = path.join(LIFECYCLE_REL, 'migrated.json');
37
37
  const MIGRATING_PREFIX = 'events.migrating-';
38
38
  const NOTICE = 'note: the lifecycle store is now .doflow/state/lifecycle/events; agent-docs/lifecycle/ is no longer read and can be deleted';
39
- const removedLine = (count , hours ) => `retention: removed ${count} event files older than ${hours} h`;
40
- const invalidLine = (raw ) => `warning: DOFLOW_RETENTION_HOURS='${raw}' is not a positive whole number of hours; ignored, nothing removed`;
39
+ const removedLine = (count , hours ) => `retention: removed ${count} event ${count === 1 ? 'file' : 'files'} older than ${hours} h`;
40
+ // A leading zero is rejected on purpose (`024` could be read as octal); the warning names it.
41
+ const invalidLine = (raw ) => `warning: DOFLOW_RETENTION_HOURS='${raw}' is not a positive whole number of hours${/^0[0-9]+$/.test(String(raw)) ? ' (no leading zeros)' : ''}; ignored, nothing removed`;
42
+ /**
43
+ * What `writeAtomic` leaves behind when a verb dies between creating its temp file and renaming it:
44
+ * `.retention-<pid>-<chars>.tmp` (the journal) and `.migrated-<pid>-<chars>.tmp` (the marker), in the
45
+ * lifecycle folder. The storage engine's work-store sweep matches `<name>.json.<pid>.tmp` inside
46
+ * record stores only, so nothing else ever removes them.
47
+ */
48
+ const WRITE_TMP = /^\.(?:retention|migrated)-[0-9]+-[A-Za-z0-9]+\.tmp$/;
49
+ /** The age past which such a file is abandoned; the storage engine passes `settings.staleTmpHours`, and a test pins this to it. */
50
+ const STALE_TMP_MS = 24 * 3600000;
41
51
  /** Bounds one pass, so its journal stays far below the reader's cap and its lock is held briefly. */
42
52
  const MAX_PASS_FILES = 20000;
43
53
  /** Unlinks between two refreshes of the lock's age. */
@@ -100,7 +110,8 @@ function fsyncCopy(fsImpl , file ) {
100
110
  function fsyncFolder(fsImpl , dir ) {
101
111
  let fd;
102
112
  try { fd = fsImpl.openSync(dir, 'r'); } catch { return; }
103
- try { fsImpl.fsyncSync(fd); } finally { fsImpl.closeSync(fd); }
113
+ // Windows opens a folder but refuses to flush it (EPERM); there is nothing to flush there.
114
+ try { fsImpl.fsyncSync(fd); } catch (error ) { if (!['EPERM', 'EINVAL', 'EISDIR', 'ENOTSUP'].includes(error?.code)) throw error; } finally { fsImpl.closeSync(fd); }
104
115
  }
105
116
 
106
117
  const migrationFailed = (code ) => ({
@@ -208,6 +219,30 @@ function refreshLock(fsImpl , root ) {
208
219
  try { fsImpl.utimesSync(`${lockTarget(root)}.lock`, now, now); } catch { /* the lock folder is ours; a failed touch only shortens its life */ }
209
220
  }
210
221
 
222
+ /**
223
+ * Removes temp files a crashed writer left in the lifecycle folder, once they are older than
224
+ * `staleMs`. Runs under the store lock, which every writer of these files holds, so none of them
225
+ * belongs to a live pass unless its lock was taken over as stale. Best effort: a file that cannot
226
+ * be removed is left for the next pass. `shouldStop` is checked before each unlink and each unlink
227
+ * counts against `maxFiles`, so a bounded caller's budget and stop hold here too.
228
+ * @returns {{removed: number, partial: boolean}} partial: stopped or out of budget with files left
229
+ */
230
+ function sweepStaleTmp(fsImpl , root , now , staleMs = STALE_TMP_MS, { maxFiles = Infinity, shouldStop } = {}) {
231
+ const dir = path.join(root, LIFECYCLE_REL);
232
+ let list ;
233
+ try { list = namesIn(fsImpl, dir); } catch { return { removed: 0, partial: false }; }
234
+ let removed = 0;
235
+ for (const name of list) {
236
+ if (!WRITE_TMP.test(name)) continue;
237
+ const file = path.join(dir, name);
238
+ const st = lstatOrNull(fsImpl, file);
239
+ if (!st || !st.isFile() || now.getTime() - st.mtimeMs <= staleMs) continue;
240
+ if (shouldStop?.() || removed >= maxFiles) return { removed, partial: true };
241
+ try { fsImpl.unlinkSync(file); removed += 1; } catch { /* left for the next pass */ }
242
+ }
243
+ return { removed, partial: false };
244
+ }
245
+
211
246
  /** Unlinks pending event files without clearing the journal until every hidden name is finished. */
212
247
  function removePending(fsImpl , root , names , onRemoved = () => {}, { maxFiles = Infinity, shouldStop } = {}) {
213
248
  const dir = path.join(root, EVENTS_REL);
@@ -294,7 +329,7 @@ function retain(fsImpl , root , window , now , { maxFiles = MAX_
294
329
 
295
330
  /** Runs a bounded lifecycle retention pass under a non-waiting store lock for the storage engine. */
296
331
  function retainLocked(root , options = {}) {
297
- const { window, now = new Date(), fsImpl = nodeFs, maxFiles = MAX_PASS_FILES, shouldStop } = options;
332
+ const { window, now = new Date(), fsImpl = nodeFs, maxFiles = MAX_PASS_FILES, shouldStop, staleTmpMs = STALE_TMP_MS } = options;
298
333
  const bounded = Object.hasOwn(options, 'maxFiles') || Object.hasOwn(options, 'shouldStop');
299
334
  const outcome = (removed , warning , partial = false, selected = [], code ) => bounded
300
335
  ? { removed, warning, partial, selected, ...(code ? { code } : {}) }
@@ -306,7 +341,9 @@ function retainLocked(root , options = {}) {
306
341
  const release = tryAcquireLock(fsImpl, lockTarget(root));
307
342
  if (!release) return outcome(0, [], true);
308
343
  try {
309
- const result = retain(fsImpl, root, window, now, { maxFiles, shouldStop });
344
+ const swept = sweepStaleTmp(fsImpl, root, now, staleTmpMs, { maxFiles, shouldStop });
345
+ if (swept.partial) return outcome(0, [], true);
346
+ const result = retain(fsImpl, root, window, now, { maxFiles: Math.max(0, maxFiles - swept.removed), shouldStop });
310
347
  return outcome(result.removedCount, result.warning, result.partial, result.selected, result.code);
311
348
  } finally {
312
349
  release();
@@ -326,7 +363,7 @@ function needsPrune(fsImpl , root , window , now ) {
326
363
  * @param {string} root projectRoot() result
327
364
  * @param {{env?: Object, now?: Date, fsImpl?: Object}} [options] defaults process.env, new Date(), node:fs
328
365
  * @returns {{ok: true, lines: string[]}
329
- * | {ok: false, finding: 'store-migration-failed'|'store-locked', message: string, lines: string[]}}
366
+ * | {ok: false, finding: 'store-migration-pending'|'store-migration-failed'|'store-locked', message: string, lines: string[]}}
330
367
  * `lines` are for stderr, without the verb prefix.
331
368
  * @throws {StoreUnsafeError} a symlinked new store folder, or a symlinked old folder when a copy must read it
332
369
  */
@@ -345,6 +382,17 @@ function prepareStore(root , { env = process.env, now = new Date(), fsImp
345
382
  try { migrate = legacyEventFiles(fsImpl, root).length > 0; } catch (error ) { return legacyUnreadable(error.code || error.message); }
346
383
  }
347
384
  }
385
+ if (env.DOFLOW_UPKEEP === 'off') {
386
+ if (migrate) {
387
+ return {
388
+ ok: false,
389
+ finding: 'store-migration-pending',
390
+ message: 'the lifecycle store has not been copied yet; run any followup, lifecycle or goal command once without DOFLOW_UPKEEP=off. Nothing was written.',
391
+ lines: [],
392
+ };
393
+ }
394
+ return { ok: true, lines: [...notice, ...invalid] };
395
+ }
348
396
  const retentionDue = readJournal(root, fsImpl).pending.length > 0 || needsPrune(fsImpl, root, window, now);
349
397
  if (!migrate && !markerDue && !retentionDue) return { ok: true, lines: [...notice, ...invalid] };
350
398
  if (migrate) refuseLinks(fsImpl, root, [LEGACY_LIFECYCLE_REL, LEGACY_EVENTS_REL]);
@@ -367,6 +415,7 @@ function prepareStore(root , { env = process.env, now = new Date(), fsImp
367
415
  throw error;
368
416
  }
369
417
  try {
418
+ sweepStaleTmp(fsImpl, root, now);
370
419
  // Decided again under the lock: another verb may have finished the copy while this one waited.
371
420
  let marked = hasMarker(fsImpl, root);
372
421
  if (!marked && storeHasEvents(fsImpl, root)) {
@@ -390,4 +439,4 @@ function prepareStore(root , { env = process.env, now = new Date(), fsImp
390
439
  }
391
440
  }
392
441
 
393
- module.exports = { prepareStore, retainLocked, LEGACY_LIFECYCLE_REL, LEGACY_EVENTS_REL, MARKER_REL };
442
+ module.exports = { prepareStore, retainLocked, removedLine, STALE_TMP_MS, LEGACY_LIFECYCLE_REL, LEGACY_EVENTS_REL, MARKER_REL };
@@ -8,7 +8,7 @@
8
8
  * the project store. Masking only narrows what a statement or an excerpt can carry.
9
9
  *
10
10
  * Two profiles. The line profile is for one-line statements and failure messages; the body profile
11
- * is for report bodies and excerpts and runs every line rule plus the body-only rules 7 to 9.
11
+ * is for report bodies and excerpts and runs every line rule plus the body-only rules 7 to 9 and 14 to 17.
12
12
  * Rules run in the table's order. Normalisation (rules 10 to 13) is a separate step applied to
13
13
  * failure messages only, after masking.
14
14
  *
@@ -36,7 +36,9 @@ const TOKEN_SHAPES = [
36
36
  /xox[abprs]-[A-Za-z0-9-]{10,}/g,
37
37
  /npm_[A-Za-z0-9]{24,}/g,
38
38
  /AIza[0-9A-Za-z_-]{35}/g,
39
- /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
39
+ // Left-anchored (DEC-069): without it every `eyJ` inside a long dotless run starts a scan to the
40
+ // run's end, which makes masking one line quadratic in its length.
41
+ /(?<![A-Za-z0-9_-])eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
40
42
  ];
41
43
 
42
44
  const PEM = /-----BEGIN [^\n-]*-----(?:[\s\S]*?-----END [^\n-]*-----|[\s\S]*$)/g;
@@ -52,13 +54,107 @@ const KEY_VALUE_BODY = new RegExp(`(${KEY_NAME}["']?[ \\t]*[=:][ \\t]*)("[^"\\r\
52
54
  const AUTH_HEADER = /((?:Proxy-)?Authorization:)[ \t]*([^\r\n]*)/gi;
53
55
  const SECRET_FLAG = /(--(?:password|passwd|token|secret|api-key|client-secret)(?:=|[ \t]+))("[^"\r\n]*"|'[^'\r\n]*'|[^\s"']+)/gi;
54
56
 
57
+ // Rules 14 to 17 (body profile only, FU-ht8b3r). Command shapes that carry a secret without a key
58
+ // word: `curl -u user:pw` (also glued, `-uuser:pw`), `mysql -psecret` (value glued to the flag; a bare
59
+ // `-p` prompts and carries none), and the phrase `password hunter2`. The flags are masked by one
60
+ // left-to-right scan (rules 14 and 15, `maskCommandFlags`) that knows which command it is in, so every
61
+ // flag of a command is masked and `sort -u`, `git push -u` and `mysql -P 3306` stay as written.
62
+ // A command word counts only at command position (see `maskCommandFlags`), and quoted text is scanned,
63
+ // not skipped. Alternatives, in order: a quote, a command break (`|`, `;`, `&`, parentheses, a
64
+ // backtick, a line end), curl's `--user` and `-u` (the short one may be glued to its value), mysql's
65
+ // `-p` glued to its value, any other word.
66
+ const COMMAND_SCAN = /(?<quote>["'])|(?<brk>[|;&()`\r\n])|(?<=[ \t])(?:(?<long>--user)(?:=|[ \t]+)|(?<short>-u)(?:=|[ \t]*)(?=[^\s|;&]))|(?<=[ \t])(?<pass>-p)(?=[^\s|;&-])|(?<word>[^\s|;&()`"']+)/g;
67
+ // The closing quote of a quoted span, on the same line.
68
+ const QUOTE_CLOSE = { '"': /[^"\r\n]*"/y, "'": /[^'\r\n]*'/y };
69
+ // A word that is the command: bare or as a path (never a URL, which holds `:`).
70
+ const COMMAND_WORD = /^(?:(?:\.{0,2}|~)?\/(?:[^\s:]*\/)?)?(?:(curl)|(mysql(?:dump|admin|pump|import)?))$/;
71
+ // Words after which the next word is the command: wrappers, remote and container runners, shell keywords.
72
+ const WRAPPER_WORDS = new Set(['sudo', 'doas', 'su', 'env', 'exec', 'nohup', 'time', 'timeout', 'nice', 'ionice', 'xargs', 'watch',
73
+ 'command', 'builtin', 'eval', 'setsid', 'stdbuf', 'flock', 'chroot', 'ssh', 'docker', 'podman', 'kubectl', 'sh', 'bash', 'zsh', 'dash', 'ksh',
74
+ 'if', 'then', 'else', 'elif', 'do', 'while', 'until', '!', '{']);
75
+ // A first word that does not use up the command position: a prompt mark, a variable assignment or a
76
+ // label such as `Running:`.
77
+ const PROMPT_OR_ASSIGNMENT = /^(?:[$#%>+]+|[A-Za-z_][A-Za-z0-9_]*=[^=]*|[A-Za-z][A-Za-z0-9_-]*:)$/;
78
+ // A flag value: quoted, or unquoted up to a space, quote or command break.
79
+ const COMMAND_FLAG_VALUE = /"[^"\r\n]*"|'[^'\r\n]*'|[^\s"'|;&]+/y;
80
+ // The value after the phrase must hold a non-letter, as in rule 4, so ordinary prose ("the password is
81
+ // required") survives; a plain-letter value after a bare `password ` is a known gap, and so is a
82
+ // password of one to three digits (`reset password 2 times` is prose).
83
+ const PASSWORD_PHRASE = /(?<![A-Za-z0-9_.-])((?:password|passwd)[ \t]+)(?!<masked>|[=:])([^\s,;&"'`)\]}]+)/gi;
84
+ // Another user's home directory (the running user's is rule 0). `Shared` is not a person and `runner`
85
+ // is the CI runner account (`/Users/runner/work/...`), so neither is masked.
86
+ const OTHER_USER_HOME = /(?<![A-Za-z0-9_.-])\/Users\/(?!(?:Shared|runner)(?![^/\s"'`<>:;,()[\]{}])|<)[^/\s"'`<>:;,()[\]{}]+/g;
87
+
88
+ /**
89
+ * Rules 14 and 15. Scans left to right and masks the value of every `-u`/`--user` of a curl command
90
+ * and every `-p<value>` of a mysql command. A command word (`curl`, `mysql*`) is the command of its
91
+ * segment only at command position: the first word of the text, of a line or of a segment after a
92
+ * break (`|`, `;`, `&`, a parenthesis, a backtick), of a quoted span, or after a wrapper word
93
+ * (`sudo`, `env`, `ssh`, `docker`, `sh`, `bash`, ...) at the start of the segment. A word that holds `:`
94
+ * (a URL) or is followed by more of its token (`mysql-8`) is not the command. A segment has one
95
+ * command. Quoted text is scanned: its start is a command position, and a break inside it ends only
96
+ * the command inside it, so `curl -H "a|b" -u x:y` keeps its command. One pass; every step consumes
97
+ * at least one character and a closing quote is searched on its own line, so the cost is linear.
98
+ */
99
+ function maskCommandFlags(text ) {
100
+ let out = '';
101
+ let copied = 0;
102
+ let command = null;
103
+ let atStart = true; // the next word is the first of its segment
104
+ let wrapped = false; // the segment began with a wrapper word
105
+ const spans = [];
106
+ COMMAND_SCAN.lastIndex = 0;
107
+ let found ;
108
+ while ((found = COMMAND_SCAN.exec(text)) !== null) {
109
+ const groups = found.groups || {};
110
+ const end = found.index + found[0].length;
111
+ if (groups.quote) {
112
+ const top = spans[spans.length - 1];
113
+ if (top && top.quote === groups.quote) {
114
+ spans.pop();
115
+ ({ command, atStart, wrapped } = top);
116
+ atStart = false;
117
+ } else {
118
+ const close = QUOTE_CLOSE[groups.quote];
119
+ close.lastIndex = end;
120
+ if (close.test(text)) {
121
+ spans.push({ quote: groups.quote, command, atStart, wrapped });
122
+ command = null; atStart = true; wrapped = false;
123
+ }
124
+ }
125
+ } else if (groups.brk) {
126
+ if (!(groups.brk === '&' && /[<>]/.test(text[found.index - 1] || ''))) { command = null; atStart = true; wrapped = false; }
127
+ } else if (groups.word) {
128
+ const word = groups.word;
129
+ if (atStart && PROMPT_OR_ASSIGNMENT.test(word)) continue;
130
+ const named = COMMAND_WORD.exec(word);
131
+ if (command === null && (atStart || wrapped) && named) command = named[1] ? 'curl' : 'mysql';
132
+ else if (atStart) wrapped = WRAPPER_WORDS.has(word.slice(word.lastIndexOf('/') + 1));
133
+ atStart = false;
134
+ } else if (((groups.long || groups.short) && command === 'curl') || (groups.pass && command === 'mysql')) {
135
+ atStart = false;
136
+ COMMAND_FLAG_VALUE.lastIndex = end;
137
+ const value = COMMAND_FLAG_VALUE.exec(text);
138
+ if (value && !isMasked(value[0]) && !(command === 'mysql' && value[0].startsWith(MASKED))) {
139
+ out += text.slice(copied, end) + MASKED;
140
+ copied = end + value[0].length;
141
+ COMMAND_SCAN.lastIndex = copied;
142
+ }
143
+ } else {
144
+ atStart = false; // another command's flag: rescan what follows it
145
+ COMMAND_SCAN.lastIndex = end;
146
+ }
147
+ }
148
+ return out + text.slice(copied);
149
+ }
150
+
55
151
  function escapeRegExp(text ) { return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
56
152
 
57
153
  /** The home directory to abbreviate: an absolute path other than the file system root. */
58
154
  function homeOf(options ) {
59
155
  const raw = options && 'home' in options ? options.home : process.env.HOME;
60
- if (typeof raw !== 'string' || !raw.startsWith('/')) return null;
61
- const home = raw.replace(/\/+$/, '');
156
+ if (typeof raw !== 'string' || !(raw.startsWith('/') || /^[A-Za-z]:[\\/]/.test(raw))) return null;
157
+ const home = raw.replace(/[\\/]+$/, '');
62
158
  return home.length > 1 ? home : null;
63
159
  }
64
160
 
@@ -89,6 +185,30 @@ function mask40(run , before ) {
89
185
  looksLikeSecretRun(value, 1) && !isPathRun(value, index === 0 ? before : run[index - 1]) ? MASKED : value));
90
186
  }
91
187
 
188
+ const TS_TYPE_NAMES = new Set([
189
+ 'string', 'number', 'boolean', 'any', 'unknown', 'void',
190
+ 'never', 'null', 'undefined', 'symbol', 'bigint',
191
+ ]);
192
+
193
+ function isCodePreserved(head , value , offset , fullString ) {
194
+ if (value.startsWith('"') || value.startsWith("'")) return false;
195
+ const isColon = /:[ \t]*$/.test(head);
196
+ if (isColon) {
197
+ const baseType = value.replace(/\[\s*\]?$|\[$/, '');
198
+ if (TS_TYPE_NAMES.has(baseType)) return true;
199
+ if (/^[A-Z]|^\[/.test(value)) return true;
200
+ if (/^(?:new\s|[A-Za-z0-9_$]+\s*\(|[A-Za-z0-9_$]+\.[A-Za-z0-9_$]+)/.test(value)) return true;
201
+ }
202
+ const isEquals = /=[ \t]*$/.test(head);
203
+ if (isEquals) {
204
+ const before = fullString.slice(Math.max(0, offset - 30), offset);
205
+ if (/(?:const|let|var|readonly)\s+$/.test(before)) return true;
206
+ if (/\bnew\s|[A-Za-z0-9_$]+\.[A-Za-z0-9_$]+|\(/.test(value)) return true;
207
+ if (/^[\[{]/.test(value)) return true;
208
+ }
209
+ return false;
210
+ }
211
+
92
212
  function isMasked(value ) { return value.replace(/^["']|["']$/g, '') === MASKED; }
93
213
 
94
214
  function count(text , token ) { return text.split(token).length - 1; }
@@ -119,8 +239,8 @@ function mask(text , profile , options = {})
119
239
  // 3. Bearer tokens and URL credentials.
120
240
  out = out.replace(BEARER, `$1$2${MASKED}`).replace(URL_CREDENTIALS, `://${MASKED}@`);
121
241
  // 4. Key=value, unquoted, when the value is not plain letters.
122
- out = out.replace(KEY_VALUE_LINE, (match , key , separator , value ) => (
123
- isMasked(value) || value.startsWith(MASKED) || !/[^A-Za-z]/.test(value) ? match : `${key}${separator}${MASKED}`));
242
+ out = out.replace(KEY_VALUE_LINE, (match , key , separator , value , offset , fullString ) => (
243
+ isMasked(value) || value.startsWith(MASKED) || !/[^A-Za-z]/.test(value) || isCodePreserved(key + separator, value, offset, fullString) ? match : `${key}${separator}${MASKED}`));
124
244
  // 5. Long secret-looking runs that are not paths.
125
245
  out = out.replace(LONG_RUN, (run , offset ) => (
126
246
  looksLikeSecretRun(run, 2) && !isPathRun(run, out[offset - 1]) ? MASKED : mask40(run, out[offset - 1])));
@@ -129,8 +249,9 @@ function mask(text , profile , options = {})
129
249
 
130
250
  if (profile === 'body') {
131
251
  // 7. Key and value, quoted or not, whatever the value looks like.
132
- out = out.replace(KEY_VALUE_BODY, (match , head , value ) => {
252
+ out = out.replace(KEY_VALUE_BODY, (match , head , value , offset , fullString ) => {
133
253
  if (isMasked(value) || value.startsWith(MASKED)) return match;
254
+ if (isCodePreserved(head, value, offset, fullString)) return match;
134
255
  const quote = value[0] === '"' || value[0] === "'" ? value[0] : '';
135
256
  return `${head}${quote}${MASKED}${quote}`;
136
257
  });
@@ -138,6 +259,12 @@ function mask(text , profile , options = {})
138
259
  out = out.replace(AUTH_HEADER, (match , name , rest ) => (rest === '' || rest === MASKED ? match : `${name} ${MASKED}`));
139
260
  // 9. Secret-bearing command-line flags.
140
261
  out = out.replace(SECRET_FLAG, (match , flag , value ) => (isMasked(value) ? match : `${flag}${MASKED}`));
262
+ // 14 and 15. `curl -u user:pw`, `curl --user user:pw`, `curl -uuser:pw` and `mysql -psecret`, every flag of a command.
263
+ out = maskCommandFlags(out);
264
+ // 16. `password hunter2`: the key word, a space and a value with a non-letter in it.
265
+ out = out.replace(PASSWORD_PHRASE, (match , head , value ) => (/[^A-Za-z]/.test(value) && !/^[0-9]{1,3}$/.test(value) ? `${head}${MASKED}` : match));
266
+ // 17. Another user's home directory: `/Users/<name>` becomes `/Users/<masked>`.
267
+ out = out.replace(OTHER_USER_HOME, `/Users/${MASKED}`);
141
268
  }
142
269
 
143
270
  const masked = Math.max(0, [MASKED, EMAIL, IP].reduce((sum, token) => sum + count(out, token) - count(input, token), 0));