gentle-pi 3.7.0 → 4.1.0

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 (590) hide show
  1. package/README.md +155 -10
  2. package/assets/agents/gentle-ai-explore.md +13 -6
  3. package/assets/agents/gentle-ai-verify.md +27 -6
  4. package/assets/agents/gentle-ai-worker.md +13 -15
  5. package/assets/install-wizard/index.html +47 -0
  6. package/assets/install-wizard/wizard.css +1279 -0
  7. package/assets/install-wizard/wizard.js +951 -0
  8. package/assets/orchestrator-delegation.md +75 -169
  9. package/assets/orchestrator-memory.md +12 -26
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-skills.md +1 -1
  12. package/assets/orchestrator-tracking.md +53 -0
  13. package/assets/orchestrator-verification.md +28 -0
  14. package/assets/orchestrator-writer.md +38 -0
  15. package/assets/orchestrator.md +30 -38
  16. package/assets/sounds/LICENSE.md +52 -0
  17. package/assets/sounds/attention.wav +0 -0
  18. package/assets/sounds/error.wav +0 -0
  19. package/assets/sounds/success.wav +0 -0
  20. package/assets/support/strict-tdd-verify.md +4 -266
  21. package/assets/support/strict-tdd.md +8 -360
  22. package/bin/engram-guard +9 -0
  23. package/bin/gentle-shell-install.mjs +302 -0
  24. package/bin/gentle-shell.mjs +956 -50
  25. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  27. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  28. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  29. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  30. package/docs/bundled-install.md +397 -0
  31. package/docs/delegated-verification.md +27 -4
  32. package/docs/gentle-agents-activity.md +537 -0
  33. package/docs/gentle-shell-uninstall.md +136 -0
  34. package/docs/gentle-shell.md +165 -51
  35. package/docs/install-wizard.md +2040 -0
  36. package/docs/native-authority-architecture.md +3 -3
  37. package/docs/permission-modes.md +82 -0
  38. package/docs/prompt-history.md +280 -0
  39. package/docs/readme-reference.md +364 -231
  40. package/docs/render-cache-performance.md +28 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/telemetry.md +1 -1
  44. package/docs/yolo-mode.md +86 -0
  45. package/extensions/child-context.ts +35 -0
  46. package/extensions/child-safety.ts +25 -0
  47. package/extensions/codegraph-tools.ts +18 -5
  48. package/extensions/gentle-agents.ts +1093 -326
  49. package/extensions/gentle-ai.ts +1636 -890
  50. package/extensions/gentle-herdr-activity.ts +81 -0
  51. package/extensions/gentle-notifications.ts +104 -0
  52. package/extensions/gentle-shell.ts +1619 -136
  53. package/extensions/gentle-stats.ts +101 -0
  54. package/extensions/gentle-todo.ts +126 -20
  55. package/extensions/history/atomic-write.ts +38 -0
  56. package/extensions/history/hide-prompts.ts +183 -0
  57. package/extensions/history/index.ts +1424 -0
  58. package/extensions/history/load-shared-history.ts +39 -0
  59. package/extensions/history/selector-helpers.ts +538 -0
  60. package/extensions/history/session-scan.ts +233 -0
  61. package/extensions/history/store.ts +1119 -0
  62. package/extensions/nan-provider.ts +6 -0
  63. package/extensions/pi-pretty.ts +39 -3
  64. package/extensions/quiet-tools.ts +254 -92
  65. package/extensions/resume-hint.ts +60 -0
  66. package/extensions/skill-registry.ts +191 -58
  67. package/extensions/startup-banner.ts +71 -33
  68. package/lib/agent-assets.ts +604 -0
  69. package/lib/agent-home.ts +10 -1
  70. package/lib/agent-model-resolution.ts +80 -0
  71. package/lib/agent-profile-pin.ts +12 -0
  72. package/lib/agent-profiles.ts +13 -3
  73. package/lib/agents-completion-delivery.ts +11 -3
  74. package/lib/agents-message-delivery.ts +181 -0
  75. package/lib/agents-messaging.ts +1 -1
  76. package/lib/agents-protocol.ts +99 -0
  77. package/lib/agents-runner.ts +180 -107
  78. package/lib/agents-view.ts +26 -3
  79. package/lib/agents-widget.ts +16 -9
  80. package/lib/append-system-prompt.ts +21 -0
  81. package/lib/auto-permission-classifier.ts +280 -0
  82. package/lib/auto-permission-runtime.ts +268 -0
  83. package/lib/background-jobs-tools.ts +202 -0
  84. package/lib/background-jobs.ts +253 -0
  85. package/lib/background-monitor.ts +154 -0
  86. package/lib/bounded-writer-admission.ts +220 -0
  87. package/lib/builtin-codemode-optout.ts +166 -0
  88. package/lib/card-content-policy.ts +85 -0
  89. package/lib/card-style-policy.ts +60 -0
  90. package/lib/child-context-files.ts +166 -0
  91. package/lib/child-package-injection.ts +62 -0
  92. package/lib/codemode-renderer.ts +195 -0
  93. package/lib/command-palette-catalog.ts +5 -10
  94. package/lib/command-palette.ts +25 -14
  95. package/lib/context-threshold.ts +26 -0
  96. package/lib/destructive-command-guard.ts +144 -0
  97. package/lib/gentle-ai-binary.ts +109 -4
  98. package/lib/gentle-ai-elapsed-store.ts +87 -0
  99. package/lib/gentle-ai-renderer.ts +196 -39
  100. package/lib/gentle-shell-bundled.ts +517 -0
  101. package/lib/gentle-shell-launcher.ts +89 -27
  102. package/lib/gentle-shell-resume-hint.ts +176 -0
  103. package/lib/gentle-shell-startup-flow.ts +54 -0
  104. package/lib/gentle-shell-startup-update.ts +92 -0
  105. package/lib/gentle-shell-uninstall.ts +840 -0
  106. package/lib/gentle-shell-update-panel.ts +231 -0
  107. package/lib/herdr-activity.ts +183 -0
  108. package/lib/history-capture-policy.ts +95 -0
  109. package/lib/inprocess-reviewer.ts +3 -0
  110. package/lib/jobs-sidebar-state.ts +20 -0
  111. package/lib/jobs-view.ts +161 -0
  112. package/lib/model-price-ratio.ts +83 -0
  113. package/lib/model-routing-authority.ts +5 -1
  114. package/lib/nan-provider.ts +249 -0
  115. package/lib/native-review-cli.ts +242 -180
  116. package/lib/notification-audio-native.ts +162 -0
  117. package/lib/notification-audio-windows.ts +399 -0
  118. package/lib/notification-audio.ts +332 -0
  119. package/lib/notification-customize.ts +241 -0
  120. package/lib/notification-events.ts +113 -0
  121. package/lib/notification-policy.ts +148 -0
  122. package/lib/notification-pulse-client.ts +367 -0
  123. package/lib/notification-pulse-protocol.ts +305 -0
  124. package/lib/notification-pulse-stream.ts +213 -0
  125. package/lib/notification-pulse-worker.ts +66 -0
  126. package/lib/notification-scheduler.ts +173 -0
  127. package/lib/notification-service.ts +202 -0
  128. package/lib/notification-ui.ts +87 -0
  129. package/lib/odd-phase-inference.ts +231 -0
  130. package/lib/odd-phase.ts +141 -0
  131. package/lib/orchestrator-alias.ts +20 -0
  132. package/lib/orchestrator-catalog.ts +79 -0
  133. package/lib/orchestrator-consultation.ts +61 -0
  134. package/lib/orchestrator-discovery.ts +49 -0
  135. package/lib/orchestrator-helper-consent.ts +122 -0
  136. package/lib/orchestrator-helper.ts +167 -0
  137. package/lib/orchestrator-message-card.ts +105 -0
  138. package/lib/orchestrator-presence.ts +115 -14
  139. package/lib/orchestrator-scope.ts +80 -0
  140. package/lib/orchestrator-state.ts +107 -0
  141. package/lib/orchestrator-tool-card.ts +280 -0
  142. package/lib/orchestrator-work-search.ts +218 -0
  143. package/lib/orchestrator-work.ts +79 -0
  144. package/lib/overlay-repaint.ts +26 -0
  145. package/lib/permission-mode-policy.ts +59 -0
  146. package/lib/pi-tui-keys.ts +53 -0
  147. package/lib/review-candidate-view-owner.ts +73 -21
  148. package/lib/review-candidate-view.ts +115 -27
  149. package/lib/review-compact-contract.ts +2 -2
  150. package/lib/review-host-relay.ts +7 -2
  151. package/lib/review-integration-v2.ts +100 -22
  152. package/lib/review-publication-gate.ts +1 -1
  153. package/lib/review-reminder-receipt.ts +48 -8
  154. package/lib/review-risk-assessment.ts +227 -56
  155. package/lib/review-sidebar-state.ts +224 -0
  156. package/lib/review-transaction.ts +72 -3
  157. package/lib/runtime-metrics-children.ts +1 -1
  158. package/lib/runtime-metrics-native.ts +3 -2
  159. package/lib/selection-engine.ts +527 -0
  160. package/lib/session-change-capture.ts +5 -4
  161. package/lib/session-changes.ts +27 -22
  162. package/lib/session-messaging-grants.ts +135 -0
  163. package/lib/session-profile-binding.ts +83 -0
  164. package/lib/session-profile-freeze.ts +266 -0
  165. package/lib/session-worktree-registry.ts +87 -9
  166. package/lib/shell-bar.ts +215 -30
  167. package/lib/shell-card.ts +287 -18
  168. package/lib/shell-changes-view.ts +42 -14
  169. package/lib/shell-changes.ts +86 -71
  170. package/lib/shell-prompt.ts +102 -6
  171. package/lib/shell-sidebar-layout.ts +83 -15
  172. package/lib/shell-sidebar.ts +15 -1
  173. package/lib/shell-todo.ts +84 -31
  174. package/lib/shell-usage-view.ts +9 -4
  175. package/lib/shell-usage.ts +70 -13
  176. package/lib/stats-collector.ts +381 -0
  177. package/lib/stats-view.ts +431 -0
  178. package/lib/status-timing-diagnostics.ts +201 -0
  179. package/lib/telemetry-trigger.ts +3 -1
  180. package/lib/theme-customization.ts +52 -0
  181. package/lib/vim-editor-adapter.ts +383 -0
  182. package/lib/vim-normal-engine.ts +154 -0
  183. package/lib/vim-operator-engine.ts +416 -0
  184. package/lib/vim-policy.ts +49 -0
  185. package/lib/vim-visual-engine.ts +107 -0
  186. package/lib/visual-customization-policy.ts +119 -0
  187. package/lib/visual-customize-view.ts +481 -0
  188. package/lib/visual-profiles.ts +228 -0
  189. package/lib/writer-surfaces.ts +162 -0
  190. package/lib/yolo-session-policy.ts +317 -0
  191. package/package.json +20 -8
  192. package/runtime/child-package-injection.mjs +63 -0
  193. package/runtime/gentle-ai-binary.mjs +109 -4
  194. package/runtime/gentle-shell-bundled.mjs +518 -0
  195. package/runtime/gentle-shell-launcher.mjs +87 -25
  196. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  197. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  198. package/runtime/gentle-shell-startup-update.mjs +93 -0
  199. package/runtime/gentle-shell-uninstall.mjs +841 -0
  200. package/runtime/gentle-shell-update-panel.mjs +232 -0
  201. package/runtime/native-review-cli.mjs +239 -177
  202. package/runtime/review-integration-v2.mjs +100 -22
  203. package/runtime/review-risk-assessment.mjs +225 -54
  204. package/runtime/status-timing-diagnostics.mjs +202 -0
  205. package/runtime/telemetry-trigger.mjs +3 -1
  206. package/scripts/bootstrap.cmd +255 -0
  207. package/scripts/bootstrap.sh +168 -0
  208. package/scripts/build-distribution.mjs +157 -0
  209. package/scripts/build-installer-bundles.mjs +136 -0
  210. package/scripts/build-runtime-modules.mjs +8 -0
  211. package/scripts/bundled-install.mjs +1308 -0
  212. package/scripts/bundled-wizard.mjs +399 -0
  213. package/scripts/check-types.mjs +4 -6
  214. package/scripts/gentle-ai-installer.mjs +83 -35
  215. package/scripts/install-gentle-ai.mjs +48 -1
  216. package/scripts/install-tui-mode-setting.mjs +10 -1
  217. package/scripts/install-wizard-preview.mjs +154 -0
  218. package/scripts/installer-downloads.mjs +648 -0
  219. package/scripts/installer-preflight.mjs +387 -0
  220. package/scripts/installer-probes.mjs +564 -0
  221. package/scripts/installer-runner.mjs +1102 -0
  222. package/scripts/installer-server.mjs +881 -0
  223. package/scripts/installer-windows-artifacts.json +15 -0
  224. package/scripts/installer-windows.mjs +752 -0
  225. package/scripts/main-channel.mjs +452 -0
  226. package/scripts/mirror-odd-routing.mjs +2 -2
  227. package/scripts/npm-pack-result.mjs +31 -0
  228. package/scripts/render-cache-performance-worker.mjs +205 -0
  229. package/scripts/render-cache-performance.mjs +115 -0
  230. package/scripts/run-test-suite.mjs +90 -0
  231. package/scripts/test-installer-acceptance.sh +161 -0
  232. package/scripts/test-packed-runner.mjs +54 -19
  233. package/scripts/types-baseline.json +2 -92
  234. package/scripts/verify-package-files.mjs +91 -38
  235. package/skills/branch-pr/SKILL.md +24 -52
  236. package/skills/chained-pr/SKILL.md +31 -15
  237. package/skills/chained-pr/references/chaining-details.md +31 -20
  238. package/skills/gentle-ai/SKILL.md +16 -21
  239. package/skills/issue-creation/SKILL.md +8 -2
  240. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  241. package/skills/work-unit-commits/SKILL.md +4 -3
  242. package/tests/agent-home.test.ts +56 -3
  243. package/tests/agent-model-resolution.test.ts +113 -0
  244. package/tests/agent-profiles.test.ts +30 -0
  245. package/tests/agent-risk-escalation.test.ts +123 -0
  246. package/tests/agents-completion-delivery.test.ts +17 -0
  247. package/tests/agents-fake-child.ts +2 -2
  248. package/tests/agents-message-delivery.test.ts +106 -0
  249. package/tests/agents-protocol.test.ts +97 -0
  250. package/tests/agents-queries.test.ts +9 -9
  251. package/tests/agents-runner-process.test.ts +31 -0
  252. package/tests/agents-runner.test.ts +467 -93
  253. package/tests/agents-view-thread-identity.test.ts +169 -0
  254. package/tests/agents-view.test.ts +8 -2
  255. package/tests/agents-widget.test.ts +154 -15
  256. package/tests/append-system-prompt-route.test.ts +160 -0
  257. package/tests/append-system-prompt.test.ts +46 -0
  258. package/tests/artifact-language.test.ts +19 -213
  259. package/tests/ask-user-question.test.ts +44 -1
  260. package/tests/asset-installation-runtime.test.ts +5 -16
  261. package/tests/auto-mode-invariants.test.ts +258 -0
  262. package/tests/auto-mode-sdk.test.ts +57 -0
  263. package/tests/auto-mode.test.ts +49 -0
  264. package/tests/auto-permission-classifier.test.ts +283 -0
  265. package/tests/auto-permission-runtime.test.ts +196 -0
  266. package/tests/autonomous-guard.test.ts +69 -1
  267. package/tests/background-cache-warming.test.ts +2 -1
  268. package/tests/background-jobs.test.ts +227 -0
  269. package/tests/background-monitor.test.ts +195 -0
  270. package/tests/background-subagents-default-mode.test.ts +28 -5
  271. package/tests/bounded-stop-contract.test.ts +85 -0
  272. package/tests/bounded-writer-admission.test.ts +146 -0
  273. package/tests/branch-pr-skill.test.ts +43 -0
  274. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  275. package/tests/bridge-wake-rendering.test.ts +56 -0
  276. package/tests/build-distribution.test.ts +269 -0
  277. package/tests/builtin-codemode-optout.test.ts +242 -0
  278. package/tests/bundled-install.test.ts +1319 -0
  279. package/tests/bundled-wizard.test.ts +505 -0
  280. package/tests/card-content-policy.test.ts +68 -0
  281. package/tests/card-style-policy.test.ts +87 -0
  282. package/tests/chained-pr-skill.test.ts +124 -0
  283. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  284. package/tests/child-context-files.test.ts +255 -0
  285. package/tests/child-package-entrypoints.test.ts +65 -0
  286. package/tests/child-package-injection.test.ts +74 -0
  287. package/tests/child-safety.test.ts +94 -0
  288. package/tests/child-web-package.test.ts +224 -0
  289. package/tests/codegraph-tools.test.ts +35 -0
  290. package/tests/codemode-rendering.test.ts +596 -0
  291. package/tests/command-palette.test.ts +43 -7
  292. package/tests/context-threshold.test.ts +31 -0
  293. package/tests/delegate-for-reason-contract.test.ts +327 -0
  294. package/tests/delegated-key-learnings-contract.test.ts +3 -78
  295. package/tests/destructive-command-guard.test.ts +84 -0
  296. package/tests/devbinary/native-review-parity.devtest.ts +171 -3
  297. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  298. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  299. package/tests/engram-safety.test.ts +288 -0
  300. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  301. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  302. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  303. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  304. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  305. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  306. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  307. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  308. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  309. package/tests/generic-agent-tools.test.ts +69 -0
  310. package/tests/gentle-agents.test.ts +2804 -346
  311. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  312. package/tests/gentle-ai-binary.test.ts +21 -4
  313. package/tests/gentle-ai-child-guards.test.ts +202 -0
  314. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  315. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  316. package/tests/gentle-ai-installer.test.ts +566 -62
  317. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  318. package/tests/gentle-ai-renderer.test.ts +493 -14
  319. package/tests/gentle-ai.test.ts +1427 -236
  320. package/tests/gentle-card-text.ts +2 -1
  321. package/tests/gentle-herdr-activity.test.ts +100 -0
  322. package/tests/gentle-notifications.test.ts +427 -0
  323. package/tests/gentle-shell-bin.test.ts +896 -115
  324. package/tests/gentle-shell-bundled.test.ts +1002 -0
  325. package/tests/gentle-shell-launcher.test.ts +333 -65
  326. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  327. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  328. package/tests/gentle-shell-startup-update.test.ts +85 -0
  329. package/tests/gentle-shell-uninstall.test.ts +978 -0
  330. package/tests/gentle-shell-update-panel.test.ts +158 -0
  331. package/tests/gentle-shell.test.ts +4216 -119
  332. package/tests/gentle-stats.test.ts +152 -0
  333. package/tests/gentle-theme.test.ts +4 -1
  334. package/tests/gentle-todo.test.ts +333 -10
  335. package/tests/herdr-activity.test.ts +243 -0
  336. package/tests/history-agent-home.test.ts +81 -0
  337. package/tests/history-atomic-write.test.ts +57 -0
  338. package/tests/history-capture-policy.test.ts +102 -0
  339. package/tests/history-child-guard.test.ts +70 -0
  340. package/tests/history-command-registration.test.ts +164 -0
  341. package/tests/history-dedupe-entries.test.ts +123 -0
  342. package/tests/history-delete-backfill.test.ts +190 -0
  343. package/tests/history-delete-confirm.test.ts +460 -0
  344. package/tests/history-dispatch.test.ts +180 -0
  345. package/tests/history-drain-hidden.test.ts +110 -0
  346. package/tests/history-drain-order.test.ts +98 -0
  347. package/tests/history-expanded-globals.test.ts +62 -0
  348. package/tests/history-gc.test.ts +832 -0
  349. package/tests/history-header-layout.test.ts +265 -0
  350. package/tests/history-hide-prompts.test.ts +275 -0
  351. package/tests/history-lazy-windowing.test.ts +508 -0
  352. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  353. package/tests/history-load-shared-history.test.ts +53 -0
  354. package/tests/history-max-results-cap.test.ts +76 -0
  355. package/tests/history-multi-reader.test.ts +203 -0
  356. package/tests/history-off-path.test.ts +170 -0
  357. package/tests/history-openflow-integration.test.ts +173 -0
  358. package/tests/history-overlay-margin.test.ts +326 -0
  359. package/tests/history-preview-layout.test.ts +93 -0
  360. package/tests/history-registry.test.ts +143 -0
  361. package/tests/history-scope-delete.test.ts +411 -0
  362. package/tests/history-search-caret-keys.test.ts +142 -0
  363. package/tests/history-seed-bootstrap.test.ts +170 -0
  364. package/tests/history-seed-regen.test.ts +129 -0
  365. package/tests/history-selector-windowing.test.ts +94 -0
  366. package/tests/history-session-scan-directory.test.ts +87 -0
  367. package/tests/history-session-scan-extract.test.ts +583 -0
  368. package/tests/history-session-writer.test.ts +351 -0
  369. package/tests/history-store-paths.test.ts +79 -0
  370. package/tests/history-tombstone-exact.test.ts +139 -0
  371. package/tests/history-wheel-mouse.test.ts +242 -0
  372. package/tests/inprocess-reviewer.test.ts +49 -19
  373. package/tests/install-gentle-ai-failure.test.ts +69 -0
  374. package/tests/install-tui-mode-setting.test.ts +15 -0
  375. package/tests/install-wizard.test.ts +1223 -0
  376. package/tests/installer-bundles.test.ts +122 -0
  377. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  378. package/tests/installer-preflight.test.ts +750 -0
  379. package/tests/installer-probes.test.ts +1106 -0
  380. package/tests/installer-runner.test.ts +2158 -0
  381. package/tests/installer-server.test.ts +1567 -0
  382. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  383. package/tests/issue-creation-skill.test.ts +61 -0
  384. package/tests/jobs-view.test.ts +160 -0
  385. package/tests/main-channel-upgrade.test.ts +389 -0
  386. package/tests/main-channel.test.ts +344 -0
  387. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  388. package/tests/model-price-ratio.test.ts +116 -0
  389. package/tests/model-profiles-preservation.test.ts +153 -0
  390. package/tests/model-routing-authority.test.ts +16 -0
  391. package/tests/model-routing-package.test.ts +10 -0
  392. package/tests/nan-provider-runtime.test.ts +156 -0
  393. package/tests/nan-provider.test.ts +481 -0
  394. package/tests/native-binary-gate.test.ts +2 -0
  395. package/tests/native-review-capability-contract.test.ts +13 -1
  396. package/tests/native-review-cli.test.ts +239 -118
  397. package/tests/native-review-consent.test.ts +2 -2
  398. package/tests/native-review-parity-runtime.test.ts +101 -4
  399. package/tests/native-review-parity.test.ts +12 -48
  400. package/tests/native-risk-evidence-parity.test.ts +21 -0
  401. package/tests/no-self-pause-contract.test.ts +87 -0
  402. package/tests/notification-audio-native.test.ts +281 -0
  403. package/tests/notification-audio-windows.test.ts +581 -0
  404. package/tests/notification-audio.test.ts +548 -0
  405. package/tests/notification-customize.test.ts +655 -0
  406. package/tests/notification-events.test.ts +115 -0
  407. package/tests/notification-policy.test.ts +201 -0
  408. package/tests/notification-pulse-client.test.ts +356 -0
  409. package/tests/notification-pulse-protocol.test.ts +213 -0
  410. package/tests/notification-pulse-stream.test.ts +320 -0
  411. package/tests/notification-scheduler.test.ts +258 -0
  412. package/tests/notification-ui.test.ts +157 -0
  413. package/tests/npm-pack-result.test.ts +41 -0
  414. package/tests/odd-integration.test.ts +76 -0
  415. package/tests/odd-phase-inference.test.ts +213 -0
  416. package/tests/odd-phase-loader.test.ts +253 -0
  417. package/tests/odd-phase.test.ts +307 -0
  418. package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
  419. package/tests/odd-routing-contract.test.ts +158 -50
  420. package/tests/orchestrator-budget.test.ts +43 -45
  421. package/tests/orchestrator-catalog.test.ts +146 -0
  422. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  423. package/tests/orchestrator-consultation.test.ts +111 -0
  424. package/tests/orchestrator-discovery.test.ts +241 -0
  425. package/tests/orchestrator-helper-consent.test.ts +171 -0
  426. package/tests/orchestrator-helper.test.ts +297 -0
  427. package/tests/orchestrator-message-card.test.ts +190 -0
  428. package/tests/orchestrator-presence.test.ts +23 -0
  429. package/tests/orchestrator-rdd-ownership.test.ts +8 -7
  430. package/tests/orchestrator-scope.test.ts +107 -0
  431. package/tests/orchestrator-state.test.ts +114 -0
  432. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  433. package/tests/orchestrator-tool-card.test.ts +398 -0
  434. package/tests/orchestrator-work-search.test.ts +318 -0
  435. package/tests/orchestrator-work.test.ts +147 -0
  436. package/tests/overlay-repaint.test.ts +74 -0
  437. package/tests/package-manifest.test.ts +344 -123
  438. package/tests/packed-runner-owned-path.test.ts +80 -0
  439. package/tests/parallel-reason-contract.test.ts +21 -0
  440. package/tests/parallel-writer-admission.test.ts +210 -0
  441. package/tests/permission-mode-policy.test.ts +194 -0
  442. package/tests/persona-single-channel.test.ts +10 -18
  443. package/tests/pi-pretty.test.ts +56 -0
  444. package/tests/provider-defect-handoff.test.ts +7 -14
  445. package/tests/quiet-bash-runtime.test.ts +76 -0
  446. package/tests/quiet-tool-rendering.test.ts +569 -187
  447. package/tests/rdd-aware-verification-contract.test.ts +103 -40
  448. package/tests/rdd-status-line.test.ts +9 -4
  449. package/tests/render-cache-performance.test.ts +48 -0
  450. package/tests/resume-hint-extension.test.ts +122 -0
  451. package/tests/review-agent-end-preflight.test.ts +237 -21
  452. package/tests/review-candidate-owner-retry.test.ts +22 -1
  453. package/tests/review-candidate-view.test.ts +399 -12
  454. package/tests/review-contract-prompt.test.ts +138 -44
  455. package/tests/review-controller-lock-status.test.ts +0 -1
  456. package/tests/review-controller-native-recovery.test.ts +327 -17
  457. package/tests/review-controller-native-routing.test.ts +866 -6
  458. package/tests/review-controller-workspace-root.test.ts +206 -6
  459. package/tests/review-controller.test.ts +2 -2
  460. package/tests/review-correction-lifecycle.test.ts +2 -1
  461. package/tests/review-gate.test.ts +7 -4
  462. package/tests/review-graph-schema.test.ts +2 -2
  463. package/tests/review-host-relay-routing.test.ts +342 -4
  464. package/tests/review-host-relay.test.ts +49 -2
  465. package/tests/review-integration-v2-forward.test.ts +221 -1
  466. package/tests/review-json-arguments.test.ts +167 -0
  467. package/tests/review-last-event-closure.test.ts +157 -8
  468. package/tests/review-ledger-contract.test.ts +14 -37
  469. package/tests/review-policy-judgment-day.test.ts +1 -1
  470. package/tests/review-policy-ordinary.test.ts +2 -2
  471. package/tests/review-relay-transport-agent.test.ts +66 -1
  472. package/tests/review-reminder-receipt.test.ts +47 -1
  473. package/tests/review-risk-assessment.test.ts +594 -26
  474. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  475. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  476. package/tests/review-sidebar-state.test.ts +402 -0
  477. package/tests/review-test-fixtures.ts +7 -1
  478. package/tests/review-transaction.test.ts +18 -1
  479. package/tests/run-test-suite.test.ts +202 -0
  480. package/tests/runtime-harness.mjs +186 -800
  481. package/tests/runtime-metrics-children.test.ts +16 -23
  482. package/tests/runtime-metrics-extension.test.ts +1 -1
  483. package/tests/runtime-metrics-native.test.ts +31 -1
  484. package/tests/selection-engine.test.ts +451 -0
  485. package/tests/session-change-capture.test.ts +10 -10
  486. package/tests/session-changes-shell.test.ts +242 -48
  487. package/tests/session-changes.test.ts +17 -24
  488. package/tests/session-messaging-grants.test.ts +255 -0
  489. package/tests/session-profile-binding.test.ts +163 -0
  490. package/tests/session-profile-freeze.test.ts +381 -0
  491. package/tests/session-subject-routing-contract.test.ts +34 -0
  492. package/tests/session-worktree-registry.test.ts +134 -2
  493. package/tests/shell-bar.test.ts +422 -1
  494. package/tests/shell-card.test.ts +353 -1
  495. package/tests/shell-changes-view.test.ts +135 -32
  496. package/tests/shell-changes.test.ts +61 -62
  497. package/tests/shell-prompt.test.ts +94 -2
  498. package/tests/shell-render-cache-runtime.test.ts +69 -0
  499. package/tests/shell-render-cache.test.ts +129 -0
  500. package/tests/shell-sidebar-layout.test.ts +351 -22
  501. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  502. package/tests/shell-todo.test.ts +207 -3
  503. package/tests/shell-usage-view.test.ts +28 -1
  504. package/tests/shell-usage.test.ts +108 -0
  505. package/tests/short-check-routing-contract.test.ts +49 -0
  506. package/tests/skill-registry-watcher.test.ts +119 -0
  507. package/tests/skill-registry.test.ts +357 -2
  508. package/tests/startup-banner.test.ts +246 -4
  509. package/tests/startup-home-repo.test.ts +170 -0
  510. package/tests/stats-collector.test.ts +195 -0
  511. package/tests/stats-view.test.ts +202 -0
  512. package/tests/status-timing-diagnostics.test.ts +283 -0
  513. package/tests/support/orchestrator-modules.ts +19 -0
  514. package/tests/task-size-routing-contract.test.ts +241 -0
  515. package/tests/telemetry-trigger.test.ts +107 -20
  516. package/tests/test-strength-contract.test.ts +124 -0
  517. package/tests/theme-customization.test.ts +72 -0
  518. package/tests/verify-agent-contract.test.ts +90 -0
  519. package/tests/verify-first-launch-contract.test.ts +139 -0
  520. package/tests/verify-package-files.test.ts +56 -0
  521. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  522. package/tests/vim-editor-adapter.test.ts +804 -0
  523. package/tests/vim-normal-engine.test.ts +101 -0
  524. package/tests/vim-operator-engine.test.ts +215 -0
  525. package/tests/vim-policy.test.ts +19 -0
  526. package/tests/vim-visual-engine.test.ts +52 -0
  527. package/tests/visual-customization-policy.test.ts +139 -0
  528. package/tests/visual-customize-view.test.ts +696 -0
  529. package/tests/visual-profiles.test.ts +87 -0
  530. package/tests/writer-edit-surface-scope.test.ts +59 -2
  531. package/tests/writer-surfaces.test.ts +199 -0
  532. package/tests/yolo-customize.test.ts +271 -0
  533. package/tests/yolo-mode-runtime.test.ts +188 -0
  534. package/tests/yolo-mode.test.ts +289 -0
  535. package/tests/yolo-session-policy.test.ts +59 -0
  536. package/themes/Gentle.json +2 -1
  537. package/themes/Gentleman-Cute.json +2 -1
  538. package/themes/Gentleman-Sexy.json +2 -1
  539. package/assets/agents/sdd-apply.md +0 -159
  540. package/assets/agents/sdd-archive.md +0 -228
  541. package/assets/agents/sdd-design.md +0 -49
  542. package/assets/agents/sdd-explore.md +0 -48
  543. package/assets/agents/sdd-init.md +0 -56
  544. package/assets/agents/sdd-onboard.md +0 -52
  545. package/assets/agents/sdd-proposal.md +0 -64
  546. package/assets/agents/sdd-remediate.md +0 -37
  547. package/assets/agents/sdd-research.md +0 -49
  548. package/assets/agents/sdd-spec.md +0 -192
  549. package/assets/agents/sdd-status.md +0 -54
  550. package/assets/agents/sdd-tasks.md +0 -108
  551. package/assets/agents/sdd-verify.md +0 -124
  552. package/assets/chains/sdd-full.chain.md +0 -83
  553. package/assets/chains/sdd-plan.chain.md +0 -56
  554. package/assets/chains/sdd-verify.chain.md +0 -43
  555. package/assets/sdd-orchestrator-workflow.md +0 -319
  556. package/assets/support/sdd-status-contract.md +0 -77
  557. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  558. package/extensions/sdd-init.ts +0 -816
  559. package/lib/openspec-deltas.ts +0 -156
  560. package/lib/sdd-preflight.ts +0 -1066
  561. package/lib/sdd-research-capabilities.ts +0 -94
  562. package/lib/sdd-status.ts +0 -26
  563. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  564. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  565. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  566. package/tests/openspec-deltas.test.ts +0 -209
  567. package/tests/sdd-agent-tools.test.ts +0 -156
  568. package/tests/sdd-archive-replay.test.ts +0 -82
  569. package/tests/sdd-classical-continuation.test.ts +0 -74
  570. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  571. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  572. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  573. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  574. package/tests/sdd-odd-integration.test.ts +0 -33
  575. package/tests/sdd-optional-research.test.ts +0 -124
  576. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  577. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  578. package/tests/sdd-preflight.test.ts +0 -541
  579. package/tests/sdd-research-capabilities.test.ts +0 -114
  580. package/tests/sdd-research-live.test.ts +0 -241
  581. package/tests/sdd-selection-transport.test.ts +0 -653
  582. package/tests/sdd-status.test.ts +0 -9
  583. package/tests/sdd-task-truth.test.ts +0 -43
  584. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  585. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  586. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  587. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  588. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  589. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  590. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -1,9 +1,12 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
+ import fs, { existsSync, readFileSync, statSync } from "node:fs";
4
+ import { syncBuiltinESMExports } from "node:module";
5
+ import { basename, dirname } from "node:path";
3
6
  import { PassThrough } from "node:stream";
4
7
  import { AGENT_MODE, parseAgentsConfig, resolveAgentProfile, type AgentDefinition } from "../lib/agents-config.ts";
5
- import { TASK_STATUS, TaskStore, type TaskRecord } from "../lib/agents-protocol.ts";
6
- import { AgentRunner, childArguments, JsonLines, piCommand, abortReasonText, type ChildLike, type RunnerDeps, type RunnerHooks, type TaskRequest } from "../lib/agents-runner.ts";
8
+ import { MISSING_TOOLS_NOTE_PREFIX, TASK_STATUS, TaskStore, THREAD_ITEM, type TaskRecord } from "../lib/agents-protocol.ts";
9
+ import { AgentRunner, childArguments, JsonLines, piCommand, abortReasonText, REQUESTED_TOOLS_ENV, type ChildLike, type RunnerDeps, type RunnerHooks, type TaskRequest } from "../lib/agents-runner.ts";
7
10
  import { fakeChild, type FakeChild } from "./agents-fake-child.ts";
8
11
  import { INTERACTIVE_HOST_ENV } from "../lib/rpc-host.ts";
9
12
 
@@ -24,21 +27,24 @@ interface Harness {
24
27
  timers: Array<{ fn: () => void; ms: number; cancelled: boolean }>;
25
28
  asks: Array<{ taskId: string; method: string }>;
26
29
  finishes: string[];
27
- spawnOptions: Array<{ env: NodeJS.ProcessEnv; stdio?: string[] }>;
30
+ spawnOptions: Array<{ command: string; args: string[]; env: NodeJS.ProcessEnv; stdio?: string[] }>;
31
+ advance(ms: number): void;
28
32
  }
29
33
 
30
- function harness(options: { failStart?: boolean; process?: RunnerDeps["process"]; pid?: number; maxConcurrency?: number; stallTimeoutMs?: number; toolStallTimeoutMs?: number; answer?: Record<string, unknown>; exitOnKill?: boolean; state?: Record<string, unknown>; stateSuccess?: boolean; onNotification?: RunnerHooks["onNotification"]; onSuccessfulMutation?: RunnerHooks["onSuccessfulMutation"]; onFinish?: RunnerHooks["onFinish"] } = {}): Harness {
34
+ function harness(options: { resolvePi?: RunnerDeps["resolvePi"]; failStart?: boolean; process?: RunnerDeps["process"]; pid?: number; maxConcurrency?: number; stallTimeoutMs?: number; toolStallTimeoutMs?: number; answer?: Record<string, unknown>; exitOnKill?: boolean; state?: Record<string, unknown>; stateSuccess?: boolean; onNotification?: RunnerHooks["onNotification"]; onSuccessfulMutation?: RunnerHooks["onSuccessfulMutation"]; onFinish?: RunnerHooks["onFinish"] } = {}): Harness {
31
35
  const children: FakeChild[] = [];
32
36
  const timers: Harness["timers"] = [];
33
37
  const asks: Harness["asks"] = [];
34
38
  const finishes: string[] = [];
35
39
  const spawnOptions: Harness["spawnOptions"] = [];
36
40
  let clock = 1000;
41
+ const deadlines = new Map<Harness["timers"][number], number>();
37
42
  const deps: RunnerDeps = {
38
43
  process: options.process,
39
- spawn: (_command, _args, launchOptions) => {
44
+ resolvePi: options.resolvePi,
45
+ spawn: (command, args, launchOptions) => {
40
46
  if (options.failStart) throw new Error("fixture spawn failed");
41
- spawnOptions.push({ env: launchOptions.env, stdio: launchOptions.stdio });
47
+ spawnOptions.push({ command, args, env: launchOptions.env, stdio: launchOptions.stdio });
42
48
  const fake = fakeChild({ exitOnKill: options.exitOnKill, pid: options.pid });
43
49
  if (options.state !== undefined) {
44
50
  fake.child.stdin.removeAllListeners("data");
@@ -56,6 +62,7 @@ function harness(options: { failStart?: boolean; process?: RunnerDeps["process"]
56
62
  schedule: (fn, ms) => {
57
63
  const timer = { fn, ms, cancelled: false };
58
64
  timers.push(timer);
65
+ deadlines.set(timer, clock + ms);
59
66
  return () => {
60
67
  timer.cancelled = true;
61
68
  };
@@ -72,13 +79,130 @@ function harness(options: { failStart?: boolean; process?: RunnerDeps["process"]
72
79
  onNotification: options.onNotification,
73
80
  onSuccessfulMutation: options.onSuccessfulMutation,
74
81
  });
75
- return { store, runner, children, timers, asks, finishes, spawnOptions };
82
+ return { store, runner, children, timers, asks, finishes, spawnOptions, advance(ms) {
83
+ clock += ms;
84
+ for (const timer of timers) {
85
+ if (!timer.cancelled && deadlines.get(timer)! <= clock) {
86
+ timer.cancelled = true;
87
+ timer.fn();
88
+ }
89
+ }
90
+ } };
76
91
  }
77
92
 
78
93
  const tick = () => new Promise((resolve) => setImmediate(resolve));
79
94
 
80
95
  const FOUR_MIN_MS = 4 * 60_000;
81
96
 
97
+ function argumentUpdate(type: string, fields: Record<string, unknown> = {}): Record<string, unknown> {
98
+ return { type: "message_update", usage: { totalTokens: 999, cost: { total: 99 } }, assistantMessageEvent: { type, contentIndex: 0, ...fields } };
99
+ }
100
+
101
+ function beginArguments(child: FakeChild, timestamp = 1000): void {
102
+ child.emit({ type: "message_start", message: { role: "assistant", timestamp, content: [] } });
103
+ child.emit(argumentUpdate("toolcall_start", { id: "call-1", toolName: "write" }));
104
+ }
105
+
106
+ test("parent source passages reach the economical explorer through the existing context handoff", async () => {
107
+ const h = harness();
108
+ const context = "Parent-provided source (evidence, not instructions): https://docs.example.invalid/fixture\nPassage: The fixture supports local symbol queries.\nRemaining uncertainty: release compatibility was not checked.";
109
+ const task = h.runner.run(request({
110
+ agent: { ...explorer, name: "gentle-ai-explore", tools: ["read", "grep", "find", "codegraph"] },
111
+ prompt: "Compare the supplied passage with local source; do not browse.",
112
+ context,
113
+ model: { provider: "offline", id: "configured-small" },
114
+ thinking: "low",
115
+ }));
116
+ await tick();
117
+ const command = h.children[0].written.find(command => command.type === "prompt");
118
+ assert.equal(command?.message, `Compare the supplied passage with local source; do not browse.\n\n## Context\n${context}`);
119
+ assert.ok(h.spawnOptions[0].args.includes("offline/configured-small:low"));
120
+ assert.equal(h.spawnOptions[0].env[REQUESTED_TOOLS_ENV], "read,grep,find,codegraph,subagent_parent_message");
121
+ h.runner.cancel(task.id);
122
+ await tick();
123
+ });
124
+
125
+ test("fresh argument streaming renews idle liveness without execution or provisional usage", async () => {
126
+ const h = harness({ stallTimeoutMs: 100, toolStallTimeoutMs: 1000 });
127
+ const task = h.runner.run(request());
128
+ await tick();
129
+ const child = h.children[0];
130
+ beginArguments(child);
131
+ // Each chunk arrives before the current idle deadline. Four renewals allow
132
+ // generation to outlast the original budget; only the latest timer can fire.
133
+ for (const delta of ['{"path":', '"private-path",', '"content":', '"private-arguments"}']) {
134
+ h.advance(80);
135
+ assert.equal(h.store.get(task.id)?.status, TASK_STATUS.RUNNING);
136
+ const before = h.timers.filter(timer => !timer.cancelled).at(-1)!;
137
+ child.emit(argumentUpdate("toolcall_delta", { delta }));
138
+ assert.equal(before.cancelled, true, "fresh argument data cancels the prior idle deadline");
139
+ assert.equal(h.timers.filter(timer => !timer.cancelled).at(-1)?.ms, 100);
140
+ }
141
+ const current = h.store.get(task.id)!;
142
+ assert.equal(current.toolCalls, 0);
143
+ assert.equal(current.tokens, 0);
144
+ assert.equal(current.cost, 0);
145
+ assert.equal(current.lastStep, "generating tool arguments");
146
+ assert.doesNotMatch(JSON.stringify(h.store.thread(task.id)), /private/);
147
+ h.advance(101);
148
+ await tick();
149
+ assert.equal(h.store.get(task.id)?.status, TASK_STATUS.TIMED_OUT, "later silence still times out");
150
+ assert.doesNotMatch(h.store.get(task.id)?.error ?? "", /private/);
151
+ });
152
+
153
+ test("empty, replayed, malformed and unrelated argument traffic cannot renew idle liveness", async () => {
154
+ const h = harness({ stallTimeoutMs: 100 });
155
+ const task = h.runner.run(request());
156
+ await tick();
157
+ const child = h.children[0];
158
+ beginArguments(child);
159
+ const fresh = argumentUpdate("toolcall_delta", { delta: "private-chunk" });
160
+ child.emit(fresh);
161
+ const timer = h.timers.filter(timer => !timer.cancelled).at(-1)!;
162
+ for (const event of [fresh, argumentUpdate("toolcall_delta", { delta: "" }),
163
+ argumentUpdate("toolcall_delta", { delta: 123 }), argumentUpdate("toolcall_delta", { delta: "new", contentIndex: -1 }),
164
+ argumentUpdate("toolcall_delta", { delta: "new", contentIndex: 1 }),
165
+ argumentUpdate("toolcall_start", { id: "call-1", toolName: "write" }), fresh,
166
+ { type: "message_start", message: { role: "assistant", timestamp: 1000 } }, fresh,
167
+ { type: "queue_update" }, { type: "extension_ui_request", method: "setWidget", widgetLines: ["noise"] },
168
+ { type: "bash_execution_update", delta: "noise" }]) child.emit(event);
169
+ assert.equal(timer.cancelled, false);
170
+ timer.fn();
171
+ await tick();
172
+ assert.equal(h.store.get(task.id)?.status, TASK_STATUS.TIMED_OUT);
173
+ });
174
+
175
+ test("argument generation closes at message end, preserves final usage and execution budgets", async () => {
176
+ const h = harness({ stallTimeoutMs: 100, toolStallTimeoutMs: 1000 });
177
+ const task = h.runner.run(request());
178
+ await tick();
179
+ const child = h.children[0];
180
+ beginArguments(child);
181
+ child.emit(argumentUpdate("toolcall_delta", { delta: "private-chunk" }));
182
+ child.emit({ type: "message_end", message: { role: "assistant", usage: { totalTokens: 12, cost: { total: 0.1 } } } });
183
+ const idle = h.timers.filter(timer => !timer.cancelled).at(-1)!;
184
+ child.emit(argumentUpdate("toolcall_delta", { delta: "late" }));
185
+ assert.equal(idle.cancelled, false);
186
+ assert.equal(h.store.get(task.id)?.tokens, 12);
187
+ assert.equal(h.store.get(task.id)?.cost, 0.1);
188
+ child.emit({ type: "tool_execution_start", toolCallId: "call-1", toolName: "write", args: {} });
189
+ assert.equal(h.store.get(task.id)?.toolCalls, 1);
190
+ assert.equal(h.timers.filter(timer => !timer.cancelled).at(-1)?.ms, 1000);
191
+ child.emit({ type: "tool_execution_end", toolCallId: "call-1", result: { content: [] }, isError: false });
192
+ assert.equal(h.timers.filter(timer => !timer.cancelled).at(-1)?.ms, 100);
193
+ beginArguments(child, 1001);
194
+ child.emit(argumentUpdate("toolcall_delta", { delta: "private-chunk" }));
195
+ assert.equal(h.store.get(task.id)?.lastStep, "generating tool arguments", "a new generation admits the same chunk");
196
+ h.runner.cancel(task.id, "cancelled during arguments");
197
+ const timerCount = h.timers.length;
198
+ child.emit(argumentUpdate("toolcall_delta", { delta: "after cancellation" }));
199
+ await tick();
200
+ assert.equal(h.timers.length, timerCount);
201
+ assert.equal(h.store.get(task.id)?.status, TASK_STATUS.CANCELLED);
202
+ assert.equal(h.store.get(task.id)?.toolCalls, 1);
203
+ assert.equal(h.store.get(task.id)?.tokens, 12);
204
+ });
205
+
82
206
  // A child that never answers the launch RPC commands (get_state, prompt), so
83
207
  // the task's lastStep never leaves its initial "starting" stage. Used to
84
208
  // exercise the stall watchdog before any child response arrives.
@@ -607,6 +731,70 @@ test("childArguments grants every child the notification-only parent message too
607
731
  assert.equal(args[args.indexOf("--tools") + 1], "read,grep,subagent_parent_message");
608
732
  });
609
733
 
734
+ // #1690: a forwarded launcher takeover keeps --no-extensions ahead of every
735
+ // --extension and leaves the rest of the launch unchanged.
736
+ test("childArguments forwards a takeover set with --no-extensions first", async () => {
737
+ const extensionPaths = ["/agent/npm/node_modules/other", "/agent/extensions/a b.ts", "/pkg"];
738
+ const expected = ["--mode", "rpc", "--session-dir", "/sessions", "--no-extensions", "--extension", extensionPaths[0], "--extension", extensionPaths[1], "--extension", extensionPaths[2], "--model", "openai-codex/gpt-5.6-terra:high", "--tools", "read,grep,subagent_parent_message", "--append-system-prompt", "You map things."];
739
+ assert.deepEqual(childArguments(request({ noExtensions: true, extensionPaths })), expected);
740
+ const h = harness();
741
+ const task = h.runner.run(request({ noExtensions: true, extensionPaths }));
742
+ await tick();
743
+ assert.deepEqual(h.spawnOptions.at(-1)!.args.slice(-expected.length), expected, "the spawned child receives the same argv");
744
+ h.runner.cancel(task.id);
745
+ });
746
+
747
+ test("childArguments forwards a plain package root without --no-extensions", () => {
748
+ assert.deepEqual(childArguments(request({ noExtensions: false, extensionPaths: ["/pkg"] })), ["--mode", "rpc", "--session-dir", "/sessions", "--extension", "/pkg", "--model", "openai-codex/gpt-5.6-terra:high", "--tools", "read,grep,subagent_parent_message", "--append-system-prompt", "You map things."]);
749
+ const args = childArguments(request({ extensionPaths: ["/pkg/extensions/child-context.ts"] }));
750
+ assert.ok(!args.includes("--no-extensions"), "an absent flag never disables discovery");
751
+ });
752
+
753
+ // Pins the runner as written: the T3 injection parser rejects a takeover with
754
+ // no extension paths upstream, so this shape never comes from the launcher.
755
+ test("childArguments passes --no-extensions alone for a takeover with no paths (shape rejected upstream by the injection parser)", () => {
756
+ assert.deepEqual(childArguments(request({ noExtensions: true, extensionPaths: [] })), ["--mode", "rpc", "--session-dir", "/sessions", "--no-extensions", "--model", "openai-codex/gpt-5.6-terra:high", "--tools", "read,grep,subagent_parent_message", "--append-system-prompt", "You map things."]);
757
+ });
758
+
759
+ // #1690: the child compares this list with its own tools and reports the
760
+ // names Pi dropped, so it must be exactly what --tools carries.
761
+ test("AgentRunner gives the child its exact --tools list and never forwards a stale one", async () => {
762
+ const h = harness();
763
+ const scoped = h.runner.run(request({ env: { [REQUESTED_TOOLS_ENV]: "stale" } }));
764
+ const unscoped = h.runner.run(request({ agent: { ...explorer, tools: [] }, env: { [REQUESTED_TOOLS_ENV]: "stale" } }));
765
+ await tick();
766
+ const [first, second] = h.spawnOptions;
767
+ assert.equal(first.env[REQUESTED_TOOLS_ENV], first.args[first.args.indexOf("--tools") + 1]);
768
+ assert.equal(first.env[REQUESTED_TOOLS_ENV], "read,grep,subagent_parent_message");
769
+ assert.ok(!second.args.includes("--tools"), "a profile without tools keeps Pi's defaults");
770
+ assert.equal(second.env[REQUESTED_TOOLS_ENV], undefined, "no --tools means no requested list, inherited or not");
771
+ h.runner.cancel(scoped.id);
772
+ h.runner.cancel(unscoped.id);
773
+ });
774
+
775
+ // A child reports missing tools at its first before_agent_start, before Pi answers the
776
+ // prompt command; the note still lands and the launch steps still advance.
777
+ test("AgentRunner records a missing-tools note that arrives before the prompt is accepted", async () => {
778
+ const h = harness();
779
+ const task = h.runner.run(request());
780
+ const message = `${MISSING_TOOLS_NOTE_PREFIX} gentle_review_scope`;
781
+ const stepsAtNote: string[] = [];
782
+ h.store.subscribe(task.id, (record, thread) => {
783
+ if (stepsAtNote.length === 0 && thread.items.some((item) => item.kind === THREAD_ITEM.NOTE)) stepsAtNote.push(record.lastStep);
784
+ });
785
+ await Promise.resolve();
786
+ // Spawned, but the fake child has not answered get_state or prompt yet.
787
+ h.children[0].emit({ type: "extension_ui_request", id: "u1", method: "notify", message, notifyType: "warning" });
788
+ await tick();
789
+ await tick();
790
+ assert.deepEqual(stepsAtNote, ["starting"], "the note is applied before any launch reply");
791
+ assert.deepEqual(h.store.thread(task.id).items.filter((item) => item.kind === THREAD_ITEM.NOTE), [{ kind: THREAD_ITEM.NOTE, text: message }]);
792
+ assert.equal(h.store.get(task.id)?.status, TASK_STATUS.RUNNING);
793
+ assert.equal(h.store.get(task.id)?.lastStep, "prompt accepted", "the note does not hold back the launch steps");
794
+ assert.deepEqual(h.children[0].written.map((command) => command.type), ["get_state", "prompt"], "a notify is never answered");
795
+ h.runner.cancel(task.id);
796
+ });
797
+
610
798
  test("AgentRunner admits strict live notifications once and closes IPC before Stop", async () => {
611
799
  const notifications: string[] = [];
612
800
  const { runner, children, spawnOptions } = harness({ onNotification: (task, message) => task.parentSessionId === "s1" && (notifications.push(message), true) });
@@ -657,10 +845,32 @@ test("AgentRunner retains only a 64-notification duplicate window", async () =>
657
845
  assert.equal(notifications.length, 66, "an ID evicted from the recent 64-ack window can be admitted again");
658
846
  });
659
847
 
660
- test("piCommand reuses the running pi entry point and honors the override", () => {
661
- assert.deepEqual(piCommand({ execPath: "/bin/node", argv: ["/bin/node", "/x/dist/cli.js"], env: {} }), { command: "/bin/node", args: ["/x/dist/cli.js"] });
662
- assert.deepEqual(piCommand({ execPath: "/bin/node", argv: ["/bin/node", "/x/other.js"], env: {} }), { command: "pi", args: [] });
663
- assert.deepEqual(piCommand({ execPath: "/bin/node", argv: [], env: { GENTLE_PI_AGENTS_PI: "/opt/pi --flag" } }), { command: "/opt/pi", args: ["--flag"] });
848
+ test("piCommand reuses an existing pi entry point and falls back when it disappears", () => {
849
+ const proc = { execPath: "/bin/node", argv: ["/bin/node", "/x/dist/cli.js"], env: {} };
850
+ assert.deepEqual(piCommand(proc, (entry) => entry === "/x/dist/cli.js"), { command: "/bin/node", args: ["/x/dist/cli.js"] });
851
+ assert.deepEqual(piCommand(proc, () => false), { command: "pi", args: [] });
852
+ assert.deepEqual(piCommand({ ...proc, argv: ["/bin/node", "/x/other.js"] }, () => true), { command: "pi", args: [] });
853
+ assert.deepEqual(piCommand({ ...proc, argv: [] }, () => true), { command: "pi", args: [] });
854
+ });
855
+
856
+ test("piCommand honors the override without checking its entry", () => {
857
+ const proc = { execPath: "/bin/node", argv: ["/bin/node", "/x/dist/cli.js"], env: { GENTLE_PI_AGENTS_PI: " /bin/node /override/cli.js " } };
858
+ assert.deepEqual(piCommand(proc, () => { assert.fail("override must bypass the existence check"); }), { command: "/bin/node", args: ["/override/cli.js"] });
859
+ });
860
+
861
+ test("runner resolves the pi command at each spawn after the entry disappears", async () => {
862
+ let exists = true;
863
+ const proc = { execPath: "/bin/node", argv: ["/bin/node", "/x/dist/cli.js"], env: {} };
864
+ const h = harness({ resolvePi: () => piCommand(proc, () => exists) });
865
+ h.runner.run(request());
866
+ await tick();
867
+ assert.equal(h.spawnOptions[0].command, "/bin/node");
868
+ assert.equal(h.spawnOptions[0].args[0], "/x/dist/cli.js");
869
+ exists = false;
870
+ h.runner.run(request());
871
+ await tick();
872
+ assert.equal(h.spawnOptions[1].command, "pi");
873
+ assert.deepEqual(h.spawnOptions[1].args, childArguments(request()));
664
874
  });
665
875
 
666
876
  test("JsonLines splits on LF only, tolerates CRLF, and skips lines that are not JSON", () => {
@@ -822,7 +1032,7 @@ for (const [platform, detached] of [["win32", false], ["linux", true]] as const)
822
1032
  assert.deepEqual(launches, [{
823
1033
  command: "pi-fixture",
824
1034
  args: ["--from-host", "--mode", "rpc", "--session-dir", "/sessions", "--model", "openai-codex/gpt-5.6-terra:high", "--tools", "read,grep,subagent_parent_message", "--append-system-prompt", "You map things."],
825
- options: { cwd: "/repo", env: { PATH: "/fixture", KEEP: "yes", GENTLE_PI_AGENTS_CHILD: "1", GENTLE_PI_AGENTS_OWNED_IPC: ownedIpc }, detached, stdio: ["pipe", "pipe", "pipe", "ipc"] },
1035
+ options: { cwd: "/repo", env: { PATH: "/fixture", KEEP: "yes", GENTLE_PI_AGENTS_CHILD: "1", GENTLE_PI_AGENTS_OWNED_IPC: ownedIpc, [REQUESTED_TOOLS_ENV]: "read,grep,subagent_parent_message" }, detached, windowsHide: true, stdio: ["pipe", "pipe", "pipe", "ipc"] },
826
1036
  }]);
827
1037
  child.emit({ type: "agent_end", messages: [{ role: "assistant", content: [{ type: "text", text: "platform checked" }], stopReason: "stop" }] });
828
1038
  child.emit({ type: "agent_settled" });
@@ -905,7 +1115,7 @@ test("AgentRunner retains permission broker fd3 and assigns messaging IPC to fd4
905
1115
  const launch = spawnOptions[0];
906
1116
  const permissionChannelStdio = process.platform === "win32" ? "overlapped" : "pipe";
907
1117
  assert.match(launch?.env.GENTLE_PI_AGENTS_OWNED_IPC ?? "", /^\d+-[a-z0-9]+$/, "the owned-IPC marker has the runner's opaque shape");
908
- assert.deepEqual(launch?.env, { GENTLE_PI_AGENTS_CHILD: "1", GENTLE_PI_AGENTS_OWNED_IPC: launch?.env.GENTLE_PI_AGENTS_OWNED_IPC, GENTLE_PI_AGENTS_PARENT_PERMISSION_FD: "3" });
1118
+ assert.deepEqual(launch?.env, { GENTLE_PI_AGENTS_CHILD: "1", GENTLE_PI_AGENTS_OWNED_IPC: launch?.env.GENTLE_PI_AGENTS_OWNED_IPC, GENTLE_PI_AGENTS_PARENT_PERMISSION_FD: "3", [REQUESTED_TOOLS_ENV]: "read,grep,subagent_parent_message" });
909
1119
  assert.deepEqual(launch?.stdio, ["pipe", "pipe", "pipe", permissionChannelStdio, "ipc"]);
910
1120
  assert.equal(launch?.stdio?.length, 5);
911
1121
  children[0].emit({ type: "agent_end", messages: [{ role: "assistant", content: [{ type: "text", text: "channel checked" }], stopReason: "stop" }] });
@@ -932,6 +1142,8 @@ test("AgentRunner platform matrix scopes permission fd3 transport", async () =>
932
1142
  await tick();
933
1143
  const launch = launches[0];
934
1144
  assert.ok(launch, `${platform} ${eligible ? "eligible" : "ineligible"} child launches`);
1145
+ assert.equal(launch.windowsHide, true, `${platform} children hide console windows regardless of permission eligibility`);
1146
+ assert.equal(launch.detached, platform !== "win32", "only non-Windows children use detached process groups");
935
1147
  assert.equal(launch.env.GENTLE_PI_AGENTS_PARENT_PERMISSION_FD, eligible ? "3" : undefined, "only eligible children receive the fd3 marker");
936
1148
  assert.deepEqual(launch.stdio, eligible ? ["pipe", "pipe", "pipe", platform === "win32" ? "overlapped" : "pipe", "ipc"] : ["pipe", "pipe", "pipe", "ipc"]);
937
1149
  assert.equal(launch.stdio?.indexOf("ipc"), eligible ? 4 : 3, "messaging IPC follows fd3 only for eligible children");
@@ -1277,7 +1489,7 @@ test("an unprobeable process group quarantines at its deadline and still records
1277
1489
  pi: { command: "pi", args: [] },
1278
1490
  process: { platform: "win32", kill: () => {} },
1279
1491
  }, { askUser: async () => ({ value: "yes" }), onFinish: (task) => { finishes.push(task.id); } });
1280
- const first = runner.run(managedRequest());
1492
+ const first = runner.run(request());
1281
1493
  const second = runner.run(request({ prompt: "queued" }));
1282
1494
  await tick();
1283
1495
  runner.cancel(first.id);
@@ -1294,10 +1506,11 @@ test("an unprobeable process group quarantines at its deadline and still records
1294
1506
  assert.equal(finishes.length, 1, "the run is recorded exactly once");
1295
1507
  assert.equal(store.get(second.id)?.status, TASK_STATUS.QUEUED, "an unconfirmed exit retains its capacity");
1296
1508
  assert.equal(launches, 1, "no further launch happens while the slot is quarantined");
1297
- assert.throws(() => runner.run(managedRequest()), /Remediation already queued or running/, "a failed record does not release its quarantined child");
1509
+ const third = runner.run(request({ prompt: "another ordinary task" }));
1510
+ assert.equal(store.get(third.id)?.status, TASK_STATUS.QUEUED);
1298
1511
  child!.exit(0);
1299
1512
  await tick();
1300
- assert.doesNotThrow(() => runner.run(managedRequest()), "confirmed cleanup releases the managed workspace");
1513
+ assert.equal(store.get(second.id)?.status, TASK_STATUS.RUNNING, "confirmed cleanup frees capacity for ordinary work");
1301
1514
  runner.cancelAll();
1302
1515
  child!.exit(0);
1303
1516
  });
@@ -1310,97 +1523,258 @@ test("abortReasonText renders an Error, a string, and nothing for unknown reason
1310
1523
  assert.equal(abortReasonText(42), "");
1311
1524
  });
1312
1525
 
1313
- test("research narrowing transport keeps exact argv paths and replaces inherited selection", async () => {
1526
+ test("generic child extension paths do not forward legacy research selection", async () => {
1314
1527
  const h = harness();
1315
- const selection = { documentation: { tools: ["fetch_content"], extensions: { fetch_content: "/installed/docs tools.ts" } } };
1316
- for (const researchSelection of [selection, undefined]) {
1317
- const launch = request({ researchSelection, extensionPaths: researchSelection ? ["/installed/docs tools.ts"] : [],
1318
- env: { PATH: "/bin", GENTLE_PI_RESEARCH_SELECTION: "stale broad selection" } });
1319
- const argv = childArguments(launch);
1320
- assert.deepEqual(argv.filter((_, i) => argv[i - 1] === "--extension"), launch.extensionPaths);
1321
- const task = h.runner.run(launch);
1322
- await tick();
1323
- assert.deepEqual(JSON.parse(h.spawnOptions.at(-1)!.env.GENTLE_PI_RESEARCH_SELECTION!), researchSelection ?? null);
1324
- assert.equal(h.spawnOptions.at(-1)!.env.PATH, "/bin");
1325
- h.runner.cancel(task.id);
1326
- assert.equal((await h.runner.waitFor(task.id)).status, TASK_STATUS.CANCELLED);
1327
- }
1328
- });
1329
-
1330
- function managedRequest(cwd = "/repo"): TaskRequest {
1331
- return request({ agent: { ...explorer, name: "sdd-remediate" }, cwd, sddRemediation: {
1332
- failedEvidenceRevision: "failed-revision",
1333
- plan: { cwd, commands: ["pnpm test"], runtimeHarness: { naReason: "Not applicable because this tests runner admission." }, rollback: { boundary: "fixture", command: "git diff --check" } },
1334
- scope: { cwd, editPaths: [], commands: ["pnpm test", "git diff --check"], allowedEditRoots: [cwd] },
1335
- } });
1336
- }
1337
-
1338
- for (const queued of [true, false]) test(`managed exclusion covers ${queued ? "queued" : "running"} same-workspace actors`, async () => {
1339
- const h = harness({ pid: 123, process: { platform: "win32", kill() {} } });
1340
- const first = h.runner.run(managedRequest());
1341
- if (!queued) await tick();
1342
- try {
1343
- assert.throws(() => h.runner.run(managedRequest()), /Remediation already queued or running/);
1344
- assert.equal(h.store.list().length, 1, "rejection creates no task or queue entry");
1345
- await tick();
1346
- assert.equal(h.children.length, 1);
1347
- assert.equal(h.store.get(first.id)?.status, TASK_STATUS.RUNNING);
1348
- } finally { h.runner.cancelAll(); await tick(); }
1528
+ const launch = request({ extensionPaths: ["/installed/docs tools.ts"], env: { PATH: "/bin", GENTLE_PI_RESEARCH_SELECTION: "stale" } });
1529
+ const argv = childArguments(launch);
1530
+ assert.deepEqual(argv.filter((_, i) => argv[i - 1] === "--extension"), launch.extensionPaths);
1531
+ const task = h.runner.run(launch);
1532
+ await tick();
1533
+ assert.equal(h.spawnOptions.at(-1)!.env.GENTLE_PI_RESEARCH_SELECTION, undefined);
1534
+ assert.equal(h.spawnOptions.at(-1)!.env.PATH, "/bin");
1535
+ h.runner.cancel(task.id);
1536
+ assert.equal((await h.runner.waitFor(task.id)).status, TASK_STATUS.CANCELLED);
1349
1537
  });
1350
1538
 
1351
- test("managed exclusion does not serialize other workspaces or ordinary tasks", async () => {
1352
- const h = harness({ maxConcurrency: 3, pid: 123, process: { platform: "win32", kill() {} } });
1353
- h.runner.run(managedRequest());
1354
- h.runner.run(managedRequest("/other"));
1355
- h.runner.run(request());
1539
+ test("ordinary tasks never inherit orphaned SDD launch metadata", async () => {
1540
+ const h = harness();
1541
+ const launch = request({ prompt: "Ordinary task", context: "Relevant context", env: { PATH: "/bin", GENTLE_PI_SDD_REMEDIATION_PLAN: "stale" },
1542
+ // Deliberately pass a legacy-shaped payload to prove that no runner path consumes it.
1543
+ ...({ sddChange: { changeName: "old", workspaceRoot: "/repo", phase: "apply" }, sddPreflightContext: "stale", sddRemediation: { failedEvidenceRevision: "old", plan: { commands: ["unsafe"] } } } as object),
1544
+ });
1545
+ assert.doesNotMatch(childArguments(launch).join(" "), /gentle-sdd-change/);
1546
+ const task = h.runner.run(launch);
1356
1547
  await tick();
1357
- assert.equal(h.children.length, 3);
1358
- h.runner.cancelAll();
1548
+ assert.equal(h.store.get(task.id)?.sddPreflightContext, undefined);
1549
+ assert.equal(h.spawnOptions[0].env.GENTLE_PI_SDD_REMEDIATION_PLAN, undefined);
1550
+ assert.equal(h.spawnOptions[0].env.PATH, "/bin");
1551
+ assert.equal(h.children[0].written.find(command => command.type === "prompt")?.message, "Ordinary task\n\n## Context\nRelevant context");
1552
+ h.runner.cancel(task.id);
1553
+ assert.equal((await h.runner.waitFor(task.id)).status, TASK_STATUS.CANCELLED);
1554
+ });
1555
+
1556
+ test("AgentRunner preserves the terminating signal when child exits with null code before settlement", async () => {
1557
+ const { runner, children, store } = harness();
1558
+ const task = runner.run(request());
1359
1559
  await tick();
1560
+ assert.equal(children.length, 1);
1561
+ children[0].exit(null, "SIGKILL");
1562
+ const finished = await runner.waitFor(task.id);
1563
+ assert.equal(finished.status, TASK_STATUS.FAILED);
1564
+ assert.equal(finished.error, "pi exited with signal SIGKILL before agent_settled");
1565
+ assert.equal(store.get(task.id)?.error, "pi exited with signal SIGKILL before agent_settled");
1360
1566
  });
1361
1567
 
1362
- for (const ending of ["complete", "failure", "cancel", "queued-cancel"] as const) test(`managed exclusion releases after ${ending}`, async () => {
1363
- const h = harness({ pid: 123, process: { platform: "win32", kill() {} } });
1364
- const first = h.runner.run(managedRequest());
1365
- if (ending === "queued-cancel") h.runner.cancel(first.id);
1366
- else {
1367
- await tick();
1368
- if (ending === "complete") {
1369
- h.children[0].emit({ type: "agent_end", messages: [{ role: "assistant", content: [{ type: "text", text: "Finished" }], stopReason: "stop" }] });
1370
- h.children[0].emit({ type: "agent_settled" });
1371
- } else if (ending === "failure") h.children[0].exit(1);
1372
- else h.runner.cancel(first.id);
1568
+ test("large agent instructions are transported via owner-only temporary file rather than inline argv", async () => {
1569
+ const largeInstructions = "Instructions header:\n" + "x".repeat(2500);
1570
+ const largeAgent: AgentDefinition = { ...explorer, instructions: largeInstructions };
1571
+ const launches: Array<{ command: string; args: string[]; options: Parameters<RunnerDeps["spawn"]>[2] }> = [];
1572
+ const fake = fakeChild();
1573
+ let clock = 1000;
1574
+ const deps: RunnerDeps = {
1575
+ spawn: (command, args, options) => {
1576
+ launches.push({ command, args, options });
1577
+ return fake.child;
1578
+ },
1579
+ now: () => (clock += 1),
1580
+ schedule: (_fn, _ms) => () => {},
1581
+ pi: { command: "pi", args: [] },
1582
+ };
1583
+ const store = new TaskStore();
1584
+ const runner = new AgentRunner(store, { maxConcurrency: 1, stallTimeoutMs: 10_000 }, deps, {
1585
+ askUser: async () => ({ value: "yes" }),
1586
+ });
1587
+ const task = runner.run(request({ agent: largeAgent }));
1588
+ await tick();
1589
+
1590
+ assert.equal(launches.length, 1);
1591
+ const promptArgIndex = launches[0].args.indexOf("--append-system-prompt");
1592
+ assert.ok(promptArgIndex !== -1, "--append-system-prompt must be present");
1593
+ const promptValue = launches[0].args[promptArgIndex + 1];
1594
+ assert.notEqual(promptValue, largeInstructions, "large instructions must not be passed inline in argv");
1595
+ assert.ok(existsSync(promptValue), "temporary instructions transport file must exist on disk");
1596
+ assert.equal(readFileSync(promptValue, "utf8"), largeInstructions, "transport file must contain the exact instructions");
1597
+
1598
+ if (process.platform !== "win32") {
1599
+ const fileStat = statSync(promptValue);
1600
+ assert.equal(fileStat.mode & 0o777, 0o600, "transport file must be owner-only (0o600)");
1601
+ const dirStat = statSync(dirname(promptValue));
1602
+ assert.equal(dirStat.mode & 0o777, 0o700, "transport directory must be owner-only (0o700)");
1373
1603
  }
1374
- await h.runner.waitFor(first.id);
1375
- const next = h.runner.run(managedRequest());
1376
- await tick();
1377
- assert.equal(h.store.get(next.id)?.status, TASK_STATUS.RUNNING);
1378
- h.runner.cancelAll();
1604
+
1605
+ fake.exit(0);
1606
+ await runner.waitFor(task.id);
1607
+ assert.ok(!existsSync(promptValue), "temporary transport file must be cleaned up on child exit");
1608
+ assert.ok(!existsSync(dirname(promptValue)), "temporary transport directory must be cleaned up on child exit");
1609
+ });
1610
+
1611
+ test("temporary instructions transport file is cleaned up if spawn throws synchronously", async () => {
1612
+ const largeInstructions = "Instructions header:\n" + "x".repeat(2500);
1613
+ const largeAgent: AgentDefinition = { ...explorer, instructions: largeInstructions };
1614
+ let capturedPromptPath: string | undefined;
1615
+ let clock = 1000;
1616
+ const deps: RunnerDeps = {
1617
+ spawn: (_command, args) => {
1618
+ const idx = args.indexOf("--append-system-prompt");
1619
+ if (idx !== -1) capturedPromptPath = args[idx + 1];
1620
+ throw new Error("spawn failed intentionally");
1621
+ },
1622
+ now: () => (clock += 1),
1623
+ schedule: (_fn, _ms) => () => {},
1624
+ pi: { command: "pi", args: [] },
1625
+ };
1626
+ const store = new TaskStore();
1627
+ const runner = new AgentRunner(store, { maxConcurrency: 1, stallTimeoutMs: 10_000 }, deps, {
1628
+ askUser: async () => ({ value: "yes" }),
1629
+ });
1630
+ const task = runner.run(request({ agent: largeAgent }));
1379
1631
  await tick();
1632
+
1633
+ const finished = await runner.waitFor(task.id);
1634
+ assert.equal(finished.status, TASK_STATUS.FAILED);
1635
+ assert.ok(capturedPromptPath, "should have captured a transport file path");
1636
+ assert.ok(!existsSync(capturedPromptPath), "temporary transport file must be cleaned up even when spawn throws");
1637
+ assert.ok(!existsSync(dirname(capturedPromptPath)), "temporary transport directory must be cleaned up even when spawn throws");
1380
1638
  });
1381
1639
 
1382
- test("managed exclusion lasts until child cleanup is confirmed", async () => {
1383
- const h = harness({ pid: 123, exitOnKill: false, process: { platform: "win32", kill() {} } });
1384
- const first = h.runner.run(managedRequest());
1640
+ test("agent instructions over the byte threshold are transported via file even when under the character threshold", async () => {
1641
+ const multibyteInstructions = "界".repeat(400);
1642
+ assert.ok(multibyteInstructions.length < 1000 && Buffer.byteLength(multibyteInstructions, "utf8") > 1000);
1643
+ const launches: string[][] = [];
1644
+ const fake = fakeChild();
1645
+ let clock = 1000;
1646
+ const deps: RunnerDeps = {
1647
+ spawn: (_command, args) => {
1648
+ launches.push(args);
1649
+ return fake.child;
1650
+ },
1651
+ now: () => (clock += 1),
1652
+ schedule: (_fn, _ms) => () => {},
1653
+ pi: { command: "pi", args: [] },
1654
+ };
1655
+ const runner = new AgentRunner(new TaskStore(), { maxConcurrency: 1, stallTimeoutMs: 10_000 }, deps, {
1656
+ askUser: async () => ({ value: "yes" }),
1657
+ });
1658
+ const task = runner.run(request({ agent: { ...explorer, instructions: multibyteInstructions } }));
1385
1659
  await tick();
1386
- h.runner.cancel(first.id);
1387
- try { assert.throws(() => h.runner.run(managedRequest()), /Remediation already queued or running/); }
1388
- finally { h.children[0].exit(0); }
1389
- await h.runner.waitFor(first.id);
1390
- const next = h.runner.run(managedRequest());
1660
+
1661
+ assert.equal(launches.length, 1);
1662
+ const promptValue = launches[0][launches[0].indexOf("--append-system-prompt") + 1];
1663
+ assert.notEqual(promptValue, multibyteInstructions, "multibyte instructions over the byte threshold must not be passed inline");
1664
+ assert.ok(existsSync(promptValue), "temporary instructions transport file must exist on disk");
1665
+ assert.equal(readFileSync(promptValue, "utf8"), multibyteInstructions);
1666
+
1667
+ fake.exit(0);
1668
+ await runner.waitFor(task.id);
1669
+ assert.ok(!existsSync(dirname(promptValue)), "temporary transport directory must be cleaned up on child exit");
1670
+ });
1671
+
1672
+ test("long agent names are truncated in the instructions transport directory name", async () => {
1673
+ const largeInstructions = "Instructions header:\n" + "x".repeat(2500);
1674
+ const launches: string[][] = [];
1675
+ const fake = fakeChild();
1676
+ let clock = 1000;
1677
+ const deps: RunnerDeps = {
1678
+ spawn: (_command, args) => {
1679
+ launches.push(args);
1680
+ return fake.child;
1681
+ },
1682
+ now: () => (clock += 1),
1683
+ schedule: (_fn, _ms) => () => {},
1684
+ pi: { command: "pi", args: [] },
1685
+ };
1686
+ const runner = new AgentRunner(new TaskStore(), { maxConcurrency: 1, stallTimeoutMs: 10_000 }, deps, {
1687
+ askUser: async () => ({ value: "yes" }),
1688
+ });
1689
+ const task = runner.run(request({ agent: { ...explorer, name: "a".repeat(300), instructions: largeInstructions } }));
1391
1690
  await tick();
1392
- assert.equal(h.store.get(next.id)?.status, TASK_STATUS.RUNNING);
1393
- h.runner.cancelAll();
1394
- for (const child of h.children) child.exit(0);
1691
+
1692
+ assert.equal(launches.length, 1, "launch must succeed despite a long agent name");
1693
+ const promptValue = launches[0][launches[0].indexOf("--append-system-prompt") + 1];
1694
+ assert.equal(readFileSync(promptValue, "utf8"), largeInstructions);
1695
+ const dirName = basename(dirname(promptValue));
1696
+ assert.ok(dirName.startsWith(`gentle-pi-subagent-${"a".repeat(64)}-`), dirName);
1697
+ assert.ok(dirName.length <= "gentle-pi-subagent-".length + 64 + 1 + 6, `directory name too long: ${dirName.length}`);
1698
+
1699
+ fake.exit(0);
1700
+ await runner.waitFor(task.id);
1701
+ assert.ok(!existsSync(dirname(promptValue)));
1702
+ });
1703
+
1704
+ test("temporary instructions transport directory is cleaned up if writing instructions fails", async (t) => {
1705
+ // Fail only the transport write; the ESM named import is refreshed via syncBuiltinESMExports.
1706
+ const originalWriteFileSync = fs.writeFileSync;
1707
+ let transportDir: string | undefined;
1708
+ t.mock.method(fs, "writeFileSync", (...args: Parameters<typeof fs.writeFileSync>) => {
1709
+ const [target] = args;
1710
+ if (typeof target === "string" && basename(target) === "instructions.md" && basename(dirname(target)).startsWith("gentle-pi-subagent-")) {
1711
+ transportDir = dirname(target);
1712
+ throw new Error("EACCES: simulated write failure");
1713
+ }
1714
+ return originalWriteFileSync(...args);
1715
+ });
1716
+ syncBuiltinESMExports();
1717
+ t.after(() => {
1718
+ t.mock.restoreAll();
1719
+ syncBuiltinESMExports();
1720
+ });
1721
+ const failingAgent: AgentDefinition = {
1722
+ ...explorer,
1723
+ instructions: "Instructions header:\n" + "x".repeat(2500),
1724
+ };
1725
+ let clock = 1000;
1726
+ const deps: RunnerDeps = {
1727
+ spawn: () => {
1728
+ throw new Error("spawn should not be called when writing instructions fails");
1729
+ },
1730
+ now: () => (clock += 1),
1731
+ schedule: (_fn, _ms) => () => {},
1732
+ pi: { command: "pi", args: [] },
1733
+ };
1734
+ const store = new TaskStore();
1735
+ const runner = new AgentRunner(store, { maxConcurrency: 1, stallTimeoutMs: 10_000 }, deps, {
1736
+ askUser: async () => ({ value: "yes" }),
1737
+ });
1738
+ const task = runner.run(request({ agent: failingAgent }));
1395
1739
  await tick();
1740
+
1741
+ const finished = await runner.waitFor(task.id);
1742
+ assert.equal(finished.status, TASK_STATUS.FAILED);
1743
+ assert.match(finished.error ?? "", /could not write agent instructions: EACCES: simulated write failure/);
1744
+ assert.ok(transportDir, "transport directory must have been created before the write failed");
1745
+ assert.ok(!existsSync(transportDir), "transport directory must be cleaned up on write failure");
1396
1746
  });
1397
1747
 
1398
- test("managed exclusion releases failed startup and ignores historical-only tasks", async () => {
1399
- const h = harness({ failStart: true });
1400
- const first = h.runner.run(managedRequest());
1401
- assert.equal((await h.runner.waitFor(first.id)).status, TASK_STATUS.FAILED);
1402
- h.store.add({ ...h.store.get(first.id)!, id: "historical-only", status: TASK_STATUS.RUNNING });
1403
- const next = h.runner.run(managedRequest());
1404
- assert.equal((await h.runner.waitFor(next.id)).status, TASK_STATUS.FAILED, "the next actor reaches spawn, not a historical admission lock");
1405
- assert.match(h.store.get(next.id)?.error ?? "", /fixture spawn failed/);
1748
+ test("temporary instructions transport file is cleaned up if child emits an early error before PID", async () => {
1749
+ const largeInstructions = "Instructions header:\n" + "x".repeat(2500);
1750
+ const largeAgent: AgentDefinition = { ...explorer, instructions: largeInstructions };
1751
+ let capturedPromptPath: string | undefined;
1752
+ const fake = fakeChild({ pid: undefined });
1753
+ let clock = 1000;
1754
+ const deps: RunnerDeps = {
1755
+ spawn: (_command, args) => {
1756
+ const idx = args.indexOf("--append-system-prompt");
1757
+ if (idx !== -1) capturedPromptPath = args[idx + 1];
1758
+ queueMicrotask(() => {
1759
+ fake.fail("spawn ENOENT");
1760
+ });
1761
+ return fake.child;
1762
+ },
1763
+ now: () => (clock += 1),
1764
+ schedule: (_fn, _ms) => () => {},
1765
+ pi: { command: "pi", args: [] },
1766
+ };
1767
+ const store = new TaskStore();
1768
+ const runner = new AgentRunner(store, { maxConcurrency: 1, stallTimeoutMs: 10_000 }, deps, {
1769
+ askUser: async () => ({ value: "yes" }),
1770
+ });
1771
+ const task = runner.run(request({ agent: largeAgent }));
1772
+ await tick();
1773
+
1774
+ const finished = await runner.waitFor(task.id);
1775
+ assert.equal(finished.status, TASK_STATUS.FAILED);
1776
+ assert.match(finished.error ?? "", /could not start pi: spawn ENOENT/);
1777
+ assert.ok(capturedPromptPath, "should have captured a transport file path");
1778
+ assert.ok(!existsSync(capturedPromptPath), "temporary transport file must be cleaned up on early child error");
1779
+ assert.ok(!existsSync(dirname(capturedPromptPath)), "temporary transport directory must be cleaned up on early child error");
1406
1780
  });