@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
@@ -23,13 +23,17 @@ export type SpawnFn = (command: string, args: ReadonlyArray<string>, options: {
23
23
  cwd: string;
24
24
  shell: boolean;
25
25
  stdio: ['ignore' | 'pipe', 'pipe', 'pipe'];
26
- /** Set only when the invocation needs env overrides (e.g. GIT_INDEX_FILE);
27
- * absent the child inherits this process's environment as before. */
26
+ /** Set only when the invocation needs env overrides git-state-guard's
27
+ * `GIT_INDEX_FILE` throwaway index is the one caller. Absent the child
28
+ * inherits this process's environment. */
28
29
  env?: NodeJS.ProcessEnv;
29
30
  /** true → give the child its own process group (POSIX `detached`), so any
30
- * server it backgrounds (`bun run dev &`) can be reaped as a group when the
31
- * child exits instead of leaking as an orphan holding a port (mx5 run 9
32
- * item 3). Set only for model children (json-events); plumbing stays put. */
31
+ * server it backgrounds (`bun run dev &`) can be reaped as a group instead
32
+ * of leaking as an orphan holding a port. The flag is what makes the reap
33
+ * possible at all: with `detached`, a `kill(-pid)` issued after the child
34
+ * has exited still takes the backgrounded grandchild with it; without it
35
+ * the same call throws ESRCH and the grandchild survives. Set only for
36
+ * model children (json-events); plumbing stays in-group. */
33
37
  detached?: boolean;
34
38
  }) => ProcLike;
35
39
  export interface ChildResult {
@@ -58,7 +62,7 @@ export interface ChildResult {
58
62
  * true when the STREAM watchdog killed the child: no output at all for the
59
63
  * configured inactivity window, regardless of whether the backend answers a
60
64
  * probe. Distinct from `stalled`, which requires an UNREACHABLE endpoint —
61
- * the run-14 hangs had a perfectly healthy server and a dead stream, so the
65
+ * these hangs have a perfectly healthy server and a dead stream, so the
62
66
  * probe path could never fire. Callers must check this BEFORE `aborted`
63
67
  * (the kill sets aborted too) and route it into the connection-error retry.
64
68
  */
@@ -72,8 +76,10 @@ export interface ToolCall {
72
76
  /**
73
77
  * pi's id for this tool call, carried on both `tool_execution_start` and
74
78
  * `tool_execution_end` so a caller can pair them (the command watchdog arms
75
- * on start and disarms on the matching end). Optional because the loop
76
- * detector the other consumer keys on name+args and never needs it.
79
+ * on start and disarms on the matching end). It is the only thing that pairs
80
+ * them: the end event carries `toolName`, `result` and `isError`, but no
81
+ * `args`. Optional because the loop detector — the other consumer — keys on
82
+ * name+args and never needs it.
77
83
  */
78
84
  toolCallId?: string;
79
85
  }
@@ -98,9 +104,10 @@ export interface ContextSnapshot {
98
104
  export interface RunChildTextOptions {
99
105
  mode: 'text';
100
106
  /**
101
- * Drop stdout chunks instead of buffering them into the result. Use for
102
- * pipe-through tools (npm install, build commands) where we only need the
103
- * exit code and stderr verbose stdout can exceed V8's max string length.
107
+ * Drop stdout chunks instead of buffering them into the result. Use when only
108
+ * the exit code and stderr are wanted and stdout is bulk noise. Its one caller
109
+ * is the docs extractor, which discards a verbose stdout and keeps stderr
110
+ * because stderr is what it came for.
104
111
  */
105
112
  discardStdout?: boolean;
106
113
  /**
@@ -117,29 +124,28 @@ export interface RunChildJsonEventsOptions {
117
124
  onContextUsage?: (snapshot: ContextSnapshot) => void;
118
125
  /**
119
126
  * The child's context window in tokens, supplied BY THE CALLER — pi's event
120
- * stream does not carry one (GitHub issue #16).
127
+ * stream does not carry one.
121
128
  *
122
- * Verified against the published tarballs of @earendil-works/pi-coding-agent
123
- * and @earendil-works/pi-agent-core at 0.80.2 and 0.84.2, and against pi's
124
- * own docs/json.md: the wire union is session / agent_* / turn_* / message_*
125
- * / tool_execution_* / queue_update / compaction_* / auto_retry_*, the
126
- * session header is {type,version,id,timestamp,cwd,parentSession}, and no
127
- * member of either carries a window or even a model id. `contextUsage`
128
- * exists ONLY as the in-process `ctx.getContextUsage()` extension API, which
129
- * a `--mode json` child never speaks back to its parent.
129
+ * Checked against a real `--mode json` capture rather than assumed: across
130
+ * every event it emits, no key anywhere names a context or a window. The
131
+ * stream DOES carry a model id and a token `usage` block, so the COUNT is
132
+ * available and only the window is missing. `getContextUsage()` exists solely
133
+ * as an in-process extension API, which a `--mode json` child never speaks
134
+ * back to its parent.
130
135
  *
131
136
  * The parent therefore has to say. Children are spawned without `-m`
132
137
  * (CHILD_BASE_ARGS), so they resolve the same default model the parent runs
133
- * and the parent session's window is the honest answer. 0 / omitted keeps
134
- * the old behaviour: report the token count with no window.
138
+ * and the parent session's window is the honest answer. 0 or omitted reports
139
+ * the token count with no window.
135
140
  */
136
141
  contextWindow?: number;
137
142
  onToolCall?: (call: ToolCall) => LoopHit | null;
138
143
  /**
139
- * Fires when a tool call finishes, carrying its RESULT (mx5 run 10 item 6: the
140
- * verify debug log recorded the `bash:` command but never its output, so "verify
141
- * claimed curl PASS on a server that cannot serve" was undecidable from the log).
142
- * Text is the tool's combined output; `isError` distinguishes a failed call.
144
+ * Fires when a tool call finishes, carrying its RESULT. `onToolCall` reports
145
+ * only the request, so without this a debug trail records that a `bash:`
146
+ * command ran and never what it printed and a step that claims a command
147
+ * passed cannot be checked against what the command actually said. Text is
148
+ * the tool's combined output; `isError` distinguishes a failed call.
143
149
  */
144
150
  onToolResult?: (result: {
145
151
  name: string;
@@ -149,13 +155,13 @@ export interface RunChildJsonEventsOptions {
149
155
  }) => void;
150
156
  onFirstByte?: () => void;
151
157
  /**
152
- * Dead-backend stall guard (mx5 run 7: model server died mid-child, the
153
- * child hung MUTE for 64 minutes). Liveness is OUTPUT PROGRESS, not wall
154
- * time: any stdout/stderr chunk resets the window, so honest long work is
155
- * never killed. Only when nothing arrived for `afterMs` is `probe` asked
156
- * whether the model backend is reachable reachable → keep waiting
157
- * (prompt processing legitimately emits nothing for minutes); unreachable
158
- * the child is killed and the result carries `stalled: true` so callers
158
+ * Dead-backend stall guard: the model server dies mid-child and the child
159
+ * hangs mute with nothing to throw. Liveness is OUTPUT PROGRESS, not wall
160
+ * time any stdout/stderr chunk resets the window, so honest long work is
161
+ * never killed. Only when nothing has arrived for `afterMs` is `probe` asked
162
+ * whether the model backend is reachable: reachable → keep waiting, because
163
+ * prompt processing legitimately emits nothing while it runs; unreachable
164
+ * the child is killed and the result carries `stalled: true` so callers
159
165
  * report the real cause instead of hanging or mislabeling a user cancel.
160
166
  */
161
167
  stall?: {
@@ -166,9 +172,9 @@ export interface RunChildJsonEventsOptions {
166
172
  * Stream-inactivity ceiling in ms (shared/stream-watchdog.ts). Unlike `stall`
167
173
  * this asks NOTHING of the backend: a stream that has produced no bytes for
168
174
  * this long is dead whether or not the server answers a health probe, which
169
- * is exactly the run-14 shape (server Up(healthy), stream silent for hours).
170
- * Any stdout/stderr chunk resets it, so a slow model emitting one token every
171
- * 30s is never killed. 0 / omitted = off.
175
+ * is exactly the shape of a hung stream: server reporting healthy, nothing
176
+ * coming down the wire for hours. Any stdout/stderr chunk resets it, so a
177
+ * merely slow model is never killed, however slow it is. 0 / omitted = off.
172
178
  */
173
179
  streamInactivityMs?: number;
174
180
  }
@@ -196,10 +202,11 @@ export declare class JsonEventSink {
196
202
  * CLEARED when a LATER agent_end delivers assistant text: pi retries a failed
197
203
  * turn itself (`auto_retry_start`) and each attempt emits its own agent_end,
198
204
  * so a recovered blip arrives as agent_end(stopReason "error", empty) followed
199
- * by agent_end(text). Measured live against a proxy that drops the first
200
- * connection: pi makes up to 4 attempts over ~15s, and on attempts 1–3 the
201
- * child returns the real answer WITH the dead first attempt's errorMessage
202
- * still in the stream. Latching that would report a failure for a run that
205
+ * by agent_end(text). How many attempts and how long they take is the user's
206
+ * `retry` setting one initial call plus `retry.maxRetries`, backing off
207
+ * from `retry.baseDelayMs` so on any attempt but the last the child
208
+ * returns the real answer WITH the dead first attempt's errorMessage still
209
+ * in the stream. Latching that would report a failure for a run that
203
210
  * succeeded. An error AFTER the last text-bearing turn still latches — that
204
211
  * one really did lose the tail of the work.
205
212
  */
@@ -35,10 +35,11 @@ export class JsonEventSink {
35
35
  * CLEARED when a LATER agent_end delivers assistant text: pi retries a failed
36
36
  * turn itself (`auto_retry_start`) and each attempt emits its own agent_end,
37
37
  * so a recovered blip arrives as agent_end(stopReason "error", empty) followed
38
- * by agent_end(text). Measured live against a proxy that drops the first
39
- * connection: pi makes up to 4 attempts over ~15s, and on attempts 1–3 the
40
- * child returns the real answer WITH the dead first attempt's errorMessage
41
- * still in the stream. Latching that would report a failure for a run that
38
+ * by agent_end(text). How many attempts and how long they take is the user's
39
+ * `retry` setting one initial call plus `retry.maxRetries`, backing off
40
+ * from `retry.baseDelayMs` so on any attempt but the last the child
41
+ * returns the real answer WITH the dead first attempt's errorMessage still
42
+ * in the stream. Latching that would report a failure for a run that
42
43
  * succeeded. An error AFTER the last text-bearing turn still latches — that
43
44
  * one really did lose the tail of the work.
44
45
  */
@@ -46,9 +47,9 @@ export class JsonEventSink {
46
47
  textDeltaAccum = '';
47
48
  // json-events lines can split across data chunks; this holds the trailing
48
49
  // partial line between feeds so events spanning a boundary still parse. We
49
- // deliberately do NOT accumulate the full raw stream: a long-running child
50
- // emits hundreds of MB of events and buffering it would overflow V8's max
51
- // string length (≈512MB). We keep only the parsed text.
50
+ // deliberately do NOT accumulate the full raw stream only the parsed text
51
+ // is kept, so a long-running child cannot walk a buffer toward the runtime's
52
+ // max-string-length ceiling.
52
53
  buf = '';
53
54
  constructor(opts,
54
55
  /** Invoked when onToolCall reports a loop hit — runChild kills the child. */
@@ -88,12 +89,12 @@ export class JsonEventSink {
88
89
  handleEvent(evt) {
89
90
  const opts = this.opts;
90
91
  const t = typeof evt.type === 'string' ? evt.type : '';
91
- // `message_end` is the ONLY context readout pi gives a `--mode json`
92
- // child's parent. There used to be a `context_usage` branch above this
93
- // one that was preferred over it; no released pi has ever emitted such
94
- // an event (see `contextWindow` on RunChildJsonEventsOptions), so it was
95
- // unreachable, and its presence is what made the zero window below look
96
- // like a harmless fallback rather than the only path. Issue #16.
92
+ // Token usage rides on assistant messages, and `message_end` is where the
93
+ // stream delivers them (`agent_end` then repeats the final one). There is
94
+ // no `context_usage` event to prefer over this: the string appears nowhere
95
+ // in the installed pi, and a real `--mode json` capture carries no key
96
+ // naming a context or a window. So the zero window below is not a
97
+ // fallback it is what every caller that omits `contextWindow` gets.
97
98
  if (t === 'message_end' && opts.onContextUsage) {
98
99
  const msg = evt.message;
99
100
  if (msg?.role === 'assistant') {
@@ -209,15 +210,15 @@ export function runChild(spawn, invocation, cwd, signal, opts) {
209
210
  let stderr = '';
210
211
  let aborted = false;
211
212
  const discardStdout = opts?.mode === 'text' && opts.discardStdout === true;
212
- // Deliver the prompt on stdin, not argv: a large prompt (e.g. an inlined
213
- // design doc) blows past the OS command-line limit Windows CreateProcessW
214
- // caps it at 32767 chars and Node throws `spawn ENAMETOOLONG`. When a
215
- // prompt is present we open stdin as a pipe; otherwise keep it 'ignore'
216
- // (git and other arg-only spawns are unaffected). See GitHub issue #1.
213
+ // Deliver the prompt on stdin, not argv. A large prompt an inlined design
214
+ // doc, say exceeds the OS argv ceiling and the spawn fails outright rather
215
+ // than truncating: on this platform it surfaces as `E2BIG`. When a prompt is
216
+ // present we open stdin as a pipe; otherwise it stays 'ignore', so git and
217
+ // other arg-only spawns are unaffected.
217
218
  const usesStdin = invocation.stdin !== undefined;
218
219
  // Model children (json-events) run arbitrary bash — they can `bun run dev &`
219
220
  // a server that outlives the child and holds a port, wrecking the final gate
220
- // with a self-inflicted EADDRINUSE (mx5 run 9 item 3). Spawn them in their
221
+ // with a self-inflicted EADDRINUSE. Spawn them in their
221
222
  // OWN process group so every such grandchild can be reaped as a unit on exit.
222
223
  // Plumbing (git, mode:'text') never backgrounds anything and stays in-group.
223
224
  const ownGroup = opts?.mode === 'json-events';
@@ -354,53 +355,43 @@ export function runChild(spawn, invocation, cwd, signal, opts) {
354
355
  else
355
356
  stdout += chunk;
356
357
  });
357
- // This accumulation is deliberately unbounded, unlike `discardStdout`
358
- // above. The asymmetry was investigated on 2026-07-28 and the "stderr can
359
- // overflow V8's max string length" hypothesis is REFUTED at ~5 orders of
360
- // magnitude — do not re-open it without new volume evidence.
358
+ // This accumulation is deliberately unbounded, unlike `discardStdout` above.
361
359
  //
362
- // Measured ceiling (binary search on `'a'.repeat`, not cited from memory):
363
- // node v26.2.0 536,870,888 chars (512 MiB, 2^29-24), bun 1.3.14
364
- // 2,147,483,647 (2 GiB). Both throw RangeError, not OOM node's default
365
- // heap is 4192 MiB, well clear of its own string ceiling. If it were ever
366
- // reached the severity would be high: a throw in this handler escapes as
367
- // an uncaughtException and the enclosing promise never settles (measured),
368
- // so it would kill the host, not fail the child.
360
+ // A string ceiling does exist the runtime's max string length, which
361
+ // throws a RangeError rather than running out of memory. What makes the
362
+ // asymmetry worth a comment is the severity if it were ever hit: a throw
363
+ // inside this handler is NOT catchable by the enclosing promise. With no
364
+ // `uncaughtException` handler installed the runtime prints the stack and
365
+ // exits, so the promise never settles because the host process is gone. It
366
+ // would take pi down rather than fail one child.
369
367
  //
370
- // Measured volume, real runs not synthetic: the largest single tool output
371
- // in a full mx5 run is 5,043 bytes and that whole run's verify-debug.log is
372
- // 112,216 bytes. The heaviest commands in the system are far under —
373
- // pi-task's own 2,452-test suite emits 162 bytes of stderr, mx5's
374
- // prettier+eslint+tsc lint 90, a cached `bun install` 0. Observed max is
375
- // ~106,000x below the node ceiling; reaching it inside the 15-min command
376
- // bound would need ~596 KB/s sustained on stderr for the full window.
368
+ // What keeps it far away is which children reach here: `pi` model children,
369
+ // `git`, and `npm install --loglevel=error`. Genuinely verbose work test
370
+ // runners, builds, boot probes never uses runChild. It goes through
371
+ // command-run's `BoundedOutput`, which caps each stream's head and tail and
372
+ // elides the middle.
377
373
  //
378
- // The structural reason is the population of children that reach here:
379
- // `pi` model children (stderr near-empty), `git`, and one
380
- // `npm install --loglevel=error`. Genuinely verbose work test runners,
381
- // builds, boot probes does not use runChild; final-gate.ts spawns its own
382
- // and already caps at 8000 chars. So `discardStdout`'s single caller
383
- // (docs-core.ts) is not an oversight to mirror: it discards the verbose
384
- // stream and KEEPS stderr precisely because stderr is that call's payload.
385
- //
386
- // A symmetric `discardStderr` would therefore have no caller, and a naive
387
- // cap here would be actively unsafe: consumers disagree about which end
388
- // carries the cause — phases.ts:474 takes the tail (-500), phases.ts:936
389
- // takes the head (0, 300), and child-runner.ts:172 feeds the whole string
390
- // into failure classification.
374
+ // So a symmetric `discardStderr` would have no caller. `discardStdout`'s one
375
+ // caller (docs-core) discards the verbose stream and KEEPS stderr precisely
376
+ // because stderr is that call's payload. A naive cap here would also be
377
+ // unsafe: consumers disagree about which END carries the cause
378
+ // research-worker slices the TAIL of stderr in two places and the HEAD in a
379
+ // third, and child-runner feeds the whole string into its failure message.
391
380
  proc.stderr?.on('data', (d) => {
392
381
  lastActivity = Date.now();
393
382
  streamWatch?.note();
394
383
  stderr += d.toString();
395
384
  });
396
385
  // One idempotent settle path for close/error/abort. Detaching the abort
397
- // listener here is the point: `{once: true}` only fires-and-removes on an
398
- // ACTUAL abort, so a child that finishes normally used to leave its
399
- // listener on the signal forever. A TaskRunner shares ONE AbortController
400
- // across every child of a run, so those listeners accumulated linearly and
401
- // each one retained, via `killProc`'s closure, the finished child process,
402
- // this invocation (including its full prompt), and `opts` together with
403
- // everything the caller's callbacks close over. See GitHub issue #9.
386
+ // listener here is the point. `{once: true}` fires-and-removes on an ACTUAL
387
+ // abort and at no other time, so a child that finishes normally leaves its
388
+ // listener attached: five children that all exited cleanly still had five
389
+ // live listeners, all five fired on the eventual abort, and all five
390
+ // closures survived a forced GC. A TaskRunner shares ONE AbortController
391
+ // across every child of a run, so they accumulate linearly, and each one
392
+ // retains through `killProc`'s closure the finished child process, this
393
+ // invocation including its full prompt, and `opts`, along with everything
394
+ // the caller's callbacks close over.
404
395
  let settled = false;
405
396
  const cleanup = () => {
406
397
  if (stallTimer)
@@ -464,8 +455,8 @@ export function summarizeToolArgs(toolName, args) {
464
455
  return a.command.replace(/\s+/g, ' ').trim();
465
456
  }
466
457
  // Each worker tool's argument shape is its own row's fact (worker-channels.ts);
467
- // this used to be a third copy of the names AND a re-statement of each one's
468
- // parameters. A row that has nothing to say falls through to the generic keys.
458
+ // without it this is a third copy of the names AND a re-statement of each
459
+ // one's parameters. A row that has nothing to say falls through to the generic keys.
469
460
  const channel = workerChannel(toolName);
470
461
  if (channel) {
471
462
  const summary = channel.summarize(a);
@@ -2,9 +2,12 @@
2
2
  * Command watchdog — the per-tool-call wall-clock machine, shared by both
3
3
  * surfaces that can run a command which never returns.
4
4
  *
5
- * WHY THIS LIVES IN shared/: pi's bash tool takes an OPTIONAL `timeout` with NO
6
- * default (pi-coding-agent core/tools/bash.js), so ANY command the model didn't
7
- * bound runs forever. That is true in two places, and they are disjoint:
5
+ * WHY THIS LIVES IN shared/: pi's bash tool takes an OPTIONAL `timeout` and has
6
+ * no default for it. Its schema says so in as many words — "Timeout in seconds
7
+ * (optional, no default timeout)" and its resolver returns `undefined` for an
8
+ * absent value, which skips arming any timer at all. So ANY command the model
9
+ * did not bound itself runs forever. That is true in two places, and they are
10
+ * disjoint:
8
11
  *
9
12
  * MAIN SESSION — the implementation turn, handed off via sendUserMessage
10
13
  * (task/orchestrator.ts). Guarded by registerCommandWatchdog
@@ -20,7 +23,8 @@
20
23
  *
21
24
  * main session — ctx.abort() ends the whole agent operation, not just the one
22
25
  * tool call (pi types it "Abort the current agent operation"),
23
- * and pi runs sibling tool calls CONCURRENTLY by default. So an
26
+ * and pi's agent defaults `toolExecution` to "parallel", so
27
+ * siblings really are in flight together. An
24
28
  * overrun kills every tool in flight in that turn — including
25
29
  * one exempted via commandTimeoutExemptTools, which only stops
26
30
  * a timer being armed FOR that tool, not its being collateral
@@ -79,9 +83,9 @@ export declare function reminderMessage(toolName: string, timeoutMs: number): st
79
83
  * child that believes it starts clean may re-apply them or misread the tree.
80
84
  * Only the CONVERSATION is gone; the hint must say so precisely.
81
85
  *
82
- * Replaces the generic worker-timeout hint for this case: that one blames
83
- * "exploring too long", which is the wrong diagnosis for a hung command and
84
- * never mentions the timeout parameter.
86
+ * Used instead of the generic worker-timeout hint, which blames the child for
87
+ * "exploring too long" the wrong diagnosis for a hung command, and one that
88
+ * never mentions the `timeout` parameter that would prevent it.
85
89
  */
86
90
  export declare function commandTimeoutHint(toolName: string, timeoutMs: number, opts?: {
87
91
  commandDetail?: string;
@@ -89,9 +93,10 @@ export declare function commandTimeoutHint(toolName: string, timeoutMs: number,
89
93
  }): string;
90
94
  export declare class CommandWatchdog {
91
95
  private readonly deps;
92
- /** Armed timers, keyed by the tool call they guard. Tool executions are
93
- * sequential, so this holds at most one entry in normal operation, but the
94
- * map keeps it correct even if pi ever overlaps two calls. */
96
+ /** Armed timers, keyed by the tool call they guard. A map rather than one
97
+ * handle because pi's agent defaults `toolExecution` to "parallel": a batch
98
+ * of sibling calls is genuinely in flight together, so several timers can be
99
+ * armed at once and each must be cancelled by its own id. */
95
100
  private readonly active;
96
101
  constructor(deps: WatchdogDeps);
97
102
  /** Arm a timer for a starting tool. No-op when the watchdog is off. */
@@ -108,11 +113,75 @@ export declare class CommandWatchdog {
108
113
  * Real-clock schedule/cancel. Shared by both adapters; tests substitute a fake
109
114
  * scheduler instead.
110
115
  *
111
- * REF'd, for the same measured reason as the stream watchdog's poll (see
112
- * realStreamTimerDeps): under Bun on windows an unref'd timer never fires once
113
- * nothing ref'd is pending, and a child sitting in a hung command is exactly
114
- * that state so the unref disabled the guard in the one case it exists for.
115
- * The timer is cleared when the tool ends and in runWorker's `finally`, so it
116
- * cannot outlive the call it watches.
116
+ * REF'd deliberately, and NOT unref'd. Measured on this platform, on both
117
+ * runtimes: an unref'd timer with nothing else ref'd and pending never fires at
118
+ * all the process simply exits while the identical ref'd timer does. A
119
+ * watchdog whose whole job is to fire while everything else is stuck cannot
120
+ * afford that. Holding the loop open is safe here because the timer is cancelled
121
+ * when the tool ends and again in runWorker's `finally`, so it cannot outlive
122
+ * the call it watches.
117
123
  */
118
124
  export declare const realTimerDeps: Pick<WatchdogDeps, 'schedule' | 'cancel'>;
125
+ /** What the command watchdog recorded when it killed a child attempt. */
126
+ export interface CommandKill {
127
+ toolName: string;
128
+ timeoutMs: number;
129
+ /** The command line itself, when the tool carried one — quoted into the hint
130
+ * so the fresh child knows which call it must not repeat unbounded. */
131
+ detail?: string;
132
+ }
133
+ /**
134
+ * The tool-call fields the child-side watchdog reads. Structural rather than
135
+ * `ToolCall` from child-process.ts, so this module keeps its zero imports and a
136
+ * caller cannot be forced to reach for the runner's types to arm a timer.
137
+ */
138
+ export interface WatchedToolCall {
139
+ name: string;
140
+ toolCallId?: string;
141
+ args: unknown;
142
+ }
143
+ /**
144
+ * Build the child-side command watchdog for ONE attempt: a per-tool-call timer
145
+ * machine whose `onFire` aborts `signal`, which runChild turns into a
146
+ * process-GROUP kill — reaping the hung command itself, not just the pi child
147
+ * holding it.
148
+ *
149
+ * LIMIT: the group kill only reaches processes still IN the group. A hung command
150
+ * that detached a daemon (setsid, nohup, a background dev server) leaves it
151
+ * running, so the fresh attempt can hit a port the dead attempt's escapee still
152
+ * holds. There is no cheap fix from here; the restart hint's "check current state"
153
+ * line is the mitigation.
154
+ *
155
+ * Returns null when the watchdog is off, so the caller keeps the plain timeout
156
+ * signal and no per-call bookkeeping happens at all.
157
+ */
158
+ export declare function commandWatch(timeoutMs: number): {
159
+ onStart: (call: WatchedToolCall) => void;
160
+ onEnd: (toolCallId: string | undefined) => void;
161
+ killed: () => CommandKill | undefined;
162
+ signal: AbortSignal;
163
+ clear: () => void;
164
+ } | null;
165
+ /**
166
+ * The per-command ceiling for attempt N, halving each time a hang recurs.
167
+ *
168
+ * The first attempt gets the full configured ceiling — a genuinely slow build or
169
+ * test suite deserves it. But every hang-caused restart carries
170
+ * commandTimeoutHint, which tells the model in as many words to bound its
171
+ * command; a SECOND hang means it ignored an explicit instruction, and a third
172
+ * means it ignored it twice. Giving a non-complying child the full ceiling again
173
+ * makes the worst case three times the ceiling, resting entirely on the model
174
+ * obeying prose. Halving bounds it at under twice the ceiling while costing a
175
+ * complying child nothing.
176
+ *
177
+ * `priorHangs` counts watchdog kills specifically, NOT total restarts — the
178
+ * restart budget is shared with loop kills, and a child restarted for LOOPING
179
+ * never received the bound-your-command hint, so its first hang still deserves
180
+ * the full ceiling. Only a hang after a hang is defiance.
181
+ *
182
+ * Floored at 30s so repeated halving cannot shrink the ceiling to something no
183
+ * real command could finish inside — but the floor is `min(base, 30s)`, never
184
+ * above the configured ceiling, so a caller asking for 10s keeps 10s at every
185
+ * hang count. A base of 0 or less disables the watchdog and stays 0.
186
+ */
187
+ export declare function commandCeilingForAttempt(baseMs: number, priorHangs: number): number;