gentle-pi 2.1.2 → 2.2.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 (171) hide show
  1. package/README.md +70 -12
  2. package/assets/agents/gentle-ai-worker.md +7 -3
  3. package/assets/agents/jd-fix-agent.md +1 -1
  4. package/assets/agents/jd-judge-a.md +3 -1
  5. package/assets/agents/jd-judge-b.md +3 -1
  6. package/assets/agents/review-readability.md +4 -1
  7. package/assets/agents/review-reliability.md +4 -1
  8. package/assets/agents/review-resilience.md +4 -1
  9. package/assets/agents/review-risk.md +4 -1
  10. package/assets/agents/sdd-apply.md +6 -1
  11. package/assets/agents/sdd-archive.md +6 -1
  12. package/assets/agents/sdd-design.md +6 -1
  13. package/assets/agents/sdd-explore.md +6 -2
  14. package/assets/agents/sdd-init.md +10 -2
  15. package/assets/agents/sdd-onboard.md +6 -1
  16. package/assets/agents/sdd-proposal.md +6 -1
  17. package/assets/agents/sdd-spec.md +6 -1
  18. package/assets/agents/sdd-status.md +6 -1
  19. package/assets/agents/sdd-sync.md +6 -1
  20. package/assets/agents/sdd-tasks.md +6 -1
  21. package/assets/agents/sdd-verify.md +6 -1
  22. package/assets/chains/4r-review.chain.md +2 -0
  23. package/assets/chains/sdd-full.chain.md +1 -1
  24. package/assets/chains/sdd-plan.chain.md +1 -1
  25. package/assets/chains/sdd-verify.chain.md +1 -1
  26. package/assets/orchestrator-delegation.md +246 -67
  27. package/assets/orchestrator.md +7 -14
  28. package/assets/sdd-orchestrator-workflow.md +154 -9
  29. package/assets/support/sdd-status-contract.md +19 -1
  30. package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
  31. package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
  32. package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
  33. package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
  34. package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
  35. package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
  36. package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
  37. package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
  38. package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
  39. package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
  40. package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
  41. package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
  42. package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
  43. package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
  44. package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
  45. package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
  46. package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
  47. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
  48. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
  49. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
  50. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
  51. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
  52. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
  53. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
  54. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
  55. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
  56. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
  57. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
  58. package/docs/native-authority-architecture.md +5 -5
  59. package/docs/review-integration.md +22 -2
  60. package/extensions/gentle-ai.ts +1595 -201
  61. package/extensions/sdd-init.ts +19 -6
  62. package/extensions/skill-registry.ts +10 -2
  63. package/extensions/startup-banner.ts +10 -4
  64. package/lib/gentle-ai-binary.ts +173 -2
  65. package/lib/git-commit-transaction.ts +77 -17
  66. package/lib/native-review-cli.ts +528 -65
  67. package/lib/provider-contract-bundle.ts +704 -0
  68. package/lib/review-candidate-view.ts +527 -18
  69. package/lib/review-compact-contract.ts +59 -248
  70. package/lib/review-host-relay.ts +436 -0
  71. package/lib/review-integration-v2.ts +537 -36
  72. package/lib/review-relay-contract.ts +16 -0
  73. package/lib/sdd-preflight.ts +53 -1
  74. package/package.json +5 -2
  75. package/runtime/gentle-ai-binary.mjs +173 -2
  76. package/runtime/git-commit-transaction.mjs +75 -15
  77. package/runtime/native-review-cli.mjs +524 -61
  78. package/runtime/review-integration-v2.mjs +536 -35
  79. package/runtime/review-relay-contract.mjs +17 -0
  80. package/scripts/build-git-commit-transaction-runner.mjs +1 -0
  81. package/scripts/check-provider-contract.mjs +138 -0
  82. package/scripts/gentle-ai-installer.mjs +23 -13
  83. package/scripts/maintainer/provider-relay-matrix.mjs +219 -0
  84. package/scripts/mirror-provider-contract.mjs +143 -0
  85. package/scripts/test-packed-runner.mjs +16 -2
  86. package/scripts/verify-package-files.mjs +110 -33
  87. package/skills/_shared/review-ledger-contract.md +4 -6
  88. package/skills/gentle-ai/SKILL.md +4 -4
  89. package/skills/issue-creation/SKILL.md +94 -168
  90. package/skills/judgment-day/SKILL.md +7 -1
  91. package/skills/judgment-day/references/prompts-and-formats.md +2 -0
  92. package/skills/rdd-defect-workflow/SKILL.md +54 -0
  93. package/tests/background-subagents.test.ts +771 -0
  94. package/tests/crosslane/cross-lane.mjs +1169 -0
  95. package/tests/delegated-key-learnings-contract.test.ts +238 -0
  96. package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
  97. package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
  98. package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
  99. package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
  100. package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
  101. package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
  102. package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
  103. package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
  104. package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
  105. package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
  106. package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
  107. package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
  108. package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
  109. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
  110. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
  111. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
  112. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
  113. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
  114. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
  115. package/tests/gentle-ai-binary.test.ts +1 -1
  116. package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
  117. package/tests/gentle-ai-dev-binary.test.ts +336 -0
  118. package/tests/gentle-ai-installer.test.ts +46 -46
  119. package/tests/git-commit-transaction.test.ts +229 -1
  120. package/tests/maintainer/provider-relay.maintest.ts +265 -0
  121. package/tests/native-review-capability-contract.test.ts +48 -2
  122. package/tests/native-review-cli.test.ts +56 -0
  123. package/tests/native-review-consent.test.ts +164 -3
  124. package/tests/native-review-parity-runtime.test.ts +37 -0
  125. package/tests/native-review-parity.test.ts +218 -15
  126. package/tests/native-sdd-attempt-authority.test.ts +235 -0
  127. package/tests/orchestrator-budget.test.ts +30 -5
  128. package/tests/package-manifest.test.ts +98 -72
  129. package/tests/provider-contract-bundle.test.ts +385 -0
  130. package/tests/provider-contract-mirror.test.ts +206 -0
  131. package/tests/provider-defect-handoff.test.ts +355 -0
  132. package/tests/review-actor-tool-deny.test.ts +12 -13
  133. package/tests/review-candidate-view.test.ts +489 -9
  134. package/tests/review-compact-contract.test.ts +52 -119
  135. package/tests/review-controller-native-recovery.test.ts +643 -47
  136. package/tests/review-controller-native-routing.test.ts +1667 -222
  137. package/tests/review-controller-workspace-root.test.ts +17 -2
  138. package/tests/review-corrected-finalize-binding.test.ts +175 -0
  139. package/tests/review-dispatch-hydration-gap.test.ts +197 -0
  140. package/tests/review-host-relay-routing.test.ts +317 -0
  141. package/tests/review-host-relay.test.ts +520 -0
  142. package/tests/review-integration-v2-forward.test.ts +631 -0
  143. package/tests/review-integration-v2.test.ts +114 -0
  144. package/tests/review-ledger-contract.test.ts +12 -28
  145. package/tests/review-recovered-lineage-routing.test.ts +246 -0
  146. package/tests/review-relay-transport-agent.test.ts +249 -0
  147. package/tests/runtime-harness.mjs +242 -14
  148. package/tests/sdd-agent-tools.test.ts +18 -33
  149. package/tests/skill-collision-prefixes.test.ts +1 -0
  150. package/tests/skill-registry.test.ts +50 -1
  151. package/tests/verify-package-files.test.ts +62 -0
  152. package/assets/agents/review-refuter.md +0 -40
  153. package/assets/agents/review-validator.md +0 -23
  154. package/lib/native-review-remediation.ts +0 -49
  155. package/lib/review-compact.ts +0 -947
  156. package/lib/review-refuter-adapter.ts +0 -129
  157. package/lib/review-runtime-contract.ts +0 -68
  158. package/prompts/gcl.md +0 -54
  159. package/prompts/gis.md +0 -25
  160. package/prompts/gpr.md +0 -41
  161. package/prompts/gwr.md +0 -31
  162. package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
  163. package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
  164. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
  165. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
  166. package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
  167. package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
  168. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
  169. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
  170. package/tests/review-compact.test.ts +0 -243
  171. package/tests/review-refuter-adapter.test.ts +0 -89
@@ -0,0 +1,249 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import test from "node:test";
7
+ import { __testing } from "../extensions/gentle-ai.ts";
8
+ import { NativeReviewIntegrationError, type NativeReviewCli } from "../lib/native-review-cli.ts";
9
+ import { CandidateViewRegistry } from "../lib/review-candidate-view.ts";
10
+ import type { ReviewCollectInputV3, ReviewStatusV3 } from "../lib/review-integration-v2.ts";
11
+ import type { ReviewHostRelayRequest } from "../lib/review-host-relay.ts";
12
+
13
+ // Third field failure on the recovered-lineage defect (2026-08-16, gentle-pi
14
+ // main 402f9f77 + gentle-ai 2.4.0-main.20278905): STATUS recognises the
15
+ // lineage, the Pi RELAY never runs, no lens is launched, zero mutations.
16
+ //
17
+ // Measured against the live binary on a faithful reproduction (linked
18
+ // worktree, uncommitted tracked files, externally recovered successor):
19
+ //
20
+ // agent-less STATUS -> collect input arguments:
21
+ // lineage, expected-revision, target, repository-context, lens, order,
22
+ // subject-hash (no agent, no materialize, no submission)
23
+ // `--agent pi` STATUS -> the same input PLUS agent=pi, materialize=true and
24
+ // the provider submission (operation_token=capture-result)
25
+ //
26
+ // The adapter's negotiated targetStatus never sent `--agent pi`, so the
27
+ // provider never offered the materialize-marked relay slot,
28
+ // reviewHostRelaySlots() returned 0 for every real lineage, and the host relay
29
+ // was unreachable. The prior fixes all exercised dispatch-shaped slots.
30
+ //
31
+ // Binaries older than v2.4.0 do not define `--agent` on `review status` and
32
+ // refuse it outright, so the agent is probed rather than version-sniffed and
33
+ // the typed refusal must reach the user instead of being swallowed.
34
+
35
+ const SHA = `sha256:${"1".repeat(64)}`;
36
+ const TRANSPORT_REFUSAL_CODE = "immutable_review_transport_unsupported";
37
+
38
+ function repository(t: test.TestContext): string {
39
+ const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-relay-transport-"));
40
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
41
+ execFileSync("git", ["init", "-b", "main"], { cwd });
42
+ writeFileSync(join(cwd, "app.ts"), "export const value = 1;\n");
43
+ execFileSync("git", ["add", "."], { cwd });
44
+ execFileSync("git", ["-c", "user.name=Relay", "-c", "user.email=relay@example.invalid", "commit", "-m", "base"], { cwd });
45
+ return cwd;
46
+ }
47
+
48
+ function collectInput(lineageId: string, materialize: boolean): ReviewCollectInputV3 {
49
+ const binding = [
50
+ { name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
51
+ { name: "expected-revision", value: SHA, token: `--expected-revision=${SHA}` },
52
+ { name: "target", value: SHA, token: `--target=${SHA}` },
53
+ { name: "repository-context", value: `rctx1_${"e".repeat(64)}`, token: `--repository-context=rctx1_${"e".repeat(64)}` },
54
+ { name: "lens", value: "review-reliability", token: "--lens=review-reliability" },
55
+ { name: "order", value: "0", token: "--order=0" },
56
+ { name: "subject-hash", value: SHA, token: `--subject-hash=${SHA}` },
57
+ ];
58
+ return {
59
+ name: "reviewer_result",
60
+ schema: "https://gentle-ai.dev/schema/review/reviewer/v1",
61
+ captureOperation: "review.capture-result",
62
+ arguments: materialize
63
+ ? [...binding, { name: "agent", value: "pi", token: "--agent=pi" }, { name: "materialize", value: "true", token: "--materialize=true" }]
64
+ : binding,
65
+ artifactSubject: {
66
+ schema: "gentle-ai.review-artifact-subject/v2",
67
+ subjectHash: SHA,
68
+ lineageId,
69
+ authorityRevision: SHA,
70
+ targetIdentity: SHA,
71
+ baseTree: "3".repeat(40),
72
+ candidateTree: "4".repeat(40),
73
+ changedPathManifestSha256: SHA,
74
+ lens: "review-reliability",
75
+ selectedOrder: 0,
76
+ },
77
+ ...(materialize
78
+ ? {
79
+ submission: {
80
+ operationToken: "capture-result",
81
+ argumentTokens: [...binding.map((argument) => argument.token), "--input={{value}}"],
82
+ values: [{ slot: "reviewer_result", domain: "artifact_path_or_stdin", substitutionLocation: binding.length }],
83
+ },
84
+ }
85
+ : {}),
86
+ } as unknown as ReviewCollectInputV3;
87
+ }
88
+
89
+ function recoveredStatus(lineageId: string, materialize: boolean): ReviewStatusV3 {
90
+ return {
91
+ contract: "gentle-ai.review-integration/v2",
92
+ applicability: "current_target",
93
+ authority: { version: "compact-v2", lineageId, state: "reviewer_results_required", generation: 2, revision: SHA },
94
+ receipt: { status: "expected_missing" },
95
+ action: "finalize",
96
+ replayability: "not_replayable",
97
+ targetIdentity: SHA,
98
+ projection: {
99
+ schema: "gentle-ai.review-integration.projection/v1",
100
+ kind: "current-changes",
101
+ projection: "workspace",
102
+ baseTree: "3".repeat(40),
103
+ initialReviewTree: "4".repeat(40),
104
+ currentCandidateTree: "4".repeat(40),
105
+ pathsDigest: SHA,
106
+ paths: ["app.ts"],
107
+ intendedUntracked: [],
108
+ intendedUntrackedProof: SHA,
109
+ initialSnapshotIdentity: SHA,
110
+ currentSnapshotIdentity: SHA,
111
+ },
112
+ candidates: [],
113
+ nextTransition: { kind: "collect", reasonCode: "reviewer_results_required", collect: { inputs: [collectInput(lineageId, materialize)] } },
114
+ raw: { schema: "gentle-ai.review-integration.status/v5", action: "finalize", lineage_id: lineageId },
115
+ } as unknown as ReviewStatusV3;
116
+ }
117
+
118
+ /**
119
+ * Mirrors the MEASURED live provider: the materialize-marked relay slot is
120
+ * offered only when the caller asks for the pi agent.
121
+ */
122
+ function transportAwareNative(options: { refusePiAgent?: boolean } = {}): { native: NativeReviewCli; agents: Array<string | undefined> } {
123
+ const agents: Array<string | undefined> = [];
124
+ const native = {
125
+ targetStatus: async (request: { lineageId?: string; agent?: string }) => {
126
+ agents.push(request.agent);
127
+ if (request.agent === "pi" && options.refusePiAgent === true) {
128
+ throw new NativeReviewIntegrationError({
129
+ schema: "gentle-ai.review-integration.failure/v2",
130
+ contract: "gentle-ai.review-integration/v2",
131
+ operation: "review.status",
132
+ phase: "pre_native",
133
+ code: TRANSPORT_REFUSAL_CODE,
134
+ message: "supported immutable review runtimes: claude-code, opencode, codex",
135
+ mutationOutcome: "none",
136
+ authorityApplicability: "current_target",
137
+ retrySafe: true,
138
+ replayability: "not_replayable",
139
+ nextAction: "stop",
140
+ raw: {},
141
+ } as never);
142
+ }
143
+ return recoveredStatus(request.lineageId ?? "relay-lineage", request.agent === "pi");
144
+ },
145
+ finalize: async () => { throw new Error("native finalize must not run while reviewer results are outstanding"); },
146
+ } as unknown as NativeReviewCli;
147
+ return { native, agents };
148
+ }
149
+
150
+ async function runFinalize(cwd: string, native: NativeReviewCli, lineageId: string, input: Record<string, unknown> = { reviewer_run_acknowledged: true }): Promise<Record<string, unknown>> {
151
+ return await __testing.executeReviewControllerOperation(
152
+ { operation: "finalize", lineageId, input: JSON.stringify(input) },
153
+ cwd, new Map(), native, undefined, undefined, undefined, new CandidateViewRegistry(),
154
+ ) as Record<string, unknown>;
155
+ }
156
+
157
+ test("the negotiated status asks for the pi agent so the provider offers its materialize relay slot", async (t) => {
158
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
159
+ const cwd = repository(t);
160
+ const lineageId = "relay-recovered-successor";
161
+ const { native, agents } = transportAwareNative();
162
+ const relayed: ReviewHostRelayRequest[] = [];
163
+ __testing.setReviewHostRelayRunnerForTesting(async (request: ReviewHostRelayRequest) => {
164
+ relayed.push(request);
165
+ return { promptByteLength: 128, resultByteLength: 64, submission: '{"admission_decision":"completed"}' };
166
+ });
167
+
168
+ const result = await runFinalize(cwd, native, lineageId);
169
+
170
+ assert.ok(agents.includes("pi"), "the controller must negotiate STATUS for the pi reviewer transport");
171
+ assert.equal(relayed.length, 1, "the materialize-marked slot must reach the host relay");
172
+ assert.ok(relayed[0]!.captureArgumentTokens.includes("--materialize=true"));
173
+ assert.ok(relayed[0]!.submission !== undefined, "the provider submission drives the completing form");
174
+ const hostRelay = result.host_relay as { transport: string; captured_slots: readonly unknown[] } | undefined;
175
+ assert.ok(hostRelay !== undefined, "the envelope reports the relay capture");
176
+ assert.equal(hostRelay.transport, "pi_host_relay");
177
+ assert.equal(hostRelay.captured_slots.length, 1);
178
+ });
179
+
180
+ test("finalize forecasts the reviewer model run once and spends nothing until it is acknowledged", async (t) => {
181
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
182
+ const cwd = repository(t);
183
+ const lineageId = "forecast-lineage";
184
+ const { native } = transportAwareNative();
185
+ let launches = 0;
186
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
187
+ launches += 1;
188
+ return { promptByteLength: 128, resultByteLength: 64, submission: '{"admission_decision":"completed"}' };
189
+ });
190
+
191
+ // An unacknowledged finalize must forecast, never spend.
192
+ const forecast = await runFinalize(cwd, native, lineageId, {});
193
+ assert.equal(launches, 0, "no reviewer model run may start before the forecast is acknowledged");
194
+ assert.equal(forecast.status, "blocked");
195
+ assert.equal(forecast.outcome, "reviewer-model-run-forecast");
196
+ assert.equal(forecast.mutation_performed, false);
197
+ assert.equal(forecast.mutation_outcome, "none");
198
+ const cost = forecast.cost_forecast as { transport: string; model_runs: number; lenses: readonly string[]; side_effects: readonly string[] };
199
+ assert.equal(cost.transport, "pi_host_relay");
200
+ assert.equal(cost.model_runs, 1, "one model run per outstanding lens, forecast once before launch");
201
+ assert.deepEqual(cost.lenses, ["review-reliability"]);
202
+ assert.ok(cost.side_effects.length > 0);
203
+ assert.match(String(forecast.reason), /model run/i);
204
+ assert.match(String(forecast.next_action), /reviewer_run_acknowledged/);
205
+
206
+ // The acknowledged finalize runs exactly the forecast work.
207
+ const acknowledged = await runFinalize(cwd, native, lineageId, { reviewer_run_acknowledged: true });
208
+ assert.equal(launches, 1, "the acknowledged finalize runs the forecast model run");
209
+ assert.equal((acknowledged.host_relay as { captured_slots: readonly unknown[] }).captured_slots.length, 1);
210
+ });
211
+
212
+ test("a provider that refuses the pi transport surfaces the typed cause and still returns status", async (t) => {
213
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
214
+ const cwd = repository(t);
215
+ const lineageId = "legacy-transport-lineage";
216
+ const { native, agents } = transportAwareNative({ refusePiAgent: true });
217
+ let relayCalls = 0;
218
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
219
+ relayCalls += 1;
220
+ return { promptByteLength: 1, resultByteLength: 1, submission: "{}" };
221
+ });
222
+
223
+ const result = await runFinalize(cwd, native, lineageId);
224
+
225
+ assert.ok(agents.includes("pi"), "the transport is probed once");
226
+ assert.ok(agents.includes(undefined), "a refusal falls back to the agent-less status instead of failing the operation");
227
+ assert.equal(relayCalls, 0, "no relay slot exists on a provider without the pi transport");
228
+ // Deliverable: the user sees the real typed cause, never a generic
229
+ // candidate-view message.
230
+ const transport = result.relay_transport as { supported: boolean; code: string; message: string } | undefined;
231
+ assert.ok(transport !== undefined, "the envelope must report the transport refusal");
232
+ assert.equal(transport.supported, false);
233
+ assert.equal(transport.code, TRANSPORT_REFUSAL_CODE);
234
+ assert.match(transport.message, /immutable review runtimes/i);
235
+ assert.doesNotMatch(JSON.stringify(result), /no current controller-owned candidate view/);
236
+ // The lifecycle still routes: reviewer results remain outstanding.
237
+ assert.equal(result.outcome, "reviewer-results-required");
238
+ });
239
+
240
+ test("the pi transport is probed once per provider and the refusal is remembered", async (t) => {
241
+ const cwd = repository(t);
242
+ const { native, agents } = transportAwareNative({ refusePiAgent: true });
243
+ await runFinalize(cwd, native, "probe-lineage");
244
+ const afterFirst = agents.filter((agent) => agent === "pi").length;
245
+ await runFinalize(cwd, native, "probe-lineage");
246
+ const afterSecond = agents.filter((agent) => agent === "pi").length;
247
+ assert.equal(afterFirst, 1, "the first flow probes the pi transport exactly once");
248
+ assert.equal(afterSecond, 1, "a remembered refusal is never re-probed for the same provider");
249
+ });
@@ -11,6 +11,7 @@ import { matchesKey } from "@earendil-works/pi-tui";
11
11
  import { fileURLToPath, pathToFileURL } from "node:url";
12
12
  import { stripAnsi } from "../lib/terminal-theme.ts";
13
13
  import { domainHashV1 } from "../lib/review-canonical.ts";
14
+ import { resolveGentleAiBinary } from "../lib/gentle-ai-binary.ts";
14
15
  import { NativeReviewCliV216, NATIVE_REVIEW_ERROR_CODE, NativeReviewCliError } from "../lib/native-review-cli.ts";
15
16
 
16
17
  const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
@@ -196,6 +197,23 @@ async function run() {
196
197
  process.env.GENTLE_PI_CONFIG_HOME = globalConfigHome;
197
198
  process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
198
199
  process.env.GENTLE_PI_TEST_ASSETS_DIR = ambientTestAssetsDir;
200
+ // The harness already sandboxes its config and agent homes; the review kill
201
+ // switch is the one piece of machine state it was still inheriting. Several
202
+ // lifecycle-gate assertions below only reach their subject while
203
+ // receipt-driven development is on — with it off, gateLifecycleCommand
204
+ // returns undefined before deriving any target, which is correct behavior
205
+ // but not what those assertions are about.
206
+ //
207
+ // gentle-ai v2.4.0 made the switch opt-in, so an unconfigured machine now
208
+ // resolves to off. Until then the harness passed or failed on whatever the
209
+ // operator happened to have set, which made it green locally and red on a
210
+ // fresh CI runner from identical bytes. It now owns a sandbox HOME and opts
211
+ // in explicitly, the same way a user does, so the gates are what is under
212
+ // test rather than the machine.
213
+ const reviewHome = await tempWorkspace();
214
+ process.env.HOME = reviewHome;
215
+ const reviewModeEnable = execFileSync(resolveGentleAiBinary(ROOT, process.platform), ["review", "mode", "enable", "--scope", "global", "--json"], { cwd: ROOT, encoding: "utf8", env: { ...process.env, HOME: reviewHome } });
216
+ assert.match(reviewModeEnable, /"effective": "on"/, "the harness sandbox HOME must have receipt-driven development explicitly enabled");
199
217
  const globalModelsPath = join(globalConfigHome, "models.json");
200
218
  const globalSubagentsPath = join(globalAgentHome, "subagents.json");
201
219
  const { pi, hooks, commands, flags, tools } = createPi();
@@ -217,6 +235,7 @@ async function run() {
217
235
  assert.ok(tools.has(toolName), `missing quiet built-in tool renderer ${toolName}`);
218
236
  }
219
237
  assert.ok(tools.has("gentle_review"), "missing registered bounded review controller tool");
238
+ assert.ok(tools.has("gentle_review_scope"), "missing registered bounded review scope tool");
220
239
  assert.deepEqual(
221
240
  tools.get("gentle_review").parameters.properties.operation.enum.filter((operation) => operation.includes("supersession") || operation === "supersede" || operation === "reconcile-authority"),
222
241
  ["reconcile-authority"],
@@ -536,7 +555,7 @@ async function run() {
536
555
  const executableDigest = "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705";
537
556
  const capabilitiesBody = {
538
557
  ...capabilitiesFixture,
539
- package: { ...capabilitiesFixture.package, version: "2.2.2" },
558
+ package: { ...capabilitiesFixture.package, version: "2.4.0" },
540
559
  };
541
560
  const statusBody = {
542
561
  schema: "gentle-ai.review-integration.status/v3",
@@ -624,6 +643,76 @@ async function run() {
624
643
  await rm(join(globalConfigHome, "banner.json"), { force: true });
625
644
  }
626
645
 
646
+ // issue-301: cancelling the color picker must be a no-op — no write,
647
+ // no notify, and the previously saved color must survive byte/semantically
648
+ // unchanged. Covers both entry points: /gentle:banner-color picker
649
+ // (cancelled), and /gentle:banner -> Color row -> nested picker (cancelled).
650
+ // Also covers an invalid non-empty argument, which must still open the
651
+ // picker and treat its cancellation as a no-op.
652
+ const cancelPickerCwd = await tempWorkspace();
653
+ try {
654
+ const bannerConfigPath = join(globalConfigHome, "banner.json");
655
+ const seeded = {
656
+ showRose: false,
657
+ showTextLogo: false,
658
+ color: "green",
659
+ };
660
+ const seededJson = `${JSON.stringify(seeded, null, 2)}\n`;
661
+ await writeFile(bannerConfigPath, seededJson, "utf8");
662
+
663
+ const cancelCtx = createCtx(cancelPickerCwd, true);
664
+
665
+ // (a) /gentle:banner-color picker cancelled: seeded color unchanged, no notify.
666
+ cancelCtx.ui.notifications.length = 0;
667
+ cancelCtx.ui.selections.length = 0;
668
+ cancelCtx.ui.select = async (label, options) => {
669
+ cancelCtx.ui.selections.push({ label, options });
670
+ return undefined;
671
+ };
672
+ await commands.get("gentle:banner-color").handler("", cancelCtx);
673
+ let afterCancel = await readFile(bannerConfigPath, "utf8");
674
+ assert.equal(afterCancel, seededJson, "banner-color cancel must not rewrite banner.json");
675
+ assert.equal(cancelCtx.ui.selections.length, 1, "banner-color cancel must open the picker once");
676
+ assert.equal(cancelCtx.ui.notifications.length, 0, "banner-color cancel must not notify");
677
+
678
+ // (d) invalid non-empty /gentle:banner-color input still opens picker;
679
+ // cancelling it is a no-op.
680
+ cancelCtx.ui.notifications.length = 0;
681
+ cancelCtx.ui.selections.length = 0;
682
+ await commands.get("gentle:banner-color").handler("purple", cancelCtx);
683
+ afterCancel = await readFile(bannerConfigPath, "utf8");
684
+ assert.equal(afterCancel, seededJson, "banner-color invalid+cancel must not rewrite banner.json");
685
+ assert.equal(cancelCtx.ui.selections.length, 1, "invalid banner-color arg must still open the picker");
686
+ assert.equal(cancelCtx.ui.notifications.length, 0, "banner-color invalid+cancel must not notify");
687
+
688
+ // (b) /gentle:banner selects the Color row, then the nested picker is
689
+ // cancelled: seeded color unchanged, no notify. The outer select
690
+ // returns the Color row; the nested select returns undefined.
691
+ cancelCtx.ui.notifications.length = 0;
692
+ cancelCtx.ui.selections.length = 0;
693
+ let selectCall = 0;
694
+ cancelCtx.ui.select = async (label, options) => {
695
+ cancelCtx.ui.selections.push({ label, options });
696
+ selectCall += 1;
697
+ // First call: outer "Startup banner" menu -> pick the Color row.
698
+ // Second call: nested color picker -> cancel (undefined).
699
+ return selectCall === 1 ? options[options.length - 1] : undefined;
700
+ };
701
+ await commands.get("gentle:banner").handler("", cancelCtx);
702
+ afterCancel = await readFile(bannerConfigPath, "utf8");
703
+ assert.equal(afterCancel, seededJson, "banner Color-row cancel must not rewrite banner.json");
704
+ assert.equal(cancelCtx.ui.selections.length, 2, "banner Color-row flow must open outer then nested picker");
705
+ assert.equal(cancelCtx.ui.notifications.length, 0, "banner Color-row cancel must not notify");
706
+
707
+ // Sanity: the seeded config round-trips through normalization unchanged,
708
+ // proving the byte equality above is semantic, not a test artifact.
709
+ const reparsed = JSON.parse(await readFile(bannerConfigPath, "utf8"));
710
+ assert.deepEqual(reparsed, seeded, "seeded non-default color must round-trip semantically");
711
+ } finally {
712
+ await rm(cancelPickerCwd, { recursive: true, force: true });
713
+ await rm(join(globalConfigHome, "banner.json"), { force: true });
714
+ }
715
+
627
716
  const noUiCwd = await tempWorkspace();
628
717
  try {
629
718
  for (const handler of hooks.get("session_start")) {
@@ -640,22 +729,22 @@ async function run() {
640
729
  "session_start must not install project-local SDD chains",
641
730
  );
642
731
  assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
732
+ // gentle-pi#311 P5: the Pi-authored adversarial role agents are retired;
733
+ // installation must not (re)create them.
643
734
  const installedRefuterPath = join(globalAgentHome, "agents", "review-refuter.md");
644
- assert.equal(existsSync(installedRefuterPath), true);
645
- assert.deepEqual(
646
- readAgentDefinition(await readFile(installedRefuterPath, "utf8")),
647
- { name: "review-refuter", tools: ["read", "grep", "find"] },
648
- "isolated package installation must activate only the refuter inspection tools",
735
+ assert.equal(existsSync(installedRefuterPath), false, "the retired review-refuter agent must not be installed");
736
+ assert.equal(
737
+ existsSync(join(globalAgentHome, "agents", "review-validator.md")),
738
+ false,
739
+ "the retired review-validator agent must not be installed",
649
740
  );
650
- const installedRefuterSource = await readFile(installedRefuterPath, "utf8");
651
- assert.match(
652
- installedRefuterSource,
653
- /^tools:\n {2}- "\*": false$/m,
654
- "installed refuter must lead its tool map with the deny-all rule",
741
+ const installedExplorePath = join(globalAgentHome, "agents", "gentle-ai-explore.md");
742
+ assert.equal(existsSync(installedExplorePath), true);
743
+ assert.deepEqual(
744
+ readAgentDefinition(await readFile(installedExplorePath, "utf8")),
745
+ { name: "gentle-ai-explore", tools: ["read", "grep", "find", "codegraph"] },
746
+ "isolated package installation must activate only the explorer inspection tools",
655
747
  );
656
- assert.match(installedRefuterSource, /complete inferential-severe frozen-row list once/);
657
- assert.match(installedRefuterSource, /refuted \| corroborated \| inconclusive/);
658
- assert.doesNotMatch(installedRefuterSource, /general, correctness, impact\/exploitability, or reproducibility/);
659
748
  const installedRiskSource = await readFile(
660
749
  join(globalAgentHome, "agents", "review-risk.md"),
661
750
  "utf8",
@@ -685,6 +774,13 @@ async function run() {
685
774
  ].join("\n");
686
775
  await writeFile(installedRefuterPath, samePathUserRefuter);
687
776
  delete managedAssetsManifest.assets["agents/review-refuter.md"];
777
+ // Retirement sweep: a hash-proven package-managed copy of a retired
778
+ // asset is deleted on refresh; the user-authored same-path refuter
779
+ // above must survive because its hash proves nothing.
780
+ const retiredManagedValidatorPath = join(globalAgentHome, "agents", "review-validator.md");
781
+ const retiredManagedValidator = "stale managed validator\n";
782
+ await writeFile(retiredManagedValidatorPath, retiredManagedValidator);
783
+ managedAssetsManifest.assets["agents/review-validator.md"] = sha256(retiredManagedValidator);
688
784
  await mkdir(join(globalAgentHome, "subagents"), { recursive: true });
689
785
  const userRefuterOverride = join(globalAgentHome, "subagents", "review-refuter.md");
690
786
  await writeFile(userRefuterOverride, "user refuter override must stay\n");
@@ -738,6 +834,24 @@ async function run() {
738
834
  "user refuter override must stay\n",
739
835
  "package refresh must not rewrite or certify an explicit user refuter",
740
836
  );
837
+ assert.equal(
838
+ existsSync(retiredManagedValidatorPath),
839
+ false,
840
+ "session refresh must delete a hash-proven package-managed copy of a retired asset",
841
+ );
842
+ const refreshedManagedAssets = JSON.parse(
843
+ await readFile(managedAssetsManifestPath, "utf8"),
844
+ );
845
+ assert.equal(
846
+ refreshedManagedAssets.assets["agents/review-validator.md"],
847
+ undefined,
848
+ "a retired asset must lose package-managed ownership",
849
+ );
850
+ assert.equal(
851
+ refreshedManagedAssets.assets["agents/review-refuter.md"],
852
+ undefined,
853
+ "a user-authored same-path retired asset must stay unowned",
854
+ );
741
855
  } finally {
742
856
  await rm(noUiCwd, { recursive: true, force: true });
743
857
  }
@@ -1007,6 +1121,82 @@ async function run() {
1007
1121
  await rm(engramSddCwd, { recursive: true, force: true });
1008
1122
  }
1009
1123
 
1124
+ // Issue #64: selecting engram as the artifact store must not cause
1125
+ // /sdd-init to create openspec/ or openspec/config.yaml.
1126
+ const engramSddInitCwd = await tempWorkspace();
1127
+ try {
1128
+ pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
1129
+ const ctx = createCtx(engramSddInitCwd, true, "engram-sdd-init-session");
1130
+ ctx.ui.select = async (label, options) => {
1131
+ if (label === "SDD artifact store") return "engram";
1132
+ return options[0];
1133
+ };
1134
+ await commands.get("gentle:sdd-preflight").handler("", ctx);
1135
+ await commands.get("sdd-init").handler("", ctx);
1136
+ assert.equal(
1137
+ existsSync(join(engramSddInitCwd, "openspec")),
1138
+ false,
1139
+ "/sdd-init must not create openspec/ when artifactStore is engram",
1140
+ );
1141
+ assert.equal(
1142
+ existsSync(join(engramSddInitCwd, "openspec", "config.yaml")),
1143
+ false,
1144
+ "/sdd-init must not write openspec/config.yaml when artifactStore is engram",
1145
+ );
1146
+ assert.doesNotMatch(
1147
+ ctx.ui.notifications.at(-1).message,
1148
+ /Wrote openspec\/config\.yaml/,
1149
+ "/sdd-init must not announce openspec/config.yaml when artifactStore is engram",
1150
+ );
1151
+ assert.match(
1152
+ ctx.ui.notifications.at(-1).message,
1153
+ /SDD initialized for engram:/,
1154
+ );
1155
+ assert.equal(ctx.ui.notifications.at(-1).level, "info");
1156
+ } finally {
1157
+ pi.setActiveTools(["read", "bash", "edit", "write"]);
1158
+ await rm(engramSddInitCwd, { recursive: true, force: true });
1159
+ }
1160
+
1161
+ // Issue #64 counterpart: the engram skip must stay narrow. Selecting both
1162
+ // still has to create the full openspec/ scaffold and write config.yaml,
1163
+ // so an over-broad skip is caught here instead of in the field.
1164
+ const bothSddInitCwd = await tempWorkspace();
1165
+ try {
1166
+ pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
1167
+ const ctx = createCtx(bothSddInitCwd, true, "both-sdd-init-session");
1168
+ ctx.ui.select = async (label, options) => {
1169
+ if (label === "SDD artifact store") return "both";
1170
+ return options[0];
1171
+ };
1172
+ await commands.get("gentle:sdd-preflight").handler("", ctx);
1173
+ await commands.get("sdd-init").handler("", ctx);
1174
+ assert.equal(
1175
+ existsSync(join(bothSddInitCwd, "openspec", "specs")),
1176
+ true,
1177
+ "/sdd-init must create openspec/specs when artifactStore is both",
1178
+ );
1179
+ assert.equal(
1180
+ existsSync(join(bothSddInitCwd, "openspec", "changes", "archive")),
1181
+ true,
1182
+ "/sdd-init must create openspec/changes/archive when artifactStore is both",
1183
+ );
1184
+ assert.equal(
1185
+ existsSync(join(bothSddInitCwd, "openspec", "config.yaml")),
1186
+ true,
1187
+ "/sdd-init must write openspec/config.yaml when artifactStore is both",
1188
+ );
1189
+ assert.match(
1190
+ ctx.ui.notifications.at(-1).message,
1191
+ /Wrote openspec\/config\.yaml/,
1192
+ "/sdd-init must announce openspec/config.yaml when artifactStore is both",
1193
+ );
1194
+ assert.equal(ctx.ui.notifications.at(-1).level, "info");
1195
+ } finally {
1196
+ pi.setActiveTools(["read", "bash", "edit", "write"]);
1197
+ await rm(bothSddInitCwd, { recursive: true, force: true });
1198
+ }
1199
+
1010
1200
  const directEngramToolCwd = await tempWorkspace();
1011
1201
  try {
1012
1202
  pi.setActiveTools(["read", "bash", "edit", "write", "engram_mem_save"]);
@@ -1699,6 +1889,44 @@ async function run() {
1699
1889
  const restoredAgent = await readFile(join(modelsCwd, ".pi", "agents", "sdd-apply.md"), "utf8");
1700
1890
  assert.match(restoredAgent, /model: restore\/provider/);
1701
1891
  assert.match(restoredAgent, /thinking: high/);
1892
+
1893
+ // issue #286: `thinking: "max"` must survive save normalization and
1894
+ // reach both subagents.json (effort) and agent frontmatter (thinking).
1895
+ ctx.ui.custom = () =>
1896
+ Promise.resolve({
1897
+ type: "save",
1898
+ config: { "sdd-apply": { model: "openai/gpt-5", thinking: "max" } },
1899
+ });
1900
+ await commands.get("gentle:models").handler("", ctx);
1901
+ const maxSavedConfig = JSON.parse(await readFile(globalModelsPath, "utf8"));
1902
+ assert.equal(maxSavedConfig["sdd-apply"].thinking, "max");
1903
+ const maxSubagents = JSON.parse(
1904
+ await readFile(join(modelsCwd, ".pi", "subagents.json"), "utf8"),
1905
+ );
1906
+ assert.equal(maxSubagents.model_profiles["sdd-apply"].effort, "max");
1907
+ const maxApplyAgent = await readFile(
1908
+ join(modelsCwd, ".pi", "agents", "sdd-apply.md"),
1909
+ "utf8",
1910
+ );
1911
+ assert.match(maxApplyAgent, /thinking: max/);
1912
+
1913
+ // issue #286: effort picker must offer `max` after `xhigh` and save it.
1914
+ let maxPickerCalls = 0;
1915
+ ctx.ui.custom = (factory) =>
1916
+ new Promise((resolve) => {
1917
+ maxPickerCalls += 1;
1918
+ const panel = factory(null, null, null, resolve);
1919
+ if (maxPickerCalls === 1) {
1920
+ panel.handleInput(kittyE); // open effort picker (set-all row)
1921
+ for (let i = 0; i < 7; i++) panel.handleInput("j"); // max
1922
+ panel.handleInput("\r");
1923
+ panel.handleInput("\u0013"); // ctrl+s saves the draft
1924
+ return;
1925
+ }
1926
+ });
1927
+ await commands.get("gentle:models").handler("", ctx);
1928
+ const pickerMaxConfig = JSON.parse(await readFile(globalModelsPath, "utf8"));
1929
+ assert.equal(pickerMaxConfig["sdd-apply"].thinking, "max");
1702
1930
  } finally {
1703
1931
  await rm(modelsCwd, { recursive: true, force: true });
1704
1932
  await rm(globalModelsPath, { force: true });
@@ -5,7 +5,6 @@ import test from "node:test";
5
5
 
6
6
  const repoRoot = process.cwd();
7
7
  const assetsAgentsDir = join(repoRoot, "assets", "agents");
8
- const REVIEW_REFUTER_TOOLS = ["read", "grep", "find"];
9
8
  const GENERIC_ROLE_TOOLS: Record<string, string[]> = {
10
9
  "gentle-ai-explore.md": ["read", "grep", "find", "codegraph"],
11
10
  "gentle-ai-worker.md": ["read", "grep", "find", "edit", "write", "bash", "mem_save"],
@@ -61,18 +60,18 @@ function assertGenericRoleBody(fileName: string, source: string): void {
61
60
  }
62
61
 
63
62
  const requiredToolsByAgent: Record<string, string[]> = {
64
- "sdd-apply.md": ["read", "grep", "glob", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
65
- "sdd-archive.md": ["read", "grep", "glob", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save"],
66
- "sdd-design.md": ["read", "grep", "glob", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
67
- "sdd-explore.md": ["read", "grep", "glob", "edit", "write", "mem_save"],
68
- "sdd-init.md": ["read", "grep", "glob", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
69
- "sdd-onboard.md": ["read", "grep", "glob", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
70
- "sdd-proposal.md": ["read", "grep", "glob", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
71
- "sdd-spec.md": ["read", "grep", "glob", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
72
- "sdd-status.md": ["read", "grep", "glob", "bash", "mem_search", "mem_get_observation"],
73
- "sdd-sync.md": ["read", "grep", "glob", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
74
- "sdd-tasks.md": ["read", "grep", "glob", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
75
- "sdd-verify.md": ["read", "grep", "glob", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save"],
63
+ "sdd-apply.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
64
+ "sdd-archive.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save"],
65
+ "sdd-design.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
66
+ "sdd-explore.md": ["read", "grep", "find", "edit", "write", "mem_save"],
67
+ "sdd-init.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
68
+ "sdd-onboard.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
69
+ "sdd-proposal.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
70
+ "sdd-spec.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
71
+ "sdd-status.md": ["read", "grep", "find", "bash", "mem_search", "mem_get_observation"],
72
+ "sdd-sync.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save", "mem_update"],
73
+ "sdd-tasks.md": ["read", "grep", "find", "edit", "write", "mem_search", "mem_get_observation", "mem_save"],
74
+ "sdd-verify.md": ["read", "grep", "find", "edit", "write", "bash", "mem_search", "mem_get_observation", "mem_save"],
76
75
  };
77
76
 
78
77
  test("SDD package agents declare role-appropriate tools as YAML arrays", () => {
@@ -123,25 +122,11 @@ test("generic non-SDD agents declare exact role tool allowlists", () => {
123
122
  }
124
123
  });
125
124
 
126
- test("review-refuter exposes only complete-list inspection tools", () => {
127
- const path = join(assetsAgentsDir, "review-refuter.md");
128
- assert.ok(existsSync(path), "review-refuter.md must exist");
129
- const entries = readTools(path);
130
- assert.equal(entries[0], '"*": false', "review-refuter must lead with the deny-all rule");
131
- assert.deepEqual(entries.slice(1), REVIEW_REFUTER_TOOLS);
132
-
133
- const frontmatter = readFrontmatter(path);
134
- assert.match(frontmatter, /^name:\s*review-refuter$/m);
135
- for (const forbidden of [
136
- "bash",
137
- "edit",
138
- "write",
139
- "task",
140
- "subagent",
141
- "subagent_run",
142
- "mem_save",
143
- "mem_update",
144
- ]) {
145
- assert.doesNotMatch(frontmatter, new RegExp(`^ - ${forbidden}$`, "m"));
125
+ test("the retired Pi adversarial role agents are not packaged", () => {
126
+ // gentle-pi#311 P5: the refuter and targeted validator verdicts execute
127
+ // through Go-owned pi processes via provider-rendered self-contained
128
+ // vectors; no Pi agent definition may reintroduce a Pi-authored verdict.
129
+ for (const retired of ["review-refuter.md", "review-validator.md"]) {
130
+ assert.ok(!existsSync(join(assetsAgentsDir, retired)), `${retired} must stay deleted`);
146
131
  }
147
132
  });
@@ -16,6 +16,7 @@ const PREFIXED_NAMES: Record<string, string> = {
16
16
  "chained-pr": "gentle-ai-chained-pr",
17
17
  "issue-creation": "gentle-ai-issue-creation",
18
18
  "judgment-day": "gentle-ai-judgment-day",
19
+ "rdd-defect-workflow": "gentle-ai-rdd-defect-workflow",
19
20
  "skill-creator": "gentle-ai-skill-creator",
20
21
  "skill-improver": "gentle-ai-skill-improver",
21
22
  };