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
@@ -10,6 +10,15 @@ import { basename } from "node:path";
10
10
 
11
11
  const tick = () => new Promise<void>((resolve) => setImmediate(resolve));
12
12
 
13
+ /** UI chains settle across a load-dependent number of turns: wait on the
14
+ * observable state instead of assuming a fixed tick count landed the async
15
+ * step (gentle-shell#2498). The deadline keeps a broken chain failing fast. */
16
+ const settleUntil = async (predicate: () => boolean, what: string): Promise<void> => {
17
+ const deadline = Date.now() + 5000;
18
+ while (!predicate() && Date.now() < deadline) await tick();
19
+ assert.ok(predicate(), `timed out waiting for ${what}`);
20
+ };
21
+
13
22
  /** Real packaged WAVs so a valid path uses an existing fixture, never a synthetic name. */
14
23
  const WAV_FIXTURE = fileURLToPath(new URL("../assets/sounds/success.wav", import.meta.url));
15
24
  const WAV_FIXTURES = {
@@ -223,7 +232,7 @@ test("an in-flight local sound validation keeps busy, blocking a second field an
223
232
  assert.match(view.render(160).join("\n"), /Local audio path/);
224
233
  view.handleInput(WAV_FIXTURE);
225
234
  view.handleInput("\r");
226
- await tick();
235
+ await settleUntil(() => validated.length === 1, "the submitted value to start validating");
227
236
  assert.deepEqual(validated, [`file:${WAV_FIXTURE}`], "the submitted value is validated exactly once");
228
237
  // Validation is still pending: `f` must not open a second field and Enter must not write.
229
238
  view.handleInput("f");
@@ -233,11 +242,11 @@ test("an in-flight local sound validation keeps busy, blocking a second field an
233
242
  assert.equal(h.writes, 0, "no write before the deferred validation resolves");
234
243
  // Resolve: a single write, then busy releases for the next edit.
235
244
  resolveValidation(true);
236
- await tick(); await tick();
245
+ await settleUntil(() => h.writes === 1, "the resolved validation to write exactly once");
237
246
  assert.equal(h.writes, 1, "the resolved validation writes exactly once");
238
247
  assert.equal(getNotificationService()!.getState().settings.audio.events["agent.failed"], `file:${WAV_FIXTURE}`);
239
248
  view.handleInput("f");
240
- assert.match(view.render(160).join("\n"), /Local audio path/, "busy is released so the next edit opens a fresh field");
249
+ await settleUntil(() => view.render(160).join("\n").includes("Local audio path"), "busy released so the next edit opens a fresh field");
241
250
  } finally { h.owner.retire(); }
242
251
  });
243
252
 
@@ -262,14 +271,17 @@ test("disposing the card while validation is in flight never writes the stale ch
262
271
  const h = harness(); try {
263
272
  const rows = h.rows();
264
273
  let resolveValidation!: (value: boolean) => void;
265
- getNotificationService()!.validateFile = () => new Promise<boolean>(resolve => { resolveValidation = resolve; });
274
+ let validationStarted = false;
275
+ getNotificationService()!.validateFile = () => { validationStarted = true; return new Promise<boolean>(resolve => { resolveValidation = resolve; }); };
266
276
  const view = customizeView(rows);
267
277
  await expandAndSelect(view, "agent.failed:");
268
278
  view.handleInput("f");
269
279
  view.render(160);
270
280
  view.handleInput(WAV_FIXTURE);
271
281
  view.handleInput("\r");
272
- await tick();
282
+ // Disposing before the validation even starts would never observe the
283
+ // stale-choice guard, so wait until the deferred validation is pending.
284
+ await settleUntil(() => validationStarted, "the in-flight validation to start");
273
285
  view.dispose();
274
286
  resolveValidation(true);
275
287
  await tick(); await tick();
@@ -48,7 +48,11 @@ test("applicability, fallback and honest evidence flow through ODD actors", () =
48
48
  test("retired SDD routes and assets are absent while ODD entry and generic workers remain", () => {
49
49
  const core = read("assets/orchestrator.md");
50
50
  const delegation = readDelegationDetail();
51
- assert.match(core, /ODD \(Default Workflow, harness section above\) is mandatory on every request/);
51
+ // S11 T3: deduplicated. The core's "ODD (Default Workflow, harness section
52
+ // above) is mandatory on every request" restated the harness; the mandatory
53
+ // ODD entry is stated once, in the harness that renders above the core.
54
+ assert.match(read("extensions/gentle-ai.ts"), /Default workflow: Organic Driven Development \(MANDATORY\)\nOrganic Driven Development \(ODD\) is the predefined workflow of this orchestrator\. Every request enters it/);
55
+ assert.doesNotMatch(core, /ODD \(Default Workflow, harness section above\)/);
52
56
  assert.match(delegation, /generic writer chain is unavailable/);
53
57
  for (const path of [
54
58
  "assets/agents/gentle-ai-worker.md",
@@ -1,46 +1,36 @@
1
1
  import assert from "node:assert/strict";
2
- import { existsSync, readFileSync } from "node:fs";
2
+ import { readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import test from "node:test";
5
- import {
6
- ODD_ROUTING_BLOCK_PREFIX,
7
- ODD_ROUTING_FIXTURE_RELATIVE,
8
- ODD_ROUTING_SOURCE_PATH,
9
- ODD_ROUTING_SOURCE_REPO,
10
- assertCleanGentleAiCheckout,
11
- parseOddRoutingFixture,
12
- renderOddRoutingFixture,
13
- resolveOddRoutingProvenance,
14
- sha256Hex,
15
- } from "../scripts/mirror-odd-routing.mjs";
5
+ import { __testing } from "../extensions/gentle-ai.ts";
6
+ import { PI_RUNTIME_IDENTITY, ROUTING_REQUIRED_SINCE_MINOR, verifyProviderContractBundleTree } from "../lib/provider-contract-bundle.ts";
7
+ import { PROVIDER_CONTRACT_LOCK_FILE, PROVIDER_CONTRACT_MIRROR_ROOT } from "../scripts/mirror-provider-contract.mjs";
16
8
 
17
9
  // ---------------------------------------------------------------------------
18
10
  // ODD routing drift ratchet (gentle-pi#1147 follow-up)
19
11
  //
20
- // gentle-pi hand-mirrors the always-on ODD routing block rendered by gentle-ai
21
- // `internal/components/agentguidance/routing.go` (RenderRouting). There is no
22
- // automated sync, so a canonical change leaves the pi mirror stale with nothing
23
- // catching it. This ratchet vendored-snapshots the canonical block into
24
- // `fixtures/odd-routing-canonical.md` (regenerated via `npm run
25
- // mirror:odd-routing`) and fails when a mandatory-delegation clause is dropped
26
- // either from the canonical fixture or from a pi mirror surface.
27
- //
28
- // Regeneration writes ONLY the fixture; this ratchet never auto-rewrites mirror
29
- // assets, so drift stays visible as a deliberate diff.
12
+ // gentle-ai renders the always-on ODD routing block for Pi
13
+ // (`internal/components/agentguidance/routing.go`) and publishes it in the
14
+ // provider contract bundle as `routing/pi.md` (contract 1.4.0). The mirrored
15
+ // bundle (`contracts/review-provider-contract-mirror/`, refreshed with
16
+ // `scripts/mirror-provider-contract.mjs`) is the canonical reference: the
17
+ // harness ODD steps are rendered from it, and this ratchet fails when a
18
+ // mandatory-delegation clause is dropped either from the canonical block or
19
+ // from a Pi surface that restates it (the `assets/orchestrator*.md` modules
20
+ // stay Pi's own and keep their anchors).
30
21
  // ---------------------------------------------------------------------------
31
22
 
32
23
  const REPO_ROOT = join(import.meta.dirname, "..");
33
- const FIXTURE_PATH = join(REPO_ROOT, ...ODD_ROUTING_FIXTURE_RELATIVE.split("/"));
24
+ const MIRROR_ROOT = join(REPO_ROOT, ...PROVIDER_CONTRACT_MIRROR_ROOT.split("/"));
34
25
  const DELEGATION_PATH = join(REPO_ROOT, "assets", "orchestrator-delegation.md");
35
26
  const CORE_PATH = join(REPO_ROOT, "assets", "orchestrator.md");
36
- const EXTENSION_PATH = join(REPO_ROOT, "extensions", "gentle-ai.ts");
37
27
 
38
28
  const readRepo = (absolutePath: string): string => readFileSync(absolutePath, "utf8");
39
29
 
40
30
  // A mandatory-delegation semantic anchor: the clause the canonical block must
41
31
  // still carry, plus the wording each pi mirror surface is expected to carry.
42
- // A canonical change that drops `canonical` is caught here on regeneration
43
- // because the fixture no longer carries it.
32
+ // A canonical change that drops `canonical` is caught here once the bundle is re-mirrored
33
+ // because the mirrored routing block no longer carries it.
44
34
  interface RoutingAnchor {
45
35
  label: string;
46
36
  canonical: string;
@@ -49,23 +39,33 @@ interface RoutingAnchor {
49
39
 
50
40
  const DELEGATION = "assets/orchestrator-delegation.md";
51
41
  const CORE = "assets/orchestrator.md";
52
- const EXTENSION = "extensions/gentle-ai.ts";
42
+ // The harness prompt rendered by extensions/gentle-ai.ts, including the ODD
43
+ // steps it takes from the mirrored routing/pi.md.
44
+ const EXTENSION = "rendered harness prompt";
53
45
 
54
46
  const ANCHORS: readonly RoutingAnchor[] = [
55
47
  {
56
- label: "mandatory delegation triggers heading",
57
- canonical: "### Mandatory Delegation Triggers",
48
+ // S9 canon change (gentle-ai 87a3b2eb): the heading dropped "Mandatory";
49
+ // a trigger is mandatory only once its condition truly holds.
50
+ label: "delegation triggers heading",
51
+ canonical: "### Delegation Triggers",
58
52
  mirrors: [
59
- { surface: DELEGATION, includes: "#### Mandatory Delegation Triggers" },
60
- { surface: CORE, includes: "Mandatory Delegation Triggers" },
53
+ { surface: DELEGATION, includes: "#### Delegation Triggers" },
54
+ // Writer-rule fix: the core names them "Delegation Triggers" and says
55
+ // each holds only when its condition truly holds; the lazy port keeps
56
+ // the canonical heading.
57
+ { surface: CORE, includes: "Delegation Triggers — each mechanism turns on only when its own trigger condition truly holds" },
61
58
  ],
62
59
  },
63
60
  {
64
- label: "triggers are mandatory, not advisory",
65
- canonical: "These triggers are mandatory, not advisory.",
61
+ // S9 canon change: "These triggers are mandatory, not advisory." read as a
62
+ // default to delegate (L8); a trigger now holds only when its condition
63
+ // truly holds, none by default, inline when in doubt.
64
+ label: "triggers hold only when their condition truly holds",
65
+ canonical: "Each trigger holds only when its condition truly holds, and only then is it mandatory, not advisory; no trigger holds by default, and when in doubt work inline.",
66
66
  mirrors: [
67
- { surface: DELEGATION, includes: "These triggers are mandatory, not advisory." },
68
- { surface: EXTENSION, includes: "These triggers are mandatory, not advisory" },
67
+ { surface: DELEGATION, includes: "Each trigger holds only when its condition truly holds, and only then is it mandatory, not advisory; no trigger holds by default, and when in doubt work inline." },
68
+ { surface: EXTENSION, includes: "No trigger holds by default and, when in doubt, work inline" },
69
69
  ],
70
70
  },
71
71
  {
@@ -94,17 +94,65 @@ const ANCHORS: readonly RoutingAnchor[] = [
94
94
  ],
95
95
  },
96
96
  {
97
- label: "writer trigger fires only for a large task, never by file count",
98
- canonical: "**Writer trigger:** a large task delegates one bounded writer per task; file count never fires this trigger",
99
- // Gentle Shell intentionally leads the canon here (gentle-shell#1731): the
100
- // writer fires on named reasons (parallelism, context), never on size
101
- // alone; T24 turned the price-ratio reason off. The canon port is G1 in
102
- // odd/tasks/delegate-for-reason.md.
97
+ // S9 canon change: exploration never reads files an inline write reads anyway.
98
+ label: "never explore to read files an inline write reads anyway",
99
+ canonical: "Never explore to read files you will read anyway before writing inline.",
100
+ mirrors: [
101
+ { surface: DELEGATION, includes: "Never explore to read files you will read anyway before writing inline." },
102
+ { surface: CORE, includes: "never for reading before an inline write" },
103
+ ],
104
+ },
105
+ {
106
+ label: "writer trigger fires only for a named reason, never by size or file count",
107
+ // S9 canon change (gentle-ai 87a3b2eb): the canon caught up with Gentle
108
+ // Shell (gentle-shell#1731): the writer fires on named reasons
109
+ // (parallelism, context), never on size alone or a price ratio.
110
+ canonical: "**Writer trigger:** a writer is delegated only for a named reason, one bounded writer per unit",
103
111
  mirrors: [
104
112
  { surface: DELEGATION, includes: "**Writer trigger (Writer rule):** a large task alone never delegates, and file count never fires this trigger" },
105
113
  { surface: CORE, includes: "**Writer rule** — never by file count or a large task alone" },
106
114
  ],
107
115
  },
116
+ {
117
+ // S9 canon change: a writer unit is one independent behavior with its
118
+ // code, tests, and docs together, never split across writers.
119
+ label: "a writer unit is one independent behavior",
120
+ canonical: "A unit is one independent piece of behavior with its code, tests, and docs together",
121
+ mirrors: [
122
+ { surface: DELEGATION, includes: "A unit is one independent piece of behavior with its code, tests, and docs together" },
123
+ { surface: CORE, includes: "Tests, docs, and code of the same behavior are one unit, never separate writers" },
124
+ ],
125
+ },
126
+ {
127
+ // S9 canon change: a unit qualifies only with several minutes of work,
128
+ // because a subagent spends one to two minutes reading before it writes.
129
+ label: "a writer unit needs several minutes of work",
130
+ canonical: "several files or roughly 100+ changed lines",
131
+ mirrors: [
132
+ { surface: DELEGATION, includes: "several files or ~100+ changed lines; a subagent spends 1-2 minutes reading before it writes" },
133
+ { surface: CORE, includes: "several files or roughly 100+ changed lines" },
134
+ ],
135
+ },
136
+ {
137
+ // S9 canon change (gentle-ai c3763aa7, C3): whoever writes reuses the
138
+ // existing shared-state helper and keeps its invariants.
139
+ label: "writers reuse the existing shared-state function",
140
+ canonical: "reuses the existing function for shared state",
141
+ mirrors: [
142
+ { surface: DELEGATION, includes: "reuses the existing function for shared state and keeps its invariants (ordering, uniqueness, validation, normalization)" },
143
+ ],
144
+ },
145
+ {
146
+ // S9 canon change (gentle-ai c3763aa7, C8): PRESERVE diffs each existing
147
+ // command that shares the changed code against the previous code.
148
+ label: "differential check against the previous code",
149
+ canonical: "run a differential check",
150
+ mirrors: [
151
+ { surface: DELEGATION, includes: "**Differential check (PRESERVE):**" },
152
+ // weak-model-economy S2: the harness gates the check on shared code an unnamed command uses.
153
+ { surface: EXTENSION, includes: "run one differential check" },
154
+ ],
155
+ },
108
156
  {
109
157
  label: "task size decides the route (gentle-shell#1494)",
110
158
  canonical: "### Task Size",
@@ -114,11 +162,14 @@ const ANCHORS: readonly RoutingAnchor[] = [
114
162
  ],
115
163
  },
116
164
  {
117
- label: "verification trigger is reserved for high risk",
118
- canonical: "**Verification trigger:** a high-risk change gets an independent verifier",
165
+ label: "verification trigger is reserved for a named high-risk item",
166
+ // S9 canon change (gentle-ai 6b9a71ef): only a change whose Risk line
167
+ // names a high-risk item gets the independent verifier.
168
+ canonical: "**Verification trigger:** only a change whose Risk line names a high-risk item gets an independent verifier",
119
169
  mirrors: [
120
- { surface: CORE, includes: "**Verification rule** — high risk → independent `gentle-ai-verify`" },
121
- { surface: DELEGATION, includes: "a high-risk change (Task Size) gets an independent `gentle-ai-verify` run" },
170
+ // Verification by named risk: the core row names the escalate-only verifier.
171
+ { surface: CORE, includes: "**Verification rule** — only a named high-risk item (assess `escalate`) → independent `gentle-ai-verify`" },
172
+ { surface: DELEGATION, includes: "only a change whose Risk line names a high-risk item (Task Size, assess `escalate`) gets an independent `gentle-ai-verify` run" },
122
173
  ],
123
174
  },
124
175
  {
@@ -138,10 +189,12 @@ const ANCHORS: readonly RoutingAnchor[] = [
138
189
  label: "long-session backstop",
139
190
  canonical: "**Long-session backstop:**",
140
191
  // Gentle Shell intentionally leads the canon here: a parent-context token
141
- // backstop replaces the tool-call count (tracked by gentle-ai#5139).
192
+ // backstop replaces the tool-call count (tracked by gentle-ai#5139), and the
193
+ // runtime measures it and signals it instead of the canon's advisory ~150k
194
+ // estimate (harness roadmap R1), so the Pi anchor names the runtime signal.
142
195
  mirrors: [
143
- { surface: DELEGATION, includes: "**Context backstop:** when the parent context passes ~150k tokens" },
144
- { surface: CORE, includes: "**Context backstop** — parent context past ~150k tokens" },
196
+ { surface: DELEGATION, includes: "**Context backstop:** when the runtime signals it" },
197
+ { surface: CORE, includes: "**Context backstop** — when the runtime signals it" },
145
198
  ],
146
199
  },
147
200
  {
@@ -173,58 +226,52 @@ const ANCHORS: readonly RoutingAnchor[] = [
173
226
  ],
174
227
  },
175
228
  {
176
- label: "ODD step 6 honors its mandatory delegation triggers",
177
- canonical: "honoring its mandatory delegation triggers",
178
- mirrors: [{ surface: EXTENSION, includes: "honoring its mandatory delegation triggers" }],
229
+ // S9 canon change: step 6 honors its triggers only when their conditions truly hold.
230
+ label: "ODD step 6 honors its delegation triggers when their conditions hold",
231
+ canonical: "honoring its delegation triggers when their conditions truly hold",
232
+ mirrors: [{ surface: EXTENSION, includes: "honoring its delegation triggers when their conditions truly hold" }],
179
233
  },
180
234
  ];
181
235
 
182
236
  // Condensed trigger rows that live only in the always-on core prompt; the
183
237
  // canonical RenderRouting block does not carry the incident or verification
184
- // rows, so they are mirror-only and not fixture-derived anchors.
238
+ // rows, so they are mirror-only and not anchors of the canonical routing block.
185
239
  const CORE_ONLY_TRIGGERS = [
186
240
  "**Incident rule** — diagnose wrong cwd/worktree/git/tooling incidents separately",
187
- "**Verification rule** — high risk → independent `gentle-ai-verify`",
241
+ "**Verification rule** — only a named high-risk item (assess `escalate`) → independent `gentle-ai-verify`",
188
242
  ] as const;
189
243
 
190
- function fixtureBody(): string {
191
- assert.ok(
192
- existsSync(FIXTURE_PATH),
193
- `missing canonical fixture ${ODD_ROUTING_FIXTURE_RELATIVE}; regenerate it with \`npm run mirror:odd-routing\``,
194
- );
195
- return parseOddRoutingFixture(readRepo(FIXTURE_PATH)).body;
244
+ // The verified mirrored routing/pi.md: the bundle tree must pass the strict
245
+ // decoder, so a hand edit to the canonical block fails here as well as in
246
+ // `pnpm run check:provider-contract`.
247
+ function canonicalRoutingBlock(): string {
248
+ const lock = JSON.parse(readFileSync(join(MIRROR_ROOT, PROVIDER_CONTRACT_LOCK_FILE), "utf8")) as { contract_semver: string };
249
+ const bundle = verifyProviderContractBundleTree(join(MIRROR_ROOT, `v${lock.contract_semver}`, "bundle"));
250
+ const block = bundle.routing.get(PI_RUNTIME_IDENTITY);
251
+ assert.ok(block !== undefined, `the mirrored provider contract ${bundle.contractSemver} carries no routing/${PI_RUNTIME_IDENTITY}.md`);
252
+ return block;
196
253
  }
197
254
 
198
255
  // ---------------------------------------------------------------------------
199
- // 1 — Fixture provenance readback
256
+ // 1 — The canonical reference is the verified mirrored bundle
200
257
  // ---------------------------------------------------------------------------
201
258
 
202
- test("the canonical routing fixture exists with managed provenance (source repo, commit, digest)", () => {
203
- assert.ok(
204
- existsSync(FIXTURE_PATH),
205
- `missing canonical fixture ${ODD_ROUTING_FIXTURE_RELATIVE}; regenerate it with \`npm run mirror:odd-routing\``,
206
- );
207
- const { header } = parseOddRoutingFixture(readRepo(FIXTURE_PATH));
208
- assert.equal(header.source_repo, ODD_ROUTING_SOURCE_REPO);
209
- assert.equal(header.source_path, ODD_ROUTING_SOURCE_PATH);
210
- assert.match(header.source_commit ?? "", /^[0-9a-f]{40}$/, "fixture must record the gentle-ai source commit");
211
- assert.match(header.generated_at ?? "", /^\d{4}-\d{2}-\d{2}T/, "fixture must record a generation date");
212
- assert.match(header.block_sha256 ?? "", /^[0-9a-f]{64}$/, "fixture must record the block body digest");
213
- });
214
-
215
- test("the fixture digest matches its block body byte-for-byte", () => {
216
- const { header, body } = parseOddRoutingFixture(readRepo(FIXTURE_PATH));
217
- assert.equal(header.block_sha256, sha256Hex(body), "the fixture body was hand-edited without regenerating its digest");
259
+ test("the canonical routing reference is the verified mirrored routing/pi.md (contract >= 1.4.0)", () => {
260
+ const lock = JSON.parse(readFileSync(join(MIRROR_ROOT, PROVIDER_CONTRACT_LOCK_FILE), "utf8")) as { contract_semver: string; entries: Record<string, string> };
261
+ const [major, minor] = lock.contract_semver.split(".").map(Number);
262
+ assert.ok(major === 1 && (minor ?? 0) >= ROUTING_REQUIRED_SINCE_MINOR, `mirrored contract ${lock.contract_semver} predates routing`);
263
+ assert.match(lock.entries[`routing/${PI_RUNTIME_IDENTITY}.md`] ?? "", /^[0-9a-f]{64}$/);
264
+ assert.ok(canonicalRoutingBlock().startsWith("## Implementation Routing\n"));
218
265
  });
219
266
 
220
267
  // ---------------------------------------------------------------------------
221
- // 2 — Canonical fixture carries every mandatory anchor
268
+ // 2 — The canonical block carries every mandatory anchor
222
269
  // ---------------------------------------------------------------------------
223
270
 
224
- test("the canonical fixture carries every mandatory-delegation anchor", () => {
225
- const body = fixtureBody();
271
+ test("the canonical routing block carries every mandatory-delegation anchor", () => {
272
+ const body = canonicalRoutingBlock();
226
273
  for (const anchor of ANCHORS) {
227
- assert.ok(body.includes(anchor.canonical), `canonical fixture dropped anchor: ${anchor.label}`);
274
+ assert.ok(body.includes(anchor.canonical), `canonical routing block dropped anchor: ${anchor.label}`);
228
275
  }
229
276
  });
230
277
 
@@ -236,7 +283,7 @@ test("the pi mirror surfaces carry every mapped mandatory-delegation anchor", ()
236
283
  const surfaces: Record<string, string> = {
237
284
  [DELEGATION]: readRepo(DELEGATION_PATH),
238
285
  [CORE]: readRepo(CORE_PATH),
239
- [EXTENSION]: readRepo(EXTENSION_PATH),
286
+ [EXTENSION]: __testing.buildGentlePrompt("gentleman"),
240
287
  };
241
288
  for (const anchor of ANCHORS) {
242
289
  for (const mirror of anchor.mirrors) {
@@ -256,84 +303,3 @@ test("the always-on core prompt carries the condensed incident and verification
256
303
  assert.ok(core.includes(row), `assets/orchestrator.md is missing condensed trigger row: ${JSON.stringify(row)}`);
257
304
  }
258
305
  });
259
-
260
- // ---------------------------------------------------------------------------
261
- // 4 — Regeneration provenance: idempotent and fail-closed
262
- // ---------------------------------------------------------------------------
263
-
264
- // The fixture header claims provenance from the source commit, so regeneration
265
- // must be byte-reproducible: the same gentle-ai commit must not produce a
266
- // date-only diff. `generated_at` therefore tracks the source commit's committer
267
- // date instead of wall-clock time.
268
- test("fixture rendering is idempotent and derives generated_at from the source commit", () => {
269
- const sourceCommit = "e7729359fd9d6cb691ed2a88e8f72b1372f7c92e";
270
- const committerDate = "2026-09-18T13:49:03+02:00";
271
- const fakeGit = (args: readonly string[]): string => {
272
- switch (args[0]) {
273
- case "status":
274
- return "";
275
- case "rev-parse":
276
- return `${sourceCommit}\n`;
277
- case "show":
278
- return `${committerDate}\n`;
279
- default:
280
- throw new Error(`unexpected git invocation: ${args.join(" ")}`);
281
- }
282
- };
283
- const block = `${ODD_ROUTING_BLOCK_PREFIX}\n\n- a canonical clause\n`;
284
- const first = renderOddRoutingFixture(block, resolveOddRoutingProvenance("/fake/gentle-ai", fakeGit));
285
- const second = renderOddRoutingFixture(block, resolveOddRoutingProvenance("/fake/gentle-ai", fakeGit));
286
- assert.equal(first, second, "rendering the same source commit twice must be byte-identical");
287
- const { header } = parseOddRoutingFixture(first);
288
- assert.equal(header.source_commit, sourceCommit);
289
- assert.equal(
290
- header.generated_at,
291
- committerDate,
292
- "generated_at must track the source commit's committer date, not wall-clock time",
293
- );
294
- });
295
-
296
- // A dirty checkout would render uncommitted content under the committed
297
- // provenance claim, so the mirror fails closed before rendering and names the
298
- // tracked paths. Untracked files (the odd/tasks notes) must never block.
299
- test("the mirror fails closed on a dirty source checkout and names the tracked paths", () => {
300
- const recordedCalls: string[][] = [];
301
- const cleanGit = (args: readonly string[]): string => {
302
- recordedCalls.push([...args]);
303
- switch (args[0]) {
304
- case "status":
305
- return "";
306
- case "rev-parse":
307
- return `${"a".repeat(40)}\n`;
308
- case "show":
309
- return "2026-09-18T13:49:03+02:00\n";
310
- default:
311
- throw new Error(`unexpected git invocation: ${args.join(" ")}`);
312
- }
313
- };
314
- resolveOddRoutingProvenance("/fake/gentle-ai", cleanGit);
315
- assert.deepEqual(
316
- recordedCalls.find((args) => args[0] === "status"),
317
- ["status", "--porcelain", "--untracked-files=no"],
318
- "the dirty guard must ignore untracked files",
319
- );
320
-
321
- assert.doesNotThrow(() => assertCleanGentleAiCheckout(""));
322
- const dirtyGit = (args: readonly string[]): string => {
323
- if (args[0] === "status") {
324
- return " M internal/components/agentguidance/routing.go\n M scripts/other.mjs\n";
325
- }
326
- throw new Error(`unexpected git invocation: ${args.join(" ")}`);
327
- };
328
- assert.throws(
329
- () => resolveOddRoutingProvenance("/fake/gentle-ai", dirtyGit),
330
- (error: unknown) => {
331
- assert.ok(error instanceof Error);
332
- assert.match(
333
- error.message,
334
- /gentle-ai checkout is dirty; provenance would be unverifiable: internal\/components\/agentguidance\/routing\.go, scripts\/other\.mjs\. Commit or stash first\./,
335
- );
336
- return true;
337
- },
338
- );
339
- });