@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
@@ -12,7 +12,19 @@
12
12
  *
13
13
  * Either pattern returns a LoopHit so the caller can kill the child and re-spawn
14
14
  * with a hint. No I/O. No imports from index.ts. Trivially unit-testable.
15
+ *
16
+ * The three tuning constants live here rather than in child-runner.ts because
17
+ * worker-profiles.ts reads them at module top level to build DEFAULT_LOOP_DETECTOR.
18
+ * From child-runner.ts that is a cycle — child-runner → worker-profiles →
19
+ * child-runner — and the failure is a TDZ ReferenceError on import order, which
20
+ * no compile step catches. This module imports nothing, so it cannot close one.
15
21
  */
22
+ /** Recent tool calls the exact-repeat rule looks back over. */
23
+ export const LOOP_WINDOW = 20;
24
+ /** Repeats of one key within the window that trip the detector. */
25
+ export const LOOP_THRESHOLD = 5;
26
+ /** Re-spawns allowed after a loop kill — 3 attempts total with the initial one. */
27
+ export const MAX_LOOP_RESTARTS = 2;
16
28
  /**
17
29
  * JSON.stringify with sorted object keys so {a:1,b:2} and {b:2,a:1} hash equal.
18
30
  * Arrays preserve their order (positional). undefined / primitives passthrough.
@@ -28,6 +40,14 @@ export function stableStringify(value) {
28
40
  return sorted;
29
41
  });
30
42
  }
43
+ /**
44
+ * The exact-match identity of a tool call. Exported because implementation-guards.ts
45
+ * keys its own per-call strike count on it, and a second hand-written copy of this
46
+ * expression is free to drift away from the one `record` uses.
47
+ */
48
+ export function loopKey(call) {
49
+ return `${call.name}\x00${stableStringify(call.args)}`;
50
+ }
31
51
  /**
32
52
  * The primary file path a tool call targets, mirroring summarizeToolArgs' field
33
53
  * precedence. Returns null when the call names no path (e.g. bash, or a grep
@@ -52,7 +72,11 @@ function readOffset(args) {
52
72
  const o = args.offset;
53
73
  return typeof o === 'number' && Number.isFinite(o) ? o : 0;
54
74
  }
55
- /** Default `limit` pi's read tool applies when the call names none. */
75
+ /**
76
+ * pi's read tool truncates its output at `DEFAULT_MAX_LINES` lines (its
77
+ * `core/tools/truncate` module). A `limit` at or above that asks for as much as
78
+ * the tool will ever return, so it is treated the same as naming no limit at all.
79
+ */
56
80
  const DEFAULT_READ_LIMIT = 2000;
57
81
  /**
58
82
  * The last line a call reaches, so a page can be told from a re-read. Absent,
@@ -74,7 +98,7 @@ export class LoopDetector {
74
98
  threshold;
75
99
  pathThreshold;
76
100
  buf = [];
77
- constructor(window = 20, threshold = 5,
101
+ constructor(window = LOOP_WINDOW, threshold = LOOP_THRESHOLD,
78
102
  /** Revisits of one path needed to trip; defaults to the exact threshold. */
79
103
  pathThreshold = threshold) {
80
104
  this.window = window;
@@ -83,7 +107,7 @@ export class LoopDetector {
83
107
  }
84
108
  /** Record a tool call. Returns LoopHit if either threshold is breached, else null. */
85
109
  record(call) {
86
- const key = `${call.name}\x00${stableStringify(call.args)}`;
110
+ const key = loopKey(call);
87
111
  const offset = readOffset(call.args);
88
112
  this.buf.push({ key, path: primaryPath(call.args), offset, end: readEnd(call.args, offset) });
89
113
  if (this.buf.length > this.window)
@@ -96,9 +120,8 @@ export class LoopDetector {
96
120
  if (exact >= this.threshold)
97
121
  return { call, count: exact, windowSize: this.buf.length };
98
122
  // 2. Path-aware loop: the same file re-targeted without forward progress.
99
- // Caught here precisely because varied offset/limit make the exact key
100
- // miss it (TASK_0017: auth.ts read/grepped ~12× with changing args until
101
- // the wall-clock timeout, never tripping the exact detector).
123
+ // Caught here precisely because varied offset/limit change the exact key on
124
+ // every call, so pattern 1 above can never see it.
102
125
  const path = this.buf[this.buf.length - 1].path;
103
126
  if (path !== null) {
104
127
  const revisits = this.countRevisits(path);
@@ -111,15 +134,16 @@ export class LoopDetector {
111
134
  /**
112
135
  * Count same-path calls in the window that are "revisits" — accesses that end
113
136
  * no further into the file than the furthest line already covered for that
114
- * path. Paging yields zero revisits and never trips; repeated whole-file
115
- * re-reads, backward jumps and path-targeting greps accumulate.
137
+ * path. The FIRST call on a path is therefore never a revisit: it sets the
138
+ * high-water mark. Paging yields zero revisits and never trips; repeated
139
+ * whole-file re-reads, backward jumps, narrower re-reads and repeated
140
+ * path-targeting greps all accumulate.
116
141
  *
117
- * The comparison is on the RANGE, not the offset. Offset alone scored a page
118
- * that re-asks from the same start with a bigger limit `{offset:80,
119
- * limit:400}` after `{offset:80, limit:300}`, real reads from a measured
120
- * decompose run as a revisit, even though it covers 100 lines the child had
121
- * never seen. That is the same mistake SingleReadGuard made by keying on the
122
- * path alone, and it is corrected the same way.
142
+ * The comparison is on the RANGE, not the offset. Keying on offset alone scores
143
+ * `{offset:80, limit:400}` after `{offset:80, limit:300}` as a revisit, even
144
+ * though it covers 100 lines the child has never seen. That is the same mistake
145
+ * SingleReadGuard made by keying on the path alone, and it is corrected the same
146
+ * way.
123
147
  */
124
148
  countRevisits(path) {
125
149
  let maxEnd = -1;
@@ -1,26 +1,28 @@
1
1
  /**
2
2
  * What a typed message DOES while a task run owns the session.
3
3
  *
4
- * Before this module the two surfaces disagreed, and both were wrong whenever the
5
- * host session was idle — which is most of a run, since the spec phases and every
6
- * gate are child `pi` processes, not host turns:
4
+ * Both surfaces the terminal Enter listener and the remote browser need ONE
5
+ * rule, because for most of a run the HOST session is idle: the spec phases and
6
+ * every gate are child `pi` processes, not host turns. Left to itself each surface
7
+ * has its own failure on an idle session:
7
8
  *
8
- * - browser → `sendUserMessage` with no delivery mode, which starts a SECOND
9
- * turn alongside the run. Reproduced live on pi 0.82.1 (issue #8):
10
- * that turn ran the write tool into the project, and when it was
11
- * still streaming as the pipeline delivered its spec the run died
12
- * with "TASK_0001 failed: Agent is already processing."
13
- * - terminal → pi's `pendingUserInputs` queue, drained only by the main loop
14
- * that our own command handler is parked inside so the line sat
15
- * there silently and then fired minutes later against a finished
16
- * run.
9
+ * - browser → `sendUserMessage` with no delivery mode. pi's own API docs call it
10
+ * "Always triggers a turn", so it opens a SECOND turn beside the run,
11
+ * with the write tool in hand. If that turn is still streaming when
12
+ * the pipeline delivers its spec, the spec's own send hits pi's
13
+ * guard: "Agent is already processing. Specify streamingBehavior
14
+ * ('steer' or 'followUp') to queue the message."
15
+ * - terminal pi's `pendingUserInputs` queue, which only `getUserInput()` drains
16
+ * and that is the main loop our own command handler is parked
17
+ * inside. The line sits there silently, then fires against a
18
+ * finished run.
17
19
  *
18
- * The shared rule both surfaces now follow:
20
+ * The shared rule both surfaces follow:
19
21
  *
20
- * agent streaming → steer the live turn (unchanged; this half always worked)
22
+ * agent streaming → steer the live turn
21
23
  * agent idle, run active → HOLD it here, show it as pending, and deliver it as
22
24
  * a steer when the next task turn starts
23
- * no run active → ordinary message, unchanged
25
+ * no run active → ordinary message
24
26
  *
25
27
  * Holding is what makes the two surfaces agree AND keeps the run safe: a held
26
28
  * line never becomes a competing turn, and it is never silently replayed after
@@ -1,26 +1,28 @@
1
1
  /**
2
2
  * What a typed message DOES while a task run owns the session.
3
3
  *
4
- * Before this module the two surfaces disagreed, and both were wrong whenever the
5
- * host session was idle — which is most of a run, since the spec phases and every
6
- * gate are child `pi` processes, not host turns:
4
+ * Both surfaces the terminal Enter listener and the remote browser need ONE
5
+ * rule, because for most of a run the HOST session is idle: the spec phases and
6
+ * every gate are child `pi` processes, not host turns. Left to itself each surface
7
+ * has its own failure on an idle session:
7
8
  *
8
- * - browser → `sendUserMessage` with no delivery mode, which starts a SECOND
9
- * turn alongside the run. Reproduced live on pi 0.82.1 (issue #8):
10
- * that turn ran the write tool into the project, and when it was
11
- * still streaming as the pipeline delivered its spec the run died
12
- * with "TASK_0001 failed: Agent is already processing."
13
- * - terminal → pi's `pendingUserInputs` queue, drained only by the main loop
14
- * that our own command handler is parked inside so the line sat
15
- * there silently and then fired minutes later against a finished
16
- * run.
9
+ * - browser → `sendUserMessage` with no delivery mode. pi's own API docs call it
10
+ * "Always triggers a turn", so it opens a SECOND turn beside the run,
11
+ * with the write tool in hand. If that turn is still streaming when
12
+ * the pipeline delivers its spec, the spec's own send hits pi's
13
+ * guard: "Agent is already processing. Specify streamingBehavior
14
+ * ('steer' or 'followUp') to queue the message."
15
+ * - terminal pi's `pendingUserInputs` queue, which only `getUserInput()` drains
16
+ * and that is the main loop our own command handler is parked
17
+ * inside. The line sits there silently, then fires against a
18
+ * finished run.
17
19
  *
18
- * The shared rule both surfaces now follow:
20
+ * The shared rule both surfaces follow:
19
21
  *
20
- * agent streaming → steer the live turn (unchanged; this half always worked)
22
+ * agent streaming → steer the live turn
21
23
  * agent idle, run active → HOLD it here, show it as pending, and deliver it as
22
24
  * a steer when the next task turn starts
23
- * no run active → ordinary message, unchanged
25
+ * no run active → ordinary message
24
26
  *
25
27
  * Holding is what makes the two surfaces agree AND keeps the run safe: a held
26
28
  * line never becomes a competing turn, and it is never silently replayed after
@@ -12,8 +12,9 @@
12
12
  * /task-cancel cancel the running task (soft-terminal — still resumable)
13
13
  *
14
14
  * The orchestrator persists after every phase boundary to
15
- * <cwd>/.pi-tasks/TASK_NNNN.md. All user interaction during phases runs through
16
- * ctx.ui dialogs; the main pi chat only receives the final spec.
15
+ * <cwd>/.pi-tasks/TASK_NNNN.md. User interaction during phases runs through the
16
+ * SessionUI bridge — a local ctx.ui dialog raced against a remote browser card
17
+ * never through the main conversation, which only receives the final spec.
17
18
  */
18
19
  import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-coding-agent';
19
20
  import { type WidgetState } from './widget.js';
@@ -43,12 +44,13 @@ export interface TaskRunnerOptions {
43
44
  * `spawn` drives the Error-triage ladder or a real process. `runChild(name,
44
45
  * tools, prompt)` answers every phase child BY NAME, with none of the
45
46
  * ladder's guards — use it when the child is a premise of the test, not its
46
- * subject. `runWorker(label, input)` answers every research worker by name,
47
- * so the three Research retry gates are reachable without matching a marker
48
- * sentence inside a prompt. The EXTERNAL CONTEXT lookups and the file
47
+ * subject. `runWorker(label, input)` answers every research worker by name, so
48
+ * the research phase's retry gates (`zeroRetrievalRetry`, `retryIfSilent`, the
49
+ * restart ladder) are reachable without matching a marker sentence inside a
50
+ * prompt. The EXTERNAL CONTEXT lookups and the file
49
51
  * inventory each default to the real implementation when absent. `timeoutMs`,
50
- * `sleepFor`, `childExtensions` and `logDebug` are seams too, and were
51
- * unreachable from here until this became one field.
52
+ * `sleepFor`, `childExtensions` and `logDebug` are seams too, and travel in the
53
+ * same field.
52
54
  */
53
55
  seams?: PhaseSeams;
54
56
  /** Called with the resolved task id once its file exists, before any phase
@@ -96,9 +98,9 @@ export declare class TaskRunner {
96
98
  private readonly _deps;
97
99
  private readonly _pc;
98
100
  /**
99
- * Per-phase wall-clock durations collected during the run. Written to the
100
- * `## phase timings` section on successful completion so we can spot
101
- * regressions and target future speed work. Each top-level entry is a
101
+ * Per-phase wall-clock durations collected during the run, written to the
102
+ * `## phase timings` section on completion, and again from the catch so a
103
+ * failed run still records what it got through. Each top-level entry is a
102
104
  * phase (refine/research/grill/compose/critique); children are optional
103
105
  * sub-step splits the phase chose to record via deps.recordSubStep.
104
106
  */
@@ -111,16 +113,15 @@ export declare class TaskRunner {
111
113
  status(): WidgetState | null;
112
114
  /** Cancel the running task by aborting the signal. */
113
115
  cancel(): void;
114
- /** Execute the full task lifecycle. Mid-run input holds instead of starting
115
- * a competing turn for the whole of it, and the terminal interception is
116
- * armed for the same window (`withRun`); nested inside `runGatedTask` or
117
- * the `/task-auto` loop the bracket refcounts, so this changes nothing there
118
- * and covers the fire-and-forget `runSingleTask` path on its own. */
119
116
  /**
120
- * Run the task and NAME how it ended.
117
+ * Run the task and NAME how it ended (`RunEnd`), so no caller has to re-read
118
+ * the task file to find out.
121
119
  *
122
- * This returned `void`, so the caller re-read the task file to learn what had
123
- * just happened here see run-end.ts for the report that got wrong.
120
+ * Mid-run input holds instead of starting a competing turn for the whole of
121
+ * it, and the terminal interception is armed for the same window (`withRun`);
122
+ * nested inside `runGatedTask` or the `/task-auto` loop the bracket refcounts,
123
+ * so this changes nothing there and covers the fire-and-forget
124
+ * `runSingleTask` path on its own.
124
125
  */
125
126
  run(): Promise<RunEnd>;
126
127
  private _run;
@@ -163,8 +164,8 @@ export interface RunSingleTaskOptions extends Pick<TaskRunnerOptions, 'resumeId'
163
164
  notifyFinish?: boolean;
164
165
  /**
165
166
  * How the implementation turn's thinking level is read and written. Defaults
166
- * to the live pi session; injected by tests, which must be able to assert
167
- * the restore without a real session to restore.
167
+ * to the live pi session; injectable so the hold-and-restore is assertable
168
+ * with no real session to restore.
168
169
  */
169
170
  thinkingControl?: ThinkingControl;
170
171
  }
@@ -172,11 +173,7 @@ export interface RunSingleTaskResult {
172
173
  taskId: string;
173
174
  /**
174
175
  * How the run ended, named by the runner rather than re-derived from disk.
175
- *
176
- * This was `ok: boolean` plus `sessionCancelled`, `interrupted` and `reason`
177
- * — four fields for one fact, three of them smuggled out of the `withSession`
178
- * closure through mutable captures. `/task-cancel` fell into the `!ok` arm
179
- * and was reported as a failure; see run-end.ts.
176
+ * One value for one fact: see run-end.ts for the endings and their policy.
180
177
  */
181
178
  end: RunEnd;
182
179
  /**
@@ -194,9 +191,8 @@ export interface RunSingleTaskResult {
194
191
  * deliver its spec. With waitForImplementation, block until the agent finishes
195
192
  * implementing the delivered spec.
196
193
  *
197
- * The ending comes from `TaskRunner.run`. It used to be read back off the task
198
- * file's front matter — a disk round-trip this process made to learn what it had
199
- * just done, and one that could not tell a cancel from a failure.
194
+ * The ending in the result comes from `TaskRunner.run` itself, not from re-reading
195
+ * the task file's front matter — which cannot tell a cancel from a failure.
200
196
  */
201
197
  export declare function runSingleTask(ctx: ExtensionCommandContext, cwd: string, rawPrompt: string, opts?: RunSingleTaskOptions): Promise<RunSingleTaskResult>;
202
198
  /**
@@ -12,8 +12,9 @@
12
12
  * /task-cancel cancel the running task (soft-terminal — still resumable)
13
13
  *
14
14
  * The orchestrator persists after every phase boundary to
15
- * <cwd>/.pi-tasks/TASK_NNNN.md. All user interaction during phases runs through
16
- * ctx.ui dialogs; the main pi chat only receives the final spec.
15
+ * <cwd>/.pi-tasks/TASK_NNNN.md. User interaction during phases runs through the
16
+ * SessionUI bridge — a local ctx.ui dialog raced against a remote browser card
17
+ * never through the main conversation, which only receives the final spec.
17
18
  */
18
19
  import * as fsp from 'node:fs/promises';
19
20
  import * as path from 'node:path';
@@ -25,6 +26,7 @@ import { readTextFile } from '../shared/fs-text.js';
25
26
  import { allocateTaskId, ensureTasksDir, readSection, readTaskFile, setTaskSection, taskFilePath, tasksDir, updateTaskFrontMatter, writeTaskFile } from './task-io.js';
26
27
  import { startWidget } from './widget.js';
27
28
  import { armImplWidget, disarmImplWidget, setupImplWidget } from './impl-widget.js';
29
+ import { armImplementationGuard, disarmImplementationGuard } from './implementation-guards.js';
28
30
  import { publishViewer, publishNotify, registerBridgeCommand, getBridge } from '../remote/bridge.js';
29
31
  import { pushNotify } from '../remote/push.js';
30
32
  import { getConfig } from '../config/config.js';
@@ -61,11 +63,12 @@ let piApi = null;
61
63
  /**
62
64
  * The live session's thinking level, as a {@link ThinkingControl}.
63
65
  *
64
- * Goes through `piApi` rather than the command ctx because
65
- * `ExtensionCommandContext` exposes `thinkingLevel` READ-ONLY; the setter lives
66
- * on the extension API. Before `registerTask(pi)` has run there is nothing to
67
- * control, so this degrades to a no-op pair rather than throwing a task that
68
- * cannot move the level should still run.
66
+ * Goes through `piApi` rather than the command ctx because `ExtensionContext`
67
+ * (which the command ctx extends) carries `thinkingLevel` as a plain optional
68
+ * VALUE with no setter; `setThinkingLevel` lives on `ExtensionAPI`. Before
69
+ * `registerTask(pi)` has run there is nothing to control, so this degrades to a
70
+ * no-op pair rather than throwing — a task that cannot move the level should
71
+ * still run.
69
72
  */
70
73
  function piThinkingControl() {
71
74
  const api = piApi;
@@ -93,9 +96,9 @@ export class TaskRunner {
93
96
  _deps;
94
97
  _pc;
95
98
  /**
96
- * Per-phase wall-clock durations collected during the run. Written to the
97
- * `## phase timings` section on successful completion so we can spot
98
- * regressions and target future speed work. Each top-level entry is a
99
+ * Per-phase wall-clock durations collected during the run, written to the
100
+ * `## phase timings` section on completion, and again from the catch so a
101
+ * failed run still records what it got through. Each top-level entry is a
99
102
  * phase (refine/research/grill/compose/critique); children are optional
100
103
  * sub-step splits the phase chose to record via deps.recordSubStep.
101
104
  */
@@ -113,8 +116,8 @@ export class TaskRunner {
113
116
  this._fixInstruction = opts.fixInstruction;
114
117
  this._implAwaited = opts.implAwaited ?? false;
115
118
  this._startedAt = Date.now();
116
- // We'll populate id/title/phase lazily in run().
117
- // Placeholder real values set in run().
119
+ // Placeholder: id/title/phase are only known once run() has allocated or
120
+ // read the task file.
118
121
  this._widgetState = {
119
122
  taskId: '',
120
123
  title: '',
@@ -137,7 +140,7 @@ export class TaskRunner {
137
140
  },
138
141
  // Handed DOWN so the child's own snapshot carries a window and the
139
142
  // StallDetector's churn rule can arm; resolveContextUsage below stays
140
- // as the fallback for a child that still reports none (issue #16).
143
+ // as the fallback for a child that still reports none.
141
144
  contextWindow: parentContextWindow,
142
145
  onContextUsage: snapshot => {
143
146
  this._widgetState.contextUsage = resolveContextUsage(snapshot, this._widgetState.contextUsage, parentContextWindow);
@@ -175,16 +178,15 @@ export class TaskRunner {
175
178
  cancel() {
176
179
  this._abort.abort();
177
180
  }
178
- /** Execute the full task lifecycle. Mid-run input holds instead of starting
179
- * a competing turn for the whole of it, and the terminal interception is
180
- * armed for the same window (`withRun`); nested inside `runGatedTask` or
181
- * the `/task-auto` loop the bracket refcounts, so this changes nothing there
182
- * and covers the fire-and-forget `runSingleTask` path on its own. */
183
181
  /**
184
- * Run the task and NAME how it ended.
182
+ * Run the task and NAME how it ended (`RunEnd`), so no caller has to re-read
183
+ * the task file to find out.
185
184
  *
186
- * This returned `void`, so the caller re-read the task file to learn what had
187
- * just happened here see run-end.ts for the report that got wrong.
185
+ * Mid-run input holds instead of starting a competing turn for the whole of
186
+ * it, and the terminal interception is armed for the same window (`withRun`);
187
+ * nested inside `runGatedTask` or the `/task-auto` loop the bracket refcounts,
188
+ * so this changes nothing there and covers the fire-and-forget
189
+ * `runSingleTask` path on its own.
188
190
  */
189
191
  async run() {
190
192
  return withRun(this._ctx, {}, () => this._run());
@@ -226,11 +228,12 @@ export class TaskRunner {
226
228
  await this._onStart(id);
227
229
  // Wire up per-task debug log (<cwd>/.pi-tasks/TASK_XXXX-debug.log).
228
230
  const debugLogPath = path.join(tasksDir(cwd), `${id}-debug.log`);
229
- // Left UNSET at level `off`, so the ~39 `logDebug?.(…)` sites downstream
230
- // short-circuit before they format a string and the file is never created.
231
- // A caller-supplied `logDebug` seam WINS: it is the only way to observe
232
- // the ~39 trail decisions from a runner-driven test, and production never
233
- // sets one, so the file writer is unaffected.
231
+ // `gateDebugWriter` returns undefined at level `off`, so every
232
+ // `logDebug?.(…)` site downstream short-circuits before it formats a string
233
+ // and the file is never created. A caller-supplied `logDebug` seam WINS
234
+ // (`??=`): it is the only way to observe the trail decisions from a
235
+ // runner-driven test, and production never sets one, so the file writer is
236
+ // unaffected.
234
237
  this._deps.logDebug ??= gateDebugWriter((msg) => {
235
238
  const line = `${new Date().toISOString()} ${msg}\n`;
236
239
  fsp.appendFile(debugLogPath, line).catch(() => {
@@ -295,9 +298,9 @@ export class TaskRunner {
295
298
  await postCommitPhase(phase, this._deps, this._pc, out);
296
299
  // SAFE CHECKPOINT (phase boundary): this phase's output is on disk
297
300
  // and the next `advance()` has not moved front-matter forward, so a
298
- // resume re-enters at exactly this phase. Without this the whole
299
- // spec pipeline (refine→critique, ~4.5 min on the mx5 run, research
300
- // alone ~3 min) runs to completion after a cancel is requested.
301
+ // resume re-enters at exactly this phase. Without a checkpoint here
302
+ // the whole refine→critique pipeline runs to completion after a
303
+ // cancel is requested.
301
304
  // Throwing USER_CANCELLED reuses the existing cancellation path:
302
305
  // handleFailure leaves the task resumable and /task-auto's catch
303
306
  // announces the resume hint.
@@ -362,12 +365,24 @@ export class TaskRunner {
362
365
  };
363
366
  if (this._sendSpec) {
364
367
  armImplWidget(meta, { oneShot: !this._implAwaited });
368
+ // Same lifetime as the widget, and for the same reason: an awaited run
369
+ // spans resume and steer turns, a fire-and-forget one does not.
370
+ armImplementationGuard({ oneShot: !this._implAwaited });
371
+ let delivered = false;
365
372
  try {
366
373
  await this._sendSpec(spec);
374
+ delivered = true;
367
375
  }
368
376
  finally {
369
- if (this._implAwaited)
377
+ // Arming precedes delivery because the turn can begin inside it. So a
378
+ // delivery that THREW leaves a guard watching a turn that will never
379
+ // run: pi's `prompt` rejects on a compaction already in progress, a
380
+ // missing model, or a failed auth. The next unrelated turn would
381
+ // inherit it, and this guard can end a turn outright.
382
+ if (this._implAwaited || !delivered) {
370
383
  disarmImplWidget();
384
+ disarmImplementationGuard();
385
+ }
371
386
  }
372
387
  return;
373
388
  }
@@ -375,11 +390,24 @@ export class TaskRunner {
375
390
  throw new Error('extension not initialised (no ExtensionAPI captured)');
376
391
  }
377
392
  armImplWidget(meta, { oneShot: true });
378
- // Always name a delivery mode. pi ignores it when the session is idle and
379
- // uses it when something else is streaming so this one call is correct
380
- // in both cases, where an isIdle() check is a check-then-act race that
381
- // loses to any turn starting in between (issue #8).
382
- piApi.sendUserMessage(spec, { deliverAs: 'followUp' });
393
+ armImplementationGuard({ oneShot: true });
394
+ // Same reason as the awaited path's `delivered` flag: this send can throw
395
+ // SYNCHRONOUSLY the loader gates every ExtensionAPI action behind
396
+ // `assertActive()` and a guard left armed over a turn that never starts
397
+ // is inherited by the next one, which it can terminate.
398
+ // Always name a delivery mode. pi's `prompt()` consults `streamingBehavior`
399
+ // only inside `if (this.isStreaming)`, so naming one is inert on an idle
400
+ // session and queues on a busy one — correct in both cases, where an
401
+ // isIdle() check is a check-then-act race that loses to any turn starting
402
+ // in between.
403
+ try {
404
+ piApi.sendUserMessage(spec, { deliverAs: 'followUp' });
405
+ }
406
+ catch (e) {
407
+ disarmImplWidget();
408
+ disarmImplementationGuard();
409
+ throw e;
410
+ }
383
411
  }
384
412
  /**
385
413
  * The spec as the implementer should receive it (Layer B). Layer A strips phantom
@@ -418,9 +446,8 @@ export class TaskRunner {
418
446
  * deliver its spec. With waitForImplementation, block until the agent finishes
419
447
  * implementing the delivered spec.
420
448
  *
421
- * The ending comes from `TaskRunner.run`. It used to be read back off the task
422
- * file's front matter — a disk round-trip this process made to learn what it had
423
- * just done, and one that could not tell a cancel from a failure.
449
+ * The ending in the result comes from `TaskRunner.run` itself, not from re-reading
450
+ * the task file's front matter — which cannot tell a cancel from a failure.
424
451
  */
425
452
  export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
426
453
  let taskId = '';
@@ -441,10 +468,12 @@ export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
441
468
  withSession: async (newCtx) => {
442
469
  freshCtx = newCtx;
443
470
  getBridge().currentCtx = newCtx; // keep remote dispatch ctx fresh across session replacement
444
- // Same reason, for the terminal: pi clears every extension terminal
445
- // input listener when the old session is invalidated, and that
446
- // happens at the START of each task — precisely the window a typed
447
- // /task-auto-cancel has to survive. No-op unless a run armed one.
471
+ // Same reason, for the terminal: pi wires
472
+ // `setBeforeSessionInvalidate(() => this.resetExtensionUI())`, and
473
+ // `resetExtensionUI` calls `clearExtensionTerminalInputListeners()`.
474
+ // Every extension listener dies at the START of each task — precisely
475
+ // the window a typed /task-auto-cancel has to survive. No-op unless a
476
+ // run armed one.
448
477
  rearmCancelListener(newCtx);
449
478
  const runner = new TaskRunner({
450
479
  ctx: newCtx,
@@ -460,7 +489,8 @@ export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
460
489
  // here rather than at either call site.
461
490
  const release = holdImplementationThinking(opts.thinkingControl ?? piThinkingControl());
462
491
  try {
463
- // Queue-or-run: never throws, whatever else is on the session (issue #8).
492
+ // Queue-or-run: naming a delivery mode means pi's
493
+ // "Agent is already processing" throw is unreachable here.
464
494
  await newCtx.sendUserMessage(spec, { deliverAs: 'followUp' });
465
495
  if (opts.waitForImplementation) {
466
496
  await newCtx.waitForIdle();
@@ -502,12 +532,9 @@ export async function runSingleTask(ctx, cwd, rawPrompt, opts = {}) {
502
532
  // delivered and the task file already reads `completed`:
503
533
  //
504
534
  // • the user interrupted and declined to steer — a pause, not a fault;
505
- // • the implementation turn died with stopReason "error" (a context-overflow
506
- // 400, say), which must stop /task-auto here rather than let it commit and
507
- // advance on a file that says `completed`.
508
- //
509
- // Both used to reach the caller as extra fields beside a boolean, and both are
510
- // endings.
535
+ // • the implementation turn died with stopReason "error", which must stop
536
+ // /task-auto here rather than let it commit and advance on a file that
537
+ // says `completed`.
511
538
  let end = runEnd;
512
539
  if (end.kind === 'completed' && interrupted)
513
540
  end = { kind: 'interrupted' };
@@ -536,9 +563,9 @@ export const gateRunTask = (c, cwd, t, opts) => runSingleTask(c, cwd, t, {
536
563
  // NO `seams` here, deliberately. Threading them would need a field on
537
564
  // `GateParams` and another on `GateDeps`, and nothing — production or
538
565
  // test — would set either: the gate is reached through two orchestrators
539
- // that build their params from a task file. An unused field on a seam
540
- // roster is the `WorkerOutcome.reason` shape (written twelve times, read
541
- // by nothing), so this stays unplumbed until a test actually needs it.
566
+ // that build their params from a task file. `WorkerOutcome.reason` is what
567
+ // that costs: written at every `workerUnavailable` call site and read
568
+ // nowhere. So this stays unplumbed until a test actually needs it.
542
569
  });
543
570
  /**
544
571
  * Demote a task file to a resumable state after a gate (or its implementation)
@@ -567,15 +594,13 @@ export async function markResumable(cwd, taskId) {
567
594
  export async function runGatedTask(ctx, cwd, raw, opts = {}) {
568
595
  // The GATES are part of the run, and they are child processes with the host
569
596
  // session idle — the same hold window as the spec phases. Bracketing only
570
- // TaskRunner would leave verify/enforce looking like "no run", which a live
571
- // run on pi 0.82.1 showed as runActive=false while the widget still read
572
- // "verifying work" (issue #8). The body has many early returns, so the
573
- // bracket lives in this wrapper rather than in a dozen places. The same
574
- // bracket arms the raw-stdin interception for the WHOLE run: without it a
575
- // plain /task had no terminal path at all — only /task-auto armed one so a
576
- // line typed during it went into pi's queue and fired after the run (seen
577
- // live). No onCancel: a typed /task-auto-cancel goes through the generic
578
- // bridge dispatch here.
597
+ // TaskRunner would leave verify/enforce reading as "no run" while the widget
598
+ // still says "verifying work". The body has many early returns, so the bracket
599
+ // lives in this wrapper rather than in a dozen places. The same bracket arms
600
+ // the raw-stdin interception for the WHOLE run: without it a line typed during
601
+ // a plain /task lands in pi's `pendingUserInputs` and fires after the run. No
602
+ // onCancel: a typed /task-auto-cancel goes through the generic bridge dispatch
603
+ // here.
579
604
  await withRun(ctx, {}, () => runGatedTaskInner(ctx, cwd, raw, opts));
580
605
  }
581
606
  async function runGatedTaskInner(ctx, cwd, raw, opts = {}) {
@@ -595,10 +620,8 @@ async function runGatedTaskInner(ctx, cwd, raw, opts = {}) {
595
620
  const res = await deps.runTask(active, cwd, raw, { resumeId: opts.resumeId });
596
621
  active = res.ctx ?? active;
597
622
  const tag = res.taskId || 'Task';
598
- // One dispatch over the named ending. The four-branch ladder this replaces put
599
- // a CANCELLED run into the `!ok` arm, so `/task-cancel` overwrote the file's
600
- // `cancelled` with `failed` and told the user their task had stopped and needed
601
- // fixing. Resumability comes from RUN_END_POLICY; the wording stays here,
623
+ // One dispatch over the named ending, so a cancel can never be reported as a
624
+ // failure. Resumability comes from RUN_END_POLICY; the wording stays here,
602
625
  // because `/task-auto` says `/task-auto-resume` where this says `/task-resume`.
603
626
  if (!runSucceeded(res.end)) {
604
627
  const policy = RUN_END_POLICY[res.end.kind];
@@ -656,7 +679,7 @@ async function handleTask(args, ctx) {
656
679
  // When a gate is enabled, /task awaits the implementation and runs the same
657
680
  // verify + enforce gates a /task-auto sub-task does. With both gates off
658
681
  // (the default), /task stays fire-and-forget: hand the spec to the main
659
- // conversation and return immediately, exactly as before.
682
+ // conversation and return immediately.
660
683
  const cfg = getConfig();
661
684
  if (cfg.verifyWork || cfg.enforceGuidelines) {
662
685
  await runGatedTask(ctx, cwd, raw);