@mjasnikovs/pi-task 0.38.29 → 0.38.31

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 (373) hide show
  1. package/dist/config/config.d.ts +70 -70
  2. package/dist/config/config.js +26 -35
  3. package/dist/config/extension-list.d.ts +6 -5
  4. package/dist/config/extension-list.js +3 -2
  5. package/dist/config/reasoning-args.d.ts +9 -7
  6. package/dist/config/reasoning-args.js +12 -10
  7. package/dist/config/reasoning.d.ts +44 -105
  8. package/dist/config/reasoning.js +27 -704
  9. package/dist/config/register.d.ts +34 -48
  10. package/dist/config/register.js +41 -51
  11. package/dist/config/tool-list.d.ts +16 -16
  12. package/dist/config/tool-list.js +1 -1
  13. package/dist/index.js +2 -0
  14. package/dist/remote/bridge.d.ts +19 -10
  15. package/dist/remote/bridge.js +3 -2
  16. package/dist/remote/broadcast.js +3 -1
  17. package/dist/remote/events.js +12 -11
  18. package/dist/remote/history.d.ts +1 -1
  19. package/dist/remote/protocol.d.ts +6 -3
  20. package/dist/remote/protocol.js +2 -1
  21. package/dist/remote/push.d.ts +16 -16
  22. package/dist/remote/push.js +27 -27
  23. package/dist/remote/register.d.ts +3 -3
  24. package/dist/remote/register.js +17 -19
  25. package/dist/remote/server.d.ts +9 -8
  26. package/dist/remote/server.js +15 -14
  27. package/dist/remote/session-state.d.ts +5 -4
  28. package/dist/remote/session-state.js +8 -5
  29. package/dist/remote/sw.d.ts +7 -6
  30. package/dist/remote/sw.js +7 -6
  31. package/dist/remote/tailscale.d.ts +4 -2
  32. package/dist/remote/tailscale.js +4 -2
  33. package/dist/remote/ui-highlight.js +6 -5
  34. package/dist/remote/ui-render.js +4 -4
  35. package/dist/remote/ui-script.js +24 -24
  36. package/dist/remote/ui-styles.d.ts +1 -1
  37. package/dist/remote/ui-styles.js +10 -13
  38. package/dist/remote/ui-tools.js +9 -6
  39. package/dist/shared/child-extensions.d.ts +29 -17
  40. package/dist/shared/child-extensions.js +29 -17
  41. package/dist/shared/child-output.d.ts +30 -24
  42. package/dist/shared/child-output.js +25 -17
  43. package/dist/shared/child-process.d.ts +47 -40
  44. package/dist/shared/child-process.js +50 -59
  45. package/dist/shared/command-watchdog.d.ts +85 -16
  46. package/dist/shared/command-watchdog.js +115 -21
  47. package/dist/shared/fs-text.d.ts +16 -10
  48. package/dist/shared/fs-text.js +16 -10
  49. package/dist/shared/git-runner.d.ts +25 -25
  50. package/dist/shared/git-runner.js +25 -25
  51. package/dist/shared/leaked-tool-call.d.ts +17 -11
  52. package/dist/shared/leaked-tool-call.js +23 -15
  53. package/dist/shared/model-endpoint.d.ts +29 -16
  54. package/dist/shared/model-endpoint.js +33 -21
  55. package/dist/shared/pi-invocation.d.ts +7 -4
  56. package/dist/shared/pi-invocation.js +12 -7
  57. package/dist/shared/pkg-version.d.ts +13 -5
  58. package/dist/shared/pkg-version.js +13 -5
  59. package/dist/shared/reasoning-capability.d.ts +35 -24
  60. package/dist/shared/reasoning-capability.js +35 -24
  61. package/dist/shared/stream-watchdog.d.ts +60 -44
  62. package/dist/shared/stream-watchdog.js +62 -45
  63. package/dist/task/accept-debt.d.ts +41 -43
  64. package/dist/task/accept-debt.js +73 -65
  65. package/dist/task/api-synthesis.d.ts +24 -21
  66. package/dist/task/api-synthesis.js +32 -26
  67. package/dist/task/apis-contract.d.ts +32 -64
  68. package/dist/task/apis-contract.js +32 -64
  69. package/dist/task/artifact-closure.d.ts +27 -13
  70. package/dist/task/artifact-closure.js +95 -67
  71. package/dist/task/auto-commit.d.ts +46 -35
  72. package/dist/task/auto-commit.js +51 -38
  73. package/dist/task/auto-io.d.ts +45 -25
  74. package/dist/task/auto-io.js +57 -29
  75. package/dist/task/auto-orchestrator.d.ts +26 -24
  76. package/dist/task/auto-orchestrator.js +192 -165
  77. package/dist/task/auto-prompts.d.ts +36 -24
  78. package/dist/task/auto-prompts.js +40 -26
  79. package/dist/task/autofix-ledger.d.ts +27 -25
  80. package/dist/task/autofix-ledger.js +29 -26
  81. package/dist/task/batch-test-task.d.ts +20 -12
  82. package/dist/task/batch-test-task.js +67 -60
  83. package/dist/task/boot-probe.d.ts +60 -44
  84. package/dist/task/boot-probe.js +91 -72
  85. package/dist/task/cancel-input.d.ts +30 -16
  86. package/dist/task/cancel-input.js +20 -11
  87. package/dist/task/cancel-points.d.ts +27 -20
  88. package/dist/task/cancel-points.js +30 -22
  89. package/dist/task/child-runner.d.ts +124 -55
  90. package/dist/task/child-runner.js +298 -90
  91. package/dist/task/child-status.d.ts +23 -16
  92. package/dist/task/child-status.js +23 -16
  93. package/dist/task/clamp-output.js +12 -5
  94. package/dist/task/command-run.d.ts +31 -28
  95. package/dist/task/command-run.js +44 -35
  96. package/dist/task/command-shrink.d.ts +25 -18
  97. package/dist/task/command-shrink.js +37 -31
  98. package/dist/task/command-watchdog.d.ts +9 -6
  99. package/dist/task/command-watchdog.js +21 -15
  100. package/dist/task/context-attribution.d.ts +34 -26
  101. package/dist/task/context-attribution.js +34 -26
  102. package/dist/task/context-silence.d.ts +39 -29
  103. package/dist/task/context-silence.js +35 -25
  104. package/dist/task/context-usage.d.ts +16 -9
  105. package/dist/task/context-usage.js +16 -9
  106. package/dist/task/contracts.d.ts +8 -4
  107. package/dist/task/contracts.js +25 -17
  108. package/dist/task/coverage-loop.d.ts +22 -18
  109. package/dist/task/coverage-loop.js +35 -30
  110. package/dist/task/critique-probes.d.ts +13 -14
  111. package/dist/task/critique-probes.js +50 -39
  112. package/dist/task/debug-log.d.ts +13 -5
  113. package/dist/task/debug-log.js +32 -20
  114. package/dist/task/decompose-fidelity.d.ts +11 -9
  115. package/dist/task/decompose-fidelity.js +38 -33
  116. package/dist/task/decompose-granularity.d.ts +41 -38
  117. package/dist/task/decompose-granularity.js +41 -38
  118. package/dist/task/deep-render-check.d.ts +22 -14
  119. package/dist/task/deep-render-check.js +40 -31
  120. package/dist/task/dropped-input.d.ts +12 -7
  121. package/dist/task/dropped-input.js +5 -2
  122. package/dist/task/enforce-attribution.d.ts +38 -47
  123. package/dist/task/enforce-attribution.js +46 -52
  124. package/dist/task/enforce-guidelines.d.ts +31 -20
  125. package/dist/task/enforce-guidelines.js +32 -21
  126. package/dist/task/enrichment.d.ts +7 -2
  127. package/dist/task/enrichment.js +26 -14
  128. package/dist/task/env-notes.d.ts +16 -7
  129. package/dist/task/env-notes.js +48 -31
  130. package/dist/task/env-template-closure.d.ts +4 -4
  131. package/dist/task/env-template-closure.js +42 -34
  132. package/dist/task/external-context.d.ts +28 -21
  133. package/dist/task/external-context.js +17 -12
  134. package/dist/task/failure-classifier.d.ts +4 -5
  135. package/dist/task/failure-classifier.js +30 -8
  136. package/dist/task/file-inventory.d.ts +15 -11
  137. package/dist/task/file-inventory.js +25 -22
  138. package/dist/task/final-gate-fix.d.ts +74 -86
  139. package/dist/task/final-gate-fix.js +97 -116
  140. package/dist/task/final-gate-progress.d.ts +29 -46
  141. package/dist/task/final-gate-progress.js +40 -51
  142. package/dist/task/final-gate.d.ts +64 -97
  143. package/dist/task/final-gate.js +192 -199
  144. package/dist/task/fix-child.d.ts +21 -27
  145. package/dist/task/fix-child.js +21 -27
  146. package/dist/task/foreign-path.d.ts +6 -5
  147. package/dist/task/foreign-path.js +0 -0
  148. package/dist/task/frozen-conflict.d.ts +9 -10
  149. package/dist/task/frozen-conflict.js +61 -64
  150. package/dist/task/frozen-path-guard.d.ts +35 -14
  151. package/dist/task/frozen-path-guard.js +56 -39
  152. package/dist/task/gate-child.d.ts +27 -28
  153. package/dist/task/gate-child.js +36 -35
  154. package/dist/task/gate-deps.d.ts +34 -27
  155. package/dist/task/gate-deps.js +169 -159
  156. package/dist/task/gate-tally.d.ts +77 -80
  157. package/dist/task/gate-tally.js +65 -68
  158. package/dist/task/git-state-guard.d.ts +15 -11
  159. package/dist/task/git-state-guard.js +76 -66
  160. package/dist/task/impl-widget.d.ts +25 -16
  161. package/dist/task/impl-widget.js +27 -17
  162. package/dist/task/implementation-guards.d.ts +26 -0
  163. package/dist/task/implementation-guards.js +177 -0
  164. package/dist/task/implementation-thinking.d.ts +33 -31
  165. package/dist/task/implementation-thinking.js +5 -6
  166. package/dist/task/implementation-turn.d.ts +39 -31
  167. package/dist/task/implementation-turn.js +41 -28
  168. package/dist/task/inline-markdown.d.ts +20 -7
  169. package/dist/task/inline-markdown.js +15 -6
  170. package/dist/task/launch-config-gap.js +25 -39
  171. package/dist/task/launch-contract.d.ts +18 -21
  172. package/dist/task/launch-contract.js +28 -30
  173. package/dist/task/launch-manifest.d.ts +6 -2
  174. package/dist/task/launch-manifest.js +35 -34
  175. package/dist/task/ledger.js +16 -14
  176. package/dist/task/lint-fix.d.ts +6 -8
  177. package/dist/task/lint-fix.js +67 -69
  178. package/dist/task/loop-detector.d.ts +27 -8
  179. package/dist/task/loop-detector.js +38 -14
  180. package/dist/task/mid-run-input.d.ts +17 -15
  181. package/dist/task/mid-run-input.js +17 -15
  182. package/dist/task/orchestrator.d.ts +24 -28
  183. package/dist/task/orchestrator.js +89 -66
  184. package/dist/task/orientation.d.ts +18 -23
  185. package/dist/task/orientation.js +24 -31
  186. package/dist/task/owned-freeze-conflict.d.ts +21 -20
  187. package/dist/task/owned-freeze-conflict.js +52 -85
  188. package/dist/task/owned-freeze-reassign.d.ts +40 -60
  189. package/dist/task/owned-freeze-reassign.js +41 -61
  190. package/dist/task/parsers.d.ts +4 -2
  191. package/dist/task/parsers.js +4 -4
  192. package/dist/task/phases.d.ts +41 -48
  193. package/dist/task/phases.js +196 -252
  194. package/dist/task/plan-io.d.ts +6 -7
  195. package/dist/task/plan-io.js +6 -7
  196. package/dist/task/plan-orchestrator.d.ts +10 -8
  197. package/dist/task/plan-orchestrator.js +14 -10
  198. package/dist/task/plan-prompts.d.ts +6 -5
  199. package/dist/task/plan-prompts.js +6 -5
  200. package/dist/task/plan-readonly.d.ts +4 -5
  201. package/dist/task/plan-readonly.js +4 -5
  202. package/dist/task/plan-rounds.d.ts +17 -29
  203. package/dist/task/plan-rounds.js +21 -34
  204. package/dist/task/plan-session.d.ts +58 -72
  205. package/dist/task/plan-session.js +61 -83
  206. package/dist/task/probe-gaming.d.ts +28 -27
  207. package/dist/task/probe-gaming.js +0 -0
  208. package/dist/task/prohibition-probe.d.ts +14 -16
  209. package/dist/task/prompts.d.ts +3 -4
  210. package/dist/task/prompts.js +17 -26
  211. package/dist/task/qa-transcript.d.ts +15 -22
  212. package/dist/task/qa-transcript.js +15 -21
  213. package/dist/task/question-box.d.ts +17 -13
  214. package/dist/task/question-box.js +19 -15
  215. package/dist/task/question-dedup.d.ts +6 -7
  216. package/dist/task/question-dedup.js +13 -14
  217. package/dist/task/question-dialog.d.ts +22 -32
  218. package/dist/task/question-dialog.js +22 -32
  219. package/dist/task/question-source.d.ts +18 -44
  220. package/dist/task/question-source.js +22 -51
  221. package/dist/task/refuted-constraint.d.ts +11 -31
  222. package/dist/task/refuted-constraint.js +27 -51
  223. package/dist/task/regenerable-artifacts.d.ts +12 -31
  224. package/dist/task/regenerable-artifacts.js +12 -31
  225. package/dist/task/render-check.d.ts +11 -22
  226. package/dist/task/render-check.js +33 -46
  227. package/dist/task/repo-health-check.d.ts +10 -14
  228. package/dist/task/repo-health-check.js +17 -23
  229. package/dist/task/requirements.d.ts +38 -71
  230. package/dist/task/requirements.js +78 -126
  231. package/dist/task/research-fanout-budget.d.ts +51 -88
  232. package/dist/task/research-fanout-budget.js +51 -88
  233. package/dist/task/research-worker.d.ts +29 -39
  234. package/dist/task/research-worker.js +37 -61
  235. package/dist/task/resume-gap.d.ts +14 -15
  236. package/dist/task/root-cause-repair.d.ts +9 -9
  237. package/dist/task/root-cause-repair.js +28 -40
  238. package/dist/task/run-bracket.d.ts +10 -13
  239. package/dist/task/run-end.d.ts +12 -22
  240. package/dist/task/run-end.js +8 -16
  241. package/dist/task/run-final-gate.d.ts +19 -21
  242. package/dist/task/run-final-gate.js +62 -80
  243. package/dist/task/runner-globs.d.ts +12 -13
  244. package/dist/task/runner-globs.js +12 -13
  245. package/dist/task/runner-resolve.d.ts +9 -9
  246. package/dist/task/runner-resolve.js +22 -23
  247. package/dist/task/script-escape.d.ts +10 -12
  248. package/dist/task/script-escape.js +13 -14
  249. package/dist/task/serve-entry.d.ts +1 -1
  250. package/dist/task/serve-entry.js +22 -25
  251. package/dist/task/service-blocks.js +4 -2
  252. package/dist/task/shipped-source.d.ts +11 -29
  253. package/dist/task/shipped-source.js +11 -29
  254. package/dist/task/skip-escape.js +10 -14
  255. package/dist/task/spec-urls.d.ts +26 -65
  256. package/dist/task/spec-urls.js +26 -65
  257. package/dist/task/spec-validation.d.ts +17 -20
  258. package/dist/task/spec-validation.js +17 -20
  259. package/dist/task/stall-detector.d.ts +23 -30
  260. package/dist/task/stall-detector.js +23 -30
  261. package/dist/task/stream-watchdog.d.ts +14 -12
  262. package/dist/task/stream-watchdog.js +14 -12
  263. package/dist/task/substitution-probe.d.ts +17 -20
  264. package/dist/task/substitution-probe.js +17 -20
  265. package/dist/task/task-gates.d.ts +36 -41
  266. package/dist/task/task-gates.js +95 -106
  267. package/dist/task/task-io.d.ts +4 -4
  268. package/dist/task/task-io.js +4 -4
  269. package/dist/task/task-parsers.js +4 -3
  270. package/dist/task/task-provenance.d.ts +2 -2
  271. package/dist/task/task-provenance.js +11 -13
  272. package/dist/task/task-types.d.ts +4 -3
  273. package/dist/task/terminal-outcome.d.ts +14 -16
  274. package/dist/task/terminal-outcome.js +12 -14
  275. package/dist/task/test-assembly.d.ts +13 -20
  276. package/dist/task/test-assembly.js +13 -20
  277. package/dist/task/timings.d.ts +5 -3
  278. package/dist/task/timings.js +5 -3
  279. package/dist/task/title-label.d.ts +9 -4
  280. package/dist/task/title-label.js +9 -4
  281. package/dist/task/type-only-answer.d.ts +44 -52
  282. package/dist/task/type-only-answer.js +44 -52
  283. package/dist/task/unfailable-command.d.ts +18 -24
  284. package/dist/task/unfailable-command.js +21 -27
  285. package/dist/task/unknown-routing.d.ts +10 -4
  286. package/dist/task/unknown-routing.js +10 -4
  287. package/dist/task/user-directives.d.ts +5 -8
  288. package/dist/task/user-directives.js +5 -8
  289. package/dist/task/verify-quality.d.ts +18 -22
  290. package/dist/task/verify-quality.js +45 -46
  291. package/dist/task/verify-reconcile.d.ts +15 -10
  292. package/dist/task/verify-reconcile.js +45 -43
  293. package/dist/task/verify-resolution.d.ts +24 -20
  294. package/dist/task/verify-resolution.js +51 -50
  295. package/dist/task/verify-work.d.ts +59 -66
  296. package/dist/task/verify-work.js +101 -138
  297. package/dist/task/widget.d.ts +15 -14
  298. package/dist/task/widget.js +22 -17
  299. package/dist/task/wiring-claims.d.ts +25 -32
  300. package/dist/task/wiring-claims.js +30 -35
  301. package/dist/task/write-guard.d.ts +39 -39
  302. package/dist/task/write-guard.js +48 -51
  303. package/dist/task/yolo.d.ts +34 -30
  304. package/dist/task/yolo.js +42 -37
  305. package/dist/workers/abstention.d.ts +21 -41
  306. package/dist/workers/abstention.js +27 -48
  307. package/dist/workers/brave-search.d.ts +4 -3
  308. package/dist/workers/brave-search.js +5 -2
  309. package/dist/workers/brave-warning.d.ts +7 -4
  310. package/dist/workers/brave-warning.js +19 -7
  311. package/dist/workers/ddg-search.d.ts +6 -6
  312. package/dist/workers/ddg-search.js +18 -12
  313. package/dist/workers/docs-cache.js +5 -2
  314. package/dist/workers/docs-chunk.d.ts +30 -37
  315. package/dist/workers/docs-chunk.js +37 -41
  316. package/dist/workers/docs-core.d.ts +28 -44
  317. package/dist/workers/docs-core.js +25 -44
  318. package/dist/workers/docs-index.js +4 -3
  319. package/dist/workers/docs-lookup.d.ts +15 -22
  320. package/dist/workers/docs-lookup.js +12 -21
  321. package/dist/workers/docs-project.d.ts +15 -9
  322. package/dist/workers/docs-project.js +17 -10
  323. package/dist/workers/docs-resolve.d.ts +19 -20
  324. package/dist/workers/docs-resolve.js +35 -32
  325. package/dist/workers/docs-retrieve.d.ts +5 -6
  326. package/dist/workers/docs-retrieve.js +18 -15
  327. package/dist/workers/exa-search.d.ts +9 -6
  328. package/dist/workers/exa-search.js +23 -12
  329. package/dist/workers/fetch-core.d.ts +13 -16
  330. package/dist/workers/fetch-core.js +23 -23
  331. package/dist/workers/focused-extractor.d.ts +13 -12
  332. package/dist/workers/focused-extractor.js +27 -19
  333. package/dist/workers/html-clean.js +24 -14
  334. package/dist/workers/http-request.d.ts +28 -20
  335. package/dist/workers/http-request.js +22 -17
  336. package/dist/workers/npm-version.d.ts +28 -11
  337. package/dist/workers/npm-version.js +24 -15
  338. package/dist/workers/phantom-imports.d.ts +15 -12
  339. package/dist/workers/phantom-imports.js +30 -24
  340. package/dist/workers/pi-worker-core.d.ts +65 -96
  341. package/dist/workers/pi-worker-core.js +93 -181
  342. package/dist/workers/pi-worker-docs.d.ts +24 -19
  343. package/dist/workers/pi-worker-docs.js +67 -76
  344. package/dist/workers/pi-worker-fetch.d.ts +7 -3
  345. package/dist/workers/pi-worker-fetch.js +27 -19
  346. package/dist/workers/pi-worker-search.js +12 -8
  347. package/dist/workers/pi-worker.d.ts +9 -4
  348. package/dist/workers/pi-worker.js +21 -14
  349. package/dist/workers/reasoning-warning.d.ts +18 -17
  350. package/dist/workers/reasoning-warning.js +22 -20
  351. package/dist/workers/research-cache.js +50 -78
  352. package/dist/workers/search-core.js +7 -5
  353. package/dist/workers/search-types.d.ts +10 -9
  354. package/dist/workers/search-types.js +9 -8
  355. package/dist/workers/session-hint.d.ts +13 -14
  356. package/dist/workers/session-hint.js +8 -9
  357. package/dist/workers/shared.d.ts +21 -25
  358. package/dist/workers/shared.js +0 -0
  359. package/dist/workers/single-read-extension.d.ts +14 -7
  360. package/dist/workers/single-read-extension.js +14 -7
  361. package/dist/workers/single-read-guard.d.ts +27 -30
  362. package/dist/workers/single-read-guard.js +36 -36
  363. package/dist/workers/typeonly-log.d.ts +12 -9
  364. package/dist/workers/typeonly-log.js +29 -33
  365. package/dist/workers/worker-channels.d.ts +15 -23
  366. package/dist/workers/worker-channels.js +15 -23
  367. package/dist/workers/worker-failure.d.ts +38 -46
  368. package/dist/workers/worker-failure.js +31 -39
  369. package/dist/workers/worker-kill.d.ts +25 -26
  370. package/dist/workers/worker-kill.js +16 -19
  371. package/dist/workers/worker-profiles.d.ts +54 -56
  372. package/dist/workers/worker-profiles.js +63 -39
  373. package/package.json +10 -8
@@ -2,8 +2,10 @@
2
2
  * /task-auto — plans a feature into a resumable list of task titles, then runs
3
3
  * each title through the existing single-task pipeline one at a time.
4
4
  *
5
- * This module currently holds the planning half (AutoDeps + planAuto). The run
6
- * loop, command handlers, and defaultDeps are added by the next task.
5
+ * The whole command lives here: the planning half (orient elicit decompose →
6
+ * cover planAuto), the run loop (runAutoLoop), the production dependency table
7
+ * (defaultDeps), and the three command handlers registerTaskAuto wires up —
8
+ * /task-auto, /task-auto-resume and /task-auto-cancel.
7
9
  */
8
10
  import { existsSync } from 'node:fs';
9
11
  import * as fsp from 'node:fs/promises';
@@ -20,7 +22,7 @@ import { drainRepairQueue, mergeRepairCandidates, planHasRepairFor, parseRepairT
20
22
  import { writeTaskFile, readTaskFile, updateTaskFrontMatter, taskFilePath, tasksDir } from './task-io.js';
21
23
  import { readTextFile } from '../shared/fs-text.js';
22
24
  import { findPhantomImports, rewritePhantomSpecifiers } from '../workers/phantom-imports.js';
23
- import { prependHint, USER_CANCELLED } from './child-runner.js';
25
+ import { isFatalChildCause, prependHint, USER_CANCELLED } from './child-runner.js';
24
26
  import { requestCancel, resetCancel, isCancelRequested, cancelCheckpoint } from './cancel-points.js';
25
27
  import { withRun, announceTerminal } from './run-bracket.js';
26
28
  import { refineExistingFilesBlock, SINGLE_READ_EXTENSION_PATH } from './phases.js';
@@ -53,7 +55,7 @@ import { findSpecDanglingArtifacts, titlesCoverArtifact, danglingMissingText, da
53
55
  import { LAUNCH_EXTRACT_PROMPT, enumerateScriptCandidates, parseScriptLines, keepGroundedScripts, appendDeclaredScripts } from './launch-contract.js';
54
56
  // Hard ceiling on clarify questions per feature. The loop is open-ended (it stops
55
57
  // when the model emits NONE), but a model that never says NONE would otherwise
56
- // barrage the user the real mx5 run asked 10, several of them redundant.
58
+ // barrage the user with redundant questions.
57
59
  const MAX_CLARIFY_QUESTIONS = 8;
58
60
  // Bounded coverage-triage rounds after decompose: judge → reprompt-with-missing
59
61
  // → judge again, at most. Two rounds so one flaky retry doesn't end the gate,
@@ -80,30 +82,32 @@ function coverageRepromptHint(missing) {
80
82
  + 'the areas above. Output every task, one "- [ ] " line each, nothing else.]');
81
83
  }
82
84
  // Deterministic distrust floor for the coverage gate. The gate's judge is the
83
- // same stochastic model as the decompose call it guards, and live (mx5 2026-07-08,
84
- // A/B N=10) it rubber-stamps a 1-task plan for an 18KB spec 3/10 times always
85
- // as the bare "COVERAGE: COMPLETE" line, which is byte-identical to a legitimate
86
- // verdict, so the rubber-stamp is NOT detectable from the judge's output. The
85
+ // same stochastic model as the decompose call it guards, and it will sometimes
86
+ // rubber-stamp a one-task plan for a whole design documentas the bare
87
+ // "COVERAGE: COMPLETE" line, byte-identical to a legitimate verdict, so the
88
+ // rubber-stamp is NOT detectable from the judge's output. The
87
89
  // distrust signal must come from the input: a plan this small for a spec this
88
- // large is near-certainly the known degenerate-decompose flake (healthy runs on
89
- // the same inputs produce 10–30 titles). The floor only ever forces a REGENERATION
90
- // it never rejects a plan on count alone (the v0.13.34 objection), so a model
91
- // that insists twice still ships its small plan, with a warning.
90
+ // large is near-certainly a degenerate generation rather than a real plan. The
91
+ // floor only ever forces a REGENERATION it never rejects a plan on count alone,
92
+ // so a model that insists twice still ships its small plan, with a warning. Nine
93
+ // tests pin the behaviour, including that a still-suspect plan ships and that a
94
+ // shorter retry keeps the original list.
92
95
  const SUSPECT_PLAN_MAX_TITLES = 2;
93
96
  const SUSPECT_PLAN_MIN_SPEC_CHARS = 4000;
94
97
  /**
95
98
  * Extra retries granted when the plan is EMPTY rather than merely small. One
96
- * hinted retry heals a small-but-nonempty plan reliably; an empty generation is a
97
- * harder fault and was measured recurring back-to-back (2026-07-28 smoke: 13 empty
98
- * draws across 24 reps of a 20KB spec, including two in a row in one rep).
99
+ * hinted retry heals a small-but-nonempty plan; an empty generation is a harder
100
+ * fault that can recur back-to-back, and falling through with zero titles aborts
101
+ * the whole run rather than merely shipping a thin plan so it is worth more
102
+ * than one roll of the dice.
99
103
  */
100
104
  const EMPTY_PLAN_RETRIES = 2;
101
105
  /**
102
106
  * An empty list is NEVER a valid decomposition of any feature request, at any spec
103
- * size. It used to escape this guard entirely the old predicate opened with
104
- * `titles.length > 0`, so zero titles was not "suspect", the suspect-retry never
105
- * fired, the coverage loop broke immediately on `titles.length === 0`, and the run
106
- * aborted with "no tasks produced from the feature". A single degenerate
107
+ * size. A predicate opening with `titles.length > 0` lets zero titles escape
108
+ * entirely: not "suspect", so the suspect-retry never fires, the coverage loop
109
+ * breaks immediately on `titles.length === 0`, and the run aborts with "no tasks
110
+ * produced from the feature". A single degenerate
107
111
  * generation killed the whole run with no retry, which is the opposite of how the
108
112
  * same fault is treated one title higher.
109
113
  */
@@ -129,41 +133,43 @@ const MENTION_RE = /(?:^|\s)@([^\s]+)/g;
129
133
  // [^\s]+ above would otherwise swallow into the path. Left unstripped, the
130
134
  // resulting "design.md," resolves to no file, expansion is silently skipped, and
131
135
  // the planner reasons over a one-line "Implement @design.md" with NO spec inline
132
- // → it fabricates generic questions/tasks the spec never called for (validated:
133
- // a stray comma turned a 32KB design into a contentless prompt). None of these
134
- // chars are legitimate trailing characters of a referenced doc path.
136
+ // → it fabricates generic questions and tasks the spec never called for.
137
+ //
138
+ // Measured against a real file: the greedy token from "Implement @design.md,
139
+ // reuse the parser" is `design.md,`, which does not exist; stripped, `design.md`
140
+ // does. None of these chars are legitimate trailing characters of a doc path.
135
141
  const MENTION_TRAILING_PUNCT = /[.,;:!?)\]}>"']+$/;
136
142
  /** The cleaned path token of an @-mention: greedy match minus trailing prose punctuation. */
137
143
  function mentionPath(token) {
138
144
  return token.replace(MENTION_TRAILING_PUNCT, '');
139
145
  }
140
- /**
141
- * Fire-and-forget debug line for the PLAN phase (clarify/decompose), which runs
142
- * before any task file — hence any per-task `TASK_XXXX-debug.log` — exists. Writes
143
- * to `.pi-tasks/plan-debug.log`; the `*-debug.log` suffix keeps it grep-compatible
144
- * with the per-task logs. Never throws (mkdir + append are best-effort).
145
- *
146
- * Every call site here records a plan DECISION (how many titles a round produced,
147
- * whether a retry was adopted, which clarify answer was auto-resolved), so all of
148
- * them are `'event'` — this file carries no model chatter and survives at the
149
- * default level. It is also the only channel the plan phase has: it runs before
150
- * any task file, hence any `TASK_NNNN-debug.log`, exists.
151
- */
152
146
  /**
153
147
  * Every plan-debug write not yet on disk, chained.
154
148
  *
155
149
  * Fire-and-forget is right for production — a plan must never wait on its own
156
- * trail — but it leaves nothing to synchronise on, and the twelve tests that
157
- * read `plan-debug.log` back were racing the append that writes it. They failed
158
- * intermittently on ENOENT, ~4ms in, at a rate that moved with how many other
159
- * files the suite was running beside them. Chaining also serialises concurrent
160
- * appends, which is what keeps a line whole.
150
+ * trail — but it leaves nothing to synchronise on, so a test that reads
151
+ * `plan-debug.log` back races the append that writes it and fails on ENOENT.
152
+ * Chaining gives {@link flushPlanDebug} something to await, and it also
153
+ * serialises concurrent appends, which is what keeps a line whole.
161
154
  */
162
155
  let planDebugChain = Promise.resolve();
163
156
  /** Wait for every plan-debug line written so far to reach disk. Tests only. */
164
157
  export function flushPlanDebug() {
165
158
  return planDebugChain;
166
159
  }
160
+ /**
161
+ * Fire-and-forget debug line for the PLAN phase (clarify/decompose). It is the
162
+ * only trail that phase has: planning runs before any task file exists, so there
163
+ * is no per-task `TASK_NNNN-debug.log` to write into yet. This goes to
164
+ * `.pi-tasks/plan-debug.log`, whose `*-debug.log` suffix matches the pattern
165
+ * debug-log.ts documents, so one grep still finds every log. Never throws — the
166
+ * mkdir and the append are both best-effort.
167
+ *
168
+ * Every call site records a plan DECISION (how many titles a round produced,
169
+ * whether a retry was adopted, which clarify answer was auto-resolved), so all of
170
+ * them are `'event'` and survive at the default level. No model chatter reaches
171
+ * this file.
172
+ */
167
173
  function logPlanDebug(cwd, msg) {
168
174
  if (!shouldLogDebug('event', debugLogLevel()))
169
175
  return;
@@ -205,8 +211,8 @@ async function triageClarifyQuestion(deps, cwd, featureForModel, existingFilesBl
205
211
  // manifest/config content) so a "scaffold/create/from scratch" question is
206
212
  // auto-resolved as an in-place UPDATE that PRESERVES what is on disk —
207
213
  // instead of "greenfield, from scratch", which the spec-only triage emitted
208
- // 13/15 of the time and would mint a destructive decompose decision that can
209
- // outrank refine's preserve directive (A/B live: 2/15 → 14/15 preserve).
214
+ // most of the time, minting a destructive decompose decision that can
215
+ // outrank refine's preserve directive.
210
216
  // Empty (greenfield repo / orientation off) → byte-identical to before.
211
217
  const source = existingFilesBlock.length > 0 ?
212
218
  `${existingFilesBlock}\n\n${featureForModel}`
@@ -293,19 +299,20 @@ export async function readableMentions(cwd, feature) {
293
299
  const DECISIONS_RE = /\s*\[decisions:\s*(.+?)\]\s*$/i;
294
300
  /**
295
301
  * Thread the feature's spec references AND any per-task decisions into every
296
- * decomposed task title. A title is ALL a per-task pipeline ever sees, so both
297
- * the design doc the feature pointed at and the user's clarification choices have
298
- * to ride along or they're invisible downstream — this is how an "Implement
299
- * @design.md" run built a generic `posts` table the spec never mentioned, and how
300
- * a "do not use vite" clarification got silently overridden by the doc's own
301
- * vite.config.ts.
302
+ * decomposed task title. A title is ALL a per-task pipeline ever sees, so both the
303
+ * design doc the feature pointed at and the user's clarification choices have to
304
+ * ride along or they are invisible downstream — a task that cannot see the doc
305
+ * invents its own schema, and one that cannot see "do not use vite" is overridden
306
+ * by whatever the doc's own config says.
302
307
  *
303
- * Precedence is the crux: a clarification is a CORRECTION to a (possibly stale)
304
- * spec doc, so the decisions clause is marked as overriding the doc, while the doc
305
- * stays authoritative for everything the decisions don't touch. Decompose scopes
306
- * each decision to the task(s) it governs, so most titles carry none. No readable
307
- * refs and no decisions → title unchanged, so a doc-less /task-auto behaves
308
- * exactly as before.
308
+ * Precedence is the crux: a clarification is a CORRECTION to a possibly stale spec
309
+ * doc, so the decisions clause is marked as overriding the doc while the doc stays
310
+ * authoritative for everything the decisions do not touch. The emitted order puts
311
+ * decisions first, then the spec ref. Decompose scopes each decision to the tasks
312
+ * it governs, so most titles carry none.
313
+ *
314
+ * Run: with no refs and no decisions the title comes back unchanged, and
315
+ * re-threading an already-threaded list is a no-op.
309
316
  */
310
317
  export function attachSpecRefs(titles, refs) {
311
318
  const list = refs.map(r => '@' + r).join(' ');
@@ -329,15 +336,15 @@ export function attachSpecRefs(titles, refs) {
329
336
  * Build the refine scope fence for step `currentIndex` of an N-step /task-auto
330
337
  * plan. Every per-step pipeline only ever sees its own title, so without this the
331
338
  * refine phase — told "the task title is only a pointer into that spec; follow the
332
- * spec" — re-expands the whole referenced design into one task (a real run
333
- * implemented all 24 steps under step 1). The fence lists the sibling steps by
334
- * number and forbids touching anything they own, so refine bounds this step's
335
- * slice. Validated on the local model: with the fence, refine's CONSTRAINTS gained
336
- * an explicit per-step deferral list and tool calls dropped 27→11.
339
+ * spec" — re-expands the whole referenced design into a single task, implementing
340
+ * the entire plan under step one. The fence lists the sibling steps by number and
341
+ * forbids touching anything they own, so refine bounds this step's slice.
337
342
  *
338
343
  * The plan listing strips the threaded "| decisions … | spec …" tail from each
339
- * title (keeps the human-readable head) so the model reads clean step names. The
340
- * authoritative spec ref still rides on THIS step's own title via attachSpecRefs.
344
+ * title and keeps the human-readable head, so the model reads clean step names,
345
+ * and it marks the current one "(THIS STEP)" both confirmed by building a fence
346
+ * over threaded titles. The authoritative spec ref still rides on THIS step's own
347
+ * title via attachSpecRefs.
341
348
  */
342
349
  export function buildScopeFence(titles, currentIndex) {
343
350
  const n = titles.length;
@@ -364,8 +371,11 @@ export function buildScopeFence(titles, currentIndex) {
364
371
  * a queued root-cause repair. A repair title ("repair test/teardown.ts: …") reads
365
372
  * to refine like any other feature step, and refine's job is to expand a title into
366
373
  * a full spec — which is exactly how "repair the teardown" becomes "overhaul the
367
- * test infrastructure" (the /task-auto drift lesson). The extra fence pins the one
368
- * editable file and pins VERIFY to the command the defect was failing.
374
+ * test infrastructure". The extra fence pins the one editable file, and pins VERIFY
375
+ * to the failing command WHEN the title carries one: `extractFailingCommand` reads
376
+ * a backticked runner command out of the defect text, so a defect quoting
377
+ * `bun run test` yields it while a plain-prose defect yields undefined and only the
378
+ * file pin applies.
369
379
  */
370
380
  function buildStepFence(titles, currentIndex) {
371
381
  const base = buildScopeFence(titles, currentIndex);
@@ -379,15 +389,17 @@ function buildStepFence(titles, currentIndex) {
379
389
  * repair step per accused FILE, spliced in directly after the step that just
380
390
  * finished.
381
391
  *
382
- * Three bounds, all mandatory (mx5 run 14 item 5 gray areas):
383
- * - DEDUP by file — run 14's two `test/teardown.ts` debts must yield ONE repair
392
+ * Three bounds, all mandatory:
393
+ * - DEDUP by file — two debts naming the same file must yield ONE repair
384
394
  * step, not two. mergeRepairCandidates collapses the drained queue, and
385
395
  * planHasRepairFor rejects a file the plan already carries a repair for.
386
396
  * - CAP 1 per file per RUN — planHasRepairFor counts CHECKED-OFF entries too, so
387
397
  * a repair step that itself failed is never re-spawned; it lands in the
388
398
  * accept-debt ledger like any other task. That is what stops a repair loop.
399
+ * Both bounds run as described: three candidates over two files merge to two,
400
+ * and a plan carrying an already-`[x]` repair for a file still answers true.
389
401
  * - MONOTONIC — insertTaskAfter only splices; no existing entry is rewritten,
390
- * reordered or dropped (the run-12 replacement lesson).
402
+ * reordered or dropped.
391
403
  *
392
404
  * Best-effort throughout: a fault here must never fail the run that produced the
393
405
  * finding — the debt is already durably recorded either way.
@@ -431,7 +443,7 @@ export async function orientFeature(cwd, feature, deps) {
431
443
  // clarify/decompose ever see it. Layer A only rewrites the per-task `refined`
432
444
  // text — which is DOWNSTREAM of here: clarify is the first phase and runs on
433
445
  // this raw inline, so the doc's affirmative `bun:sql` is parroted straight into
434
- // the very first clarifying question ("instantly bun:sql is back"). Apply the
446
+ // the very first clarifying question. Apply the
435
447
  // same deterministic, no-LLM strike at the single point that feeds both planning
436
448
  // children. Silent + no-op when nothing is flagged or the runtime's types aren't
437
449
  // installed.
@@ -451,7 +463,7 @@ export async function orientFeature(cwd, feature, deps) {
451
463
  taskId: '',
452
464
  signal: new AbortController().signal
453
465
  }).catch(() => '');
454
- // Requirement extraction (mx5 run 11, goal A): grounded requirement units,
466
+ // Requirement extraction: grounded requirement units,
455
467
  // extracted from whatever structure the spec has, BEFORE decompose — they ride
456
468
  // into the decompose prompt as a ledger (structure-mirroring can't discharge
457
469
  // them) and drive the per-requirement coverage accounting below.
@@ -462,8 +474,8 @@ export async function orientFeature(cwd, feature, deps) {
462
474
  try {
463
475
  // Recall floor: the obligation-marked passages ride into the prompt as a
464
476
  // checklist, and a marked passage that produced NO quote is hard evidence
465
- // for one forced re-extraction (measured live: 1/5 extractions missed the
466
- // entire marked testing section without this).
477
+ // for one forced re-extraction: without it an extraction can miss an
478
+ // entire marked section.
467
479
  const passages = enumerateObligationPassages(featureForModel);
468
480
  const extractOnce = async (hint) => keepGroundedRequirements(parseRequirementLines(await deps.runChild('requirement-extract', '', prependHint(hint, REQUIREMENT_EXTRACT_PROMPT(featureForModel, passages)))), featureForModel);
469
481
  reqEntries = await extractOnce(null);
@@ -475,20 +487,30 @@ export async function orientFeature(cwd, feature, deps) {
475
487
  // Union of both grounded passes (keepGrounded dedupes).
476
488
  reqEntries = keepGroundedRequirements([...reqEntries, ...retry], featureForModel);
477
489
  }
478
- // Bound with marked-passage priority a plain first-N cap truncates the
479
- // doc's tail sections (measured live: an eager model fills 40 top-down).
490
+ // Bound with marked-passage priority. A plain first-N cap truncates the
491
+ // doc's tail sections, because an eager extraction fills the budget from the
492
+ // top down and never reaches them.
480
493
  reqEntries = capRequirements(reqEntries, passages, featureForModel);
481
494
  logPlanDebug(cwd, `requirement extraction: ${reqEntries.length} grounded requirement(s) kept`);
482
495
  }
483
- catch {
484
- // best-effort channel
496
+ catch (e) {
497
+ // Best-effort covers a child that answered badly. It must NOT cover a user
498
+ // ESC or a dead backend: planning would continue on an EMPTY ledger, moving
499
+ // the granularity floor and shipping a degraded plan instead of a cancel or
500
+ // a failure. Same rule as verify-resolution.ts.
501
+ if (isFatalChildCause(e))
502
+ throw e;
503
+ // Best-effort, but not silent: nothing else records a guard kill here.
504
+ logPlanDebug(cwd, `requirement extraction: skipped — ${e.message}`);
485
505
  }
486
- // Granularity floor (mx5 Jul 25 vs Jul 27): the plan's task COUNT was being set
487
- // by an auto-resolved clarify line the user never saw the same spec planned
488
- // into 41 tasks one day and 11 the next, with identical code. Derive the floor
489
- // from the requirements a task can own, so an unreviewable "one task per
506
+ // Granularity floor: without it the plan's task COUNT is set by an
507
+ // auto-resolved clarify line the user never sees, so the same spec and the same
508
+ // code can plan coarse one run and fine the next. Derive the floor from the
509
+ // requirements a task can own instead, so an unreviewable "one task per
490
510
  // milestone" decision cannot collapse the plan; it also gates whether the
491
- // plan-shape fork below is the host's to answer at all. 0 ownable ⇒ no channel.
511
+ // plan-shape fork below is the host's to answer at all. Measured:
512
+ // granularityFloor is 0 for three or fewer ownable requirements — no channel —
513
+ // and roughly half the count above that.
492
514
  const ownableRequirements = reqEntries.filter(e => !isCrossCuttingRequirement(e.quote)).length;
493
515
  const coarseFloor = granularityFloor(ownableRequirements);
494
516
  if (coarseFloor > 0) {
@@ -509,8 +531,9 @@ export async function orientFeature(cwd, feature, deps) {
509
531
  * settles is auto-resolved and never shown — only genuine open forks reach the user.
510
532
  * The model emits NONE when nothing remains.
511
533
  *
512
- * The ONLY stage that talks to the user, and so the only one that can be dismissed:
513
- * `null` means the user cancelled and the cancellation has already been announced.
534
+ * The ONLY stage that ASKS the user anything, and so the only one that can be
535
+ * dismissed the others only notify, which cannot be. `null` means the user
536
+ * cancelled and the cancellation has already been announced.
514
537
  * Every other outcome is a transcript, possibly empty.
515
538
  */
516
539
  export async function elicitClarifications(ctx, cwd, deps, oriented) {
@@ -527,17 +550,18 @@ export async function elicitClarifications(ctx, cwd, deps, oriented) {
527
550
  // strikes out (can't produce anything novel) we stop instead of barraging the
528
551
  // user with the same decision worded N ways. Also caps the absolute count.
529
552
  // The generate → parse → pick → dedupe → re-prompt state machine is
530
- // task/question-source.ts, shared with the plan session. Clarify used to write
531
- // its own, and it had drifted from the sibling in five ways every one of
532
- // which the shared source closes for free: `parsed[0]` became `pickQuestion`
533
- // (an analysis note is no longer shown as the question, and the SUGGESTED
534
- // attached further down is no longer lost), and an unparseable reply now buys
535
- // one format re-prompt instead of ending the whole clarify and decomposing
536
- // the feature with ZERO clarifications — on a formatting slip.
553
+ // task/question-source.ts, shared with the plan session. A second copy here
554
+ // would drift from the sibling, and the shared source buys two things: it
555
+ // PICKS a question rather than taking the first parsed line, so an analysis
556
+ // note is never shown as the question and its SUGGESTED is not lost; and an
557
+ // unparseable reply costs one format re-prompt instead of ending clarify
558
+ // outright and decomposing the feature with ZERO clarifications.
537
559
  //
538
- // Of plan's three quality rules only the DEFERRAL guard crosses. The other two
539
- // cost an extra child call every time they fire, and clarify is the most A/B'd
540
- // path here; moving them is its own experiment. See CLARIFY_QUALITY_RULES.
560
+ // Of plan's three quality rules only the DEFERRAL guard crosses. Checked:
561
+ // PLAN_QUALITY_RULES holds three ('no SUGGESTED', 'SUGGESTED deferred the
562
+ // decision', 'fork-shaped question with no ALT') and CLARIFY_QUALITY_RULES
563
+ // holds exactly the middle one. The other two cost an extra child call every
564
+ // time they fire, so moving them is its own change to make and measure.
541
565
  const source = makeQuestionSource({
542
566
  generate: hint => deps.runChild('auto-clarify', 'read', prependHint(hint, AUTO_CLARIFY_PROMPT(featureForModel, transcript.forGenerator()))),
543
567
  formatHint: PLAN_FORMAT_HINT,
@@ -554,9 +578,10 @@ export async function elicitClarifications(ctx, cwd, deps, oriented) {
554
578
  // for the editable default and the persisted file.
555
579
  const shownQ = renderInlineMarkdown(question, theme);
556
580
  const plainQ = drawn.plain;
557
- // PLAN SHAPE is the host's call, not the triage's (mx5 41→11 tasks on the
558
- // same spec, same base commit, same code see decompose-granularity.ts).
559
- // The triage answers this fork for itself in 8/8 live reps and stamps it
581
+ // PLAN SHAPE is the host's call, not the triage's the same spec off the
582
+ // same base commit can plan far coarser or finer depending on this one
583
+ // answer (see decompose-granularity.ts).
584
+ // The triage answers this fork for itself every time and stamps it
560
585
  // "already settled by the spec" while the spec settles no such thing, so the
561
586
  // single most load-bearing decision in a run was an invisible coin flip.
562
587
  // Answer it deterministically instead: same channel, same transcript, but a
@@ -610,9 +635,9 @@ export async function elicitClarifications(ctx, cwd, deps, oriented) {
610
635
  */
611
636
  export async function decomposePlan(cwd, deps, oriented, clarifications) {
612
637
  const { featureForModel, reqEntries, ownableRequirements, coarseFloor } = oriented;
613
- // Tests-in-the-same-change cadence (mx5 run 14, PROMPT item 6): when the
614
- // decisions mandate it, a whole-project batch test task contradicts them
615
- // run 14 shipped one anyway (TASK_0037, 4.7h, yolo-accepted FAIL) because
638
+ // Tests-in-the-same-change cadence: when the
639
+ // decisions mandate it, a whole-project batch test task contradicts them, and
640
+ // one still ships because
616
641
  // decompose mirrors the spec's milestone shape. The decisions channel
617
642
  // OVERRIDES the spec doc, so this resolves toward the decision without asking.
618
643
  const noBatchTests = mandatesTestsInSameChange(clarifications, featureForModel);
@@ -622,7 +647,7 @@ export async function decomposePlan(cwd, deps, oriented, clarifications) {
622
647
  }
623
648
  // decompose
624
649
  const decomposePrompt = AUTO_DECOMPOSE_PROMPT(featureForModel, clarifications, buildRequirementsLedger(reqEntries), noBatchTests);
625
- // Parse + FIDELITY RECONCILIATION (mx5 run 11, goal B): ground each title's
650
+ // Parse + FIDELITY RECONCILIATION: ground each title's
626
651
  // [source: "…"] citation against the doc, strip the clause, and re-attach any
627
652
  // `+`-joined constraint fragment the paraphrased title dropped (the silently
628
653
  // stripped "+ tests" class). Applied to EVERY decompose output — initial,
@@ -639,7 +664,7 @@ export async function decomposePlan(cwd, deps, oriented, clarifications) {
639
664
  // Batch-test ban (item 6): drop or scope a whole-project "write all the
640
665
  // tests" task. Identity unless the cadence decision is present, and the
641
666
  // sweep replacement re-grounds every requirement the drop would cost — so
642
- // planned coverage cannot fall (run 12's lesson).
667
+ // planned coverage cannot fall.
643
668
  const debatched = rewriteBatchTestPlan(plan.titles, clarifications, featureForModel, reqEntries.map(e => e.quote), isCrossCuttingRequirement);
644
669
  for (const a of debatched.actions) {
645
670
  logPlanDebug(cwd, `batch test task ${a.kind} (tests-in-same-change decision): "${a.title}"`
@@ -654,7 +679,7 @@ export async function decomposePlan(cwd, deps, oriented, clarifications) {
654
679
  logPlanDebug(cwd, `decompose produced ${planTitles.length} title(s)`);
655
680
  // BRACES for the floor: the prompt clause alone is a preference the model can
656
681
  // ignore, so a plan under the floor is sent back ONCE to be split (never
657
- // regenerated — a fresh roll can drop a covered area, mx5 run 12). Longer plan
682
+ // regenerated — a fresh roll can drop a covered area). Longer plan
658
683
  // wins; a still-coarse plan falls through to the coverage judge as before, so
659
684
  // this can never block planning.
660
685
  if (isTooCoarse(planTitles.length, coarseFloor)) {
@@ -668,8 +693,8 @@ export async function decomposePlan(cwd, deps, oriented, clarifications) {
668
693
  }
669
694
  // Distrust floor (see isSuspectPlan): a ≤2-title plan for a multi-KB spec is
670
695
  // regenerated once BEFORE the judge runs — the judge cannot be trusted to
671
- // catch it (3/10 live false-pass) and a hinted retry heals it reliably
672
- // (5/5 live). Longer list wins; a still-suspect plan falls through to the
696
+ // catch it, and a hinted retry heals it reliably.
697
+ // Longer list wins; a still-suspect plan falls through to the
673
698
  // judge loop as before, so this never blocks planning.
674
699
  // An EMPTY plan gets extra attempts (see EMPTY_PLAN_RETRIES): falling through
675
700
  // with zero titles aborts the whole run, so one roll of the dice is not enough.
@@ -717,17 +742,16 @@ export async function coverPlan(ctx, cwd, deps, oriented, clarifications, decomp
717
742
  const { featureForModel, reqEntries } = oriented;
718
743
  const { decomposePrompt, parsePlan } = decomposed;
719
744
  let planTitles = decomposed.planTitles;
720
- // Coverage gate: a stochastic degenerate completion (live mx5: ONE task +
721
- // natural EOS for an 18KB design doc) is nonempty, so the length guard below
745
+ // Coverage gate: a degenerate completion ONE task and a natural EOS for a
746
+ // whole design document is nonempty, so the length guard below
722
747
  // never fires and the whole run "completes" after one task. Judge the list
723
748
  // against the feature with a no-tools child; on INCOMPLETE, re-run decompose
724
749
  // with the missing areas as a hint. Best-effort so a triage fault never blocks
725
750
  // planning (mirrors triageClarifyQuestion).
726
751
  //
727
- // Two hard-won invariants (mx5 run 12: a complete full-stack plan 31
728
- // requirements mapped, frontend pages present was overwritten by a
729
- // backend-only regeneration and shipped with only a toast, driven by 3 NEGATIVE
730
- // requirements no task could own that kept the verdict INCOMPLETE forever):
752
+ // Two invariants. Without them a complete full-stack plan is overwritten by a
753
+ // narrower regeneration, driven by a handful of NEGATIVE requirements that no
754
+ // task can own and that therefore keep the verdict INCOMPLETE forever:
731
755
  // • MONOTONIC replacement (coverage-loop.ts): a retry that DROPS a requirement
732
756
  // the current plan already owns is REJECTED, never adopted. Coverage can
733
757
  // only hold or grow across rounds — a worse regeneration can no longer
@@ -759,8 +783,8 @@ export async function coverPlan(ctx, cwd, deps, oriented, clarifications, decomp
759
783
  // numbers. Live (Qwen3.6-27B) the model over-credits ownership, mapping a
760
784
  // "--json output" requirement to a generic "scaffold + argument parser"
761
785
  // task, so a plan with no --json task still "owned" it and the drop guard
762
- // went blind (treatment 1/5). Grounding the drop-signal in the titles the
763
- // model can't fake takes it back to 5/5. The model map still drives Fix A's
786
+ // went blind. Grounding the drop-signal in the titles the model cannot
787
+ // fake restores it. The model map still drives Fix A's
764
788
  // cross-cutting/unmapped accounting below (that only affects reprompt
765
789
  // aggressiveness, which the monotonic guard now backstops).
766
790
  const covered = groundedCoverage(reqEntries.map(e => e.quote), titles, isCrossCuttingRequirement);
@@ -796,9 +820,9 @@ export async function coverPlan(ctx, cwd, deps, oriented, clarifications, decomp
796
820
  // `best` is both the plan the next round reprompts FROM and the plan that
797
821
  // ships — kept identical because adoption is monotone (see coverage-loop.ts).
798
822
  //
799
- // It is also the ONLY handle on the accounting. There used to be a second,
800
- // `let accounting`, carried alongside see the ScoredPlan doc comment for the
801
- // requirement-to-wrong-task bug that cost us.
823
+ // It is also the ONLY handle on the accounting. A second one carried
824
+ // alongside is how a requirement gets attached to the wrong task see the
825
+ // ScoredPlan doc comment.
802
826
  // The record, and the decisions it makes: task/plan-rounds.ts. This was five
803
827
  // locals threaded by closure through a ~90-line loop, plus a
804
828
  // snapshot-before-overwrite pair that existed only because the bonus-round
@@ -859,8 +883,8 @@ export async function coverPlan(ctx, cwd, deps, oriented, clarifications, decomp
859
883
  const round = rounds.round();
860
884
  planTitles = best.plan.titles;
861
885
  // Exhausted still INCOMPLETE: the best plan ships (the gate is best-effort), but
862
- // silently shipping a KNOWN-gapped plan is how mx5 run 5 lost its whole test
863
- // suite — tell the user what is still uncovered.
886
+ // silently shipping a KNOWN-gapped plan is how a run loses a whole area of
887
+ // work — tell the user what is still uncovered.
864
888
  const unresolvedMissing = rounds.unresolved();
865
889
  if (unresolvedMissing !== null) {
866
890
  logPlanDebug(cwd, `decompose-coverage exhausted ${round} round(s) still INCOMPLETE — missing: `
@@ -883,14 +907,14 @@ export async function planAuto(ctx, cwd, feature, deps) {
883
907
  const clarifications = await elicitClarifications(ctx, cwd, deps, oriented);
884
908
  if (clarifications === null)
885
909
  return null; // dismissed; already announced
886
- // Artifact-production closure, plan side (mx5 run 13, PROMPT 2): runtime
910
+ // Artifact-production closure, plan side: runtime
887
911
  // files the spec REFERENCES (server snippets, prose "serve the built
888
912
  // index.html") that neither its file tree, its parsed build outputs, nor the
889
913
  // existing scaffold produce. Sentence-grounded coverage credited the SERVING
890
914
  // side and reported "0 unowned" while nothing ever CREATED the file — so
891
915
  // these ride the coverage loop's `missing` list as unowned areas until some
892
916
  // task title claims the artifact (grounded in titles, which the coverage-map
893
- // model cannot fake — the run-12 lesson). Deterministic and best-effort.
917
+ // model cannot fake — the lesson). Deterministic and best-effort.
894
918
  let specDangling = [];
895
919
  try {
896
920
  specDangling = findSpecDanglingArtifacts(featureForModel, rel => existsSync(path.join(cwd, rel)));
@@ -910,16 +934,16 @@ export async function planAuto(ctx, cwd, feature, deps) {
910
934
  const planTitles = covered.planTitles;
911
935
  // Carry what no single task owns (goal A(b)/(c)): cross-cutting requirements
912
936
  // become `.pi-tasks/requirements.md`, injected VERBATIM into every task's
913
- // refine/compose (run 11: §10's test-first cadence had no carrier the "spec
914
- // is authoritative" pointer recovered it in 1 of ~6 tasks; content travels,
915
- // pointers don't). Requirements still unmapped after the rounds are carried
937
+ // refine/compose. A rule stated only in the spec body has no carrier, and a
938
+ // "spec is authoritative" pointer recovers it in a minority of tasks: content
939
+ // travels, pointers do not. Requirements still unmapped after the rounds are carried
916
940
  // too — marked — and recorded user-visibly in the plan file, never dropped.
917
941
  //
918
942
  // #1: the holistic-judge missing areas are carried as a THIRD channel. They are
919
943
  // areas requirement-extraction never captured as a tracked entry (so the
920
944
  // grounded accounting is structurally blind to them), seen only by the judge —
921
- // exactly the class that, having no carrier, was warned-about then dropped (mx5
922
- // 2026-07-16, §10 test-infra). Carried independent of `accounting` so a mapping
945
+ // exactly the class that, having no carrier, is warned about and then
946
+ // dropped. Carried independent of `accounting` so a mapping
923
947
  // fault (accounting === null) can't strand them either.
924
948
  const carriedCrossCutting = best.accounting?.crossCutting ?? [];
925
949
  const carriedUnmapped = best.accounting?.unmapped ?? [];
@@ -959,7 +983,7 @@ export async function planAuto(ctx, cwd, feature, deps) {
959
983
  // empty-plan path the plan is discarded one line later, so running them first
960
984
  // burned two model calls and left contracts.md / launch-contract.md carrying
961
985
  // facts for a run that never produced a task.
962
- // Cross-slice contract registry (mx5 run 8, F3): now that the plan is settled,
986
+ // Cross-slice contract registry: now that the plan is settled,
963
987
  // extract the interface facts MORE THAN ONE slice must agree on — endpoint paths,
964
988
  // exported signatures, file layouts, env var names the DESIGN pins — into a
965
989
  // run-level artifact each downstream refine/compose/verify reads. The extraction
@@ -979,12 +1003,12 @@ export async function planAuto(ctx, cwd, feature, deps) {
979
1003
  ground: emitted => keepGroundedContracts(emitted, featureForModel),
980
1004
  append: appendContracts
981
1005
  });
982
- // Launch contract (mx5 run 10 item 4): extract the package/build SCRIPTS the design
1006
+ // Launch contract: extract the package/build SCRIPTS the design
983
1007
  // declares the project must expose (`migrate`/`seed` fell through decompose and
984
1008
  // shipped missing, unchecked). Each emitted name is re-grounded against the design
985
1009
  // (keepGroundedScripts — kept only if the design backticks it), so the final gate's
986
1010
  // manifest diff can never false-flag a hallucinated script. Recall is mechanical
987
- // (mx5 run 11): enumerateScriptCandidates hands the child every backticked
1011
+ //: enumerateScriptCandidates hands the child every backticked
988
1012
  // script-shaped token near the word "script" as a checklist, so a script declared
989
1013
  // far from the design's summary list (`test:ct` in §2 vs §9's five) can't be
990
1014
  // missed by a weak model's recall — the child classifies, it no longer recalls.
@@ -1001,10 +1025,10 @@ export async function planAuto(ctx, cwd, feature, deps) {
1001
1025
  append: appendDeclaredScripts
1002
1026
  });
1003
1027
  // Persist the TASK-MAPPED requirements keyed by the (spec-ref-attached) title
1004
- // each task will carry (mx5 run 16: only cross-cutting entries travelled;
1005
- // the 33 mapped ones shaped the title list and vanished TASK_0008 narrowed
1006
- // §9's "serves `/api` + static `dist/`" out of its spec with nothing to stop
1007
- // it). Inert until the owned-requirements injection is wired into the phase
1028
+ // each task will carry. With only cross-cutting entries travelling, the
1029
+ // mapped ones shape the title list and then vanish, and a task can narrow a
1030
+ // requirement out of its own spec with nothing to stop it.
1031
+ // Inert until the owned-requirements injection is wired into the phase
1008
1032
  // prompts; recorded regardless so the plan's mapping is auditable per run.
1009
1033
  if (best.accounting && best.accounting.mapped.length > 0) {
1010
1034
  await writeOwnedRequirements(cwd, best.accounting.mapped
@@ -1042,7 +1066,7 @@ export async function planAuto(ctx, cwd, feature, deps) {
1042
1066
  *
1043
1067
  * The grounding step is the reason this shape exists rather than a plain child
1044
1068
  * call. A child asked for interface facts will paraphrase and occasionally invent
1045
- * them (mx5 run 8, F3), and an invented fact in a run-level registry is read as
1069
+ * them, and an invented fact in a run-level registry is read as
1046
1070
  * authoritative by every downstream refine/compose/verify. So nothing the child
1047
1071
  * says is trusted: `ground` re-checks each emitted line against the design text
1048
1072
  * host-side, and only substrings survive.
@@ -1084,11 +1108,15 @@ function defaultDeps(ctx, cwd, signal, title) {
1084
1108
  const status = new ChildStatus({ parentContextWindow });
1085
1109
  const phaseDeps = {
1086
1110
  cwd,
1111
+ // No task file, so appendLoopEvent swallows its ENOENT. Its docblock
1112
+ // allows that because "the kill is already reported through the debug
1113
+ // log" — which is why logDebug below is not optional here.
1087
1114
  taskId: '',
1088
1115
  signal,
1089
- // IN-RUN thrash guard for the planning children (mx5-n 2026-08-14: a
1090
- // decompose child re-read DESIGN/marketplace.html until it filled a
1091
- // 120k window, and ran 16m23s without returning). Every planning child
1116
+ logDebug: msg => logPlanDebug(cwd, msg),
1117
+ // IN-RUN thrash guard for the planning children: without it a decompose
1118
+ // child can re-read its design document until it fills the whole context
1119
+ // window, and never return. Every planning child
1092
1120
  // gets its source doc INLINED in its prompt, so a second read of a file
1093
1121
  // it has already opened can only be thrash — which makes the read-once
1094
1122
  // block safe here in a way it is not for a phase that must explore.
@@ -1128,14 +1156,14 @@ function defaultDeps(ctx, cwd, signal, title) {
1128
1156
  runFinalIntegrationGate(cwd2, { planText, signal })
1129
1157
  : Promise.resolve({ ok: true, reason: 'disabled' }),
1130
1158
  // Uncommitted paths, for the stranded-sub-fix handling around the final-gate
1131
- // picker (mx5 run 13 PROMPT 4 item 3). Every task is committed by the time
1159
+ // picker. Every task is committed by the time
1132
1160
  // the gate runs, so whatever is dirty here belongs to the fix pass.
1133
1161
  pendingChanges: async (cwd2) => {
1134
1162
  const changes = await collectTreeChanges(cwd2, signal);
1135
1163
  return [...changes.modified, ...changes.added, ...changes.deleted].sort();
1136
1164
  },
1137
1165
  // Re-derive the debt ledger against the FINAL tree after a converged
1138
- // autofix (nexttask 6). Only ever reached from inside the gate's own
1166
+ // autofix. Only ever reached from inside the gate's own
1139
1167
  // resolution loop, so it needs no `verify work` switch of its own.
1140
1168
  // Same section, same cancel: this re-runs every ACCEPT-debt VERIFY command
1141
1169
  // against the final tree, each under its own 300s cap.
@@ -1150,11 +1178,11 @@ export function requestAutoCancel() {
1150
1178
  /**
1151
1179
  * Report a stash pushed during one task and left behind.
1152
1180
  *
1153
- * An orphan stash later pops as an unresolvable conflict (mx5 run 6), so the
1154
- * capture before the task and this check after it are ONE fact. They were a local
1155
- * and a check ~120 lines and three returns apart, which is how the check came to
1156
- * run only on the success path. Best-effort: never throws, so it cannot mask the
1157
- * outcome of the attempt it closes.
1181
+ * An orphan stash later pops as an unresolvable conflict, so the capture before
1182
+ * the task and this check after it are ONE fact which is why the call sits in a
1183
+ * `finally` rather than on the fall-through, where it would only run when the task
1184
+ * succeeded. Best-effort: never throws, so it cannot mask the outcome of the
1185
+ * attempt it closes.
1158
1186
  */
1159
1187
  async function reportStashDrift(active, deps, cwd, id, title, before) {
1160
1188
  if (!deps.stashRef || before === undefined)
@@ -1198,8 +1226,8 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
1198
1226
  const next = entries.find(e => !e.done);
1199
1227
  if (!next) {
1200
1228
  // FINAL INTEGRATION GATE: every task passed its own per-slice gates,
1201
- // but per-slice green has shipped a dead app twice (mx5 runs 3 & 5:
1202
- // statics clean, every protected route 500ing). The run-level stage
1229
+ // but per-slice green can still ship a dead app every slice's own
1230
+ // checks clean while the assembled whole does not serve. The run-level stage
1203
1231
  // runs the project's OWN whole-repo commands once, unaided, before the
1204
1232
  // run is declared complete, and resolves a FAIL with the user. It
1205
1233
  // touches none of this loop's per-task state — see run-final-gate.ts —
@@ -1210,7 +1238,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
1210
1238
  runId: id,
1211
1239
  // The parent plan (the task list) is what lets the gate tell a
1212
1240
  // served app from a CLI — the boot check requires a listener only
1213
- // for the former (mx5 run 10: a CSS watcher satisfied "still alive").
1241
+ // for the former.
1214
1242
  planText: body,
1215
1243
  taskCount: entries.length
1216
1244
  });
@@ -1228,8 +1256,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
1228
1256
  return;
1229
1257
  }
1230
1258
  // REFUSE to start on a conflicted tree: an unmerged index dooms every
1231
- // commit ahead and a `git add -A` would silently mis-resolve it. mx5
1232
- // run 6 burned a full impl turn + three verify passes exactly here.
1259
+ // commit ahead and a `git add -A` would silently mis-resolve it.
1233
1260
  const unmerged = deps.unmergedPaths ? await deps.unmergedPaths(cwd) : [];
1234
1261
  if (unmerged.length > 0) {
1235
1262
  await updateTaskFrontMatter(cwd, id, { state: 'failed' });
@@ -1280,8 +1307,8 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
1280
1307
  try {
1281
1308
  // SAFE CHECKPOINT (pre-task): the tree is committed and no inner task
1282
1309
  // is stamped yet, so stopping here just leaves this entry unchecked —
1283
- // a resume restarts it from scratch. Cheapest possible stop, and the
1284
- // last one before we commit to a ~30-minute task.
1310
+ // a resume restarts it from scratch. The cheapest possible stop, and
1311
+ // the last one before a whole task is under way.
1285
1312
  if (cancelCheckpoint('pre-task')) {
1286
1313
  announceDone(active, `${id} cancelled before "${next.title}" — resume with /task-auto-resume.`, 'warning');
1287
1314
  return;
@@ -1297,18 +1324,18 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
1297
1324
  onStart: resumeId ? undefined : (innerId => stampTaskInProgress(cwd, id, next.index, innerId, next.title))
1298
1325
  });
1299
1326
  active = res.ctx ?? active;
1300
- // One dispatch over the named ending. The five-branch ladder this
1301
- // replaces had to ask `isCancelRequested()` a module global
1302
- // `/task-cancel` never setsto tell a user stop from a fault, so a
1303
- // cancel during a task was announced in red as "stopped … fix and
1304
- // resume" and the inner file's `cancelled` was overwritten with
1305
- // `failed`. The runner names the ending now; resumability is
1306
- // RUN_END_POLICY's; only the wording is this command's.
1327
+ // One dispatch over the named ending. The runner NAMES how the run
1328
+ // ended, so nothing here has to infer a user stop from a fault by
1329
+ // consulting a module global the way that inference went wrong was
1330
+ // announcing a cancel in red as "stopped … fix and resume" and
1331
+ // overwriting the inner file's `cancelled` with `failed`.
1332
+ // Resumability is RUN_END_POLICY's call (shared with /task's loop);
1333
+ // only the wording is this command's.
1307
1334
  if (!runSucceeded(res.end)) {
1308
1335
  const policy = RUN_END_POLICY[res.end.kind];
1309
1336
  // Demote the INNER task file: it reads `completed` from
1310
1337
  // spec-handoff, and leaving it that way is how a failed run's task
1311
- // file claimed success in the run 6 audit.
1338
+ // file claims success after the run failed.
1312
1339
  if (policy.resumable)
1313
1340
  await markResumable(cwd, res.taskId);
1314
1341
  // The PLAN fails only on a fault. A declined-steer interrupt leaves
@@ -1373,20 +1400,20 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
1373
1400
  }), outcome.level);
1374
1401
  return;
1375
1402
  }
1376
- // ROOT-CAUSE REPAIR (mx5 run 14 item 5): the gate may have attributed a
1403
+ // ROOT-CAUSE REPAIR: the gate may have attributed a
1377
1404
  // FAIL to a pre-existing defect in a file some OTHER task created. It can
1378
1405
  // only QUEUE that finding — mutating the plan is this loop's job. Drain
1379
1406
  // the queue and splice a scoped repair step in right after the step that
1380
1407
  // just finished, so the defect is fixed BEFORE the next dependent task
1381
- // trips over it too (run 14 recorded the same `test/teardown.ts` cause
1382
- // twice, scheduled nothing, and the bug outlived ~24h of the run).
1408
+ // trips over it too. Recording the same cause twice and scheduling
1409
+ // nothing lets one defect outlive most of a run.
1383
1410
  await schedulePendingRepairs(cwd, id, next.index, active, deps);
1384
1411
  }
1385
1412
  finally {
1386
1413
  // EVERY exit from this attempt passes here — the two mid-attempt
1387
- // returns and a throw included. The check used to sit at the very
1388
- // end of the fall-through, ~120 lines and three returns below the
1389
- // capture, so it ran only when the task SUCCEEDED and the gate said
1414
+ // returns and a throw included. Sitting at the end of the
1415
+ // fall-through instead, below the capture and three returns, it
1416
+ // would run only when the task SUCCEEDED and the gate said
1390
1417
  // `done`. On a failed or interrupted task the user is told to run
1391
1418
  // /task-auto-resume, straight onto the landmine the guard exists to
1392
1419
  // name. The pairing is structural now, not positional.
@@ -1428,7 +1455,7 @@ async function handleTaskAuto(args, ctx) {
1428
1455
  // and the ordinary command path cannot reach us.
1429
1456
  try {
1430
1457
  await withRun(ctx, { onCancel: terminalCancel }, async () => {
1431
- // Stamp a fresh per-run research-cache id (F10) BEFORE planning so enrichment and
1458
+ // Stamp a fresh per-run research-cache id BEFORE planning so enrichment and
1432
1459
  // every task's research phase share one run's cache; disabled ⇒ clears any token a
1433
1460
  // prior run left, so nothing is cached.
1434
1461
  configureResearchRun(getConfig().researchCache);
@@ -1493,10 +1520,10 @@ async function handleTaskAutoResume(args, ctx) {
1493
1520
  try {
1494
1521
  await withRun(ctx, { onCancel: terminalCancel }, async () => {
1495
1522
  // Reuse the interrupted run's research-cache id, dropping only the entries whose
1496
- // own package moved version (F10). mx5 run 13 resumed three times and each
1497
- // resume's fresh id discarded a working 201-entry cache; run 14 then showed a
1498
- // whole-file freshness gate can never hold on a greenfield run that installs
1499
- // packages as it goes, so invalidation is per entry. See resumeResearchRun.
1523
+ // own package moved version. A fresh id per resume discards the whole
1524
+ // working cache, and a whole-file freshness gate can never hold on a
1525
+ // greenfield run that installs packages as it goes so invalidation is
1526
+ // per entry. See resumeResearchRun.
1500
1527
  const research = await resumeResearchRun(cwd, getConfig().researchCache);
1501
1528
  if (research.reused) {
1502
1529
  logPlanDebug(cwd, `research cache: resume reused ${research.entries} entr(ies), `