@mjasnikovs/pi-task 0.38.28 → 0.38.30

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 (370) 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/remote/bridge.d.ts +19 -10
  14. package/dist/remote/bridge.js +3 -2
  15. package/dist/remote/broadcast.js +3 -1
  16. package/dist/remote/events.js +12 -11
  17. package/dist/remote/history.d.ts +1 -1
  18. package/dist/remote/protocol.d.ts +6 -3
  19. package/dist/remote/protocol.js +2 -1
  20. package/dist/remote/push.d.ts +16 -16
  21. package/dist/remote/push.js +27 -27
  22. package/dist/remote/register.d.ts +3 -3
  23. package/dist/remote/register.js +17 -19
  24. package/dist/remote/server.d.ts +9 -8
  25. package/dist/remote/server.js +15 -14
  26. package/dist/remote/session-state.d.ts +5 -4
  27. package/dist/remote/session-state.js +8 -5
  28. package/dist/remote/sw.d.ts +7 -6
  29. package/dist/remote/sw.js +7 -6
  30. package/dist/remote/tailscale.d.ts +4 -2
  31. package/dist/remote/tailscale.js +4 -2
  32. package/dist/remote/ui-highlight.js +6 -5
  33. package/dist/remote/ui-render.js +4 -4
  34. package/dist/remote/ui-script.js +24 -24
  35. package/dist/remote/ui-styles.d.ts +1 -1
  36. package/dist/remote/ui-styles.js +10 -13
  37. package/dist/remote/ui-tools.js +9 -6
  38. package/dist/shared/child-extensions.d.ts +29 -17
  39. package/dist/shared/child-extensions.js +29 -17
  40. package/dist/shared/child-output.d.ts +30 -24
  41. package/dist/shared/child-output.js +25 -17
  42. package/dist/shared/child-process.d.ts +47 -40
  43. package/dist/shared/child-process.js +50 -59
  44. package/dist/shared/command-watchdog.d.ts +22 -16
  45. package/dist/shared/command-watchdog.js +28 -21
  46. package/dist/shared/fs-text.d.ts +16 -10
  47. package/dist/shared/fs-text.js +16 -10
  48. package/dist/shared/git-runner.d.ts +25 -25
  49. package/dist/shared/git-runner.js +25 -25
  50. package/dist/shared/leaked-tool-call.d.ts +17 -11
  51. package/dist/shared/leaked-tool-call.js +23 -15
  52. package/dist/shared/model-endpoint.d.ts +29 -16
  53. package/dist/shared/model-endpoint.js +33 -21
  54. package/dist/shared/pi-invocation.d.ts +7 -4
  55. package/dist/shared/pi-invocation.js +12 -7
  56. package/dist/shared/pkg-version.d.ts +13 -5
  57. package/dist/shared/pkg-version.js +13 -5
  58. package/dist/shared/reasoning-capability.d.ts +35 -24
  59. package/dist/shared/reasoning-capability.js +35 -24
  60. package/dist/shared/stream-watchdog.d.ts +60 -44
  61. package/dist/shared/stream-watchdog.js +62 -45
  62. package/dist/task/accept-debt.d.ts +41 -43
  63. package/dist/task/accept-debt.js +73 -65
  64. package/dist/task/api-synthesis.d.ts +24 -21
  65. package/dist/task/api-synthesis.js +32 -26
  66. package/dist/task/apis-contract.d.ts +32 -64
  67. package/dist/task/apis-contract.js +32 -64
  68. package/dist/task/artifact-closure.d.ts +27 -13
  69. package/dist/task/artifact-closure.js +95 -67
  70. package/dist/task/auto-commit.d.ts +46 -35
  71. package/dist/task/auto-commit.js +51 -38
  72. package/dist/task/auto-io.d.ts +45 -25
  73. package/dist/task/auto-io.js +57 -29
  74. package/dist/task/auto-orchestrator.d.ts +26 -24
  75. package/dist/task/auto-orchestrator.js +178 -162
  76. package/dist/task/auto-prompts.d.ts +36 -24
  77. package/dist/task/auto-prompts.js +40 -26
  78. package/dist/task/autofix-ledger.d.ts +27 -25
  79. package/dist/task/autofix-ledger.js +29 -26
  80. package/dist/task/batch-test-task.d.ts +20 -12
  81. package/dist/task/batch-test-task.js +67 -60
  82. package/dist/task/boot-probe.d.ts +60 -44
  83. package/dist/task/boot-probe.js +91 -72
  84. package/dist/task/cancel-input.d.ts +30 -16
  85. package/dist/task/cancel-input.js +20 -11
  86. package/dist/task/cancel-points.d.ts +27 -20
  87. package/dist/task/cancel-points.js +30 -22
  88. package/dist/task/child-runner.d.ts +46 -51
  89. package/dist/task/child-runner.js +48 -49
  90. package/dist/task/child-status.d.ts +23 -16
  91. package/dist/task/child-status.js +23 -16
  92. package/dist/task/clamp-output.js +12 -5
  93. package/dist/task/command-run.d.ts +31 -28
  94. package/dist/task/command-run.js +44 -35
  95. package/dist/task/command-shrink.d.ts +25 -18
  96. package/dist/task/command-shrink.js +37 -31
  97. package/dist/task/command-watchdog.d.ts +9 -6
  98. package/dist/task/command-watchdog.js +21 -15
  99. package/dist/task/context-attribution.d.ts +34 -26
  100. package/dist/task/context-attribution.js +34 -26
  101. package/dist/task/context-silence.d.ts +39 -29
  102. package/dist/task/context-silence.js +35 -25
  103. package/dist/task/context-usage.d.ts +25 -7
  104. package/dist/task/context-usage.js +21 -6
  105. package/dist/task/contracts.d.ts +8 -4
  106. package/dist/task/contracts.js +25 -17
  107. package/dist/task/coverage-loop.d.ts +22 -18
  108. package/dist/task/coverage-loop.js +35 -30
  109. package/dist/task/critique-probes.d.ts +13 -14
  110. package/dist/task/critique-probes.js +50 -39
  111. package/dist/task/debug-log.d.ts +13 -5
  112. package/dist/task/debug-log.js +32 -20
  113. package/dist/task/decompose-fidelity.d.ts +11 -9
  114. package/dist/task/decompose-fidelity.js +38 -33
  115. package/dist/task/decompose-granularity.d.ts +41 -38
  116. package/dist/task/decompose-granularity.js +41 -38
  117. package/dist/task/deep-render-check.d.ts +22 -14
  118. package/dist/task/deep-render-check.js +40 -31
  119. package/dist/task/dropped-input.d.ts +12 -7
  120. package/dist/task/dropped-input.js +5 -2
  121. package/dist/task/enforce-attribution.d.ts +38 -47
  122. package/dist/task/enforce-attribution.js +46 -52
  123. package/dist/task/enforce-guidelines.d.ts +31 -20
  124. package/dist/task/enforce-guidelines.js +32 -21
  125. package/dist/task/enrichment.d.ts +7 -2
  126. package/dist/task/enrichment.js +26 -14
  127. package/dist/task/env-notes.d.ts +16 -7
  128. package/dist/task/env-notes.js +48 -31
  129. package/dist/task/env-template-closure.d.ts +4 -4
  130. package/dist/task/env-template-closure.js +42 -34
  131. package/dist/task/external-context.d.ts +28 -21
  132. package/dist/task/external-context.js +17 -12
  133. package/dist/task/failure-classifier.d.ts +4 -5
  134. package/dist/task/failure-classifier.js +6 -7
  135. package/dist/task/file-inventory.d.ts +15 -11
  136. package/dist/task/file-inventory.js +25 -22
  137. package/dist/task/final-gate-fix.d.ts +74 -86
  138. package/dist/task/final-gate-fix.js +97 -116
  139. package/dist/task/final-gate-progress.d.ts +29 -46
  140. package/dist/task/final-gate-progress.js +40 -51
  141. package/dist/task/final-gate.d.ts +64 -97
  142. package/dist/task/final-gate.js +192 -199
  143. package/dist/task/fix-child.d.ts +21 -27
  144. package/dist/task/fix-child.js +21 -27
  145. package/dist/task/foreign-path.d.ts +6 -5
  146. package/dist/task/foreign-path.js +0 -0
  147. package/dist/task/frozen-conflict.d.ts +9 -10
  148. package/dist/task/frozen-conflict.js +61 -64
  149. package/dist/task/frozen-path-guard.d.ts +35 -14
  150. package/dist/task/frozen-path-guard.js +56 -39
  151. package/dist/task/gate-child.d.ts +27 -28
  152. package/dist/task/gate-child.js +37 -35
  153. package/dist/task/gate-deps.d.ts +34 -27
  154. package/dist/task/gate-deps.js +169 -159
  155. package/dist/task/gate-tally.d.ts +77 -80
  156. package/dist/task/gate-tally.js +65 -68
  157. package/dist/task/git-state-guard.d.ts +15 -11
  158. package/dist/task/git-state-guard.js +76 -66
  159. package/dist/task/impl-widget.d.ts +25 -16
  160. package/dist/task/impl-widget.js +27 -17
  161. package/dist/task/implementation-thinking.d.ts +33 -31
  162. package/dist/task/implementation-thinking.js +5 -6
  163. package/dist/task/implementation-turn.d.ts +34 -31
  164. package/dist/task/implementation-turn.js +29 -27
  165. package/dist/task/inline-markdown.d.ts +20 -7
  166. package/dist/task/inline-markdown.js +15 -6
  167. package/dist/task/launch-config-gap.js +25 -39
  168. package/dist/task/launch-contract.d.ts +18 -21
  169. package/dist/task/launch-contract.js +28 -30
  170. package/dist/task/launch-manifest.d.ts +6 -2
  171. package/dist/task/launch-manifest.js +35 -34
  172. package/dist/task/ledger.js +16 -14
  173. package/dist/task/lint-fix.d.ts +6 -8
  174. package/dist/task/lint-fix.js +67 -69
  175. package/dist/task/loop-detector.d.ts +9 -8
  176. package/dist/task/loop-detector.js +16 -12
  177. package/dist/task/mid-run-input.d.ts +17 -15
  178. package/dist/task/mid-run-input.js +17 -15
  179. package/dist/task/orchestrator.d.ts +24 -28
  180. package/dist/task/orchestrator.js +62 -64
  181. package/dist/task/orientation.d.ts +18 -23
  182. package/dist/task/orientation.js +24 -31
  183. package/dist/task/owned-freeze-conflict.d.ts +21 -20
  184. package/dist/task/owned-freeze-conflict.js +52 -85
  185. package/dist/task/owned-freeze-reassign.d.ts +40 -60
  186. package/dist/task/owned-freeze-reassign.js +41 -61
  187. package/dist/task/parsers.d.ts +4 -2
  188. package/dist/task/parsers.js +4 -4
  189. package/dist/task/phases.d.ts +41 -48
  190. package/dist/task/phases.js +180 -248
  191. package/dist/task/plan-io.d.ts +6 -7
  192. package/dist/task/plan-io.js +6 -7
  193. package/dist/task/plan-orchestrator.d.ts +10 -8
  194. package/dist/task/plan-orchestrator.js +14 -10
  195. package/dist/task/plan-prompts.d.ts +6 -5
  196. package/dist/task/plan-prompts.js +6 -5
  197. package/dist/task/plan-readonly.d.ts +4 -5
  198. package/dist/task/plan-readonly.js +4 -5
  199. package/dist/task/plan-rounds.d.ts +17 -29
  200. package/dist/task/plan-rounds.js +21 -34
  201. package/dist/task/plan-session.d.ts +58 -72
  202. package/dist/task/plan-session.js +61 -83
  203. package/dist/task/probe-gaming.d.ts +28 -27
  204. package/dist/task/probe-gaming.js +0 -0
  205. package/dist/task/prohibition-probe.d.ts +14 -16
  206. package/dist/task/prompts.d.ts +3 -4
  207. package/dist/task/prompts.js +17 -26
  208. package/dist/task/qa-transcript.d.ts +15 -22
  209. package/dist/task/qa-transcript.js +15 -21
  210. package/dist/task/question-box.d.ts +17 -13
  211. package/dist/task/question-box.js +19 -15
  212. package/dist/task/question-dedup.d.ts +6 -7
  213. package/dist/task/question-dedup.js +13 -14
  214. package/dist/task/question-dialog.d.ts +22 -32
  215. package/dist/task/question-dialog.js +22 -32
  216. package/dist/task/question-source.d.ts +18 -44
  217. package/dist/task/question-source.js +22 -51
  218. package/dist/task/refuted-constraint.d.ts +11 -31
  219. package/dist/task/refuted-constraint.js +27 -51
  220. package/dist/task/regenerable-artifacts.d.ts +12 -31
  221. package/dist/task/regenerable-artifacts.js +12 -31
  222. package/dist/task/render-check.d.ts +11 -22
  223. package/dist/task/render-check.js +33 -46
  224. package/dist/task/repo-health-check.d.ts +10 -14
  225. package/dist/task/repo-health-check.js +17 -23
  226. package/dist/task/requirements.d.ts +38 -71
  227. package/dist/task/requirements.js +78 -126
  228. package/dist/task/research-fanout-budget.d.ts +51 -88
  229. package/dist/task/research-fanout-budget.js +51 -88
  230. package/dist/task/research-worker.d.ts +33 -36
  231. package/dist/task/research-worker.js +39 -61
  232. package/dist/task/resume-gap.d.ts +14 -15
  233. package/dist/task/root-cause-repair.d.ts +9 -9
  234. package/dist/task/root-cause-repair.js +28 -40
  235. package/dist/task/run-bracket.d.ts +10 -13
  236. package/dist/task/run-end.d.ts +12 -22
  237. package/dist/task/run-end.js +8 -16
  238. package/dist/task/run-final-gate.d.ts +19 -21
  239. package/dist/task/run-final-gate.js +62 -80
  240. package/dist/task/runner-globs.d.ts +12 -13
  241. package/dist/task/runner-globs.js +12 -13
  242. package/dist/task/runner-resolve.d.ts +9 -9
  243. package/dist/task/runner-resolve.js +22 -23
  244. package/dist/task/script-escape.d.ts +10 -12
  245. package/dist/task/script-escape.js +13 -14
  246. package/dist/task/serve-entry.d.ts +1 -1
  247. package/dist/task/serve-entry.js +22 -25
  248. package/dist/task/service-blocks.js +4 -2
  249. package/dist/task/shipped-source.d.ts +11 -29
  250. package/dist/task/shipped-source.js +11 -29
  251. package/dist/task/skip-escape.js +10 -14
  252. package/dist/task/spec-urls.d.ts +26 -65
  253. package/dist/task/spec-urls.js +26 -65
  254. package/dist/task/spec-validation.d.ts +17 -20
  255. package/dist/task/spec-validation.js +17 -20
  256. package/dist/task/stall-detector.d.ts +23 -30
  257. package/dist/task/stall-detector.js +23 -30
  258. package/dist/task/stream-watchdog.d.ts +14 -12
  259. package/dist/task/stream-watchdog.js +14 -12
  260. package/dist/task/substitution-probe.d.ts +17 -20
  261. package/dist/task/substitution-probe.js +17 -20
  262. package/dist/task/task-gates.d.ts +36 -41
  263. package/dist/task/task-gates.js +95 -106
  264. package/dist/task/task-io.d.ts +4 -4
  265. package/dist/task/task-io.js +4 -4
  266. package/dist/task/task-parsers.js +4 -3
  267. package/dist/task/task-provenance.d.ts +2 -2
  268. package/dist/task/task-provenance.js +11 -13
  269. package/dist/task/task-types.d.ts +4 -3
  270. package/dist/task/terminal-outcome.d.ts +14 -16
  271. package/dist/task/terminal-outcome.js +12 -14
  272. package/dist/task/test-assembly.d.ts +13 -20
  273. package/dist/task/test-assembly.js +13 -20
  274. package/dist/task/timings.d.ts +5 -3
  275. package/dist/task/timings.js +5 -3
  276. package/dist/task/title-label.d.ts +9 -4
  277. package/dist/task/title-label.js +9 -4
  278. package/dist/task/type-only-answer.d.ts +44 -52
  279. package/dist/task/type-only-answer.js +44 -52
  280. package/dist/task/unfailable-command.d.ts +18 -24
  281. package/dist/task/unfailable-command.js +21 -27
  282. package/dist/task/unknown-routing.d.ts +10 -4
  283. package/dist/task/unknown-routing.js +10 -4
  284. package/dist/task/user-directives.d.ts +5 -8
  285. package/dist/task/user-directives.js +5 -8
  286. package/dist/task/verify-quality.d.ts +18 -22
  287. package/dist/task/verify-quality.js +45 -46
  288. package/dist/task/verify-reconcile.d.ts +15 -10
  289. package/dist/task/verify-reconcile.js +45 -43
  290. package/dist/task/verify-resolution.d.ts +24 -20
  291. package/dist/task/verify-resolution.js +51 -50
  292. package/dist/task/verify-work.d.ts +59 -66
  293. package/dist/task/verify-work.js +101 -138
  294. package/dist/task/widget.d.ts +15 -14
  295. package/dist/task/widget.js +22 -17
  296. package/dist/task/wiring-claims.d.ts +25 -32
  297. package/dist/task/wiring-claims.js +30 -35
  298. package/dist/task/write-guard.d.ts +39 -39
  299. package/dist/task/write-guard.js +48 -51
  300. package/dist/task/yolo.d.ts +34 -30
  301. package/dist/task/yolo.js +42 -37
  302. package/dist/workers/abstention.d.ts +21 -41
  303. package/dist/workers/abstention.js +27 -48
  304. package/dist/workers/brave-search.d.ts +4 -3
  305. package/dist/workers/brave-search.js +5 -2
  306. package/dist/workers/brave-warning.d.ts +7 -4
  307. package/dist/workers/brave-warning.js +19 -7
  308. package/dist/workers/ddg-search.d.ts +6 -6
  309. package/dist/workers/ddg-search.js +18 -12
  310. package/dist/workers/docs-cache.js +5 -2
  311. package/dist/workers/docs-chunk.d.ts +30 -37
  312. package/dist/workers/docs-chunk.js +37 -41
  313. package/dist/workers/docs-core.d.ts +28 -44
  314. package/dist/workers/docs-core.js +25 -44
  315. package/dist/workers/docs-index.js +4 -3
  316. package/dist/workers/docs-lookup.d.ts +15 -22
  317. package/dist/workers/docs-lookup.js +12 -21
  318. package/dist/workers/docs-project.d.ts +15 -9
  319. package/dist/workers/docs-project.js +17 -10
  320. package/dist/workers/docs-resolve.d.ts +19 -20
  321. package/dist/workers/docs-resolve.js +35 -32
  322. package/dist/workers/docs-retrieve.d.ts +5 -6
  323. package/dist/workers/docs-retrieve.js +18 -15
  324. package/dist/workers/exa-search.d.ts +9 -6
  325. package/dist/workers/exa-search.js +23 -12
  326. package/dist/workers/fetch-core.d.ts +13 -16
  327. package/dist/workers/fetch-core.js +23 -23
  328. package/dist/workers/focused-extractor.d.ts +12 -12
  329. package/dist/workers/focused-extractor.js +16 -19
  330. package/dist/workers/html-clean.js +24 -14
  331. package/dist/workers/http-request.d.ts +28 -20
  332. package/dist/workers/http-request.js +22 -17
  333. package/dist/workers/npm-version.d.ts +28 -11
  334. package/dist/workers/npm-version.js +24 -15
  335. package/dist/workers/phantom-imports.d.ts +15 -12
  336. package/dist/workers/phantom-imports.js +30 -24
  337. package/dist/workers/pi-worker-core.d.ts +86 -54
  338. package/dist/workers/pi-worker-core.js +112 -112
  339. package/dist/workers/pi-worker-docs.d.ts +24 -19
  340. package/dist/workers/pi-worker-docs.js +67 -76
  341. package/dist/workers/pi-worker-fetch.d.ts +7 -3
  342. package/dist/workers/pi-worker-fetch.js +27 -19
  343. package/dist/workers/pi-worker-search.js +12 -8
  344. package/dist/workers/pi-worker.d.ts +9 -4
  345. package/dist/workers/pi-worker.js +23 -10
  346. package/dist/workers/reasoning-warning.d.ts +18 -17
  347. package/dist/workers/reasoning-warning.js +22 -20
  348. package/dist/workers/research-cache.js +50 -78
  349. package/dist/workers/search-core.js +7 -5
  350. package/dist/workers/search-types.d.ts +10 -9
  351. package/dist/workers/search-types.js +9 -8
  352. package/dist/workers/session-hint.d.ts +13 -14
  353. package/dist/workers/session-hint.js +8 -9
  354. package/dist/workers/shared.d.ts +21 -25
  355. package/dist/workers/shared.js +0 -0
  356. package/dist/workers/single-read-extension.d.ts +14 -7
  357. package/dist/workers/single-read-extension.js +14 -7
  358. package/dist/workers/single-read-guard.d.ts +25 -28
  359. package/dist/workers/single-read-guard.js +32 -32
  360. package/dist/workers/typeonly-log.d.ts +12 -9
  361. package/dist/workers/typeonly-log.js +29 -33
  362. package/dist/workers/worker-channels.d.ts +15 -23
  363. package/dist/workers/worker-channels.js +15 -23
  364. package/dist/workers/worker-failure.d.ts +38 -46
  365. package/dist/workers/worker-failure.js +31 -39
  366. package/dist/workers/worker-kill.d.ts +25 -26
  367. package/dist/workers/worker-kill.js +16 -19
  368. package/dist/workers/worker-profiles.d.ts +43 -53
  369. package/dist/workers/worker-profiles.js +30 -38
  370. 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';
@@ -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.
307
+ *
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.
302
313
  *
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.
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,23 @@ 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
496
  catch {
484
497
  // best-effort channel
485
498
  }
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
499
+ // Granularity floor: without it the plan's task COUNT is set by an
500
+ // auto-resolved clarify line the user never sees, so the same spec and the same
501
+ // code can plan coarse one run and fine the next. Derive the floor from the
502
+ // requirements a task can own instead, so an unreviewable "one task per
490
503
  // 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.
504
+ // plan-shape fork below is the host's to answer at all. Measured:
505
+ // granularityFloor is 0 for three or fewer ownable requirements — no channel —
506
+ // and roughly half the count above that.
492
507
  const ownableRequirements = reqEntries.filter(e => !isCrossCuttingRequirement(e.quote)).length;
493
508
  const coarseFloor = granularityFloor(ownableRequirements);
494
509
  if (coarseFloor > 0) {
@@ -509,8 +524,9 @@ export async function orientFeature(cwd, feature, deps) {
509
524
  * settles is auto-resolved and never shown — only genuine open forks reach the user.
510
525
  * The model emits NONE when nothing remains.
511
526
  *
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.
527
+ * The ONLY stage that ASKS the user anything, and so the only one that can be
528
+ * dismissed the others only notify, which cannot be. `null` means the user
529
+ * cancelled and the cancellation has already been announced.
514
530
  * Every other outcome is a transcript, possibly empty.
515
531
  */
516
532
  export async function elicitClarifications(ctx, cwd, deps, oriented) {
@@ -527,17 +543,18 @@ export async function elicitClarifications(ctx, cwd, deps, oriented) {
527
543
  // strikes out (can't produce anything novel) we stop instead of barraging the
528
544
  // user with the same decision worded N ways. Also caps the absolute count.
529
545
  // 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.
546
+ // task/question-source.ts, shared with the plan session. A second copy here
547
+ // would drift from the sibling, and the shared source buys two things: it
548
+ // PICKS a question rather than taking the first parsed line, so an analysis
549
+ // note is never shown as the question and its SUGGESTED is not lost; and an
550
+ // unparseable reply costs one format re-prompt instead of ending clarify
551
+ // outright and decomposing the feature with ZERO clarifications.
537
552
  //
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.
553
+ // Of plan's three quality rules only the DEFERRAL guard crosses. Checked:
554
+ // PLAN_QUALITY_RULES holds three ('no SUGGESTED', 'SUGGESTED deferred the
555
+ // decision', 'fork-shaped question with no ALT') and CLARIFY_QUALITY_RULES
556
+ // holds exactly the middle one. The other two cost an extra child call every
557
+ // time they fire, so moving them is its own change to make and measure.
541
558
  const source = makeQuestionSource({
542
559
  generate: hint => deps.runChild('auto-clarify', 'read', prependHint(hint, AUTO_CLARIFY_PROMPT(featureForModel, transcript.forGenerator()))),
543
560
  formatHint: PLAN_FORMAT_HINT,
@@ -554,9 +571,10 @@ export async function elicitClarifications(ctx, cwd, deps, oriented) {
554
571
  // for the editable default and the persisted file.
555
572
  const shownQ = renderInlineMarkdown(question, theme);
556
573
  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
574
+ // PLAN SHAPE is the host's call, not the triage's the same spec off the
575
+ // same base commit can plan far coarser or finer depending on this one
576
+ // answer (see decompose-granularity.ts).
577
+ // The triage answers this fork for itself every time and stamps it
560
578
  // "already settled by the spec" while the spec settles no such thing, so the
561
579
  // single most load-bearing decision in a run was an invisible coin flip.
562
580
  // Answer it deterministically instead: same channel, same transcript, but a
@@ -610,9 +628,9 @@ export async function elicitClarifications(ctx, cwd, deps, oriented) {
610
628
  */
611
629
  export async function decomposePlan(cwd, deps, oriented, clarifications) {
612
630
  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
631
+ // Tests-in-the-same-change cadence: when the
632
+ // decisions mandate it, a whole-project batch test task contradicts them, and
633
+ // one still ships because
616
634
  // decompose mirrors the spec's milestone shape. The decisions channel
617
635
  // OVERRIDES the spec doc, so this resolves toward the decision without asking.
618
636
  const noBatchTests = mandatesTestsInSameChange(clarifications, featureForModel);
@@ -622,7 +640,7 @@ export async function decomposePlan(cwd, deps, oriented, clarifications) {
622
640
  }
623
641
  // decompose
624
642
  const decomposePrompt = AUTO_DECOMPOSE_PROMPT(featureForModel, clarifications, buildRequirementsLedger(reqEntries), noBatchTests);
625
- // Parse + FIDELITY RECONCILIATION (mx5 run 11, goal B): ground each title's
643
+ // Parse + FIDELITY RECONCILIATION: ground each title's
626
644
  // [source: "…"] citation against the doc, strip the clause, and re-attach any
627
645
  // `+`-joined constraint fragment the paraphrased title dropped (the silently
628
646
  // stripped "+ tests" class). Applied to EVERY decompose output — initial,
@@ -639,7 +657,7 @@ export async function decomposePlan(cwd, deps, oriented, clarifications) {
639
657
  // Batch-test ban (item 6): drop or scope a whole-project "write all the
640
658
  // tests" task. Identity unless the cadence decision is present, and the
641
659
  // sweep replacement re-grounds every requirement the drop would cost — so
642
- // planned coverage cannot fall (run 12's lesson).
660
+ // planned coverage cannot fall.
643
661
  const debatched = rewriteBatchTestPlan(plan.titles, clarifications, featureForModel, reqEntries.map(e => e.quote), isCrossCuttingRequirement);
644
662
  for (const a of debatched.actions) {
645
663
  logPlanDebug(cwd, `batch test task ${a.kind} (tests-in-same-change decision): "${a.title}"`
@@ -654,7 +672,7 @@ export async function decomposePlan(cwd, deps, oriented, clarifications) {
654
672
  logPlanDebug(cwd, `decompose produced ${planTitles.length} title(s)`);
655
673
  // BRACES for the floor: the prompt clause alone is a preference the model can
656
674
  // 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
675
+ // regenerated — a fresh roll can drop a covered area). Longer plan
658
676
  // wins; a still-coarse plan falls through to the coverage judge as before, so
659
677
  // this can never block planning.
660
678
  if (isTooCoarse(planTitles.length, coarseFloor)) {
@@ -668,8 +686,8 @@ export async function decomposePlan(cwd, deps, oriented, clarifications) {
668
686
  }
669
687
  // Distrust floor (see isSuspectPlan): a ≤2-title plan for a multi-KB spec is
670
688
  // 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
689
+ // catch it, and a hinted retry heals it reliably.
690
+ // Longer list wins; a still-suspect plan falls through to the
673
691
  // judge loop as before, so this never blocks planning.
674
692
  // An EMPTY plan gets extra attempts (see EMPTY_PLAN_RETRIES): falling through
675
693
  // with zero titles aborts the whole run, so one roll of the dice is not enough.
@@ -717,17 +735,16 @@ export async function coverPlan(ctx, cwd, deps, oriented, clarifications, decomp
717
735
  const { featureForModel, reqEntries } = oriented;
718
736
  const { decomposePrompt, parsePlan } = decomposed;
719
737
  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
738
+ // Coverage gate: a degenerate completion ONE task and a natural EOS for a
739
+ // whole design document is nonempty, so the length guard below
722
740
  // never fires and the whole run "completes" after one task. Judge the list
723
741
  // against the feature with a no-tools child; on INCOMPLETE, re-run decompose
724
742
  // with the missing areas as a hint. Best-effort so a triage fault never blocks
725
743
  // planning (mirrors triageClarifyQuestion).
726
744
  //
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):
745
+ // Two invariants. Without them a complete full-stack plan is overwritten by a
746
+ // narrower regeneration, driven by a handful of NEGATIVE requirements that no
747
+ // task can own and that therefore keep the verdict INCOMPLETE forever:
731
748
  // • MONOTONIC replacement (coverage-loop.ts): a retry that DROPS a requirement
732
749
  // the current plan already owns is REJECTED, never adopted. Coverage can
733
750
  // only hold or grow across rounds — a worse regeneration can no longer
@@ -759,8 +776,8 @@ export async function coverPlan(ctx, cwd, deps, oriented, clarifications, decomp
759
776
  // numbers. Live (Qwen3.6-27B) the model over-credits ownership, mapping a
760
777
  // "--json output" requirement to a generic "scaffold + argument parser"
761
778
  // 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
779
+ // went blind. Grounding the drop-signal in the titles the model cannot
780
+ // fake restores it. The model map still drives Fix A's
764
781
  // cross-cutting/unmapped accounting below (that only affects reprompt
765
782
  // aggressiveness, which the monotonic guard now backstops).
766
783
  const covered = groundedCoverage(reqEntries.map(e => e.quote), titles, isCrossCuttingRequirement);
@@ -796,9 +813,9 @@ export async function coverPlan(ctx, cwd, deps, oriented, clarifications, decomp
796
813
  // `best` is both the plan the next round reprompts FROM and the plan that
797
814
  // ships — kept identical because adoption is monotone (see coverage-loop.ts).
798
815
  //
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.
816
+ // It is also the ONLY handle on the accounting. A second one carried
817
+ // alongside is how a requirement gets attached to the wrong task see the
818
+ // ScoredPlan doc comment.
802
819
  // The record, and the decisions it makes: task/plan-rounds.ts. This was five
803
820
  // locals threaded by closure through a ~90-line loop, plus a
804
821
  // snapshot-before-overwrite pair that existed only because the bonus-round
@@ -859,8 +876,8 @@ export async function coverPlan(ctx, cwd, deps, oriented, clarifications, decomp
859
876
  const round = rounds.round();
860
877
  planTitles = best.plan.titles;
861
878
  // 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.
879
+ // silently shipping a KNOWN-gapped plan is how a run loses a whole area of
880
+ // work — tell the user what is still uncovered.
864
881
  const unresolvedMissing = rounds.unresolved();
865
882
  if (unresolvedMissing !== null) {
866
883
  logPlanDebug(cwd, `decompose-coverage exhausted ${round} round(s) still INCOMPLETE — missing: `
@@ -883,14 +900,14 @@ export async function planAuto(ctx, cwd, feature, deps) {
883
900
  const clarifications = await elicitClarifications(ctx, cwd, deps, oriented);
884
901
  if (clarifications === null)
885
902
  return null; // dismissed; already announced
886
- // Artifact-production closure, plan side (mx5 run 13, PROMPT 2): runtime
903
+ // Artifact-production closure, plan side: runtime
887
904
  // files the spec REFERENCES (server snippets, prose "serve the built
888
905
  // index.html") that neither its file tree, its parsed build outputs, nor the
889
906
  // existing scaffold produce. Sentence-grounded coverage credited the SERVING
890
907
  // side and reported "0 unowned" while nothing ever CREATED the file — so
891
908
  // these ride the coverage loop's `missing` list as unowned areas until some
892
909
  // task title claims the artifact (grounded in titles, which the coverage-map
893
- // model cannot fake — the run-12 lesson). Deterministic and best-effort.
910
+ // model cannot fake — the lesson). Deterministic and best-effort.
894
911
  let specDangling = [];
895
912
  try {
896
913
  specDangling = findSpecDanglingArtifacts(featureForModel, rel => existsSync(path.join(cwd, rel)));
@@ -910,16 +927,16 @@ export async function planAuto(ctx, cwd, feature, deps) {
910
927
  const planTitles = covered.planTitles;
911
928
  // Carry what no single task owns (goal A(b)/(c)): cross-cutting requirements
912
929
  // 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
930
+ // refine/compose. A rule stated only in the spec body has no carrier, and a
931
+ // "spec is authoritative" pointer recovers it in a minority of tasks: content
932
+ // travels, pointers do not. Requirements still unmapped after the rounds are carried
916
933
  // too — marked — and recorded user-visibly in the plan file, never dropped.
917
934
  //
918
935
  // #1: the holistic-judge missing areas are carried as a THIRD channel. They are
919
936
  // areas requirement-extraction never captured as a tracked entry (so the
920
937
  // 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
938
+ // exactly the class that, having no carrier, is warned about and then
939
+ // dropped. Carried independent of `accounting` so a mapping
923
940
  // fault (accounting === null) can't strand them either.
924
941
  const carriedCrossCutting = best.accounting?.crossCutting ?? [];
925
942
  const carriedUnmapped = best.accounting?.unmapped ?? [];
@@ -959,7 +976,7 @@ export async function planAuto(ctx, cwd, feature, deps) {
959
976
  // empty-plan path the plan is discarded one line later, so running them first
960
977
  // burned two model calls and left contracts.md / launch-contract.md carrying
961
978
  // facts for a run that never produced a task.
962
- // Cross-slice contract registry (mx5 run 8, F3): now that the plan is settled,
979
+ // Cross-slice contract registry: now that the plan is settled,
963
980
  // extract the interface facts MORE THAN ONE slice must agree on — endpoint paths,
964
981
  // exported signatures, file layouts, env var names the DESIGN pins — into a
965
982
  // run-level artifact each downstream refine/compose/verify reads. The extraction
@@ -979,12 +996,12 @@ export async function planAuto(ctx, cwd, feature, deps) {
979
996
  ground: emitted => keepGroundedContracts(emitted, featureForModel),
980
997
  append: appendContracts
981
998
  });
982
- // Launch contract (mx5 run 10 item 4): extract the package/build SCRIPTS the design
999
+ // Launch contract: extract the package/build SCRIPTS the design
983
1000
  // declares the project must expose (`migrate`/`seed` fell through decompose and
984
1001
  // shipped missing, unchecked). Each emitted name is re-grounded against the design
985
1002
  // (keepGroundedScripts — kept only if the design backticks it), so the final gate's
986
1003
  // manifest diff can never false-flag a hallucinated script. Recall is mechanical
987
- // (mx5 run 11): enumerateScriptCandidates hands the child every backticked
1004
+ //: enumerateScriptCandidates hands the child every backticked
988
1005
  // script-shaped token near the word "script" as a checklist, so a script declared
989
1006
  // far from the design's summary list (`test:ct` in §2 vs §9's five) can't be
990
1007
  // missed by a weak model's recall — the child classifies, it no longer recalls.
@@ -1001,10 +1018,10 @@ export async function planAuto(ctx, cwd, feature, deps) {
1001
1018
  append: appendDeclaredScripts
1002
1019
  });
1003
1020
  // 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
1021
+ // each task will carry. With only cross-cutting entries travelling, the
1022
+ // mapped ones shape the title list and then vanish, and a task can narrow a
1023
+ // requirement out of its own spec with nothing to stop it.
1024
+ // Inert until the owned-requirements injection is wired into the phase
1008
1025
  // prompts; recorded regardless so the plan's mapping is auditable per run.
1009
1026
  if (best.accounting && best.accounting.mapped.length > 0) {
1010
1027
  await writeOwnedRequirements(cwd, best.accounting.mapped
@@ -1042,7 +1059,7 @@ export async function planAuto(ctx, cwd, feature, deps) {
1042
1059
  *
1043
1060
  * The grounding step is the reason this shape exists rather than a plain child
1044
1061
  * 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
1062
+ * them, and an invented fact in a run-level registry is read as
1046
1063
  * authoritative by every downstream refine/compose/verify. So nothing the child
1047
1064
  * says is trusted: `ground` re-checks each emitted line against the design text
1048
1065
  * host-side, and only substrings survive.
@@ -1086,9 +1103,9 @@ function defaultDeps(ctx, cwd, signal, title) {
1086
1103
  cwd,
1087
1104
  taskId: '',
1088
1105
  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
1106
+ // IN-RUN thrash guard for the planning children: without it a decompose
1107
+ // child can re-read its design document until it fills the whole context
1108
+ // window, and never return. Every planning child
1092
1109
  // gets its source doc INLINED in its prompt, so a second read of a file
1093
1110
  // it has already opened can only be thrash — which makes the read-once
1094
1111
  // block safe here in a way it is not for a phase that must explore.
@@ -1128,14 +1145,14 @@ function defaultDeps(ctx, cwd, signal, title) {
1128
1145
  runFinalIntegrationGate(cwd2, { planText, signal })
1129
1146
  : Promise.resolve({ ok: true, reason: 'disabled' }),
1130
1147
  // 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
1148
+ // picker. Every task is committed by the time
1132
1149
  // the gate runs, so whatever is dirty here belongs to the fix pass.
1133
1150
  pendingChanges: async (cwd2) => {
1134
1151
  const changes = await collectTreeChanges(cwd2, signal);
1135
1152
  return [...changes.modified, ...changes.added, ...changes.deleted].sort();
1136
1153
  },
1137
1154
  // 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
1155
+ // autofix. Only ever reached from inside the gate's own
1139
1156
  // resolution loop, so it needs no `verify work` switch of its own.
1140
1157
  // Same section, same cancel: this re-runs every ACCEPT-debt VERIFY command
1141
1158
  // against the final tree, each under its own 300s cap.
@@ -1150,11 +1167,11 @@ export function requestAutoCancel() {
1150
1167
  /**
1151
1168
  * Report a stash pushed during one task and left behind.
1152
1169
  *
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.
1170
+ * An orphan stash later pops as an unresolvable conflict, so the capture before
1171
+ * the task and this check after it are ONE fact which is why the call sits in a
1172
+ * `finally` rather than on the fall-through, where it would only run when the task
1173
+ * succeeded. Best-effort: never throws, so it cannot mask the outcome of the
1174
+ * attempt it closes.
1158
1175
  */
1159
1176
  async function reportStashDrift(active, deps, cwd, id, title, before) {
1160
1177
  if (!deps.stashRef || before === undefined)
@@ -1198,8 +1215,8 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
1198
1215
  const next = entries.find(e => !e.done);
1199
1216
  if (!next) {
1200
1217
  // 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
1218
+ // but per-slice green can still ship a dead app every slice's own
1219
+ // checks clean while the assembled whole does not serve. The run-level stage
1203
1220
  // runs the project's OWN whole-repo commands once, unaided, before the
1204
1221
  // run is declared complete, and resolves a FAIL with the user. It
1205
1222
  // touches none of this loop's per-task state — see run-final-gate.ts —
@@ -1210,7 +1227,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
1210
1227
  runId: id,
1211
1228
  // The parent plan (the task list) is what lets the gate tell a
1212
1229
  // 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").
1230
+ // for the former.
1214
1231
  planText: body,
1215
1232
  taskCount: entries.length
1216
1233
  });
@@ -1228,8 +1245,7 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
1228
1245
  return;
1229
1246
  }
1230
1247
  // 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.
1248
+ // commit ahead and a `git add -A` would silently mis-resolve it.
1233
1249
  const unmerged = deps.unmergedPaths ? await deps.unmergedPaths(cwd) : [];
1234
1250
  if (unmerged.length > 0) {
1235
1251
  await updateTaskFrontMatter(cwd, id, { state: 'failed' });
@@ -1280,8 +1296,8 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
1280
1296
  try {
1281
1297
  // SAFE CHECKPOINT (pre-task): the tree is committed and no inner task
1282
1298
  // 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.
1299
+ // a resume restarts it from scratch. The cheapest possible stop, and
1300
+ // the last one before a whole task is under way.
1285
1301
  if (cancelCheckpoint('pre-task')) {
1286
1302
  announceDone(active, `${id} cancelled before "${next.title}" — resume with /task-auto-resume.`, 'warning');
1287
1303
  return;
@@ -1297,18 +1313,18 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
1297
1313
  onStart: resumeId ? undefined : (innerId => stampTaskInProgress(cwd, id, next.index, innerId, next.title))
1298
1314
  });
1299
1315
  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.
1316
+ // One dispatch over the named ending. The runner NAMES how the run
1317
+ // ended, so nothing here has to infer a user stop from a fault by
1318
+ // consulting a module global the way that inference went wrong was
1319
+ // announcing a cancel in red as "stopped … fix and resume" and
1320
+ // overwriting the inner file's `cancelled` with `failed`.
1321
+ // Resumability is RUN_END_POLICY's call (shared with /task's loop);
1322
+ // only the wording is this command's.
1307
1323
  if (!runSucceeded(res.end)) {
1308
1324
  const policy = RUN_END_POLICY[res.end.kind];
1309
1325
  // Demote the INNER task file: it reads `completed` from
1310
1326
  // spec-handoff, and leaving it that way is how a failed run's task
1311
- // file claimed success in the run 6 audit.
1327
+ // file claims success after the run failed.
1312
1328
  if (policy.resumable)
1313
1329
  await markResumable(cwd, res.taskId);
1314
1330
  // The PLAN fails only on a fault. A declined-steer interrupt leaves
@@ -1373,20 +1389,20 @@ export async function runAutoLoop(ctx, cwd, id, deps) {
1373
1389
  }), outcome.level);
1374
1390
  return;
1375
1391
  }
1376
- // ROOT-CAUSE REPAIR (mx5 run 14 item 5): the gate may have attributed a
1392
+ // ROOT-CAUSE REPAIR: the gate may have attributed a
1377
1393
  // FAIL to a pre-existing defect in a file some OTHER task created. It can
1378
1394
  // only QUEUE that finding — mutating the plan is this loop's job. Drain
1379
1395
  // the queue and splice a scoped repair step in right after the step that
1380
1396
  // 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).
1397
+ // trips over it too. Recording the same cause twice and scheduling
1398
+ // nothing lets one defect outlive most of a run.
1383
1399
  await schedulePendingRepairs(cwd, id, next.index, active, deps);
1384
1400
  }
1385
1401
  finally {
1386
1402
  // 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
1403
+ // returns and a throw included. Sitting at the end of the
1404
+ // fall-through instead, below the capture and three returns, it
1405
+ // would run only when the task SUCCEEDED and the gate said
1390
1406
  // `done`. On a failed or interrupted task the user is told to run
1391
1407
  // /task-auto-resume, straight onto the landmine the guard exists to
1392
1408
  // name. The pairing is structural now, not positional.
@@ -1428,7 +1444,7 @@ async function handleTaskAuto(args, ctx) {
1428
1444
  // and the ordinary command path cannot reach us.
1429
1445
  try {
1430
1446
  await withRun(ctx, { onCancel: terminalCancel }, async () => {
1431
- // Stamp a fresh per-run research-cache id (F10) BEFORE planning so enrichment and
1447
+ // Stamp a fresh per-run research-cache id BEFORE planning so enrichment and
1432
1448
  // every task's research phase share one run's cache; disabled ⇒ clears any token a
1433
1449
  // prior run left, so nothing is cached.
1434
1450
  configureResearchRun(getConfig().researchCache);
@@ -1493,10 +1509,10 @@ async function handleTaskAutoResume(args, ctx) {
1493
1509
  try {
1494
1510
  await withRun(ctx, { onCancel: terminalCancel }, async () => {
1495
1511
  // 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.
1512
+ // own package moved version. A fresh id per resume discards the whole
1513
+ // working cache, and a whole-file freshness gate can never hold on a
1514
+ // greenfield run that installs packages as it goes so invalidation is
1515
+ // per entry. See resumeResearchRun.
1500
1516
  const research = await resumeResearchRun(cwd, getConfig().researchCache);
1501
1517
  if (research.reused) {
1502
1518
  logPlanDebug(cwd, `research cache: resume reused ${research.entries} entr(ies), `