gentle-pi 2.3.0 → 2.5.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 (153) hide show
  1. package/README.md +195 -11
  2. package/assets/agents/gentle-ai-worker.md +9 -0
  3. package/assets/agents/sdd-explore.md +1 -0
  4. package/assets/orchestrator-delegation.md +21 -10
  5. package/assets/orchestrator.md +8 -12
  6. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  7. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
  8. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
  9. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
  10. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
  11. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
  12. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
  13. package/docs/delegated-verification.md +25 -0
  14. package/docs/review-integration.md +1 -1
  15. package/docs/telemetry.md +38 -0
  16. package/extensions/ask-user-choice.ts +26 -20
  17. package/extensions/codegraph-tools.ts +94 -5
  18. package/extensions/gentle-agents.ts +588 -0
  19. package/extensions/gentle-ai.ts +1421 -143
  20. package/extensions/gentle-shell.ts +547 -0
  21. package/extensions/gentle-todo.ts +199 -0
  22. package/extensions/quiet-tools.ts +1 -1
  23. package/lib/agent-home.ts +8 -0
  24. package/lib/agents-config.ts +318 -0
  25. package/lib/agents-history.ts +80 -0
  26. package/lib/agents-protocol.ts +429 -0
  27. package/lib/agents-runner.ts +490 -0
  28. package/lib/agents-transcript.ts +87 -0
  29. package/lib/agents-view.ts +557 -0
  30. package/lib/agents-widget.ts +222 -0
  31. package/lib/gentle-ai-renderer.ts +142 -26
  32. package/lib/native-choice-list.ts +194 -0
  33. package/lib/native-fullscreen-interaction.ts +47 -0
  34. package/lib/native-pointer-region.ts +164 -0
  35. package/lib/native-review-cli.ts +103 -12
  36. package/lib/provider-contract-bundle.ts +88 -6
  37. package/lib/review-candidate-view-owner.ts +177 -0
  38. package/lib/review-candidate-view.ts +127 -35
  39. package/lib/review-consent-ui.ts +65 -0
  40. package/lib/review-host-relay.ts +146 -60
  41. package/lib/review-integration-v2.ts +92 -13
  42. package/lib/review-last-event-controller.ts +1 -0
  43. package/lib/review-relay-contract.ts +11 -0
  44. package/lib/review-repository.ts +2 -2
  45. package/lib/review-risk-assessment.ts +339 -0
  46. package/lib/review-session-standing-permission-ipc.ts +309 -0
  47. package/lib/review-session-standing-permission.ts +219 -0
  48. package/lib/sdd-preflight.ts +2 -2
  49. package/lib/shell-bar.ts +138 -0
  50. package/lib/shell-card.ts +136 -0
  51. package/lib/shell-changes-view.ts +205 -0
  52. package/lib/shell-changes.ts +210 -0
  53. package/lib/shell-gauge.ts +40 -0
  54. package/lib/shell-prompt.ts +119 -0
  55. package/lib/shell-todo.ts +280 -0
  56. package/lib/shell-usage-view.ts +76 -0
  57. package/lib/shell-usage.ts +246 -0
  58. package/lib/telemetry-trigger.ts +151 -0
  59. package/package.json +4 -4
  60. package/runtime/native-review-cli.mjs +102 -11
  61. package/runtime/review-integration-v2.mjs +92 -13
  62. package/runtime/review-relay-contract.mjs +11 -0
  63. package/runtime/review-risk-assessment.mjs +340 -0
  64. package/runtime/telemetry-trigger.mjs +152 -0
  65. package/scripts/build-runtime-modules.mjs +2 -0
  66. package/scripts/gentle-ai-installer.mjs +10 -10
  67. package/scripts/test-packed-runner.mjs +22 -0
  68. package/scripts/verify-package-files.mjs +18 -13
  69. package/skills/_shared/review-ledger-contract.md +9 -1
  70. package/skills/issue-creation/SKILL.md +53 -93
  71. package/tests/agents-config.test.ts +143 -0
  72. package/tests/agents-fake-child.ts +52 -0
  73. package/tests/agents-history.test.ts +54 -0
  74. package/tests/agents-protocol.test.ts +153 -0
  75. package/tests/agents-runner-process.test.ts +111 -0
  76. package/tests/agents-runner.test.ts +402 -0
  77. package/tests/agents-transcript.test.ts +30 -0
  78. package/tests/agents-view.test.ts +274 -0
  79. package/tests/agents-widget.test.ts +111 -0
  80. package/tests/ask-user-choice.test.ts +157 -3
  81. package/tests/codegraph-tools.test.ts +110 -1
  82. package/tests/devbinary/native-review-parity.devtest.ts +108 -0
  83. package/tests/fixtures/agents-process-child.mjs +23 -0
  84. package/tests/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
  85. package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
  86. package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
  87. package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
  88. package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
  89. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
  90. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
  91. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -0
  92. package/tests/gentle-agents.test.ts +741 -0
  93. package/tests/gentle-ai-binary.test.ts +1 -1
  94. package/tests/gentle-ai-installer.test.ts +47 -47
  95. package/tests/gentle-ai-renderer.test.ts +65 -0
  96. package/tests/gentle-ai.test.ts +31 -14
  97. package/tests/gentle-card-text.ts +35 -0
  98. package/tests/gentle-shell.test.ts +527 -0
  99. package/tests/gentle-todo.test.ts +182 -0
  100. package/tests/issue-creation-skill.test.ts +103 -0
  101. package/tests/native-choice-list.test.ts +202 -0
  102. package/tests/native-fullscreen-interaction.test.ts +125 -0
  103. package/tests/native-pointer-region.test.ts +245 -0
  104. package/tests/native-review-capability-contract.test.ts +33 -1
  105. package/tests/native-review-cli.test.ts +40 -0
  106. package/tests/native-review-consent.test.ts +91 -0
  107. package/tests/native-review-parity-runtime.test.ts +8 -2
  108. package/tests/native-review-parity.test.ts +29 -22
  109. package/tests/orchestrator-budget.test.ts +71 -2
  110. package/tests/orchestrator-rdd-ownership.test.ts +10 -1
  111. package/tests/package-manifest.test.ts +134 -9
  112. package/tests/provider-contract-bundle.test.ts +76 -0
  113. package/tests/provider-contract-mirror.test.ts +19 -0
  114. package/tests/quiet-tool-rendering.test.ts +96 -37
  115. package/tests/rdd-aware-verification-contract.test.ts +216 -0
  116. package/tests/rdd-status-line.test.ts +286 -0
  117. package/tests/review-agent-end-preflight.test.ts +408 -0
  118. package/tests/review-candidate-view.test.ts +452 -6
  119. package/tests/review-contract-prompt.test.ts +142 -0
  120. package/tests/review-controller-native-recovery.test.ts +29 -4
  121. package/tests/review-controller-native-routing.test.ts +321 -4
  122. package/tests/review-controller-workspace-root.test.ts +45 -2
  123. package/tests/review-controller.test.ts +26 -1
  124. package/tests/review-host-relay-routing.test.ts +229 -11
  125. package/tests/review-host-relay.test.ts +195 -7
  126. package/tests/review-integration-v2-forward.test.ts +47 -0
  127. package/tests/review-integration-v2.test.ts +112 -0
  128. package/tests/review-last-event-closure.test.ts +7 -2
  129. package/tests/review-ledger-contract.test.ts +1 -1
  130. package/tests/review-relay-contract.test.ts +26 -0
  131. package/tests/review-repository.test.ts +28 -1
  132. package/tests/review-risk-assessment.test.ts +626 -0
  133. package/tests/review-session-standing-permission-controller.test.ts +608 -0
  134. package/tests/review-session-standing-permission-ipc.test.ts +233 -0
  135. package/tests/review-session-standing-permission-runtime.test.ts +212 -0
  136. package/tests/review-session-standing-permission.test.ts +126 -0
  137. package/tests/runtime-harness.mjs +1 -0
  138. package/tests/shell-bar.test.ts +176 -0
  139. package/tests/shell-card.test.ts +118 -0
  140. package/tests/shell-changes-view.test.ts +146 -0
  141. package/tests/shell-changes.test.ts +182 -0
  142. package/tests/shell-prompt.test.ts +118 -0
  143. package/tests/shell-todo.test.ts +170 -0
  144. package/tests/shell-usage-view.test.ts +62 -0
  145. package/tests/shell-usage.test.ts +197 -0
  146. package/tests/telemetry-trigger.test.ts +349 -0
  147. package/tests/writer-edit-surface-scope.test.ts +153 -17
  148. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
  149. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
  150. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
  151. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
  152. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
  153. /package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/schemas/targeted-validator.schema.json +0 -0
@@ -114,8 +114,21 @@ function measureOrchestratorPromptBytes(assetsDir: string): number {
114
114
  // 2.2 — Byte budget (Spec: Always-On Injection Byte Budget)
115
115
  // ---------------------------------------------------------------------------
116
116
 
117
+ // gentle-pi#661: `renderOrchestratorPrompt`/`getOrchestratorPrompt` default
118
+ // `rddStatusLine` to the "unknown (native status unavailable)" line -- the
119
+ // longest of the three renderable RDD status lines -- precisely so that a
120
+ // no-argument call renders the worst case this budget measures, not a
121
+ // smaller placeholder that production would later exceed. Assert that line
122
+ // is actually present so a future default change cannot silently start
123
+ // measuring a shorter render again.
124
+ const RDD_WORST_CASE_LINE = "Receipt-driven development: unknown (native status unavailable)";
125
+
117
126
  test("getOrchestratorPrompt return value stays within the canonical 8,192 B budget at a short assets root", () => {
118
127
  const rendered = __testing.renderOrchestratorPrompt(representativeProductionAssetsDir);
128
+ assert.ok(
129
+ rendered.includes(RDD_WORST_CASE_LINE),
130
+ "the default render must include the worst-case RDD status line to measure the real production budget",
131
+ );
119
132
  const bytes = Buffer.byteLength(rendered, "utf8");
120
133
  assert.ok(
121
134
  bytes <= BUDGET_BYTES,
@@ -125,6 +138,10 @@ test("getOrchestratorPrompt return value stays within the canonical 8,192 B budg
125
138
 
126
139
  test(`getOrchestratorPrompt keeps a controlled long (>= ${MIN_CONTROLLED_LONG_ASSETS_ROOT_CHARS} char) assets root within the canonical budget`, () => {
127
140
  const rendered = __testing.renderOrchestratorPrompt(controlledLongAssetsDir);
141
+ assert.ok(
142
+ rendered.includes(RDD_WORST_CASE_LINE),
143
+ "the default render must include the worst-case RDD status line to measure the real production budget",
144
+ );
128
145
  const bytes = measureOrchestratorPromptBytes(controlledLongAssetsDir);
129
146
  assert.equal(bytes, Buffer.byteLength(rendered, "utf8"), "child-process and direct render byte counts must match");
130
147
  assert.ok(
@@ -347,8 +364,8 @@ test("core-alone: load-bearing direct-delegation tokens remain without lazy unio
347
364
 
348
365
  test("core-alone: dynamic Gentle AI ownership replaces package lifecycle instructions", () => {
349
366
  const core = readRealAsset("orchestrator.md");
350
- assert.match(core, /dynamically supplies runtime-specific RDD instructions via generated Pi APPEND_SYSTEM composition/);
351
- assert.match(core, /if absent or unsupported, this package does not invent or fall back/);
367
+ assert.match(core, /injects the mirrored provider-bundle review execution contract into this session's system prompt at start/);
368
+ assert.match(core, /Absent that mirrored contract, this package invents no lifecycle instructions/);
352
369
  assert.doesNotMatch(core, /start -> finalize -> validate/i);
353
370
  assert.doesNotMatch(core, /receipt validation/i);
354
371
  });
@@ -420,3 +437,55 @@ test("getOrchestratorPrompt memoizes the return across calls", () => {
420
437
  const second = __testing.getOrchestratorPrompt();
421
438
  assert.equal(second, first, "second call must return the memoized string");
422
439
  });
440
+
441
+ // ---------------------------------------------------------------------------
442
+ // gentle-pi#661 follow-up: byte-budget compression must not turn a pointer
443
+ // into an opaque "Detail: `file.md`" -- each pointer line must still name
444
+ // the lazy-loaded material it points to, in the shortest form that still
445
+ // says what is over there.
446
+ // ---------------------------------------------------------------------------
447
+
448
+ test("every compressed lazy-file pointer in the core still names the material it points to", () => {
449
+ const core = readFileSync(join(REAL_ASSETS_DIR, "orchestrator.md"), "utf8");
450
+ // One-sentence "<named material>: `file.md`." pointers, compressed for the
451
+ // byte budget -- each must keep naming what is over there, not collapse
452
+ // to an opaque "Detail: `file.md`.".
453
+ const namedPointers: ReadonlyArray<{ file: string; mustName: readonly string[] }> = [
454
+ {
455
+ file: "orchestrator-delegation.md",
456
+ mustName: ["Per-action table", "Work Routing Ladder", "Canonical Workflows", "blocking-prompt relays"],
457
+ },
458
+ {
459
+ file: "orchestrator-memory.md",
460
+ mustName: ["Phase table", "artifact keys"],
461
+ },
462
+ {
463
+ file: "orchestrator-skills.md",
464
+ mustName: ["Discovery order", "intent hints"],
465
+ },
466
+ ];
467
+ for (const { file, mustName } of namedPointers) {
468
+ // The pointer sentence: from the previous sentence boundary up to and
469
+ // including the backtick-quoted filename. `orchestrator-delegation.md`
470
+ // is referenced more than once in the core (a language-boundary pointer
471
+ // earlier, this compressed per-action pointer later) -- take the LAST
472
+ // occurrence, which is the one under test here.
473
+ const fileToken = `\`${file}\``;
474
+ const fileIndex = core.lastIndexOf(fileToken);
475
+ assert.ok(fileIndex >= 0, `core is missing a pointer to ${file}`);
476
+ const sentenceStart = core.lastIndexOf(".", fileIndex);
477
+ const pointerSentence = core.slice(sentenceStart + 1, fileIndex + fileToken.length);
478
+ for (const name of mustName) {
479
+ assert.ok(
480
+ pointerSentence.includes(name),
481
+ `the pointer to ${file} must name "${name}", got: ${JSON.stringify(pointerSentence.trim())}`,
482
+ );
483
+ }
484
+ }
485
+ // The SDD workflow pointer was never compressed to a bare filename; its
486
+ // surrounding paragraphs already name the material at length.
487
+ assert.match(
488
+ core,
489
+ /SDD phases, native dispatcher rules, status contract, preflight\/init guards, artifact-store policy, execution mode, Strict TDD forwarding, phase result contract, and review workload guard/,
490
+ );
491
+ });
@@ -6,7 +6,7 @@ import test from "node:test";
6
6
  const ROOT = join(import.meta.dirname, "..");
7
7
  const ASSETS = join(ROOT, "assets");
8
8
  const BOUNDARY =
9
- "Gentle AI dynamically supplies runtime-specific RDD instructions via generated Pi APPEND_SYSTEM composition. Follow only those exact native instructions; if absent or unsupported, this package does not invent or fall back.";
9
+ "This package injects the mirrored provider-bundle review execution contract into this session's system prompt at start; Gentle AI writes nothing into the Pi system prompt, and this package owns everything else here. Absent that mirrored contract, this package invents no lifecycle instructions.";
10
10
 
11
11
  function read(relativePath: string): string {
12
12
  return readFileSync(join(ROOT, relativePath), "utf8");
@@ -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
 
@@ -9,11 +9,12 @@ import {
9
9
  rmSync,
10
10
  writeFileSync,
11
11
  } from "node:fs";
12
- import { tmpdir } from "node:os";
12
+ import { homedir, tmpdir } from "node:os";
13
13
  import { dirname, join } from "node:path";
14
14
  import test from "node:test";
15
15
  import { fileURLToPath } from "node:url";
16
16
  import { applyModelConfig } from "../extensions/gentle-ai.ts";
17
+ import { resolveGentlePiAgentHome } from "../lib/agent-home.ts";
17
18
  import { installSddAssets } from "../lib/sdd-preflight.ts";
18
19
 
19
20
  const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
@@ -79,6 +80,8 @@ interface PackageJson {
79
80
  files?: string[];
80
81
  scripts?: Record<string, string>;
81
82
  dependencies?: Record<string, string>;
83
+ peerDependencies?: Record<string, string>;
84
+ devDependencies?: Record<string, string>;
82
85
  bundledDependencies?: string[];
83
86
  bundleDependencies?: string[];
84
87
  repository?: {
@@ -98,6 +101,15 @@ function readPackageJson(): PackageJson {
98
101
  }
99
102
  }
100
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
+
101
113
  test("package manifest has no obsolete native activation build surface", () => {
102
114
  const packageJson = readPackageJson();
103
115
  const manifest = JSON.stringify(packageJson);
@@ -261,20 +273,20 @@ test("package manifest installs pi-pretty through a wrapper without bundling nat
261
273
  );
262
274
  });
263
275
 
264
- test("package verification binds the published Gentle AI v2.5.0 runtime pin", () => {
276
+ test("package verification binds the published Gentle AI v2.7.0 runtime pin", () => {
265
277
  const installer = readFileSync(join(PACKAGE_ROOT, "scripts", "gentle-ai-installer.mjs"), "utf8");
266
278
  const binary = readFileSync(join(PACKAGE_ROOT, "lib", "gentle-ai-binary.ts"), "utf8");
267
279
  const verifier = readFileSync(join(PACKAGE_ROOT, "scripts", "verify-package-files.mjs"), "utf8");
268
280
 
269
- assert.match(installer, /INSTALLER_VERSION = "2\.5\.0"/);
281
+ assert.match(installer, /INSTALLER_VERSION = "2\.7\.0"/);
270
282
  assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_PACKAGE.*GENTLE_AI_WINDOWS_SOURCE_MODULE/);
271
- assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:0nvTLJFAf9ruBDwTvi\+aDrWt\/dt\/GKvUP2cFVB6c2DI="/);
283
+ assert.match(installer, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:SE4KCLo3y1qWaTq\+7HJKZ1DkAAAl\+w\+82YY0\+qQkFwM="/);
272
284
  assert.match(installer, /GOTOOLCHAIN: "local"/);
273
285
  assert.match(installer, /GOSUMDB: "sum\.golang\.org"/);
274
286
  assert.match(binary, /GENTLE_AI_VERSION = INSTALLER_VERSION/);
275
287
  assert.match(binary, /GO_SUMDB_SOURCE_BUILD/);
276
288
  assert.match(binary, /GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM/);
277
- assert.match(verifier, /v2\.5\.0/);
289
+ assert.match(verifier, /v2\.7\.0/);
278
290
  });
279
291
 
280
292
 
@@ -895,6 +907,22 @@ test("jd-fix-agent packaged allowlist includes write tools", () => {
895
907
  }
896
908
  });
897
909
 
910
+ test("sdd-explore packages its CodeGraph-enabled exploration allowlist", () => {
911
+ const agentPath = join(PACKAGE_ROOT, "assets", "agents", "sdd-explore.md");
912
+ const { name, tools } = readAgentDefinition(agentPath);
913
+
914
+ assert.equal(name, "sdd-explore");
915
+ assert.deepEqual(tools, [
916
+ "read",
917
+ "grep",
918
+ "find",
919
+ "codegraph",
920
+ "edit",
921
+ "write",
922
+ "mem_save",
923
+ ]);
924
+ });
925
+
898
926
  test("gentle-ai-worker packages the exact scoped writer contract", () => {
899
927
  const agentsDir = join(PACKAGE_ROOT, "assets", "agents");
900
928
  const agentPath = join(agentsDir, "gentle-ai-worker.md");
@@ -1055,6 +1083,103 @@ test("installSddAssets installs gentle-ai-worker with a loader-compatible scoped
1055
1083
  );
1056
1084
  });
1057
1085
 
1086
+ test("agent home resolver centralizes Gentle and Pi agent-dir precedence", () => {
1087
+ const explicitGentleHome = mkdtempSync(join(tmpdir(), "gentle-pi-resolver-explicit-"));
1088
+ const piAgentDir = mkdtempSync(join(tmpdir(), "gentle-pi-resolver-pi-dir-"));
1089
+
1090
+ try {
1091
+ assert.equal(
1092
+ resolveGentlePiAgentHome({
1093
+ GENTLE_PI_AGENT_HOME: explicitGentleHome,
1094
+ PI_CODING_AGENT_DIR: piAgentDir,
1095
+ }),
1096
+ explicitGentleHome,
1097
+ );
1098
+ assert.equal(resolveGentlePiAgentHome({ PI_CODING_AGENT_DIR: piAgentDir }), piAgentDir);
1099
+ assert.equal(resolveGentlePiAgentHome({}), join(homedir(), ".pi", "agent"));
1100
+ assert.equal(
1101
+ resolveGentlePiAgentHome({ GENTLE_PI_AGENT_HOME: "", PI_CODING_AGENT_DIR: piAgentDir }),
1102
+ piAgentDir,
1103
+ "an empty explicit override falls through like Pi Subagents does",
1104
+ );
1105
+ assert.equal(
1106
+ resolveGentlePiAgentHome({ PI_CODING_AGENT_DIR: "" }),
1107
+ join(homedir(), ".pi", "agent"),
1108
+ "an empty PI_CODING_AGENT_DIR falls through like Pi Subagents does",
1109
+ );
1110
+ } finally {
1111
+ rmSync(explicitGentleHome, { recursive: true, force: true });
1112
+ rmSync(piAgentDir, { recursive: true, force: true });
1113
+ }
1114
+ });
1115
+
1116
+ test("asset installation uses PI_CODING_AGENT_DIR as the Pi agent home when no explicit Gentle override is set", () => {
1117
+ const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
1118
+ const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
1119
+ const temporaryPiAgentDir = mkdtempSync(join(tmpdir(), "gentle-pi-agent-dir-"));
1120
+ const explicitGentleHome = mkdtempSync(join(tmpdir(), "gentle-pi-explicit-home-"));
1121
+
1122
+ try {
1123
+ delete process.env.GENTLE_PI_AGENT_HOME;
1124
+ process.env.PI_CODING_AGENT_DIR = temporaryPiAgentDir;
1125
+
1126
+ installSddAssets(PACKAGE_ROOT, true);
1127
+
1128
+ const installedPath = join(temporaryPiAgentDir, "agents", "gentle-ai-explore.md");
1129
+ assert.ok(existsSync(installedPath), "managed agents must install where Pi Subagents reads global definitions");
1130
+ assert.deepEqual(readAgentDefinition(installedPath).tools, MANAGED_EXEMPLAR_TOOLS);
1131
+ assert.ok(
1132
+ !existsSync(join(explicitGentleHome, "agents", "gentle-ai-explore.md")),
1133
+ "the explicit override fixture must still be untouched before it is selected",
1134
+ );
1135
+
1136
+ process.env.GENTLE_PI_AGENT_HOME = explicitGentleHome;
1137
+ installSddAssets(PACKAGE_ROOT, true);
1138
+ assert.ok(
1139
+ existsSync(join(explicitGentleHome, "agents", "gentle-ai-explore.md")),
1140
+ "GENTLE_PI_AGENT_HOME remains the explicit test/operator override",
1141
+ );
1142
+ } finally {
1143
+ if (previousAgentHome === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
1144
+ else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
1145
+ if (previousPiAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
1146
+ else process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
1147
+ rmSync(temporaryPiAgentDir, { recursive: true, force: true });
1148
+ rmSync(explicitGentleHome, { recursive: true, force: true });
1149
+ }
1150
+ });
1151
+
1152
+ test("global model routing uses PI_CODING_AGENT_DIR for package-installed agents", () => {
1153
+ const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
1154
+ const previousPiAgentDir = process.env.PI_CODING_AGENT_DIR;
1155
+ const temporaryPiAgentDir = mkdtempSync(join(tmpdir(), "gentle-pi-model-agent-dir-"));
1156
+ const temporaryProject = mkdtempSync(join(tmpdir(), "gentle-pi-model-project-"));
1157
+
1158
+ try {
1159
+ delete process.env.GENTLE_PI_AGENT_HOME;
1160
+ process.env.PI_CODING_AGENT_DIR = temporaryPiAgentDir;
1161
+ installSddAssets(PACKAGE_ROOT, true);
1162
+
1163
+ const result = applyModelConfig(temporaryProject, {
1164
+ "gentle-ai-explore": { model: "provider/model", thinking: "high" },
1165
+ });
1166
+
1167
+ assert.equal(result.updated, 2);
1168
+ const config = JSON.parse(readFileSync(join(temporaryPiAgentDir, "subagents.json"), "utf8"));
1169
+ assert.deepEqual(config.model_profiles["gentle-ai-explore"], {
1170
+ model: "provider/model",
1171
+ effort: "high",
1172
+ });
1173
+ } finally {
1174
+ if (previousAgentHome === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
1175
+ else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
1176
+ if (previousPiAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
1177
+ else process.env.PI_CODING_AGENT_DIR = previousPiAgentDir;
1178
+ rmSync(temporaryPiAgentDir, { recursive: true, force: true });
1179
+ rmSync(temporaryProject, { recursive: true, force: true });
1180
+ }
1181
+ });
1182
+
1058
1183
  test("normal and forced installation copy generic agents with complete role contracts", () => {
1059
1184
  const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
1060
1185
  const expectedTools = {
@@ -1146,8 +1271,8 @@ test("orchestrator routes generic roles without static RDD lens routing", () =>
1146
1271
  }
1147
1272
 
1148
1273
  const core = readFileSync(join(PACKAGE_ROOT, "assets", "orchestrator.md"), "utf8");
1149
- assert.match(core, /Gentle AI dynamically supplies runtime-specific RDD instructions/);
1150
- assert.match(core, /this package does not invent or fall back/);
1274
+ assert.match(core, /injects the mirrored provider-bundle review execution contract/);
1275
+ assert.match(core, /this package invents no lifecycle instructions/);
1151
1276
  });
1152
1277
 
1153
1278
  test("pi-pretty wrapper uses real package path resolution for pnpm symlink installs", () => {
@@ -1163,9 +1288,9 @@ test("pi-pretty wrapper uses real package path resolution for pnpm symlink insta
1163
1288
  assert.match(wrapper, /quietToolsEnabled/);
1164
1289
  });
1165
1290
 
1166
- test("v2.3.0 release package and runtime stop before publication", () => {
1291
+ test("v2.5.0 release package and runtime stop before publication", () => {
1167
1292
  const packageJson = readPackageJson();
1168
- assert.equal(packageJson.version, "2.3.0", "the release manifest must remain explicitly pinned to v2.3.0");
1293
+ assert.equal(packageJson.version, "2.5.0", "the release manifest must remain explicitly pinned to v2.5.0");
1169
1294
  assert.equal(
1170
1295
  packageJson.scripts?.test,
1171
1296
  "node --experimental-strip-types --test tests/*.test.ts && pnpm run check:provider-contract && pnpm run test:harness",
@@ -35,6 +35,16 @@ function fixtureEntries(): Map<string, Buffer> {
35
35
  return new Map(FIXTURE_PATHS.map((path) => [path, readFileSync(join(FIXTURE_ROOT, path))]));
36
36
  }
37
37
 
38
+ // Fixture provenance: the real gentle-ai provider contract bundle 1.2.0
39
+ // release archive (contract semver 1.2.0), extracted. Adds `orchestration`
40
+ // to the manifest: gentle-pi#560 / gentle-ai#4056, #4057.
41
+ const FIXTURE_1_2_0_ROOT = join(import.meta.dirname, "fixtures", "provider-contract-bundle", "v1.2.0");
42
+ const FIXTURE_1_2_0_PATHS = [...FIXTURE_PATHS, "orchestration/pi.md"] as const;
43
+
44
+ function fixture120Entries(): Map<string, Buffer> {
45
+ return new Map(FIXTURE_1_2_0_PATHS.map((path) => [path, readFileSync(join(FIXTURE_1_2_0_ROOT, path))]));
46
+ }
47
+
38
48
  function sha256Hex(bytes: Buffer): string {
39
49
  return createHash("sha256").update(bytes).digest("hex");
40
50
  }
@@ -177,11 +187,25 @@ test("generated baselines project roles, capabilities, and runtime registration
177
187
  assert.equal(capabilities.transport_capability, PROVIDER_TRANSPORT_CAPABILITY);
178
188
  assert.deepEqual(capabilities.mandatory_capabilities, [PROVIDER_TRANSPORT_CAPABILITY]);
179
189
  assert.equal(capabilities.pi_registered, true);
190
+ assert.deepEqual(capabilities.orchestration, []);
180
191
  // Byte determinism: regenerating from the same bundle is identical.
181
192
  const again = generateProviderContractBaselines(verifyProviderContractBundleTree(FIXTURE_ROOT));
182
193
  assert.deepEqual([...again.entries()], [...baselines.entries()]);
183
194
  });
184
195
 
196
+ test("generated 1.2.0 baselines record the mirrored orchestration entry", () => {
197
+ const bundle = verifyProviderContractBundleTree(FIXTURE_1_2_0_ROOT);
198
+ const baselines = generateProviderContractBaselines(bundle);
199
+ const capabilities = JSON.parse(baselines.get("provider-capabilities.baseline.json") as string) as { orchestration: { runtime: string; path: string; sha256: string }[] };
200
+ assert.deepEqual(capabilities.orchestration, [
201
+ {
202
+ runtime: PI_RUNTIME_IDENTITY,
203
+ path: "orchestration/pi.md",
204
+ sha256: bundle.entrySha256.get("orchestration/pi.md"),
205
+ },
206
+ ]);
207
+ });
208
+
185
209
  // --- inventory and hash rejections ---------------------------------------------
186
210
 
187
211
  test("rejects a ninth file in the inventory", () => {
@@ -315,6 +339,58 @@ test("rejects a schema whose $id does not match the manifest result schema id",
315
339
  );
316
340
  });
317
341
 
342
+ // --- orchestration (contract 1.2.0) -------------------------------------------------
343
+
344
+ test("verifies the 1.2.0 fixture tree and exposes the mirrored pi orchestration text", () => {
345
+ const bundle = verifyProviderContractBundleTree(FIXTURE_1_2_0_ROOT);
346
+ assert.equal(bundle.contractSemver, "1.2.0");
347
+ assert.deepEqual([bundle.major, bundle.minor, bundle.patch], [1, 2, 0]);
348
+ assert.equal(bundle.entries.size, 9);
349
+ assert.deepEqual([...bundle.orchestration.keys()], [PI_RUNTIME_IDENTITY]);
350
+ const piText = bundle.orchestration.get(PI_RUNTIME_IDENTITY);
351
+ assert.equal(piText, readFileSync(join(FIXTURE_1_2_0_ROOT, "orchestration/pi.md"), "utf8"));
352
+ assert.equal(bundle.entrySha256.get("orchestration/pi.md"), sha256Hex(readFileSync(join(FIXTURE_1_2_0_ROOT, "orchestration/pi.md"))));
353
+ });
354
+
355
+ test("rejects a 1.2.0 manifest without the required orchestration array", () => {
356
+ assertRejects(
357
+ withManifest(fixture120Entries(), (manifest) => {
358
+ delete manifest.orchestration;
359
+ }),
360
+ "manifest.orchestration is required from contract 1.2.0",
361
+ );
362
+ });
363
+
364
+ test("rejects a 1.2.0 orchestration entry whose runtime is not registered in manifest.runtimes", () => {
365
+ assertRejects(
366
+ withManifest(fixture120Entries(), (manifest) => {
367
+ const orchestration = manifest.orchestration as { runtime: string; file: { path: string } }[];
368
+ orchestration[0]!.runtime = "aider";
369
+ orchestration[0]!.file.path = "orchestration/aider.md";
370
+ }),
371
+ 'manifest.orchestration[0].runtime "aider" is not registered in manifest.runtimes',
372
+ );
373
+ });
374
+
375
+ test("rejects a 1.2.0 orchestration entry whose file bytes do not match its manifest SHA-256", () => {
376
+ const entries = fixture120Entries();
377
+ const tampered = Buffer.from(entries.get("orchestration/pi.md") as Buffer);
378
+ tampered[tampered.length - 2] = 0x20;
379
+ entries.set("orchestration/pi.md", tampered);
380
+ assertRejects(entries, "does not match its manifest SHA-256");
381
+ });
382
+
383
+ test("rejects an orchestration array on a 1.1.0 manifest", () => {
384
+ assertRejects(
385
+ withManifest(fixtureEntries(), (manifest) => {
386
+ manifest.orchestration = [
387
+ { runtime: PI_RUNTIME_IDENTITY, file: { path: "orchestration/pi.md", sha256: "0".repeat(64) } },
388
+ ];
389
+ }),
390
+ "manifest.orchestration is not part of contract 1.1.0",
391
+ );
392
+ });
393
+
318
394
  // --- archive safety ------------------------------------------------------------------
319
395
 
320
396
  test("rejects a tar entry that escapes the bundle root", () => {
@@ -27,6 +27,10 @@ const FIXTURE_PATHS = [
27
27
  "vectors/targeted-validator.json",
28
28
  ] as const;
29
29
 
30
+ // Fixture provenance: the real gentle-ai provider contract bundle 1.2.0
31
+ // release archive, extracted. gentle-pi#560 / gentle-ai#4056, #4057.
32
+ const FIXTURE_1_2_0_ROOT = join(import.meta.dirname, "fixtures", "provider-contract-bundle", "v1.2.0");
33
+
30
34
  interface LockRecord {
31
35
  schema: string;
32
36
  acquisition: string;
@@ -105,6 +109,21 @@ test("mirrors a verified local bundle tree and the offline drift check passes",
105
109
  });
106
110
  });
107
111
 
112
+ test("mirrors a verified local 1.2.0 bundle tree, including its orchestration entry, and the offline drift check passes", () => {
113
+ withTemporaryRoot((packageRoot) => {
114
+ mirrorProviderContractBundle(FIXTURE_1_2_0_ROOT, packageRoot);
115
+ const lockPath = join(packageRoot, PROVIDER_CONTRACT_MIRROR_ROOT, PROVIDER_CONTRACT_LOCK_FILE);
116
+ const lock = JSON.parse(readFileSync(lockPath, "utf8")) as LockRecord;
117
+ assert.equal(lock.contract_semver, "1.2.0");
118
+ assert.equal(Object.keys(lock.entries).length, 9);
119
+ assert.ok("orchestration/pi.md" in lock.entries, "the mirrored lock must record orchestration/pi.md");
120
+ const mirrored = join(packageRoot, PROVIDER_CONTRACT_MIRROR_ROOT, "v1.2.0", "bundle", "orchestration", "pi.md");
121
+ assert.ok(existsSync(mirrored), "orchestration/pi.md must be mirrored");
122
+ assert.deepEqual(readFileSync(mirrored), readFileSync(join(FIXTURE_1_2_0_ROOT, "orchestration", "pi.md")));
123
+ assert.deepEqual(checkProviderContractMirror(packageRoot), []);
124
+ });
125
+ });
126
+
108
127
  test("mirrors a verified local archive and records the archive digest in the lock", () => {
109
128
  withTemporaryRoot((packageRoot) => {
110
129
  const archivePath = join(packageRoot, "gentle-ai-review-provider-contract-1.1.0.tar.gz");