gentle-pi 4.0.0 → 4.2.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 (425) hide show
  1. package/README.md +157 -18
  2. package/assets/agents/gentle-ai-explore.md +11 -4
  3. package/assets/agents/gentle-ai-verify.md +23 -4
  4. package/assets/agents/gentle-ai-worker.md +6 -6
  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 +70 -163
  9. package/assets/orchestrator-memory.md +11 -4
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-tracking.md +53 -0
  12. package/assets/orchestrator-verification.md +28 -0
  13. package/assets/orchestrator-writer.md +38 -0
  14. package/assets/orchestrator.md +29 -25
  15. package/assets/sounds/LICENSE.md +52 -0
  16. package/assets/sounds/attention.wav +0 -0
  17. package/assets/sounds/error.wav +0 -0
  18. package/assets/sounds/success.wav +0 -0
  19. package/bin/engram-guard +9 -0
  20. package/bin/gentle-shell-install.mjs +302 -0
  21. package/bin/gentle-shell.mjs +708 -22
  22. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  23. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  24. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  25. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +399 -0
  30. package/docs/delegated-verification.md +3 -5
  31. package/docs/gentle-agents-activity.md +513 -0
  32. package/docs/gentle-shell-uninstall.md +136 -0
  33. package/docs/gentle-shell.md +90 -33
  34. package/docs/install-wizard.md +2040 -0
  35. package/docs/native-authority-architecture.md +1 -1
  36. package/docs/permission-modes.md +127 -0
  37. package/docs/prompt-history.md +1 -1
  38. package/docs/readme-reference.md +310 -47
  39. package/docs/render-cache-performance.md +28 -0
  40. package/docs/session-profile-format.md +39 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/yolo-mode.md +11 -10
  44. package/extensions/ask-user-choice.ts +4 -0
  45. package/extensions/ask-user-question.ts +6 -1
  46. package/extensions/child-context.ts +11 -2
  47. package/extensions/child-safety.ts +4 -2
  48. package/extensions/codegraph-tools.ts +18 -5
  49. package/extensions/gentle-agents.ts +718 -87
  50. package/extensions/gentle-ai.ts +991 -263
  51. package/extensions/gentle-herdr-activity.ts +81 -0
  52. package/extensions/gentle-notifications.ts +104 -0
  53. package/extensions/gentle-shell.ts +271 -80
  54. package/extensions/gentle-todo.ts +123 -18
  55. package/extensions/history/index.ts +11 -6
  56. package/extensions/pi-pretty.ts +39 -3
  57. package/extensions/quiet-tools.ts +91 -21
  58. package/extensions/skill-registry.ts +175 -46
  59. package/extensions/startup-banner.ts +11 -2
  60. package/lib/agent-home.ts +10 -1
  61. package/lib/agent-model-resolution.ts +80 -0
  62. package/lib/agent-profiles.ts +13 -3
  63. package/lib/agents-completion-delivery.ts +11 -3
  64. package/lib/agents-messaging.ts +1 -1
  65. package/lib/agents-protocol.ts +40 -1
  66. package/lib/agents-runner.ts +90 -15
  67. package/lib/auto-permission-answers.ts +133 -0
  68. package/lib/auto-permission-classifier.ts +459 -0
  69. package/lib/auto-permission-evidence.ts +88 -0
  70. package/lib/auto-permission-memory.ts +69 -0
  71. package/lib/auto-permission-runtime.ts +484 -0
  72. package/lib/auto-permission-tool-context.ts +61 -0
  73. package/lib/background-jobs-tools.ts +202 -0
  74. package/lib/background-jobs.ts +253 -0
  75. package/lib/background-monitor.ts +154 -0
  76. package/lib/bounded-writer-admission.ts +83 -10
  77. package/lib/builtin-codemode-optout.ts +166 -0
  78. package/lib/card-content-policy.ts +85 -0
  79. package/lib/child-package-injection.ts +62 -0
  80. package/lib/codemode-renderer.ts +14 -4
  81. package/lib/command-palette-catalog.ts +3 -2
  82. package/lib/context-threshold.ts +26 -0
  83. package/lib/destructive-command-guard.ts +104 -28
  84. package/lib/gentle-ai-binary.ts +109 -4
  85. package/lib/gentle-shell-bundled.ts +535 -0
  86. package/lib/gentle-shell-launcher.ts +75 -24
  87. package/lib/gentle-shell-startup-flow.ts +54 -0
  88. package/lib/gentle-shell-startup-update.ts +92 -0
  89. package/lib/gentle-shell-uninstall.ts +840 -0
  90. package/lib/gentle-shell-update-panel.ts +231 -0
  91. package/lib/herdr-activity.ts +183 -0
  92. package/lib/inprocess-reviewer.ts +3 -0
  93. package/lib/jobs-sidebar-state.ts +20 -0
  94. package/lib/jobs-view.ts +161 -0
  95. package/lib/model-price-ratio.ts +83 -0
  96. package/lib/model-routing-authority.ts +6 -1
  97. package/lib/nan-provider.ts +33 -11
  98. package/lib/native-review-cli.ts +194 -73
  99. package/lib/notification-audio-native.ts +162 -0
  100. package/lib/notification-audio-windows.ts +399 -0
  101. package/lib/notification-audio.ts +332 -0
  102. package/lib/notification-customize.ts +241 -0
  103. package/lib/notification-events.ts +113 -0
  104. package/lib/notification-policy.ts +148 -0
  105. package/lib/notification-pulse-client.ts +367 -0
  106. package/lib/notification-pulse-protocol.ts +305 -0
  107. package/lib/notification-pulse-stream.ts +213 -0
  108. package/lib/notification-pulse-worker.ts +66 -0
  109. package/lib/notification-scheduler.ts +173 -0
  110. package/lib/notification-service.ts +202 -0
  111. package/lib/notification-ui.ts +87 -0
  112. package/lib/odd-phase.ts +1 -1
  113. package/lib/orchestrator-alias.ts +20 -0
  114. package/lib/orchestrator-catalog.ts +79 -0
  115. package/lib/orchestrator-consultation.ts +61 -0
  116. package/lib/orchestrator-discovery.ts +49 -0
  117. package/lib/orchestrator-helper-consent.ts +122 -0
  118. package/lib/orchestrator-helper.ts +167 -0
  119. package/lib/orchestrator-message-card.ts +105 -0
  120. package/lib/orchestrator-presence.ts +115 -14
  121. package/lib/orchestrator-scope.ts +80 -0
  122. package/lib/orchestrator-state.ts +107 -0
  123. package/lib/orchestrator-tool-card.ts +280 -0
  124. package/lib/orchestrator-work-search.ts +218 -0
  125. package/lib/orchestrator-work.ts +79 -0
  126. package/lib/permission-mode-policy.ts +59 -0
  127. package/lib/review-candidate-view-owner.ts +8 -6
  128. package/lib/review-candidate-view.ts +3 -2
  129. package/lib/review-compact-contract.ts +2 -2
  130. package/lib/review-host-relay.ts +7 -2
  131. package/lib/review-integration-v2.ts +100 -22
  132. package/lib/review-publication-gate.ts +1 -1
  133. package/lib/review-risk-assessment.ts +77 -51
  134. package/lib/review-sidebar-state.ts +2 -1
  135. package/lib/review-transaction.ts +72 -3
  136. package/lib/runtime-metrics-children.ts +1 -1
  137. package/lib/runtime-metrics-native.ts +3 -2
  138. package/lib/selection-engine.ts +22 -7
  139. package/lib/session-change-capture.ts +5 -4
  140. package/lib/session-changes.ts +27 -22
  141. package/lib/session-profile-binding.ts +83 -0
  142. package/lib/session-profile-freeze.ts +261 -0
  143. package/lib/session-profile-persistence.ts +187 -0
  144. package/lib/session-worktree-registry.ts +74 -8
  145. package/lib/shell-bar.ts +41 -11
  146. package/lib/shell-changes-view.ts +40 -13
  147. package/lib/shell-changes.ts +88 -71
  148. package/lib/shell-prompt.ts +6 -4
  149. package/lib/shell-sidebar-layout.ts +5 -1
  150. package/lib/shell-todo.ts +81 -26
  151. package/lib/shell-usage.ts +4 -3
  152. package/lib/status-timing-diagnostics.ts +201 -0
  153. package/lib/telemetry-trigger.ts +3 -1
  154. package/lib/vim-editor-adapter.ts +27 -5
  155. package/lib/visual-customization-policy.ts +20 -9
  156. package/lib/visual-customize-view.ts +171 -20
  157. package/lib/writer-surfaces.ts +162 -0
  158. package/lib/yolo-session-policy.ts +198 -121
  159. package/package.json +5 -5
  160. package/runtime/child-package-injection.mjs +63 -0
  161. package/runtime/gentle-ai-binary.mjs +109 -4
  162. package/runtime/gentle-shell-bundled.mjs +536 -0
  163. package/runtime/gentle-shell-launcher.mjs +74 -23
  164. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  165. package/runtime/gentle-shell-startup-update.mjs +93 -0
  166. package/runtime/gentle-shell-uninstall.mjs +841 -0
  167. package/runtime/gentle-shell-update-panel.mjs +232 -0
  168. package/runtime/native-review-cli.mjs +191 -70
  169. package/runtime/review-integration-v2.mjs +100 -22
  170. package/runtime/review-risk-assessment.mjs +77 -51
  171. package/runtime/status-timing-diagnostics.mjs +202 -0
  172. package/runtime/telemetry-trigger.mjs +3 -1
  173. package/scripts/bootstrap.cmd +255 -0
  174. package/scripts/bootstrap.sh +168 -0
  175. package/scripts/build-distribution.mjs +157 -0
  176. package/scripts/build-installer-bundles.mjs +136 -0
  177. package/scripts/build-runtime-modules.mjs +7 -0
  178. package/scripts/bundled-install.mjs +1308 -0
  179. package/scripts/bundled-wizard.mjs +399 -0
  180. package/scripts/check-types.mjs +4 -6
  181. package/scripts/gentle-ai-installer.mjs +79 -32
  182. package/scripts/install-gentle-ai.mjs +48 -1
  183. package/scripts/install-tui-mode-setting.mjs +10 -1
  184. package/scripts/install-wizard-preview.mjs +154 -0
  185. package/scripts/installer-downloads.mjs +648 -0
  186. package/scripts/installer-preflight.mjs +387 -0
  187. package/scripts/installer-probes.mjs +564 -0
  188. package/scripts/installer-runner.mjs +1102 -0
  189. package/scripts/installer-server.mjs +881 -0
  190. package/scripts/installer-windows-artifacts.json +15 -0
  191. package/scripts/installer-windows.mjs +752 -0
  192. package/scripts/main-channel.mjs +452 -0
  193. package/scripts/npm-pack-result.mjs +31 -0
  194. package/scripts/pi-update.mjs +223 -0
  195. package/scripts/render-cache-performance-worker.mjs +205 -0
  196. package/scripts/render-cache-performance.mjs +115 -0
  197. package/scripts/run-test-suite.mjs +22 -8
  198. package/scripts/test-installer-acceptance.sh +161 -0
  199. package/scripts/test-packed-runner.mjs +23 -5
  200. package/scripts/types-baseline.json +2 -92
  201. package/scripts/verify-package-files.mjs +82 -18
  202. package/skills/gentle-ai/SKILL.md +12 -11
  203. package/tests/agent-home.test.ts +56 -3
  204. package/tests/agent-model-resolution.test.ts +113 -0
  205. package/tests/agent-profiles.test.ts +12 -0
  206. package/tests/agent-risk-escalation.test.ts +123 -0
  207. package/tests/agents-completion-delivery.test.ts +17 -0
  208. package/tests/agents-protocol.test.ts +57 -0
  209. package/tests/agents-queries.test.ts +9 -9
  210. package/tests/agents-runner-process.test.ts +31 -0
  211. package/tests/agents-runner.test.ts +89 -4
  212. package/tests/append-system-prompt-route.test.ts +18 -1
  213. package/tests/auto-mode-human-answers.test.ts +68 -0
  214. package/tests/auto-mode-invariants.test.ts +276 -0
  215. package/tests/auto-mode-sdk.test.ts +84 -0
  216. package/tests/auto-mode.test.ts +49 -0
  217. package/tests/auto-permission-answers.test.ts +170 -0
  218. package/tests/auto-permission-classifier.test.ts +634 -0
  219. package/tests/auto-permission-compound.test.ts +24 -0
  220. package/tests/auto-permission-evidence.test.ts +88 -0
  221. package/tests/auto-permission-memory.test.ts +46 -0
  222. package/tests/auto-permission-public-regressions.test.ts +111 -0
  223. package/tests/auto-permission-runtime.test.ts +454 -0
  224. package/tests/autonomous-guard.test.ts +86 -16
  225. package/tests/background-cache-warming.test.ts +2 -1
  226. package/tests/background-jobs.test.ts +227 -0
  227. package/tests/background-monitor.test.ts +195 -0
  228. package/tests/background-subagents-default-mode.test.ts +28 -5
  229. package/tests/bounded-stop-contract.test.ts +85 -0
  230. package/tests/bounded-writer-admission.test.ts +52 -1
  231. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  232. package/tests/bridge-wake-rendering.test.ts +56 -0
  233. package/tests/build-distribution.test.ts +269 -0
  234. package/tests/builtin-codemode-optout.test.ts +242 -0
  235. package/tests/bundled-install.test.ts +1319 -0
  236. package/tests/bundled-wizard.test.ts +505 -0
  237. package/tests/card-content-policy.test.ts +68 -0
  238. package/tests/card-style-policy.test.ts +34 -2
  239. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  240. package/tests/child-package-entrypoints.test.ts +65 -0
  241. package/tests/child-package-injection.test.ts +74 -0
  242. package/tests/child-safety.test.ts +12 -0
  243. package/tests/child-web-package.test.ts +224 -0
  244. package/tests/codegraph-tools.test.ts +35 -0
  245. package/tests/codemode-rendering.test.ts +107 -2
  246. package/tests/command-palette.test.ts +5 -5
  247. package/tests/context-threshold.test.ts +31 -0
  248. package/tests/delegate-for-reason-contract.test.ts +327 -0
  249. package/tests/delegated-key-learnings-contract.test.ts +3 -2
  250. package/tests/devbinary/native-review-parity.devtest.ts +1 -1
  251. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  252. package/tests/engram-safety.test.ts +316 -0
  253. package/tests/generic-agent-tools.test.ts +16 -1
  254. package/tests/gentle-agents.test.ts +1895 -76
  255. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  256. package/tests/gentle-ai-binary.test.ts +19 -2
  257. package/tests/gentle-ai-child-guards.test.ts +202 -0
  258. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  259. package/tests/gentle-ai-installer.test.ts +548 -58
  260. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  261. package/tests/gentle-ai-renderer.test.ts +6 -6
  262. package/tests/gentle-ai.test.ts +999 -31
  263. package/tests/gentle-herdr-activity.test.ts +100 -0
  264. package/tests/gentle-notifications.test.ts +427 -0
  265. package/tests/gentle-shell-bin.test.ts +263 -19
  266. package/tests/gentle-shell-bundled.test.ts +1036 -0
  267. package/tests/gentle-shell-launcher.test.ts +251 -25
  268. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  269. package/tests/gentle-shell-startup-update.test.ts +85 -0
  270. package/tests/gentle-shell-uninstall.test.ts +978 -0
  271. package/tests/gentle-shell-update-panel.test.ts +158 -0
  272. package/tests/gentle-shell.test.ts +611 -100
  273. package/tests/gentle-todo.test.ts +317 -14
  274. package/tests/herdr-activity.test.ts +243 -0
  275. package/tests/history-agent-home.test.ts +81 -0
  276. package/tests/history-child-guard.test.ts +70 -0
  277. package/tests/inprocess-reviewer.test.ts +20 -0
  278. package/tests/install-gentle-ai-failure.test.ts +69 -0
  279. package/tests/install-tui-mode-setting.test.ts +15 -0
  280. package/tests/install-wizard.test.ts +1223 -0
  281. package/tests/installer-bundles.test.ts +122 -0
  282. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  283. package/tests/installer-preflight.test.ts +750 -0
  284. package/tests/installer-probes.test.ts +1106 -0
  285. package/tests/installer-runner.test.ts +2159 -0
  286. package/tests/installer-server.test.ts +1567 -0
  287. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  288. package/tests/jobs-view.test.ts +160 -0
  289. package/tests/main-channel-upgrade.test.ts +395 -0
  290. package/tests/main-channel.test.ts +344 -0
  291. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  292. package/tests/model-price-ratio.test.ts +116 -0
  293. package/tests/model-profiles-preservation.test.ts +153 -0
  294. package/tests/model-routing-package.test.ts +10 -0
  295. package/tests/nan-provider-runtime.test.ts +156 -0
  296. package/tests/nan-provider.test.ts +28 -18
  297. package/tests/native-binary-gate.test.ts +2 -0
  298. package/tests/native-review-capability-contract.test.ts +7 -1
  299. package/tests/native-review-cli.test.ts +236 -1
  300. package/tests/native-review-consent.test.ts +2 -2
  301. package/tests/native-review-parity-runtime.test.ts +1 -1
  302. package/tests/native-review-parity.test.ts +12 -48
  303. package/tests/native-risk-evidence-parity.test.ts +21 -0
  304. package/tests/no-self-pause-contract.test.ts +87 -0
  305. package/tests/notification-audio-native.test.ts +281 -0
  306. package/tests/notification-audio-windows.test.ts +581 -0
  307. package/tests/notification-audio.test.ts +548 -0
  308. package/tests/notification-customize.test.ts +655 -0
  309. package/tests/notification-events.test.ts +115 -0
  310. package/tests/notification-policy.test.ts +201 -0
  311. package/tests/notification-pulse-client.test.ts +356 -0
  312. package/tests/notification-pulse-protocol.test.ts +213 -0
  313. package/tests/notification-pulse-stream.test.ts +320 -0
  314. package/tests/notification-scheduler.test.ts +258 -0
  315. package/tests/notification-ui.test.ts +157 -0
  316. package/tests/npm-pack-result.test.ts +41 -0
  317. package/tests/odd-integration.test.ts +14 -5
  318. package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
  319. package/tests/odd-routing-contract.test.ts +80 -23
  320. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  321. package/tests/orchestrator-budget.test.ts +29 -6
  322. package/tests/orchestrator-catalog.test.ts +146 -0
  323. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  324. package/tests/orchestrator-consultation.test.ts +111 -0
  325. package/tests/orchestrator-discovery.test.ts +241 -0
  326. package/tests/orchestrator-helper-consent.test.ts +171 -0
  327. package/tests/orchestrator-helper.test.ts +297 -0
  328. package/tests/orchestrator-message-card.test.ts +190 -0
  329. package/tests/orchestrator-presence.test.ts +23 -0
  330. package/tests/orchestrator-rdd-ownership.test.ts +6 -5
  331. package/tests/orchestrator-scope.test.ts +107 -0
  332. package/tests/orchestrator-state.test.ts +114 -0
  333. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  334. package/tests/orchestrator-tool-card.test.ts +398 -0
  335. package/tests/orchestrator-work-search.test.ts +318 -0
  336. package/tests/orchestrator-work.test.ts +147 -0
  337. package/tests/package-manifest.test.ts +105 -21
  338. package/tests/packed-runner-owned-path.test.ts +35 -1
  339. package/tests/parallel-reason-contract.test.ts +21 -0
  340. package/tests/parallel-writer-admission.test.ts +210 -0
  341. package/tests/permission-mode-policy.test.ts +194 -0
  342. package/tests/persona-single-channel.test.ts +4 -12
  343. package/tests/pi-pretty.test.ts +56 -0
  344. package/tests/pi-update-launcher.test.ts +87 -0
  345. package/tests/pi-update.test.ts +186 -0
  346. package/tests/provider-defect-handoff.test.ts +4 -3
  347. package/tests/quiet-tool-rendering.test.ts +160 -3
  348. package/tests/rdd-aware-verification-contract.test.ts +29 -41
  349. package/tests/rdd-status-line.test.ts +52 -0
  350. package/tests/render-cache-performance.test.ts +48 -0
  351. package/tests/review-agent-end-preflight.test.ts +62 -10
  352. package/tests/review-candidate-view.test.ts +101 -12
  353. package/tests/review-contract-prompt.test.ts +33 -4
  354. package/tests/review-controller-native-recovery.test.ts +327 -17
  355. package/tests/review-controller-native-routing.test.ts +255 -1
  356. package/tests/review-controller-workspace-root.test.ts +43 -2
  357. package/tests/review-controller.test.ts +2 -2
  358. package/tests/review-correction-lifecycle.test.ts +2 -1
  359. package/tests/review-gate.test.ts +7 -4
  360. package/tests/review-graph-schema.test.ts +2 -2
  361. package/tests/review-host-relay-routing.test.ts +5 -3
  362. package/tests/review-host-relay.test.ts +49 -2
  363. package/tests/review-integration-v2-forward.test.ts +21 -1
  364. package/tests/review-json-arguments.test.ts +167 -0
  365. package/tests/review-last-event-closure.test.ts +157 -8
  366. package/tests/review-ledger-contract.test.ts +5 -4
  367. package/tests/review-policy-judgment-day.test.ts +1 -1
  368. package/tests/review-policy-ordinary.test.ts +2 -2
  369. package/tests/review-relay-transport-agent.test.ts +66 -1
  370. package/tests/review-risk-assessment.test.ts +109 -33
  371. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  372. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  373. package/tests/review-test-fixtures.ts +7 -1
  374. package/tests/review-transaction.test.ts +18 -1
  375. package/tests/run-test-suite.test.ts +83 -5
  376. package/tests/runtime-harness.mjs +42 -15
  377. package/tests/runtime-metrics-extension.test.ts +1 -1
  378. package/tests/runtime-metrics-native.test.ts +31 -1
  379. package/tests/selection-engine.test.ts +65 -5
  380. package/tests/session-change-capture.test.ts +10 -10
  381. package/tests/session-changes-shell.test.ts +242 -48
  382. package/tests/session-changes.test.ts +17 -24
  383. package/tests/session-profile-binding.test.ts +163 -0
  384. package/tests/session-profile-freeze.test.ts +394 -0
  385. package/tests/session-profile-persistence.test.ts +334 -0
  386. package/tests/session-subject-routing-contract.test.ts +34 -0
  387. package/tests/session-worktree-registry.test.ts +57 -2
  388. package/tests/shell-bar.test.ts +40 -0
  389. package/tests/shell-changes-view.test.ts +85 -34
  390. package/tests/shell-changes.test.ts +79 -62
  391. package/tests/shell-render-cache-runtime.test.ts +69 -0
  392. package/tests/shell-render-cache.test.ts +129 -0
  393. package/tests/shell-sidebar-layout.test.ts +27 -2
  394. package/tests/shell-todo.test.ts +132 -11
  395. package/tests/shell-usage-view.test.ts +1 -1
  396. package/tests/shell-usage.test.ts +35 -0
  397. package/tests/short-check-routing-contract.test.ts +49 -0
  398. package/tests/skill-registry-watcher.test.ts +119 -0
  399. package/tests/skill-registry.test.ts +311 -5
  400. package/tests/startup-banner.test.ts +117 -3
  401. package/tests/startup-home-repo.test.ts +170 -0
  402. package/tests/status-timing-diagnostics.test.ts +283 -0
  403. package/tests/support/orchestrator-modules.ts +19 -0
  404. package/tests/task-size-routing-contract.test.ts +241 -0
  405. package/tests/telemetry-trigger.test.ts +27 -1
  406. package/tests/test-strength-contract.test.ts +124 -0
  407. package/tests/verify-agent-contract.test.ts +90 -0
  408. package/tests/verify-first-launch-contract.test.ts +139 -0
  409. package/tests/verify-package-files.test.ts +56 -0
  410. package/tests/vim-editor-adapter.test.ts +47 -21
  411. package/tests/visual-customization-policy.test.ts +32 -3
  412. package/tests/visual-customize-view.test.ts +288 -10
  413. package/tests/writer-edit-surface-scope.test.ts +59 -2
  414. package/tests/writer-surfaces.test.ts +199 -0
  415. package/tests/yolo-customize.test.ts +18 -3
  416. package/tests/yolo-mode-runtime.test.ts +37 -10
  417. package/tests/yolo-mode.test.ts +38 -5
  418. package/tests/yolo-session-policy.test.ts +1 -1
  419. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  420. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  421. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  422. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  423. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  424. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  425. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -0,0 +1,113 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import test, { after } from "node:test";
6
+ import { resolveAgentHomeDirectory, resolveEffectiveAgentModel, resolvePinnedAgentProfile } from "../lib/agent-model-resolution.ts";
7
+ import type { AgentDefinition } from "../lib/agents-config.ts";
8
+
9
+ // gentle-shell#1731 S3/AC3: the subagent launch and the harness model-routing
10
+ // fact resolve the effective subagent model through one helper, so the fact
11
+ // can never disagree with the model a launch would actually use.
12
+
13
+ const root = mkdtempSync(join(tmpdir(), "gentle-pi-agent-model-resolution-"));
14
+ after(() => rmSync(root, { recursive: true, force: true }));
15
+
16
+ function fixture(name: string) {
17
+ const base = join(root, name);
18
+ const home = join(base, "home");
19
+ const agentHome = join(base, "agent-home");
20
+ const cwd = join(base, "project");
21
+ const commonDir = join(base, "git-common");
22
+ const configHome = join(base, "config");
23
+ for (const dir of [home, join(agentHome, "agents"), cwd, commonDir, configHome]) mkdirSync(dir, { recursive: true });
24
+ return {
25
+ roots: { cwd, home, agentHome },
26
+ cwd,
27
+ configHome,
28
+ resolveWorktree: () => ({ root: cwd, commonDir }),
29
+ writeAgent(agentName: string, model?: string) {
30
+ writeFileSync(join(agentHome, "agents", `${agentName}.md`), `---\nname: ${agentName}\ndescription: Test agent.\n${model === undefined ? "" : `model: ${model}\n`}---\nBody.\n`);
31
+ },
32
+ writeSubagents(config: Record<string, unknown>) {
33
+ writeFileSync(join(agentHome, "subagents.json"), JSON.stringify(config));
34
+ },
35
+ writePinnedProfile(profiles: Record<string, unknown>, pin: string) {
36
+ writeFileSync(join(configHome, "profiles.json"), JSON.stringify({ kind: "gentle-pi.agent_model_profiles", version: 1, profiles }));
37
+ mkdirSync(join(commonDir, "gentle-ai"), { recursive: true });
38
+ writeFileSync(join(commonDir, "gentle-ai", "profile-pin.json"), JSON.stringify({ kind: "gentle-pi.agent_model_profile_pin", version: 1, profile: pin }));
39
+ },
40
+ };
41
+ }
42
+
43
+ const orchestrator = { provider: "anthropic", id: "fable" };
44
+
45
+ test("the effective worker model comes from the global subagent profile", () => {
46
+ const f = fixture("global-profile");
47
+ f.writeAgent("gentle-ai-worker");
48
+ f.writeSubagents({ model_profiles: { "gentle-ai-worker": { model: "anthropic/opus" } } });
49
+ assert.deepEqual(
50
+ resolveEffectiveAgentModel("gentle-ai-worker", { roots: f.roots, pinCwd: f.cwd, configHome: f.configHome, resolveWorktree: f.resolveWorktree, fallback: orchestrator }),
51
+ { agent: "gentle-ai-worker", model: { provider: "anthropic", id: "opus" }, inherited: false },
52
+ );
53
+ });
54
+
55
+ test("a repository profile pin replaces global routing for the effective worker model", () => {
56
+ const f = fixture("pinned-profile");
57
+ f.writeAgent("gentle-ai-worker");
58
+ f.writeSubagents({ model_profiles: { "gentle-ai-worker": { model: "anthropic/opus" } } });
59
+ f.writePinnedProfile({ cheap: { "gentle-ai-worker": { model: "openai/mini" } } }, "cheap");
60
+ assert.deepEqual(
61
+ resolveEffectiveAgentModel("gentle-ai-worker", { roots: f.roots, pinCwd: f.cwd, configHome: f.configHome, resolveWorktree: f.resolveWorktree, fallback: orchestrator })?.model,
62
+ { provider: "openai", id: "mini" },
63
+ );
64
+ });
65
+
66
+ test("the agent definition model applies when no profile routes the agent", () => {
67
+ const f = fixture("definition-model");
68
+ f.writeAgent("gentle-ai-worker", "openai/defined");
69
+ assert.deepEqual(
70
+ resolveEffectiveAgentModel("gentle-ai-worker", { roots: f.roots, pinCwd: f.cwd, configHome: f.configHome, resolveWorktree: f.resolveWorktree, fallback: orchestrator })?.model,
71
+ { provider: "openai", id: "defined" },
72
+ );
73
+ });
74
+
75
+ test("an unrouted agent inherits the fallback session model", () => {
76
+ const f = fixture("inherit");
77
+ f.writeAgent("gentle-ai-worker");
78
+ assert.deepEqual(
79
+ resolveEffectiveAgentModel("gentle-ai-worker", { roots: f.roots, pinCwd: f.cwd, configHome: f.configHome, resolveWorktree: f.resolveWorktree, fallback: orchestrator }),
80
+ { agent: "gentle-ai-worker", model: orchestrator, inherited: true },
81
+ );
82
+ assert.deepEqual(
83
+ resolveEffectiveAgentModel("gentle-ai-worker", { roots: f.roots, pinCwd: f.cwd, configHome: f.configHome, resolveWorktree: f.resolveWorktree }),
84
+ { agent: "gentle-ai-worker", model: undefined, inherited: true },
85
+ );
86
+ });
87
+
88
+ test("an undiscovered agent resolves to undefined", () => {
89
+ const f = fixture("missing-agent");
90
+ assert.equal(
91
+ resolveEffectiveAgentModel("gentle-ai-worker", { roots: f.roots, pinCwd: f.cwd, configHome: f.configHome, resolveWorktree: f.resolveWorktree, fallback: orchestrator }),
92
+ undefined,
93
+ );
94
+ });
95
+
96
+ test("resolvePinnedAgentProfile returns the launch profile with its model and thinking", () => {
97
+ const f = fixture("launch-profile");
98
+ f.writePinnedProfile({ deep: { explore: { model: "openai/alpha", thinking: "minimal" } } }, "deep");
99
+ const agent: AgentDefinition = { name: "explore", description: "", filePath: "", scope: "global", instructions: "", model: undefined, thinking: undefined, mode: undefined, tools: [] };
100
+ const profile = resolvePinnedAgentProfile(agent, { roots: f.roots, pinCwd: f.cwd, configHome: f.configHome, resolveWorktree: f.resolveWorktree });
101
+ assert.deepEqual(profile.model, { provider: "openai", id: "alpha" });
102
+ assert.equal(profile.thinking, "minimal");
103
+ assert.equal(profile.source.model, "profile");
104
+ });
105
+
106
+ test("resolveAgentHomeDirectory follows the environment, expands tildes, and keeps explicit overrides literal", () => {
107
+ const home = "/home/tester";
108
+ assert.equal(resolveAgentHomeDirectory({ env: { GENTLE_PI_AGENT_HOME: "/opt/agents" }, home }), resolve("/opt/agents"));
109
+ assert.equal(resolveAgentHomeDirectory({ env: { PI_CODING_AGENT_DIR: "~/pi-agent" }, home }), resolve(join(home, "pi-agent")));
110
+ assert.equal(resolveAgentHomeDirectory({ env: { GENTLE_PI_AGENT_HOME: "~" }, home }), resolve(home));
111
+ assert.equal(resolveAgentHomeDirectory({ env: { GENTLE_PI_AGENT_HOME: "/ignored" }, home, homeOverridden: true }), resolve(join(home, ".pi", "agent")));
112
+ assert.equal(resolveAgentHomeDirectory({ env: {}, home, agentHome: "/explicit" }), resolve("/explicit"));
113
+ });
@@ -767,3 +767,15 @@ test("formatOrchestratorSelection separates set, inherit, and inherit-with-think
767
767
  "nan/glm5.3 · max",
768
768
  );
769
769
  });
770
+
771
+ test("buildProfileListItems marks the session-bound profile ahead of the pinned one", () => {
772
+ const file = createProfile(createProfile(emptyProfilesFile(), "work", { worker: { model: "zai/glm-4.7" } }), "personal", { solo: { model: "openai/o4-mini" } });
773
+ const items = buildProfileListItems(file, "work", "personal");
774
+ assert.equal(items.find((item) => item.id === "personal")?.label, "personal (session)");
775
+ assert.equal(items.find((item) => item.id === "work")?.label, "work (pinned)");
776
+ assert.equal(items.find((item) => item.id === "personal")?.description, "1 role");
777
+ const both = buildProfileListItems(file, "work", "work");
778
+ assert.equal(both.find((item) => item.id === "work")?.label, "work (session)", "the session marker outranks the pin marker on the same profile");
779
+ const unbound = buildProfileListItems(file, "work");
780
+ assert.equal(unbound.find((item) => item.id === "personal")?.label, "personal", "no session marker without a binding");
781
+ });
@@ -0,0 +1,123 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
5
+ import { NATIVE_REVIEW_MODE_SOURCE, type NativeReviewCli } from "../lib/native-review-cli.ts";
6
+ import {
7
+ decodeAgentRiskEscalation,
8
+ escalatedRisk,
9
+ HIGH_RISK_ITEMS,
10
+ REVIEW_ASSESSMENT_SCHEMA,
11
+ VERIFICATION_TIER,
12
+ } from "../lib/review-risk-assessment.ts";
13
+
14
+ // gentle-shell#1494 S6: the agent may raise a candidate's risk to high by
15
+ // citing one item of the shared high-risk list, never lower it. The raise
16
+ // travels inside the existing assess call; no new operation.
17
+
18
+ test("the high-risk list has exactly the six items of the always-on Task Size section", () => {
19
+ assert.deepEqual(Object.keys(HIGH_RISK_ITEMS).map(Number), [1, 2, 3, 4, 5, 6]);
20
+ assert.equal(HIGH_RISK_ITEMS[2], "security");
21
+ assert.equal(HIGH_RISK_ITEMS[6], "no test would catch a regression");
22
+ });
23
+
24
+ test("decodeAgentRiskEscalation accepts an item from 1 to 6 with a non-empty reason", () => {
25
+ assert.deepEqual(decodeAgentRiskEscalation({ item: 2, reason: "weakens the permission check in utils.ts" }), {
26
+ item: 2,
27
+ reason: "weakens the permission check in utils.ts",
28
+ });
29
+ });
30
+
31
+ test("decodeAgentRiskEscalation rejects unknown items, empty or oversized reasons, and extra fields", () => {
32
+ for (const bad of [
33
+ { item: 0, reason: "x" },
34
+ { item: 7, reason: "x" },
35
+ { item: 2.5, reason: "x" },
36
+ { item: "2", reason: "x" },
37
+ { item: 2, reason: " " },
38
+ { item: 2, reason: "x".repeat(501) },
39
+ { item: 2, reason: "x", tier: "low" },
40
+ { item: 2 },
41
+ null,
42
+ "high",
43
+ ]) {
44
+ assert.throws(() => decodeAgentRiskEscalation(bad), /escalate/, `must reject ${JSON.stringify(bad)}`);
45
+ }
46
+ });
47
+
48
+ test("escalatedRisk raises passive and medium to high and never lowers", () => {
49
+ const escalation = { item: 3 as const, reason: "changes a published export" };
50
+ assert.equal(escalatedRisk(VERIFICATION_TIER.PASSIVE, escalation), VERIFICATION_TIER.HIGH);
51
+ assert.equal(escalatedRisk(VERIFICATION_TIER.MEDIUM, escalation), VERIFICATION_TIER.HIGH);
52
+ assert.equal(escalatedRisk(VERIFICATION_TIER.HIGH, escalation), VERIFICATION_TIER.HIGH);
53
+ assert.equal(escalatedRisk(VERIFICATION_TIER.UNASSESSABLE, escalation), VERIFICATION_TIER.UNASSESSABLE);
54
+ for (const tier of Object.values(VERIFICATION_TIER)) assert.equal(escalatedRisk(tier, undefined), tier);
55
+ });
56
+
57
+ function assessTool(): { execute: (id: string, params: Record<string, unknown>, signal: AbortSignal | undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<{ details: unknown }> } {
58
+ const tools = new Map<string, any>();
59
+ const pi = { on() {}, registerCommand() {}, registerTool(tool: { name: string }) { tools.set(tool.name, tool); } } as unknown as ExtensionAPI;
60
+ const nativeReviewCli: Partial<NativeReviewCli> = {
61
+ reviewMode: async () => ({ operation: "status", scope: "clone", status: { global: "off", cloneLocal: "off", effective: "off", source: NATIVE_REVIEW_MODE_SOURCE.GLOBAL } }),
62
+ assess: async () => ({
63
+ schema: REVIEW_ASSESSMENT_SCHEMA,
64
+ risk: "medium",
65
+ reasons: [],
66
+ changedPaths: 1,
67
+ changedLines: 3,
68
+ candidate: { kind: "current-changes", baseRef: undefined },
69
+ }),
70
+ };
71
+ createGentleAiExtension({ nativeReviewCli: nativeReviewCli as NativeReviewCli })(pi);
72
+ return tools.get("gentle_review");
73
+ }
74
+
75
+ const ctx = { cwd: process.cwd() } as ExtensionContext;
76
+
77
+ test("gentle_review assess: an agent escalation raises a medium candidate to high and records why", async () => {
78
+ const tool = assessTool();
79
+ const input = JSON.stringify({ writerModelId: "openai/gpt-6.1-sol", writerEffort: "high", escalate: { item: 2, reason: "weakens the permission check in utils.ts" } });
80
+ const { details } = await tool.execute("esc-1", { operation: "assess", input }, undefined, undefined, ctx);
81
+ const plan = details as { risk: string; nativeRisk: string; agentEscalation: { item: number; label: string; reason: string }; plan: { independentVerifier: boolean } };
82
+ assert.equal(plan.nativeRisk, "medium");
83
+ assert.equal(plan.risk, VERIFICATION_TIER.HIGH);
84
+ assert.deepEqual(plan.agentEscalation, { item: 2, label: "security", reason: "weakens the permission check in utils.ts", applied: true });
85
+ assert.equal(plan.plan.independentVerifier, true, "a raised candidate always gets the independent verifier");
86
+ });
87
+
88
+ test("gentle_review assess: without an escalation the native tier stands", async () => {
89
+ const tool = assessTool();
90
+ const input = JSON.stringify({ writerModelId: "openai/gpt-6.1-sol", writerEffort: "high" });
91
+ const { details } = await tool.execute("esc-2", { operation: "assess", input }, undefined, undefined, ctx);
92
+ const plan = details as { risk: string; nativeRisk: string; agentEscalation?: unknown; plan: { independentVerifier: boolean } };
93
+ assert.equal(plan.risk, "medium");
94
+ assert.equal(plan.nativeRisk, "medium");
95
+ assert.equal(plan.agentEscalation, undefined);
96
+ assert.equal(plan.plan.independentVerifier, false);
97
+ });
98
+
99
+ test("gentle_review assess: a malformed escalation is rejected instead of ignored", async () => {
100
+ const tool = assessTool();
101
+ await assert.rejects(
102
+ () => tool.execute("esc-3", { operation: "assess", input: JSON.stringify({ escalate: { item: 9, reason: "x" } }) }, undefined, undefined, ctx),
103
+ /escalate/,
104
+ );
105
+ });
106
+
107
+ test("the rule text tells the agent when and how to escalate, and that it never lowers", async () => {
108
+ const { readFileSync } = await import("node:fs");
109
+ const verification = readFileSync(new URL("../assets/orchestrator-verification.md", import.meta.url), "utf8");
110
+ const tracking = readFileSync(new URL("../assets/orchestrator-tracking.md", import.meta.url), "utf8");
111
+ const extension = readFileSync(new URL("../extensions/gentle-ai.ts", import.meta.url), "utf8");
112
+ assert.match(verification, /"escalate": \{"item": <1-6>, "reason": "<one line>"\}/);
113
+ assert.match(verification, /never lowers a tier/);
114
+ assert.match(tracking, /assess `escalate` field \(`orchestrator-verification\.md`\); never lower one/);
115
+ assert.match(extension, /\{"escalate":\{"item":1-6,"reason":"<one line>"\}\}/);
116
+ });
117
+
118
+ test("gentle_review assess: agentEscalation says whether the escalation changed the tier", async () => {
119
+ const tool = assessTool();
120
+ const input = JSON.stringify({ escalate: { item: 4, reason: "reorders an async write" } });
121
+ const { details } = await tool.execute("esc-5", { operation: "assess", input }, undefined, undefined, ctx);
122
+ assert.equal((details as { agentEscalation: { applied: boolean } }).agentEscalation.applied, true, "medium raised to high is applied");
123
+ });
@@ -92,3 +92,20 @@ test("dropAll discards pending, consumed, and delivered state", () => {
92
92
  queue.enqueue(task("t1"), 5000);
93
93
  assert.deepEqual(queue.takeDeliverable(5000).map((entry) => entry.task.id), ["t1"], "the queue is fresh after dropAll");
94
94
  });
95
+
96
+ test("requeue returns a failed delivery once, keeps its settle time, and respects consume", () => {
97
+ const queue = createCompletionQueue<FakeTask>();
98
+ queue.enqueue(task("t1"), 1000);
99
+ queue.enqueue(task("t2"), 1000);
100
+ const [first, second] = queue.takeDeliverable(2000);
101
+ queue.requeue(first!);
102
+ queue.requeue(first!);
103
+ queue.requeue(second!);
104
+ queue.consume("t2");
105
+ const retried = queue.takeDeliverable(1000 + STALE_COMPLETION_MS);
106
+ assert.deepEqual(retried.map((entry) => entry.task.id), ["t1"], "a double requeue yields one entry and a consumed one is dropped");
107
+ assert.equal(retried[0]!.settledAt, 1000, "the original settle time is kept");
108
+ assert.equal(retried[0]!.stale, true, "a retried completion can become stale");
109
+ queue.enqueue(task("t1"), 5000);
110
+ assert.deepEqual(queue.takeDeliverable(5000), [], "a delivered retry is still delivered at most once");
111
+ });
@@ -3,6 +3,7 @@ import test from "node:test";
3
3
  import {
4
4
  applyTaskEvent,
5
5
  emptyThread,
6
+ MISSING_TOOLS_NOTE_PREFIX,
6
7
  normalizeRpcEvent,
7
8
  ToolArgumentProgress,
8
9
  TASK_EVENT,
@@ -13,6 +14,51 @@ import {
13
14
  type TaskRecord,
14
15
  } from "../lib/agents-protocol.ts";
15
16
 
17
+ test("status observers see only live transitions, remain isolated and cannot fail mutations", () => {
18
+ const store = new TaskStore();
19
+ const changes: Array<{ status: string; sequence: number; parentSessionId: string; producerId: string; runId: string }> = [];
20
+ store.subscribeStatusChanges(() => { throw new Error("listener failure"); });
21
+ store.subscribeStatusChanges(async () => { throw new Error("async failure"); });
22
+ const stop = store.subscribeStatusChanges(change => { changes.push(change); });
23
+ store.restore(record({ id: "restored" }), emptyThread());
24
+ store.add(record());
25
+ store.update("t1", { status: TASK_STATUS.RUNNING });
26
+ store.update("t1", { status: TASK_STATUS.RUNNING, label: "new label" });
27
+ const ask = { type: TASK_EVENT.ASK, request: { id: "q", method: "input", title: "Question" } } as const;
28
+ store.apply("t1", ask, 2);
29
+ store.apply("t1", ask, 3);
30
+ store.apply("t1", { type: TASK_EVENT.TEXT, text: "answer" }, 4);
31
+ store.apply("t1", { type: TASK_EVENT.TEXT, text: "more" }, 5);
32
+ store.add(record({ id: "other", parentSessionId: "s2" }));
33
+ assert.deepEqual(changes.map(change => change.status), ["queued", "running", "waiting", "running", "queued"]);
34
+ assert.deepEqual(changes.map(change => change.sequence), [1, 2, 3, 4, 5]);
35
+ assert.deepEqual(changes.map(change => change.parentSessionId), ["s1", "s1", "s1", "s1", "s2"]);
36
+ assert.equal(new Set(changes.slice(0, 4).map(change => change.runId)).size, 1);
37
+ assert.equal(store.get("t1")?.status, "running");
38
+ const another = new TaskStore();
39
+ let otherProducer: string | undefined;
40
+ another.subscribeStatusChanges(change => { otherProducer = change.producerId; });
41
+ another.add(record());
42
+ assert.notEqual(otherProducer, changes[0].producerId);
43
+ stop();
44
+ store.update("t1", { status: TASK_STATUS.COMPLETED });
45
+ assert.equal(changes.length, 5);
46
+ });
47
+
48
+ test("all terminal statuses produce exactly one transition and restore establishes a silent baseline", () => {
49
+ for (const status of [TASK_STATUS.COMPLETED, TASK_STATUS.FAILED, TASK_STATUS.CANCELLED, TASK_STATUS.TIMED_OUT]) {
50
+ const store = new TaskStore();
51
+ const statuses: string[] = [];
52
+ store.subscribeStatusChanges(change => { statuses.push(change.status); });
53
+ store.restore(record({ status: TASK_STATUS.RUNNING }), emptyThread());
54
+ store.update("t1", { status });
55
+ store.update("t1", { status, result: "report" });
56
+ store.add(record({ status }));
57
+ assert.deepEqual(statuses, [status]);
58
+ assert.equal(store.get("t1")?.status, status);
59
+ }
60
+ });
61
+
16
62
  // Gentle Agents protocol: the child pi process streams RPC events; the host
17
63
  // normalizes them into small typed deltas, applies them to an append-only
18
64
  // thread, and notifies only the listeners of the task that changed.
@@ -68,6 +114,17 @@ test("normalizeRpcEvent maps pi RPC events to task deltas and ignores the rest",
68
114
  assert.deepEqual(normalizeRpcEvent("garbage"), []);
69
115
  });
70
116
 
117
+ // #1690: Pi drops unknown --tools names silently, so the child reports them
118
+ // with one marked notify; only that notify reaches the task thread.
119
+ test("normalizeRpcEvent turns the marked missing-tools notify into a note and drops every other notify", () => {
120
+ const message = `${MISSING_TOOLS_NOTE_PREFIX} gentle_review_scope, codegraph`;
121
+ assert.deepEqual(normalizeRpcEvent({ type: "extension_ui_request", id: "u1", method: "notify", message, notifyType: "warning" }), [{ type: TASK_EVENT.NOTE, text: message }]);
122
+ assert.deepEqual(normalizeRpcEvent({ type: "extension_ui_request", id: "u2", method: "notify", message: "requested tools missing in child: x" }), [], "an unmarked notify stays dropped");
123
+ assert.deepEqual(normalizeRpcEvent({ type: "extension_ui_request", id: "u3", method: "notify", message: ` ${MISSING_TOOLS_NOTE_PREFIX} x` }), [], "the marker must be a prefix");
124
+ assert.deepEqual(normalizeRpcEvent({ type: "extension_ui_request", id: "u4", method: "setStatus", statusKey: "k", statusText: `${MISSING_TOOLS_NOTE_PREFIX} x` }), [], "only notify carries the marker");
125
+ assert.deepEqual(normalizeRpcEvent({ type: "extension_ui_request", id: "u5", method: "notify", message: `${MISSING_TOOLS_NOTE_PREFIX} \u001B[2Jx` }), [{ type: TASK_EVENT.NOTE, text: `${MISSING_TOOLS_NOTE_PREFIX} x` }], "child text is sanitized");
126
+ });
127
+
71
128
  test("argument liveness accepts current and legacy Pi starts without display or usage deltas", () => {
72
129
  for (const start of [{ type: "toolcall_start", contentIndex: 0, id: "c1", toolName: "write" },
73
130
  { type: "toolcall_start", contentIndex: 0, partial: { content: [{ type: "toolCall", id: "c1", name: "write", arguments: {} }] } }]) {
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
- import { AGENT_MODE, type AgentDefinition } from "../lib/agents-config.ts";
3
+ import { AGENT_MODE, type AgentDefinition, type AgentMode } from "../lib/agents-config.ts";
4
4
  import { CHILD_MESSAGE_MAX_BYTES, ChildMessenger } from "../lib/agents-messaging.ts";
5
5
  import { TaskStore } from "../lib/agents-protocol.ts";
6
6
  import { AgentRunner } from "../lib/agents-runner.ts";
@@ -8,7 +8,7 @@ import { fakeChild } from "./agents-fake-child.ts";
8
8
 
9
9
  const tick = () => new Promise((resolve) => setImmediate(resolve));
10
10
  const agent: AgentDefinition = { name: "worker", description: "", filePath: "/a.md", scope: "global", instructions: "", model: undefined, thinking: undefined, mode: undefined, tools: ["read"] };
11
- const request = (mode = AGENT_MODE.BACKGROUND) => ({ agent, prompt: "work", label: undefined, context: undefined, mode, cwd: "/repo", parentSessionId: "s1", model: undefined, thinking: undefined, sessionDir: "/sessions", resumeSessionPath: undefined, env: {} });
11
+ const request = (mode: AgentMode = AGENT_MODE.BACKGROUND) => ({ agent, prompt: "work", label: undefined, context: undefined, mode, cwd: "/repo", parentSessionId: "s1", model: undefined, thinking: undefined, sessionDir: "/sessions", resumeSessionPath: undefined, env: {} });
12
12
 
13
13
  test("ChildMessenger validates query replies and settles timeout, callback failure, and disconnect once", async () => {
14
14
  const listeners = new Map<string, Array<(value: Record<string, unknown>) => void>>();
@@ -74,7 +74,7 @@ test("ChildMessenger rejects an empty reply error and ignores mixed reply fields
74
74
 
75
75
  test("runner sends bounded static query rejection frames that settle the child before timeout", async () => {
76
76
  const child = fakeChild();
77
- const listeners = new Map<string, Array<(value: Record<string, unknown>) => void>>();
77
+ const listeners = new Map<string, Array<(value: unknown) => void>>();
78
78
  const timers: Array<{ cancelled: boolean; fn: () => void }> = [];
79
79
  child.child.send = (frame, done) => {
80
80
  child.sent.push(frame);
@@ -83,8 +83,8 @@ test("runner sends bounded static query rejection frames that settle the child b
83
83
  return true;
84
84
  };
85
85
  const messenger = new ChildMessenger({
86
- send: (frame) => { child.message(frame); return true; },
87
- on: (event, listener) => listeners.set(event, [...(listeners.get(event) ?? []), listener as (value: Record<string, unknown>) => void]),
86
+ send: (frame: Record<string, unknown>) => { child.message(frame); return true; },
87
+ on: (event, listener) => listeners.set(event, [...(listeners.get(event) ?? []), listener]),
88
88
  }, (fn) => {
89
89
  const timer = { cancelled: false, fn };
90
90
  timers.push(timer);
@@ -102,7 +102,7 @@ test("runner sends bounded static query rejection frames that settle the child b
102
102
 
103
103
  test("a malformed query with a valid q ID gets a reply that clears the matching child query", async () => {
104
104
  const child = fakeChild();
105
- const listeners = new Map<string, Array<(value: Record<string, unknown>) => void>>();
105
+ const listeners = new Map<string, Array<(value: unknown) => void>>();
106
106
  const timers: Array<{ cancelled: boolean; fn: () => void }> = [];
107
107
  child.child.send = (frame, done) => {
108
108
  child.sent.push(frame);
@@ -111,8 +111,8 @@ test("a malformed query with a valid q ID gets a reply that clears the matching
111
111
  return true;
112
112
  };
113
113
  const messenger = new ChildMessenger({
114
- send: (frame) => { child.message(frame); return true; },
115
- on: (event, listener) => listeners.set(event, [...(listeners.get(event) ?? []), listener as (value: Record<string, unknown>) => void]),
114
+ send: (frame: Record<string, unknown>) => { child.message(frame); return true; },
115
+ on: (event, listener) => listeners.set(event, [...(listeners.get(event) ?? []), listener]),
116
116
  }, (fn) => {
117
117
  const timer = { cancelled: false, fn };
118
118
  timers.push(timer);
@@ -133,7 +133,7 @@ test("background queries keep correlation, bounds, and ownership before replying
133
133
  const child = fakeChild();
134
134
  const queries: Array<{ taskId: string; requestId: string; message: string }> = [];
135
135
  const runner = new AgentRunner(new TaskStore(), { maxConcurrency: 1, stallTimeoutMs: 10_000 }, { spawn: () => child.child, now: () => 1, schedule: () => () => {}, pi: { command: "pi", args: [] } }, {
136
- askUser: async () => ({ cancelled: true }), onQuery: (task, requestId, message) => queries.push({ taskId: task.id, requestId, message }),
136
+ askUser: async () => ({ cancelled: true }), onQuery: (task, requestId, message) => { queries.push({ taskId: task.id, requestId, message }); },
137
137
  });
138
138
  const task = runner.run(request());
139
139
  await tick();
@@ -5,6 +5,7 @@ import test from "node:test";
5
5
  import { AGENT_MODE, type AgentDefinition } from "../lib/agents-config.ts";
6
6
  import { AgentRunner, type ChildLike, type RunnerDeps, type TaskRequest } from "../lib/agents-runner.ts";
7
7
  import { TASK_STATUS, TaskStore } from "../lib/agents-protocol.ts";
8
+ import { fakeChild } from "./agents-fake-child.ts";
8
9
 
9
10
  const fixture = fileURLToPath(new URL("./fixtures/agents-process-child.mjs", import.meta.url));
10
11
  const agent: AgentDefinition = { name: "process", description: "test", filePath: "/test.md", scope: "global", instructions: "", model: undefined, thinking: undefined, mode: undefined, tools: [] };
@@ -18,6 +19,36 @@ const waitFor = async (predicate: () => boolean, timeoutMs = 10_000): Promise<vo
18
19
  }
19
20
  };
20
21
 
22
+ test("missing RPC streams fail the task without announcing a launch or leaking its queue slot", async () => {
23
+ const store = new TaskStore();
24
+ const child = fakeChild();
25
+ let launched = false;
26
+ const runner = new AgentRunner(store, { maxConcurrency: 1, stallTimeoutMs: 1000 }, {
27
+ spawn: () => ({ ...child.child, stdin: null, stdout: null }), now: Date.now,
28
+ schedule: (fn, ms) => { const timer = setTimeout(fn, ms); return () => clearTimeout(timer); },
29
+ pi: { command: "pi", args: [] },
30
+ }, { askUser: async () => ({ cancelled: true }) });
31
+ const task = runner.run({ ...request("missing streams"), onLaunch: () => { launched = true; } });
32
+ await waitFor(() => store.get(task.id)?.status === TASK_STATUS.FAILED);
33
+ assert.equal(launched, false);
34
+ assert.match(store.get(task.id)!.error!, /RPC streams/);
35
+ assert.ok(child.killed.length > 0, "the malformed child is still stopped");
36
+ });
37
+
38
+ test("a real failed OS spawn has no pid and is retained as a launch failure", async () => {
39
+ const store = new TaskStore();
40
+ let launched = false;
41
+ const runner = new AgentRunner(store, { maxConcurrency: 1, stallTimeoutMs: 1000 }, {
42
+ spawn: (_command, _args, options) => nodeSpawn("/nonexistent/gentle-pi-child", [], { ...options, stdio: ["pipe", "pipe", "pipe", "ipc"] }),
43
+ now: Date.now, schedule: (fn, ms) => { const timer = setTimeout(fn, ms); return () => clearTimeout(timer); },
44
+ pi: { command: "unused", args: [] },
45
+ }, { askUser: async () => ({ cancelled: true }) });
46
+ const task = runner.run({ ...request("failed spawn"), onLaunch: () => { launched = true; } });
47
+ await waitFor(() => store.get(task.id)?.status === TASK_STATUS.FAILED);
48
+ assert.equal(launched, false);
49
+ assert.match(store.get(task.id)!.error!, /ENOENT/);
50
+ });
51
+
21
52
  test("POSIX cleanup retains queue slots when a leader exits but its TERM-resisting descendant remains", { skip: process.platform === "win32" }, async () => {
22
53
  const store = new TaskStore();
23
54
  let launches = 0;
@@ -5,8 +5,8 @@ import { syncBuiltinESMExports } from "node:module";
5
5
  import { basename, dirname } from "node:path";
6
6
  import { PassThrough } from "node:stream";
7
7
  import { AGENT_MODE, parseAgentsConfig, resolveAgentProfile, type AgentDefinition } from "../lib/agents-config.ts";
8
- import { TASK_STATUS, TaskStore, type TaskRecord } from "../lib/agents-protocol.ts";
9
- 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";
10
10
  import { fakeChild, type FakeChild } from "./agents-fake-child.ts";
11
11
  import { INTERACTIVE_HOST_ENV } from "../lib/rpc-host.ts";
12
12
 
@@ -103,6 +103,25 @@ function beginArguments(child: FakeChild, timestamp = 1000): void {
103
103
  child.emit(argumentUpdate("toolcall_start", { id: "call-1", toolName: "write" }));
104
104
  }
105
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
+
106
125
  test("fresh argument streaming renews idle liveness without execution or provisional usage", async () => {
107
126
  const h = harness({ stallTimeoutMs: 100, toolStallTimeoutMs: 1000 });
108
127
  const task = h.runner.run(request());
@@ -712,6 +731,70 @@ test("childArguments grants every child the notification-only parent message too
712
731
  assert.equal(args[args.indexOf("--tools") + 1], "read,grep,subagent_parent_message");
713
732
  });
714
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
+
715
798
  test("AgentRunner admits strict live notifications once and closes IPC before Stop", async () => {
716
799
  const notifications: string[] = [];
717
800
  const { runner, children, spawnOptions } = harness({ onNotification: (task, message) => task.parentSessionId === "s1" && (notifications.push(message), true) });
@@ -949,7 +1032,7 @@ for (const [platform, detached] of [["win32", false], ["linux", true]] as const)
949
1032
  assert.deepEqual(launches, [{
950
1033
  command: "pi-fixture",
951
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."],
952
- 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"] },
953
1036
  }]);
954
1037
  child.emit({ type: "agent_end", messages: [{ role: "assistant", content: [{ type: "text", text: "platform checked" }], stopReason: "stop" }] });
955
1038
  child.emit({ type: "agent_settled" });
@@ -1032,7 +1115,7 @@ test("AgentRunner retains permission broker fd3 and assigns messaging IPC to fd4
1032
1115
  const launch = spawnOptions[0];
1033
1116
  const permissionChannelStdio = process.platform === "win32" ? "overlapped" : "pipe";
1034
1117
  assert.match(launch?.env.GENTLE_PI_AGENTS_OWNED_IPC ?? "", /^\d+-[a-z0-9]+$/, "the owned-IPC marker has the runner's opaque shape");
1035
- 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" });
1036
1119
  assert.deepEqual(launch?.stdio, ["pipe", "pipe", "pipe", permissionChannelStdio, "ipc"]);
1037
1120
  assert.equal(launch?.stdio?.length, 5);
1038
1121
  children[0].emit({ type: "agent_end", messages: [{ role: "assistant", content: [{ type: "text", text: "channel checked" }], stopReason: "stop" }] });
@@ -1059,6 +1142,8 @@ test("AgentRunner platform matrix scopes permission fd3 transport", async () =>
1059
1142
  await tick();
1060
1143
  const launch = launches[0];
1061
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");
1062
1147
  assert.equal(launch.env.GENTLE_PI_AGENTS_PARENT_PERMISSION_FD, eligible ? "3" : undefined, "only eligible children receive the fd3 marker");
1063
1148
  assert.deepEqual(launch.stdio, eligible ? ["pipe", "pipe", "pipe", platform === "win32" ? "overlapped" : "pipe", "ipc"] : ["pipe", "pipe", "pipe", "ipc"]);
1064
1149
  assert.equal(launch.stdio?.indexOf("ipc"), eligible ? 4 : 3, "messaging IPC follows fd3 only for eligible children");
@@ -115,8 +115,25 @@ test("both extensions land their block in appendSystemPrompt on one shared optio
115
115
 
116
116
  const appended = event.systemPromptOptions.appendSystemPrompt;
117
117
  assert.match(appended, /el Gentleman Identity and Harness/);
118
+ const continuityStart = appended.indexOf("## Authorized work continuity");
119
+ const coreStart = appended.indexOf("# el Gentleman Orchestrator");
120
+ assert.ok(continuityStart >= 0 && continuityStart < coreStart,
121
+ "continuity must be always injected in the identity/harness, outside the budgeted core");
122
+ assert.equal(appended.split("## Authorized work continuity").length - 1, 1);
123
+ const continuity = appended.slice(continuityStart, coreStart);
124
+ for (const clause of [
125
+ "necessary in-scope implementation, documentation and verification steps",
126
+ "Never end a turn merely announcing an immediately executable authorized next step; execute it instead",
127
+ "required explicit consent, a denied action, unavailable required proof, or the human's explicit stop",
128
+ "Do not infer permission for publishing, destructive actions, unrelated scope or native consent",
129
+ "Report a concrete blocker and the exact decision needed",
130
+ "launch the bounded action and end the turn when no independent in-scope work remains",
131
+ "Retain its identity and resume the already-authorized continuation automatically on its completion event",
132
+ "do not ask the human to say \"continue\", poll, or claim completion while checks remain pending",
133
+ ]) assert.ok(continuity.includes(clause), `missing continuity clause: ${clause}`);
118
134
  assert.match(appended, /## Todo list/);
119
- assert.match(appended, /1\. \[pending\] Fix the bug/);
135
+ // gentle-shell#1662: the list itself travels as a request-local message, never in the prompt.
136
+ assert.doesNotMatch(appended, /Fix the bug/);
120
137
  assert.ok(
121
138
  appended.indexOf("el Gentleman Identity and Harness") < appended.indexOf("## Todo list"),
122
139
  "gentle-ai's block must precede gentle-todo's, matching handler registration order",