gentle-pi 2.4.0 → 2.6.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 (227) hide show
  1. package/README.md +292 -25
  2. package/assets/agents/gentle-ai-worker.md +13 -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 +39 -11
  25. package/assets/orchestrator.md +5 -5
  26. package/assets/sdd-orchestrator-workflow.md +54 -21
  27. package/assets/support/sdd-status-contract.md +34 -90
  28. package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
  29. package/docs/delegated-verification.md +25 -0
  30. package/docs/telemetry.md +94 -0
  31. package/docs/windows-startup-console-visibility.md +18 -0
  32. package/extensions/ask-user-choice.ts +159 -25
  33. package/extensions/codegraph-tools.ts +95 -5
  34. package/extensions/gentle-agents.ts +1337 -0
  35. package/extensions/gentle-ai.ts +2916 -386
  36. package/extensions/gentle-shell.ts +650 -0
  37. package/extensions/gentle-todo.ts +234 -0
  38. package/extensions/quiet-tools.ts +2 -1
  39. package/extensions/runtime-metrics.ts +130 -0
  40. package/extensions/sdd-init.ts +2 -2
  41. package/extensions/startup-banner.ts +52 -75
  42. package/lib/agent-profiles.ts +550 -0
  43. package/lib/agents-completion-delivery.ts +72 -0
  44. package/lib/agents-config.ts +315 -0
  45. package/lib/agents-history.ts +88 -0
  46. package/lib/agents-messaging.ts +187 -0
  47. package/lib/agents-protocol.ts +501 -0
  48. package/lib/agents-runner.ts +1012 -0
  49. package/lib/agents-thread-view.ts +57 -0
  50. package/lib/agents-transcript.ts +87 -0
  51. package/lib/agents-view-layout.ts +40 -0
  52. package/lib/agents-view.ts +914 -0
  53. package/lib/agents-widget.ts +241 -0
  54. package/lib/gentle-ai-binary.ts +3 -1
  55. package/lib/gentle-ai-renderer.ts +143 -25
  56. package/lib/native-choice-list.ts +194 -0
  57. package/lib/native-fullscreen-interaction.ts +47 -0
  58. package/lib/native-pointer-region.ts +164 -0
  59. package/lib/native-review-cli.ts +371 -13
  60. package/lib/orchestrator-presence.ts +337 -0
  61. package/lib/profiles-orchestrator.ts +203 -0
  62. package/lib/review-candidate-view-owner.ts +427 -0
  63. package/lib/review-candidate-view.ts +150 -48
  64. package/lib/review-consent-component.ts +247 -0
  65. package/lib/review-consent-ui.ts +110 -0
  66. package/lib/review-host-relay.ts +28 -0
  67. package/lib/review-integration-v2.ts +243 -11
  68. package/lib/review-last-event-controller.ts +8 -4
  69. package/lib/review-relay-contract.ts +11 -0
  70. package/lib/review-reminder-receipt.ts +74 -0
  71. package/lib/review-repository.ts +2 -2
  72. package/lib/review-risk-assessment.ts +339 -0
  73. package/lib/review-session-standing-permission-ipc.ts +309 -0
  74. package/lib/review-session-standing-permission.ts +240 -0
  75. package/lib/runtime-metrics-children.ts +199 -0
  76. package/lib/runtime-metrics-delivery.ts +68 -0
  77. package/lib/runtime-metrics-native.ts +166 -0
  78. package/lib/runtime-metrics-pi-identity.ts +113 -0
  79. package/lib/runtime-metrics-policy.ts +51 -0
  80. package/lib/runtime-metrics.ts +255 -0
  81. package/lib/sdd-preflight.ts +362 -81
  82. package/lib/sdd-research-capabilities.ts +228 -0
  83. package/lib/sdd-status.ts +29 -7
  84. package/lib/session-worktree-registry.ts +118 -0
  85. package/lib/shell-bar.ts +184 -0
  86. package/lib/shell-card.ts +133 -0
  87. package/lib/shell-changes-view.ts +530 -0
  88. package/lib/shell-changes.ts +290 -0
  89. package/lib/shell-gauge.ts +40 -0
  90. package/lib/shell-prompt.ts +115 -0
  91. package/lib/shell-sidebar-banner.ts +11 -0
  92. package/lib/shell-sidebar-layout.ts +213 -0
  93. package/lib/shell-sidebar.ts +41 -0
  94. package/lib/shell-todo.ts +297 -0
  95. package/lib/shell-usage-view.ts +76 -0
  96. package/lib/shell-usage.ts +246 -0
  97. package/lib/telemetry-trigger.ts +153 -0
  98. package/package.json +8 -5
  99. package/runtime/gentle-ai-binary.mjs +3 -1
  100. package/runtime/native-review-cli.mjs +370 -12
  101. package/runtime/review-integration-v2.mjs +243 -11
  102. package/runtime/review-relay-contract.mjs +11 -0
  103. package/runtime/review-risk-assessment.mjs +340 -0
  104. package/runtime/telemetry-trigger.mjs +154 -0
  105. package/scripts/build-runtime-modules.mjs +11 -1
  106. package/scripts/check-types.mjs +125 -0
  107. package/scripts/gentle-ai-installer.mjs +10 -10
  108. package/scripts/install-gentle-ai.mjs +12 -0
  109. package/scripts/install-tui-mode-setting.mjs +114 -0
  110. package/scripts/test-packed-runner.mjs +38 -2
  111. package/scripts/types-baseline.json +99 -0
  112. package/scripts/verify-package-files.mjs +8 -2
  113. package/skills/_shared/review-ledger-contract.md +20 -2
  114. package/skills/issue-creation/SKILL.md +3 -3
  115. package/skills/judgment-day/SKILL.md +17 -3
  116. package/skills/judgment-day/references/prompts-and-formats.md +14 -3
  117. package/tests/agent-profiles.test.ts +722 -0
  118. package/tests/agents-completion-delivery.test.ts +94 -0
  119. package/tests/agents-config.test.ts +205 -0
  120. package/tests/agents-fake-child.ts +66 -0
  121. package/tests/agents-grouping.test.ts +179 -0
  122. package/tests/agents-history.test.ts +54 -0
  123. package/tests/agents-integration.test.ts +100 -0
  124. package/tests/agents-messaging.test.ts +94 -0
  125. package/tests/agents-protocol.test.ts +198 -0
  126. package/tests/agents-queries.test.ts +190 -0
  127. package/tests/agents-responsive.test.ts +43 -0
  128. package/tests/agents-runner-process.test.ts +111 -0
  129. package/tests/agents-runner.test.ts +959 -0
  130. package/tests/agents-thread-view.test.ts +45 -0
  131. package/tests/agents-transcript.test.ts +30 -0
  132. package/tests/agents-view.test.ts +685 -0
  133. package/tests/agents-widget.test.ts +141 -0
  134. package/tests/artifact-language.test.ts +25 -2
  135. package/tests/ask-user-choice.test.ts +325 -5
  136. package/tests/asset-installation-runtime.test.ts +108 -0
  137. package/tests/autonomous-guard.test.ts +116 -1
  138. package/tests/codegraph-tools.test.ts +112 -2
  139. package/tests/delegated-key-learnings-contract.test.ts +1 -1
  140. package/tests/devbinary/native-review-parity.devtest.ts +110 -0
  141. package/tests/feature-request-form.test.ts +67 -0
  142. package/tests/fixtures/agents-messaging-child.mjs +5 -0
  143. package/tests/fixtures/agents-process-child.mjs +23 -0
  144. package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
  145. package/tests/gentle-agents.test.ts +2168 -0
  146. package/tests/gentle-ai-binary.test.ts +7 -2
  147. package/tests/gentle-ai-installer.test.ts +47 -47
  148. package/tests/gentle-ai-renderer.test.ts +103 -0
  149. package/tests/gentle-ai.test.ts +971 -15
  150. package/tests/gentle-card-text.ts +35 -0
  151. package/tests/gentle-shell.test.ts +818 -0
  152. package/tests/gentle-todo.test.ts +226 -0
  153. package/tests/install-tui-mode-setting.test.ts +324 -0
  154. package/tests/issue-creation-skill.test.ts +22 -0
  155. package/tests/model-routing-authority.test.ts +12 -0
  156. package/tests/native-choice-list.test.ts +202 -0
  157. package/tests/native-fullscreen-interaction.test.ts +125 -0
  158. package/tests/native-pointer-region.test.ts +245 -0
  159. package/tests/native-review-capability-contract.test.ts +27 -1
  160. package/tests/native-review-cli.test.ts +317 -3
  161. package/tests/native-review-consent.test.ts +91 -0
  162. package/tests/native-review-parity-runtime.test.ts +8 -2
  163. package/tests/native-review-parity.test.ts +43 -29
  164. package/tests/native-sdd-attempt-authority.test.ts +7 -2
  165. package/tests/orchestrator-budget.test.ts +69 -0
  166. package/tests/orchestrator-presence.test.ts +389 -0
  167. package/tests/orchestrator-rdd-ownership.test.ts +9 -0
  168. package/tests/package-manifest.test.ts +243 -7
  169. package/tests/profiles-orchestrator.test.ts +208 -0
  170. package/tests/quiet-tool-rendering.test.ts +97 -37
  171. package/tests/rdd-aware-verification-contract.test.ts +226 -0
  172. package/tests/rdd-status-line.test.ts +286 -0
  173. package/tests/review-agent-end-preflight.test.ts +332 -24
  174. package/tests/review-candidate-view.test.ts +751 -7
  175. package/tests/review-consent-ui.test.ts +352 -0
  176. package/tests/review-contract-prompt.test.ts +17 -0
  177. package/tests/review-controller-native-recovery.test.ts +29 -4
  178. package/tests/review-controller-native-routing.test.ts +884 -7
  179. package/tests/review-controller-workspace-root.test.ts +45 -2
  180. package/tests/review-controller.test.ts +26 -1
  181. package/tests/review-host-relay-restart-parity.test.ts +142 -1
  182. package/tests/review-host-relay-routing.test.ts +384 -8
  183. package/tests/review-host-relay.test.ts +29 -0
  184. package/tests/review-integration-v2-forward.test.ts +44 -0
  185. package/tests/review-integration-v2.test.ts +276 -0
  186. package/tests/review-last-event-closure.test.ts +112 -3
  187. package/tests/review-ledger-contract.test.ts +61 -6
  188. package/tests/review-relay-contract.test.ts +26 -0
  189. package/tests/review-reminder-receipt.test.ts +62 -0
  190. package/tests/review-repository.test.ts +28 -1
  191. package/tests/review-risk-assessment.test.ts +626 -0
  192. package/tests/review-session-standing-permission-controller.test.ts +656 -0
  193. package/tests/review-session-standing-permission-ipc.test.ts +233 -0
  194. package/tests/review-session-standing-permission-runtime.test.ts +212 -0
  195. package/tests/review-session-standing-permission.test.ts +156 -0
  196. package/tests/runtime-harness.mjs +447 -39
  197. package/tests/runtime-metrics-children.test.ts +206 -0
  198. package/tests/runtime-metrics-delivery.test.ts +85 -0
  199. package/tests/runtime-metrics-extension.test.ts +187 -0
  200. package/tests/runtime-metrics-native.test.ts +209 -0
  201. package/tests/runtime-metrics-pi-identity.test.ts +113 -0
  202. package/tests/runtime-metrics-policy.test.ts +62 -0
  203. package/tests/runtime-metrics.test.ts +184 -0
  204. package/tests/sdd-agent-tools.test.ts +10 -1
  205. package/tests/sdd-execution-routing-contract.test.ts +28 -0
  206. package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
  207. package/tests/sdd-native-managed-uptake.test.ts +253 -0
  208. package/tests/sdd-planning-routing-contract.test.ts +45 -0
  209. package/tests/sdd-preflight.test.ts +252 -8
  210. package/tests/sdd-research-capabilities.test.ts +256 -0
  211. package/tests/sdd-research-live.test.ts +241 -0
  212. package/tests/sdd-selection-transport.test.ts +504 -0
  213. package/tests/sdd-status.test.ts +51 -0
  214. package/tests/session-worktree-registry.test.ts +135 -0
  215. package/tests/shell-bar.test.ts +176 -0
  216. package/tests/shell-card.test.ts +139 -0
  217. package/tests/shell-changes-view.test.ts +609 -0
  218. package/tests/shell-changes.test.ts +350 -0
  219. package/tests/shell-prompt.test.ts +140 -0
  220. package/tests/shell-sidebar-banner.test.ts +23 -0
  221. package/tests/shell-sidebar-layout.test.ts +387 -0
  222. package/tests/shell-sidebar.test.ts +50 -0
  223. package/tests/shell-todo.test.ts +259 -0
  224. package/tests/shell-usage-view.test.ts +62 -0
  225. package/tests/shell-usage.test.ts +197 -0
  226. package/tests/startup-banner.test.ts +126 -0
  227. package/tests/telemetry-trigger.test.ts +351 -0
@@ -60,6 +60,15 @@ test("rendered parent prompt keeps the RDD boundary while omitting lifecycle mir
60
60
  for (const marker of ["Authority-First Terminal Procedure", "reconcile-terminal-mirrors", "next_transition"]) {
61
61
  assert.ok(!rendered.includes(marker), `rendered parent prompt leaked: ${marker}`);
62
62
  }
63
+ // gentle-pi#661: getOrchestratorPrompt()'s no-argument default renders the
64
+ // "unknown (native status unavailable)" RDD status line -- the longest of
65
+ // the three renderable forms -- so this IS the worst-case render the 8 KiB
66
+ // budget below must cover, not a smaller placeholder production later
67
+ // exceeds.
68
+ assert.ok(
69
+ rendered.includes("Receipt-driven development: unknown (native status unavailable)"),
70
+ "the default render must include the worst-case RDD status line",
71
+ );
63
72
  assert.ok(Buffer.byteLength(rendered, "utf8") <= 8192, "the rendered parent prompt must stay below the reduced 8 KiB budget");
64
73
  });
65
74
 
@@ -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";
@@ -80,6 +80,8 @@ interface PackageJson {
80
80
  files?: string[];
81
81
  scripts?: Record<string, string>;
82
82
  dependencies?: Record<string, string>;
83
+ peerDependencies?: Record<string, string>;
84
+ devDependencies?: Record<string, string>;
83
85
  bundledDependencies?: string[];
84
86
  bundleDependencies?: string[];
85
87
  repository?: {
@@ -99,6 +101,15 @@ function readPackageJson(): PackageJson {
99
101
  }
100
102
  }
101
103
 
104
+ test("package declares the tested Pi minimum required for agent_settled", () => {
105
+ const manifest = readPackageJson();
106
+ assert.equal(manifest.peerDependencies?.["@earendil-works/pi-coding-agent"], ">=0.85.1");
107
+ assert.equal(manifest.devDependencies?.["@earendil-works/pi-coding-agent"], "0.85.1");
108
+ const readme = readFileSync(join(PACKAGE_ROOT, "README.md"), "utf8");
109
+ assert.match(readme, /Pi 0\.85\.1 or newer/);
110
+ assert.match(readme, /agent_settled/);
111
+ });
112
+
102
113
  test("package manifest has no obsolete native activation build surface", () => {
103
114
  const packageJson = readPackageJson();
104
115
  const manifest = JSON.stringify(packageJson);
@@ -218,8 +229,12 @@ test("generated runtime modules and packed-package checks are deterministic", ()
218
229
  test("package manifest ships and runs the checked-in package-local Gentle AI installer", () => {
219
230
  const packageJson = readPackageJson();
220
231
  const verifier = readFileSync(join(PACKAGE_ROOT, "scripts", "verify-package-files.mjs"), "utf8");
232
+ const readme = readFileSync(join(PACKAGE_ROOT, "README.md"), "utf8");
221
233
 
222
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");
223
238
  assert.ok(packageJson.files?.includes("scripts/"));
224
239
  assert.match(verifier, /"scripts\/install-gentle-ai\.mjs"/);
225
240
  assert.match(verifier, /"scripts\/gentle-ai-installer\.mjs"/);
@@ -262,20 +277,20 @@ test("package manifest installs pi-pretty through a wrapper without bundling nat
262
277
  );
263
278
  });
264
279
 
265
- test("package verification binds the published Gentle AI v2.6.0 runtime pin", () => {
280
+ test("package verification binds the published Gentle AI v2.8.0 runtime pin", () => {
266
281
  const installer = readFileSync(join(PACKAGE_ROOT, "scripts", "gentle-ai-installer.mjs"), "utf8");
267
282
  const binary = readFileSync(join(PACKAGE_ROOT, "lib", "gentle-ai-binary.ts"), "utf8");
268
283
  const verifier = readFileSync(join(PACKAGE_ROOT, "scripts", "verify-package-files.mjs"), "utf8");
269
284
 
270
- assert.match(installer, /INSTALLER_VERSION = "2\.6\.0"/);
285
+ assert.match(installer, /INSTALLER_VERSION = "2\.8\.0"/);
271
286
  assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_PACKAGE.*GENTLE_AI_WINDOWS_SOURCE_MODULE/);
272
- assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:scGoZYnPHh4oCVqlcfQAOSpm22Ii5lmB0kBbiBKvNZk="/);
287
+ assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:QNdrYDxq\/Bi08o9PCk0jc4r5YObLdFmRCLXjKMqsze8="/);
273
288
  assert.match(installer, /GOTOOLCHAIN: "local"/);
274
289
  assert.match(installer, /GOSUMDB: "sum\.golang\.org"/);
275
290
  assert.match(binary, /GENTLE_AI_VERSION = INSTALLER_VERSION/);
276
291
  assert.match(binary, /GO_SUMDB_SOURCE_BUILD/);
277
292
  assert.match(binary, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM/);
278
- assert.match(verifier, /v2\.6\.0/);
293
+ assert.match(verifier, /v2\.8\.0/);
279
294
  });
280
295
 
281
296
 
@@ -458,6 +473,222 @@ test("packaged agents declare only tool names a Pi child session can resolve", (
458
473
  }
459
474
  });
460
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
+
461
692
  test("the retired Pi adversarial role agents are not packaged", () => {
462
693
  // gentle-pi#311 P5: the refuter and targeted validator verdicts execute
463
694
  // through Go-owned pi processes via provider-rendered self-contained
@@ -1277,9 +1508,9 @@ test("pi-pretty wrapper uses real package path resolution for pnpm symlink insta
1277
1508
  assert.match(wrapper, /quietToolsEnabled/);
1278
1509
  });
1279
1510
 
1280
- test("v2.4.0 release package and runtime stop before publication", () => {
1511
+ test("v2.6.0 release package and runtime stop before publication", () => {
1281
1512
  const packageJson = readPackageJson();
1282
- assert.equal(packageJson.version, "2.4.0", "the release manifest must remain explicitly pinned to v2.4.0");
1513
+ assert.equal(packageJson.version, "2.6.0", "the release manifest must remain explicitly pinned to v2.6.0");
1283
1514
  assert.equal(
1284
1515
  packageJson.scripts?.test,
1285
1516
  "node --experimental-strip-types --test tests/*.test.ts && pnpm run check:provider-contract && pnpm run test:harness",
@@ -1322,3 +1553,8 @@ test("README documents dynamic Gentle AI RDD ownership and the installed permiss
1322
1553
  }
1323
1554
  assert.doesNotMatch(readme, /New ordinary review uses compact `gentle_review` `start -> finalize -> validate`\./);
1324
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
+ });