gentle-pi 2.5.0 → 2.6.1

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 (188) hide show
  1. package/README.md +139 -30
  2. package/assets/agents/gentle-ai-worker.md +4 -0
  3. package/assets/agents/jd-fix-agent.md +18 -0
  4. package/assets/agents/jd-judge-a.md +1 -1
  5. package/assets/agents/jd-judge-b.md +1 -1
  6. package/assets/agents/sdd-apply.md +7 -5
  7. package/assets/agents/sdd-archive.md +5 -3
  8. package/assets/agents/sdd-design.md +4 -0
  9. package/assets/agents/sdd-explore.md +4 -0
  10. package/assets/agents/sdd-init.md +4 -0
  11. package/assets/agents/sdd-onboard.md +4 -0
  12. package/assets/agents/sdd-proposal.md +4 -0
  13. package/assets/agents/sdd-remediate.md +37 -0
  14. package/assets/agents/sdd-research.md +26 -3
  15. package/assets/agents/sdd-spec.md +4 -0
  16. package/assets/agents/sdd-status.md +9 -75
  17. package/assets/agents/sdd-sync.md +4 -0
  18. package/assets/agents/sdd-tasks.md +4 -0
  19. package/assets/agents/sdd-verify.md +5 -3
  20. package/assets/chains/sdd-full.chain.md +4 -0
  21. package/assets/chains/sdd-plan.chain.md +4 -0
  22. package/assets/chains/sdd-verify.chain.md +4 -0
  23. package/assets/migrations/managed-assets-v2.5.0.json +7 -0
  24. package/assets/orchestrator-delegation.md +21 -3
  25. package/assets/sdd-orchestrator-workflow.md +54 -21
  26. package/assets/support/sdd-status-contract.md +34 -90
  27. package/contracts/telemetry/runtime-aggregate-v1.schema.json +67 -0
  28. package/docs/telemetry.md +57 -1
  29. package/docs/windows-startup-console-visibility.md +18 -0
  30. package/extensions/ask-user-choice.ts +143 -15
  31. package/extensions/codegraph-tools.ts +1 -0
  32. package/extensions/gentle-agents.ts +795 -50
  33. package/extensions/gentle-ai.ts +2033 -322
  34. package/extensions/gentle-shell.ts +145 -42
  35. package/extensions/gentle-todo.ts +47 -12
  36. package/extensions/quiet-tools.ts +1 -0
  37. package/extensions/runtime-metrics.ts +121 -0
  38. package/extensions/sdd-init.ts +2 -2
  39. package/extensions/startup-banner.ts +52 -75
  40. package/lib/agent-profiles.ts +550 -0
  41. package/lib/agents-completion-delivery.ts +72 -0
  42. package/lib/agents-config.ts +7 -10
  43. package/lib/agents-history.ts +9 -1
  44. package/lib/agents-messaging.ts +187 -0
  45. package/lib/agents-protocol.ts +77 -5
  46. package/lib/agents-runner.ts +548 -26
  47. package/lib/agents-thread-view.ts +57 -0
  48. package/lib/agents-view-layout.ts +40 -0
  49. package/lib/agents-view.ts +548 -191
  50. package/lib/agents-widget.ts +33 -14
  51. package/lib/gentle-ai-binary.ts +3 -1
  52. package/lib/gentle-ai-renderer.ts +8 -6
  53. package/lib/native-review-cli.ts +288 -1
  54. package/lib/orchestrator-presence.ts +337 -0
  55. package/lib/profiles-orchestrator.ts +203 -0
  56. package/lib/review-candidate-view-owner.ts +296 -46
  57. package/lib/review-candidate-view.ts +30 -20
  58. package/lib/review-consent-component.ts +247 -0
  59. package/lib/review-consent-ui.ts +53 -8
  60. package/lib/review-host-relay.ts +28 -0
  61. package/lib/review-integration-v2.ts +187 -5
  62. package/lib/review-last-event-controller.ts +7 -4
  63. package/lib/review-reminder-receipt.ts +74 -0
  64. package/lib/review-session-standing-permission.ts +27 -6
  65. package/lib/runtime-metrics-children.ts +197 -0
  66. package/lib/runtime-metrics-delivery.ts +68 -0
  67. package/lib/runtime-metrics-native.ts +174 -0
  68. package/lib/runtime-metrics-policy.ts +51 -0
  69. package/lib/runtime-metrics.ts +308 -0
  70. package/lib/sdd-preflight.ts +362 -81
  71. package/lib/sdd-research-capabilities.ts +228 -0
  72. package/lib/sdd-status.ts +29 -7
  73. package/lib/session-worktree-registry.ts +118 -0
  74. package/lib/shell-bar.ts +47 -1
  75. package/lib/shell-card.ts +1 -4
  76. package/lib/shell-changes-view.ts +362 -37
  77. package/lib/shell-changes.ts +81 -1
  78. package/lib/shell-prompt.ts +11 -15
  79. package/lib/shell-sidebar-banner.ts +11 -0
  80. package/lib/shell-sidebar-layout.ts +213 -0
  81. package/lib/shell-sidebar.ts +41 -0
  82. package/lib/shell-todo.ts +28 -11
  83. package/lib/telemetry-trigger.ts +2 -0
  84. package/package.json +6 -3
  85. package/runtime/gentle-ai-binary.mjs +3 -1
  86. package/runtime/native-review-cli.mjs +288 -1
  87. package/runtime/review-integration-v2.mjs +187 -5
  88. package/runtime/telemetry-trigger.mjs +2 -0
  89. package/scripts/build-runtime-modules.mjs +9 -1
  90. package/scripts/check-types.mjs +125 -0
  91. package/scripts/gentle-ai-installer.mjs +10 -10
  92. package/scripts/install-gentle-ai.mjs +12 -0
  93. package/scripts/install-tui-mode-setting.mjs +114 -0
  94. package/scripts/test-packed-runner.mjs +16 -2
  95. package/scripts/types-baseline.json +95 -0
  96. package/scripts/verify-package-files.mjs +4 -2
  97. package/skills/_shared/review-ledger-contract.md +17 -1
  98. package/skills/issue-creation/SKILL.md +3 -3
  99. package/skills/judgment-day/SKILL.md +17 -3
  100. package/skills/judgment-day/references/prompts-and-formats.md +14 -3
  101. package/tests/agent-profiles.test.ts +722 -0
  102. package/tests/agents-completion-delivery.test.ts +94 -0
  103. package/tests/agents-config.test.ts +62 -0
  104. package/tests/agents-fake-child.ts +15 -1
  105. package/tests/agents-grouping.test.ts +179 -0
  106. package/tests/agents-integration.test.ts +100 -0
  107. package/tests/agents-messaging.test.ts +94 -0
  108. package/tests/agents-protocol.test.ts +45 -0
  109. package/tests/agents-queries.test.ts +190 -0
  110. package/tests/agents-responsive.test.ts +43 -0
  111. package/tests/agents-runner.test.ts +571 -14
  112. package/tests/agents-thread-view.test.ts +45 -0
  113. package/tests/agents-view.test.ts +476 -65
  114. package/tests/agents-widget.test.ts +31 -1
  115. package/tests/artifact-language.test.ts +25 -2
  116. package/tests/ask-user-choice.test.ts +169 -3
  117. package/tests/asset-installation-runtime.test.ts +108 -0
  118. package/tests/autonomous-guard.test.ts +116 -1
  119. package/tests/codegraph-tools.test.ts +2 -1
  120. package/tests/delegated-key-learnings-contract.test.ts +1 -1
  121. package/tests/devbinary/native-review-parity.devtest.ts +2 -0
  122. package/tests/feature-request-form.test.ts +67 -0
  123. package/tests/fixtures/agents-messaging-child.mjs +5 -0
  124. package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
  125. package/tests/gentle-agents.test.ts +1454 -33
  126. package/tests/gentle-ai-binary.test.ts +7 -2
  127. package/tests/gentle-ai-installer.test.ts +47 -47
  128. package/tests/gentle-ai-renderer.test.ts +38 -0
  129. package/tests/gentle-ai.test.ts +944 -4
  130. package/tests/gentle-shell.test.ts +303 -12
  131. package/tests/gentle-todo.test.ts +54 -10
  132. package/tests/install-tui-mode-setting.test.ts +324 -0
  133. package/tests/issue-creation-skill.test.ts +22 -0
  134. package/tests/model-routing-authority.test.ts +12 -0
  135. package/tests/native-review-capability-contract.test.ts +23 -1
  136. package/tests/native-review-cli.test.ts +277 -3
  137. package/tests/native-review-parity.test.ts +14 -7
  138. package/tests/native-sdd-attempt-authority.test.ts +7 -2
  139. package/tests/orchestrator-presence.test.ts +389 -0
  140. package/tests/package-manifest.test.ts +232 -7
  141. package/tests/profiles-orchestrator.test.ts +208 -0
  142. package/tests/quiet-tool-rendering.test.ts +1 -0
  143. package/tests/rdd-aware-verification-contract.test.ts +10 -0
  144. package/tests/review-agent-end-preflight.test.ts +332 -24
  145. package/tests/review-candidate-view.test.ts +304 -6
  146. package/tests/review-consent-ui.test.ts +352 -0
  147. package/tests/review-contract-prompt.test.ts +14 -0
  148. package/tests/review-controller-native-routing.test.ts +563 -3
  149. package/tests/review-controller.test.ts +1 -1
  150. package/tests/review-host-relay-restart-parity.test.ts +142 -1
  151. package/tests/review-host-relay-routing.test.ts +364 -4
  152. package/tests/review-host-relay.test.ts +29 -0
  153. package/tests/review-integration-v2-forward.test.ts +44 -0
  154. package/tests/review-integration-v2.test.ts +164 -0
  155. package/tests/review-last-event-closure.test.ts +105 -1
  156. package/tests/review-ledger-contract.test.ts +61 -6
  157. package/tests/review-reminder-receipt.test.ts +62 -0
  158. package/tests/review-session-standing-permission-controller.test.ts +52 -4
  159. package/tests/review-session-standing-permission.test.ts +30 -0
  160. package/tests/runtime-harness.mjs +447 -39
  161. package/tests/runtime-metrics-children.test.ts +207 -0
  162. package/tests/runtime-metrics-delivery.test.ts +85 -0
  163. package/tests/runtime-metrics-extension.test.ts +196 -0
  164. package/tests/runtime-metrics-model.test.ts +76 -0
  165. package/tests/runtime-metrics-native.test.ts +275 -0
  166. package/tests/runtime-metrics-policy.test.ts +62 -0
  167. package/tests/runtime-metrics.test.ts +187 -0
  168. package/tests/sdd-agent-tools.test.ts +10 -1
  169. package/tests/sdd-execution-routing-contract.test.ts +28 -0
  170. package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
  171. package/tests/sdd-native-managed-uptake.test.ts +253 -0
  172. package/tests/sdd-planning-routing-contract.test.ts +45 -0
  173. package/tests/sdd-preflight.test.ts +252 -8
  174. package/tests/sdd-research-capabilities.test.ts +256 -0
  175. package/tests/sdd-research-live.test.ts +241 -0
  176. package/tests/sdd-selection-transport.test.ts +504 -0
  177. package/tests/sdd-status.test.ts +51 -0
  178. package/tests/session-worktree-registry.test.ts +135 -0
  179. package/tests/shell-card.test.ts +24 -3
  180. package/tests/shell-changes-view.test.ts +471 -8
  181. package/tests/shell-changes.test.ts +168 -0
  182. package/tests/shell-prompt.test.ts +28 -6
  183. package/tests/shell-sidebar-banner.test.ts +23 -0
  184. package/tests/shell-sidebar-layout.test.ts +387 -0
  185. package/tests/shell-sidebar.test.ts +50 -0
  186. package/tests/shell-todo.test.ts +100 -11
  187. package/tests/startup-banner.test.ts +126 -0
  188. package/tests/telemetry-trigger.test.ts +3 -1
@@ -15,7 +15,7 @@ import test from "node:test";
15
15
  import { fileURLToPath } from "node:url";
16
16
  import { applyModelConfig } from "../extensions/gentle-ai.ts";
17
17
  import { resolveGentlePiAgentHome } from "../lib/agent-home.ts";
18
- import { installSddAssets } from "../lib/sdd-preflight.ts";
18
+ import { getPackageAssetOwner, installPackageAssets, installSddAssets, type PackageAssetOwner } from "../lib/sdd-preflight.ts";
19
19
 
20
20
  const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
21
21
  const MANAGED_EXEMPLAR_FILE = "gentle-ai-explore.md";
@@ -229,8 +229,12 @@ test("generated runtime modules and packed-package checks are deterministic", ()
229
229
  test("package manifest ships and runs the checked-in package-local Gentle AI installer", () => {
230
230
  const packageJson = readPackageJson();
231
231
  const verifier = readFileSync(join(PACKAGE_ROOT, "scripts", "verify-package-files.mjs"), "utf8");
232
+ const readme = readFileSync(join(PACKAGE_ROOT, "README.md"), "utf8");
232
233
 
233
234
  assert.equal(packageJson.scripts?.postinstall, "node scripts/install-gentle-ai.mjs");
235
+ assert.match(readme, /run `node scripts\/install-gentle-ai\.mjs`/, "missing-binary recovery documentation must use the package postinstall entrypoint");
236
+ assert.match(readme, /installed `gentle-pi` package directory/, "recovery documentation must name the package working directory");
237
+ assert.match(readme, /if `GENTLE_PI_SKIP_GENTLE_AI_INSTALL` is set, remove or unset it before/i, "recovery documentation must prevent the installer skip from repeating");
234
238
  assert.ok(packageJson.files?.includes("scripts/"));
235
239
  assert.match(verifier, /"scripts\/install-gentle-ai\.mjs"/);
236
240
  assert.match(verifier, /"scripts\/gentle-ai-installer\.mjs"/);
@@ -273,20 +277,20 @@ test("package manifest installs pi-pretty through a wrapper without bundling nat
273
277
  );
274
278
  });
275
279
 
276
- test("package verification binds the published Gentle AI v2.7.0 runtime pin", () => {
280
+ test("package verification binds the published Gentle AI v2.8.1 runtime pin", () => {
277
281
  const installer = readFileSync(join(PACKAGE_ROOT, "scripts", "gentle-ai-installer.mjs"), "utf8");
278
282
  const binary = readFileSync(join(PACKAGE_ROOT, "lib", "gentle-ai-binary.ts"), "utf8");
279
283
  const verifier = readFileSync(join(PACKAGE_ROOT, "scripts", "verify-package-files.mjs"), "utf8");
280
284
 
281
- assert.match(installer, /INSTALLER_VERSION = "2\.7\.0"/);
285
+ assert.match(installer, /INSTALLER_VERSION = "2\.8\.1"/);
282
286
  assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_PACKAGE.*GENTLE_AI_WINDOWS_SOURCE_MODULE/);
283
- assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:SE4KCLo3y1qWaTq\+7HJKZ1DkAAAl\+w\+82YY0\+qQkFwM="/);
287
+ assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:EixFRy7P3JLVi1ovr18pMxQIrtjDib9i7VEH9g4XRVE="/);
284
288
  assert.match(installer, /GOTOOLCHAIN: "local"/);
285
289
  assert.match(installer, /GOSUMDB: "sum\.golang\.org"/);
286
290
  assert.match(binary, /GENTLE_AI_VERSION = INSTALLER_VERSION/);
287
291
  assert.match(binary, /GO_SUMDB_SOURCE_BUILD/);
288
292
  assert.match(binary, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM/);
289
- assert.match(verifier, /v2\.7\.0/);
293
+ assert.match(verifier, /v2\.8\.1/);
290
294
  });
291
295
 
292
296
 
@@ -469,6 +473,222 @@ test("packaged agents declare only tool names a Pi child session can resolve", (
469
473
  }
470
474
  });
471
475
 
476
+ function withIsolatedAssetHome(run: (agentHome: string) => void): void {
477
+ const temporary = mkdtempSync(join(tmpdir(), "gentle-asset-owners-"));
478
+ const previous = process.env.GENTLE_PI_AGENT_HOME;
479
+ try {
480
+ process.env.GENTLE_PI_AGENT_HOME = temporary;
481
+ run(temporary);
482
+ } finally {
483
+ if (previous === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
484
+ else process.env.GENTLE_PI_AGENT_HOME = previous;
485
+ rmSync(temporary, { recursive: true, force: true });
486
+ }
487
+ }
488
+
489
+ function installedAssetManifest(agentHome: string): ManagedAssetsManifest {
490
+ return JSON.parse(readFileSync(join(agentHome, "gentle-ai", "managed-assets.json"), "utf8"));
491
+ }
492
+
493
+ test("selective delegation installation owns only generic agents", () => {
494
+ withIsolatedAssetHome((agentHome) => {
495
+ const result = installPackageAssets(agentHome, false, ["delegation"]);
496
+ assert.deepEqual(Object.keys(installedAssetManifest(agentHome).assets).sort(), [
497
+ "agents/gentle-ai-explore.md",
498
+ "agents/gentle-ai-verify.md",
499
+ "agents/gentle-ai-worker.md",
500
+ ]);
501
+ assert.deepEqual(result, { agents: 3, chains: 0, support: 0, skipped: 0 });
502
+ assert.deepEqual(readdirSync(join(agentHome, "agents")).sort(), [
503
+ "gentle-ai-explore.md", "gentle-ai-verify.md", "gentle-ai-worker.md",
504
+ ]);
505
+ assert.equal(existsSync(join(agentHome, "chains")), false);
506
+ assert.equal(existsSync(join(agentHome, "gentle-ai", "support")), false);
507
+ });
508
+ });
509
+
510
+ test("selective installation retires only assets belonging to the selected owner", () => {
511
+ withIsolatedAssetHome((agentHome) => {
512
+ installSddAssets(agentHome, false);
513
+ const manifestPath = join(agentHome, "gentle-ai", "managed-assets.json");
514
+ const manifest = installedAssetManifest(agentHome);
515
+ for (const name of RETIRED_ADVERSARIAL_AGENTS) {
516
+ writeFileSync(join(agentHome, "agents", name), "Previously managed review agent\n");
517
+ manifest.assets[`agents/${name}`] = sha256("Previously managed review agent\n");
518
+ }
519
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
520
+ for (const owner of ["delegation", "sdd"] as const) {
521
+ installPackageAssets(agentHome, true, [owner]);
522
+ assert.deepEqual(installedAssetManifest(agentHome), manifest);
523
+ for (const name of RETIRED_ADVERSARIAL_AGENTS) {
524
+ assert.equal(readFileSync(join(agentHome, "agents", name), "utf8"), "Previously managed review agent\n");
525
+ }
526
+ }
527
+ writeFileSync(join(agentHome, "agents", "review-validator.md"), "User-modified retired agent\n");
528
+ installPackageAssets(agentHome, false, ["review"]);
529
+ assert.equal(existsSync(join(agentHome, "agents", "review-refuter.md")), false);
530
+ assert.equal(readFileSync(join(agentHome, "agents", "review-validator.md"), "utf8"), "User-modified retired agent\n");
531
+ for (const name of RETIRED_ADVERSARIAL_AGENTS) {
532
+ assert.equal(installedAssetManifest(agentHome).assets[`agents/${name}`], undefined);
533
+ }
534
+ });
535
+ });
536
+
537
+ const EXPECTED_OWNER_ASSETS: Record<PackageAssetOwner, readonly string[]> = {
538
+ delegation: [
539
+ "agents/gentle-ai-explore.md", "agents/gentle-ai-verify.md", "agents/gentle-ai-worker.md",
540
+ ],
541
+ review: [
542
+ "agents/jd-fix-agent.md", "agents/jd-judge-a.md", "agents/jd-judge-b.md",
543
+ "agents/review-readability.md", "agents/review-reliability.md",
544
+ "agents/review-resilience.md", "agents/review-risk.md", "chains/4r-review.chain.md",
545
+ ],
546
+ sdd: [
547
+ "agents/sdd-apply.md", "agents/sdd-archive.md", "agents/sdd-design.md",
548
+ "agents/sdd-explore.md", "agents/sdd-init.md", "agents/sdd-onboard.md",
549
+ "agents/sdd-proposal.md", "agents/sdd-remediate.md", "agents/sdd-research.md", "agents/sdd-spec.md",
550
+ "agents/sdd-status.md", "agents/sdd-sync.md", "agents/sdd-tasks.md", "agents/sdd-verify.md",
551
+ "chains/sdd-full.chain.md", "chains/sdd-plan.chain.md", "chains/sdd-verify.chain.md",
552
+ "gentle-ai/support/sdd-status-contract.md", "gentle-ai/support/strict-tdd-verify.md",
553
+ "gentle-ai/support/strict-tdd.md",
554
+ ],
555
+ };
556
+
557
+ function assetFileKeys(root: string, prefix = ""): string[] {
558
+ if (!existsSync(root)) return [];
559
+ return readdirSync(root, { withFileTypes: true }).flatMap(entry => {
560
+ const key = `${prefix}${entry.name}`;
561
+ return entry.isDirectory() ? assetFileKeys(join(root, entry.name), `${key}/`) : [key];
562
+ }).sort();
563
+ }
564
+
565
+ for (const owner of Object.keys(EXPECTED_OWNER_ASSETS) as PackageAssetOwner[]) {
566
+ test(`selective ${owner} installation covers its catalog without cross-owner files`, () => {
567
+ withIsolatedAssetHome((agentHome) => {
568
+ const expected = [...EXPECTED_OWNER_ASSETS[owner]].sort();
569
+ installPackageAssets(agentHome, false, [owner]);
570
+ assert.deepEqual(Object.keys(installedAssetManifest(agentHome).assets).sort(), expected);
571
+ assert.deepEqual(assetFileKeys(agentHome), [...expected, "gentle-ai/managed-assets.json"].sort());
572
+ for (const key of expected) {
573
+ assert.equal(getPackageAssetOwner(key), owner);
574
+ const source = join(PACKAGE_ROOT, "assets", key.replace(/^gentle-ai\//, ""));
575
+ assert.equal(readFileSync(join(agentHome, key), "utf8"), readFileSync(source, "utf8"));
576
+ }
577
+ const counts = installPackageAssets(agentHome, false, [owner, owner]);
578
+ assert.deepEqual(counts, { agents: 0, chains: 0, support: 0, skipped: expected.length });
579
+ });
580
+ });
581
+ }
582
+
583
+ test("legacy all-assets installation covers every packaged file with explicit ownership", () => {
584
+ const packaged = ["agents", "chains", "support"].flatMap(group =>
585
+ assetFileKeys(join(PACKAGE_ROOT, "assets", group), group === "support" ? "gentle-ai/support/" : `${group}/`),
586
+ ).sort();
587
+ assert.deepEqual(packaged, Object.values(EXPECTED_OWNER_ASSETS).flat().sort());
588
+ for (const key of packaged) assert.notEqual(getPackageAssetOwner(key), undefined, key);
589
+ for (const key of ["agents/sdd-new.md", "gentle-ai/support/new.md", "toString", "__proto__"]) {
590
+ assert.equal(getPackageAssetOwner(key), undefined, "unknown assets must not default to SDD");
591
+ }
592
+ withIsolatedAssetHome((agentHome) => {
593
+ assert.deepEqual(installSddAssets(agentHome, false), { agents: 24, chains: 4, support: 3, skipped: 0 });
594
+ assert.deepEqual(Object.keys(installedAssetManifest(agentHome).assets).sort(), packaged);
595
+ assert.deepEqual(installSddAssets(agentHome, false), { agents: 0, chains: 0, support: 0, skipped: 31 });
596
+ assert.deepEqual(installSddAssets(agentHome, true), { agents: 24, chains: 4, support: 3, skipped: 0 });
597
+ });
598
+ });
599
+
600
+ test("selective refresh preserves unselected ownership and selected user changes after an all-assets install", () => {
601
+ withIsolatedAssetHome((agentHome) => {
602
+ installSddAssets(agentHome, false);
603
+ const manifest = installedAssetManifest(agentHome);
604
+ const selectedUserKey = "agents/gentle-ai-explore.md";
605
+ const unselectedUserKey = "agents/sdd-apply.md";
606
+ for (const key of [selectedUserKey, unselectedUserKey, "agents/custom.md"]) {
607
+ writeFileSync(join(agentHome, key), "User-authored instructions\n");
608
+ }
609
+ const before = new Map(assetFileKeys(agentHome).map(key => [key, readFileSync(join(agentHome, key), "utf8")]));
610
+ assert.deepEqual(installPackageAssets(agentHome, true, ["delegation"]), { agents: 2, chains: 0, support: 0, skipped: 1 });
611
+ delete manifest.assets[selectedUserKey];
612
+ assert.deepEqual(installedAssetManifest(agentHome), manifest);
613
+ for (const [key, content] of before) {
614
+ if (key !== "gentle-ai/managed-assets.json") assert.equal(readFileSync(join(agentHome, key), "utf8"), content, key);
615
+ }
616
+ const manifestBytes = readFileSync(join(agentHome, "gentle-ai", "managed-assets.json"), "utf8");
617
+ assert.deepEqual(installPackageAssets(agentHome, true, []), { agents: 0, chains: 0, support: 0, skipped: 0 });
618
+ assert.equal(readFileSync(join(agentHome, "gentle-ai", "managed-assets.json"), "utf8"), manifestBytes);
619
+ });
620
+ });
621
+
622
+ test("selective review migration adopts only untouched legacy copies and preserves routing", () => {
623
+ for (const edited of [false, true]) {
624
+ withIsolatedAssetHome((agentHome) => {
625
+ mkdirSync(join(agentHome, "agents"));
626
+ const legacy = readFileSync(V014_REVIEW_RISK_FIXTURE, "utf8")
627
+ .replace("name: review-risk\n", "name: review-risk\nmodel: custom/model\nthinking: high\n")
628
+ + (edited ? "\nUser review restrictions.\n" : "");
629
+ const target = join(agentHome, "agents", REVIEW_RISK_FILE);
630
+ writeFileSync(target, legacy);
631
+ installPackageAssets(agentHome, true, ["delegation"]);
632
+ assert.equal(readFileSync(target, "utf8"), legacy);
633
+ assert.equal(installedAssetManifest(agentHome).assets["agents/review-risk.md"], undefined);
634
+ const result = installPackageAssets(agentHome, true, ["review"]);
635
+ const actual = readFileSync(target, "utf8");
636
+ const manifest = installedAssetManifest(agentHome);
637
+ assert.equal(result.skipped, edited ? 1 : 0);
638
+ if (edited) {
639
+ assert.equal(actual, legacy);
640
+ assert.equal(manifest.assets["agents/review-risk.md"], undefined);
641
+ } else {
642
+ assert.notEqual(actual, legacy);
643
+ assert.match(actual, /model: custom\/model\nthinking: high/);
644
+ assert.equal(manifest.assets["agents/review-risk.md"], sha256(actual));
645
+ }
646
+ assert.equal(existsSync(join(agentHome, "agents", "sdd-apply.md")), false);
647
+ assert.equal(existsSync(join(agentHome, "gentle-ai", "support")), false);
648
+ });
649
+ }
650
+ });
651
+
652
+ test("unowned legacy research migrates by exact normalized hash, preserving routing and user edits", () => {
653
+ const packaged = readFileSync(join(PACKAGE_ROOT, "assets", "agents", "sdd-research.md"), "utf8");
654
+ const oldAdmission = "- Evidence grants for this runtime are `documentation=[]; open-web=[]`. Never infer evidence capability from bash, persistence tools, or any inherited tool; persistence tools are not evidence grants. Unsupported or undeclared classes deny admission and emit no claims.\n- Because this runtime declares no evidence grants, retain the selected request, persist a `blocked` outcome with no claims, and stop.\n";
655
+ const legacy = packaged
656
+ .replace(/## Parent Preflight Transport\n[\s\S]*?(?=## Skill Resolution Contract)/, "")
657
+ .replace(/## Bounded artifact handoff\n[\s\S]*?(?=## Memory Contract)/, "")
658
+ .replace(/ - fetch_content\n - web_search\n - source_check\n - get_search_content\n/, "")
659
+ .replace(/- Use the injected `## SDD Research Capabilities`[\s\S]*?(?=- Admission denial)/, oldAdmission)
660
+ .replace(/Use `done` only when all selected questions[\s\S]*?product decisions remain separately confirmed by the parent\./i, "For this runtime the outcome is `blocked` with an admission denial and no claims.");
661
+ const manifest = JSON.parse(readFileSync(join(PACKAGE_ROOT, "assets", "migrations", "managed-assets-v2.5.0.json"), "utf8"));
662
+ assert.equal(sha256(legacy), manifest.assets["agents/sdd-research.md"], "fixture reconstruction must match observed old package bytes");
663
+ const temporary = mkdtempSync(join(tmpdir(), "gentle-research-migration-"));
664
+ const previous = process.env.GENTLE_PI_AGENT_HOME;
665
+ try {
666
+ for (const edited of [false, true]) {
667
+ const agentHome = join(temporary, edited ? "edited" : "legacy");
668
+ process.env.GENTLE_PI_AGENT_HOME = agentHome;
669
+ mkdirSync(join(agentHome, "agents"), { recursive: true });
670
+ const target = join(agentHome, "agents", "sdd-research.md");
671
+ const routed = legacy.replace("name: sdd-research\n", "name: sdd-research\nmodel: custom/model\nthinking: high\n") + (edited ? "\nUser research restrictions.\n" : "");
672
+ writeFileSync(target, routed);
673
+ installSddAssets(temporary, true);
674
+ const actual = readFileSync(target, "utf8");
675
+ if (edited) assert.equal(actual, routed);
676
+ else {
677
+ assert.match(actual, / - fetch_content/);
678
+ assert.match(actual, /model: custom\/model\nthinking: high/);
679
+ const ownership = JSON.parse(readFileSync(join(agentHome, "gentle-ai", "managed-assets.json"), "utf8"));
680
+ assert.equal(ownership.assets["agents/sdd-research.md"], sha256(actual));
681
+ installSddAssets(temporary, true);
682
+ assert.equal(readFileSync(target, "utf8"), actual, "subsequent refresh keeps adopted model routing");
683
+ }
684
+ }
685
+ } finally {
686
+ if (previous === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
687
+ else process.env.GENTLE_PI_AGENT_HOME = previous;
688
+ rmSync(temporary, { recursive: true, force: true });
689
+ }
690
+ });
691
+
472
692
  test("the retired Pi adversarial role agents are not packaged", () => {
473
693
  // gentle-pi#311 P5: the refuter and targeted validator verdicts execute
474
694
  // through Go-owned pi processes via provider-rendered self-contained
@@ -1288,9 +1508,9 @@ test("pi-pretty wrapper uses real package path resolution for pnpm symlink insta
1288
1508
  assert.match(wrapper, /quietToolsEnabled/);
1289
1509
  });
1290
1510
 
1291
- test("v2.5.0 release package and runtime stop before publication", () => {
1511
+ test("v2.6.1 release package and runtime stop before publication", () => {
1292
1512
  const packageJson = readPackageJson();
1293
- assert.equal(packageJson.version, "2.5.0", "the release manifest must remain explicitly pinned to v2.5.0");
1513
+ assert.equal(packageJson.version, "2.6.1", "the release manifest must remain explicitly pinned to v2.6.1");
1294
1514
  assert.equal(
1295
1515
  packageJson.scripts?.test,
1296
1516
  "node --experimental-strip-types --test tests/*.test.ts && pnpm run check:provider-contract && pnpm run test:harness",
@@ -1333,3 +1553,8 @@ test("README documents dynamic Gentle AI RDD ownership and the installed permiss
1333
1553
  }
1334
1554
  assert.doesNotMatch(readme, /New ordinary review uses compact `gentle_review` `start -> finalize -> validate`\./);
1335
1555
  });
1556
+
1557
+
1558
+ test("package verification explicitly requires the managed remediation actor", () => {
1559
+ assert.match(readFileSync(join(PACKAGE_ROOT, "scripts/verify-package-files.mjs"), "utf8"), /assets\/agents\/sdd-remediate\.md/);
1560
+ });
@@ -0,0 +1,208 @@
1
+ import assert from "node:assert/strict";
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+ import {
7
+ applyOrchestratorSettings,
8
+ parseOrchestratorModelRef,
9
+ readOrchestratorSettings,
10
+ restoreOrchestratorSettings,
11
+ } from "../lib/profiles-orchestrator.ts";
12
+
13
+ function fixture(t: test.TestContext) {
14
+ const root = mkdtempSync(join(tmpdir(), "gentle-pi-orchestrator-"));
15
+ t.after(() => rmSync(root, { recursive: true, force: true }));
16
+ return { root, settingsPath: join(root, "settings.json") };
17
+ }
18
+
19
+ test("orchestrator model refs split only on a real provider boundary", () => {
20
+ assert.deepEqual(parseOrchestratorModelRef("nan/glm5.3"), {
21
+ provider: "nan",
22
+ model: "glm5.3",
23
+ });
24
+ assert.deepEqual(parseOrchestratorModelRef(" openai-codex/gpt-6-astra "), {
25
+ provider: "openai-codex",
26
+ model: "gpt-6-astra",
27
+ });
28
+ // A nested slash stays with the model half.
29
+ assert.deepEqual(parseOrchestratorModelRef("nan/qwen3.8/flash"), {
30
+ provider: "nan",
31
+ model: "qwen3.8/flash",
32
+ });
33
+ for (const value of ["glm5.3", "/glm5.3", "nan/", "nan", "", " ", 42, undefined, null]) {
34
+ assert.equal(parseOrchestratorModelRef(value), undefined, `rejects ${JSON.stringify(value)}`);
35
+ }
36
+ });
37
+
38
+ test("reading the orchestrator distinguishes missing, unreadable, unset and set", (t) => {
39
+ const { settingsPath } = fixture(t);
40
+ assert.deepEqual(readOrchestratorSettings(settingsPath), { status: "missing" });
41
+
42
+ writeFileSync(settingsPath, "{ not json");
43
+ assert.equal(readOrchestratorSettings(settingsPath).status, "invalid");
44
+
45
+ writeFileSync(settingsPath, JSON.stringify(["not", "an", "object"]));
46
+ assert.equal(readOrchestratorSettings(settingsPath).status, "invalid");
47
+
48
+ writeFileSync(settingsPath, JSON.stringify({ theme: "Gentleman-Cute" }));
49
+ const unset = readOrchestratorSettings(settingsPath);
50
+ assert.equal(unset.status, "valid");
51
+ assert.equal(unset.status === "valid" ? unset.entry : undefined, undefined);
52
+
53
+ writeFileSync(
54
+ settingsPath,
55
+ JSON.stringify({
56
+ defaultProvider: "nan",
57
+ defaultModel: "glm5.3",
58
+ defaultThinkingLevel: "high",
59
+ }),
60
+ );
61
+ const set = readOrchestratorSettings(settingsPath);
62
+ assert.deepEqual(set.status === "valid" ? set.entry : undefined, {
63
+ model: "nan/glm5.3",
64
+ thinking: "high",
65
+ });
66
+
67
+ // A half-written pair is "unset", never a fabricated provider or model.
68
+ writeFileSync(settingsPath, JSON.stringify({ defaultProvider: "nan" }));
69
+ const half = readOrchestratorSettings(settingsPath);
70
+ assert.equal(half.status, "valid");
71
+ assert.equal(half.status === "valid" ? half.entry : undefined, undefined);
72
+ });
73
+
74
+ test("an orchestrator entry without a model never touches the settings file", (t) => {
75
+ const { root, settingsPath } = fixture(t);
76
+ const before = '{\n "packages": [\n "npm:pi-mcp-adapter"\n ],\n "theme": "Gentleman-Cute"\n}\n';
77
+ writeFileSync(settingsPath, before);
78
+ assert.deepEqual(applyOrchestratorSettings(settingsPath, { thinking: "high" }), {
79
+ status: "unchanged",
80
+ });
81
+ assert.equal(readFileSync(settingsPath, "utf8"), before);
82
+
83
+ // Also true when there is no file at all: nothing to write, nothing created.
84
+ const absent = join(root, "absent-settings.json");
85
+ assert.deepEqual(applyOrchestratorSettings(absent, {}), { status: "unchanged" });
86
+ assert.equal(existsSync(absent), false);
87
+ });
88
+
89
+ test("applying the orchestrator preserves every unrelated settings key", (t) => {
90
+ const { settingsPath } = fixture(t);
91
+ const before =
92
+ JSON.stringify(
93
+ {
94
+ packages: ["npm:pi-mcp-adapter", "some-package"],
95
+ theme: "Gentleman-Cute",
96
+ tuiMode: "fullscreen",
97
+ telemetry: { enabled: true },
98
+ defaultProvider: "nan",
99
+ defaultModel: "deepseek-v4-flash",
100
+ defaultThinkingLevel: "high",
101
+ images: { autoResize: true },
102
+ },
103
+ null,
104
+ 2,
105
+ ) + "\n";
106
+ writeFileSync(settingsPath, before);
107
+
108
+ const result = applyOrchestratorSettings(settingsPath, {
109
+ model: "nan/glm5.3",
110
+ thinking: "max",
111
+ });
112
+ assert.equal(result.status, "written");
113
+ assert.equal(result.status === "written" ? result.previous : undefined, before);
114
+
115
+ const after = JSON.parse(readFileSync(settingsPath, "utf8"));
116
+ assert.equal(after.defaultProvider, "nan");
117
+ assert.equal(after.defaultModel, "glm5.3");
118
+ assert.equal(after.defaultThinkingLevel, "max");
119
+ assert.deepEqual(after.packages, ["npm:pi-mcp-adapter", "some-package"]);
120
+ assert.equal(after.theme, "Gentleman-Cute");
121
+ assert.equal(after.tuiMode, "fullscreen");
122
+ assert.deepEqual(after.telemetry, { enabled: true });
123
+ assert.deepEqual(after.images, { autoResize: true });
124
+ assert.equal(readFileSync(settingsPath, "utf8").endsWith("\n"), true);
125
+ });
126
+
127
+ test("a missing settings file is created with only the orchestrator keys", (t) => {
128
+ const { settingsPath } = fixture(t);
129
+ const result = applyOrchestratorSettings(settingsPath, { model: "nan/glm5.3" });
130
+ assert.equal(result.status, "written");
131
+ assert.equal(result.status === "written" ? result.previous : "sentinel", undefined);
132
+ assert.deepEqual(JSON.parse(readFileSync(settingsPath, "utf8")), {
133
+ defaultProvider: "nan",
134
+ defaultModel: "glm5.3",
135
+ });
136
+ });
137
+
138
+ test("an orchestrator entry without thinking clears a stale thinking level", (t) => {
139
+ const { settingsPath } = fixture(t);
140
+ writeFileSync(
141
+ settingsPath,
142
+ JSON.stringify({
143
+ defaultProvider: "nan",
144
+ defaultModel: "glm5.3",
145
+ defaultThinkingLevel: "high",
146
+ }),
147
+ );
148
+ const result = applyOrchestratorSettings(settingsPath, { model: "nan/glm5.3-flash" });
149
+ assert.equal(result.status, "written");
150
+ const after = JSON.parse(readFileSync(settingsPath, "utf8"));
151
+ assert.equal(after.defaultModel, "glm5.3-flash");
152
+ assert.equal("defaultThinkingLevel" in after, false);
153
+ });
154
+
155
+ test("re-applying the same selection is a no-op", (t) => {
156
+ const { settingsPath } = fixture(t);
157
+ assert.equal(applyOrchestratorSettings(settingsPath, { model: "nan/glm5.3" }).status, "written");
158
+ const bytes = readFileSync(settingsPath, "utf8");
159
+ assert.deepEqual(applyOrchestratorSettings(settingsPath, { model: "nan/glm5.3" }), {
160
+ status: "unchanged",
161
+ });
162
+ assert.equal(readFileSync(settingsPath, "utf8"), bytes);
163
+ });
164
+
165
+ test("an unusable target is refused instead of being overwritten", (t) => {
166
+ const { settingsPath } = fixture(t);
167
+ const before = "{ this is not json\n";
168
+ writeFileSync(settingsPath, before);
169
+ const broken = applyOrchestratorSettings(settingsPath, { model: "nan/glm5.3" });
170
+ assert.equal(broken.status, "invalid");
171
+ assert.equal(readFileSync(settingsPath, "utf8"), before);
172
+
173
+ writeFileSync(settingsPath, "[]");
174
+ const notObject = applyOrchestratorSettings(settingsPath, { model: "nan/glm5.3" });
175
+ assert.equal(notObject.status, "invalid");
176
+ assert.equal(readFileSync(settingsPath, "utf8"), "[]");
177
+
178
+ // A model id that is not a provider/model pair is refused for the same reason:
179
+ // there is no provider to write, and guessing one would corrupt the selection.
180
+ writeFileSync(settingsPath, JSON.stringify({ theme: "Gentleman-Cute" }));
181
+ const noProvider = applyOrchestratorSettings(settingsPath, { model: "glm5.3" });
182
+ assert.equal(noProvider.status, "invalid");
183
+ assert.deepEqual(JSON.parse(readFileSync(settingsPath, "utf8")), { theme: "Gentleman-Cute" });
184
+ });
185
+
186
+ test("restoring returns the previous bytes, or removes a file the write created", (t) => {
187
+ const { root, settingsPath } = fixture(t);
188
+ const before = '{\n "theme": "Gentleman-Cute"\n}\n';
189
+ writeFileSync(settingsPath, before);
190
+ const written = applyOrchestratorSettings(settingsPath, { model: "nan/glm5.3" });
191
+ assert.equal(written.status, "written");
192
+ restoreOrchestratorSettings(
193
+ settingsPath,
194
+ written.status === "written" ? written.previous : undefined,
195
+ );
196
+ assert.equal(readFileSync(settingsPath, "utf8"), before);
197
+
198
+ // A file the write created restores by disappearing, not by holding an empty object.
199
+ const createdPath = join(root, "created-settings.json");
200
+ const created = applyOrchestratorSettings(createdPath, { model: "nan/glm5.3" });
201
+ assert.equal(created.status, "written");
202
+ assert.equal(existsSync(createdPath), true);
203
+ restoreOrchestratorSettings(
204
+ createdPath,
205
+ created.status === "written" ? created.previous : undefined,
206
+ );
207
+ assert.equal(existsSync(createdPath), false);
208
+ });
@@ -173,6 +173,7 @@ test("quiet tool rendering registers noisy built-in tools", () => {
173
173
  for (const toolName of ["read", "bash", "grep", "find", "ls", "edit", "write"]) {
174
174
  const tool = tools.get(toolName);
175
175
  assert.ok(tool, `missing quiet renderer for ${toolName}`);
176
+ assert.equal(tool.renderShell, "self", `${toolName} must opt out of Pi's painted Box`);
176
177
  assert.equal(typeof tool.execute, "function", `${toolName} must delegate execution`);
177
178
  assert.ok(tool.parameters, `${toolName} must preserve built-in parameters`);
178
179
  }
@@ -199,6 +199,16 @@ test("worker asset never claims completion while a required verification command
199
199
  );
200
200
  });
201
201
 
202
+ test("worker keeps candidate review disposition and lifecycle parent-owned", () => {
203
+ for (const clause of [
204
+ "The primary parent owns candidate review disposition and lifecycle, including preflight and any explicit candidate-level opt-out.",
205
+ "Never search for, request, or invoke review tools, including `gentle_review`.",
206
+ "Missing review tools never block this worker's implementation or verification handoff.",
207
+ ]) {
208
+ assert.ok(worker.includes(clause), `worker asset is missing: ${clause}`);
209
+ }
210
+ });
211
+
202
212
  test("worker asset keeps the existing Return contract fields", () => {
203
213
  for (const field of [
204
214
  "status: completed | partial | blocked | interaction_required",