gentle-pi 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/README.md +76 -79
  2. package/assets/agents/review-readability.md +0 -2
  3. package/assets/agents/review-reliability.md +0 -2
  4. package/assets/agents/review-resilience.md +0 -2
  5. package/assets/agents/review-risk.md +0 -2
  6. package/assets/agents/sdd-apply.md +5 -3
  7. package/assets/agents/sdd-explore.md +1 -0
  8. package/assets/agents/sdd-proposal.md +2 -0
  9. package/assets/agents/sdd-research.md +54 -0
  10. package/assets/agents/sdd-status.md +4 -4
  11. package/assets/agents/sdd-tasks.md +2 -3
  12. package/assets/agents/sdd-verify.md +21 -1
  13. package/assets/chains/sdd-full.chain.md +1 -1
  14. package/assets/chains/sdd-verify.chain.md +1 -1
  15. package/assets/orchestrator-delegation.md +56 -249
  16. package/assets/orchestrator-memory.md +2 -0
  17. package/assets/orchestrator.md +20 -40
  18. package/assets/sdd-orchestrator-workflow.md +30 -37
  19. package/assets/support/sdd-status-contract.md +7 -7
  20. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  21. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  22. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  23. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  24. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
  25. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
  26. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
  27. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
  28. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
  29. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
  30. package/docs/native-authority-architecture.md +9 -11
  31. package/docs/review-integration.md +27 -373
  32. package/extensions/ask-user-choice.ts +151 -0
  33. package/extensions/gentle-ai.ts +2349 -3785
  34. package/extensions/quiet-tools.ts +515 -32
  35. package/extensions/sdd-init.ts +4 -8
  36. package/lib/agent-home.ts +8 -0
  37. package/lib/gentle-ai-renderer.ts +70 -0
  38. package/lib/model-routing-authority.ts +133 -0
  39. package/lib/native-review-cli.ts +387 -940
  40. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  41. package/lib/provider-contract-bundle.ts +88 -6
  42. package/lib/review-candidate-view.ts +341 -132
  43. package/lib/review-host-relay.ts +324 -96
  44. package/lib/review-integration-v2.ts +868 -307
  45. package/lib/review-last-event-controller.ts +35 -0
  46. package/lib/sdd-preflight.ts +179 -68
  47. package/lib/sdd-status.ts +66 -111
  48. package/lib/terminal-theme.ts +1 -1
  49. package/package.json +83 -82
  50. package/runtime/gentle-ai-binary.mjs +1 -1
  51. package/runtime/native-review-cli.mjs +337 -890
  52. package/runtime/review-integration-v2.mjs +816 -255
  53. package/runtime/review-relay-contract.mjs +1 -1
  54. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
  55. package/scripts/gentle-ai-installer.mjs +75 -22
  56. package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
  57. package/scripts/test-packed-runner.mjs +4 -7
  58. package/scripts/verify-package-files.mjs +23 -23
  59. package/skills/_shared/review-ledger-contract.md +14 -14
  60. package/skills/chained-pr/SKILL.md +3 -0
  61. package/skills/cognitive-doc-design/SKILL.md +1 -1
  62. package/skills/comment-writer/SKILL.md +1 -1
  63. package/skills/gentle-ai/SKILL.md +7 -74
  64. package/skills/issue-creation/SKILL.md +53 -93
  65. package/skills/judgment-day/SKILL.md +5 -7
  66. package/skills/rdd-defect-workflow/SKILL.md +3 -3
  67. package/skills/release/SKILL.md +3 -3
  68. package/skills/skill-registry/SKILL.md +1 -1
  69. package/skills/work-unit-commits/SKILL.md +3 -1
  70. package/tests/artifact-language.test.ts +24 -11
  71. package/tests/ask-user-choice.test.ts +264 -0
  72. package/tests/codegraph-tools.test.ts +3 -3
  73. package/tests/crosslane/cross-lane.mjs +15 -1168
  74. package/tests/delegated-key-learnings-contract.test.ts +8 -6
  75. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  76. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  77. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  78. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  79. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  80. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  81. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  82. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  83. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  84. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  85. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  86. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  87. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  88. package/tests/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
  89. package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
  90. package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
  91. package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
  92. package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
  93. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
  94. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
  95. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -0
  96. package/tests/gentle-ai-binary.test.ts +83 -13
  97. package/tests/gentle-ai-dev-binary.test.ts +51 -228
  98. package/tests/gentle-ai-installer.test.ts +137 -47
  99. package/tests/gentle-ai.test.ts +821 -131
  100. package/tests/gentle-theme.test.ts +133 -0
  101. package/tests/issue-creation-skill.test.ts +103 -0
  102. package/tests/maintainer/provider-relay.maintest.ts +340 -4
  103. package/tests/model-routing-authority.test.ts +257 -0
  104. package/tests/native-review-capability-contract.test.ts +45 -1
  105. package/tests/native-review-cli.test.ts +586 -962
  106. package/tests/native-review-consent.test.ts +150 -66
  107. package/tests/native-review-parity-runtime.test.ts +80 -359
  108. package/tests/native-review-parity.test.ts +674 -925
  109. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  110. package/tests/orchestrator-budget.test.ts +132 -88
  111. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  112. package/tests/package-manifest.test.ts +152 -36
  113. package/tests/provider-contract-bundle.test.ts +76 -0
  114. package/tests/provider-contract-mirror.test.ts +19 -0
  115. package/tests/provider-defect-handoff.test.ts +15 -118
  116. package/tests/quiet-tool-rendering.test.ts +1055 -28
  117. package/tests/review-agent-end-preflight.test.ts +408 -0
  118. package/tests/review-authority-recovery-docs.test.ts +1 -2
  119. package/tests/review-candidate-view.test.ts +409 -3
  120. package/tests/review-compact-contract.test.ts +4 -30
  121. package/tests/review-contract-prompt.test.ts +139 -0
  122. package/tests/review-controller-lock-status.test.ts +2 -2
  123. package/tests/review-controller-native-recovery.test.ts +363 -1450
  124. package/tests/review-controller-native-routing.test.ts +1251 -5642
  125. package/tests/review-controller-retired-ops.test.ts +1 -1
  126. package/tests/review-controller-workspace-root.test.ts +222 -71
  127. package/tests/review-controller.test.ts +26 -816
  128. package/tests/review-corrected-finalize-binding.test.ts +112 -153
  129. package/tests/review-dispatch-hydration-gap.test.ts +1 -53
  130. package/tests/review-gate.test.ts +0 -45
  131. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  132. package/tests/review-host-relay-routing.test.ts +328 -91
  133. package/tests/review-host-relay.test.ts +436 -14
  134. package/tests/review-integration-v2-forward.test.ts +623 -553
  135. package/tests/review-integration-v2.test.ts +239 -165
  136. package/tests/review-last-event-closure.test.ts +408 -0
  137. package/tests/review-ledger-contract.test.ts +97 -35
  138. package/tests/review-recovered-lineage-routing.test.ts +0 -47
  139. package/tests/review-relay-transport-agent.test.ts +119 -46
  140. package/tests/review-snapshot.test.ts +3 -2
  141. package/tests/runtime-harness.mjs +362 -196
  142. package/tests/sdd-agent-tools.test.ts +36 -0
  143. package/tests/sdd-preflight.test.ts +81 -15
  144. package/tests/sdd-status.test.ts +109 -110
  145. package/tests/skill-collision-prefixes.test.ts +5 -8
  146. package/tests/writer-edit-surface-scope.test.ts +366 -0
  147. package/themes/Gentleman-Cute.json +94 -0
  148. package/themes/Gentleman-Sexy.json +92 -0
  149. package/lib/git-commit-transaction.ts +0 -861
  150. package/runtime/git-commit-transaction.mjs +0 -862
  151. package/scripts/run-git-commit-transaction.mjs +0 -35
  152. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  153. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  154. package/tests/git-commit-transaction.test.ts +0 -530
  155. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
  156. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
  157. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
  158. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
  159. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
  160. /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
@@ -0,0 +1,257 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+ import { applySavedModelConfig, readModelConfig, readModelConfigAsync } from "../extensions/gentle-ai.ts";
7
+
8
+ test("model routing authority normalizes and preserves sync/async source status", async (t) => {
9
+ const loaded = await import("../lib/model-routing-authority.ts").then(
10
+ (module) => ({ module, error: undefined }),
11
+ (error) => ({ module: undefined, error }),
12
+ );
13
+ assert.ok(
14
+ loaded.module,
15
+ `shared model routing authority must load: ${String(loaded.error)}`,
16
+ );
17
+ const authority = loaded.module;
18
+ const root = mkdtempSync(join(tmpdir(), "gentle-pi-model-routing-authority-"));
19
+ const globalDir = join(root, "global");
20
+ const projectDir = join(root, "project");
21
+ const projectConfigDir = join(projectDir, ".pi", "gentle-ai");
22
+ const agentsDir = join(root, "agents");
23
+ mkdirSync(globalDir, { recursive: true });
24
+ mkdirSync(projectConfigDir, { recursive: true });
25
+ mkdirSync(agentsDir, { recursive: true });
26
+ t.after(() => rmSync(root, { recursive: true, force: true }));
27
+
28
+ assert.equal(authority.normalizeModelId(" openai/gpt-5 "), "openai/gpt-5");
29
+ assert.equal(authority.normalizeModelId("bad model"), undefined);
30
+ assert.deepEqual(authority.normalizeRoutingEntry(" inherit "), { model: "inherit" });
31
+ assert.deepEqual(
32
+ authority.normalizeRoutingEntry({ model: " anthropic/opus ", thinking: "high" }),
33
+ { model: "anthropic/opus", thinking: "high" },
34
+ );
35
+ assert.deepEqual(authority.normalizeRoutingEntry(null), undefined);
36
+ assert.deepEqual(
37
+ authority.normalizeModelConfig({
38
+ worker: " openai/gpt-5 ",
39
+ clear: {},
40
+ "not valid": "ignored",
41
+ nullValue: null,
42
+ }),
43
+ { worker: { model: "openai/gpt-5" }, clear: {} },
44
+ );
45
+
46
+ const missingPath = join(globalDir, "missing.json");
47
+ assert.deepEqual(authority.readModelConfigFile(missingPath), { status: "missing" });
48
+ assert.deepEqual(await authority.readModelConfigFileAsync(missingPath), { status: "missing" });
49
+
50
+ const validGlobalPath = join(globalDir, "valid.json");
51
+ writeFileSync(
52
+ validGlobalPath,
53
+ JSON.stringify({ worker: "openai/gpt-5", reviewer: { thinking: "medium" }, "not valid": "openai/gpt-4" }),
54
+ );
55
+ const validSync = authority.readModelConfigFile(validGlobalPath);
56
+ const validAsync = await authority.readModelConfigFileAsync(validGlobalPath);
57
+ assert.deepEqual(validSync, {
58
+ status: "valid",
59
+ config: {
60
+ worker: { model: "openai/gpt-5" },
61
+ reviewer: { model: undefined, thinking: "medium" },
62
+ "not valid": { model: "openai/gpt-4" },
63
+ },
64
+ });
65
+ assert.deepEqual(validAsync, validSync);
66
+
67
+ const invalidGlobalPath = join(globalDir, "invalid.json");
68
+ writeFileSync(invalidGlobalPath, "[]");
69
+ assert.deepEqual(authority.readModelConfigFile(invalidGlobalPath), {
70
+ status: "invalid",
71
+ path: invalidGlobalPath,
72
+ });
73
+ assert.deepEqual(await authority.readModelConfigFileAsync(invalidGlobalPath), {
74
+ status: "invalid",
75
+ path: invalidGlobalPath,
76
+ });
77
+
78
+ const projectPath = join(projectConfigDir, "models.json");
79
+ writeFileSync(projectPath, JSON.stringify({ project: "google/gemini" }));
80
+ assert.deepEqual(
81
+ authority.readSavedModelConfig(missingPath, projectPath),
82
+ { status: "valid", config: { project: { model: "google/gemini" } } },
83
+ );
84
+ assert.deepEqual(
85
+ await authority.readSavedModelConfigAsync(missingPath, projectPath),
86
+ await authority.readSavedModelConfig(missingPath, projectPath),
87
+ );
88
+ assert.deepEqual(authority.readSavedModelConfig(invalidGlobalPath, projectPath), {
89
+ status: "invalid",
90
+ path: invalidGlobalPath,
91
+ });
92
+ assert.deepEqual(await authority.readSavedModelConfigAsync(invalidGlobalPath, projectPath), {
93
+ status: "invalid",
94
+ path: invalidGlobalPath,
95
+ });
96
+
97
+ const previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
98
+ process.env.GENTLE_PI_CONFIG_HOME = globalDir;
99
+ t.after(() => {
100
+ if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
101
+ else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
102
+ });
103
+ writeFileSync(projectPath, JSON.stringify({ project: "google/gemini" }));
104
+ assert.deepEqual(readModelConfig(projectDir), { project: { model: "google/gemini" } });
105
+ assert.deepEqual(await readModelConfigAsync(projectDir), readModelConfig(projectDir));
106
+
107
+ writeFileSync(projectPath, "[]");
108
+ assert.deepEqual(authority.readModelConfigFile(projectPath), { status: "invalid", path: projectPath });
109
+ assert.deepEqual(await authority.readModelConfigFileAsync(projectPath), { status: "invalid", path: projectPath });
110
+ assert.deepEqual(readModelConfig(projectDir), {});
111
+ assert.deepEqual(await readModelConfigAsync(projectDir), {});
112
+
113
+ writeFileSync(join(globalDir, "models.json"), JSON.stringify({ global: "openai/gpt-5" }));
114
+ assert.deepEqual(readModelConfig(projectDir), { global: { model: "openai/gpt-5" } });
115
+ assert.deepEqual(await readModelConfigAsync(projectDir), readModelConfig(projectDir));
116
+
117
+ writeFileSync(join(globalDir, "models.json"), "[]");
118
+ assert.deepEqual(readModelConfig(projectDir), {});
119
+ assert.deepEqual(await readModelConfigAsync(projectDir), {});
120
+ });
121
+
122
+ test("saved-routing apply fails closed for invalid project and global sources", async (t) => {
123
+ const root = mkdtempSync(join(tmpdir(), "gentle-pi-model-routing-apply-"));
124
+ const configHome = join(root, "global");
125
+ const projectConfigDir = join(root, ".pi", "gentle-ai");
126
+ const projectAgentsDir = join(root, ".pi", "agents");
127
+ const projectProfileDir = join(root, ".pi");
128
+ const agentHome = join(root, "agent-home");
129
+ const agentHomeAgentsDir = join(agentHome, "agents");
130
+ const agentHomeSubagentsDir = join(agentHome, "subagents");
131
+ mkdirSync(configHome, { recursive: true });
132
+ mkdirSync(projectConfigDir, { recursive: true });
133
+ mkdirSync(projectAgentsDir, { recursive: true });
134
+ mkdirSync(projectProfileDir, { recursive: true });
135
+ mkdirSync(agentHomeAgentsDir, { recursive: true });
136
+ mkdirSync(agentHomeSubagentsDir, { recursive: true });
137
+ t.after(() => rmSync(root, { recursive: true, force: true }));
138
+
139
+ const previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
140
+ const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
141
+ process.env.GENTLE_PI_CONFIG_HOME = configHome;
142
+ process.env.GENTLE_PI_AGENT_HOME = agentHome;
143
+ t.after(() => {
144
+ if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
145
+ else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
146
+ if (previousAgentHome === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
147
+ else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
148
+ });
149
+
150
+ const agentPath = join(projectAgentsDir, "worker.md");
151
+ writeFileSync(agentPath, "---\nname: worker\ndescription: Worker\n---\nbody\n");
152
+ const profilePath = join(projectProfileDir, "subagents.json");
153
+ const profileBytes = JSON.stringify({
154
+ unrelated: { keep: true },
155
+ model_profiles: { worker: { model: "existing/model", effort: "low" } },
156
+ }, null, 2) + "\n";
157
+ writeFileSync(profilePath, profileBytes);
158
+ const context = { cwd: root } as Parameters<typeof applySavedModelConfig>[0];
159
+ const projectPath = join(projectConfigDir, "models.json");
160
+ const globalPath = join(configHome, "models.json");
161
+ let mutatorCalls = 0;
162
+ const applyConfig = async () => {
163
+ mutatorCalls += 1;
164
+ return { updated: 0, skipped: 0 };
165
+ };
166
+
167
+ for (const projectValue of ["{", "[]", "null"] as const) {
168
+ writeFileSync(projectPath, projectValue);
169
+ const before = statSync(profilePath);
170
+ const result = await applySavedModelConfig(context, applyConfig);
171
+ assert.deepEqual(result, { updated: 0, skipped: 0, invalidPath: projectPath });
172
+ assert.equal(mutatorCalls, 0);
173
+ assert.equal(readFileSync(profilePath, "utf8"), profileBytes);
174
+ assert.equal(statSync(profilePath).mtimeMs, before.mtimeMs);
175
+ }
176
+
177
+ writeFileSync(globalPath, "[]");
178
+ writeFileSync(projectPath, JSON.stringify({ worker: "new/model" }));
179
+ const before = statSync(profilePath);
180
+ const result = await applySavedModelConfig(context, applyConfig);
181
+ assert.deepEqual(result, { updated: 0, skipped: 0, invalidPath: globalPath });
182
+ assert.equal(mutatorCalls, 0);
183
+ assert.equal(readFileSync(profilePath, "utf8"), profileBytes);
184
+ assert.equal(statSync(profilePath).mtimeMs, before.mtimeMs);
185
+ });
186
+
187
+ test("saved-routing apply preserves missing, valid, null, inherit, and omission behavior", async (t) => {
188
+ const root = mkdtempSync(join(tmpdir(), "gentle-pi-model-routing-apply-valid-"));
189
+ const configHome = join(root, "global");
190
+ const projectConfigDir = join(root, ".pi", "gentle-ai");
191
+ const projectAgentsDir = join(root, ".pi", "agents");
192
+ const projectProfileDir = join(root, ".pi");
193
+ const agentHome = join(root, "agent-home");
194
+ const agentHomeAgentsDir = join(agentHome, "agents");
195
+ const agentHomeSubagentsDir = join(agentHome, "subagents");
196
+ mkdirSync(configHome, { recursive: true });
197
+ mkdirSync(projectConfigDir, { recursive: true });
198
+ mkdirSync(projectAgentsDir, { recursive: true });
199
+ mkdirSync(projectProfileDir, { recursive: true });
200
+ mkdirSync(agentHomeAgentsDir, { recursive: true });
201
+ mkdirSync(agentHomeSubagentsDir, { recursive: true });
202
+ t.after(() => rmSync(root, { recursive: true, force: true }));
203
+
204
+ const previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
205
+ const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
206
+ process.env.GENTLE_PI_CONFIG_HOME = configHome;
207
+ process.env.GENTLE_PI_AGENT_HOME = agentHome;
208
+ t.after(() => {
209
+ if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
210
+ else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
211
+ if (previousAgentHome === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
212
+ else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
213
+ });
214
+
215
+ const agentPath = join(projectAgentsDir, "worker.md");
216
+ writeFileSync(agentPath, "---\nname: worker\ndescription: Worker\n---\nbody\n");
217
+ const profilePath = join(projectProfileDir, "subagents.json");
218
+ const initialProfile = {
219
+ unrelated: { keep: true },
220
+ model_profiles: { worker: { model: "existing/model", effort: "low" } },
221
+ };
222
+ writeFileSync(profilePath, `${JSON.stringify(initialProfile, null, 2)}\n`);
223
+ const context = { cwd: root } as Parameters<typeof applySavedModelConfig>[0];
224
+ const projectPath = join(projectConfigDir, "models.json");
225
+
226
+ const missing = await applySavedModelConfig(context);
227
+ assert.equal(missing.invalidPath, undefined);
228
+ assert.deepEqual(JSON.parse(readFileSync(profilePath, "utf8")), initialProfile);
229
+
230
+ writeFileSync(projectPath, JSON.stringify({ worker: "inherit" }));
231
+ const valid = await applySavedModelConfig(context);
232
+ assert.equal(valid.invalidPath, undefined);
233
+ const validProfile = JSON.parse(readFileSync(profilePath, "utf8")) as Record<string, any>;
234
+ assert.deepEqual(validProfile.unrelated, initialProfile.unrelated);
235
+ assert.deepEqual(validProfile.model_profiles.worker, { model: "inherit" });
236
+ assert.match(readFileSync(agentPath, "utf8"), /model: inherit\n/);
237
+ assert.doesNotMatch(readFileSync(agentPath, "utf8"), /thinking:/);
238
+
239
+ const afterValidBytes = readFileSync(profilePath, "utf8");
240
+ const afterValid = statSync(profilePath);
241
+ writeFileSync(projectPath, JSON.stringify({ worker: null }));
242
+ const nullEntry = await applySavedModelConfig(context);
243
+ assert.equal(nullEntry.invalidPath, undefined);
244
+ assert.equal(readFileSync(profilePath, "utf8"), afterValidBytes);
245
+ assert.equal(statSync(profilePath).mtimeMs, afterValid.mtimeMs);
246
+ assert.doesNotMatch(readFileSync(agentPath, "utf8"), /model: null/);
247
+
248
+ let mutatorCalls = 0;
249
+ const applyConfig = async () => {
250
+ mutatorCalls += 1;
251
+ return { updated: 0, skipped: 0 };
252
+ };
253
+ writeFileSync(projectPath, JSON.stringify({ worker: "new/model" }));
254
+ const injected = await applySavedModelConfig(context, applyConfig);
255
+ assert.equal(injected.invalidPath, undefined);
256
+ assert.equal(mutatorCalls, 1);
257
+ });
@@ -123,11 +123,55 @@ test("2.4.0 repeats 2.2.3 because the START envelope Pi consumes still omits ris
123
123
  assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["2.2.3"] as Record<string, boolean>);
124
124
  });
125
125
 
126
+ test("2.5.0-rc.3 repeats 2.4.0 because the negotiated lane Pi consumes is unchanged", () => {
127
+ // Ground-truthed by driving the exact v2.5.0-rc.3 tagged build through the
128
+ // gentle-ai-bench driven journey corpus (exit 0), which exercises the
129
+ // start/status/capture/validate/mode/delivery lifecycles Pi consumes. The
130
+ // lane now advertises capabilities/v2.3 and the reviewing START is the
131
+ // `start/v4` continuation envelope that #499 already decodes; the closed
132
+ // fields Pi reads did not change. riskEvidence and hint stay dark: still
133
+ // not proven to reach the negotiated START path Pi consumes.
134
+ const contract = NATIVE_CLI_CONTRACTS["2.5.0-rc.3"] as Record<string, boolean>;
135
+ assert.equal(contract.riskEvidence, false);
136
+ assert.equal(contract.hint, false);
137
+ assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["2.4.0"] as Record<string, boolean>);
138
+ });
139
+
140
+ test("2.5.0 repeats 2.5.0-rc.3 because the stable lane Pi consumes is unchanged", () => {
141
+ // Ground-truthed against the published v2.5.0 binary installed from its
142
+ // signed archive: the v2 lane advertises capabilities/v2.4 and answers
143
+ // status/v6, consent/v3, and the `start/v4` continuation, all already
144
+ // decoded. The closed fields Pi reads did not change between rc.3 and
145
+ // stable. riskEvidence and hint stay dark: still not proven to reach the
146
+ // negotiated START path Pi consumes.
147
+ const contract = NATIVE_CLI_CONTRACTS["2.5.0"] as Record<string, boolean>;
148
+ assert.equal(contract.riskEvidence, false);
149
+ assert.equal(contract.hint, false);
150
+ assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["2.5.0-rc.3"] as Record<string, boolean>);
151
+ });
152
+
153
+ test("2.6.0 repeats 2.5.0 because the negotiated lane Pi consumes is unchanged", () => {
154
+ // Ground-truthed against the published v2.6.0 binary installed from its
155
+ // signed archive: the v2 lane advertises capabilities/v2.5 and answers
156
+ // status/v7, consent/v3, and the `start/v4` continuation, all already
157
+ // decoded. `review mode status --json` and `review validate --gate
158
+ // pre-commit` were re-run against the binary and still answer
159
+ // `gentle-ai.rdd-mode-status/v1` and `gentle-ai.review-gate-result/v1`
160
+ // respectively. Declining the consent/v3 prompt during `review start`
161
+ // still surfaces `risk_evidence` only on that blocking envelope, not on
162
+ // the negotiated `start/v4` continuation, so riskEvidence and hint stay
163
+ // dark: still not proven to reach the negotiated START path Pi consumes.
164
+ const contract = NATIVE_CLI_CONTRACTS["2.6.0"] as Record<string, boolean>;
165
+ assert.equal(contract.riskEvidence, false);
166
+ assert.equal(contract.hint, false);
167
+ assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["2.5.0"] as Record<string, boolean>);
168
+ });
169
+
126
170
  test("no shipped version key was added beyond the pin bump", () => {
127
171
  // Rows are promises to consumers, so a new key only ever appears in a
128
172
  // dedicated commit alongside a pin bump, never as a side effect. v2.2.4 and
129
173
  // v2.3.0 shipped upstream while Pi stayed on 2.2.3 and were never pinned,
130
174
  // so they get no row: a row asserts ground truth measured against a binary
131
175
  // Pi actually ran, and the table only has to be ascending, not gapless.
132
- assert.deepEqual(Object.keys(NATIVE_CLI_CONTRACTS), [...DARK_VERSIONS, "2.2.0", "2.2.1", "2.2.2", "2.2.3", "2.4.0"]);
176
+ assert.deepEqual(Object.keys(NATIVE_CLI_CONTRACTS), [...DARK_VERSIONS, "2.2.0", "2.2.1", "2.2.2", "2.2.3", "2.4.0", "2.5.0-rc.3", "2.5.0", "2.6.0"]);
133
177
  });