@mjasnikovs/pi-task 0.38.29 → 0.38.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (373) hide show
  1. package/dist/config/config.d.ts +70 -70
  2. package/dist/config/config.js +26 -35
  3. package/dist/config/extension-list.d.ts +6 -5
  4. package/dist/config/extension-list.js +3 -2
  5. package/dist/config/reasoning-args.d.ts +9 -7
  6. package/dist/config/reasoning-args.js +12 -10
  7. package/dist/config/reasoning.d.ts +44 -105
  8. package/dist/config/reasoning.js +27 -704
  9. package/dist/config/register.d.ts +34 -48
  10. package/dist/config/register.js +41 -51
  11. package/dist/config/tool-list.d.ts +16 -16
  12. package/dist/config/tool-list.js +1 -1
  13. package/dist/index.js +2 -0
  14. package/dist/remote/bridge.d.ts +19 -10
  15. package/dist/remote/bridge.js +3 -2
  16. package/dist/remote/broadcast.js +3 -1
  17. package/dist/remote/events.js +12 -11
  18. package/dist/remote/history.d.ts +1 -1
  19. package/dist/remote/protocol.d.ts +6 -3
  20. package/dist/remote/protocol.js +2 -1
  21. package/dist/remote/push.d.ts +16 -16
  22. package/dist/remote/push.js +27 -27
  23. package/dist/remote/register.d.ts +3 -3
  24. package/dist/remote/register.js +17 -19
  25. package/dist/remote/server.d.ts +9 -8
  26. package/dist/remote/server.js +15 -14
  27. package/dist/remote/session-state.d.ts +5 -4
  28. package/dist/remote/session-state.js +8 -5
  29. package/dist/remote/sw.d.ts +7 -6
  30. package/dist/remote/sw.js +7 -6
  31. package/dist/remote/tailscale.d.ts +4 -2
  32. package/dist/remote/tailscale.js +4 -2
  33. package/dist/remote/ui-highlight.js +6 -5
  34. package/dist/remote/ui-render.js +4 -4
  35. package/dist/remote/ui-script.js +24 -24
  36. package/dist/remote/ui-styles.d.ts +1 -1
  37. package/dist/remote/ui-styles.js +10 -13
  38. package/dist/remote/ui-tools.js +9 -6
  39. package/dist/shared/child-extensions.d.ts +29 -17
  40. package/dist/shared/child-extensions.js +29 -17
  41. package/dist/shared/child-output.d.ts +30 -24
  42. package/dist/shared/child-output.js +25 -17
  43. package/dist/shared/child-process.d.ts +47 -40
  44. package/dist/shared/child-process.js +50 -59
  45. package/dist/shared/command-watchdog.d.ts +85 -16
  46. package/dist/shared/command-watchdog.js +115 -21
  47. package/dist/shared/fs-text.d.ts +16 -10
  48. package/dist/shared/fs-text.js +16 -10
  49. package/dist/shared/git-runner.d.ts +25 -25
  50. package/dist/shared/git-runner.js +25 -25
  51. package/dist/shared/leaked-tool-call.d.ts +17 -11
  52. package/dist/shared/leaked-tool-call.js +23 -15
  53. package/dist/shared/model-endpoint.d.ts +29 -16
  54. package/dist/shared/model-endpoint.js +33 -21
  55. package/dist/shared/pi-invocation.d.ts +7 -4
  56. package/dist/shared/pi-invocation.js +12 -7
  57. package/dist/shared/pkg-version.d.ts +13 -5
  58. package/dist/shared/pkg-version.js +13 -5
  59. package/dist/shared/reasoning-capability.d.ts +35 -24
  60. package/dist/shared/reasoning-capability.js +35 -24
  61. package/dist/shared/stream-watchdog.d.ts +60 -44
  62. package/dist/shared/stream-watchdog.js +62 -45
  63. package/dist/task/accept-debt.d.ts +41 -43
  64. package/dist/task/accept-debt.js +73 -65
  65. package/dist/task/api-synthesis.d.ts +24 -21
  66. package/dist/task/api-synthesis.js +32 -26
  67. package/dist/task/apis-contract.d.ts +32 -64
  68. package/dist/task/apis-contract.js +32 -64
  69. package/dist/task/artifact-closure.d.ts +27 -13
  70. package/dist/task/artifact-closure.js +95 -67
  71. package/dist/task/auto-commit.d.ts +46 -35
  72. package/dist/task/auto-commit.js +51 -38
  73. package/dist/task/auto-io.d.ts +45 -25
  74. package/dist/task/auto-io.js +57 -29
  75. package/dist/task/auto-orchestrator.d.ts +26 -24
  76. package/dist/task/auto-orchestrator.js +192 -165
  77. package/dist/task/auto-prompts.d.ts +36 -24
  78. package/dist/task/auto-prompts.js +40 -26
  79. package/dist/task/autofix-ledger.d.ts +27 -25
  80. package/dist/task/autofix-ledger.js +29 -26
  81. package/dist/task/batch-test-task.d.ts +20 -12
  82. package/dist/task/batch-test-task.js +67 -60
  83. package/dist/task/boot-probe.d.ts +60 -44
  84. package/dist/task/boot-probe.js +91 -72
  85. package/dist/task/cancel-input.d.ts +30 -16
  86. package/dist/task/cancel-input.js +20 -11
  87. package/dist/task/cancel-points.d.ts +27 -20
  88. package/dist/task/cancel-points.js +30 -22
  89. package/dist/task/child-runner.d.ts +124 -55
  90. package/dist/task/child-runner.js +298 -90
  91. package/dist/task/child-status.d.ts +23 -16
  92. package/dist/task/child-status.js +23 -16
  93. package/dist/task/clamp-output.js +12 -5
  94. package/dist/task/command-run.d.ts +31 -28
  95. package/dist/task/command-run.js +44 -35
  96. package/dist/task/command-shrink.d.ts +25 -18
  97. package/dist/task/command-shrink.js +37 -31
  98. package/dist/task/command-watchdog.d.ts +9 -6
  99. package/dist/task/command-watchdog.js +21 -15
  100. package/dist/task/context-attribution.d.ts +34 -26
  101. package/dist/task/context-attribution.js +34 -26
  102. package/dist/task/context-silence.d.ts +39 -29
  103. package/dist/task/context-silence.js +35 -25
  104. package/dist/task/context-usage.d.ts +16 -9
  105. package/dist/task/context-usage.js +16 -9
  106. package/dist/task/contracts.d.ts +8 -4
  107. package/dist/task/contracts.js +25 -17
  108. package/dist/task/coverage-loop.d.ts +22 -18
  109. package/dist/task/coverage-loop.js +35 -30
  110. package/dist/task/critique-probes.d.ts +13 -14
  111. package/dist/task/critique-probes.js +50 -39
  112. package/dist/task/debug-log.d.ts +13 -5
  113. package/dist/task/debug-log.js +32 -20
  114. package/dist/task/decompose-fidelity.d.ts +11 -9
  115. package/dist/task/decompose-fidelity.js +38 -33
  116. package/dist/task/decompose-granularity.d.ts +41 -38
  117. package/dist/task/decompose-granularity.js +41 -38
  118. package/dist/task/deep-render-check.d.ts +22 -14
  119. package/dist/task/deep-render-check.js +40 -31
  120. package/dist/task/dropped-input.d.ts +12 -7
  121. package/dist/task/dropped-input.js +5 -2
  122. package/dist/task/enforce-attribution.d.ts +38 -47
  123. package/dist/task/enforce-attribution.js +46 -52
  124. package/dist/task/enforce-guidelines.d.ts +31 -20
  125. package/dist/task/enforce-guidelines.js +32 -21
  126. package/dist/task/enrichment.d.ts +7 -2
  127. package/dist/task/enrichment.js +26 -14
  128. package/dist/task/env-notes.d.ts +16 -7
  129. package/dist/task/env-notes.js +48 -31
  130. package/dist/task/env-template-closure.d.ts +4 -4
  131. package/dist/task/env-template-closure.js +42 -34
  132. package/dist/task/external-context.d.ts +28 -21
  133. package/dist/task/external-context.js +17 -12
  134. package/dist/task/failure-classifier.d.ts +4 -5
  135. package/dist/task/failure-classifier.js +30 -8
  136. package/dist/task/file-inventory.d.ts +15 -11
  137. package/dist/task/file-inventory.js +25 -22
  138. package/dist/task/final-gate-fix.d.ts +74 -86
  139. package/dist/task/final-gate-fix.js +97 -116
  140. package/dist/task/final-gate-progress.d.ts +29 -46
  141. package/dist/task/final-gate-progress.js +40 -51
  142. package/dist/task/final-gate.d.ts +64 -97
  143. package/dist/task/final-gate.js +192 -199
  144. package/dist/task/fix-child.d.ts +21 -27
  145. package/dist/task/fix-child.js +21 -27
  146. package/dist/task/foreign-path.d.ts +6 -5
  147. package/dist/task/foreign-path.js +0 -0
  148. package/dist/task/frozen-conflict.d.ts +9 -10
  149. package/dist/task/frozen-conflict.js +61 -64
  150. package/dist/task/frozen-path-guard.d.ts +35 -14
  151. package/dist/task/frozen-path-guard.js +56 -39
  152. package/dist/task/gate-child.d.ts +27 -28
  153. package/dist/task/gate-child.js +36 -35
  154. package/dist/task/gate-deps.d.ts +34 -27
  155. package/dist/task/gate-deps.js +169 -159
  156. package/dist/task/gate-tally.d.ts +77 -80
  157. package/dist/task/gate-tally.js +65 -68
  158. package/dist/task/git-state-guard.d.ts +15 -11
  159. package/dist/task/git-state-guard.js +76 -66
  160. package/dist/task/impl-widget.d.ts +25 -16
  161. package/dist/task/impl-widget.js +27 -17
  162. package/dist/task/implementation-guards.d.ts +26 -0
  163. package/dist/task/implementation-guards.js +177 -0
  164. package/dist/task/implementation-thinking.d.ts +33 -31
  165. package/dist/task/implementation-thinking.js +5 -6
  166. package/dist/task/implementation-turn.d.ts +39 -31
  167. package/dist/task/implementation-turn.js +41 -28
  168. package/dist/task/inline-markdown.d.ts +20 -7
  169. package/dist/task/inline-markdown.js +15 -6
  170. package/dist/task/launch-config-gap.js +25 -39
  171. package/dist/task/launch-contract.d.ts +18 -21
  172. package/dist/task/launch-contract.js +28 -30
  173. package/dist/task/launch-manifest.d.ts +6 -2
  174. package/dist/task/launch-manifest.js +35 -34
  175. package/dist/task/ledger.js +16 -14
  176. package/dist/task/lint-fix.d.ts +6 -8
  177. package/dist/task/lint-fix.js +67 -69
  178. package/dist/task/loop-detector.d.ts +27 -8
  179. package/dist/task/loop-detector.js +38 -14
  180. package/dist/task/mid-run-input.d.ts +17 -15
  181. package/dist/task/mid-run-input.js +17 -15
  182. package/dist/task/orchestrator.d.ts +24 -28
  183. package/dist/task/orchestrator.js +89 -66
  184. package/dist/task/orientation.d.ts +18 -23
  185. package/dist/task/orientation.js +24 -31
  186. package/dist/task/owned-freeze-conflict.d.ts +21 -20
  187. package/dist/task/owned-freeze-conflict.js +52 -85
  188. package/dist/task/owned-freeze-reassign.d.ts +40 -60
  189. package/dist/task/owned-freeze-reassign.js +41 -61
  190. package/dist/task/parsers.d.ts +4 -2
  191. package/dist/task/parsers.js +4 -4
  192. package/dist/task/phases.d.ts +41 -48
  193. package/dist/task/phases.js +196 -252
  194. package/dist/task/plan-io.d.ts +6 -7
  195. package/dist/task/plan-io.js +6 -7
  196. package/dist/task/plan-orchestrator.d.ts +10 -8
  197. package/dist/task/plan-orchestrator.js +14 -10
  198. package/dist/task/plan-prompts.d.ts +6 -5
  199. package/dist/task/plan-prompts.js +6 -5
  200. package/dist/task/plan-readonly.d.ts +4 -5
  201. package/dist/task/plan-readonly.js +4 -5
  202. package/dist/task/plan-rounds.d.ts +17 -29
  203. package/dist/task/plan-rounds.js +21 -34
  204. package/dist/task/plan-session.d.ts +58 -72
  205. package/dist/task/plan-session.js +61 -83
  206. package/dist/task/probe-gaming.d.ts +28 -27
  207. package/dist/task/probe-gaming.js +0 -0
  208. package/dist/task/prohibition-probe.d.ts +14 -16
  209. package/dist/task/prompts.d.ts +3 -4
  210. package/dist/task/prompts.js +17 -26
  211. package/dist/task/qa-transcript.d.ts +15 -22
  212. package/dist/task/qa-transcript.js +15 -21
  213. package/dist/task/question-box.d.ts +17 -13
  214. package/dist/task/question-box.js +19 -15
  215. package/dist/task/question-dedup.d.ts +6 -7
  216. package/dist/task/question-dedup.js +13 -14
  217. package/dist/task/question-dialog.d.ts +22 -32
  218. package/dist/task/question-dialog.js +22 -32
  219. package/dist/task/question-source.d.ts +18 -44
  220. package/dist/task/question-source.js +22 -51
  221. package/dist/task/refuted-constraint.d.ts +11 -31
  222. package/dist/task/refuted-constraint.js +27 -51
  223. package/dist/task/regenerable-artifacts.d.ts +12 -31
  224. package/dist/task/regenerable-artifacts.js +12 -31
  225. package/dist/task/render-check.d.ts +11 -22
  226. package/dist/task/render-check.js +33 -46
  227. package/dist/task/repo-health-check.d.ts +10 -14
  228. package/dist/task/repo-health-check.js +17 -23
  229. package/dist/task/requirements.d.ts +38 -71
  230. package/dist/task/requirements.js +78 -126
  231. package/dist/task/research-fanout-budget.d.ts +51 -88
  232. package/dist/task/research-fanout-budget.js +51 -88
  233. package/dist/task/research-worker.d.ts +29 -39
  234. package/dist/task/research-worker.js +37 -61
  235. package/dist/task/resume-gap.d.ts +14 -15
  236. package/dist/task/root-cause-repair.d.ts +9 -9
  237. package/dist/task/root-cause-repair.js +28 -40
  238. package/dist/task/run-bracket.d.ts +10 -13
  239. package/dist/task/run-end.d.ts +12 -22
  240. package/dist/task/run-end.js +8 -16
  241. package/dist/task/run-final-gate.d.ts +19 -21
  242. package/dist/task/run-final-gate.js +62 -80
  243. package/dist/task/runner-globs.d.ts +12 -13
  244. package/dist/task/runner-globs.js +12 -13
  245. package/dist/task/runner-resolve.d.ts +9 -9
  246. package/dist/task/runner-resolve.js +22 -23
  247. package/dist/task/script-escape.d.ts +10 -12
  248. package/dist/task/script-escape.js +13 -14
  249. package/dist/task/serve-entry.d.ts +1 -1
  250. package/dist/task/serve-entry.js +22 -25
  251. package/dist/task/service-blocks.js +4 -2
  252. package/dist/task/shipped-source.d.ts +11 -29
  253. package/dist/task/shipped-source.js +11 -29
  254. package/dist/task/skip-escape.js +10 -14
  255. package/dist/task/spec-urls.d.ts +26 -65
  256. package/dist/task/spec-urls.js +26 -65
  257. package/dist/task/spec-validation.d.ts +17 -20
  258. package/dist/task/spec-validation.js +17 -20
  259. package/dist/task/stall-detector.d.ts +23 -30
  260. package/dist/task/stall-detector.js +23 -30
  261. package/dist/task/stream-watchdog.d.ts +14 -12
  262. package/dist/task/stream-watchdog.js +14 -12
  263. package/dist/task/substitution-probe.d.ts +17 -20
  264. package/dist/task/substitution-probe.js +17 -20
  265. package/dist/task/task-gates.d.ts +36 -41
  266. package/dist/task/task-gates.js +95 -106
  267. package/dist/task/task-io.d.ts +4 -4
  268. package/dist/task/task-io.js +4 -4
  269. package/dist/task/task-parsers.js +4 -3
  270. package/dist/task/task-provenance.d.ts +2 -2
  271. package/dist/task/task-provenance.js +11 -13
  272. package/dist/task/task-types.d.ts +4 -3
  273. package/dist/task/terminal-outcome.d.ts +14 -16
  274. package/dist/task/terminal-outcome.js +12 -14
  275. package/dist/task/test-assembly.d.ts +13 -20
  276. package/dist/task/test-assembly.js +13 -20
  277. package/dist/task/timings.d.ts +5 -3
  278. package/dist/task/timings.js +5 -3
  279. package/dist/task/title-label.d.ts +9 -4
  280. package/dist/task/title-label.js +9 -4
  281. package/dist/task/type-only-answer.d.ts +44 -52
  282. package/dist/task/type-only-answer.js +44 -52
  283. package/dist/task/unfailable-command.d.ts +18 -24
  284. package/dist/task/unfailable-command.js +21 -27
  285. package/dist/task/unknown-routing.d.ts +10 -4
  286. package/dist/task/unknown-routing.js +10 -4
  287. package/dist/task/user-directives.d.ts +5 -8
  288. package/dist/task/user-directives.js +5 -8
  289. package/dist/task/verify-quality.d.ts +18 -22
  290. package/dist/task/verify-quality.js +45 -46
  291. package/dist/task/verify-reconcile.d.ts +15 -10
  292. package/dist/task/verify-reconcile.js +45 -43
  293. package/dist/task/verify-resolution.d.ts +24 -20
  294. package/dist/task/verify-resolution.js +51 -50
  295. package/dist/task/verify-work.d.ts +59 -66
  296. package/dist/task/verify-work.js +101 -138
  297. package/dist/task/widget.d.ts +15 -14
  298. package/dist/task/widget.js +22 -17
  299. package/dist/task/wiring-claims.d.ts +25 -32
  300. package/dist/task/wiring-claims.js +30 -35
  301. package/dist/task/write-guard.d.ts +39 -39
  302. package/dist/task/write-guard.js +48 -51
  303. package/dist/task/yolo.d.ts +34 -30
  304. package/dist/task/yolo.js +42 -37
  305. package/dist/workers/abstention.d.ts +21 -41
  306. package/dist/workers/abstention.js +27 -48
  307. package/dist/workers/brave-search.d.ts +4 -3
  308. package/dist/workers/brave-search.js +5 -2
  309. package/dist/workers/brave-warning.d.ts +7 -4
  310. package/dist/workers/brave-warning.js +19 -7
  311. package/dist/workers/ddg-search.d.ts +6 -6
  312. package/dist/workers/ddg-search.js +18 -12
  313. package/dist/workers/docs-cache.js +5 -2
  314. package/dist/workers/docs-chunk.d.ts +30 -37
  315. package/dist/workers/docs-chunk.js +37 -41
  316. package/dist/workers/docs-core.d.ts +28 -44
  317. package/dist/workers/docs-core.js +25 -44
  318. package/dist/workers/docs-index.js +4 -3
  319. package/dist/workers/docs-lookup.d.ts +15 -22
  320. package/dist/workers/docs-lookup.js +12 -21
  321. package/dist/workers/docs-project.d.ts +15 -9
  322. package/dist/workers/docs-project.js +17 -10
  323. package/dist/workers/docs-resolve.d.ts +19 -20
  324. package/dist/workers/docs-resolve.js +35 -32
  325. package/dist/workers/docs-retrieve.d.ts +5 -6
  326. package/dist/workers/docs-retrieve.js +18 -15
  327. package/dist/workers/exa-search.d.ts +9 -6
  328. package/dist/workers/exa-search.js +23 -12
  329. package/dist/workers/fetch-core.d.ts +13 -16
  330. package/dist/workers/fetch-core.js +23 -23
  331. package/dist/workers/focused-extractor.d.ts +13 -12
  332. package/dist/workers/focused-extractor.js +27 -19
  333. package/dist/workers/html-clean.js +24 -14
  334. package/dist/workers/http-request.d.ts +28 -20
  335. package/dist/workers/http-request.js +22 -17
  336. package/dist/workers/npm-version.d.ts +28 -11
  337. package/dist/workers/npm-version.js +24 -15
  338. package/dist/workers/phantom-imports.d.ts +15 -12
  339. package/dist/workers/phantom-imports.js +30 -24
  340. package/dist/workers/pi-worker-core.d.ts +65 -96
  341. package/dist/workers/pi-worker-core.js +93 -181
  342. package/dist/workers/pi-worker-docs.d.ts +24 -19
  343. package/dist/workers/pi-worker-docs.js +67 -76
  344. package/dist/workers/pi-worker-fetch.d.ts +7 -3
  345. package/dist/workers/pi-worker-fetch.js +27 -19
  346. package/dist/workers/pi-worker-search.js +12 -8
  347. package/dist/workers/pi-worker.d.ts +9 -4
  348. package/dist/workers/pi-worker.js +21 -14
  349. package/dist/workers/reasoning-warning.d.ts +18 -17
  350. package/dist/workers/reasoning-warning.js +22 -20
  351. package/dist/workers/research-cache.js +50 -78
  352. package/dist/workers/search-core.js +7 -5
  353. package/dist/workers/search-types.d.ts +10 -9
  354. package/dist/workers/search-types.js +9 -8
  355. package/dist/workers/session-hint.d.ts +13 -14
  356. package/dist/workers/session-hint.js +8 -9
  357. package/dist/workers/shared.d.ts +21 -25
  358. package/dist/workers/shared.js +0 -0
  359. package/dist/workers/single-read-extension.d.ts +14 -7
  360. package/dist/workers/single-read-extension.js +14 -7
  361. package/dist/workers/single-read-guard.d.ts +27 -30
  362. package/dist/workers/single-read-guard.js +36 -36
  363. package/dist/workers/typeonly-log.d.ts +12 -9
  364. package/dist/workers/typeonly-log.js +29 -33
  365. package/dist/workers/worker-channels.d.ts +15 -23
  366. package/dist/workers/worker-channels.js +15 -23
  367. package/dist/workers/worker-failure.d.ts +38 -46
  368. package/dist/workers/worker-failure.js +31 -39
  369. package/dist/workers/worker-kill.d.ts +25 -26
  370. package/dist/workers/worker-kill.js +16 -19
  371. package/dist/workers/worker-profiles.d.ts +54 -56
  372. package/dist/workers/worker-profiles.js +63 -39
  373. package/package.json +10 -8
@@ -2,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
@@ -32,16 +36,17 @@
32
36
  * {@link commandTimeoutHint} prepended. Coarser by necessity:
33
37
  * the child's accumulated context is lost.
34
38
  */
35
- /** Whole minutes, floored at 1, for the human-facing ceiling in both messages. */
39
+ /** Whole minutes for the human-facing ceiling in both messages: rounded to
40
+ * nearest, then floored at 1 so a sub-minute ceiling never reads "0 minutes". */
36
41
  function minutes(timeoutMs) {
37
42
  const mins = Math.max(1, Math.round(timeoutMs / 60_000));
38
43
  return `${mins} minute${mins === 1 ? '' : 's'}`;
39
44
  }
40
45
  /**
41
- * The core correction, shared by both adapters' messages. Two jobs, both
42
- * learned from live runs: stop the model reporting a killed command as a
43
- * success, and name the ONE mechanism that prevents a repeat (the bash tool's
44
- * own `timeout` parameter) rather than leaving "be faster" as the takeaway.
46
+ * The core correction, shared by both adapters' messages. Two jobs: stop the
47
+ * model reporting a killed command as a success, and name the one mechanism
48
+ * that prevents a repeat the bash tool's own `timeout` parameter — rather
49
+ * than leaving "be faster" as the takeaway.
45
50
  */
46
51
  function correction() {
47
52
  return (`The command was killed before it finished and produced NO result, so do not `
@@ -82,9 +87,9 @@ export function reminderMessage(toolName, timeoutMs) {
82
87
  * child that believes it starts clean may re-apply them or misread the tree.
83
88
  * Only the CONVERSATION is gone; the hint must say so precisely.
84
89
  *
85
- * Replaces the generic worker-timeout hint for this case: that one blames
86
- * "exploring too long", which is the wrong diagnosis for a hung command and
87
- * never mentions the timeout parameter.
90
+ * Used instead of the generic worker-timeout hint, which blames the child for
91
+ * "exploring too long" the wrong diagnosis for a hung command, and one that
92
+ * never mentions the `timeout` parameter that would prevent it.
88
93
  */
89
94
  export function commandTimeoutHint(toolName, timeoutMs, opts) {
90
95
  const what = opts?.commandDetail ? ` (${opts.commandDetail})` : '';
@@ -103,9 +108,10 @@ export function commandTimeoutHint(toolName, timeoutMs, opts) {
103
108
  }
104
109
  export class CommandWatchdog {
105
110
  deps;
106
- /** Armed timers, keyed by the tool call they guard. Tool executions are
107
- * sequential, so this holds at most one entry in normal operation, but the
108
- * map keeps it correct even if pi ever overlaps two calls. */
111
+ /** Armed timers, keyed by the tool call they guard. A map rather than one
112
+ * handle because pi's agent defaults `toolExecution` to "parallel": a batch
113
+ * of sibling calls is genuinely in flight together, so several timers can be
114
+ * armed at once and each must be cancelled by its own id. */
109
115
  active = new Map();
110
116
  constructor(deps) {
111
117
  this.deps = deps;
@@ -154,14 +160,102 @@ export class CommandWatchdog {
154
160
  * Real-clock schedule/cancel. Shared by both adapters; tests substitute a fake
155
161
  * scheduler instead.
156
162
  *
157
- * REF'd, for the same measured reason as the stream watchdog's poll (see
158
- * realStreamTimerDeps): under Bun on windows an unref'd timer never fires once
159
- * nothing ref'd is pending, and a child sitting in a hung command is exactly
160
- * that state so the unref disabled the guard in the one case it exists for.
161
- * The timer is cleared when the tool ends and in runWorker's `finally`, so it
162
- * cannot outlive the call it watches.
163
+ * REF'd deliberately, and NOT unref'd. Measured on this platform, on both
164
+ * runtimes: an unref'd timer with nothing else ref'd and pending never fires at
165
+ * all the process simply exits while the identical ref'd timer does. A
166
+ * watchdog whose whole job is to fire while everything else is stuck cannot
167
+ * afford that. Holding the loop open is safe here because the timer is cancelled
168
+ * when the tool ends and again in runWorker's `finally`, so it cannot outlive
169
+ * the call it watches.
163
170
  */
164
171
  export const realTimerDeps = {
165
172
  schedule: (fn, ms) => setTimeout(fn, ms),
166
173
  cancel: handle => clearTimeout(handle)
167
174
  };
175
+ /**
176
+ * Build the child-side command watchdog for ONE attempt: a per-tool-call timer
177
+ * machine whose `onFire` aborts `signal`, which runChild turns into a
178
+ * process-GROUP kill — reaping the hung command itself, not just the pi child
179
+ * holding it.
180
+ *
181
+ * LIMIT: the group kill only reaches processes still IN the group. A hung command
182
+ * that detached a daemon (setsid, nohup, a background dev server) leaves it
183
+ * running, so the fresh attempt can hit a port the dead attempt's escapee still
184
+ * holds. There is no cheap fix from here; the restart hint's "check current state"
185
+ * line is the mitigation.
186
+ *
187
+ * Returns null when the watchdog is off, so the caller keeps the plain timeout
188
+ * signal and no per-call bookkeeping happens at all.
189
+ */
190
+ export function commandWatch(timeoutMs) {
191
+ if (!(timeoutMs > 0))
192
+ return null;
193
+ const ctrl = new AbortController();
194
+ // pi's toolCallId pairs start↔end. When it is absent (a fake stream in a
195
+ // test, an older pi), fall back to one shared slot: tool executions in a
196
+ // child are sequential, so a single slot is still correctly paired.
197
+ const key = (id) => id ?? 'anon';
198
+ const details = new Map();
199
+ let killed;
200
+ const watchdog = new CommandWatchdog({
201
+ getTimeoutMs: () => timeoutMs,
202
+ ...realTimerDeps,
203
+ onFire: (toolCallId, toolName, ms) => {
204
+ killed = {
205
+ toolName,
206
+ timeoutMs: ms,
207
+ ...(details.has(toolCallId) ? { detail: details.get(toolCallId) } : {})
208
+ };
209
+ ctrl.abort();
210
+ }
211
+ });
212
+ return {
213
+ onStart: call => {
214
+ const id = key(call.toolCallId);
215
+ const args = call.args;
216
+ if (typeof args?.command === 'string') {
217
+ details.set(id, args.command.slice(0, 120));
218
+ }
219
+ watchdog.onStart(id, call.name);
220
+ },
221
+ onEnd: id => {
222
+ // Drop the command line with its call. The `'anon'` fallback above is a
223
+ // SHARED slot, so a stale entry would be attributed to whatever ran
224
+ // next: a `read` that later overran would be reported as
225
+ // "ran a `read` command (bun run dev)".
226
+ details.delete(key(id));
227
+ watchdog.onEnd(key(id));
228
+ },
229
+ killed: () => killed,
230
+ signal: ctrl.signal,
231
+ clear: () => watchdog.clearAll()
232
+ };
233
+ }
234
+ /**
235
+ * The per-command ceiling for attempt N, halving each time a hang recurs.
236
+ *
237
+ * The first attempt gets the full configured ceiling — a genuinely slow build or
238
+ * test suite deserves it. But every hang-caused restart carries
239
+ * commandTimeoutHint, which tells the model in as many words to bound its
240
+ * command; a SECOND hang means it ignored an explicit instruction, and a third
241
+ * means it ignored it twice. Giving a non-complying child the full ceiling again
242
+ * makes the worst case three times the ceiling, resting entirely on the model
243
+ * obeying prose. Halving bounds it at under twice the ceiling while costing a
244
+ * complying child nothing.
245
+ *
246
+ * `priorHangs` counts watchdog kills specifically, NOT total restarts — the
247
+ * restart budget is shared with loop kills, and a child restarted for LOOPING
248
+ * never received the bound-your-command hint, so its first hang still deserves
249
+ * the full ceiling. Only a hang after a hang is defiance.
250
+ *
251
+ * Floored at 30s so repeated halving cannot shrink the ceiling to something no
252
+ * real command could finish inside — but the floor is `min(base, 30s)`, never
253
+ * above the configured ceiling, so a caller asking for 10s keeps 10s at every
254
+ * hang count. A base of 0 or less disables the watchdog and stays 0.
255
+ */
256
+ export function commandCeilingForAttempt(baseMs, priorHangs) {
257
+ if (!(baseMs > 0))
258
+ return 0;
259
+ const floor = Math.min(baseMs, 30_000);
260
+ return Math.max(floor, Math.round(baseMs / 2 ** priorHangs));
261
+ }
@@ -1,18 +1,24 @@
1
1
  /**
2
2
  * Text-file reads with line endings normalized to LF.
3
3
  *
4
- * Every task-file parser in pi-task assumes `\n` line endings: the front-matter
5
- * regex (`^---\n`), the body-delimiter strip, `sectionRegex` (`## h[ \t]*\n`),
6
- * the checkbox/VERIFY-token line splits. On Windows a task file can carry CRLF
7
- * (an editor save, or a `git` checkout with `core.autocrlf=true`), and classic
8
- * Mac tooling can emit lone CR. Any of those makes `parseFrontMatter` return
9
- * null ("malformed front matter") and every `extractSection` miss.
4
+ * A checked-out task file need not hold LF. Confirmed against real git: with
5
+ * `core.autocrlf=true`, a blob committed as LF comes back into the working tree
6
+ * as CRLF while the stored object is unchanged. Other tooling can write a lone CR.
10
7
  *
11
- * Rather than teach each parser about `\r`, we normalize ONCE at the read
12
- * boundary the single place file bytes enter the task layer so everything
13
- * downstream sees LF. See GitHub issue #1.
8
+ * CRLF alone would not need this any more `parseFrontMatter` and `sectionRegex`
9
+ * both spell `\r?\n`, and a CRLF task file parses correctly with or without
10
+ * normalization. LONE CR is the case that still does. Measured on an otherwise
11
+ * valid task file: with CR line endings `parseFrontMatter` returns null and
12
+ * `extractSection` finds nothing; normalized first, both succeed. Neither parser
13
+ * regex can match a lone CR, because both require an `\n`.
14
+ *
15
+ * Normalizing once at the read boundary is what keeps that from becoming a third
16
+ * line-ending case in every parser. Every task-file read goes through
17
+ * `readTextFile`; the `readFileSync` calls elsewhere in the task layer read
18
+ * package.json, Makefiles and .env files, which have parsers of their own.
14
19
  */
15
- /** Collapse CRLF and lone CR to LF. `\r\n?` matches Windows CRLF and classic-Mac CR. */
20
+ /** Collapse CRLF and lone CR to LF. The optional `\n` in `\r\n?` consumes a CRLF
21
+ * pair as one unit, so a CRLF file does not come back with doubled newlines. */
16
22
  export declare function normalizeNewlines(text: string): string;
17
23
  /** Read a UTF-8 text file with line endings normalized to LF. */
18
24
  export declare function readTextFile(filePath: string): Promise<string>;
@@ -1,19 +1,25 @@
1
1
  /**
2
2
  * Text-file reads with line endings normalized to LF.
3
3
  *
4
- * Every task-file parser in pi-task assumes `\n` line endings: the front-matter
5
- * regex (`^---\n`), the body-delimiter strip, `sectionRegex` (`## h[ \t]*\n`),
6
- * the checkbox/VERIFY-token line splits. On Windows a task file can carry CRLF
7
- * (an editor save, or a `git` checkout with `core.autocrlf=true`), and classic
8
- * Mac tooling can emit lone CR. Any of those makes `parseFrontMatter` return
9
- * null ("malformed front matter") and every `extractSection` miss.
4
+ * A checked-out task file need not hold LF. Confirmed against real git: with
5
+ * `core.autocrlf=true`, a blob committed as LF comes back into the working tree
6
+ * as CRLF while the stored object is unchanged. Other tooling can write a lone CR.
10
7
  *
11
- * Rather than teach each parser about `\r`, we normalize ONCE at the read
12
- * boundary the single place file bytes enter the task layer so everything
13
- * downstream sees LF. See GitHub issue #1.
8
+ * CRLF alone would not need this any more `parseFrontMatter` and `sectionRegex`
9
+ * both spell `\r?\n`, and a CRLF task file parses correctly with or without
10
+ * normalization. LONE CR is the case that still does. Measured on an otherwise
11
+ * valid task file: with CR line endings `parseFrontMatter` returns null and
12
+ * `extractSection` finds nothing; normalized first, both succeed. Neither parser
13
+ * regex can match a lone CR, because both require an `\n`.
14
+ *
15
+ * Normalizing once at the read boundary is what keeps that from becoming a third
16
+ * line-ending case in every parser. Every task-file read goes through
17
+ * `readTextFile`; the `readFileSync` calls elsewhere in the task layer read
18
+ * package.json, Makefiles and .env files, which have parsers of their own.
14
19
  */
15
20
  import * as fsp from 'node:fs/promises';
16
- /** Collapse CRLF and lone CR to LF. `\r\n?` matches Windows CRLF and classic-Mac CR. */
21
+ /** Collapse CRLF and lone CR to LF. The optional `\n` in `\r\n?` consumes a CRLF
22
+ * pair as one unit, so a CRLF file does not come back with doubled newlines. */
17
23
  export function normalizeNewlines(text) {
18
24
  return text.replace(/\r\n?/g, '\n');
19
25
  }
@@ -1,33 +1,33 @@
1
1
  /**
2
- * git-runner — the ONE way this codebase runs `git`.
3
- *
4
- * WHY IT EXISTS. The right shape already existed, privately, inside
5
- * `task/git-state-guard.ts`: an abort-aware async runner carrying an injectable
6
- * `spawnFn` seam, returning `{stdout, exitCode}` and never throwing. Nothing
7
- * outside that file could reach it, so ~27 other sites hand-rolled their own —
8
- * with FOUR incompatible failure contracts (throws / returns null / returns '' /
9
- * returns a result object) and THREE different maxBuffer limits. A caller reading
10
- * one of them learns nothing about the next, and a git failure means something
11
- * different at every site. Lifting the seam here does not add a capability; it
12
- * removes the ambiguity about which contract you are holding.
2
+ * git-runner — the shared async way to run `git`, and the one to reach for.
13
3
  *
14
4
  * THE CONTRACT, deliberately narrow:
15
- * - NEVER THROWS. A missing git, a non-repo cwd, a fatal subcommand — all of
16
- * them arrive as a non-zero `exitCode`. Callers branch on the number, they do
17
- * not wrap in try/catch. (`git rev-parse --verify HEAD` on an unborn HEAD is
18
- * an ordinary answer, not an exception, and every guard built on this one
19
- * treats "git could not tell me" as "no claim" rather than a crash.)
5
+ * - NEVER THROWS. Every failure arrives as a non-zero `exitCode`, measured
6
+ * rather than assumed: a non-repo cwd answers 128, a bogus subcommand
7
+ * answers 1, `git rev-parse --verify HEAD` on a freshly-init'd repo with an
8
+ * unborn HEAD answers 128, and even a MISSING binary comes back as
9
+ * `{exitCode: 1, aborted: false}` instead of rejecting. Callers branch on
10
+ * the number, they do not wrap in try/catch, and every guard built on this
11
+ * treats "git could not tell me" as "no claim" rather than a crash.
20
12
  * - Only `stdout` and `exitCode` are exposed. stderr is deliberately absent:
21
- * the callers that legitimately need it (auto-commit's identity-failure
22
- * sniffing) also need `aborted`, and widening this type for them would push
23
- * two fields nobody else reads onto every call site.
13
+ * the caller that legitimately needs it (auto-commit's identity-failure
14
+ * sniffing) also needs `aborted`, and widening this type for it would push
15
+ * two fields nobody else reads onto every call site. That one caller goes
16
+ * to `runChildDefault` directly instead.
24
17
  * - `signal` is honoured by the underlying runChild, including its listener
25
- * detach discipline (GitHub issue #9) — a run-long orchestrator signal does
26
- * not accumulate one listener per git invocation.
27
- * - `env` entries are MERGED over `process.env` (the GIT_INDEX_FILE
28
- * throwaway-index pattern), not substituted for it.
29
- * - `spawnFn` is the test seam: pass a fake from `test-utils/fake-spawn.ts` and
30
- * the runner never touches a real repo.
18
+ * detach discipline — a run-long orchestrator signal does not accumulate
19
+ * one listener per git invocation.
20
+ * - `env` entries are MERGED over `process.env`, not substituted for it, which
21
+ * is what lets git-state-guard point `GIT_INDEX_FILE` at a throwaway index
22
+ * without losing PATH and HOME. Confirmed: such a call still resolves the
23
+ * repo normally.
24
+ * - `spawnFn` is the test seam: pass a fake from `test/test-utils/fake-spawn.ts`
25
+ * and the runner never touches a real repo.
26
+ *
27
+ * It is NOT universal, which matters before assuming any git call arrived here.
28
+ * Several sites still invoke git themselves with `spawnSync`, each carrying its
29
+ * own timeout and one its own maxBuffer, and accept-debt goes through the
30
+ * bounded command runner instead.
31
31
  */
32
32
  import { type SpawnFn } from './child-process.js';
33
33
  export interface GitRunner {
@@ -1,33 +1,33 @@
1
1
  /**
2
- * git-runner — the ONE way this codebase runs `git`.
3
- *
4
- * WHY IT EXISTS. The right shape already existed, privately, inside
5
- * `task/git-state-guard.ts`: an abort-aware async runner carrying an injectable
6
- * `spawnFn` seam, returning `{stdout, exitCode}` and never throwing. Nothing
7
- * outside that file could reach it, so ~27 other sites hand-rolled their own —
8
- * with FOUR incompatible failure contracts (throws / returns null / returns '' /
9
- * returns a result object) and THREE different maxBuffer limits. A caller reading
10
- * one of them learns nothing about the next, and a git failure means something
11
- * different at every site. Lifting the seam here does not add a capability; it
12
- * removes the ambiguity about which contract you are holding.
2
+ * git-runner — the shared async way to run `git`, and the one to reach for.
13
3
  *
14
4
  * THE CONTRACT, deliberately narrow:
15
- * - NEVER THROWS. A missing git, a non-repo cwd, a fatal subcommand — all of
16
- * them arrive as a non-zero `exitCode`. Callers branch on the number, they do
17
- * not wrap in try/catch. (`git rev-parse --verify HEAD` on an unborn HEAD is
18
- * an ordinary answer, not an exception, and every guard built on this one
19
- * treats "git could not tell me" as "no claim" rather than a crash.)
5
+ * - NEVER THROWS. Every failure arrives as a non-zero `exitCode`, measured
6
+ * rather than assumed: a non-repo cwd answers 128, a bogus subcommand
7
+ * answers 1, `git rev-parse --verify HEAD` on a freshly-init'd repo with an
8
+ * unborn HEAD answers 128, and even a MISSING binary comes back as
9
+ * `{exitCode: 1, aborted: false}` instead of rejecting. Callers branch on
10
+ * the number, they do not wrap in try/catch, and every guard built on this
11
+ * treats "git could not tell me" as "no claim" rather than a crash.
20
12
  * - Only `stdout` and `exitCode` are exposed. stderr is deliberately absent:
21
- * the callers that legitimately need it (auto-commit's identity-failure
22
- * sniffing) also need `aborted`, and widening this type for them would push
23
- * two fields nobody else reads onto every call site.
13
+ * the caller that legitimately needs it (auto-commit's identity-failure
14
+ * sniffing) also needs `aborted`, and widening this type for it would push
15
+ * two fields nobody else reads onto every call site. That one caller goes
16
+ * to `runChildDefault` directly instead.
24
17
  * - `signal` is honoured by the underlying runChild, including its listener
25
- * detach discipline (GitHub issue #9) — a run-long orchestrator signal does
26
- * not accumulate one listener per git invocation.
27
- * - `env` entries are MERGED over `process.env` (the GIT_INDEX_FILE
28
- * throwaway-index pattern), not substituted for it.
29
- * - `spawnFn` is the test seam: pass a fake from `test-utils/fake-spawn.ts` and
30
- * the runner never touches a real repo.
18
+ * detach discipline — a run-long orchestrator signal does not accumulate
19
+ * one listener per git invocation.
20
+ * - `env` entries are MERGED over `process.env`, not substituted for it, which
21
+ * is what lets git-state-guard point `GIT_INDEX_FILE` at a throwaway index
22
+ * without losing PATH and HOME. Confirmed: such a call still resolves the
23
+ * repo normally.
24
+ * - `spawnFn` is the test seam: pass a fake from `test/test-utils/fake-spawn.ts`
25
+ * and the runner never touches a real repo.
26
+ *
27
+ * It is NOT universal, which matters before assuming any git call arrived here.
28
+ * Several sites still invoke git themselves with `spawnSync`, each carrying its
29
+ * own timeout and one its own maxBuffer, and accept-debt goes through the
30
+ * bounded command runner instead.
31
31
  */
32
32
  import { runChildDefault } from './child-process.js';
33
33
  export function makeGit(cwd, signal, spawnFn) {
@@ -2,10 +2,9 @@
2
2
  * Detect tool calls that leaked into a child's assistant *text* instead of
3
3
  * being executed.
4
4
  *
5
- * Background: every child pi runs under `--mode json`; pi-task only ever treats
6
- * a structured `tool_execution_start` event as a tool call (see
7
- * shared/child-process.ts). When a local model emits a call in a markup dialect
8
- * pi's harness doesn't recognise — e.g.
5
+ * A call only counts when a structured `tool_execution_start` event fires
6
+ * shared/child-process.ts recognises nothing else. Model-side tool-call markup
7
+ * like
9
8
  *
10
9
  * <tool_call>
11
10
  * <function=bash>
@@ -13,14 +12,21 @@
13
12
  * </function>
14
13
  * </tool_call>
15
14
  *
16
- * pi passes the raw markup through as ordinary assistant text. The command never
17
- * runs, no event fires, and pi-task's guards (loop detector, widget) never see
18
- * it. The phase then "passes" on its only gates (non-empty text + exit 0) and
19
- * the unexecuted call flows downstream — a silently skipped beat.
15
+ * is not a format pi itself parses: those tags appear nowhere in any installed
16
+ * pi package. So whether such a turn RUNS is decided entirely by the inference
17
+ * server in front of it, and both outcomes are real.
20
18
  *
21
- * This is fundamentally an upstream mismatch (model output format pi's parser)
22
- * that pi-task cannot fix. What it CAN do is notice the leaked markup and refuse
23
- * to accept the turn, so the skip becomes visible instead of silent.
19
+ * Checked against a live endpoint, the server parsed the dialect itself and
20
+ * handed pi structured tool calls the markup EXECUTED rather than leaking, and
21
+ * the parse was sloppy enough to fold the closing tags and the surrounding prose
22
+ * into the command argument. A server that does not parse it produces the case
23
+ * this module exists for: pi receives the markup as ordinary assistant text, the
24
+ * command never runs, no event fires, and pi-task's guards never see it. The
25
+ * turn then clears its only acceptance gates — non-empty assistant text and exit
26
+ * 0 — and the unexecuted call flows downstream as a silently skipped beat.
27
+ *
28
+ * pi-task cannot fix the mismatch. What it CAN do is notice the markup in the
29
+ * answer and refuse the turn, so the skip becomes visible instead of silent.
24
30
  */
25
31
  export declare const MAX_LEAK_RETRIES = 2;
26
32
  /**
@@ -2,10 +2,9 @@
2
2
  * Detect tool calls that leaked into a child's assistant *text* instead of
3
3
  * being executed.
4
4
  *
5
- * Background: every child pi runs under `--mode json`; pi-task only ever treats
6
- * a structured `tool_execution_start` event as a tool call (see
7
- * shared/child-process.ts). When a local model emits a call in a markup dialect
8
- * pi's harness doesn't recognise — e.g.
5
+ * A call only counts when a structured `tool_execution_start` event fires
6
+ * shared/child-process.ts recognises nothing else. Model-side tool-call markup
7
+ * like
9
8
  *
10
9
  * <tool_call>
11
10
  * <function=bash>
@@ -13,25 +12,34 @@
13
12
  * </function>
14
13
  * </tool_call>
15
14
  *
16
- * pi passes the raw markup through as ordinary assistant text. The command never
17
- * runs, no event fires, and pi-task's guards (loop detector, widget) never see
18
- * it. The phase then "passes" on its only gates (non-empty text + exit 0) and
19
- * the unexecuted call flows downstream — a silently skipped beat.
15
+ * is not a format pi itself parses: those tags appear nowhere in any installed
16
+ * pi package. So whether such a turn RUNS is decided entirely by the inference
17
+ * server in front of it, and both outcomes are real.
20
18
  *
21
- * This is fundamentally an upstream mismatch (model output format pi's parser)
22
- * that pi-task cannot fix. What it CAN do is notice the leaked markup and refuse
23
- * to accept the turn, so the skip becomes visible instead of silent.
19
+ * Checked against a live endpoint, the server parsed the dialect itself and
20
+ * handed pi structured tool calls the markup EXECUTED rather than leaking, and
21
+ * the parse was sloppy enough to fold the closing tags and the surrounding prose
22
+ * into the command argument. A server that does not parse it produces the case
23
+ * this module exists for: pi receives the markup as ordinary assistant text, the
24
+ * command never runs, no event fires, and pi-task's guards never see it. The
25
+ * turn then clears its only acceptance gates — non-empty assistant text and exit
26
+ * 0 — and the unexecuted call flows downstream as a silently skipped beat.
27
+ *
28
+ * pi-task cannot fix the mismatch. What it CAN do is notice the markup in the
29
+ * answer and refuse the turn, so the skip becomes visible instead of silent.
24
30
  */
25
31
  // A child that wrote a tool call as plain text (wrong dialect, never executed)
26
32
  // gets re-prompted with a correction hint up to this many times before the
27
33
  // caller gives up. Mirrors MAX_LOOP_RESTARTS: 3 attempts total.
28
34
  export const MAX_LEAK_RETRIES = 2;
29
- // The Hermes-style wrapper a leaked call is most often wrapped in. pi-task never
30
- // legitimately emits this tag, so its presence alone is a confident signal.
35
+ // The Hermes-style wrapper. Nothing else in this codebase emits the tag its
36
+ // only other appearance is the correction hint below, which names it back to the
37
+ // model — so seeing it in an answer is signal enough on its own.
31
38
  const TOOL_CALL_WRAPPER = /<tool_call\b[^>]*>/i;
32
39
  // The "XML function call" dialect: <function=name> … <parameter=key>. Either tag
33
- // alone is too weak (a stray "<function=x>" can appear in prose or source), so we
34
- // require the structural pair before flagging it.
40
+ // alone is too weak one can appear in prose or in source so the structural
41
+ // PAIR is required. Confirmed: a lone <function=bash> and a lone
42
+ // <parameter=command> each return null, and only the two together flag.
35
43
  const FUNCTION_TAG = /<function=[\w.-]+\s*>/i;
36
44
  const PARAMETER_TAG = /<parameter=[\w.-]+\s*>/i;
37
45
  /**
@@ -1,21 +1,26 @@
1
1
  /** Base URLs of every custom provider pi is configured with (possibly none). */
2
2
  export declare function discoverModelEndpoints(agentDir?: string): string[];
3
3
  /**
4
- * true → at least one endpoint ANSWERED (any HTTP status counts a 404 still
5
- * proves the server is alive); false every probe was refused or hung past the
6
- * timeout. An empty list is true: nothing to probe means never kill.
4
+ * true → at least one endpoint ANSWERED. Any HTTP status counts, because the
5
+ * question is liveness, not correctness: probing a path that 404s still returns
6
+ * true, while a closed port returns false. Both confirmed against a live server.
7
+ * An empty list is true — nothing to probe means never kill.
8
+ *
9
+ * `models` is joined RELATIVELY here, and deliberately: it lives under the
10
+ * OpenAI-compatible prefix a baseUrl already carries. Contrast `/props` below.
7
11
  */
8
12
  export declare function probeModelEndpoints(urls: string[], timeoutMs?: number): Promise<boolean>;
9
13
  /**
10
14
  * What a llama.cpp server's own chat template can actually do about reasoning,
11
15
  * as reported by `GET /props`.
12
16
  *
13
- * This is the only source of truth that does NOT come from models.json. It
14
- * answers the one question the host-side clamp cannot: *is models.json lying
15
- * about the server?* the case that matters being pi's built-in llama.cpp
16
- * provider, which hardcodes `reasoning: false`, so anyone who reached their
17
- * server through `/login llama.cpp` rather than a hand-written provider entry
18
- * has a dead knob and nothing to tell them so.
17
+ * This is the only source of truth that does NOT come from models.json, and it
18
+ * answers the question the host-side clamp cannot: is models.json lying about
19
+ * the server? The case that matters is pi's built-in llama.cpp provider, which
20
+ * really does hardcode `reasoning: false` the literal line is in
21
+ * `extensions/llama/provider.js`, under `LLAMA_PROVIDER_ID = "llama.cpp"`. So
22
+ * anyone who reached their server through `/login llama.cpp` rather than a
23
+ * hand-written provider entry has a dead knob and nothing to tell them so.
19
24
  */
20
25
  export interface ChatTemplateCaps {
21
26
  /** The template reads `reasoning_effort` — i.e. levels, not just on/off. */
@@ -30,13 +35,21 @@ export interface ChatTemplateCaps {
30
35
  * answer worth having" — not a llama.cpp server, unreachable, or a body in a
31
36
  * shape this does not recognise.
32
37
  *
33
- * `null` is a first-class result, not an error: every non-llama.cpp backend
34
- * returns it, and the caller must degrade to the models.json view rather than
35
- * warn about a server it could not read. Never throws.
38
+ * `null` is a first-class result, not an error: a backend that does not serve
39
+ * this shape returns it, and the caller must degrade to the models.json view
40
+ * rather than warn about a server it could not read. Never throws — a closed
41
+ * port answers `null`, confirmed.
42
+ *
43
+ * The LEADING SLASH in `/props` is load-bearing, because `/props` lives at the
44
+ * server ROOT while a configured baseUrl carries an OpenAI-compatible prefix.
45
+ * How a relative `'props'` resolves depends on whether that prefix ends in a
46
+ * slash, which is not something this can rely on:
47
+ *
48
+ * new URL('props', 'http://host/v1') → /props
49
+ * new URL('props', 'http://host/v1/') → /v1/props ← 404
50
+ * new URL('/props', either) → /props
36
51
  *
37
- * The LEADING SLASH in `/props` is load-bearing. A configured baseUrl normally
38
- * ends in `/v1` (llama-server's OpenAI-compatible prefix) while `/props` lives at
39
- * the server root, so a relative `'props'` would resolve to `/v1/props` and 404 —
40
- * which this would report as `null`, i.e. as a silent loss of the better signal.
52
+ * A 404 would come back from here as `null`, silently losing the better signal.
53
+ * The absolute form is right for both.
41
54
  */
42
55
  export declare function probeChatTemplateCaps(baseUrl: string, timeoutMs?: number): Promise<ChatTemplateCaps | null>;