@mjasnikovs/pi-task 0.38.29 → 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 +16 -9
  104. package/dist/task/context-usage.js +16 -9
  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 +36 -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 +179 -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 +29 -39
  231. package/dist/task/research-worker.js +37 -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 +69 -71
  338. package/dist/workers/pi-worker-core.js +100 -109
  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 +21 -14
  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
@@ -1,34 +1,30 @@
1
1
  /**
2
- * In-process thrash guards for the TOOLING research worker.
2
+ * In-process thrash guards, armed by single-read-extension.ts in the TOOLING
3
+ * research worker and the planning children.
3
4
  *
4
- * Two guards, one mechanism — deny a wasteful repeat *inside the run* (the
5
+ * Two guards, one mechanism — deny a wasteful repeat *inside the run*. The
5
6
  * extension returns the block from a `tool_call` handler, pi feeds `reason` back
6
- * as an error tool result, the worker continues). No kill, no restart: detect-
7
- * and-kill only re-spawns a model that deterministically re-thrashes.
7
+ * as an error tool result, and the child continues. No kill, no restart:
8
+ * detect-and-kill only re-spawns a model that deterministically re-thrashes.
8
9
  *
9
- * - SingleReadGuard: "read each LINE of a file once". Validated against every
10
- * recorded mx5 run — a healthy TOOLING worker reads each file exactly once
11
- * (max same-file reads = 1 across 7 tasks). TASK_0017 re-read one file 50×.
10
+ * - SingleReadGuard: "read each LINE of a file once".
12
11
  *
13
- * It used to key on the resolved path alone, blocking any second read
14
- * "regardless of offset". That made a file bigger than one read into a trap.
15
- * Measured 2026-08-17 on a captured auto-decompose request: the planner asked
16
- * for `DESIGN/marketplace.html` with `limit: 80` the first 80 lines of 743,
17
- * deliberately paging and its request for offset 80 was refused. It never
18
- * reached the end of the file, and spent the rest of the run asking for the
19
- * remainder: 197 of 200 tool calls were this guard's own refusal. The guard
20
- * did not stop a thrash, it CAUSED one.
12
+ * Keying on the resolved path alone blocking any second read regardless of
13
+ * offset turns a file bigger than one read into a TRAP. A child paging
14
+ * deliberately, first 80 lines then the next 80, has its second request
15
+ * refused; it never reaches the end of the file and spends the rest of the run
16
+ * asking for the remainder. That guard does not stop a thrash, it causes one.
21
17
  *
22
- * So the unit is the line range, not the file. A request that extends past
23
- * the furthest line already delivered is forward paging and passes; one that
24
- * lies entirely within ground already delivered is a re-read and is blocked.
18
+ * So the unit is the line range, not the file. A request that extends past the
19
+ * furthest line already delivered is forward paging and passes; one that lies
20
+ * entirely within ground already delivered is a re-read and is blocked.
25
21
  *
26
- * - RepeatedCallGuard: "no identical search twice", for grep/find/ls. TASK_0017
27
- * also looped on grep({pattern:"^\\s*}",path:".../index.ts"}) ×5 a path the
28
- * read guard never covered, so the call fell back to the ineffective detect→
29
- * restart path. Keyed on (toolName, stableStringify(args)) the *same* key
30
- * the LoopDetector uses so only byte-identical repeats trip; a legitimately
31
- * different grep pattern on the same file still passes.
22
+ * - RepeatedCallGuard: "no identical search twice", for grep/find/ls — the
23
+ * shapes the read guard cannot see, such as the same grep pattern re-run
24
+ * against the same path. Keyed on `${toolName}\0${stableStringify(args)}`,
25
+ * byte-identical to the key `LoopDetector.record` builds, so argument key
26
+ * order never causes a miss and only an identical repeat trips. A different
27
+ * pattern on the same file still passes.
32
28
  *
33
29
  * Pure logic, no I/O — the extension does path resolution and tool routing.
34
30
  */
@@ -40,10 +36,11 @@ export interface ReadBlock {
40
36
  * The error text the model receives in place of the re-read's contents.
41
37
  *
42
38
  * It must say what to do NEXT, and the honest next move depends on whether there
43
- * is any of the file left: with `covered` lines already delivered, asking for
44
- * line `covered + 1` is always allowed, so the message says so. The old wording
45
- * ("Do not read it again") was a dead end for a model that was mid-way through a
46
- * file it had nowhere legal to go and kept asking anyway.
39
+ * is any of the file left: with `covered` lines already delivered, asking for line
40
+ * `covered + 1` is always allowed, so the message says so. A bare "do not read it
41
+ * again" is a dead end for a model mid-way through a file — it has nowhere legal
42
+ * to go and keeps asking anyway. A read that reached EOF has `covered` set to
43
+ * Infinity and gets the other wording, with no line to resume from.
47
44
  */
48
45
  export declare function singleReadReason(path: string, covered: number): string;
49
46
  export declare class SingleReadGuard {
@@ -1,34 +1,30 @@
1
1
  /**
2
- * In-process thrash guards for the TOOLING research worker.
2
+ * In-process thrash guards, armed by single-read-extension.ts in the TOOLING
3
+ * research worker and the planning children.
3
4
  *
4
- * Two guards, one mechanism — deny a wasteful repeat *inside the run* (the
5
+ * Two guards, one mechanism — deny a wasteful repeat *inside the run*. The
5
6
  * extension returns the block from a `tool_call` handler, pi feeds `reason` back
6
- * as an error tool result, the worker continues). No kill, no restart: detect-
7
- * and-kill only re-spawns a model that deterministically re-thrashes.
7
+ * as an error tool result, and the child continues. No kill, no restart:
8
+ * detect-and-kill only re-spawns a model that deterministically re-thrashes.
8
9
  *
9
- * - SingleReadGuard: "read each LINE of a file once". Validated against every
10
- * recorded mx5 run — a healthy TOOLING worker reads each file exactly once
11
- * (max same-file reads = 1 across 7 tasks). TASK_0017 re-read one file 50×.
10
+ * - SingleReadGuard: "read each LINE of a file once".
12
11
  *
13
- * It used to key on the resolved path alone, blocking any second read
14
- * "regardless of offset". That made a file bigger than one read into a trap.
15
- * Measured 2026-08-17 on a captured auto-decompose request: the planner asked
16
- * for `DESIGN/marketplace.html` with `limit: 80` the first 80 lines of 743,
17
- * deliberately paging and its request for offset 80 was refused. It never
18
- * reached the end of the file, and spent the rest of the run asking for the
19
- * remainder: 197 of 200 tool calls were this guard's own refusal. The guard
20
- * did not stop a thrash, it CAUSED one.
12
+ * Keying on the resolved path alone blocking any second read regardless of
13
+ * offset turns a file bigger than one read into a TRAP. A child paging
14
+ * deliberately, first 80 lines then the next 80, has its second request
15
+ * refused; it never reaches the end of the file and spends the rest of the run
16
+ * asking for the remainder. That guard does not stop a thrash, it causes one.
21
17
  *
22
- * So the unit is the line range, not the file. A request that extends past
23
- * the furthest line already delivered is forward paging and passes; one that
24
- * lies entirely within ground already delivered is a re-read and is blocked.
18
+ * So the unit is the line range, not the file. A request that extends past the
19
+ * furthest line already delivered is forward paging and passes; one that lies
20
+ * entirely within ground already delivered is a re-read and is blocked.
25
21
  *
26
- * - RepeatedCallGuard: "no identical search twice", for grep/find/ls. TASK_0017
27
- * also looped on grep({pattern:"^\\s*}",path:".../index.ts"}) ×5 a path the
28
- * read guard never covered, so the call fell back to the ineffective detect→
29
- * restart path. Keyed on (toolName, stableStringify(args)) the *same* key
30
- * the LoopDetector uses so only byte-identical repeats trip; a legitimately
31
- * different grep pattern on the same file still passes.
22
+ * - RepeatedCallGuard: "no identical search twice", for grep/find/ls — the
23
+ * shapes the read guard cannot see, such as the same grep pattern re-run
24
+ * against the same path. Keyed on `${toolName}\0${stableStringify(args)}`,
25
+ * byte-identical to the key `LoopDetector.record` builds, so argument key
26
+ * order never causes a miss and only an identical repeat trips. A different
27
+ * pattern on the same file still passes.
32
28
  *
33
29
  * Pure logic, no I/O — the extension does path resolution and tool routing.
34
30
  */
@@ -37,10 +33,11 @@ import { stableStringify } from '../task/loop-detector.js';
37
33
  * The error text the model receives in place of the re-read's contents.
38
34
  *
39
35
  * It must say what to do NEXT, and the honest next move depends on whether there
40
- * is any of the file left: with `covered` lines already delivered, asking for
41
- * line `covered + 1` is always allowed, so the message says so. The old wording
42
- * ("Do not read it again") was a dead end for a model that was mid-way through a
43
- * file it had nowhere legal to go and kept asking anyway.
36
+ * is any of the file left: with `covered` lines already delivered, asking for line
37
+ * `covered + 1` is always allowed, so the message says so. A bare "do not read it
38
+ * again" is a dead end for a model mid-way through a file — it has nowhere legal
39
+ * to go and keeps asking anyway. A read that reached EOF has `covered` set to
40
+ * Infinity and gets the other wording, with no line to resume from.
44
41
  */
45
42
  export function singleReadReason(path, covered) {
46
43
  if (!Number.isFinite(covered)) {
@@ -53,7 +50,8 @@ export function singleReadReason(path, covered) {
53
50
  + `starting at line ${covered + 1}; otherwise use what you have already gathered and `
54
51
  + `write your final answer now.`);
55
52
  }
56
- /** Default `limit` pi's read tool applies when the call names none. */
53
+ /** pi's own read truncation ceiling (`DEFAULT_MAX_LINES` in its truncate.js): a
54
+ * read that names no `limit` returns at most this many lines. */
57
55
  const DEFAULT_READ_LIMIT = 2000;
58
56
  /** The 1-based line a read starts at (`offset` absent or junk means line 1). */
59
57
  function startLine(offset) {
@@ -62,9 +60,11 @@ function startLine(offset) {
62
60
  : 1;
63
61
  }
64
62
  /**
65
- * The last line a read reaches. A `limit` of exactly the tool default is treated
66
- * as "no limit given" — indistinguishable at this layer, and the safe reading is
67
- * the generous one, since blocking honest paging is the failure this guard had.
63
+ * The last line a read reaches, or Infinity when the read is unbounded. A `limit`
64
+ * at or above pi's own ceiling is treated as "no limit given" — indistinguishable
65
+ * at this layer and so is a `limit` that is absent, zero, negative or not a
66
+ * number. The generous reading is the safe one, since blocking honest paging is
67
+ * the failure mode this guard has.
68
68
  */
69
69
  function endLine(start, limit) {
70
70
  if (typeof limit !== 'number' || !Number.isFinite(limit) || limit <= 0)
@@ -23,12 +23,12 @@ export interface TypeOnlyLogRecord {
23
23
  * The FULL verification record behind `excerptVerified` — the normalised excerpt that was
24
24
  * searched for, plus a sha256 + length of the normalised content it was searched in.
25
25
  *
26
- * This is the other half of the F-3(f) hole described above: retaining the answer text
27
- * says WHAT was claimed, and this says what it was checked against, so a false verdict can
28
- * be attributed to fabrication (the excerpt is nowhere near the content) rather than a
29
- * normaliser gap (a markdown-escape variant of text that IS present) without re-running
30
- * the lookup. Only fetch's extractor used to keep it; all four focused-extractor call
31
- * sites now can (workers/focused-extractor.ts). Optional — records predating it parse.
26
+ * The other half of the retention above: the answer text says WHAT was claimed, and
27
+ * this says what it was checked against so a false verdict can be attributed to
28
+ * fabrication (the excerpt is nowhere near the content) rather than to a normaliser
29
+ * gap (a markdown-escape variant of text that IS present) without re-running the
30
+ * lookup. focused-extractor.ts hands the struct to both of its call sites.
31
+ * Optional — older records still parse.
32
32
  */
33
33
  excerptCheck?: ExcerptVerification;
34
34
  /**
@@ -50,9 +50,12 @@ export interface TypeOnlyLogRecord {
50
50
  /**
51
51
  * Append one record to the JSONL sink named by `PI_TASK_TYPEONLY_LOG`, if set.
52
52
  *
53
- * @param rec everything but `at` and `unclear`, which are derived here so every call site
54
- * stamps them identically.
53
+ * @param rec everything but `at` (an ISO timestamp) and `unclear` (the abstention
54
+ * predicate over `answer`), which are derived here so both call sites in
55
+ * pi-worker-docs stamp them identically.
55
56
  */
56
57
  export declare function logDocsAnswer(rec: Omit<TypeOnlyLogRecord, 'at' | 'unclear'>, getEnv?: (k: string) => string | undefined): void;
57
- /** Parse a sink written by {@link logDocsAnswer}; malformed lines are skipped, not thrown. */
58
+ /** Parse a sink written by {@link logDocsAnswer}. Blank lines are skipped and a
59
+ * malformed one — including the truncated tail a killed process leaves — is dropped
60
+ * rather than thrown. */
58
61
  export declare function readTypeOnlyLog(text: string): TypeOnlyLogRecord[];
@@ -1,40 +1,33 @@
1
1
  /**
2
- * STAGE 1 INSTRUMENTATION for F-2 / PROMPT 2 — firing-rate observability, no behaviour.
2
+ * Firing-rate instrumentation for the TYPE-ONLY detector. Observability only; it
3
+ * changes no behaviour.
3
4
  *
4
- * WHY THIS EXISTS. PROMPT 2's live A/B measured 82% baseline vs 91% treatment, p = 0.88,
5
- * and was written off as "the lever does not work". It was a broken EXPERIMENT, and it was
6
- * broken for a reason this module fixes: the metric counted ALL research terminations while
7
- * the lever touches only TYPE-ONLY answers, and nothing anywhere recorded how often a
8
- * type-only answer actually occurs. The causal claim "workers stop BECAUSE of type-only
9
- * answers" was asserted from one static corpus (1 flag in 150 recorded answers, 0.7%) and
10
- * never measured live. You cannot size an arm, pick a metric, or decide whether the lever is
11
- * a population fix or a single-case guard without that firing rate.
5
+ * WHY THIS EXISTS. "How often does a type-only answer occur" cannot be answered by
6
+ * counting the times the detector FIRED a log of firings has no denominator. So
7
+ * every pi-worker-docs answer is recorded, flagged or not, and the rate is
8
+ * computable rather than inferable.
12
9
  *
13
- * WHAT IT DOES. When `PI_TASK_TYPEONLY_LOG` names a file, every pi-worker-docs answer not
14
- * only the flagged ones appends one JSON line there. Denominator and numerator both, from
15
- * the same channel, so a rate is computable rather than inferable. With the variable unset
16
- * this is a no-op and nothing is written.
10
+ * WHAT IT DOES. When `PI_TASK_TYPEONLY_LOG` names a file, each answer appends one
11
+ * JSON line there. Unset or blank and this is a no-op that writes nothing.
17
12
  *
18
- * MECHANICAL, NOT SELF-REPORT. The record is written at the TOOL layer from the verdict the
19
- * shipped detector just returned, next to the same `details` the caller receives. No model is
20
- * asked whether it thought the answer was a type signature.
13
+ * MECHANICAL, NOT SELF-REPORT. The record is written at the TOOL layer from the
14
+ * verdict the shipped detector just returned, next to the same `details` the caller
15
+ * receives. No model is asked whether it thought the answer was a type signature.
21
16
  *
22
- * BEHAVIOUR-NEUTRALITY IS THE WHOLE POINT — this lands in a stage that forbids src/ behaviour
23
- * change, so:
17
+ * BEHAVIOUR-NEUTRAL BY CONSTRUCTION:
24
18
  * - it is called for its side effect only; nothing reads its return value;
25
- * - every failure is swallowed (a full disk or an unwritable path must not turn a working
26
- * docs lookup into an error — instrumentation that can break the thing it measures is
27
- * worse than no instrumentation);
28
- * - it appends synchronously, because the process that writes it (a pi child running the
29
- * docs extension) can exit immediately after the tool returns and a queued async write
30
- * would be lost.
19
+ * - every failure is swallowed. A full disk or an unwritable path must not turn a
20
+ * working docs lookup into an error — instrumentation that can break the thing
21
+ * it measures is worse than none;
22
+ * - it appends SYNCHRONOUSLY, because the process that writes it is a pi child
23
+ * that can exit the moment the tool returns, and a queued async write would be
24
+ * lost.
31
25
  *
32
- * THE FULL ANSWER TEXT IS RETAINED, deliberately. Run 15's audit could not decide F-3(f) —
33
- * whether an `excerptVerified === false` was fabrication or a normaliser gap — because the
34
- * text it judged was kept nowhere. The same hole would make every stability question here
35
- * unanswerable: whether a question is type-only in EVERY rep or churns between identical
36
- * reps can only be settled by re-scoring the recorded answers. Cheap to keep, impossible to
37
- * reconstruct later.
26
+ * THE FULL ANSWER TEXT IS RETAINED, deliberately. Without it, an
27
+ * `excerptVerified === false` cannot afterwards be attributed to fabrication rather
28
+ * than to a normaliser gap, and "is this question type-only every time or does it
29
+ * churn between identical runs" cannot be settled at all both need the recorded
30
+ * answers re-scored. Cheap to keep, impossible to reconstruct later.
38
31
  */
39
32
  import * as fs from 'node:fs';
40
33
  import { isAbstention } from './abstention.js';
@@ -43,8 +36,9 @@ export const TYPEONLY_LOG_ENV = 'PI_TASK_TYPEONLY_LOG';
43
36
  /**
44
37
  * Append one record to the JSONL sink named by `PI_TASK_TYPEONLY_LOG`, if set.
45
38
  *
46
- * @param rec everything but `at` and `unclear`, which are derived here so every call site
47
- * stamps them identically.
39
+ * @param rec everything but `at` (an ISO timestamp) and `unclear` (the abstention
40
+ * predicate over `answer`), which are derived here so both call sites in
41
+ * pi-worker-docs stamp them identically.
48
42
  */
49
43
  export function logDocsAnswer(rec, getEnv = k => process.env[k]) {
50
44
  const sink = getEnv(TYPEONLY_LOG_ENV);
@@ -63,7 +57,9 @@ export function logDocsAnswer(rec, getEnv = k => process.env[k]) {
63
57
  // succeeded must not be reported as failed because the sink was unwritable.
64
58
  }
65
59
  }
66
- /** Parse a sink written by {@link logDocsAnswer}; malformed lines are skipped, not thrown. */
60
+ /** Parse a sink written by {@link logDocsAnswer}. Blank lines are skipped and a
61
+ * malformed one — including the truncated tail a killed process leaves — is dropped
62
+ * rather than thrown. */
67
63
  export function readTypeOnlyLog(text) {
68
64
  const out = [];
69
65
  for (const line of text.split('\n')) {
@@ -1,28 +1,19 @@
1
1
  /**
2
2
  * worker-channels — what a worker TOOL is, as data.
3
3
  *
4
- * `makeWorkerTool` already gives every worker tool one registration adapter, but
5
- * `spec.name` never left the registration closure. So the same four name strings
6
- * were re-typed as literals in three directories and had to agree by hand:
4
+ * `makeWorkerTool` gives every worker tool one registration adapter, but
5
+ * `spec.name` never leaves that closure. Without this table the three tool names
6
+ * are re-typed as literals wherever anything else needs them: the tools string
7
+ * and its matching `-e` path list, the grounding set, the debug-log summary and
8
+ * its per-tool parameter shape, and — worst for locality — the GENERIC child
9
+ * runner, which would have to name one tool AND one of its parameters to decide a
10
+ * fan-out deadline extension. Nothing links those edits at compile time. Here
11
+ * each tool is one row.
7
12
  *
8
- * `phases.ts` paired `'…,pi-worker-docs'` with `DOCS_EXTENSION_PATH`, and
9
- * `',pi-worker-search,pi-worker-fetch'` with `SEARCH_EXTENSION_PATH` a tools
10
- * string and an `-e` path list that mean the same thing, written twice and
11
- * kept in step by eye.
12
- * • `GROUNDING_RETRIEVAL_TOOLS` was a second copy of the names.
13
- * • `summarizeToolArgs` was a third, and also re-stated each tool's parameter
14
- * shape (`module`/`query`, `query`, `url`).
15
- * • Worst for locality: `runWorker` — the GENERIC child runner — hardcoded one
16
- * tool's identity AND its parameter, `call.name === 'pi-worker-docs' &&
17
- * args.module === '.'`, to decide a fan-out deadline extension.
18
- *
19
- * A rename or a new tool was five edits in three directories with no compile
20
- * error linking them. It is one row here now.
21
- *
22
- * What is NOT in a row: the tools string's non-worker members (`read`, `grep`,
23
- * `find`, `ls`) are pi's own built-ins, not channels — they appear in
24
- * {@link GROUNDING_RETRIEVAL_TOOLS} because grounding is about RETRIEVAL, not
25
- * about which extension supplies it.
13
+ * What is NOT a row: pi's own built-ins. `read` and `grep` are listed in
14
+ * {@link GROUNDING_RETRIEVAL_TOOLS} because grounding is about RETRIEVAL rather
15
+ * than about which extension supplies it, while `find` and `ls` return names only
16
+ * and are excluded from it.
26
17
  */
27
18
  /** One worker tool, and everything the rest of the codebase knows about it. */
28
19
  export interface WorkerChannel {
@@ -49,8 +40,9 @@ export declare const WORKER_CHANNELS: readonly WorkerChannel[];
49
40
  export declare function workerChannel(toolName: string): WorkerChannel | undefined;
50
41
  /**
51
42
  * The tools string and the `-e` paths for a set of channels, together — they are
52
- * one fact and used to be two literals. Entry paths are de-duplicated: search and
53
- * fetch ship in one extension file.
43
+ * one fact, and two literals would drift. The tools string preserves the order it
44
+ * was asked for; entry paths are de-duplicated, so asking for search AND fetch
45
+ * yields ONE path, since both ship in search-extension.js.
54
46
  */
55
47
  export declare function channelSet(names: readonly string[]): {
56
48
  tools: string;
@@ -1,28 +1,19 @@
1
1
  /**
2
2
  * worker-channels — what a worker TOOL is, as data.
3
3
  *
4
- * `makeWorkerTool` already gives every worker tool one registration adapter, but
5
- * `spec.name` never left the registration closure. So the same four name strings
6
- * were re-typed as literals in three directories and had to agree by hand:
4
+ * `makeWorkerTool` gives every worker tool one registration adapter, but
5
+ * `spec.name` never leaves that closure. Without this table the three tool names
6
+ * are re-typed as literals wherever anything else needs them: the tools string
7
+ * and its matching `-e` path list, the grounding set, the debug-log summary and
8
+ * its per-tool parameter shape, and — worst for locality — the GENERIC child
9
+ * runner, which would have to name one tool AND one of its parameters to decide a
10
+ * fan-out deadline extension. Nothing links those edits at compile time. Here
11
+ * each tool is one row.
7
12
  *
8
- * `phases.ts` paired `'…,pi-worker-docs'` with `DOCS_EXTENSION_PATH`, and
9
- * `',pi-worker-search,pi-worker-fetch'` with `SEARCH_EXTENSION_PATH` a tools
10
- * string and an `-e` path list that mean the same thing, written twice and
11
- * kept in step by eye.
12
- * • `GROUNDING_RETRIEVAL_TOOLS` was a second copy of the names.
13
- * • `summarizeToolArgs` was a third, and also re-stated each tool's parameter
14
- * shape (`module`/`query`, `query`, `url`).
15
- * • Worst for locality: `runWorker` — the GENERIC child runner — hardcoded one
16
- * tool's identity AND its parameter, `call.name === 'pi-worker-docs' &&
17
- * args.module === '.'`, to decide a fan-out deadline extension.
18
- *
19
- * A rename or a new tool was five edits in three directories with no compile
20
- * error linking them. It is one row here now.
21
- *
22
- * What is NOT in a row: the tools string's non-worker members (`read`, `grep`,
23
- * `find`, `ls`) are pi's own built-ins, not channels — they appear in
24
- * {@link GROUNDING_RETRIEVAL_TOOLS} because grounding is about RETRIEVAL, not
25
- * about which extension supplies it.
13
+ * What is NOT a row: pi's own built-ins. `read` and `grep` are listed in
14
+ * {@link GROUNDING_RETRIEVAL_TOOLS} because grounding is about RETRIEVAL rather
15
+ * than about which extension supplies it, while `find` and `ls` return names only
16
+ * and are excluded from it.
26
17
  */
27
18
  import { fileURLToPath } from 'node:url';
28
19
  const DOCS_ENTRY = fileURLToPath(new URL('./docs-extension.js', import.meta.url));
@@ -64,8 +55,9 @@ export function workerChannel(toolName) {
64
55
  }
65
56
  /**
66
57
  * The tools string and the `-e` paths for a set of channels, together — they are
67
- * one fact and used to be two literals. Entry paths are de-duplicated: search and
68
- * fetch ship in one extension file.
58
+ * one fact, and two literals would drift. The tools string preserves the order it
59
+ * was asked for; entry paths are de-duplicated, so asking for search AND fetch
60
+ * yields ONE path, since both ship in search-extension.js.
69
61
  */
70
62
  export function channelSet(names) {
71
63
  // REFUSED, not dropped. Silently skipping an unrecognised name is the exact
@@ -5,35 +5,34 @@
5
5
  * Why this module exists. `RunWorkerResult` reports each kill cause as its own
6
6
  * optional field (`loopHit`, `timedOut`, `stalled`, `commandTimedOut`,
7
7
  * `streamStalled`, `leakedToolCall`, `aborted`, `exitCode`), and every kill path
8
- * ALSO sets `aborted` and a non-zero exit — killProc flips those on every route
9
- * out. So a consumer cannot read the fields in any order it likes: the specific
10
- * causes must be matched before the generic `aborted`/`exitCode` ones, or a dead
11
- * backend is reported to the user as "you cancelled".
8
+ * ALSO sets the two generic ones. `killProc` sets `aborted` on every route out,
9
+ * and pi's own print/json mode installs a SIGTERM handler that exits 143, so a
10
+ * terminated child carries a non-zero exit too. So a consumer cannot read the
11
+ * fields in any order it likes: the specific causes must be matched before the
12
+ * generic `aborted`/`exitCode` ones, or a dead backend is reported to the user as
13
+ * "you cancelled".
12
14
  *
13
- * That rule used to live as prose in three doc comments and as the source order
14
- * of three hand-written ladders (`classifyEnforceChildFailure`,
15
- * `classifyResearchWorker`, and the gate's own reading). Three copies of an
16
- * ordering is three chances to drift, and it had already drifted: `streamStalled`
17
- * was added to the result and to `finalAttemptFailed`, but the enforce ladder
18
- * never grew an arm for it, so an enforcement child killed for a hung model
19
- * stream fell all the way through to `if (aborted) return USER_CANCELLED` — the
20
- * exact mislabel the comment above that line warns against.
15
+ * Written as prose, or as the source order of each consumer's own hand-rolled
16
+ * ladder, that rule gets a fresh chance to drift per consumer — and the failure is
17
+ * silent: add a new kill cause to the result, forget one ladder's arm for it, and
18
+ * that consumer falls through to `if (aborted)` and reports a cancel.
21
19
  *
22
- * The fix is to make the order data. `FAILURE_RULES` is ordered, the first
23
- * matching row wins, and consumers `switch` on the resulting `kind` instead of
24
- * re-deriving the ladder. A new kill cause is one row here plus a compile error
25
- * in every consumer that has not handled it which is what makes the drift that
26
- * caused the bug impossible rather than merely fixed.
20
+ * So the order is DATA. `FAILURE_RULES` is ordered, the first matching row wins,
21
+ * and consumers `switch` on the resulting `kind` `classifyEnforceChildFailure`
22
+ * (enforce-guidelines.ts) and `classifyResearchWorker` (research-worker.ts) both
23
+ * call `classifyWorkerFailure` and say only what each cause means to THEM. A new
24
+ * kill cause is one row here plus a compile error in every consumer that has not
25
+ * handled it.
27
26
  */
28
27
  import type { LoopHit } from '../task/loop-detector.js';
29
28
  import type { WorkerKillId } from './worker-kill.js';
30
29
  /**
31
30
  * The subset of a finished child result this classification reads.
32
31
  *
33
- * Structural on purpose: `runWorker` returns a superset, and
34
- * `EnforceChildResult` is a hand-written narrowing of the same shape. Typing the
35
- * input as what is actually READ lets both pass without either importing the
36
- * other's interface.
32
+ * Structural on purpose: `runWorker` returns a superset, and `EnforceChildResult`
33
+ * extends this interface with the one field enforcement adds. Typing the input as
34
+ * what is actually READ lets both pass without either importing the other's
35
+ * interface.
37
36
  */
38
37
  export interface WorkerFailureInput {
39
38
  exitCode: number;
@@ -55,9 +54,9 @@ export interface WorkerFailureInput {
55
54
  *
56
55
  * Note what is NOT here: an empty answer, and a reported `modelError` on a run
57
56
  * that still produced text. Neither is a kill — whether they count as a failure
58
- * is the consumer's policy (research accepts a genuinely empty section; the gate
59
- * does not), so folding them in would move a decision out of the module that
60
- * owns it.
57
+ * is the consumer's policy research-worker.ts, for one, accepts an explicit
58
+ * empty section after a retry — so folding them in would move a decision out of
59
+ * the module that owns it.
61
60
  */
62
61
  export type WorkerFailure = {
63
62
  kind: 'stalled';
@@ -102,9 +101,8 @@ export type WorkerFailureKind = WorkerFailure['kind'];
102
101
  * class of event: a watchdog, not the model, ended the attempt.
103
102
  * 4. `worker-timeout` — the wall-clock backstop.
104
103
  * 5. `loop` — killed for repeating one tool call past threshold. After the
105
- * timeouts, matching the enforce ladder this replaces; in practice the two
106
- * cannot both fire, since a loop kill stops the attempt before its own timer
107
- * can expire.
104
+ * timeouts: a loop kill ends the attempt, so its own timer does not go on to
105
+ * expire as well.
108
106
  * 6. `leaked-tool-call` — the model wrote a call as prose instead of invoking
109
107
  * it. Only ever set on an otherwise clean run.
110
108
  * 7. `aborted` — no specific cause survived, so this really is a cancel.
@@ -117,35 +115,29 @@ export declare const FAILURE_RULES: ReadonlyArray<{
117
115
  id: WorkerKillId;
118
116
  match: (r: WorkerFailureInput) => WorkerFailure | null;
119
117
  }>;
120
- /**
121
- * Classify a finished child. Returns `undefined` when nothing killed it —
122
- * which is not the same as "it answered": the text may still be empty, and that
123
- * judgement belongs to the caller.
124
- */
125
118
  /**
126
119
  * What a worker failure SAYS to the caller that asked for the work.
127
120
  *
128
- * WHY IT EXISTS. The ladder above already names the cause exactly, with its
129
- * detail — which tool hung, how long the stream was idle, which exit code. None
130
- * of that reached a human. `formatChildFailure` was handed a `ChildOutcome`
131
- * (`{aborted, exitCode, stderr}`) and answered `if (aborted) return
132
- * abortedMessage`, so a 240s wall-clock kill, a hung `bash`, a dead model
133
- * backend, a loop kill and a user pressing ESC all printed the SAME four words.
134
- * The discriminating value was computed a line later by `childFailureReason` and
135
- * put in the debug trail, which a user reading a tool result never sees.
136
- *
137
- * That is not only unhelpful; it is why the `pi-worker` tool's 240s cap has no
138
- * base rate. 53 recorded invocations across eight repos carry 14 failures, and
139
- * NOTHING in the transcript says which of them ran out of time — the honest
140
- * bound recoverable from timestamps alone is "somewhere between 0 and 8".
121
+ * WHY IT EXISTS. The ladder above already names the cause exactly, with its detail
122
+ * — which tool hung, how long the stream was idle, which exit code. Answering
123
+ * `if (aborted) return abortedMessage` instead throws all of it away: a wall-clock
124
+ * kill, a hung `bash`, a dead model backend, a loop kill and a user pressing ESC
125
+ * then print the SAME words, and the discriminating value only reaches a debug
126
+ * trail a user reading a tool result never sees. Every arm below returns a
127
+ * different message, and only `aborted` returns the caller's own.
141
128
  *
142
129
  * A switch, not a table: `WorkerFailure` is a discriminated union carrying a
143
130
  * different payload per arm, so the exhaustiveness check is the compiler's and a
144
- * ninth arm cannot be added without a message.
131
+ * new arm cannot be added without a message.
145
132
  */
146
133
  export declare function describeWorkerFailure(f: WorkerFailure,
147
134
  /** What a genuine user cancel says. The caller's wording — only this arm is theirs. */
148
135
  abortedMessage: string,
149
136
  /** stderr for the `exit` arm; ignored by every other. */
150
137
  stderr?: string): string;
138
+ /**
139
+ * Classify a finished child against the ladder. Returns `undefined` when nothing
140
+ * killed it — which is not the same as "it answered": the text may still be empty,
141
+ * and that judgement belongs to the caller.
142
+ */
151
143
  export declare function classifyWorkerFailure(r: WorkerFailureInput): WorkerFailure | undefined;