gentle-pi 4.1.0 → 4.3.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 (249) hide show
  1. package/README.md +352 -128
  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/orchestrator-delegation-reference.md +163 -0
  6. package/assets/orchestrator-delegation.md +25 -176
  7. package/assets/orchestrator-tracking.md +1 -1
  8. package/assets/orchestrator-verification.md +14 -8
  9. package/assets/orchestrator-writer.md +5 -1
  10. package/assets/orchestrator.md +9 -9
  11. package/bin/engram-guard +1 -1
  12. package/bin/gentle-shell.mjs +188 -35
  13. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  14. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
  15. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
  16. package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
  17. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
  18. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
  19. package/docs/assets/diagrams/agent-orchestration.svg +54 -15
  20. package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
  21. package/docs/assets/diagrams/measured-routing.svg +49 -0
  22. package/docs/assets/diagrams/native-review.svg +52 -12
  23. package/docs/assets/diagrams/odd-workflow.svg +128 -66
  24. package/docs/assets/diagrams/permission-modes.svg +51 -0
  25. package/docs/assets/features/audio-notifications.png +0 -0
  26. package/docs/assets/features/command-palette.png +0 -0
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +40 -8
  30. package/docs/delegated-verification.md +10 -8
  31. package/docs/gentle-agents-activity.md +11 -5
  32. package/docs/gentle-shell-uninstall.md +8 -1
  33. package/docs/gentle-shell.md +20 -12
  34. package/docs/install-wizard.md +17 -10
  35. package/docs/permission-modes.md +51 -6
  36. package/docs/readme-reference.md +155 -31
  37. package/docs/session-profile-format.md +39 -0
  38. package/docs/yolo-mode.md +21 -7
  39. package/extensions/ask-user-choice.ts +4 -0
  40. package/extensions/ask-user-question.ts +6 -1
  41. package/extensions/codegraph-tools.ts +68 -6
  42. package/extensions/gentle-agents.ts +146 -28
  43. package/extensions/gentle-ai.ts +380 -68
  44. package/extensions/gentle-herdr-activity.ts +2 -3
  45. package/extensions/gentle-shell.ts +53 -43
  46. package/extensions/gentle-todo.ts +62 -18
  47. package/extensions/quiet-tools.ts +1 -1
  48. package/extensions/session-scratch.ts +365 -0
  49. package/extensions/startup-banner.ts +2 -3
  50. package/lib/agent-home.ts +8 -3
  51. package/lib/agents-protocol.ts +3 -0
  52. package/lib/agents-runner.ts +55 -6
  53. package/lib/agents-view.ts +3 -1
  54. package/lib/auto-permission-answers.ts +133 -0
  55. package/lib/auto-permission-classifier.ts +215 -36
  56. package/lib/auto-permission-evidence.ts +88 -0
  57. package/lib/auto-permission-memory.ts +69 -0
  58. package/lib/auto-permission-runtime.ts +251 -35
  59. package/lib/auto-permission-tool-context.ts +61 -0
  60. package/lib/bounded-writer-admission.ts +51 -0
  61. package/lib/command-palette-catalog.ts +0 -4
  62. package/lib/commit-slice-signal.ts +100 -0
  63. package/lib/context-backstop.ts +84 -0
  64. package/lib/delivered-test-lint.ts +80 -0
  65. package/lib/destructive-command-guard.ts +215 -28
  66. package/lib/gauge-theme.ts +146 -0
  67. package/lib/gentle-ai-binary.ts +28 -7
  68. package/lib/gentle-shell-bundled.ts +62 -8
  69. package/lib/gentle-shell-import-pi.ts +233 -0
  70. package/lib/gentle-shell-launcher.ts +146 -12
  71. package/lib/gentle-shell-uninstall.ts +18 -1
  72. package/lib/herdr-activity.ts +24 -0
  73. package/lib/model-routing-authority.ts +6 -1
  74. package/lib/native-review-cli.ts +9 -1
  75. package/lib/orchestrator-discovery.ts +5 -1
  76. package/lib/orchestrator-presence.ts +9 -0
  77. package/lib/orchestrator-scope.ts +21 -3
  78. package/lib/provider-contract-bundle.ts +99 -53
  79. package/lib/review-candidate-view-owner.ts +29 -19
  80. package/lib/review-candidate-view.ts +36 -2
  81. package/lib/review-risk-assessment.ts +43 -38
  82. package/lib/review-session-standing-permission.ts +3 -1
  83. package/lib/selection-engine.ts +4 -1
  84. package/lib/session-profile-freeze.ts +40 -16
  85. package/lib/session-profile-persistence.ts +187 -0
  86. package/lib/session-scratch.ts +474 -0
  87. package/lib/shell-bar.ts +25 -25
  88. package/lib/shell-changes-view.ts +80 -13
  89. package/lib/shell-changes.ts +2 -0
  90. package/lib/shell-gauge.ts +49 -6
  91. package/lib/shell-prompt.ts +6 -4
  92. package/lib/shell-todo.ts +39 -8
  93. package/lib/shell-usage-view.ts +5 -1
  94. package/lib/shell-usage.ts +7 -7
  95. package/lib/verifier-admission.ts +29 -0
  96. package/lib/vim-editor-adapter.ts +22 -4
  97. package/lib/yolo-temp-scratch.ts +69 -0
  98. package/package.json +5 -6
  99. package/runtime/gentle-ai-binary.mjs +28 -7
  100. package/runtime/gentle-shell-bundled.mjs +61 -7
  101. package/runtime/gentle-shell-import-pi.mjs +234 -0
  102. package/runtime/gentle-shell-launcher.mjs +144 -10
  103. package/runtime/gentle-shell-uninstall.mjs +18 -1
  104. package/runtime/native-review-cli.mjs +8 -0
  105. package/runtime/review-risk-assessment.mjs +43 -38
  106. package/scripts/build-runtime-modules.mjs +1 -0
  107. package/scripts/bundled-engram-migration.mjs +125 -0
  108. package/scripts/gentle-ai-installer.mjs +10 -10
  109. package/scripts/install-wizard-preview.mjs +2 -2
  110. package/scripts/installer-downloads.mjs +7 -7
  111. package/scripts/installer-preflight.mjs +1 -1
  112. package/scripts/installer-server.mjs +14 -4
  113. package/scripts/main-channel.mjs +3 -2
  114. package/scripts/pi-update.mjs +223 -0
  115. package/scripts/run-test-suite.mjs +5 -0
  116. package/scripts/verify-package-files.mjs +19 -13
  117. package/skills/gentle-ai/SKILL.md +3 -3
  118. package/skills/work-unit-commits/SKILL.md +5 -3
  119. package/tests/agent-risk-escalation.test.ts +37 -10
  120. package/tests/agents-runner.test.ts +260 -3
  121. package/tests/append-system-prompt-route.test.ts +18 -1
  122. package/tests/auto-mode-human-answers.test.ts +68 -0
  123. package/tests/auto-mode-invariants.test.ts +23 -5
  124. package/tests/auto-mode-sdk.test.ts +29 -2
  125. package/tests/auto-permission-answers.test.ts +170 -0
  126. package/tests/auto-permission-classifier.test.ts +353 -2
  127. package/tests/auto-permission-compound.test.ts +24 -0
  128. package/tests/auto-permission-evidence.test.ts +88 -0
  129. package/tests/auto-permission-memory.test.ts +46 -0
  130. package/tests/auto-permission-public-regressions.test.ts +111 -0
  131. package/tests/auto-permission-runtime.test.ts +270 -12
  132. package/tests/autonomous-guard.test.ts +208 -16
  133. package/tests/bounded-writer-admission.test.ts +11 -3
  134. package/tests/bundled-install.test.ts +3 -3
  135. package/tests/card-content-policy.test.ts +19 -9
  136. package/tests/child-safety.test.ts +39 -0
  137. package/tests/codegraph-tools.test.ts +123 -2
  138. package/tests/command-palette.test.ts +14 -4
  139. package/tests/commit-slice-signal.test.ts +162 -0
  140. package/tests/context-backstop.test.ts +159 -0
  141. package/tests/delegate-for-reason-contract.test.ts +91 -24
  142. package/tests/delivered-test-lint.test.ts +105 -0
  143. package/tests/destructive-command-guard.test.ts +85 -1
  144. package/tests/engram-safety.test.ts +35 -7
  145. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
  146. package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
  147. package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
  148. package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
  149. package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
  150. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
  151. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
  152. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
  153. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
  154. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
  155. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
  156. package/tests/gauge-theme.test.ts +120 -0
  157. package/tests/gentle-agents.test.ts +443 -8
  158. package/tests/gentle-ai-binary.test.ts +1 -1
  159. package/tests/gentle-ai-child-guards.test.ts +61 -0
  160. package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
  161. package/tests/gentle-ai-installer.test.ts +51 -51
  162. package/tests/gentle-shell-bin.test.ts +180 -17
  163. package/tests/gentle-shell-bundled.test.ts +235 -38
  164. package/tests/gentle-shell-import-pi.test.ts +262 -0
  165. package/tests/gentle-shell-launcher.test.ts +97 -13
  166. package/tests/gentle-shell-uninstall.test.ts +30 -0
  167. package/tests/gentle-shell.test.ts +113 -14
  168. package/tests/gentle-todo.test.ts +258 -27
  169. package/tests/headless-delivery-question.test.ts +58 -0
  170. package/tests/herdr-activity.test.ts +34 -1
  171. package/tests/install-wizard.test.ts +2 -2
  172. package/tests/installer-posix-bootstrap.test.ts +8 -8
  173. package/tests/installer-preflight.test.ts +3 -3
  174. package/tests/installer-probes.test.ts +8 -8
  175. package/tests/installer-runner.test.ts +14 -13
  176. package/tests/installer-server.test.ts +13 -5
  177. package/tests/installer-windows-bootstrap.test.ts +33 -4
  178. package/tests/main-channel-upgrade.test.ts +7 -1
  179. package/tests/native-review-capability-contract.test.ts +7 -1
  180. package/tests/native-review-parity.test.ts +22 -1
  181. package/tests/no-self-pause-contract.test.ts +5 -2
  182. package/tests/odd-integration.test.ts +5 -1
  183. package/tests/odd-routing-canonical-ratchet.test.ts +127 -162
  184. package/tests/odd-routing-contract.test.ts +115 -30
  185. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  186. package/tests/orchestrator-budget.test.ts +19 -8
  187. package/tests/orchestrator-discovery.test.ts +29 -0
  188. package/tests/orchestrator-rdd-ownership.test.ts +3 -1
  189. package/tests/orchestrator-scope.test.ts +66 -0
  190. package/tests/orchestrator-status-publication-contract.test.ts +12 -8
  191. package/tests/package-manifest.test.ts +28 -24
  192. package/tests/pi-update-launcher.test.ts +106 -0
  193. package/tests/pi-update.test.ts +186 -0
  194. package/tests/primary-session-prompt.test.ts +101 -0
  195. package/tests/provider-contract-bundle.test.ts +118 -0
  196. package/tests/provider-contract-mirror.test.ts +24 -0
  197. package/tests/quiet-tool-rendering.test.ts +23 -0
  198. package/tests/rdd-aware-verification-contract.test.ts +29 -11
  199. package/tests/rdd-status-line.test.ts +131 -0
  200. package/tests/review-agent-end-preflight.test.ts +6 -0
  201. package/tests/review-candidate-view.test.ts +49 -0
  202. package/tests/review-contract-prompt.test.ts +5 -2
  203. package/tests/review-controller-native-routing.test.ts +14 -1
  204. package/tests/review-controller.test.ts +1 -1
  205. package/tests/review-host-relay-routing.test.ts +59 -2
  206. package/tests/review-loadout-rdd-off-sdk.test.ts +237 -0
  207. package/tests/review-owner-identity.test.ts +132 -0
  208. package/tests/review-risk-assessment.test.ts +163 -76
  209. package/tests/review-session-standing-permission-controller.test.ts +14 -1
  210. package/tests/review-session-standing-permission.test.ts +27 -1
  211. package/tests/run-test-suite.test.ts +15 -0
  212. package/tests/runtime-harness.mjs +16 -6
  213. package/tests/selection-engine.test.ts +35 -5
  214. package/tests/session-changes-shell.test.ts +1 -1
  215. package/tests/session-profile-freeze.test.ts +37 -1
  216. package/tests/session-profile-persistence.test.ts +334 -0
  217. package/tests/session-scratch.test.ts +945 -0
  218. package/tests/session-subject-routing-contract.test.ts +2 -1
  219. package/tests/shell-bar.test.ts +7 -0
  220. package/tests/shell-changes-view.test.ts +137 -1
  221. package/tests/shell-changes.test.ts +18 -0
  222. package/tests/shell-gauge.test.ts +42 -0
  223. package/tests/shell-todo.test.ts +15 -12
  224. package/tests/small-task-inline-contract.test.ts +37 -0
  225. package/tests/standalone-config-home.test.ts +324 -0
  226. package/tests/startup-banner.test.ts +5 -4
  227. package/tests/startup-home-repo.test.ts +34 -0
  228. package/tests/support/orchestrator-modules.ts +1 -0
  229. package/tests/task-size-routing-contract.test.ts +69 -9
  230. package/tests/test-scratch-contract.test.ts +102 -0
  231. package/tests/test-strength-contract.test.ts +36 -1
  232. package/tests/tool-batching-contract.test.ts +64 -0
  233. package/tests/verification-proportionality.test.ts +46 -0
  234. package/tests/verifier-admission.test.ts +137 -0
  235. package/tests/verify-agent-contract.test.ts +8 -8
  236. package/tests/vim-editor-adapter.test.ts +41 -15
  237. package/tests/windows-hidden-processes.test.ts +5 -0
  238. package/tests/writer-edit-surface-scope.test.ts +3 -0
  239. package/tests/writer-feature-document-admission.test.ts +156 -0
  240. package/tests/yolo-mode.test.ts +24 -1
  241. package/tests/yolo-temp-scratch.test.ts +71 -0
  242. package/scripts/mirror-odd-routing.mjs +0 -242
  243. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
  244. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
  245. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
  246. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  247. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
  248. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
  249. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -45,6 +45,26 @@ function fixture120Entries(): Map<string, Buffer> {
45
45
  return new Map(FIXTURE_1_2_0_PATHS.map((path) => [path, readFileSync(join(FIXTURE_1_2_0_ROOT, path))]));
46
46
  }
47
47
 
48
+ // Fixture provenance: the gentle-ai provider contract bundle 1.4.0 archive
49
+ // built by goreleaser (archives `review-provider-contract`) from gentle-ai
50
+ // 9e6f24f6, extracted. Adds `routing` to the manifest: Pi's ODD routing steps.
51
+ const FIXTURE_1_4_0_ROOT = join(import.meta.dirname, "fixtures", "provider-contract-bundle", "v1.4.0");
52
+ const FIXTURE_1_4_0_PATHS = [...FIXTURE_1_2_0_PATHS, "routing/pi.md"] as const;
53
+
54
+ function fixture140Entries(): Map<string, Buffer> {
55
+ return new Map(FIXTURE_1_4_0_PATHS.map((path) => [path, readFileSync(join(FIXTURE_1_4_0_ROOT, path))]));
56
+ }
57
+
58
+ // A valid 1.3.0-shaped bundle: the 1.4.0 fixture without the routing field and file.
59
+ function contractOneThreeEntries(): Map<string, Buffer> {
60
+ const entries = withManifest(fixture140Entries(), (manifest) => {
61
+ manifest.contract_semver = "1.3.0";
62
+ delete manifest.routing;
63
+ });
64
+ entries.delete("routing/pi.md");
65
+ return entries;
66
+ }
67
+
48
68
  function sha256Hex(bytes: Buffer): string {
49
69
  return createHash("sha256").update(bytes).digest("hex");
50
70
  }
@@ -391,6 +411,104 @@ test("rejects an orchestration array on a 1.1.0 manifest", () => {
391
411
  );
392
412
  });
393
413
 
414
+ // --- routing (contract 1.4.0) ----------------------------------------------------------
415
+
416
+ test("verifies the 1.4.0 fixture tree and exposes the mirrored pi routing text", () => {
417
+ const bundle = verifyProviderContractBundleTree(FIXTURE_1_4_0_ROOT);
418
+ assert.equal(bundle.contractSemver, "1.4.0");
419
+ assert.equal(bundle.entries.size, 10);
420
+ assert.deepEqual([...bundle.orchestration.keys()], [PI_RUNTIME_IDENTITY]);
421
+ assert.deepEqual([...bundle.routing.keys()], [PI_RUNTIME_IDENTITY]);
422
+ assert.equal(bundle.routing.get(PI_RUNTIME_IDENTITY), readFileSync(join(FIXTURE_1_4_0_ROOT, "routing/pi.md"), "utf8"));
423
+ assert.equal(bundle.entrySha256.get("routing/pi.md"), sha256Hex(readFileSync(join(FIXTURE_1_4_0_ROOT, "routing/pi.md"))));
424
+ });
425
+
426
+ test("verifies the 1.4.0 release archive in memory", () => {
427
+ withTemporaryDirectory((directory) => {
428
+ const archivePath = join(directory, "gentle-ai-review-provider-contract-1.4.0.tar.gz");
429
+ writeFileSync(archivePath, buildTarGz([...fixture140Entries()].map(([name, bytes]) => ({ name, bytes }))));
430
+ const { bundle } = verifyProviderContractBundleArchive(archivePath);
431
+ assert.equal(bundle.contractSemver, "1.4.0");
432
+ assert.deepEqual([...bundle.routing.keys()], [PI_RUNTIME_IDENTITY]);
433
+ });
434
+ });
435
+
436
+ test("a 1.3.0 bundle without routing still verifies with an empty routing inventory", () => {
437
+ const bundle = verifyProviderContractBundleEntries(contractOneThreeEntries());
438
+ assert.equal(bundle.contractSemver, "1.3.0");
439
+ assert.equal(bundle.entries.size, 9);
440
+ assert.equal(bundle.routing.size, 0);
441
+ assert.deepEqual([...bundle.orchestration.keys()], [PI_RUNTIME_IDENTITY]);
442
+ });
443
+
444
+ test("rejects a 1.4.0 manifest without the required routing array", () => {
445
+ const entries = withManifest(fixture140Entries(), (manifest) => {
446
+ delete manifest.routing;
447
+ });
448
+ assertRejects(entries, "manifest.routing is required from contract 1.4.0");
449
+ });
450
+
451
+ test("rejects a routing array on a 1.3.0 manifest", () => {
452
+ const entries = withManifest(fixture140Entries(), (manifest) => {
453
+ manifest.contract_semver = "1.3.0";
454
+ });
455
+ assertRejects(entries, "manifest.routing is not part of contract 1.3.0");
456
+ });
457
+
458
+ test("rejects a 1.4.0 routing entry whose runtime is not registered in manifest.runtimes", () => {
459
+ assertRejects(
460
+ withManifest(fixture140Entries(), (manifest) => {
461
+ const routing = manifest.routing as { runtime: string; file: { path: string } }[];
462
+ routing[0]!.runtime = "aider";
463
+ routing[0]!.file.path = "routing/aider.md";
464
+ }),
465
+ 'manifest.routing[0].runtime "aider" is not registered in manifest.runtimes',
466
+ );
467
+ });
468
+
469
+ test("rejects a 1.4.0 routing entry that points outside routing/<runtime>.md", () => {
470
+ assertRejects(
471
+ withManifest(fixture140Entries(), (manifest) => {
472
+ (manifest.routing as { file: { path: string } }[])[0]!.file.path = "orchestration/pi.md";
473
+ }),
474
+ 'manifest.routing[0].file.path must be "routing/pi.md"',
475
+ );
476
+ });
477
+
478
+ test("rejects a 1.4.0 routing entry whose file bytes do not match its manifest SHA-256", () => {
479
+ const entries = fixture140Entries();
480
+ const tampered = Buffer.from(entries.get("routing/pi.md") as Buffer);
481
+ tampered[tampered.length - 2] = 0x20;
482
+ entries.set("routing/pi.md", tampered);
483
+ assertRejects(entries, "does not match its manifest SHA-256");
484
+ });
485
+
486
+ test("rejects a 1.4.0 bundle missing its routing file", () => {
487
+ const entries = fixture140Entries();
488
+ entries.delete("routing/pi.md");
489
+ assertRejects(entries, "bundle inventory has 9 files; exactly 10 are expected");
490
+ });
491
+
492
+ test("the raw-entry bound leaves room for one routing file per runtime before the exact count decides", () => {
493
+ const entries = fixture140Entries();
494
+ for (let index = 0; index < 70; index += 1) entries.set(`extra/${index}.md`, Buffer.from("x\n"));
495
+ assertRejects(entries, "bundle inventory has 80 files; exactly 10 are expected");
496
+ });
497
+
498
+ test("generated 1.4.0 baselines record the mirrored routing entry", () => {
499
+ const bundle = verifyProviderContractBundleTree(FIXTURE_1_4_0_ROOT);
500
+ const capabilities = JSON.parse(
501
+ generateProviderContractBaselines(bundle).get("provider-capabilities.baseline.json") as string,
502
+ ) as { routing: { runtime: string; path: string; sha256: string }[] };
503
+ assert.deepEqual(capabilities.routing, [
504
+ { runtime: PI_RUNTIME_IDENTITY, path: "routing/pi.md", sha256: bundle.entrySha256.get("routing/pi.md") },
505
+ ]);
506
+ const older = JSON.parse(
507
+ generateProviderContractBaselines(verifyProviderContractBundleTree(FIXTURE_1_2_0_ROOT)).get("provider-capabilities.baseline.json") as string,
508
+ ) as { routing: unknown[] };
509
+ assert.deepEqual(older.routing, []);
510
+ });
511
+
394
512
  // --- archive safety ------------------------------------------------------------------
395
513
 
396
514
  test("rejects a tar entry that escapes the bundle root", () => {
@@ -31,6 +31,10 @@ const FIXTURE_PATHS = [
31
31
  // release archive, extracted. gentle-pi#560 / gentle-ai#4056, #4057.
32
32
  const FIXTURE_1_2_0_ROOT = join(import.meta.dirname, "fixtures", "provider-contract-bundle", "v1.2.0");
33
33
 
34
+ // Fixture provenance: the gentle-ai provider contract bundle 1.4.0 archive
35
+ // built by goreleaser, extracted. Adds Pi's ODD routing block as routing/pi.md.
36
+ const FIXTURE_1_4_0_ROOT = join(import.meta.dirname, "fixtures", "provider-contract-bundle", "v1.4.0");
37
+
34
38
  interface LockRecord {
35
39
  schema: string;
36
40
  acquisition: string;
@@ -124,6 +128,26 @@ test("mirrors a verified local 1.2.0 bundle tree, including its orchestration en
124
128
  });
125
129
  });
126
130
 
131
+ test("mirrors a verified local 1.4.0 bundle tree, including its routing entry, and the offline drift check passes", () => {
132
+ withTemporaryRoot((packageRoot) => {
133
+ mirrorProviderContractBundle(FIXTURE_1_4_0_ROOT, packageRoot);
134
+ const lock = JSON.parse(
135
+ readFileSync(join(packageRoot, PROVIDER_CONTRACT_MIRROR_ROOT, PROVIDER_CONTRACT_LOCK_FILE), "utf8"),
136
+ ) as LockRecord;
137
+ assert.equal(lock.contract_semver, "1.4.0");
138
+ assert.equal(Object.keys(lock.entries).length, 10);
139
+ assert.ok("routing/pi.md" in lock.entries, "the mirrored lock must record routing/pi.md");
140
+ const mirrored = join(packageRoot, PROVIDER_CONTRACT_MIRROR_ROOT, "v1.4.0", "bundle", "routing", "pi.md");
141
+ assert.deepEqual(readFileSync(mirrored), readFileSync(join(FIXTURE_1_4_0_ROOT, "routing", "pi.md")));
142
+ assert.deepEqual(checkProviderContractMirror(packageRoot), []);
143
+
144
+ writeFileSync(mirrored, `${readFileSync(mirrored, "utf8")}tampered\n`);
145
+ const problems = checkProviderContractMirror(packageRoot);
146
+ assert.equal(problems.length, 1);
147
+ assert.match(problems[0] ?? "", /mirrored bundle failed verification: .*"routing\/pi\.md" does not match its manifest SHA-256/);
148
+ });
149
+ });
150
+
127
151
  test("mirrors a verified local archive and records the archive digest in the lock", () => {
128
152
  withTemporaryRoot((packageRoot) => {
129
153
  const archivePath = join(packageRoot, "gentle-ai-review-provider-contract-1.1.0.tar.gz");
@@ -1232,6 +1232,29 @@ test("quiet tool rendering preserves complete expanded text and delegates saniti
1232
1232
  assert.doesNotMatch(image, /\x1b/);
1233
1233
  });
1234
1234
 
1235
+ test("quiet tool rendering re-renders expanded read-with-image results without crashing when lastComponent is passed (#1620)", () => {
1236
+ const tools = registeredQuietTools();
1237
+ const readTool = tools.get("read");
1238
+ const result = {
1239
+ content: [{ type: "image", data: "iVBORw0KGgo=", mimeType: "image/png" }],
1240
+ details: { path: "image.png" },
1241
+ };
1242
+ const context = { args: { path: "image.png" }, cwd: "/repo", showImages: false, isError: false };
1243
+
1244
+ const initialComponent = readTool.renderResult(result, { expanded: true, isPartial: false }, passthroughTheme, context);
1245
+ const initialOutput = renderToString(initialComponent);
1246
+ assert.ok(initialOutput.includes(imageFallback("image/png")));
1247
+
1248
+ const reRenderComponent = readTool.renderResult(
1249
+ result,
1250
+ { expanded: true, isPartial: false },
1251
+ passthroughTheme,
1252
+ { ...context, lastComponent: initialComponent },
1253
+ );
1254
+ const reRenderOutput = renderToString(reRenderComponent);
1255
+ assert.ok(reRenderOutput.includes(imageFallback("image/png")));
1256
+ });
1257
+
1235
1258
  test("quiet tool rendering preserves directional visual preview rows at narrow widths", () => {
1236
1259
  const tools = registeredQuietTools();
1237
1260
  const bash = tools.get("bash");
@@ -48,13 +48,18 @@ const ROUTING_SENTENCE =
48
48
  'After the writer returns, call `gentle_review` with `{"operation":"assess"}` over the writer\'s diff and follow the returned plan, whatever the `Receipt-driven development:` line reads (`on`, `off`, or `unknown`), instead of judging non-triviality from the task description: the operation resolves the native risk tier and states exactly who verifies next.';
49
49
  const RDD_ON_SENTENCE =
50
50
  "When the line reads `on`, the native review is an additional outside view of the change; it never replaces or skips the tier's verification, and the tier's verifier runs before review.";
51
- const TIER_TABLE_HEADER = "| Native risk tier | Verification (any RDD state) |";
51
+ // Verification by named risk changed the tier table and replaced the
52
+ // small-model bias sentence: the independent verifier runs only through an
53
+ // `escalate` naming a Task Size high-risk item; a native high, a failed
54
+ // assessment, and the writer's model or effort inform only.
55
+ const TIER_TABLE_HEADER = "| Tier | Verification (any RDD state) |";
52
56
  const PASSIVE_TIER_ROW = "| passive | structural readback by the parent; no separate verifier, no tests |";
53
- const MEDIUM_TIER_ROW = "| medium | writer self-verification stands; a separate `gentle-ai-verify` run is added only when the writer profile is a small model (mini or low effort) |";
54
- const HIGH_TIER_ROW = "| high | writer self-verification plus a separate `gentle-ai-verify` run, always |";
55
- const UNASSESSABLE_TIER_ROW = "| unknown / assess failed | treated as high |";
56
- const SMALL_MODEL_BIAS_SENTENCE =
57
- "The small-model bias raises the tier by one for verification purposes (medium becomes high); an unknown `Receipt-driven development:` line never lowers a tier below `off`.";
57
+ const MEDIUM_TIER_ROW = "| medium | writer self-verification stands; no separate verifier, whatever model wrote the change |";
58
+ const HIGH_TIER_ROW = "| high | only through `escalate` naming a Task Size high-risk item: writer self-verification plus one separate `gentle-ai-verify` run |";
59
+ const UNASSESSABLE_TIER_ROW = "| native high, unknown, or assess failed | informs only: writer self-verification stands; escalate when a named high-risk item applies |";
60
+ // odd/tasks/verifier-admission.md S6: the sentence names the runtime admission of the verifier launch.
61
+ const NAMED_RISK_SENTENCE =
62
+ "The independent verifier runs only when the change touches a named Task Size high-risk item, passed as `escalate` (the runtime admits its launch on a `Risk: item N` line, a feature-document task line, or past the context backstop); never for size, a native tier alone, a failed assessment, or the model or effort that wrote the change. An unknown `Receipt-driven development:` line never lowers a tier below `off`.";
58
63
  const SPOT_CHECK_SENTENCE =
59
64
  "The parent spot check (re-running one reported command before delivery) stays required in every tier.";
60
65
  test("trigger 5 (Verification rule) routes every RDD line through assess and the risk tier", () => {
@@ -74,8 +79,9 @@ test("trigger 5 states the native risk tier table exactly once, with all four ro
74
79
  }
75
80
  });
76
81
 
77
- test("trigger 5 states the small-model bias and the unknown-never-lowers-a-tier rule", () => {
78
- assert.ok(delegation.includes(SMALL_MODEL_BIAS_SENTENCE), "trigger 5 is missing the small-model bias sentence");
82
+ test("trigger 5 states the named-risk verifier rule and the unknown-never-lowers-a-tier rule", () => {
83
+ assert.ok(delegation.includes(NAMED_RISK_SENTENCE), "trigger 5 is missing the named-risk verifier sentence");
84
+ assert.ok(!delegation.includes("small-model bias"), "the retired small-model bias must not survive in any orchestrator module");
79
85
  });
80
86
 
81
87
  test("trigger 5 keeps the parent spot check requirement in every tier", () => {
@@ -99,10 +105,11 @@ test("trigger 5 never restates the retired #661 off/unknown non-trivial judgment
99
105
  assert.doesNotMatch(delegation, /purely passive documentation with no behavior to verify/);
100
106
  });
101
107
 
108
+ // S9 canon change (gentle-ai 87a3b2eb): the trigger section heading dropped "Mandatory".
102
109
  test("the Simple Delegation paragraph references trigger 3 instead of restating the on/off/unknown routing", () => {
103
110
  assert.match(
104
111
  delegation,
105
- /per the RDD-aware Verification rule \(trigger 3 under Mandatory Delegation Triggers, gentle-pi#661\)/,
112
+ /per the RDD-aware Verification rule \(trigger 3 under Delegation Triggers, gentle-pi#661\)/,
106
113
  );
107
114
  assert.match(delegation, /the normative on\/off\/unknown routing lives in `orchestrator-verification.md`, not here/);
108
115
  });
@@ -223,7 +230,9 @@ test("worker asset keeps the existing Return contract fields", () => {
223
230
  // delegation asset, and docs/delegated-verification.md -- and they must say
224
231
  // the same thing about the behavior ASSESS implements: closure only from the
225
232
  // native `candidate.consumed` fact for this candidate, the runtime writer
226
- // profile with a caller fallback, the conservative small-model bias, the
233
+ // profile with a caller fallback, a writer profile that never adds a verifier
234
+ // (verification by named risk retired the small-model bias), the escalate-only
235
+ // independent verifier, the
227
236
  // declined/unavailable/unknown fallback, and functional checks that a native
228
237
  // review never replaces. Assertions use short stable phrases, normalized for
229
238
  // case, quoting, and line wrapping, not whole paragraphs.
@@ -267,7 +276,8 @@ const SHARED_POLICY_PHRASES: ReadonlyArray<readonly [string, string]> = [
267
276
  ["caller closure claim is not authority", "a caller-supplied closed is not authority"],
268
277
  ["runtime writer profile", "runtime-recorded model and effort of the pending mutations"],
269
278
  ["caller profile is only a fallback", "only a fallback when no runtime evidence exists"],
270
- ["conservative small-model bias", "keeps the conservative small-model bias"],
279
+ ["writer profile never adds a verifier", "the writer profile is reported but never adds a verifier"],
280
+ ["independent verifier only through escalate", "only when escalate names a task size high-risk item"],
271
281
  ["native review never replaces verification (verify-always-rdd-high S1)", "native review is an additional outside view"],
272
282
  ["functional checks still run", "a native code review is not a substitute for applicable functional checks"],
273
283
  ];
@@ -280,6 +290,14 @@ test("the three verification policy surfaces carry the same ASSESS closure, prof
280
290
  }
281
291
  });
282
292
 
293
+ test("no verification policy surface still carries the retired small-model bias or unassessable-as-high rule", () => {
294
+ for (const [surface, text] of POLICY_SURFACES) {
295
+ for (const stale of ["small-model bias", "treated as high", "verified exactly like high", "fail closed to high"]) {
296
+ assert.ok(!text.includes(stale), `${surface} still carries the retired wording: "${stale}"`);
297
+ }
298
+ }
299
+ });
300
+
283
301
  test("no verification policy surface still tells callers to pass closed explicitly (gentle-pi#1175)", () => {
284
302
  for (const [surface, text] of POLICY_SURFACES) {
285
303
  for (const stale of ["closed is never derived", "closed is never auto-derived", "closed is never inferred", "pass it explicitly", "pass it only right after"]) {
@@ -2,7 +2,10 @@ import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import { __testing } from "../extensions/gentle-ai.ts";
4
4
  import {
5
+ NATIVE_REVIEW_ERROR_CODE,
5
6
  NATIVE_REVIEW_MODE_OPERATION,
7
+ NATIVE_REVIEW_OPERATION,
8
+ NativeReviewCliError,
6
9
  NATIVE_REVIEW_MODE_SOURCE,
7
10
  type NativeReviewCli,
8
11
  type NativeReviewModeRequest,
@@ -31,6 +34,7 @@ import {
31
34
 
32
35
  const {
33
36
  renderRddStatusLine,
37
+ pinRddStatusLine,
34
38
  resolveRddModeStatus,
35
39
  resolveRddStatusLine,
36
40
  getOrchestratorPrompt,
@@ -306,3 +310,130 @@ test("the on and off renders are never longer than the unknown (worst-case) rend
306
310
  }
307
311
  }
308
312
  });
313
+
314
+ // gentle-shell#1029: the rendered RDD line is part of the cached system-prompt
315
+ // prefix. A re-read after the TTL that fails or times out must not flip a
316
+ // known on/off line to "unknown" and back: the last successfully read status
317
+ // stays until the native reader reports a different status.
318
+ test("resolveRddModeStatus keeps the last known status when a later re-read fails", async () => {
319
+ clearRddStatusMemoForTesting();
320
+ const cwd = "/repo-sticky-status";
321
+ let clock = 1_000_000;
322
+ const now = () => clock;
323
+ let fail = false;
324
+ let calls = 0;
325
+ const cli: Pick<NativeReviewCli, "reviewMode"> = {
326
+ async reviewMode() {
327
+ calls += 1;
328
+ if (fail) throw new Error("native review CLI unavailable");
329
+ return modeResult("on", NATIVE_REVIEW_MODE_SOURCE.GLOBAL);
330
+ },
331
+ };
332
+ const first = await resolveRddModeStatus(cli, cwd, undefined, now);
333
+ assert.equal(first?.effective, "on");
334
+ fail = true;
335
+ clock += RDD_STATUS_MEMO_TTL_MS + 1;
336
+ const second = await resolveRddModeStatus(cli, cwd, undefined, now);
337
+ assert.equal(calls, 2, "the expired memo must still trigger a re-read");
338
+ assert.deepEqual(second, first, "a failed re-read must keep the last known status, not render unknown");
339
+ assert.equal(
340
+ await resolveRddStatusLine(cli, cwd, undefined, now),
341
+ "Receipt-driven development: on (decided by global)",
342
+ );
343
+ });
344
+
345
+ test("resolveRddModeStatus still follows a real status change after a re-read", async () => {
346
+ clearRddStatusMemoForTesting();
347
+ const cwd = "/repo-status-change";
348
+ let clock = 1_000_000;
349
+ const now = () => clock;
350
+ let effective: "on" | "off" = "on";
351
+ const cli: Pick<NativeReviewCli, "reviewMode"> = {
352
+ async reviewMode() { return modeResult(effective, NATIVE_REVIEW_MODE_SOURCE.GLOBAL); },
353
+ };
354
+ assert.equal((await resolveRddModeStatus(cli, cwd, undefined, now))?.effective, "on");
355
+ effective = "off";
356
+ clock += RDD_STATUS_MEMO_TTL_MS + 1;
357
+ assert.equal((await resolveRddModeStatus(cli, cwd, undefined, now))?.effective, "off");
358
+ });
359
+
360
+ test("resolveRddModeStatus stays unknown when no status was ever read", async () => {
361
+ clearRddStatusMemoForTesting();
362
+ const cli = fakeReviewMode(() => { throw new Error("never available"); });
363
+ assert.equal(await resolveRddModeStatus(cli, "/repo-never-known"), undefined);
364
+ });
365
+
366
+ // gentle-shell#2250: the one-shot unavailable notice must name the cause so a
367
+ // user on a slow or foreign-owned filesystem can tell a timeout from a native
368
+ // refusal instead of reading a generic "absent, timed out, or failed".
369
+ function captureNotices(): { ctx: { hasUI: true; ui: { notify(message: string, level: string): void } }; messages: string[] } {
370
+ const messages: string[] = [];
371
+ return { ctx: { hasUI: true, ui: { notify(message: string) { messages.push(message); } } }, messages };
372
+ }
373
+
374
+ test("the unavailable notice names the native failure and its stderr", async () => {
375
+ clearRddStatusMemoForTesting();
376
+ const { ctx, messages } = captureNotices();
377
+ const cli = fakeReviewMode(() => {
378
+ throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, NATIVE_REVIEW_OPERATION.MODE, true, false, "native process failed", {
379
+ operation: NATIVE_REVIEW_OPERATION.MODE,
380
+ error_code: NATIVE_REVIEW_ERROR_CODE.NON_ZERO,
381
+ exit_code: 1,
382
+ timed_out: false,
383
+ output_limit_exceeded: false,
384
+ stderr: "fatal: detected dubious ownership in repository",
385
+ });
386
+ });
387
+ assert.equal(await resolveRddModeStatus(cli, "/repo-native-failure", undefined, Date.now, ctx as never), undefined);
388
+ assert.equal(messages.length, 1);
389
+ assert.match(messages[0], /status is unavailable: native `gentle-ai review mode status` failed \(non-zero, exit 1\): fatal: detected dubious ownership in repository\./);
390
+ assert.match(messages[0], /keeps the last known status/);
391
+ });
392
+
393
+ test("the unavailable notice reports a timeout distinctly", async () => {
394
+ clearRddStatusMemoForTesting();
395
+ const { ctx, messages } = captureNotices();
396
+ const keepalive = setTimeout(() => {}, 2000);
397
+ try {
398
+ await resolveRddModeStatus(fakeReviewMode(() => new Promise<never>(() => {})), "/repo-timeout", AbortSignal.timeout(50), Date.now, ctx as never);
399
+ } finally {
400
+ clearTimeout(keepalive);
401
+ }
402
+ assert.equal(messages.length, 1);
403
+ assert.match(messages[0], new RegExp(`status is unavailable: native \`gentle-ai review mode status\` did not answer within ${RDD_STATUS_TIMEOUT_MS} ms\.`));
404
+ });
405
+
406
+ test("the unavailable notice reports an absent native review CLI", async () => {
407
+ clearRddStatusMemoForTesting();
408
+ const { ctx, messages } = captureNotices();
409
+ await resolveRddModeStatus(null, "/repo-absent", undefined, Date.now, ctx as never);
410
+ assert.equal(messages.length, 1);
411
+ assert.match(messages[0], /status is unavailable: the native gentle-ai review CLI is not available\./);
412
+ });
413
+
414
+ test("the unavailable notice still fires only once per session", async () => {
415
+ clearRddStatusMemoForTesting();
416
+ const { ctx, messages } = captureNotices();
417
+ const cli = fakeReviewMode(() => { throw new Error("boom"); });
418
+ await resolveRddModeStatus(cli, "/repo-once-a", undefined, Date.now, ctx as never);
419
+ await resolveRddModeStatus(cli, "/repo-once-b", undefined, Date.now, ctx as never);
420
+ assert.equal(messages.length, 1);
421
+ assert.match(messages[0], /status is unavailable: boom\./);
422
+ });
423
+
424
+ test("pinRddStatusLine keeps the session's first line until the off state changes (#2195)", () => {
425
+ const unknown = "Receipt-driven development: unknown (native status unavailable)";
426
+ const onGlobal = "Receipt-driven development: on (decided by global)";
427
+ const onClone = "Receipt-driven development: on (decided by clone)";
428
+ const off = "Receipt-driven development: off (decided by clone)";
429
+ const offGlobal = "Receipt-driven development: off (decided by global)";
430
+ assert.equal(pinRddStatusLine(undefined, onGlobal), onGlobal, "the first render of a session is the resolved line");
431
+ assert.equal(pinRddStatusLine(unknown, onGlobal), unknown, "unknown to on keeps the cached prompt prefix");
432
+ assert.equal(pinRddStatusLine(onGlobal, unknown), onGlobal, "on to unknown keeps the cached prompt prefix");
433
+ assert.equal(pinRddStatusLine(onGlobal, onClone), onGlobal, "a deciding-source change alone keeps the prefix");
434
+ assert.equal(pinRddStatusLine(off, offGlobal), off, "a source change while off keeps the prefix");
435
+ assert.equal(pinRddStatusLine(onGlobal, off), off, "turning RDD off re-renders, since it drops the review contract");
436
+ assert.equal(pinRddStatusLine(unknown, off), off, "unknown to off re-renders");
437
+ assert.equal(pinRddStatusLine(off, onGlobal), onGlobal, "turning RDD back on re-renders");
438
+ assert.equal(pinRddStatusLine(off, unknown), unknown, "off to unknown re-renders, since unknown keeps the review contract");
439
+ });
@@ -128,13 +128,18 @@ function toolContext(context: ExtensionContext): ExtensionToolContext {
128
128
  async function withSessionStartEnv<T>(callback: (cwd: string) => Promise<T>, initializeGit = true): Promise<T> {
129
129
  const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
130
130
  const previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
131
+ // Each call made three temp dirs and never removed them (87 left behind per suite run).
132
+ const created: string[] = [];
131
133
  process.env.GENTLE_PI_AGENT_HOME = await mkdtemp(join(tmpdir(), "gentle-pi-session-baseline-agent-home-"));
134
+ created.push(process.env.GENTLE_PI_AGENT_HOME);
132
135
  // Isolates both the model-config sweep and the dev-binary registration
133
136
  // lookup from this machine's real ~/.pi/gentle-ai, so `session_start`'s
134
137
  // unrelated notifications never leak into these assertions.
135
138
  process.env.GENTLE_PI_CONFIG_HOME = await mkdtemp(join(tmpdir(), "gentle-pi-session-baseline-config-home-"));
139
+ created.push(process.env.GENTLE_PI_CONFIG_HOME);
136
140
  try {
137
141
  const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-session-baseline-cwd-"));
142
+ created.push(cwd);
138
143
  if (initializeGit) childProcess.execFileSync("git", ["init", "--quiet", cwd]);
139
144
  await mkdir(join(cwd, "src"));
140
145
  await writeFile(join(cwd, "src/example.ts"), "export const value = 1;");
@@ -144,6 +149,7 @@ async function withSessionStartEnv<T>(callback: (cwd: string) => Promise<T>, ini
144
149
  else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
145
150
  if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
146
151
  else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
152
+ for (const dir of created) await rm(dir, { recursive: true, force: true });
147
153
  }
148
154
  }
149
155
 
@@ -1326,6 +1326,55 @@ test("candidate view recursively protects nested content and worktree metadata,
1326
1326
  }
1327
1327
  });
1328
1328
 
1329
+ // macOS File Provider (iCloud-synced Desktop/Documents) asynchronously resets
1330
+ // directory modes to exactly 0700 while leaving file modes intact.
1331
+ function simulateFileProviderDirectoryReset(root: string, directories: readonly string[]): void {
1332
+ for (const directory of directories) chmodSync(join(root, directory), 0o700);
1333
+ }
1334
+
1335
+ test("candidate view repairs owner-only directories reset by macOS File Provider and stays verifiable", { skip: process.platform === "win32" }, (t) => {
1336
+ const contributorRoot = repository(t);
1337
+ mkdirSync(join(contributorRoot, "nested", "deeper"), { recursive: true });
1338
+ writeFileSync(join(contributorRoot, "nested", "deeper", "candidate.txt"), "candidate\n");
1339
+ const view = createCandidateView({ contributorRoot });
1340
+ try {
1341
+ simulateFileProviderDirectoryReset(view.root, ["nested/deeper", "nested", "."]);
1342
+ view.verify();
1343
+ for (const directory of [".", "nested", "nested/deeper"]) assert.equal(lstatSync(join(view.root, directory)).mode & 0o777, 0o555, directory);
1344
+ assert.equal(lstatSync(join(view.root, "nested", "deeper", "candidate.txt")).mode & 0o777, 0o444);
1345
+ } finally {
1346
+ view.cleanup();
1347
+ }
1348
+ });
1349
+
1350
+ test("candidate view does not repair owner-only directories carrying special mode bits", { skip: process.platform === "win32" }, (t) => {
1351
+ const contributorRoot = repository(t);
1352
+ mkdirSync(join(contributorRoot, "nested"), { recursive: true });
1353
+ writeFileSync(join(contributorRoot, "nested", "candidate.txt"), "candidate\n");
1354
+ const view = createCandidateView({ contributorRoot });
1355
+ try {
1356
+ chmodSync(join(view.root, "nested"), 0o1700);
1357
+ assert.throws(() => view.verify(), /candidate view directory is unsafe or writable/);
1358
+ } finally {
1359
+ view.cleanup();
1360
+ }
1361
+ });
1362
+
1363
+ test("candidate view refuses an ignored entry injected into an owner-only directory it would repair", { skip: process.platform === "win32" }, (t) => {
1364
+ const contributorRoot = repository(t);
1365
+ mkdirSync(join(contributorRoot, "nested"), { recursive: true });
1366
+ writeFileSync(join(contributorRoot, ".gitignore"), "*.log\n");
1367
+ writeFileSync(join(contributorRoot, "nested", "candidate.txt"), "candidate\n");
1368
+ const view = createCandidateView({ contributorRoot });
1369
+ try {
1370
+ simulateFileProviderDirectoryReset(view.root, ["nested"]);
1371
+ writeFileSync(join(view.root, "nested", "injected.log"), "injected\n");
1372
+ assert.throws(() => view.verify(), /candidate view directory contains unexpected entries/);
1373
+ } finally {
1374
+ view.cleanup();
1375
+ }
1376
+ });
1377
+
1329
1378
  test("candidate view registry rejects unsafe, moved, writable, stale, and unselected lens contexts before dispatch", (t) => {
1330
1379
  const contributorRoot = repository(t);
1331
1380
  writeFileSync(join(contributorRoot, "tracked.txt"), "candidate\n");
@@ -109,9 +109,12 @@ test("before_agent_start injects the mirrored review execution contract for the
109
109
  assert.match(appended, /no meaningful RED, explain why and run proportionate ordinary functional or structural verification/);
110
110
  assert.match(appended, /Never invent lifecycle evidence or skip checks/);
111
111
  assert.doesNotMatch(appended, /If tests exist, use strict TDD/);
112
- assert.match(appended, /ODD \(Default Workflow, harness section above\) is mandatory on every request/);
112
+ // S11 T3: deduplicated. The core pointer sentence restated the harness; the
113
+ // mandatory ODD entry is injected once, from the harness steps' header.
114
+ assert.match(appended, /Organic Driven Development \(ODD\) is the predefined workflow of this orchestrator\. Every request enters it/);
115
+ assert.doesNotMatch(appended, /ODD \(Default Workflow, harness section above\)/);
113
116
  assert.doesNotMatch(appended, /Prefer SDD\/OpenSpec artifacts/);
114
- assert.match(appended, /## Gentle AI review execution contract \(mirrored provider bundle 1\.3\.0\)/);
117
+ assert.match(appended, /## Gentle AI review execution contract \(mirrored provider bundle 1\.4\.0\)/);
115
118
  assert.ok(appended.includes(expected), "the mirrored orchestration/pi.md text must appear verbatim");
116
119
  assert.match(appended, /call `gentle_review` with {"operation":"inspect"}/);
117
120
  assert.match(appended, /call `gentle_review` with operation `status`, the exact retained `lineageId`, and `workspaceRoot`/);
@@ -6,13 +6,24 @@ import fsp from "node:fs/promises";
6
6
  import os, { tmpdir } from "node:os";
7
7
  import { syncBuiltinESMExports } from "node:module";
8
8
  import { dirname, join, resolve, sep } from "node:path";
9
- import test from "node:test";
9
+ import test, { after, before } from "node:test";
10
+ import { setWindowsAclAuthorityForTesting } from "../lib/review-candidate-view-owner.ts";
10
11
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
11
12
  import { __testing, createGentleAiExtension, PendingReviewConsentRegistry } from "../extensions/gentle-ai.ts";
12
13
  import { CandidateViewError, CandidateViewRegistry } from "../lib/review-candidate-view.ts";
13
14
  import { NATIVE_REVIEW_ERROR_CODE, NativeReviewCliError, NativeReviewConsentRequiredError, type NativeReviewCli } from "../lib/native-review-cli.ts";
14
15
  import { decodeReviewLastEventClosureV1, decodeReviewConsentV3, decodeReviewStatusV3, type ReviewCollectInputV3, type ReviewStatusV3 } from "../lib/review-integration-v2.ts";
15
16
 
17
+ // Coordination tests use fixture authority; real Windows ACL coverage lives
18
+ // in review-candidate-view.test.ts, in a separate test process.
19
+ let fixtureAclChecks = 0;
20
+ before(() => {
21
+ if (process.platform === "win32") {
22
+ setWindowsAclAuthorityForTesting(() => { fixtureAclChecks++; });
23
+ }
24
+ });
25
+ after(() => setWindowsAclAuthorityForTesting());
26
+
16
27
  const SHA = `sha256:${"a".repeat(64)}`;
17
28
  const TREE = "b".repeat(40);
18
29
 
@@ -1076,7 +1087,9 @@ function reviewContext(cwd: string): ExtensionContext {
1076
1087
 
1077
1088
  function startStatus(cwd: string, baseRef?: string, intendedUntracked: readonly string[] = []): ReviewStatusV3 {
1078
1089
  const candidateViews = new CandidateViewRegistry();
1090
+ const checksBefore = fixtureAclChecks;
1079
1091
  const view = candidateViews.create({ contributorRoot: cwd, intendedUntracked, ...(baseRef === undefined ? {} : { baseRef, committedOnly: true }) });
1092
+ if (process.platform === "win32") assert.ok(fixtureAclChecks > checksBefore, "coordination candidates must use the fixture ACL authority");
1080
1093
  try {
1081
1094
  return {
1082
1095
  contract: "gentle-ai.review-integration/v2",
@@ -406,7 +406,7 @@ test("shipped controller fails closed while static prompts defer RDD lifecycle o
406
406
  const core = readFileSync("assets/orchestrator.md", "utf8");
407
407
  assert.match(core, new RegExp(boundary.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
408
408
 
409
- for (const path of ["assets/orchestrator-delegation.md", "skills/gentle-ai/SKILL.md"]) {
409
+ for (const path of ["assets/orchestrator-delegation.md", "assets/orchestrator-delegation-reference.md", "skills/gentle-ai/SKILL.md"]) {
410
410
  const contract = readFileSync(path, "utf8");
411
411
  assert.doesNotMatch(contract, /INSPECT before START|start -> finalize -> validate|next_transition|review\.capture-result/is, path);
412
412
  }