gentle-pi 4.2.0 → 4.4.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 (334) hide show
  1. package/README.md +340 -138
  2. package/assets/agents/gentle-ai-explore.md +1 -0
  3. package/assets/agents/gentle-ai-verify.md +6 -4
  4. package/assets/agents/gentle-ai-worker.md +7 -1
  5. package/assets/design-canvas/canvas.css +648 -0
  6. package/assets/design-canvas/canvas.js +1497 -0
  7. package/assets/design-canvas/index.html +88 -0
  8. package/assets/design-canvas/inspector.js +290 -0
  9. package/assets/orchestrator-delegation-reference.md +163 -0
  10. package/assets/orchestrator-delegation.md +25 -176
  11. package/assets/orchestrator-tracking.md +1 -1
  12. package/assets/orchestrator-verification.md +14 -8
  13. package/assets/orchestrator-writer.md +5 -1
  14. package/assets/orchestrator.md +11 -11
  15. package/bin/engram-guard +1 -1
  16. package/bin/gentle-shell.mjs +237 -139
  17. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  18. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
  19. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
  20. package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
  21. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
  22. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
  23. package/docs/assets/diagrams/agent-orchestration.svg +54 -15
  24. package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
  25. package/docs/assets/diagrams/measured-routing.svg +49 -0
  26. package/docs/assets/diagrams/native-review.svg +52 -12
  27. package/docs/assets/diagrams/odd-workflow.svg +128 -66
  28. package/docs/assets/diagrams/permission-modes.svg +51 -0
  29. package/docs/assets/features/audio-notifications.png +0 -0
  30. package/docs/assets/features/command-palette.png +0 -0
  31. package/docs/bundled-install.md +65 -30
  32. package/docs/delegated-verification.md +16 -8
  33. package/docs/gentle-agents-activity.md +16 -6
  34. package/docs/gentle-music.md +61 -0
  35. package/docs/gentle-shell-uninstall.md +8 -1
  36. package/docs/gentle-shell.md +80 -9
  37. package/docs/install-wizard.md +37 -6
  38. package/docs/permission-modes.md +8 -3
  39. package/docs/readme-reference.md +120 -28
  40. package/docs/session-profile-format.md +26 -0
  41. package/docs/voice.md +259 -0
  42. package/docs/yolo-mode.md +38 -10
  43. package/extensions/ask-user-choice.ts +4 -1
  44. package/extensions/codegraph-tools.ts +68 -6
  45. package/extensions/gentle-agents.ts +270 -48
  46. package/extensions/gentle-ai.ts +490 -113
  47. package/extensions/gentle-design.ts +158 -0
  48. package/extensions/gentle-herdr-activity.ts +2 -3
  49. package/extensions/gentle-music.ts +289 -0
  50. package/extensions/gentle-shell.ts +359 -85
  51. package/extensions/gentle-todo.ts +43 -9
  52. package/extensions/gentle-voice.ts +11 -0
  53. package/extensions/quiet-tools.ts +1 -1
  54. package/extensions/session-scratch.ts +384 -0
  55. package/extensions/skill-registry.ts +63 -8
  56. package/extensions/startup-banner.ts +2 -3
  57. package/lib/agent-home.ts +8 -3
  58. package/lib/agent-profiles.ts +26 -0
  59. package/lib/agents-config.ts +35 -0
  60. package/lib/agents-protocol.ts +60 -1
  61. package/lib/agents-runner.ts +107 -9
  62. package/lib/agents-session-transport.ts +101 -6
  63. package/lib/agents-view.ts +3 -1
  64. package/lib/agents-widget.ts +14 -1
  65. package/lib/auto-permission-memory.ts +48 -18
  66. package/lib/auto-permission-runtime.ts +21 -6
  67. package/lib/bounded-writer-admission.ts +51 -0
  68. package/lib/clipboard-image.ts +147 -0
  69. package/lib/command-palette-catalog.ts +2 -4
  70. package/lib/commit-slice-signal.ts +100 -0
  71. package/lib/context-backstop.ts +120 -0
  72. package/lib/delivered-test-lint.ts +80 -0
  73. package/lib/design-canvas-server.ts +947 -0
  74. package/lib/design-comments.ts +437 -0
  75. package/lib/design-handoffs.ts +313 -0
  76. package/lib/design-open-browser.ts +58 -0
  77. package/lib/design-sources.ts +120 -0
  78. package/lib/destructive-command-guard.ts +120 -9
  79. package/lib/external-editor.ts +31 -0
  80. package/lib/gauge-theme.ts +146 -0
  81. package/lib/gentle-ai-binary.ts +28 -7
  82. package/lib/gentle-design.ts +513 -0
  83. package/lib/gentle-shell-bundled.ts +146 -36
  84. package/lib/gentle-shell-import-pi.ts +233 -0
  85. package/lib/gentle-shell-launcher.ts +154 -15
  86. package/lib/gentle-shell-startup-update.ts +61 -9
  87. package/lib/gentle-shell-uninstall.ts +51 -2
  88. package/lib/herdr-activity.ts +24 -0
  89. package/lib/image-attachments.ts +150 -0
  90. package/lib/inprocess-reviewer.ts +33 -2
  91. package/lib/nan-provider.ts +3 -0
  92. package/lib/native-review-cli.ts +20 -5
  93. package/lib/notification-audio-native.ts +149 -13
  94. package/lib/notification-audio.ts +4 -0
  95. package/lib/notification-pulse-worker.ts +82 -18
  96. package/lib/notification-service.ts +5 -1
  97. package/lib/orchestrator-discovery.ts +5 -1
  98. package/lib/orchestrator-presence.ts +87 -8
  99. package/lib/orchestrator-scope.ts +31 -3
  100. package/lib/orchestrator-state.ts +11 -3
  101. package/lib/permission-mode-policy.ts +2 -2
  102. package/lib/provider-contract-bundle.ts +99 -53
  103. package/lib/review-candidate-view-owner.ts +29 -19
  104. package/lib/review-candidate-view.ts +36 -2
  105. package/lib/review-host-relay.ts +11 -2
  106. package/lib/review-reminder-receipt.ts +24 -1
  107. package/lib/review-risk-assessment.ts +43 -38
  108. package/lib/review-session-standing-permission.ts +3 -1
  109. package/lib/session-profile-append-controller.ts +713 -0
  110. package/lib/session-profile-disk-reader.ts +182 -0
  111. package/lib/session-profile-freeze.ts +30 -1
  112. package/lib/session-profile-persistence.ts +15 -7
  113. package/lib/session-scratch.ts +523 -0
  114. package/lib/session-worktree-registry.ts +7 -0
  115. package/lib/shell-bar.ts +29 -28
  116. package/lib/shell-changes-view.ts +80 -13
  117. package/lib/shell-changes.ts +81 -5
  118. package/lib/shell-gauge.ts +49 -6
  119. package/lib/shell-prompt.ts +4 -4
  120. package/lib/shell-sidebar-layout.ts +26 -6
  121. package/lib/shell-todo.ts +24 -5
  122. package/lib/shell-usage-view.ts +5 -1
  123. package/lib/shell-usage.ts +7 -7
  124. package/lib/verifier-admission.ts +30 -0
  125. package/lib/vim-editor-adapter.ts +11 -8
  126. package/lib/voice-artifacts.ts +188 -0
  127. package/lib/voice-command.ts +576 -0
  128. package/lib/voice-install.ts +455 -0
  129. package/lib/voice-platform.ts +99 -0
  130. package/lib/voice-push-to-talk.ts +262 -0
  131. package/lib/voice-session.ts +460 -0
  132. package/lib/voice-settings.ts +50 -0
  133. package/lib/voice-ui.ts +158 -0
  134. package/lib/yolo-session-policy.ts +58 -22
  135. package/lib/yolo-temp-scratch.ts +69 -0
  136. package/package.json +1 -2
  137. package/runtime/gentle-ai-binary.mjs +28 -7
  138. package/runtime/gentle-shell-bundled.mjs +145 -35
  139. package/runtime/gentle-shell-import-pi.mjs +234 -0
  140. package/runtime/gentle-shell-launcher.mjs +152 -13
  141. package/runtime/gentle-shell-startup-update.mjs +61 -9
  142. package/runtime/gentle-shell-uninstall.mjs +51 -2
  143. package/runtime/native-review-cli.mjs +19 -4
  144. package/runtime/review-risk-assessment.mjs +43 -38
  145. package/scripts/build-runtime-modules.mjs +1 -0
  146. package/scripts/bundled-engram-migration.mjs +119 -0
  147. package/scripts/gentle-ai-installer.mjs +10 -10
  148. package/scripts/installer-downloads.mjs +7 -7
  149. package/scripts/installer-preflight.mjs +5 -3
  150. package/scripts/installer-server.mjs +14 -4
  151. package/scripts/installer-windows.mjs +37 -7
  152. package/scripts/main-channel.mjs +94 -19
  153. package/scripts/pi-update.mjs +34 -6
  154. package/scripts/run-test-suite.mjs +5 -0
  155. package/scripts/verify-package-files.mjs +43 -13
  156. package/scripts/voice-worker.cjs +476 -0
  157. package/skills/design/SKILL.md +110 -0
  158. package/skills/gentle-ai/SKILL.md +3 -3
  159. package/skills/work-unit-commits/SKILL.md +5 -3
  160. package/tests/agent-profiles.test.ts +24 -0
  161. package/tests/agent-risk-escalation.test.ts +37 -10
  162. package/tests/agents-config.test.ts +61 -0
  163. package/tests/agents-fake-child.ts +8 -1
  164. package/tests/agents-protocol.test.ts +32 -0
  165. package/tests/agents-runner.test.ts +485 -7
  166. package/tests/agents-session-transport.test.ts +53 -3
  167. package/tests/ask-user-choice.test.ts +30 -0
  168. package/tests/auto-mode.test.ts +17 -0
  169. package/tests/auto-permission-memory-diagnostics.test.ts +56 -0
  170. package/tests/auto-permission-runtime.test.ts +85 -1
  171. package/tests/autonomous-guard.test.ts +142 -0
  172. package/tests/bounded-writer-admission.test.ts +11 -3
  173. package/tests/bundled-install.test.ts +3 -3
  174. package/tests/card-content-policy.test.ts +19 -9
  175. package/tests/child-safety.test.ts +39 -0
  176. package/tests/clipboard-image.test.ts +114 -0
  177. package/tests/codegraph-tools.test.ts +123 -2
  178. package/tests/command-palette.test.ts +15 -5
  179. package/tests/commit-slice-signal.test.ts +162 -0
  180. package/tests/context-backstop.test.ts +225 -0
  181. package/tests/delegate-for-reason-contract.test.ts +92 -24
  182. package/tests/delivered-test-lint.test.ts +105 -0
  183. package/tests/design-canvas-assets.test.ts +710 -0
  184. package/tests/design-canvas-server.test.ts +1096 -0
  185. package/tests/design-comments.test.ts +201 -0
  186. package/tests/design-handoffs.test.ts +282 -0
  187. package/tests/design-open-browser.test.ts +70 -0
  188. package/tests/design-sources.test.ts +132 -0
  189. package/tests/destructive-command-guard.test.ts +85 -1
  190. package/tests/engram-safety.test.ts +132 -57
  191. package/tests/external-editor.test.ts +84 -0
  192. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
  193. package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
  194. package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
  195. package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
  196. package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
  197. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
  198. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
  199. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
  200. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
  201. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
  202. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
  203. package/tests/gauge-theme.test.ts +120 -0
  204. package/tests/gentle-agents.test.ts +645 -21
  205. package/tests/gentle-ai-binary.test.ts +1 -1
  206. package/tests/gentle-ai-child-guards.test.ts +61 -0
  207. package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
  208. package/tests/gentle-ai-installer.test.ts +51 -51
  209. package/tests/gentle-ai.test.ts +292 -13
  210. package/tests/gentle-design.test.ts +1313 -0
  211. package/tests/gentle-music.test.ts +335 -0
  212. package/tests/gentle-shell-bin.test.ts +163 -0
  213. package/tests/gentle-shell-bundled.test.ts +392 -176
  214. package/tests/gentle-shell-import-pi.test.ts +262 -0
  215. package/tests/gentle-shell-launcher.test.ts +107 -2
  216. package/tests/gentle-shell-startup-update.test.ts +77 -1
  217. package/tests/gentle-shell-uninstall.test.ts +89 -0
  218. package/tests/gentle-shell.test.ts +225 -17
  219. package/tests/gentle-todo.test.ts +190 -0
  220. package/tests/headless-delivery-question.test.ts +58 -0
  221. package/tests/herdr-activity.test.ts +34 -1
  222. package/tests/image-attachments.test.ts +119 -0
  223. package/tests/image-paste-chips.test.ts +236 -0
  224. package/tests/inprocess-reviewer.test.ts +55 -2
  225. package/tests/installer-bundles.test.ts +12 -6
  226. package/tests/installer-posix-bootstrap.test.ts +8 -8
  227. package/tests/installer-preflight.test.ts +16 -0
  228. package/tests/installer-server.test.ts +10 -2
  229. package/tests/installer-windows-bootstrap.test.ts +94 -7
  230. package/tests/installer-windows-diagnostics.test.ts +111 -0
  231. package/tests/main-channel-upgrade.test.ts +232 -2
  232. package/tests/main-channel.test.ts +140 -21
  233. package/tests/nan-provider.test.ts +36 -0
  234. package/tests/native-review-capability-contract.test.ts +17 -4
  235. package/tests/native-review-parity.test.ts +23 -2
  236. package/tests/no-self-pause-contract.test.ts +5 -2
  237. package/tests/notification-audio-native.test.ts +128 -9
  238. package/tests/notification-customize.test.ts +17 -5
  239. package/tests/odd-integration.test.ts +5 -1
  240. package/tests/odd-routing-canonical-ratchet.test.ts +128 -162
  241. package/tests/odd-routing-contract.test.ts +115 -30
  242. package/tests/orchestrator-budget.test.ts +22 -8
  243. package/tests/orchestrator-discovery.test.ts +29 -0
  244. package/tests/orchestrator-presence.test.ts +100 -2
  245. package/tests/orchestrator-rdd-ownership.test.ts +3 -1
  246. package/tests/orchestrator-scope.test.ts +80 -0
  247. package/tests/orchestrator-state.test.ts +29 -0
  248. package/tests/orchestrator-status-publication-contract.test.ts +12 -8
  249. package/tests/package-files.test.ts +34 -0
  250. package/tests/package-manifest.test.ts +24 -20
  251. package/tests/permission-mode-policy.test.ts +10 -1
  252. package/tests/pi-update-launcher.test.ts +21 -2
  253. package/tests/pi-update.test.ts +99 -4
  254. package/tests/primary-session-prompt.test.ts +101 -0
  255. package/tests/provider-contract-bundle.test.ts +118 -0
  256. package/tests/provider-contract-mirror.test.ts +24 -0
  257. package/tests/quiet-tool-rendering.test.ts +23 -0
  258. package/tests/rdd-aware-verification-contract.test.ts +29 -11
  259. package/tests/rdd-status-line.test.ts +79 -0
  260. package/tests/review-agent-end-preflight.test.ts +6 -0
  261. package/tests/review-candidate-view.test.ts +49 -0
  262. package/tests/review-contract-prompt.test.ts +5 -2
  263. package/tests/review-controller-native-routing.test.ts +14 -1
  264. package/tests/review-controller.test.ts +1 -1
  265. package/tests/review-host-relay-routing.test.ts +147 -3
  266. package/tests/review-host-relay.test.ts +9 -2
  267. package/tests/review-json-arguments.test.ts +20 -3
  268. package/tests/review-loadout-rdd-off-sdk.test.ts +258 -0
  269. package/tests/review-owner-identity.test.ts +132 -0
  270. package/tests/review-relay-transport-agent.test.ts +26 -0
  271. package/tests/review-reminder-receipt.test.ts +31 -1
  272. package/tests/review-risk-assessment.test.ts +163 -76
  273. package/tests/review-session-standing-permission-controller.test.ts +14 -1
  274. package/tests/review-session-standing-permission.test.ts +27 -1
  275. package/tests/run-test-suite.test.ts +15 -0
  276. package/tests/runtime-harness.mjs +16 -6
  277. package/tests/session-changes-shell.test.ts +1 -1
  278. package/tests/session-changes.test.ts +2 -1
  279. package/tests/session-profile-append-controller-fork.test.ts +458 -0
  280. package/tests/session-profile-append-controller-pi.test.ts +290 -0
  281. package/tests/session-profile-append-controller.test.ts +731 -0
  282. package/tests/session-profile-disk-reader-pi.test.ts +183 -0
  283. package/tests/session-profile-disk-reader.test.ts +311 -0
  284. package/tests/session-profile-freeze.test.ts +23 -0
  285. package/tests/session-scratch.test.ts +1011 -0
  286. package/tests/session-subject-routing-contract.test.ts +2 -1
  287. package/tests/session-worktree-registry.test.ts +13 -1
  288. package/tests/shell-bar.test.ts +21 -2
  289. package/tests/shell-changes-view.test.ts +137 -1
  290. package/tests/shell-changes.test.ts +148 -1
  291. package/tests/shell-gauge.test.ts +42 -0
  292. package/tests/shell-prompt.test.ts +3 -3
  293. package/tests/shell-render-cache.test.ts +134 -1
  294. package/tests/shell-sidebar-layout.test.ts +50 -0
  295. package/tests/short-check-routing-contract.test.ts +1 -1
  296. package/tests/skill-collision-prefixes.test.ts +3 -1
  297. package/tests/skill-registry-watcher.test.ts +17 -5
  298. package/tests/skill-registry.test.ts +47 -3
  299. package/tests/small-task-inline-contract.test.ts +37 -0
  300. package/tests/standalone-config-home.test.ts +324 -0
  301. package/tests/startup-banner.test.ts +5 -4
  302. package/tests/startup-home-repo.test.ts +34 -0
  303. package/tests/support/orchestrator-modules.ts +1 -0
  304. package/tests/task-size-routing-contract.test.ts +70 -10
  305. package/tests/test-scratch-contract.test.ts +102 -0
  306. package/tests/test-strength-contract.test.ts +36 -1
  307. package/tests/tool-batching-contract.test.ts +64 -0
  308. package/tests/verification-proportionality.test.ts +46 -0
  309. package/tests/verifier-admission.test.ts +154 -0
  310. package/tests/verify-agent-contract.test.ts +8 -8
  311. package/tests/voice-command.test.ts +773 -0
  312. package/tests/voice-install.test.ts +323 -0
  313. package/tests/voice-platform.test.ts +176 -0
  314. package/tests/voice-push-to-talk.test.ts +444 -0
  315. package/tests/voice-session.test.ts +434 -0
  316. package/tests/voice-settings.test.ts +49 -0
  317. package/tests/voice-ui.test.ts +97 -0
  318. package/tests/voice-worker.test.ts +445 -0
  319. package/tests/weak-model-economy-prompt.test.ts +32 -0
  320. package/tests/windows-hidden-processes.test.ts +30 -2
  321. package/tests/writer-edit-surface-scope.test.ts +3 -0
  322. package/tests/writer-feature-document-admission.test.ts +156 -0
  323. package/tests/yolo-customize.test.ts +5 -1
  324. package/tests/yolo-mode.test.ts +19 -1
  325. package/tests/yolo-session-policy-capture.test.ts +104 -0
  326. package/tests/yolo-temp-scratch.test.ts +71 -0
  327. package/scripts/mirror-odd-routing.mjs +0 -242
  328. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
  329. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
  330. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
  331. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  332. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
  333. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
  334. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -168,3 +168,37 @@ test("startup returns before the native review negotiation finishes, and the fir
168
168
  await __testing.startupNegotiationSettled();
169
169
  assert.equal(calls.target, 1, "the negotiation still completes after startup");
170
170
  });
171
+
172
+ // gentle-shell#2195: the RDD line sits in the cached system-prompt prefix, so a
173
+ // session keeps the line it first rendered and re-renders only when RDD turns
174
+ // off or back on.
175
+ test("a session keeps its first RDD status line until the off state changes", async (t) => {
176
+ const { cwd, environment } = fixture(t, false);
177
+ let status: { effective: string; source: string } | undefined;
178
+ const cli = {
179
+ reviewMode: async () => {
180
+ if (status === undefined) throw new Error("native review CLI unavailable");
181
+ return { operation: "status", scope: "clone", status: { global: "", cloneLocal: status.effective, ...status } };
182
+ },
183
+ } as unknown as NativeReviewCli;
184
+ const { handlers, ctx } = start(cli, cwd, environment);
185
+ const line = async () => {
186
+ __testing.clearRddStatusMemoForTesting();
187
+ const event = { systemPrompt: "base", systemPromptOptions: { appendSystemPrompt: "" } };
188
+ await handlers.get("before_agent_start")!(event, ctx);
189
+ return /Receipt-driven development: [^;\n]*/.exec(event.systemPromptOptions.appendSystemPrompt)?.[0];
190
+ };
191
+ await handlers.get("session_shutdown")!({}, ctx);
192
+ assert.equal(await line(), "Receipt-driven development: unknown (native status unavailable)");
193
+ status = { effective: "on", source: "clone_local" };
194
+ assert.equal(await line(), "Receipt-driven development: unknown (native status unavailable)", "unknown to on keeps the prompt prefix");
195
+ status = { effective: "on", source: "global" };
196
+ assert.equal(await line(), "Receipt-driven development: unknown (native status unavailable)", "a source change keeps the prompt prefix");
197
+ status = { effective: "off", source: "clone_local" };
198
+ assert.equal(await line(), "Receipt-driven development: off (decided by clone_local)", "turning RDD off re-renders");
199
+ status = { effective: "on", source: "global" };
200
+ assert.equal(await line(), "Receipt-driven development: on (decided by global)", "turning RDD back on re-renders");
201
+ await handlers.get("session_shutdown")!({}, ctx);
202
+ status = { effective: "on", source: "clone_local" };
203
+ assert.equal(await line(), "Receipt-driven development: on (decided by clone_local)", "a new session renders the current line");
204
+ });
@@ -6,6 +6,7 @@ import { join } from "node:path";
6
6
  // so a moved clause stays pinned instead of silently disappearing.
7
7
  export const DELEGATION_MODULES = [
8
8
  "orchestrator-delegation.md",
9
+ "orchestrator-delegation-reference.md",
9
10
  "orchestrator-tracking.md",
10
11
  "orchestrator-verification.md",
11
12
  "orchestrator-writer.md",
@@ -61,7 +61,7 @@ test("AC2: counts never classify, and the step-count classifier is gone", () =>
61
61
  test("AC3: small work runs its checks inline and keeps test-first", () => {
62
62
  const size = sectionOf(core, "## Task Size");
63
63
  for (const clause of [
64
- "run the focused test and the suite inline, once each",
64
+ "run the focused test and the suite inline in one command",
65
65
  "observe RED inline before the fix",
66
66
  "No explore, worker, or verifier",
67
67
  "no feature document, mirror, or commits unless the user asks",
@@ -80,7 +80,8 @@ test("AC3: small work runs its checks inline and keeps test-first", () => {
80
80
  test("AC4: each mechanism turns on only by its own trigger and size is re-evaluated after", () => {
81
81
  const triggers = sectionOf(core, "## Mechanisms");
82
82
  for (const clause of [
83
- "each mechanism turns on only by its own trigger",
83
+ // Writer-rule fix: a trigger holds only when its condition truly holds.
84
+ "each mechanism turns on only when its own trigger condition truly holds",
84
85
  "re-evaluate task size",
85
86
  "1. **Ask**",
86
87
  "2. **Evidence-budget rule**",
@@ -90,7 +91,8 @@ test("AC4: each mechanism turns on only by its own trigger and size is re-evalua
90
91
  "6. **Incident rule**",
91
92
  "7. **Context backstop**",
92
93
  "open product or design decision",
93
- "high risk",
94
+ // Verification by named risk: rule 3 names the escalated high-risk item.
95
+ "named high-risk item (assess `escalate`)",
94
96
  "never by file count",
95
97
  // gentle-shell#1731: the writer fires on reasons, not on size alone.
96
98
  "never by file count or a large task alone",
@@ -134,15 +136,29 @@ test("AC5: the high-risk list lives once in the core, native tier wins, unclear
134
136
  const MODULE_BUDGETS: Record<string, number> = {
135
137
  // gentle-shell#1731 merge with main: main's session subject/state guidance plus the reason-based Writer rule (20,000 -> 20,500 B).
136
138
  // gentle-shell#1269: capability-compatible routing rule checked before delegation (20,500 -> 20,800 B).
137
- "orchestrator-delegation.md": 20_800,
139
+ // S9 canon change: conditional triggers, behavior-unit writer threshold, named-risk verifier, invariants,
140
+ // and the PRESERVE differential pointer mirrored from gentle-ai 5f6faa16 (20,800 -> 21,900 B).
141
+ // Pain-point iteration S11 T2: the parent read this module in most runs and kept ~5K tokens of it in the
142
+ // transcript on every later call, so it became the lean delegation card (21,900 -> 7,000 B) and the
143
+ // reference material moved to orchestrator-delegation-reference.md, read only when a situation names it.
144
+ // Pain-point iteration S13 (C10): the Preparation trigger tells the parent not to read what the worker will
145
+ // read and to pass paths, not content (7,000 -> 7,100 B; every other card sentence is pinned by a contract).
146
+ // S15: units of about 100 changed lines or less always run inline (7,100 -> 7,300 B).
147
+ "orchestrator-delegation.md": 7_300,
148
+ "orchestrator-delegation-reference.md": 14_800,
138
149
  // gentle-shell#1731 T23: Close gate accepts one Needs your decision result as a stop (12,500 -> 12,600 B).
139
- "orchestrator-tracking.md": 12_600,
150
+ // S14: close the open slice before a high-risk commit; outcomes per commit (12,600 -> 12,850 B).
151
+ "orchestrator-tracking.md": 12_850,
140
152
  // gentle-shell#1731 T3: parallel review protocol; lazy (delegation or high risk only), core and normative rule untouched.
141
153
  // gentle-shell#1731 T11/T12: verify-per-unit timing and the every-S# verify handoff (6,000 -> 6,400 B).
142
154
  // gentle-shell#1731 T15: one end-of-feature verify for same-model inline deliveries (6,400 -> 6,500 B).
143
155
  // gentle-shell#1731 T23: bounded self-review and partial/blocked writer -> Needs your decision (7,000 -> 7,300 B).
144
- "orchestrator-verification.md": 7_300,
145
- "orchestrator-writer.md": 4_500,
156
+ // Verification by named risk: escalate-only tier table and the concrete-occasions rule (7,300 -> 7,500 B).
157
+ "orchestrator-verification.md": 7_500,
158
+ // Writer-rule fix (size threshold, one behavior per unit) and C3 (writer prompts name the helpers and
159
+ // invariants to reuse): 4,500 -> 5,300 B.
160
+ // Pain-point iteration S10 (C7): writer handoffs keep test scratch outside the worktree (5,300 -> 5,500 B).
161
+ "orchestrator-writer.md": 5_500,
146
162
  "orchestrator-prompts.md": 13_000,
147
163
  };
148
164
 
@@ -167,6 +183,46 @@ test("AC7: each delegation module stays under its byte budget and is loaded by i
167
183
  }
168
184
  });
169
185
 
186
+ // Pain-point iteration S11 T2: once read, a lazy module rides along on every later model call, so the
187
+ // default delegation read must be the lean card; reference material loads only when a pointer names it.
188
+ test("S11: the default delegation pointer resolves to the lean card and the reference loads only by name", () => {
189
+ const card = read("assets/orchestrator-delegation.md");
190
+ const reference = read("assets/orchestrator-delegation-reference.md");
191
+ assert.ok(Buffer.byteLength(card, "utf8") <= MODULE_BUDGETS["orchestrator-delegation.md"], "the delegation card is over its budget");
192
+ // Every core line that names a delegation module names the card, except the one reference pointer.
193
+ const pointerLines = core.split("\n").filter((line) => line.includes("`orchestrator-delegation"));
194
+ const referenceLines = pointerLines.filter((line) => line.includes("`orchestrator-delegation-reference.md`"));
195
+ assert.equal(referenceLines.length, 1, "the core names the reference exactly once");
196
+ for (const name of ["Per-action table", "Work Routing Ladder", "Canonical Workflows"]) {
197
+ assert.ok(referenceLines[0].includes(name), `the reference pointer must name ${name}`);
198
+ }
199
+ assert.ok(pointerLines.some((line) => line.includes("`orchestrator-delegation.md`")), "the core must point at the card");
200
+ // The card carries what the parent needs at the moment it delegates.
201
+ for (const clause of [
202
+ "#### Delegation Triggers",
203
+ "Each trigger holds only when its condition truly holds",
204
+ "A unit is one independent piece of behavior with its code, tests, and docs together",
205
+ "`## Allowed edit surfaces`",
206
+ "Spec: odd/tasks/<feature>.md (read until \"## Log\"). Do T#; S#.",
207
+ "`## Skills to load before work`",
208
+ "`orchestrator-verification.md`",
209
+ "#### Key Learnings closing block",
210
+ "disjoint Allowed edit surfaces",
211
+ "Subagent-facing prompts should be written in English by default",
212
+ "technical artifacts default to English",
213
+ "returns a task id",
214
+ "`orchestrator-delegation-reference.md`",
215
+ ]) {
216
+ assert.ok(card.includes(clause), `the delegation card is missing: ${clause}`);
217
+ }
218
+ // Reference material lives once, in the reference.
219
+ for (const heading of ["### Work Routing Ladder", "#### Background Subagent Policy", "### Canonical Lightweight Workflows", "#### Pi Subagent Model Routing"]) {
220
+ assert.ok(reference.includes(heading), `the reference is missing: ${heading}`);
221
+ assert.ok(!card.includes(heading), `the card still carries reference material: ${heading}`);
222
+ }
223
+ assert.ok(!reference.includes("#### Delegation Triggers"), "the triggers live once, in the card");
224
+ });
225
+
170
226
  // gentle-shell#1269/#1598: the capability rule is the substantive part of the fix,
171
227
  // so losing it must fail a test instead of quietly passing the AC7 budget gate.
172
228
  // Focused on the two claims that carry the fix, not on the exact wording around them.
@@ -178,9 +234,11 @@ test("delegation reports a capability gap instead of substituting inline work fo
178
234
  });
179
235
 
180
236
  test("work usage stays complete in the human guide while routing precedence stays canonical", () => {
181
- const asset = read("assets/orchestrator-delegation.md");
237
+ // S11 T2: the work-publication pointer and the runtime bindings moved to the reference; the writer
238
+ // fallback stays in the card, so the counts read the card plus the reference.
239
+ const asset = `${read("assets/orchestrator-delegation.md")}\n${read("assets/orchestrator-delegation-reference.md")}`;
182
240
  const guide = read("docs/gentle-agents-activity.md");
183
- assert.ok(asset.includes("tool schemas and `docs/gentle-agents-activity.md`"));
241
+ assert.ok(read("assets/orchestrator-delegation-reference.md").includes("tool schemas and `docs/gentle-agents-activity.md`"));
184
242
  for (const clause of [
185
243
  '`{"area":"Auth","topic":"Login","tags":["Review"],"refs":[{"kind":"issue","repository":"github.com/Owner/Repo","id":"12"}]}`',
186
244
  '`filter: {"related_to":{"session_id":"<stable owner ID>"}}`',
@@ -216,8 +274,10 @@ test("T4: tracking updates edit in place and mirror without re-emitting the docu
216
274
  test("T6: each delegation module carries its own clauses and names the modules it depends on", () => {
217
275
  const placement: Record<string, readonly string[]> = {
218
276
  "orchestrator-tracking.md": ["#### Authorization and progress", "Delivery follows work units", "Raise a candidate you know is high risk"],
277
+ // The tier table header lost "Native risk" with verification by named
278
+ // risk: the high row is reached only through escalate, not a native tier.
219
279
  "orchestrator-verification.md": [
220
- "| Native risk tier | Verification (any RDD state) |",
280
+ "| Tier | Verification (any RDD state) |",
221
281
  "## Agent escalation (gentle-shell#1494)",
222
282
  "## Parallel review protocol (gentle-shell#1731)",
223
283
  ],
@@ -0,0 +1,102 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync, readdirSync } from "node:fs";
3
+ import { join, relative } from "node:path";
4
+ import test from "node:test";
5
+
6
+ // Pain-point iteration S10 (C7): blind reviewers flagged tool artifacts in every
7
+ // Gentle Shell delivery. Orchestrators told writers to create a scratch
8
+ // directory in the repository root and point TMPDIR at it, so test state and
9
+ // tool caches entered the diff. Test scratch now lives outside the worktree by
10
+ // default. These are instruction-delivery contracts, not proof of model adherence.
11
+
12
+ const ROOT = join(import.meta.dirname, "..");
13
+ const read = (path: string): string => readFileSync(join(ROOT, path), "utf8");
14
+ const worker = read("assets/agents/gentle-ai-worker.md");
15
+ const writer = read("assets/orchestrator-writer.md");
16
+ const verifier = read("assets/agents/gentle-ai-verify.md");
17
+
18
+ function section(text: string, heading: string): string {
19
+ const start = text.indexOf(`\n## ${heading}\n`);
20
+ assert.ok(start >= 0, `missing section: ${heading}`);
21
+ const end = text.indexOf("\n## ", start + heading.length + 4);
22
+ return text.slice(start, end < 0 ? undefined : end);
23
+ }
24
+
25
+ function containsAll(text: string, clauses: readonly string[], label: string): void {
26
+ for (const clause of clauses) assert.ok(text.includes(clause), `${label} is missing: ${clause}`);
27
+ }
28
+
29
+ test("S10: the worker keeps test scratch outside the worktree and checks git status before returning", () => {
30
+ const discipline = section(worker, "Test discipline");
31
+ containsAll(discipline, [
32
+ "Test scratch lives outside the worktree",
33
+ "`mktemp -d`",
34
+ "never point `TMPDIR` or a cache directory into the repository",
35
+ "use an excluded path that the test removes before it ends",
36
+ "Never leave tool caches or scratch paths in the change",
37
+ ], "worker test discipline");
38
+ const finish = discipline.split("\n").find((line) => line.includes("`git status --short`"));
39
+ assert.ok(finish, "worker finish checks must run `git status --short`");
40
+ containsAll(finish, ["Before returning", "no tool cache or scratch path", "`risks`"], "worker finish check");
41
+ });
42
+
43
+ test("S10: writer handoffs never place test scratch inside the repository", () => {
44
+ const line = writer.split("\n").find((entry) => entry.includes("Test scratch"));
45
+ assert.ok(line, "orchestrator-writer.md must state where writers put test scratch");
46
+ containsAll(line, [
47
+ "outside the worktree",
48
+ "`mktemp -d`",
49
+ "never in the repository",
50
+ "`TMPDIR`",
51
+ "excluded path",
52
+ ], "writer handoff scratch rule");
53
+ });
54
+
55
+ test("S10: no shipped guidance points test temp state into the repository", () => {
56
+ const offenders: string[] = [];
57
+ const walk = (dir: string): void => {
58
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
59
+ const path = join(dir, entry.name);
60
+ if (entry.isDirectory()) walk(path);
61
+ else if (/\.(md|ts)$/.test(entry.name) && /TMPDIR=\$(PWD|\(pwd\))|\.test-tmp|\.atl\/test-tmp/.test(readFileSync(path, "utf8"))) {
62
+ offenders.push(relative(ROOT, path));
63
+ }
64
+ }
65
+ };
66
+ for (const dir of ["assets", "extensions", "docs"]) walk(join(ROOT, dir));
67
+ assert.deepEqual(offenders, []);
68
+ });
69
+
70
+ // S10 follow-up (user, 2026-10-14: "lo de los temporales tambien hacemos limpieza luego?"):
71
+ // scratch outside the worktree was never removed; verifier scratch copies piled up
72
+ // in the system temp directory. The verifier removes only the exact directory its
73
+ // own `mktemp -d` printed; the worker, which may not delete, has tests clean up in
74
+ // teardown and names any leftover outside directory so the parent removes it.
75
+ test("S10: scratch outside the worktree is removed after use", () => {
76
+ // Independent verification of C7 found child safety blocks `rm -rf` in every
77
+ // subagent (lib/destructive-command-guard.ts), so a verifier told to delete its
78
+ // own scratch always failed and nobody removed it. Subagents now report exact
79
+ // scratch paths and the parent, where deletion is only confirmed, removes them.
80
+ containsAll(
81
+ verifier,
82
+ ["list every scratch directory you created by exact path in your result so the parent removes it", "never delete it yourself"],
83
+ "gentle-ai-verify scratch report",
84
+ );
85
+ assert.ok(!/\brm\s+-r/.test(verifier), "gentle-ai-verify must not instruct a deletion child safety blocks");
86
+ containsAll(
87
+ section(worker, "Test discipline"),
88
+ [
89
+ "Tests delete the temp directories they create in teardown",
90
+ "name any scratch directory you created outside the worktree in `risks` so the parent removes it",
91
+ ],
92
+ "worker scratch cleanup",
93
+ );
94
+ // Session scratch and artifact ledger: the runtime owns the parent session's
95
+ // TMPDIR (subagent TMPDIRs nest under it) and records clones and worktrees,
96
+ // so the writer rule points at the command instead of a model cleanup step.
97
+ containsAll(writer, ["The runtime owns session scratch", "`/gentle:scratch` lists or cleans recorded clones and worktrees"], "writer runtime-owned cleanup");
98
+ // The verifier's mktemp copy lives in its own TMPDIR, which the runner removes.
99
+ const verification = read("assets/orchestrator-verification.md");
100
+ containsAll(verification, ["the runtime removes each subagent's own TMPDIR, which holds that copy"], "verification runtime cleanup");
101
+ assert.ok(!verification.includes("the parent removes the scratch paths the verifier reports"), "the parent no longer removes verifier scratch");
102
+ });
@@ -111,7 +111,7 @@ test("T31: high-risk item 3 is about unrequested breaks, not the requested chang
111
111
  );
112
112
  });
113
113
 
114
- // T33 (L74): in p31 x5 both runs declared `Risk: none` after adding --year to
114
+ // T33 (L74): in a benchmark run both cells declared `Risk: none` after adding --year to
115
115
  // the shared budget options; one shipped `budget set --year` silently saving a
116
116
  // wrong budget, the regression verify used to catch. The Risk line must check
117
117
  // shared code before claiming none.
@@ -122,3 +122,38 @@ test("T33: the Close step checks shared code before Risk: none", () => {
122
122
  assert.ok(step?.includes(SHARED_CHECK), `${persona} Close step must check shared code before Risk: none`);
123
123
  }
124
124
  });
125
+
126
+ // C8 (benchmark finding): the PRESERVE test alone missed the `budget set
127
+ // --year` regression in 1 of 2 runs. A differential run of every existing
128
+ // command that shares the changed code, old code against new, caught it in
129
+ // 2 of 2. Inline work and the worker carry the same check.
130
+ // S15: the differential check runs in scratch and is never committed as a test (blind round 4 found tests pinning a git commit).
131
+ const DIFFERENTIAL = "Only when your diff changes shared code (options, parsers, helpers) that an existing command the request did not name also uses, run one differential check before finishing, as a single script: for each such command, run it with the new or changed input (for example a new option passed to a sibling subcommand) against the previous code (a copy of the base commit in a `mktemp -d` scratch directory, never committed as a test; committed tests never read git history or a fixed commit) and against your change, and compare stdout, stderr, and exit code; any difference nobody asked for is a regression to fix and pin with a test.";
132
+ test("C8: inline test-first and the worker PRESERVE step run the differential check", () => {
133
+ for (const persona of ["gentleman", "neutral"] as const) {
134
+ const principle = __testing.buildGentlePrompt(persona).split("\n").find((line) => line.includes("use test-first by default"));
135
+ assert.ok(principle?.includes(`${TOUCHED}. ${DIFFERENTIAL}`), `${persona} principle must run the differential check after PRESERVE`);
136
+ }
137
+ const step = read("assets/agents/gentle-ai-worker.md").split("\n").find((line) => line.startsWith("3. PRESERVE — "));
138
+ assert.ok(step?.includes(DIFFERENTIAL), "worker step 3 must run the differential check");
139
+ });
140
+
141
+ // C3 (benchmark finding): parallel workers appended a shared `Fees` entry
142
+ // inline, unsorted, instead of reusing the helper that keeps the order. The
143
+ // worker reuses the existing function for shared state, and each writer prompt
144
+ // names the helpers to reuse, because parallel units cannot see each other.
145
+ test("C3: the worker reuses the existing function for shared state and keeps its invariants", () => {
146
+ const worker = read("assets/agents/gentle-ai-worker.md");
147
+ assert.ok(
148
+ worker.includes("- Before you create or change shared state (a record, list entry, file, or setting that other commands also create or read), find the existing function that already does it and reuse it, keeping every invariant it enforces (ordering, uniqueness, validation, normalization). Never re-implement that step inline with a different shape."),
149
+ "worker must reuse the existing shared-state function",
150
+ );
151
+ const invariants = read("assets/orchestrator-writer.md").split("\n").find((line) => line.startsWith("- **Invariants**"));
152
+ for (const clause of [
153
+ "each writer prompt names the existing helpers its unit must reuse for shared state",
154
+ "and the invariants they keep",
155
+ "units built in parallel cannot see each other",
156
+ ]) {
157
+ assert.ok(invariants?.includes(clause), `writer module invariants reason is missing: ${clause}`);
158
+ }
159
+ });
@@ -0,0 +1,64 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+
6
+ // Pain-point iteration S13 (C9/C10, from the S12 session analysis): subagents
7
+ // spent 78% of their turns on a single tool call, and 31% of their turns were
8
+ // consecutive read-only calls one turn could batch; the parent read repo files
9
+ // and then delegated, and 69% of the files it read were read again by the
10
+ // worker. Codex reads 8 files with one command. These are instruction-delivery
11
+ // contracts, not proof of model adherence.
12
+
13
+ const ROOT = join(import.meta.dirname, "..");
14
+ const read = (path: string): string => readFileSync(join(ROOT, path), "utf8");
15
+
16
+ function containsAll(text: string, clauses: readonly string[], label: string): void {
17
+ for (const clause of clauses) assert.ok(text.includes(clause), `${label} is missing: ${clause}`);
18
+ }
19
+
20
+ function declaredTools(prompt: string): string[] {
21
+ const frontmatter = prompt.split("\n---\n")[0];
22
+ return [...frontmatter.matchAll(/^\s+- (\S+)$/gm)].map((match) => match[1]);
23
+ }
24
+
25
+ test("S13 C9: every generic subagent prompt batches independent tool calls in one turn", () => {
26
+ for (const agent of ["gentle-ai-worker", "gentle-ai-verify", "gentle-ai-explore"]) {
27
+ const prompt = read(`assets/agents/${agent}.md`);
28
+ const rule = prompt.split("\n").find((line) => line.includes("**Batch tool calls:**"));
29
+ assert.ok(rule, `${agent} must carry the batch tool calls rule`);
30
+ containsAll(rule, [
31
+ "issue independent calls together in the same turn (parallel tool calls)",
32
+ "read each relevant file whole once instead of many small ranges",
33
+ ], agent);
34
+ const tools = declaredTools(prompt);
35
+ // A shell-less agent must not be told to read files or run tests with a command it cannot run.
36
+ if (tools.includes("bash")) {
37
+ containsAll(rule, ["read several files with one command", "the focused test and the suite without status-only turns"], agent);
38
+ } else {
39
+ assert.doesNotMatch(rule, /one command|the suite/, `${agent} has no shell, so its rule must not name a command`);
40
+ }
41
+ if (tools.includes("edit")) containsAll(rule, ["all edits to one file in one `edit` call"], agent);
42
+ }
43
+ });
44
+
45
+ test("S13 C10: the delegation card keeps the parent from reading what the worker will read", () => {
46
+ const card = read("assets/orchestrator-delegation.md");
47
+ const rule = card.split("\n").find((line) => line.startsWith("**Preparation trigger:**"));
48
+ assert.ok(rule, "the delegation card must carry the Preparation trigger");
49
+ containsAll(rule, [
50
+ "When delegating, read only what decides the route",
51
+ "never the files the worker will read",
52
+ "pass paths, the spec reference, and what to change, not file contents",
53
+ ], "delegation card preparation trigger");
54
+ });
55
+
56
+ // S13 C9 parent: 95 of 389 parent turns did only coordination (todo 55, orchestrator_session_id 17,
57
+ // gentle_review assess 14, subagent_list_agents 8) and parent multi-tool turns were ~0.
58
+ test("S13 C9: the always-on orchestrator batches independent calls and never spends a turn only on coordination", () => {
59
+ const core = read("assets/orchestrator.md");
60
+ containsAll(core, [
61
+ "Batch independent tool calls in one turn (parallel calls or one multi-file command).",
62
+ "Never spend a turn only on a `todo` or status update.",
63
+ ], "orchestrator core");
64
+ });
@@ -0,0 +1,46 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import test from "node:test";
4
+
5
+ const read = (path: string) => readFileSync(new URL(`../${path}`, import.meta.url), "utf8");
6
+ const verify = read("assets/agents/gentle-ai-verify.md");
7
+ const policy = read("assets/orchestrator-verification.md");
8
+ const parent = read("assets/orchestrator.md");
9
+
10
+ // Instruction-delivery checks, not evidence of model adherence or delivery time.
11
+ test("unit verification keeps whole-feature context without blocking on future work", () => {
12
+ for (const text of [verify, policy]) {
13
+ assert.ok(text.includes("out of scope, not unmet"));
14
+ assert.ok(text.includes("affected interactions"));
15
+ }
16
+ assert.ok(!verify.includes("no partial-scope carve-out"));
17
+ assert.ok(verify.includes("At final feature verification, cover every requirement"));
18
+ assert.ok(policy.includes("final feature verification covers every `S#`"));
19
+ assert.ok(verify.includes("met, unmet, not implemented, or out of scope"));
20
+ assert.ok(verify.includes("unrelated future work must not block that unit"));
21
+ });
22
+
23
+ test("observed writer evidence is reusable but does not replace independent probes", () => {
24
+ assert.ok(verify.includes("Reuse observed writer checks when their candidate and scope remain valid"));
25
+ assert.ok(verify.includes("independent probes"));
26
+ assert.ok(!verify.includes("never evidence of correctness"));
27
+ });
28
+
29
+ test("auxiliary gates must protect a concrete requirement or threat", () => {
30
+ assert.ok(parent.includes("gates/overengineering: `orchestrator-verification.md`"));
31
+ assert.ok(policy.includes("acceptance criterion, concrete threat, or authoritative contract"));
32
+ assert.ok(policy.includes("must not become a delivery blocker"));
33
+ assert.ok(policy.includes("Do not build a framework for a bounded capability probe"));
34
+ });
35
+
36
+ test("overengineering warnings visibly simplify the plan without weakening safeguards", () => {
37
+ assert.ok(policy.includes("remove or collapse optional artifacts and checks"));
38
+ assert.ok(policy.includes("State what was removed, what remains indispensable, and the next application outcome"));
39
+ assert.ok(policy.includes("Keep required functional checks, security controls, and native review authority intact"));
40
+ });
41
+
42
+ test("existing verification isolation and bounded correction remain required", () => {
43
+ assert.ok(verify.includes("run it only against isolated state"));
44
+ assert.ok(verify.includes("one correction batch and one recheck limited to the reported blockers"));
45
+ assert.ok(policy.includes("Advisories never start a correction"));
46
+ });
@@ -0,0 +1,154 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
7
+ import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
8
+ import { rejectUnriskedVerifierLaunch } from "../lib/verifier-admission.ts";
9
+
10
+ // odd/tasks/verifier-admission.md S2: a bench gate run launched `gentle-ai-verify` on a small, not high-risk task
11
+ // although the prompt's Verification rule reserves the independent verifier for a named high-risk item. The runtime
12
+ // now admits a primary-session verifier launch only with a `Risk: item N` line, a feature-document task, or a
13
+ // measured context at the backstop.
14
+
15
+ const quiet = { tokens: 20_000, contextWindow: 400_000 };
16
+
17
+ function workspace(withDocument: boolean): string {
18
+ const root = mkdtempSync(join(tmpdir(), "verifier-admission-"));
19
+ if (withDocument) {
20
+ mkdirSync(join(root, "odd", "tasks"), { recursive: true });
21
+ writeFileSync(join(root, "odd", "tasks", "ledger.md"), "# Ledger\n");
22
+ }
23
+ return root;
24
+ }
25
+
26
+ test("S3/S4: a verifier launch without a high-risk line is rejected with an inline instruction", (t) => {
27
+ const root = workspace(false);
28
+ t.after(() => rmSync(root, { recursive: true, force: true }));
29
+ for (const task of ["Verify the annual budget change.", "Verify it.\nRisk: none", "Verify it.\nRisk: none (local rename)"]) {
30
+ const denied = rejectUnriskedVerifierLaunch({ agent: "gentle-ai-verify", task }, { root, contextUsage: quiet });
31
+ assert.ok(denied?.block, `${JSON.stringify(task)} must be blocked`);
32
+ assert.match(denied.reason, /independent verifier runs only for a change that declares a named high-risk item/);
33
+ assert.match(denied.reason, /focused test and suite inline/);
34
+ assert.match(denied.reason, /do not retry the launch/);
35
+ assert.match(denied.reason, /never add a Risk line or create a feature document only to get a verifier admitted/);
36
+ assert.doesNotMatch(denied.reason, /claude|gpt|openai|anthropic|gemini|sonnet|opus/i, "the message stays generic");
37
+ }
38
+ });
39
+
40
+ test("S3: a Risk line naming a Task Size high-risk item admits the verifier", (t) => {
41
+ const root = workspace(false);
42
+ t.after(() => rmSync(root, { recursive: true, force: true }));
43
+ for (const line of ["Risk: item 3 (shared parser)", "- Risk: item 2, item 3 (auth and parser)", "Risk: item 6", " Risk:item 1"]) {
44
+ assert.equal(rejectUnriskedVerifierLaunch({ agent: "gentle-ai-verify", task: `Verify the change.\n${line}\n` }, { root, contextUsage: quiet }), undefined, line);
45
+ }
46
+ const viaContext = { agent: "gentle-ai-verify", task: "Verify the change.", context: "Risk: item 4 (migration)" };
47
+ assert.equal(rejectUnriskedVerifierLaunch(viaContext, { root, contextUsage: quiet }), undefined, "the context carries the line");
48
+ });
49
+
50
+ // gentle-shell-bench model-economy L9: a DeepSeek verifier launch was refused twice because its valid declaration
51
+ // opened a sentence mid-paragraph ("... on top of commit ebf3f35). Risk: item 1 (...)"), not a line.
52
+ test("S3: a Risk declaration that opens a sentence admits the verifier", (t) => {
53
+ const root = workspace(false);
54
+ t.after(() => rmSync(root, { recursive: true, force: true }));
55
+ for (const task of [
56
+ "Independently verify an ODD change on top of commit ebf3f35. Risk: item 1 (schema migration of stored data).",
57
+ "Verify the parser change! Risk: item 3 (shared parser)",
58
+ "Is the guard still sound? Risk: item 2",
59
+ ]) {
60
+ assert.equal(rejectUnriskedVerifierLaunch({ agent: "gentle-ai-verify", task }, { root, contextUsage: quiet }), undefined, task);
61
+ }
62
+ for (const task of ["Verify it. Risk: none (local rename)", "Verify it. Do not write Risk: item 3 here.", "Verify it.Risk: item 3"]) {
63
+ assert.ok(rejectUnriskedVerifierLaunch({ agent: "gentle-ai-verify", task }, { root, contextUsage: quiet })?.block, task);
64
+ }
65
+ });
66
+
67
+ test("S3: an item outside 1-6, a Risk mention inside prose, or none first is rejected", (t) => {
68
+ const root = workspace(false);
69
+ t.after(() => rmSync(root, { recursive: true, force: true }));
70
+ for (const line of ["Risk: item 7", "Risk: item 0", "Risk: item 12", "Do not write Risk: item 3 here.", "Risk: none; item 3 does not apply", "Risk: items"]) {
71
+ assert.ok(rejectUnriskedVerifierLaunch({ agent: "gentle-ai-verify", task: `Verify it.\n${line}` }, { root, contextUsage: quiet })?.block, line);
72
+ }
73
+ });
74
+
75
+ test("S3: a task of an existing feature document admits the verifier; a missing document does not", (t) => {
76
+ const documented = workspace(true);
77
+ const bare = workspace(false);
78
+ t.after(() => { rmSync(documented, { recursive: true, force: true }); rmSync(bare, { recursive: true, force: true }); });
79
+ const input = { agent: "gentle-ai-verify", task: 'Spec: odd/tasks/ledger.md (read until "## Log"). Verify T2; S3.' };
80
+ assert.equal(rejectUnriskedVerifierLaunch(input, { root: documented, contextUsage: quiet }), undefined);
81
+ assert.ok(rejectUnriskedVerifierLaunch(input, { root: bare, contextUsage: quiet })?.block, "a missing document does not admit");
82
+ });
83
+
84
+ test("S3: the measured context backstop admits the verifier", (t) => {
85
+ const root = workspace(false);
86
+ t.after(() => rmSync(root, { recursive: true, force: true }));
87
+ const input = { agent: "gentle-ai-verify", task: "Verify the change." };
88
+ assert.equal(rejectUnriskedVerifierLaunch(input, { root, contextUsage: { tokens: 150_000, contextWindow: 1_000_000 } }), undefined);
89
+ assert.equal(rejectUnriskedVerifierLaunch(input, { root, contextUsage: { tokens: 19_200, contextWindow: 128_000 } }), undefined, "15% of a small window admits");
90
+ assert.ok(rejectUnriskedVerifierLaunch(input, { root, contextUsage: { tokens: 149_999, contextWindow: 1_000_000 } })?.block);
91
+ assert.ok(rejectUnriskedVerifierLaunch(input, { root, contextUsage: undefined })?.block, "missing context usage does not admit");
92
+ });
93
+
94
+ test("S5: other agents are not gated by the verifier admission", (t) => {
95
+ const root = workspace(false);
96
+ t.after(() => rmSync(root, { recursive: true, force: true }));
97
+ for (const agent of ["gentle-ai-worker", "worker", "gentle-ai-explore", "jd-fix-agent", "jd-judge-a", "jd-judge-b", "review-risk", "verify"]) {
98
+ assert.equal(rejectUnriskedVerifierLaunch({ agent, task: "Check src/budget.ts" }, { root, contextUsage: quiet }), undefined, agent);
99
+ }
100
+ for (const input of [undefined, null, "gentle-ai-verify", ["gentle-ai-verify"], { task: "Verify it." }]) {
101
+ assert.equal(rejectUnriskedVerifierLaunch(input, { root, contextUsage: quiet }), undefined, JSON.stringify(input));
102
+ }
103
+ });
104
+
105
+ type ToolCallHandler = (event: { toolName: string; input: unknown }, ctx: ExtensionContext) => Promise<{ block?: boolean; reason?: string } | undefined>;
106
+
107
+ function toolCallThroughHook(toolName: string, input: Record<string, unknown>, cwd: string, tokens: number, processEnv: NodeJS.ProcessEnv = {}) {
108
+ const handlers = new Map<string, ToolCallHandler>();
109
+ const pi = { on(name: string, handler: ToolCallHandler) { handlers.set(name, handler); }, events: { emit() {} }, registerCommand() {}, registerTool() {} } as unknown as ExtensionAPI;
110
+ createGentleAiExtension({ nativeReviewCli: null, processEnv })(pi);
111
+ return handlers.get("tool_call")!({ toolName, input }, {
112
+ cwd, hasUI: false, ui: { confirm: async () => true },
113
+ getContextUsage: () => ({ tokens, contextWindow: 272_000, percent: Math.round((tokens / 272_000) * 100) }),
114
+ } as unknown as ExtensionContext);
115
+ }
116
+
117
+ test("S3/S5: the orchestrator tool_call hook gates only primary verifier launches", async (t) => {
118
+ const bare = workspace(false);
119
+ const documented = workspace(true);
120
+ t.after(() => { rmSync(bare, { recursive: true, force: true }); rmSync(documented, { recursive: true, force: true }); });
121
+ const blocked = await toolCallThroughHook("subagent_run", { agent: "gentle-ai-verify", mode: "task", task: "Verify the budget change." }, bare, 20_000);
122
+ assert.equal(blocked?.block, true);
123
+ assert.match(blocked?.reason ?? "", /focused test and suite inline/);
124
+ const risked = await toolCallThroughHook("subagent_run", { agent: "gentle-ai-verify", mode: "task", task: "Verify the budget change.\nRisk: item 3 (shared parser)" }, bare, 20_000);
125
+ assert.equal(risked?.block, undefined, "a named high-risk item admits");
126
+ const documentedLaunch = await toolCallThroughHook("subagent_run", { agent: "gentle-ai-verify", mode: "task", workspace_root: documented, task: "Spec: odd/tasks/ledger.md, T1. Verify it." }, bare, 20_000);
127
+ assert.equal(documentedLaunch?.block, undefined, "the feature document resolves under the selected workspace root");
128
+ const backstop = await toolCallThroughHook("subagent_run", { agent: "gentle-ai-verify", mode: "task", task: "Verify the budget change." }, bare, 136_000);
129
+ assert.equal(backstop?.block, undefined, "half of the measured window admits");
130
+ const child = await toolCallThroughHook("subagent_run", { agent: "gentle-ai-verify", mode: "task", task: "Verify it." }, bare, 0, { GENTLE_PI_AGENTS_CHILD: "1" });
131
+ assert.equal(child?.block, undefined, "a child session is not gated");
132
+ const continuation = await toolCallThroughHook("subagent_continue", { agent: "gentle-ai-verify", task_id: "t-1", task: "Recheck it." }, bare, 0);
133
+ assert.notEqual(continuation?.reason, blocked?.reason, "a continuation is not gated by the verifier admission");
134
+ for (const agent of ["gentle-ai-explore", "jd-judge-a", "jd-judge-b"]) {
135
+ const other = await toolCallThroughHook("subagent_run", { agent, mode: "task", task: "Map src/budget.ts" }, bare, 0);
136
+ assert.equal(other?.block, undefined, `${agent} is unaffected`);
137
+ }
138
+ // A native review relay keeps its own candidate-view gate, which already rejects a dispatch without a frozen view.
139
+ const relay = await toolCallThroughHook("subagent_run", { agent: "review-risk", mode: "task", task: "Review the candidate." }, bare, 0);
140
+ assert.match(relay?.reason ?? "", /^review subagent dispatch /, "review relays keep their previous gate");
141
+ const writer = await toolCallThroughHook("subagent_run", { agent: "gentle-ai-worker", mode: "background", task: "Implement it.\nRisk: item 3\n\n## Allowed edit surfaces\nsrc/budget.ts\n" }, bare, 0);
142
+ assert.match(writer?.reason ?? "", /write this unit inline yourself/, "writer admission is unchanged and ignores Risk lines");
143
+ });
144
+
145
+ test("S6: the Verification rule and the reference docs point at the runtime admission", () => {
146
+ const read = (path: string) => readFileSync(join(import.meta.dirname, "..", path), "utf8");
147
+ assert.ok(read("assets/orchestrator.md").includes("independent `gentle-ai-verify`; else inline; runtime admission, gates/overengineering"), "the core rule names the runtime check");
148
+ assert.ok(read("assets/orchestrator-verification.md").includes("(runtime admits its launch on a `Risk: item N` line or sentence, a feature-document task line, or past the context backstop)"), "the normative rule names the admitted line");
149
+ const docs = read("docs/readme-reference.md");
150
+ const writer = docs.indexOf("A second admission check stops small-task delegation");
151
+ const verifier = docs.indexOf("A third admission check keeps the independent verifier for high-risk changes");
152
+ assert.ok(writer >= 0 && verifier > writer, "the verifier admission is documented right after the writer admission");
153
+ assert.match(docs.slice(verifier, docs.indexOf("\n\n", verifier)), /`gentle-ai-verify` launch is admitted only when[^]*`Risk: item N`[^]*context backstop/);
154
+ });