gentle-pi 2.2.0 → 2.3.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 (130) hide show
  1. package/README.md +51 -80
  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-proposal.md +2 -0
  8. package/assets/agents/sdd-research.md +54 -0
  9. package/assets/agents/sdd-status.md +4 -4
  10. package/assets/agents/sdd-tasks.md +2 -3
  11. package/assets/agents/sdd-verify.md +21 -1
  12. package/assets/chains/sdd-full.chain.md +1 -1
  13. package/assets/chains/sdd-verify.chain.md +1 -1
  14. package/assets/orchestrator-delegation.md +54 -248
  15. package/assets/orchestrator-memory.md +2 -0
  16. package/assets/orchestrator.md +22 -38
  17. package/assets/sdd-orchestrator-workflow.md +30 -37
  18. package/assets/support/sdd-status-contract.md +7 -7
  19. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  20. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  21. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  22. package/docs/native-authority-architecture.md +9 -11
  23. package/docs/review-integration.md +27 -373
  24. package/extensions/ask-user-choice.ts +151 -0
  25. package/extensions/gentle-ai.ts +1778 -3673
  26. package/extensions/quiet-tools.ts +515 -32
  27. package/extensions/sdd-init.ts +4 -8
  28. package/lib/gentle-ai-renderer.ts +70 -0
  29. package/lib/model-routing-authority.ts +133 -0
  30. package/lib/native-review-cli.ts +372 -940
  31. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  32. package/lib/review-candidate-view.ts +341 -132
  33. package/lib/review-host-relay.ts +210 -68
  34. package/lib/review-integration-v2.ts +839 -307
  35. package/lib/review-last-event-controller.ts +35 -0
  36. package/lib/sdd-preflight.ts +177 -66
  37. package/lib/sdd-status.ts +66 -111
  38. package/lib/terminal-theme.ts +1 -1
  39. package/package.json +83 -82
  40. package/runtime/gentle-ai-binary.mjs +1 -1
  41. package/runtime/native-review-cli.mjs +322 -890
  42. package/runtime/review-integration-v2.mjs +789 -257
  43. package/runtime/review-relay-contract.mjs +1 -1
  44. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
  45. package/scripts/gentle-ai-installer.mjs +75 -22
  46. package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
  47. package/scripts/test-packed-runner.mjs +4 -7
  48. package/scripts/verify-package-files.mjs +12 -13
  49. package/skills/_shared/review-ledger-contract.md +8 -14
  50. package/skills/chained-pr/SKILL.md +3 -0
  51. package/skills/cognitive-doc-design/SKILL.md +1 -1
  52. package/skills/comment-writer/SKILL.md +1 -1
  53. package/skills/gentle-ai/SKILL.md +7 -74
  54. package/skills/judgment-day/SKILL.md +5 -7
  55. package/skills/rdd-defect-workflow/SKILL.md +3 -3
  56. package/skills/release/SKILL.md +3 -3
  57. package/skills/skill-registry/SKILL.md +1 -1
  58. package/skills/work-unit-commits/SKILL.md +3 -1
  59. package/tests/artifact-language.test.ts +24 -11
  60. package/tests/ask-user-choice.test.ts +264 -0
  61. package/tests/codegraph-tools.test.ts +3 -3
  62. package/tests/crosslane/cross-lane.mjs +15 -1168
  63. package/tests/delegated-key-learnings-contract.test.ts +8 -6
  64. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  65. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  66. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  67. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  68. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  69. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  70. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  71. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  72. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  73. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  74. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  75. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  76. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  77. package/tests/gentle-ai-binary.test.ts +83 -13
  78. package/tests/gentle-ai-dev-binary.test.ts +51 -228
  79. package/tests/gentle-ai-installer.test.ts +137 -47
  80. package/tests/gentle-ai.test.ts +820 -131
  81. package/tests/gentle-theme.test.ts +133 -0
  82. package/tests/maintainer/provider-relay.maintest.ts +340 -4
  83. package/tests/model-routing-authority.test.ts +257 -0
  84. package/tests/native-review-capability-contract.test.ts +28 -1
  85. package/tests/native-review-cli.test.ts +586 -962
  86. package/tests/native-review-consent.test.ts +150 -66
  87. package/tests/native-review-parity-runtime.test.ts +80 -359
  88. package/tests/native-review-parity.test.ts +674 -925
  89. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  90. package/tests/orchestrator-budget.test.ts +132 -88
  91. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  92. package/tests/package-manifest.test.ts +37 -35
  93. package/tests/provider-defect-handoff.test.ts +15 -118
  94. package/tests/quiet-tool-rendering.test.ts +1055 -28
  95. package/tests/review-authority-recovery-docs.test.ts +1 -2
  96. package/tests/review-candidate-view.test.ts +409 -3
  97. package/tests/review-compact-contract.test.ts +4 -30
  98. package/tests/review-controller-lock-status.test.ts +2 -2
  99. package/tests/review-controller-native-recovery.test.ts +363 -1450
  100. package/tests/review-controller-native-routing.test.ts +1251 -5642
  101. package/tests/review-controller-retired-ops.test.ts +1 -1
  102. package/tests/review-controller-workspace-root.test.ts +222 -71
  103. package/tests/review-controller.test.ts +26 -816
  104. package/tests/review-corrected-finalize-binding.test.ts +112 -153
  105. package/tests/review-dispatch-hydration-gap.test.ts +1 -53
  106. package/tests/review-gate.test.ts +0 -45
  107. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  108. package/tests/review-host-relay-routing.test.ts +117 -82
  109. package/tests/review-host-relay.test.ts +242 -8
  110. package/tests/review-integration-v2-forward.test.ts +576 -553
  111. package/tests/review-integration-v2.test.ts +239 -165
  112. package/tests/review-last-event-closure.test.ts +408 -0
  113. package/tests/review-ledger-contract.test.ts +97 -35
  114. package/tests/review-recovered-lineage-routing.test.ts +0 -47
  115. package/tests/review-relay-transport-agent.test.ts +119 -46
  116. package/tests/review-snapshot.test.ts +3 -2
  117. package/tests/runtime-harness.mjs +361 -196
  118. package/tests/sdd-agent-tools.test.ts +36 -0
  119. package/tests/sdd-preflight.test.ts +81 -15
  120. package/tests/sdd-status.test.ts +109 -110
  121. package/tests/skill-collision-prefixes.test.ts +5 -8
  122. package/tests/writer-edit-surface-scope.test.ts +230 -0
  123. package/themes/Gentleman-Cute.json +94 -0
  124. package/themes/Gentleman-Sexy.json +92 -0
  125. package/lib/git-commit-transaction.ts +0 -861
  126. package/runtime/git-commit-transaction.mjs +0 -862
  127. package/scripts/run-git-commit-transaction.mjs +0 -35
  128. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  129. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  130. package/tests/git-commit-transaction.test.ts +0 -530
@@ -1,1557 +1,470 @@
1
1
  import assert from "node:assert/strict";
2
- import { mkdtempSync, readFileSync, rmSync } from "node:fs";
3
- import { tmpdir } from "node:os";
2
+ import { readFileSync } from "node:fs";
4
3
  import { join } from "node:path";
5
4
  import test from "node:test";
6
5
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
7
6
  import { __testing, createGentleAiExtension } from "../extensions/gentle-ai.ts";
8
7
  import {
9
- NATIVE_REVIEW_ERROR_CODE,
8
+ NATIVE_REVIEW_LEGACY_ALIAS_REPAIR,
9
+ NATIVE_REVIEW_LEGACY_QUARANTINE,
10
10
  NativeReviewCliError,
11
- NativeReviewCliV214 as NativeReviewCliV214Production,
12
11
  NativeReviewCliV216,
13
- clearNativeReviewCapabilitiesCacheForTesting,
14
12
  nativeReviewAbandonAuthorization,
15
13
  nativeReviewLegacyAliasRepairAuthorization,
14
+ nativeReviewLegacyQuarantineAuthorization,
15
+ nativeReviewReconcileAuthorization,
16
16
  type ExecFileAdapter,
17
- type NativeReviewCli,
18
17
  } from "../lib/native-review-cli.ts";
19
- import { GENTLE_AI_VERSION } from "../lib/gentle-ai-binary.ts";
20
18
 
21
- const v2FixtureRoot = join(process.cwd(), "contracts", "review-integration", "v2", "fixtures");
22
- const v2Fixture = <T = unknown>(name: string): T => JSON.parse(readFileSync(join(v2FixtureRoot, name), "utf8")) as T;
19
+ interface QueuedResult { stdout: string; exitCode?: number; }
23
20
 
24
- // Queued-adapter clients never execute a real process; a fixed absolute
25
- // package-local path keeps these tests independent of an installed binary.
26
- class NativeReviewCliV214 extends NativeReviewCliV214Production {
27
- constructor(...parameters: ConstructorParameters<typeof NativeReviewCliV214Production>) {
28
- const [adapter, executable, ...rest] = parameters;
29
- super(adapter, executable ?? "/package/.gentle-ai/gentle-ai", ...rest);
30
- }
31
- }
32
-
33
- interface QueuedResult { stdout: string; stderr?: string; exitCode?: number; }
34
-
35
- function queuedAdapter(results: QueuedResult[]): { adapter: ExecFileAdapter; calls: Array<{ file: string; arguments: readonly string[]; cwd: string }> } {
36
- const calls: Array<{ file: string; arguments: readonly string[]; cwd: string }> = [];
21
+ function queuedAdapter(results: readonly QueuedResult[]): {
22
+ adapter: ExecFileAdapter;
23
+ calls: Array<{ arguments: readonly string[]; cwd: string }>;
24
+ } {
25
+ const queue = [...results];
26
+ const calls: Array<{ arguments: readonly string[]; cwd: string }> = [];
37
27
  return {
38
28
  calls,
39
29
  adapter: async (request) => {
40
- calls.push({ file: request.file, arguments: request.arguments, cwd: request.cwd });
41
- const result = results.shift();
42
- if (!result) throw new Error("unexpected native invocation");
43
- return { stdout: result.stdout, stderr: result.stderr ?? "", exitCode: result.exitCode ?? 0, signal: null, timedOut: false, outputLimitExceeded: false };
44
- },
45
- };
46
- }
47
-
48
- const VERSION_219 = { stdout: "gentle-ai 2.1.9\n" };
49
- const VERSION_218 = { stdout: "gentle-ai 2.1.8\n" };
50
- const VERSION_220 = { stdout: "gentle-ai 2.2.0\n" };
51
- const RECLAIM_RECORD = { schema: "gentle-ai.review-reclaim-audit/v1", lineage: "stuck-lineage", actor: "maintainer", reason: "incomplete entry" };
52
- const RECOVER_RECORD = { schema: "gentle-ai.review-recovery/v1", predecessor_lineage: "broken", successor_lineage: "successor" };
53
- const RECONCILE_RECORD = { schema: "gentle-ai.review-reconcile-audit/v1", predecessor_lineage: "predecessor", successor_lineage: "successor", outcome: "quarantined" };
54
- const RECONCILE_RESULT = { operation: "review/reconcile-authority", record: RECONCILE_RECORD };
55
- const ABANDON_RECORD = { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: "pristine", status: "committed" };
56
- const LEGACY_QUARANTINE_RECORD = { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: "legacy", status: "committed" };
57
- const LEGACY_FREEZE_DIAGNOSTIC = "historical findings freeze changed unrelated transaction state";
58
- const LEGACY_FREEZE_DISPOSITION = "quarantine-malformed-freeze-event";
59
- const COMBINED_RECONCILE_ANOMALIES = "unchanged_target,malformed_recovery_authorization";
60
- const LEGACY_ALIAS_DIAGNOSTIC = "unsupported historical v1 operation alias";
61
- const LEGACY_ALIAS_DISPOSITION = "quarantine-approved-historical-alias";
62
- const LEGACY_ALIAS_RECORD = { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: "legacy-alias", status: "committed" };
63
- const LEGACY_ALIAS_AUTHORIZATION = [
64
- "gentle-ai.review-legacy-alias-repair-authorization/v1",
65
- "repository=/repo",
66
- "lineage=legacy-alias",
67
- `revision=sha256:${"c".repeat(64)}`,
68
- `diagnostic=${LEGACY_ALIAS_DIAGNOSTIC}`,
69
- `disposition=${LEGACY_ALIAS_DISPOSITION}`,
70
- "actor=maintainer",
71
- "reason=quarantine approved historical alias",
72
- ].join("\n");
73
- const ABANDON_DISCARDED_WORK = {
74
- capturedLensResults: ["00-risk.json", "01-readability.json"],
75
- findingsPresent: true,
76
- evidenceRecordsPresent: false,
77
- } as const;
78
- const ABANDON_AUTHORIZATION = [
79
- "gentle-ai.review-abandon-authorization/v2",
80
- "lineage=pristine",
81
- "revision=revision",
82
- "snapshot_identity=sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
83
- "reason=retire pristine lineage",
84
- "captured_lens_results=00-risk.json,01-readability.json",
85
- "findings_present=true",
86
- "evidence_records_present=false",
87
- "actor=maintainer",
88
- ].join("\n");
89
- const LEGACY_V1_ABANDON_AUTHORIZATION = [
90
- "gentle-ai.review-abandon-authorization/v1",
91
- "lineage=pristine",
92
- "revision=revision",
93
- "snapshot_identity=sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
94
- "actor=maintainer",
95
- "reason=retire pristine lineage",
96
- ].join("\n");
97
- const ABANDON_REQUEST = {
98
- cwd: "/repo",
99
- lineage: "pristine",
100
- expectedRevision: "revision",
101
- snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
102
- ...ABANDON_DISCARDED_WORK,
103
- actor: "maintainer",
104
- reason: "retire pristine lineage",
105
- maintainerAuthorization: ABANDON_AUTHORIZATION,
106
- } as const;
107
- const LEGACY_QUARANTINE_AUTHORIZATION = [
108
- "gentle-ai.review-legacy-quarantine-authorization/v1",
109
- "repository=/repo",
110
- "lineage=legacy",
111
- "revision=sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
112
- `diagnostic=${LEGACY_FREEZE_DIAGNOSTIC}`,
113
- `disposition=${LEGACY_FREEZE_DISPOSITION}`,
114
- "actor=maintainer",
115
- "reason=quarantine malformed legacy freeze",
116
- ].join("\n");
117
- const RECONCILE_AUTHORIZATION = [
118
- "gentle-ai.review-reconcile-authorization/v1",
119
- "predecessor_lineage=predecessor",
120
- "predecessor_revision=predecessor-revision",
121
- "successor_lineage=successor",
122
- "successor_revision=successor-revision",
123
- "actor=maintainer",
124
- "reason=invalid recovery edge",
125
- ].join("\n");
126
- const COMBINED_RECONCILE_AUTHORIZATION = `${RECONCILE_AUTHORIZATION}\nanomalies=${COMBINED_RECONCILE_ANOMALIES}`;
127
- const RECOVER_TARGET_IDENTITY = `sha256:${"e".repeat(64)}`;
128
- // The six canonical native recovery inputs, and nothing else. INSPECT never
129
- // publishes the legacy RESET quartet for a compact-v2 recovery, so this is the
130
- // complete request a maintainer can actually assemble (issue #212).
131
- const RECOVER_INPUT = {
132
- predecessorLineage: "broken",
133
- expectedPredecessorRevision: "rev-1",
134
- successorLineage: "successor",
135
- disposition: "invalidated",
136
- actor: "maintainer",
137
- reason: "invalid authority",
138
- } as const;
139
- const RECOVER_AUTHORIZATION = [
140
- "gentle-ai.review-recovery-authorization/v1",
141
- "predecessor_lineage=broken",
142
- "predecessor_revision=rev-1",
143
- `target_identity=${RECOVER_TARGET_IDENTITY}`,
144
- "actor=maintainer",
145
- "reason=invalid authority",
146
- ].join("\n");
147
-
148
- function scratchDir(prefix: string): string {
149
- const dir = mkdtempSync(join(tmpdir(), prefix));
150
- test.after(() => rmSync(dir, { recursive: true, force: true }));
151
- return dir;
152
- }
153
-
154
- interface RecordedNativeCall { operation: "reclaim" | "recover" | "reconcileAuthority" | "abandon" | "quarantineLegacy"; request: Record<string, unknown>; }
155
-
156
- function fakeRecoveryNative(record: Record<string, unknown>): { native: NativeReviewCli; calls: RecordedNativeCall[]; statusReads: Array<string | undefined> } {
157
- const calls: RecordedNativeCall[] = [];
158
- const statusReads: Array<string | undefined> = [];
159
- const native = {
160
- async reclaim(request: Record<string, unknown>) {
161
- calls.push({ operation: "reclaim", request });
162
- return { record };
163
- },
164
- async recover(request: Record<string, unknown>) {
165
- calls.push({ operation: "recover", request });
166
- return { record };
167
- },
168
- async reconcileAuthority(request: Record<string, unknown>) {
169
- calls.push({ operation: "reconcileAuthority", request });
170
- return { record };
171
- },
172
- async abandon(request: Record<string, unknown>) {
173
- calls.push({ operation: "abandon", request });
174
- return { record };
175
- },
176
- async quarantineLegacy(request: Record<string, unknown>) {
177
- calls.push({ operation: "quarantineLegacy", request });
178
- return { record };
179
- },
180
- async targetStatus(request: { lineageId?: string }) {
181
- statusReads.push(request.lineageId);
182
- return recoveryTargetStatus(request.lineageId ?? "broken");
30
+ calls.push({ arguments: request.arguments, cwd: request.cwd });
31
+ const result = queue.shift();
32
+ if (result === undefined) throw new Error("unexpected native invocation");
33
+ return { stdout: result.stdout, stderr: "", exitCode: result.exitCode ?? 0, signal: null, timedOut: false, outputLimitExceeded: false };
183
34
  },
184
- } as unknown as NativeReviewCli;
185
- return { native, calls, statusReads };
186
- }
187
-
188
- /** One recovery-eligible native target status, shaped as the provider publishes it. */
189
- function recoveryTargetStatus(lineageId: string, overrides: Record<string, unknown> = {}): unknown {
190
- return {
191
- action: "recover",
192
- actionDisposition: "invalidated",
193
- authority: { lineageId, revision: "rev-1" },
194
- targetIdentity: RECOVER_TARGET_IDENTITY,
195
- raw: { action: "recover", target_identity: RECOVER_TARGET_IDENTITY },
196
- ...overrides,
197
- };
198
- }
199
-
200
- async function runControllerOperation(
201
- parameters: Record<string, unknown>,
202
- native: NativeReviewCli | null,
203
- pendingAuthorizations: Map<string, unknown> = new Map(),
204
- signal?: AbortSignal,
205
- ): Promise<Record<string, unknown>> {
206
- const cwd = scratchDir("gentle-pi-native-recovery-");
207
- return await __testing.executeReviewControllerOperation(
208
- parameters,
209
- cwd,
210
- pendingAuthorizations as Map<string, never>,
211
- native,
212
- signal,
213
- );
214
- }
215
-
216
- /**
217
- * The registered `gentle_review` tool, driven exactly as Pi drives it. RECOVER
218
- * regressions hide from `executeReviewControllerOperation` alone, because the
219
- * defect in issue #212 lived in the authorization preflight the tool runs first.
220
- */
221
- function recoveryController(native: NativeReviewCli, prefix: string): (id: string, parameters: Record<string, unknown>, context: ExtensionContext) => Promise<Record<string, unknown>> {
222
- const tools = new Map<string, { execute: (id: string, params: unknown, signal: undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<unknown> }>();
223
- const pi = { on() {}, registerTool(definition: { name: string; execute: never }) { tools.set(definition.name, definition as never); }, registerCommand() {} } as unknown as ExtensionAPI;
224
- createGentleAiExtension({ nativeReviewCli: native })(pi);
225
- const controller = tools.get("gentle_review");
226
- assert.ok(controller);
227
- const cwd = scratchDir(prefix);
228
- return async (id, parameters, context) => {
229
- const result = await controller.execute(id, parameters, undefined, undefined, { ...context, cwd } as unknown as ExtensionContext) as { details: Record<string, unknown> };
230
- return result.details;
231
35
  };
232
36
  }
233
37
 
234
- /** An interactive Pi context that records every prompt it is shown. */
235
- function interactiveContext(prompts: string[], approve = true): ExtensionContext {
236
- return {
237
- hasUI: true,
238
- ui: {
239
- async confirm(_title: string, message: string) {
240
- prompts.push(message);
241
- return approve;
242
- },
243
- },
244
- } as unknown as ExtensionContext;
38
+ function client(adapter: ExecFileAdapter): NativeReviewCliV216 {
39
+ return new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai");
245
40
  }
246
41
 
247
- const HEADLESS_CONTEXT = { hasUI: false, ui: { confirm: async () => true } } as unknown as ExtensionContext;
42
+ const SHA = `sha256:${"a".repeat(64)}`;
248
43
 
249
- test("native reclaim wrapper issues the exact review reclaim command and returns the audit record", async () => {
250
- const { adapter, calls } = queuedAdapter([VERSION_219, { stdout: JSON.stringify(RECLAIM_RECORD) }]);
251
- const cli = new NativeReviewCliV214(adapter);
252
- const result = await cli.reclaim!({ cwd: "/repo", lineage: "stuck-lineage", actor: "maintainer", reason: "incomplete entry" });
253
- assert.deepEqual(result.record, RECLAIM_RECORD);
254
- assert.deepEqual(calls[1]?.arguments, ["review", "reclaim", "--cwd", "/repo", "--lineage", "stuck-lineage", "--actor", "maintainer", "--reason", "incomplete entry"]);
255
- });
256
-
257
- test("native recover wrapper issues the exact review recover command including the authorization binding", async () => {
258
- const { adapter, calls } = queuedAdapter([VERSION_219, { stdout: JSON.stringify(RECOVER_RECORD) }]);
259
- const cli = new NativeReviewCliV214(adapter);
260
- const result = await cli.recover!({
44
+ // Recovery and maintenance commands retain their published legacy wire forms.
45
+ // These records are intentionally opaque: Pi validates command bindings, then
46
+ // relays the provider's audit record without reinterpreting authority state.
47
+ test("native reclaim and recover preserve their exact authority bindings", async () => {
48
+ const queue = queuedAdapter([
49
+ { stdout: JSON.stringify({ schema: "gentle-ai.review-reclaim-audit/v1", lineage: "stuck", actor: "maintainer", reason: "incomplete" }) },
50
+ { stdout: JSON.stringify({ schema: "gentle-ai.review-recovery/v1", predecessor_lineage: "stuck", successor_lineage: "recovered" }) },
51
+ ]);
52
+ const review = client(queue.adapter);
53
+ assert.equal((await review.reclaim({ cwd: "/repo", lineage: "stuck", actor: "maintainer", reason: "incomplete" })).record.lineage, "stuck");
54
+ assert.equal((await review.recover({
261
55
  cwd: "/repo",
262
- predecessorLineage: "broken",
263
- expectedPredecessorRevision: "rev-1",
264
- successorLineage: "successor",
56
+ predecessorLineage: "stuck",
57
+ expectedPredecessorRevision: "revision-1",
58
+ successorLineage: "recovered",
265
59
  disposition: "invalidated",
266
60
  actor: "maintainer",
267
- reason: "invalid authority",
268
- maintainerAuthorization: "binding",
269
- });
270
- assert.deepEqual(result.record, RECOVER_RECORD);
271
- assert.deepEqual(calls[1]?.arguments, [
272
- "review", "recover", "--cwd", "/repo",
273
- "--predecessor-lineage", "broken",
274
- "--expected-predecessor-revision", "rev-1",
275
- "--successor-lineage", "successor",
276
- "--disposition", "invalidated",
277
- "--actor", "maintainer",
278
- "--reason", "invalid authority",
279
- "--maintainer-authorization", "binding",
280
- ]);
281
- });
282
-
283
- test("native reconcile-authority wrapper binds the exact target revisions and authorization without a shell", async () => {
284
- const { adapter, calls } = queuedAdapter([VERSION_219, { stdout: JSON.stringify(RECONCILE_RESULT) }]);
285
- const cli = new NativeReviewCliV214(adapter);
286
- const result = await cli.reconcileAuthority!({
287
- cwd: "/repo with spaces",
288
- predecessorLineage: "predecessor",
289
- expectedPredecessorRevision: "predecessor-revision",
290
- successorLineage: "successor",
291
- expectedSuccessorRevision: "successor-revision",
292
- actor: "maintainer",
293
- reason: "invalid recovery edge",
294
- maintainerAuthorization: RECONCILE_AUTHORIZATION,
295
- });
296
- assert.deepEqual(result.record, RECONCILE_RECORD);
297
- assert.deepEqual(calls[1]?.arguments, [
298
- "review", "reconcile-authority", "--cwd", "/repo with spaces",
299
- "--predecessor-lineage", "predecessor",
300
- "--expected-predecessor-revision", "predecessor-revision",
301
- "--successor-lineage", "successor",
302
- "--expected-successor-revision", "successor-revision",
303
- "--actor", "maintainer",
304
- "--reason", "invalid recovery edge",
305
- "--maintainer-authorization", RECONCILE_AUTHORIZATION,
61
+ reason: "recovery required",
62
+ maintainerAuthorization: "approved\nbinding",
63
+ })).record.successor_lineage, "recovered");
64
+ assert.deepEqual(queue.calls.map((call) => call.arguments), [
65
+ ["review", "reclaim", "--cwd", "/repo", "--lineage", "stuck", "--actor", "maintainer", "--reason", "incomplete"],
66
+ ["review", "recover", "--cwd", "/repo", "--predecessor-lineage", "stuck", "--expected-predecessor-revision", "revision-1", "--successor-lineage", "recovered", "--disposition", "invalidated", "--actor", "maintainer", "--reason", "recovery required", "--maintainer-authorization", "approved\nbinding"],
306
67
  ]);
307
68
  });
308
69
 
309
- test("native v2.1.8 reconcile-authority accepts its raw audit response but modern envelopes remain strict", async () => {
70
+ test("native abandon carries the exact discarded-work authorization and audit record", async () => {
310
71
  const request = {
311
72
  cwd: "/repo",
312
- predecessorLineage: "predecessor",
313
- expectedPredecessorRevision: "predecessor-revision",
314
- successorLineage: "successor",
315
- expectedSuccessorRevision: "successor-revision",
73
+ lineage: "abandoned",
74
+ expectedRevision: "revision-1",
75
+ snapshotIdentity: SHA,
76
+ capturedLensResults: ["00-risk.json"],
77
+ findingsPresent: true,
78
+ evidenceRecordsPresent: false,
316
79
  actor: "maintainer",
317
- reason: "invalid recovery edge",
318
- maintainerAuthorization: RECONCILE_AUTHORIZATION,
80
+ reason: "discard candidate",
319
81
  };
320
- const legacy = queuedAdapter([VERSION_218, { stdout: JSON.stringify(RECONCILE_RECORD) }]);
321
- assert.deepEqual((await new NativeReviewCliV214(legacy.adapter).reconcileAuthority!(request)).record, RECONCILE_RECORD);
322
-
323
- for (const [version, response] of [
324
- [VERSION_218, { schema: "gentle-ai.review-reconcile-audit/v1", predecessor_lineage: "predecessor" }],
325
- [VERSION_219, RECONCILE_RECORD],
326
- ] as const) {
327
- const queue = queuedAdapter([version, { stdout: JSON.stringify(response) }]);
328
- await assert.rejects(
329
- () => new NativeReviewCliV214(queue.adapter).reconcileAuthority!(request),
330
- (error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE,
331
- );
332
- }
82
+ const queue = queuedAdapter([{ stdout: JSON.stringify({ operation: "review/abandon", record: { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: "abandoned", status: "committed" } }) }]);
83
+ const result = await client(queue.adapter).abandon({ ...request, maintainerAuthorization: nativeReviewAbandonAuthorization(request) });
84
+ assert.equal(result.record.status, "committed");
85
+ assert.deepEqual(queue.calls[0]?.arguments.slice(0, 10), ["review", "abandon", "--cwd", "/repo", "--lineage", "abandoned", "--expected-revision", "revision-1", "--actor", "maintainer"]);
333
86
  });
334
87
 
335
- test("native v2.1.9 maintenance wrappers use exact argv and published authorization bindings", async () => {
336
- const { adapter, calls } = queuedAdapter([
337
- VERSION_219, { stdout: JSON.stringify({ operation: "review/abandon", record: ABANDON_RECORD }) },
338
- VERSION_219, { stdout: JSON.stringify({ operation: "review/quarantine-legacy", record: LEGACY_QUARANTINE_RECORD }) },
339
- VERSION_219, { stdout: JSON.stringify({ operation: "review/reconcile-authority", record: RECONCILE_RECORD }) },
340
- ]);
341
- const cli = new NativeReviewCliV214(adapter);
342
- assert.deepEqual((await cli.abandon!({ ...ABANDON_REQUEST })).record, ABANDON_RECORD);
343
- assert.deepEqual((await cli.quarantineLegacy!({ cwd: "/repo", repository: "/repo", lineage: "legacy", expectedRevision: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", diagnostic: LEGACY_FREEZE_DIAGNOSTIC, disposition: LEGACY_FREEZE_DISPOSITION, actor: "maintainer", reason: "quarantine malformed legacy freeze", maintainerAuthorization: LEGACY_QUARANTINE_AUTHORIZATION })).record, LEGACY_QUARANTINE_RECORD);
344
- assert.deepEqual((await cli.reconcileAuthority!({ cwd: "/repo", predecessorLineage: "predecessor", expectedPredecessorRevision: "predecessor-revision", successorLineage: "successor", expectedSuccessorRevision: "successor-revision", actor: "maintainer", reason: "invalid recovery edge", anomalies: COMBINED_RECONCILE_ANOMALIES, maintainerAuthorization: COMBINED_RECONCILE_AUTHORIZATION })).record, RECONCILE_RECORD);
345
- assert.deepEqual(calls.filter((call) => call.arguments[0] === "review").map((call) => call.arguments), [
346
- ["review", "abandon", "--cwd", "/repo", "--lineage", "pristine", "--expected-revision", "revision", "--actor", "maintainer", "--reason", "retire pristine lineage", "--maintainer-authorization", ABANDON_AUTHORIZATION],
347
- ["review", "quarantine-legacy", "--cwd", "/repo", "--lineage", "legacy", "--expected-revision", "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "--diagnostic", LEGACY_FREEZE_DIAGNOSTIC, "--disposition", LEGACY_FREEZE_DISPOSITION, "--actor", "maintainer", "--reason", "quarantine malformed legacy freeze", "--maintainer-authorization", LEGACY_QUARANTINE_AUTHORIZATION],
348
- ["review", "reconcile-authority", "--cwd", "/repo", "--predecessor-lineage", "predecessor", "--expected-predecessor-revision", "predecessor-revision", "--successor-lineage", "successor", "--expected-successor-revision", "successor-revision", "--actor", "maintainer", "--reason", "invalid recovery edge", "--maintainer-authorization", COMBINED_RECONCILE_AUTHORIZATION],
349
- ]);
350
- });
351
-
352
- test("native v2.2.0 repair-legacy-alias uses the exact fixed binding and preserves idempotent audit records", async () => {
353
- const { adapter, calls } = queuedAdapter([
354
- VERSION_220,
355
- { stdout: JSON.stringify({ operation: "review/repair-legacy-alias", record: LEGACY_ALIAS_RECORD }) },
356
- ]);
357
- const cli = new NativeReviewCliV214(adapter);
88
+ test("native quarantine retains published legacy-v1 maintenance compatibility", async () => {
358
89
  const request = {
359
90
  cwd: "/repo",
360
91
  repository: "/repo",
361
- lineage: "legacy-alias",
362
- expectedRevision: `sha256:${"c".repeat(64)}`,
363
- diagnostic: LEGACY_ALIAS_DIAGNOSTIC,
364
- disposition: LEGACY_ALIAS_DISPOSITION,
92
+ lineage: "legacy-freeze",
93
+ expectedRevision: SHA,
94
+ diagnostic: NATIVE_REVIEW_LEGACY_QUARANTINE.DIAGNOSTIC,
95
+ disposition: NATIVE_REVIEW_LEGACY_QUARANTINE.DISPOSITION,
365
96
  actor: "maintainer",
366
- reason: "quarantine approved historical alias",
367
- maintainerAuthorization: LEGACY_ALIAS_AUTHORIZATION,
97
+ reason: "quarantine malformed freeze",
368
98
  };
369
- assert.equal(nativeReviewLegacyAliasRepairAuthorization(request), LEGACY_ALIAS_AUTHORIZATION);
370
- assert.deepEqual((await cli.repairLegacyAlias!(request)).record, LEGACY_ALIAS_RECORD);
371
- assert.deepEqual(calls[1]?.arguments, [
372
- "review", "repair-legacy-alias", "--cwd", "/repo", "--lineage", "legacy-alias",
373
- "--expected-revision", `sha256:${"c".repeat(64)}`,
374
- "--diagnostic", LEGACY_ALIAS_DIAGNOSTIC,
375
- "--disposition", LEGACY_ALIAS_DISPOSITION,
376
- "--actor", "maintainer", "--reason", "quarantine approved historical alias",
377
- "--maintainer-authorization", LEGACY_ALIAS_AUTHORIZATION,
378
- ]);
99
+ const queue = queuedAdapter([{ stdout: JSON.stringify({ operation: "review/quarantine-legacy", record: { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: "legacy-freeze", status: "committed" } }) }]);
100
+ const result = await client(queue.adapter).quarantineLegacy({ ...request, maintainerAuthorization: nativeReviewLegacyQuarantineAuthorization(request) });
101
+ assert.equal(result.record.lineage_id, "legacy-freeze");
102
+ assert.deepEqual(queue.calls[0]?.arguments.slice(0, 8), ["review", "quarantine-legacy", "--cwd", "/repo", "--lineage", "legacy-freeze", "--expected-revision", SHA]);
379
103
  });
380
104
 
381
- // Task 11.1 (migrate-review-integration-v2): `repairLegacyAlias` above stays
382
- // unnegotiated and carries no --contract (asserted at :281-288). The net-new
383
- // negotiated `repair()` is a DIFFERENT operation and must carry --contract on
384
- // every invocation, including the capabilities preflight `negotiated()` runs
385
- // first. A non-eligible preflight assessment lets this test stop after one
386
- // repair call, without needing a second queued execute-mode response.
387
- test("negotiated repair carries --contract on every invocation, unlike repair-legacy-alias", async (t) => {
388
- t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
389
- const capabilities = v2Fixture<Record<string, unknown>>("capabilities.fixture.json");
390
- const executableDigest = "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705";
391
- const capabilitiesBody = { ...capabilities, package: { ...(capabilities.package as Record<string, unknown>), version: GENTLE_AI_VERSION } };
392
- const preflightResult = {
393
- schema: "gentle-ai.review-integration.repair/v2",
394
- contract: "gentle-ai.review-integration/v2",
395
- operation: "review.repair",
396
- mode: "preflight",
397
- assessment: {
398
- schema: "gentle-ai.review-authority-repair-assessment/v1",
399
- status: "unsupported",
400
- counts: { lineages: 0, compact_lineages: 0, legacy_lineages: 0, events: 0, bytes: 0, eligible_candidates: 0, unsupported_lineages: 0, conflicts: 0 },
401
- supported_operations: ["review/complete-fix", "review/validate-fix"],
402
- authorization_schema: "gentle-ai.review-repair-authorization/v1",
403
- },
404
- required_inputs: [],
405
- };
406
- const { adapter, calls } = queuedAdapter([
407
- { stdout: JSON.stringify(capabilitiesBody) },
408
- { stdout: JSON.stringify(preflightResult) },
409
- ]);
410
- const cli = new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => executableDigest);
411
- const result = await cli.repair!({ cwd: "/repo", actor: "maintainer", reason: "quarantine approved historical alias", maintainerAuthorization: "irrelevant-for-non-eligible-preflight" });
412
- assert.equal(result.mode, "preflight");
413
- assert.equal(result.assessment.status, "unsupported");
414
- assert.equal(calls.length, 2, "a non-eligible preflight must never issue an execute-mode call");
415
- assert.deepEqual(calls[0]?.arguments, ["review", "capabilities", "--contract", "gentle-ai.review-integration/v2"]);
416
- assert.deepEqual(calls[1]?.arguments, ["review", "repair", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo", "--mode", "preflight"]);
417
- for (const call of calls) assert.ok(call.arguments.includes("--contract"), "every negotiated repair() invocation must carry --contract");
418
- });
419
-
420
- test("native repair-legacy-alias fails closed for stale bindings, malformed output, cancellation, and partial failure", async () => {
105
+ test("native reconciliation accepts only its exact predecessor and successor binding", async () => {
421
106
  const request = {
422
- cwd: "/repo", repository: "/repo", lineage: "legacy-alias", expectedRevision: `sha256:${"c".repeat(64)}`,
423
- diagnostic: LEGACY_ALIAS_DIAGNOSTIC, disposition: LEGACY_ALIAS_DISPOSITION, actor: "maintainer", reason: "quarantine approved historical alias", maintainerAuthorization: LEGACY_ALIAS_AUTHORIZATION,
424
- };
425
- const stale = queuedAdapter([]);
426
- await assert.rejects(() => new NativeReviewCliV214(stale.adapter).repairLegacyAlias!({ ...request, expectedRevision: `sha256:${"d".repeat(64)}` }), /exact repository, lineage, revision/);
427
- assert.equal(stale.calls.length, 0);
428
- for (const result of [
429
- { stdout: JSON.stringify({ operation: "review/repair-legacy-alias" }) },
430
- { stdout: JSON.stringify({ operation: "review/repair-legacy-alias", record: LEGACY_ALIAS_RECORD }), stderr: "interrupted", exitCode: 1 },
431
- ]) {
432
- const queue = queuedAdapter([VERSION_220, result]);
433
- await assert.rejects(
434
- () => new NativeReviewCliV214(queue.adapter).repairLegacyAlias!(request),
435
- (error: unknown) => error instanceof NativeReviewCliError
436
- && (result.exitCode === 1 ? error.auditRecord?.schema === LEGACY_ALIAS_RECORD.schema : error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE),
437
- );
438
- }
439
- });
440
-
441
- test("native v2.1.9 maintenance wrappers fail closed before launch for unsupported versions and invalid bindings", async () => {
442
- const unsupported = queuedAdapter([{ stdout: "gentle-ai 2.1.8\n" }]);
443
- await assert.rejects(() => new NativeReviewCliV214(unsupported.adapter).abandon!({ ...ABANDON_REQUEST }), NativeReviewCliError);
444
- const invalid = queuedAdapter([]);
445
- await assert.rejects(() => new NativeReviewCliV214(invalid.adapter).reconcileAuthority!({ cwd: "/repo", predecessorLineage: "predecessor", expectedPredecessorRevision: "predecessor-revision", successorLineage: "successor", expectedSuccessorRevision: "successor-revision", actor: "maintainer", reason: "invalid recovery edge", anomalies: "malformed_recovery_authorization,unchanged_target", maintainerAuthorization: COMBINED_RECONCILE_AUTHORIZATION }), TypeError);
446
- assert.equal(invalid.calls.length, 0);
447
- });
448
-
449
- test("native v2.1.9 maintenance wrappers preserve only valid prepared audit records on partial failures", async () => {
450
- for (const [operation, invoke, result] of [
451
- ["review/abandon", (cli: NativeReviewCliV214) => cli.abandon!({ ...ABANDON_REQUEST }), ABANDON_RECORD],
452
- ["review/quarantine-legacy", (cli: NativeReviewCliV214) => cli.quarantineLegacy!({ cwd: "/repo", repository: "/repo", lineage: "legacy", expectedRevision: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", diagnostic: LEGACY_FREEZE_DIAGNOSTIC, disposition: LEGACY_FREEZE_DISPOSITION, actor: "maintainer", reason: "quarantine malformed legacy freeze", maintainerAuthorization: LEGACY_QUARANTINE_AUTHORIZATION }), LEGACY_QUARANTINE_RECORD],
453
- ] as const) {
454
- const queue = queuedAdapter([VERSION_219, { stdout: JSON.stringify({ operation, record: result }), stderr: "quarantine interrupted", exitCode: 1 }]);
455
- await assert.rejects(() => invoke(new NativeReviewCliV214(queue.adapter)), (error: unknown) => error instanceof NativeReviewCliError && error.mutationOutcome === "unknown" && error.nextAction === "review.status" && error.auditRecord?.schema === result.schema);
456
- }
457
- const malformed = queuedAdapter([VERSION_219, { stdout: JSON.stringify({ operation: "review/abandon" }), exitCode: 1 }]);
458
- await assert.rejects(() => new NativeReviewCliV214(malformed.adapter).abandon!({ ...ABANDON_REQUEST }), (error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE && error.auditRecord === undefined);
459
- });
460
-
461
- test("native abandon forwards cancellation and preserves the unknown mutation outcome", async () => {
462
- const controller = new AbortController();
463
- let calls = 0;
464
- const adapter: ExecFileAdapter = async (request) => {
465
- calls += 1;
466
- if (calls === 1) return { ...VERSION_219, stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
467
- assert.equal(request.signal, controller.signal);
468
- const error = new Error("cancelled");
469
- error.name = "AbortError";
470
- throw error;
107
+ cwd: "/repo",
108
+ predecessorLineage: "predecessor",
109
+ expectedPredecessorRevision: "predecessor-revision",
110
+ successorLineage: "successor",
111
+ expectedSuccessorRevision: "successor-revision",
112
+ actor: "maintainer",
113
+ reason: "repair authority edge",
471
114
  };
472
- await assert.rejects(() => new NativeReviewCliV214(adapter).abandon!({ ...ABANDON_REQUEST, signal: controller.signal }), (error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.CANCELLED && error.mutationOutcome === "unknown" && error.nextAction === "review.status");
473
- });
474
-
475
- test("native abandon authorization emits the exact nine-line v2 discarded-work binding", () => {
476
- assert.equal(
477
- nativeReviewAbandonAuthorization({
478
- lineage: "pristine",
479
- expectedRevision: "revision",
480
- snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
481
- actor: " maintainer ",
482
- reason: "operator_disposition",
483
- capturedLensResults: ["00-risk.json", "01-readability.json"],
484
- findingsPresent: true,
485
- evidenceRecordsPresent: false,
486
- }),
487
- [
488
- "gentle-ai.review-abandon-authorization/v2",
489
- "lineage=pristine",
490
- "revision=revision",
491
- "snapshot_identity=sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
492
- "reason=operator_disposition",
493
- "captured_lens_results=00-risk.json,01-readability.json",
494
- "findings_present=true",
495
- "evidence_records_present=false",
496
- "actor=maintainer",
497
- ].join("\n"),
498
- );
499
- assert.equal(
500
- nativeReviewAbandonAuthorization({
501
- lineage: "pristine",
502
- expectedRevision: "revision",
503
- snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
504
- actor: "maintainer",
505
- reason: "retired_schema",
506
- capturedLensResults: [],
507
- findingsPresent: false,
508
- evidenceRecordsPresent: true,
509
- }),
510
- [
511
- "gentle-ai.review-abandon-authorization/v2",
512
- "lineage=pristine",
513
- "revision=revision",
514
- "snapshot_identity=sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
515
- "reason=retired_schema",
516
- "captured_lens_results=",
517
- "findings_present=false",
518
- "evidence_records_present=true",
519
- "actor=maintainer",
520
- ].join("\n"),
521
- );
522
- });
523
-
524
- test("native abandon refuses the legacy v1 authorization binding before process launch", async () => {
525
- const { adapter, calls } = queuedAdapter([]);
526
- await assert.rejects(
527
- () => new NativeReviewCliV214(adapter).abandon!({ ...ABANDON_REQUEST, maintainerAuthorization: LEGACY_V1_ABANDON_AUTHORIZATION }),
528
- TypeError,
529
- );
530
- assert.equal(calls.length, 0);
531
- });
532
-
533
- test("native abandon validates discarded-work inputs before process launch", async () => {
534
- const { adapter, calls } = queuedAdapter([]);
535
- const cli = new NativeReviewCliV214(adapter);
536
- await assert.rejects(() => cli.abandon!({ ...ABANDON_REQUEST, capturedLensResults: "00-risk.json" as unknown as string[] }), /capturedLensResults/);
537
- await assert.rejects(() => cli.abandon!({ ...ABANDON_REQUEST, capturedLensResults: ["00-risk.json", " padded "] }), /capturedLensResults/);
538
- await assert.rejects(() => cli.abandon!({ ...ABANDON_REQUEST, findingsPresent: "true" as unknown as boolean }), /findingsPresent/);
539
- await assert.rejects(() => cli.abandon!({ ...ABANDON_REQUEST, evidenceRecordsPresent: undefined as unknown as boolean }), /evidenceRecordsPresent/);
540
- assert.equal(calls.length, 0);
115
+ const queue = queuedAdapter([{ stdout: JSON.stringify({ operation: "review/reconcile-authority", record: { schema: "gentle-ai.review-reconcile-audit/v1", predecessor_lineage: "predecessor", successor_lineage: "successor", outcome: "quarantined" } }) }]);
116
+ const result = await client(queue.adapter).reconcileAuthority({ ...request, maintainerAuthorization: nativeReviewReconcileAuthorization(request) });
117
+ assert.equal(result.record.successor_lineage, "successor");
118
+ assert.deepEqual(queue.calls[0]?.arguments.slice(0, 12), ["review", "reconcile-authority", "--cwd", "/repo", "--predecessor-lineage", "predecessor", "--expected-predecessor-revision", "predecessor-revision", "--successor-lineage", "successor", "--expected-successor-revision", "successor-revision"]);
541
119
  });
542
120
 
543
- test("native reconcile-authority refuses a mismatched authorization before process launch", async () => {
544
- const { adapter, calls } = queuedAdapter([]);
545
- const cli = new NativeReviewCliV214(adapter);
121
+ test("native maintenance rejects tampered authority authorization before launch", async () => {
122
+ const queue = queuedAdapter([]);
546
123
  await assert.rejects(
547
- cli.reconcileAuthority!({
124
+ () => client(queue.adapter).reconcileAuthority({
548
125
  cwd: "/repo",
549
126
  predecessorLineage: "predecessor",
550
127
  expectedPredecessorRevision: "predecessor-revision",
551
128
  successorLineage: "successor",
552
- expectedSuccessorRevision: "changed-revision",
129
+ expectedSuccessorRevision: "successor-revision",
553
130
  actor: "maintainer",
554
- reason: "invalid recovery edge",
555
- maintainerAuthorization: RECONCILE_AUTHORIZATION,
131
+ reason: "repair authority edge",
132
+ maintainerAuthorization: "tampered",
556
133
  }),
557
134
  /exact target and revision binding/,
558
135
  );
559
- assert.equal(calls.length, 0);
560
- });
561
-
562
- test("native reconcile-authority forwards cancellation and preserves unknown mutation outcome", async () => {
563
- const controller = new AbortController();
564
- let calls = 0;
565
- const adapter: ExecFileAdapter = async (request) => {
566
- calls += 1;
567
- if (calls === 1) return { ...VERSION_219, stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
568
- assert.equal(request.signal, controller.signal);
569
- const error = new Error("cancelled");
570
- error.name = "AbortError";
571
- throw error;
136
+ assert.equal(queue.calls.length, 0);
137
+ });
138
+
139
+ test("native authority inventory remains a strict read-only maintenance surface", async () => {
140
+ const queue = queuedAdapter([{ stdout: JSON.stringify({
141
+ schema: "gentle-ai.review-authority-status/v1",
142
+ operation: "review/status",
143
+ repository: process.cwd(),
144
+ complete: true,
145
+ authoritative: true,
146
+ status: "active",
147
+ entries: [{ version: "compact-v2", path: ".git/gentle-ai/reviews/current.json", status: "active", lineage_id: "current", state: "reviewing", revision: "revision-1", problems: [] }],
148
+ locks: [],
149
+ diagnostics: [],
150
+ }) }]);
151
+ const inventory = await client(queue.adapter).reviewStatus({ cwd: process.cwd() });
152
+ assert.equal(inventory.authoritative, true);
153
+ assert.equal(inventory.entries[0]?.lineageId, "current");
154
+ assert.deepEqual(queue.calls[0]?.arguments, ["review", "status", "--cwd", process.cwd()]);
155
+ });
156
+
157
+ test("negotiated repair replays only provider-published inputs after a strict preflight", async () => {
158
+ const preflight = JSON.parse(readFileSync(join(process.cwd(), "contracts", "review-integration", "v1", "fixtures", "repair-preflight.fixture.json"), "utf8")) as Record<string, unknown>;
159
+ preflight.schema = "gentle-ai.review-integration.repair/v2";
160
+ preflight.contract = "gentle-ai.review-integration/v2";
161
+ const execute = structuredClone(preflight) as Record<string, unknown>;
162
+ execute.mode = "execute";
163
+ delete execute.provider_inputs;
164
+ execute.required_inputs = [];
165
+ execute.execution = {
166
+ status: "committed",
167
+ class: "legacy_v1_historical_alias",
168
+ lineage_id: "historical-alias",
169
+ revision: `sha256:${"1".repeat(64)}`,
170
+ chain_identity: `sha256:${"2".repeat(64)}`,
171
+ cause: "unsupported_historical_v1_operation_alias",
172
+ disposition: "quarantine-approved-historical-alias",
173
+ assessment_digest: `sha256:${"3".repeat(64)}`,
174
+ request_digest: `sha256:${"4".repeat(64)}`,
175
+ record_identity: `sha256:${"5".repeat(64)}`,
572
176
  };
573
- const cli = new NativeReviewCliV214(adapter);
574
- await assert.rejects(
575
- cli.reconcileAuthority!({
576
- cwd: "/repo",
577
- predecessorLineage: "predecessor",
578
- expectedPredecessorRevision: "predecessor-revision",
579
- successorLineage: "successor",
580
- expectedSuccessorRevision: "successor-revision",
581
- actor: "maintainer",
582
- reason: "invalid recovery edge",
583
- maintainerAuthorization: RECONCILE_AUTHORIZATION,
584
- signal: controller.signal,
585
- }),
586
- (error: unknown) => error instanceof NativeReviewCliError
587
- && error.code === NATIVE_REVIEW_ERROR_CODE.CANCELLED
588
- && error.operation === "review/reconcile-authority"
589
- && error.mutationOutcome === "unknown",
590
- );
591
- });
592
-
593
- test("native reconcile-authority preserves the prepared audit record on partial failure", async () => {
594
- const { adapter } = queuedAdapter([VERSION_219, { stdout: JSON.stringify(RECONCILE_RESULT), stderr: "quarantine interrupted", exitCode: 1 }]);
595
- const cli = new NativeReviewCliV214(adapter);
596
- await assert.rejects(
597
- cli.reconcileAuthority!({
598
- cwd: "/repo",
599
- predecessorLineage: "predecessor",
600
- expectedPredecessorRevision: "predecessor-revision",
601
- successorLineage: "successor",
602
- expectedSuccessorRevision: "successor-revision",
603
- actor: "maintainer",
604
- reason: "invalid recovery edge",
605
- maintainerAuthorization: RECONCILE_AUTHORIZATION,
606
- }),
607
- (error: unknown) => error instanceof NativeReviewCliError
608
- && error.mutationOutcome === "unknown"
609
- && error.nextAction === "review.status"
610
- && error.auditRecord?.schema === RECONCILE_RECORD.schema,
611
- );
612
- });
613
-
614
- test("native recovery wrappers accept the published 2.1.9 contract and refuse older recovery binaries", async () => {
615
- const { adapter } = queuedAdapter([{ stdout: "gentle-ai 2.1.7\n" }]);
616
- const cli = new NativeReviewCliV214(adapter);
617
- await assert.rejects(
618
- cli.reclaim!({ cwd: "/repo", lineage: "stuck", actor: "maintainer", reason: "incomplete" }),
619
- (error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE,
620
- );
621
- });
622
-
623
- test("RESET maps to native review reclaim with the exact audited inputs and clears pending authorizations", async () => {
624
- const { native, calls } = fakeRecoveryNative(RECLAIM_RECORD);
625
- const pending = new Map<string, unknown>([["stale", { command: "git push" }]]);
626
- const details = await runControllerOperation({
627
- operation: "reset",
628
- input: JSON.stringify({
629
- repositoryId: "repo-id",
630
- commonDirHash: "c".repeat(64),
631
- inventoryHash: "d".repeat(64),
632
- confirmation: "DESTROY REVIEW AUTHORITY repo-id",
633
- lineage: "stuck-lineage",
634
- actor: "maintainer",
635
- reason: "incomplete entry",
636
- }),
637
- }, native, pending);
638
- assert.equal(details.operation, "reset");
639
- assert.equal(details.native_operation, "review reclaim");
640
- assert.equal(details.mutation_performed, true);
641
- assert.equal(details.mutation_outcome, "committed");
642
- assert.deepEqual(details.result, RECLAIM_RECORD);
643
- assert.equal(details.next_action, "inspect");
644
- assert.equal(calls.length, 1);
645
- assert.equal(calls[0]?.operation, "reclaim");
646
- assert.equal(calls[0]?.request.lineage, "stuck-lineage");
647
- assert.equal(calls[0]?.request.actor, "maintainer");
648
- assert.equal(calls[0]?.request.reason, "incomplete entry");
649
- assert.equal(pending.size, 0);
650
- });
651
-
652
- test("RESET without the native reclaim inputs returns a structured request instead of inventing values", async () => {
653
- const { native, calls } = fakeRecoveryNative(RECLAIM_RECORD);
654
- const pending = new Map<string, unknown>([["stale", { command: "git push" }]]);
655
- const details = await runControllerOperation({
656
- operation: "reset",
657
- input: JSON.stringify({
658
- repositoryId: "repo-id",
659
- commonDirHash: "c".repeat(64),
660
- inventoryHash: "d".repeat(64),
661
- confirmation: "DESTROY REVIEW AUTHORITY repo-id",
662
- }),
663
- }, native, pending);
664
- assert.equal(details.status, "blocked");
665
- assert.equal(details.outcome, "native-input-required");
666
- assert.equal(details.native_operation, "review reclaim");
667
- assert.deepEqual(details.missing_input, ["lineage", "actor", "reason"]);
668
- assert.equal(details.mutation_performed, false);
669
- assert.equal(details.mutation_outcome, "none");
670
- assert.equal(calls.length, 0);
671
- assert.equal(pending.size, 1);
672
- });
673
-
674
- test("RESET without a native client fails closed as unavailable", async () => {
675
- const details = await runControllerOperation({
676
- operation: "reset",
677
- input: JSON.stringify({ lineage: "stuck", actor: "maintainer", reason: "incomplete" }),
678
- }, null);
679
- assert.equal(details.status, "blocked");
680
- assert.equal(details.outcome, "native-recovery-unavailable");
681
- assert.equal(details.native_operation, "review reclaim");
682
- assert.equal(details.mutation_performed, false);
683
- });
684
-
685
- // Issue #212: `authorizeDestructiveReviewOperation` classified native RECOVER as
686
- // a legacy repository-wide RESET, so a correct six-field native recovery was
687
- // refused with "Review controller recover requires an exact string repositoryId"
688
- // before it ever reached the native router. INSPECT does not publish that
689
- // quartet for compact-v2 recovery, which left the only supported flow
690
- // unreachable. Reported by @PwnLabsmx, independently reproduced on macOS by
691
- // @e-Evolution and again by the maintainer.
692
- //
693
- // Every RECOVER test below drives the registered tool rather than
694
- // `executeReviewControllerOperation`, because the defect lived in the
695
- // authorization preflight the tool runs before that function.
696
- test("RECOVER accepts the six canonical native inputs without the legacy RESET challenge", async () => {
697
- const { native, calls, statusReads } = fakeRecoveryNative(RECOVER_RECORD);
698
- const run = recoveryController(native, "gentle-pi-recover-six-field-");
699
- const prompts: string[] = [];
700
- const details = await run("recover", { operation: "recover", input: JSON.stringify(RECOVER_INPUT) }, interactiveContext(prompts));
701
- assert.equal(details.status, undefined, "the native six-field recovery contract is complete on its own");
702
- assert.equal(details.native_operation, "review recover");
703
- assert.equal(details.mutation_performed, true);
704
- assert.deepEqual(details.result, RECOVER_RECORD);
705
- assert.equal(calls.length, 1);
706
- assert.equal(calls[0]?.operation, "recover");
707
- assert.equal(calls[0]?.request.predecessorLineage, "broken");
708
- assert.equal(calls[0]?.request.expectedPredecessorRevision, "rev-1");
709
- assert.equal(calls[0]?.request.successorLineage, "successor");
710
- assert.equal(calls[0]?.request.disposition, "invalidated");
711
- // Pi derives the binding; it is never the caller's to supply.
712
- assert.equal(calls[0]?.request.maintainerAuthorization, RECOVER_AUTHORIZATION);
713
- assert.equal(prompts.length, 1);
714
- assert.match(prompts[0]!, /gentle-ai\.review-recovery-authorization\/v1/);
715
- assert.match(prompts[0]!, new RegExp(`target_identity=${RECOVER_TARGET_IDENTITY}`));
716
- assert.match(prompts[0]!, /Provider-selected disposition: invalidated/);
717
- assert.equal(prompts[0]!.includes("repositoryId"), false, "the legacy reset challenge has no place in native recovery");
718
- // Status is read before approval and read again before mutation.
719
- assert.deepEqual(statusReads, ["broken", "broken"]);
177
+ const queue = queuedAdapter([{ stdout: JSON.stringify(preflight) }, { stdout: JSON.stringify(execute) }]);
178
+ const result = await client(queue.adapter).repair({ cwd: "/repo", actor: "maintainer", reason: "repair alias", maintainerAuthorization: "approved" });
179
+ assert.equal(result.execution?.status, "committed");
180
+ assert.deepEqual(queue.calls.map((call) => call.arguments.slice(0, 8)), [
181
+ ["review", "repair", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo", "--mode", "preflight"],
182
+ ["review", "repair", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo", "--mode", "execute"],
183
+ ]);
720
184
  });
721
185
 
722
- test("RECOVER rejects a caller-supplied maintainerAuthorization before reading or prompting", async () => {
723
- const { native, calls, statusReads } = fakeRecoveryNative(RECOVER_RECORD);
724
- const run = recoveryController(native, "gentle-pi-recover-caller-authorization-");
725
- const prompts: string[] = [];
726
- const details = await run(
727
- "recover",
728
- { operation: "recover", input: JSON.stringify({ ...RECOVER_INPUT, maintainerAuthorization: RECOVER_AUTHORIZATION }) },
729
- interactiveContext(prompts),
730
- );
731
- assert.equal(details.status, "blocked");
732
- assert.equal(details.outcome, "native-recovery-caller-authorization-rejected");
733
- assert.equal(details.mutation_performed, false);
734
- assert.equal(details.mutation_outcome, "none");
735
- assert.equal(details.next_action, "resubmit-without-maintainer-authorization");
736
- assert.equal(calls.length, 0);
737
- assert.deepEqual(statusReads, []);
738
- assert.deepEqual(prompts, []);
186
+ test("native repair-legacy-alias keeps the historical fixed binding and rejects a tampered one before launch", async () => {
187
+ const request = {
188
+ cwd: "/repo",
189
+ repository: "/repo",
190
+ lineage: "historical-alias",
191
+ expectedRevision: SHA,
192
+ diagnostic: NATIVE_REVIEW_LEGACY_ALIAS_REPAIR.DIAGNOSTIC,
193
+ disposition: NATIVE_REVIEW_LEGACY_ALIAS_REPAIR.DISPOSITION,
194
+ actor: "maintainer",
195
+ reason: "repair historical alias",
196
+ };
197
+ const queue = queuedAdapter([{ stdout: JSON.stringify({ operation: "review/repair-legacy-alias", record: { schema: "gentle-ai.review-repair-audit/v1", lineage_id: request.lineage, status: "committed" } }) }]);
198
+ const result = await client(queue.adapter).repairLegacyAlias({ ...request, maintainerAuthorization: nativeReviewLegacyAliasRepairAuthorization(request) });
199
+ assert.equal(result.record.lineage_id, request.lineage);
200
+ assert.deepEqual(queue.calls[0]?.arguments.slice(0, 10), ["review", "repair-legacy-alias", "--cwd", "/repo", "--lineage", request.lineage, "--expected-revision", SHA, "--diagnostic", request.diagnostic]);
201
+ const rejected = queuedAdapter([]);
202
+ await assert.rejects(() => client(rejected.adapter).repairLegacyAlias({ ...request, maintainerAuthorization: "tampered" }), /exact repository, lineage, revision/);
203
+ assert.equal(rejected.calls.length, 0);
739
204
  });
740
205
 
741
- test("RECOVER fails closed without an interactive Pi UI", async () => {
742
- const { native, calls, statusReads } = fakeRecoveryNative(RECOVER_RECORD);
743
- const run = recoveryController(native, "gentle-pi-recover-headless-");
744
- await assert.rejects(
745
- run("recover", { operation: "recover", input: JSON.stringify(RECOVER_INPUT) }, HEADLESS_CONTEXT),
746
- /Review controller RECOVER requires fresh explicit authorization through the interactive Pi UI; headless execution fails closed/,
747
- );
748
- assert.equal(calls.length, 0);
749
- // Fresh native evidence is read, but nothing mutates without a human.
750
- assert.deepEqual(statusReads, ["broken"]);
206
+ test("native recovery operations reject malformed canonical inputs before a process can mutate", async () => {
207
+ const queue = queuedAdapter([]);
208
+ await assert.rejects(() => client(queue.adapter).recover({
209
+ cwd: "/repo", predecessorLineage: " predecessor", expectedPredecessorRevision: "revision", successorLineage: "successor", disposition: "invalidated", actor: "maintainer", reason: "reason",
210
+ }), /predecessorLineage/);
211
+ await assert.rejects(() => client(queue.adapter).reclaim({ cwd: "/repo", lineage: "lineage\n", actor: "maintainer", reason: "reason" }), /lineage/);
212
+ assert.equal(queue.calls.length, 0);
751
213
  });
752
214
 
753
- test("RECOVER refuses to mutate when the human declines the derived binding", async () => {
754
- const { native, calls } = fakeRecoveryNative(RECOVER_RECORD);
755
- const run = recoveryController(native, "gentle-pi-recover-declined-");
756
- const prompts: string[] = [];
215
+ test("partial maintenance failures preserve the provider audit record and unknown mutation outcome", async () => {
216
+ const request = {
217
+ cwd: "/repo", lineage: "abandoned", expectedRevision: "revision-1", snapshotIdentity: SHA,
218
+ capturedLensResults: ["00-risk.json"], findingsPresent: true, evidenceRecordsPresent: false, actor: "maintainer", reason: "discard candidate",
219
+ };
220
+ const queue = queuedAdapter([{ exitCode: 1, stdout: JSON.stringify({ operation: "review/abandon", record: { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: request.lineage, status: "partial" } }) }]);
757
221
  await assert.rejects(
758
- run("recover", { operation: "recover", input: JSON.stringify(RECOVER_INPUT) }, interactiveContext(prompts, false)),
759
- /Review controller RECOVER was not explicitly authorized/,
760
- );
761
- assert.equal(prompts.length, 1);
762
- assert.equal(calls.length, 0);
763
- });
764
-
765
- test("RECOVER refuses a foreign, stale, or no-longer-recoverable authority before prompting", async () => {
766
- for (const [label, status] of [
767
- ["foreign", recoveryTargetStatus("someone-elses-lineage")],
768
- ["stale", recoveryTargetStatus("broken", { authority: { lineageId: "broken", revision: "rev-2" } })],
769
- ["not recovery-eligible", recoveryTargetStatus("broken", { action: "start", actionDisposition: undefined })],
770
- ] as const) {
771
- const calls: Record<string, unknown>[] = [];
772
- const native = {
773
- async recover(request: Record<string, unknown>) { calls.push(request); return { record: RECOVER_RECORD }; },
774
- async targetStatus() { return status; },
775
- } as unknown as NativeReviewCli;
776
- const run = recoveryController(native, `gentle-pi-recover-${label.replace(/[^a-z]+/g, "-")}-`);
777
- const prompts: string[] = [];
778
- const details = await run("recover", { operation: "recover", input: JSON.stringify(RECOVER_INPUT) }, interactiveContext(prompts));
779
- assert.equal(details.status, "blocked", label);
780
- assert.equal(details.outcome, "native-recovery-status-mismatch", label);
781
- assert.equal(details.mutation_performed, false, label);
782
- assert.equal(calls.length, 0, label);
783
- assert.deepEqual(prompts, [], label);
784
- }
785
- });
786
-
787
- test("RECOVER refuses a disposition the provider did not select", async () => {
788
- const { native, calls } = fakeRecoveryNative(RECOVER_RECORD);
789
- const run = recoveryController(native, "gentle-pi-recover-disposition-");
790
- const prompts: string[] = [];
791
- const details = await run("recover", { operation: "recover", input: JSON.stringify({ ...RECOVER_INPUT, disposition: "escalated" }) }, interactiveContext(prompts));
792
- assert.equal(details.status, "blocked");
793
- assert.equal(details.outcome, "native-recovery-disposition-mismatch");
794
- assert.equal(details.provider_disposition, "invalidated");
795
- assert.equal(details.mutation_performed, false);
796
- assert.equal(calls.length, 0);
797
- assert.deepEqual(prompts, []);
798
- });
799
-
800
- // The human deliberates for an unbounded interval. An authority that advanced
801
- // while they were deciding was never the one they approved, so the approval and
802
- // its derived binding are pinned and re-checked against a fresh read.
803
- test("RECOVER refuses to mutate when the authority changes between approval and mutation", async () => {
804
- for (const [label, drifted] of [
805
- ["revision advanced", recoveryTargetStatus("broken", { authority: { lineageId: "broken", revision: "rev-2" } })],
806
- ["target identity moved", recoveryTargetStatus("broken", { targetIdentity: `sha256:${"f".repeat(64)}` })],
807
- ["disposition changed", recoveryTargetStatus("broken", { actionDisposition: "escalated" })],
808
- ["no longer recoverable", recoveryTargetStatus("broken", { action: "start", actionDisposition: undefined })],
809
- ] as const) {
810
- const calls: Record<string, unknown>[] = [];
811
- let reads = 0;
812
- const native = {
813
- async recover(request: Record<string, unknown>) { calls.push(request); return { record: RECOVER_RECORD }; },
814
- async targetStatus() {
815
- reads += 1;
816
- return reads === 1 ? recoveryTargetStatus("broken") : drifted;
817
- },
818
- } as unknown as NativeReviewCli;
819
- const run = recoveryController(native, `gentle-pi-recover-toctou-${label.replace(/[^a-z]+/g, "-")}-`);
820
- const prompts: string[] = [];
821
- const details = await run("recover", { operation: "recover", input: JSON.stringify(RECOVER_INPUT) }, interactiveContext(prompts));
822
- assert.equal(prompts.length, 1, label);
823
- assert.equal(reads, 2, label);
824
- assert.equal(details.status, "blocked", label);
825
- assert.equal(details.outcome, "native-recovery-authority-changed", label);
826
- assert.equal(details.mutation_performed, false, label);
827
- assert.equal(details.mutation_outcome, "none", label);
828
- assert.equal(details.next_action, "reinspect-and-reauthorize-recovery", label);
829
- assert.equal(calls.length, 0, label);
830
- }
831
- });
832
-
833
- test("RECOVER surfaces every missing successor input including an unsupported disposition", async () => {
834
- const { native, calls, statusReads } = fakeRecoveryNative(RECOVER_RECORD);
835
- const run = recoveryController(native, "gentle-pi-recover-missing-input-");
836
- const prompts: string[] = [];
837
- const details = await run(
838
- "recover",
839
- { operation: "recover", input: JSON.stringify({ predecessorLineage: "broken", disposition: "not-a-disposition" }) },
840
- interactiveContext(prompts),
222
+ () => client(queue.adapter).abandon({ ...request, maintainerAuthorization: nativeReviewAbandonAuthorization(request) }),
223
+ (error: unknown) => error instanceof NativeReviewCliError && error.mutationOutcome === "unknown" && error.auditRecord?.status === "partial",
841
224
  );
842
- assert.equal(details.outcome, "native-input-required");
843
- assert.equal(details.native_operation, "review recover");
844
- // The missing inputs are the native six, never the legacy reset quartet.
845
- assert.deepEqual(details.missing_input, ["expectedPredecessorRevision", "successorLineage", "disposition", "actor", "reason"]);
846
- assert.equal(calls.length, 0);
847
- assert.deepEqual(statusReads, []);
848
- assert.deepEqual(prompts, []);
849
- });
850
-
851
- test("RECONCILE_AUTHORITY routes one exact native mutation and returns its audit record", async () => {
852
- const { native, calls } = fakeRecoveryNative(RECONCILE_RECORD);
853
- const pending = new Map<string, unknown>([["stale", { command: "git push" }]]);
854
- const details = await runControllerOperation({
855
- operation: "reconcile-authority",
856
- input: JSON.stringify({
857
- predecessorLineage: "predecessor",
858
- expectedPredecessorRevision: "predecessor-revision",
859
- successorLineage: "successor",
860
- expectedSuccessorRevision: "successor-revision",
861
- actor: "maintainer",
862
- reason: "invalid recovery edge",
863
- }),
864
- }, native, pending);
865
- assert.equal(details.operation, "reconcile-authority");
866
- assert.equal(details.native_operation, "review reconcile-authority");
867
- assert.equal(details.mutation_performed, true);
868
- assert.equal(details.mutation_outcome, "committed");
869
- assert.deepEqual(details.result, RECONCILE_RECORD);
870
- assert.equal(details.next_action, "inspect");
871
- assert.equal(calls.length, 1);
872
- assert.equal(calls[0]?.operation, "reconcileAuthority");
873
- assert.equal(calls[0]?.request.expectedPredecessorRevision, "predecessor-revision");
874
- assert.equal(calls[0]?.request.expectedSuccessorRevision, "successor-revision");
875
- assert.equal(calls[0]?.request.maintainerAuthorization, RECONCILE_AUTHORIZATION);
876
- assert.equal(pending.size, 0);
877
- });
878
-
879
- test("RECONCILE_AUTHORITY requests every exact native binding before authorization or mutation", async () => {
880
- const { native, calls } = fakeRecoveryNative(RECONCILE_RECORD);
881
- const details = await runControllerOperation({ operation: "reconcile-authority", input: "{}" }, native);
882
- assert.equal(details.status, "blocked");
883
- assert.equal(details.outcome, "native-input-required");
884
- assert.deepEqual(details.missing_input, ["predecessorLineage", "expectedPredecessorRevision", "successorLineage", "expectedSuccessorRevision", "actor", "reason"]);
885
- assert.equal(details.mutation_performed, false);
886
- assert.equal(details.mutation_outcome, "none");
887
- assert.equal(calls.length, 0);
225
+ assert.equal(queue.calls.length, 1);
888
226
  });
889
227
 
890
- test("RECONCILE_AUTHORITY returns a typed fail-closed envelope for native cancellation", async () => {
891
- const native = {
892
- async reconcileAuthority() {
893
- throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.CANCELLED, "review/reconcile-authority", true, true, "native process was cancelled");
894
- },
895
- } as unknown as NativeReviewCli;
896
- const details = await runControllerOperation({
897
- operation: "reconcile-authority",
898
- input: JSON.stringify({
899
- predecessorLineage: "predecessor",
900
- expectedPredecessorRevision: "predecessor-revision",
901
- successorLineage: "successor",
902
- expectedSuccessorRevision: "successor-revision",
903
- actor: "maintainer",
904
- reason: "invalid recovery edge",
905
- }),
906
- }, native);
907
- assert.equal(details.status, "blocked");
908
- assert.equal(details.outcome, "native-operation-failed");
909
- assert.equal(details.mutation_outcome, "unknown");
910
- assert.equal(details.replayability, "status_required");
911
- assert.equal(details.next_action, "review.status");
912
- assert.deepEqual(details.diagnostics, {
913
- operation: "review/reconcile-authority",
914
- error_code: "cancelled",
915
- timed_out: false,
916
- output_limit_exceeded: false,
917
- });
228
+ test("RESET requests native reclaim inputs rather than inventing authority values", async () => {
229
+ const native = { reclaim: async () => { throw new Error("must not reclaim"); } } as unknown as import("../lib/native-review-cli.ts").NativeReviewCli;
230
+ const result = await __testing.executeReviewControllerOperation({ operation: "reset", input: JSON.stringify({ lineage: "only-lineage" }) }, process.cwd(), native);
231
+ assert.equal(result.outcome, "native-input-required");
232
+ assert.deepEqual(result.missing_input, ["actor", "reason"]);
233
+ assert.equal(result.mutation_outcome, "none");
918
234
  });
919
235
 
920
- test("RECONCILE_AUTHORITY relays a partial-failure audit record without weakening status reconciliation", async () => {
236
+ test("RECOVER rejects caller-authored authorization before status, UI, or native mutation", async () => {
237
+ let statusCalls = 0;
921
238
  const native = {
922
- async reconcileAuthority() {
923
- throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, "review/reconcile-authority", true, true, "partial failure", undefined, RECONCILE_RECORD);
924
- },
925
- } as unknown as NativeReviewCli;
926
- const details = await runControllerOperation({
927
- operation: "reconcile-authority",
928
- input: JSON.stringify({ predecessorLineage: "predecessor", expectedPredecessorRevision: "predecessor-revision", successorLineage: "successor", expectedSuccessorRevision: "successor-revision", actor: "maintainer", reason: "invalid recovery edge" }),
929
- }, native);
930
- assert.equal(details.mutation_outcome, "unknown");
931
- assert.equal(details.next_action, "review.status");
932
- assert.deepEqual(details.native_audit_record, RECONCILE_RECORD);
933
- });
934
-
935
- test("RECOVER_LOCK still requires the exact ownerHash before routing to native reclaim", async () => {
936
- const { native, calls } = fakeRecoveryNative(RECLAIM_RECORD);
239
+ targetStatus: async () => { statusCalls += 1; throw new Error("must not read status"); },
240
+ } as unknown as import("../lib/native-review-cli.ts").NativeReviewCli;
241
+ const result = await __testing.executeReviewControllerOperation({
242
+ operation: "recover",
243
+ input: JSON.stringify({ predecessorLineage: "predecessor", expectedPredecessorRevision: "revision", successorLineage: "successor", disposition: "invalidated", actor: "maintainer", reason: "recover", maintainerAuthorization: "caller-authored" }),
244
+ }, process.cwd(), native);
245
+ assert.equal(result.outcome, "native-recovery-caller-authorization-rejected");
246
+ assert.equal(statusCalls, 0);
247
+ assert.equal(result.mutation_outcome, "none");
248
+ });
249
+
250
+ test("RECONCILE_AUTHORITY returns exact missing-input guidance before native mutation", async () => {
251
+ const native = { reconcileAuthority: async () => { throw new Error("must not reconcile"); } } as unknown as import("../lib/native-review-cli.ts").NativeReviewCli;
252
+ const result = await __testing.executeReviewControllerOperation({ operation: "reconcile-authority", input: JSON.stringify({ predecessorLineage: "predecessor" }) }, process.cwd(), native);
253
+ assert.equal(result.outcome, "native-input-required");
254
+ assert.deepEqual(result.missing_input, ["expectedPredecessorRevision", "successorLineage", "expectedSuccessorRevision", "actor", "reason"]);
255
+ assert.equal(result.mutation_outcome, "none");
256
+ });
257
+
258
+ test("native abandon rejects a legacy authorization before process launch", async () => {
259
+ const request = {
260
+ cwd: "/repo",
261
+ lineage: "abandoned",
262
+ expectedRevision: "revision-1",
263
+ snapshotIdentity: SHA,
264
+ capturedLensResults: ["00-risk.json"],
265
+ findingsPresent: true,
266
+ evidenceRecordsPresent: false,
267
+ actor: "maintainer",
268
+ reason: "discard candidate",
269
+ };
270
+ const queue = queuedAdapter([]);
937
271
  await assert.rejects(
938
- runControllerOperation({ operation: "recover-lock", input: JSON.stringify({ lineage: "stuck", actor: "maintainer", reason: "stale lock" }) }, native),
939
- /ownerHash/,
272
+ () => client(queue.adapter).abandon({ ...request, maintainerAuthorization: "gentle-ai.review-abandon-authorization/v1" }),
273
+ /exact lineage, revision, snapshot, reason, discarded-work, and actor binding/,
940
274
  );
941
- assert.equal(calls.length, 0);
942
- const details = await runControllerOperation({
943
- operation: "recover-lock",
944
- input: JSON.stringify({ ownerHash: "a".repeat(64), lineage: "stuck", actor: "maintainer", reason: "stale lock" }),
945
- }, native);
946
- assert.equal(details.native_operation, "review reclaim");
947
- assert.equal(details.mutation_performed, true);
948
- assert.deepEqual(details.result, RECLAIM_RECORD);
949
- assert.equal(calls.length, 1);
950
- assert.equal(calls[0]?.operation, "reclaim");
951
- assert.equal(calls[0]?.request.lineage, "stuck");
275
+ assert.equal(queue.calls.length, 0);
952
276
  });
953
277
 
954
- test("RECOVER_LOCK without the native reclaim inputs requests them explicitly", async () => {
955
- const { native, calls } = fakeRecoveryNative(RECLAIM_RECORD);
956
- const details = await runControllerOperation({
957
- operation: "recover-lock",
958
- input: JSON.stringify({ ownerHash: "a".repeat(64) }),
959
- }, native);
960
- assert.equal(details.outcome, "native-input-required");
961
- assert.deepEqual(details.missing_input, ["lineage", "actor", "reason"]);
962
- assert.equal(calls.length, 0);
963
- });
278
+ function recoveryStatus(options: {
279
+ lineageId?: string;
280
+ revision?: string;
281
+ targetIdentity?: string;
282
+ disposition?: "scope_changed" | "invalidated" | "escalated";
283
+ action?: "recover" | "start";
284
+ } = {}) {
285
+ return {
286
+ action: options.action ?? "recover",
287
+ actionDisposition: options.disposition ?? "invalidated",
288
+ authority: { lineageId: options.lineageId ?? "predecessor", revision: options.revision ?? "revision-1" },
289
+ targetIdentity: options.targetIdentity ?? SHA,
290
+ raw: { schema: "gentle-ai.review-integration.status/v5" },
291
+ } as unknown as import("../lib/review-integration-v2.ts").ReviewStatusV3;
292
+ }
964
293
 
965
- test("destructive RESET still fails closed without fresh interactive authorization", async () => {
966
- const tools = new Map<string, { execute: (id: string, params: unknown, signal: undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<unknown> }>();
967
- const pi = {
294
+ function registeredController(nativeReviewCli: import("../lib/native-review-cli.ts").NativeReviewCli) {
295
+ const tools = new Map<string, { execute: (toolCallId: string, params: unknown, signal: AbortSignal | undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<{ details?: unknown }> }>();
296
+ createGentleAiExtension({ nativeReviewCli })({
968
297
  on() {},
969
- registerTool(definition: { name: string; execute: never }) {
970
- tools.set(definition.name, definition as unknown as { execute: (id: string, params: unknown, signal: undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<unknown> });
971
- },
298
+ registerTool(definition: { name: string; execute: (toolCallId: string, params: unknown, signal: AbortSignal | undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<{ details?: unknown }> }) { tools.set(definition.name, definition); },
972
299
  registerCommand() {},
973
- } as unknown as ExtensionAPI;
974
- const { native, calls } = fakeRecoveryNative(RECLAIM_RECORD);
975
- createGentleAiExtension({ nativeReviewCli: native })(pi);
300
+ } as unknown as ExtensionAPI);
976
301
  const controller = tools.get("gentle_review");
977
302
  assert.ok(controller);
978
- const cwd = scratchDir("gentle-pi-native-recovery-headless-");
979
- const ctx = { cwd, hasUI: false, ui: { confirm: async () => true } } as unknown as ExtensionContext;
980
- await assert.rejects(
981
- controller.execute("headless-reset", {
982
- operation: "reset",
983
- input: JSON.stringify({
984
- repositoryId: "repo-id",
985
- commonDirHash: "c".repeat(64),
986
- inventoryHash: "d".repeat(64),
987
- confirmation: "DESTROY REVIEW AUTHORITY repo-id",
988
- lineage: "stuck",
989
- actor: "maintainer",
990
- reason: "incomplete",
991
- }),
992
- }, undefined, undefined, ctx),
993
- /interactive Pi UI.*fails closed/i,
994
- );
995
- assert.equal(calls.length, 0);
996
- });
303
+ return controller;
304
+ }
997
305
 
998
- // Regression guard for issue #212: separating RECOVER out must leave the legacy
999
- // repository-wide RESET challenge exactly as it was.
1000
- test("RESET still demands the complete legacy repository challenge and its own fresh approval", async () => {
1001
- const tools = new Map<string, { execute: (id: string, params: unknown, signal: undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<unknown> }>();
1002
- const pi = { on() {}, registerTool(definition: { name: string; execute: never }) { tools.set(definition.name, definition as never); }, registerCommand() {} } as unknown as ExtensionAPI;
1003
- const { native, calls } = fakeRecoveryNative(RECLAIM_RECORD);
1004
- createGentleAiExtension({ nativeReviewCli: native })(pi);
1005
- const controller = tools.get("gentle_review");
1006
- assert.ok(controller);
1007
- const cwd = scratchDir("gentle-pi-native-reset-challenge-");
1008
- const challenge = {
1009
- repositoryId: "repo-id",
1010
- commonDirHash: "c".repeat(64),
1011
- inventoryHash: "d".repeat(64),
1012
- confirmation: "DESTROY REVIEW AUTHORITY repo-id",
1013
- };
1014
- const reclaim = { lineage: "stuck", actor: "maintainer", reason: "incomplete" };
1015
- for (const key of ["repositoryId", "commonDirHash", "inventoryHash", "confirmation"] as const) {
1016
- const incomplete: Record<string, unknown> = { ...challenge, ...reclaim };
1017
- delete incomplete[key];
1018
- await assert.rejects(
1019
- controller.execute(`reset-missing-${key}`, { operation: "reset", input: JSON.stringify(incomplete) }, undefined, undefined, {
1020
- cwd, hasUI: true, ui: { confirm: async () => true },
1021
- } as unknown as ExtensionContext),
1022
- new RegExp(`Review controller reset requires an exact string ${key}`),
1023
- );
1024
- }
1025
- assert.equal(calls.length, 0);
1026
- let prompt = "";
1027
- const approved = await controller.execute("approved-reset", { operation: "reset", input: JSON.stringify({ ...challenge, ...reclaim }) }, undefined, undefined, {
1028
- cwd,
1029
- hasUI: true,
1030
- ui: { confirm: async (_title: string, message: string) => { prompt = message; return true; } },
1031
- } as unknown as ExtensionContext) as { details: Record<string, unknown> };
1032
- assert.match(prompt, /Repository: repo-id/);
1033
- assert.match(prompt, /Exact challenge: DESTROY REVIEW AUTHORITY repo-id/);
1034
- assert.match(prompt, /This invalidates all prior review authority for this repository\./);
1035
- assert.equal(approved.details.mutation_performed, true);
1036
- assert.equal(calls.length, 1);
1037
- assert.equal(calls[0]?.operation, "reclaim");
1038
- });
306
+ function interactiveContext(confirm: boolean): ExtensionContext {
307
+ return { cwd: process.cwd(), hasUI: true, ui: { confirm: async () => confirm } } as unknown as ExtensionContext;
308
+ }
1039
309
 
1040
- test("RECONCILE_AUTHORITY requires fresh Pi approval for the exact seven-line binding", async () => {
1041
- const tools = new Map<string, { execute: (id: string, params: unknown, signal: undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<unknown> }>();
1042
- const pi = {
1043
- on() {},
1044
- registerTool(definition: { name: string; execute: never }) { tools.set(definition.name, definition as never); },
1045
- registerCommand() {},
1046
- } as unknown as ExtensionAPI;
1047
- const { native, calls } = fakeRecoveryNative(RECONCILE_RECORD);
1048
- createGentleAiExtension({ nativeReviewCli: native })(pi);
1049
- const controller = tools.get("gentle_review");
1050
- assert.ok(controller);
1051
- const cwd = scratchDir("gentle-pi-native-reconcile-authorization-");
1052
- const parameters = {
1053
- operation: "reconcile-authority",
1054
- input: JSON.stringify({
1055
- predecessorLineage: "predecessor",
1056
- expectedPredecessorRevision: "predecessor-revision",
1057
- successorLineage: "successor",
1058
- expectedSuccessorRevision: "successor-revision",
1059
- actor: "maintainer",
1060
- reason: "invalid recovery edge",
1061
- }),
310
+ test("maintenance cancellation preserves the exact signal and unknown mutation outcome", async () => {
311
+ const controller = new AbortController();
312
+ const request = {
313
+ cwd: "/repo", lineage: "abandoned", expectedRevision: "revision-1", snapshotIdentity: SHA,
314
+ capturedLensResults: ["00-risk.json"], findingsPresent: true, evidenceRecordsPresent: false,
315
+ actor: "maintainer", reason: "discard candidate",
316
+ };
317
+ const adapter: ExecFileAdapter = async (call) => {
318
+ assert.equal(call.signal, controller.signal);
319
+ const error = new Error("cancelled");
320
+ error.name = "AbortError";
321
+ throw error;
1062
322
  };
1063
323
  await assert.rejects(
1064
- controller.execute("headless-reconcile", parameters, undefined, undefined, { cwd, hasUI: false, ui: { confirm: async () => true } } as unknown as ExtensionContext),
1065
- /interactive Pi UI.*fails closed/i,
324
+ () => client(adapter).abandon({ ...request, maintainerAuthorization: nativeReviewAbandonAuthorization(request), signal: controller.signal }),
325
+ (error: unknown) => error instanceof NativeReviewCliError && error.code === "cancelled" && error.mutationOutcome === "unknown" && error.nextAction === "review.status",
1066
326
  );
1067
- let prompt = "";
1068
- const approved = await controller.execute("approved-reconcile", parameters, undefined, undefined, {
1069
- cwd,
1070
- hasUI: true,
1071
- ui: { confirm: async (_title: string, message: string) => { prompt = message; return true; } },
1072
- } as unknown as ExtensionContext) as { details: Record<string, unknown> };
1073
- assert.match(prompt, /predecessor_revision=predecessor-revision/);
1074
- assert.match(prompt, /successor_revision=successor-revision/);
1075
- assert.equal(approved.details.mutation_outcome, "committed");
1076
- assert.equal(calls.length, 1);
1077
- });
1078
-
1079
- test("published maintenance controller actions require exact inputs and fresh UI approval", async () => {
1080
- const tools = new Map<string, { execute: (id: string, params: unknown, signal: undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<unknown> }>();
1081
- const pi = { on() {}, registerTool(definition: { name: string; execute: never }) { tools.set(definition.name, definition as never); }, registerCommand() {} } as unknown as ExtensionAPI;
1082
- const { native, calls } = fakeRecoveryNative(ABANDON_RECORD);
1083
- createGentleAiExtension({ nativeReviewCli: native })(pi);
1084
- const controller = tools.get("gentle_review");
1085
- assert.ok(controller);
1086
- const cwd = scratchDir("gentle-pi-v219-maintenance-");
1087
- const abandon = { operation: "abandon", input: JSON.stringify({ lineage: "pristine", expectedRevision: "revision", snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", ...ABANDON_DISCARDED_WORK, actor: "maintainer", reason: "retire pristine lineage" }) };
1088
- await assert.rejects(controller.execute("headless-abandon", abandon, undefined, undefined, { cwd, hasUI: false, ui: { confirm: async () => true } } as unknown as ExtensionContext), /interactive Pi UI.*fails closed/i);
1089
- await assert.rejects(controller.execute("denied-abandon", abandon, undefined, undefined, { cwd, hasUI: true, ui: { confirm: async () => false } } as unknown as ExtensionContext), /not explicitly authorized/);
1090
- let abandonPrompt = "";
1091
- const approved = await controller.execute("approved-abandon", abandon, undefined, undefined, { cwd, hasUI: true, ui: { confirm: async (_title: string, message: string) => { abandonPrompt = message; return true; } } } as unknown as ExtensionContext) as { details: Record<string, unknown> };
1092
- assert.equal(approved.details.mutation_outcome, "committed");
1093
- assert.match(abandonPrompt, /gentle-ai\.review-abandon-authorization\/v2/);
1094
- assert.match(abandonPrompt, /captured_lens_results=00-risk\.json,01-readability\.json/);
1095
- assert.match(abandonPrompt, /findings_present=true\nevidence_records_present=false\nactor=maintainer/);
1096
- assert.equal(calls[0]?.operation, "abandon");
1097
- assert.equal((calls[0]?.request as { maintainerAuthorization?: string }).maintainerAuthorization, ABANDON_AUTHORIZATION);
1098
- assert.deepEqual(calls[0]?.request.capturedLensResults, ["00-risk.json", "01-readability.json"]);
1099
- assert.equal(calls[0]?.request.findingsPresent, true);
1100
- assert.equal(calls[0]?.request.evidenceRecordsPresent, false);
1101
- const missingDiscarded = await runControllerOperation({ operation: "abandon", input: JSON.stringify({ lineage: "pristine", expectedRevision: "revision", snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", actor: "maintainer", reason: "retire pristine lineage" }) }, native);
1102
- assert.equal(missingDiscarded.outcome, "native-input-required");
1103
- assert.deepEqual(missingDiscarded.missing_input, ["capturedLensResults", "findingsPresent", "evidenceRecordsPresent"]);
1104
- const invalidDiscarded = await runControllerOperation({ operation: "abandon", input: JSON.stringify({ lineage: "pristine", expectedRevision: "revision", snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", capturedLensResults: ["00-risk.json", " padded "], findingsPresent: true, evidenceRecordsPresent: false, actor: "maintainer", reason: "retire pristine lineage" }) }, native);
1105
- assert.equal(invalidDiscarded.outcome, "native-input-required");
1106
- assert.deepEqual(invalidDiscarded.missing_input, ["capturedLensResults"]);
1107
- const legacy = await controller.execute("approved-legacy-quarantine", { operation: "quarantine-legacy", input: JSON.stringify({ repository: "/repo", lineage: "legacy", expectedRevision: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", diagnostic: LEGACY_FREEZE_DIAGNOSTIC, disposition: LEGACY_FREEZE_DISPOSITION, actor: "maintainer", reason: "quarantine malformed legacy freeze" }) }, undefined, undefined, { cwd, hasUI: true, ui: { confirm: async () => true } } as unknown as ExtensionContext) as { details: Record<string, unknown> };
1108
- assert.equal(legacy.details.mutation_outcome, "committed");
1109
- assert.equal(calls[1]?.operation, "quarantineLegacy");
1110
- for (const input of [
1111
- { operation: "quarantine-legacy", input: JSON.stringify({ repository: "/repo", lineage: "legacy", expectedRevision: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", diagnostic: "unsupported historical v1 operation alias", disposition: LEGACY_FREEZE_DISPOSITION, actor: "maintainer", reason: "no-op" }) },
1112
- { operation: "reconcile-authority", input: JSON.stringify({ predecessorLineage: "predecessor", expectedPredecessorRevision: "predecessor-revision", successorLineage: "successor", expectedSuccessorRevision: "successor-revision", actor: "maintainer", reason: "no-op", anomalies: "malformed_recovery_authorization,unchanged_target" }) },
1113
- ]) {
1114
- const details = await runControllerOperation(input, native);
1115
- assert.equal(details.outcome, "native-input-invalid");
1116
- }
1117
- assert.equal(calls.length, 2);
1118
- });
1119
-
1120
- test("REPAIR_LEGACY_ALIAS derives fixed inputs from fresh inventory and requires fresh UI approval", async () => {
1121
- const tools = new Map<string, { execute: (id: string, params: unknown, signal: undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<unknown> }>();
1122
- const pi = { on() {}, registerTool(definition: { name: string; execute: never }) { tools.set(definition.name, definition as never); }, registerCommand() {} } as unknown as ExtensionAPI;
1123
- const calls: Record<string, unknown>[] = [];
1124
- const native = {
1125
- async reviewStatus() {
1126
- return {
1127
- repository: "/canonical/repository",
1128
- complete: true,
1129
- entries: [{ version: "legacy-v1", status: "invalid", lineageId: "legacy-alias", revision: `sha256:${"c".repeat(64)}`, problems: [LEGACY_ALIAS_DIAGNOSTIC] }],
1130
- };
1131
- },
1132
- async repairLegacyAlias(request: Record<string, unknown>) {
1133
- calls.push(request);
1134
- return { record: LEGACY_ALIAS_RECORD };
1135
- },
1136
- } as unknown as NativeReviewCli;
1137
- createGentleAiExtension({ nativeReviewCli: native })(pi);
1138
- const controller = tools.get("gentle_review");
1139
- assert.ok(controller);
1140
- const cwd = scratchDir("gentle-pi-v2110-alias-repair-");
1141
- const parameters = { operation: "repair-legacy-alias", input: JSON.stringify({ lineage: "legacy-alias", actor: "maintainer", reason: "quarantine approved historical alias" }) };
327
+ const reconcile = {
328
+ cwd: "/repo", predecessorLineage: "predecessor", expectedPredecessorRevision: "revision-1",
329
+ successorLineage: "successor", expectedSuccessorRevision: "revision-2", actor: "maintainer", reason: "repair edge",
330
+ };
1142
331
  await assert.rejects(
1143
- controller.execute("headless-alias-repair", parameters, undefined, undefined, { cwd, hasUI: false, ui: { confirm: async () => true } } as unknown as ExtensionContext),
1144
- /interactive Pi UI.*fails closed/i,
332
+ () => client(adapter).reconcileAuthority({ ...reconcile, maintainerAuthorization: nativeReviewReconcileAuthorization(reconcile), signal: controller.signal }),
333
+ (error: unknown) => error instanceof NativeReviewCliError && error.code === "cancelled" && error.mutationOutcome === "unknown" && error.nextAction === "review.status",
1145
334
  );
1146
- assert.equal(calls.length, 0);
1147
- let prompt = "";
1148
- const approved = await controller.execute("approved-alias-repair", parameters, undefined, undefined, {
1149
- cwd,
1150
- hasUI: true,
1151
- ui: { confirm: async (_title: string, message: string) => { prompt = message; return true; } },
1152
- } as unknown as ExtensionContext) as { details: Record<string, unknown> };
1153
- assert.match(prompt, /repository=\/canonical\/repository/);
1154
- assert.match(prompt, /revision=sha256:c{64}/);
1155
- assert.equal(approved.details.mutation_outcome, "committed");
1156
- assert.equal(calls[0]?.repository, "/canonical/repository");
1157
- assert.equal(calls[0]?.diagnostic, LEGACY_ALIAS_DIAGNOSTIC);
1158
- assert.equal(calls[0]?.disposition, LEGACY_ALIAS_DISPOSITION);
1159
- const injected = await runControllerOperation({ operation: "repair-legacy-alias", input: JSON.stringify({ lineage: "legacy-alias", actor: "maintainer", reason: "no-op", repository: "/attacker" }) }, native);
1160
- assert.equal(injected.outcome, "native-input-invalid");
1161
- await assert.rejects(runControllerOperation({ operation: "dispose-result", input: "{}" }, native), /operation/);
1162
- });
1163
-
1164
- // The capture-result gap, found by benchmarking Pi's client against the real
1165
- // binary: `finalize()` emitted `--result <file>` per lens, a flag gentle-ai
1166
- // retired because "a reviewer result supplied this way carries no
1167
- // provider-owned admission, so it cannot prove the lens inspected the frozen
1168
- // candidate". There was no capture-result surface at all, so Pi could only
1169
- // finalize a zero-lens low-risk candidate. The suite never caught it because
1170
- // it mocks the finalize response.
1171
- //
1172
- // Two inverse contracts meet here. `repair()` above MUST carry --contract.
1173
- // `capture-result` MUST NOT: it is an additive headless command, not a
1174
- // negotiated repository operation, and the provider's own tokens already
1175
- // carry the repository context -- it accepts that or --cwd, never both. So
1176
- // Pi passes the transition's tokens through verbatim and adds only --input.
1177
- test("captureResult passes the provider tokens through verbatim and carries no --contract", async (t) => {
1178
- t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
1179
- // The full live envelope shape, confirmed against real capture-result runs
1180
- // on both the pinned line and gentle-ai main (tests/fixtures/devbinary/
1181
- // result-artifact-v2.captured.json): the opaque locator prefix is rart1_.
1182
- const manifest = {
1183
- schema: "gentle-ai.review-result-artifact/v2",
1184
- capability: "review.native_result_artifact",
1185
- reference: "rart1_" + "b".repeat(64),
1186
- sha256: "sha256:" + "f".repeat(64),
1187
- lineage_id: "review-1d5aadacc600e167",
1188
- target_identity: "sha256:" + "d".repeat(64),
1189
- lens: "review-reliability",
1190
- selected_order: 0,
1191
- subject_hash: "sha256:" + "a".repeat(64),
1192
- admission_decision: "completed",
1193
- };
1194
- const { adapter, calls } = queuedAdapter([{ stdout: JSON.stringify(manifest) }]);
1195
- const cli = new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
1196
-
1197
- const tokens = [
1198
- "--lineage=review-1d5aadacc600e167",
1199
- "--expected-revision=sha256:" + "c".repeat(64),
1200
- "--target=sha256:" + "d".repeat(64),
1201
- "--repository-context=rctx1_" + "e".repeat(64),
1202
- "--lens=review-reliability",
1203
- "--order=0",
1204
- "--subject-hash=" + manifest.subject_hash,
1205
- ];
1206
- const captured = await cli.captureResult({ argumentTokens: tokens, resultDocument: JSON.stringify({ subject_hash: manifest.subject_hash, inspection: { status: "completed", paths: ["a.ts"] }, findings: [], evidence: ["reviewed the complete frozen candidate scope"] }) });
1207
-
1208
- assert.equal(captured.subjectHash, manifest.subject_hash);
1209
- assert.equal(captured.admissionDecision, "completed");
1210
-
1211
- // Exactly one invocation: capture-result is headless and never negotiates,
1212
- // so it must not drag a capabilities preflight along with it.
1213
- assert.equal(calls.length, 1);
1214
- const argv = calls[0]!.arguments;
1215
- assert.deepEqual(argv.slice(0, 2), ["review", "capture-result"]);
1216
- assert.equal(argv.includes("--contract"), false, "capture-result accepts no --contract");
1217
- assert.equal(argv.includes("--cwd"), false, "the provider tokens already carry the repository context");
1218
- // Tokens pass through in order, untouched, and --input is the only addition.
1219
- assert.deepEqual(argv.slice(2, 2 + tokens.length), tokens);
1220
- assert.equal(argv.at(-2), "--input");
1221
- assert.match(argv.at(-1) as string, /\S/);
1222
- assert.equal(argv.length, 2 + tokens.length + 2);
1223
335
  });
1224
336
 
1225
- // The admission answer routes through the exact-identity forward decoder:
1226
- // a manifest missing its binding fields (the shape no real binary ever
1227
- // emitted) or carrying a foreign locator prefix is refused, never partially
1228
- // consumed. This is the decoder-freshness discipline reaching the consumer.
1229
- test("captureResult refuses an under-specified or foreign-locator manifest", async (t) => {
1230
- t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
1231
- const full = {
1232
- schema: "gentle-ai.review-result-artifact/v2",
1233
- capability: "review.native_result_artifact",
1234
- reference: "rart1_" + "b".repeat(64),
1235
- sha256: "sha256:" + "f".repeat(64),
1236
- lineage_id: "review-1d5aadacc600e167",
1237
- target_identity: "sha256:" + "d".repeat(64),
1238
- lens: "review-reliability",
1239
- selected_order: 0,
1240
- subject_hash: "sha256:" + "a".repeat(64),
1241
- admission_decision: "completed",
1242
- };
1243
- const legacyPartial = {
1244
- schema: full.schema,
1245
- capability: full.capability,
1246
- subject_hash: full.subject_hash,
1247
- admission_decision: full.admission_decision,
1248
- lens: full.lens,
1249
- reference: full.reference,
1250
- };
1251
- for (const body of [
1252
- legacyPartial,
1253
- { ...full, reference: "rref1_" + "b".repeat(64) },
1254
- { ...full, unadvertised: true },
337
+ test("RECOVER derives a provider-bound authorization, rechecks it, and fails closed on every drift", async () => {
338
+ const requests: Array<Record<string, unknown>> = [];
339
+ const native = {
340
+ targetStatus: async () => recoveryStatus(),
341
+ recover: async (request: Record<string, unknown>) => { requests.push(request); return { record: { schema: "gentle-ai.review-recovery/v1", lineage: "successor" } }; },
342
+ } as unknown as import("../lib/native-review-cli.ts").NativeReviewCli;
343
+ const input = { predecessorLineage: "predecessor", expectedPredecessorRevision: "revision-1", successorLineage: "successor", disposition: "invalidated", actor: "maintainer", reason: "recover authority" };
344
+ const result = await __testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify(input) }, process.cwd(), native, undefined, undefined, interactiveContext(true));
345
+ assert.equal(result.mutation_outcome, "committed");
346
+ assert.equal(requests.length, 1);
347
+ assert.match(String(requests[0]?.maintainerAuthorization), /^gentle-ai\.review-recovery-authorization\/v1\npredecessor_lineage=predecessor\npredecessor_revision=revision-1\ntarget_identity=/);
348
+
349
+ const wrongDisposition = await __testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify({ ...input, disposition: "escalated" }) }, process.cwd(), native, undefined, undefined, interactiveContext(true));
350
+ assert.equal(wrongDisposition.outcome, "native-recovery-disposition-mismatch");
351
+ assert.equal(wrongDisposition.mutation_outcome, "none");
352
+
353
+ for (const status of [
354
+ recoveryStatus({ lineageId: "foreign" }),
355
+ recoveryStatus({ revision: "revision-2" }),
356
+ recoveryStatus({ action: "start" }),
1255
357
  ]) {
1256
- const cli = new NativeReviewCliV216(async () => ({ stdout: JSON.stringify(body), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false }), "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
1257
- await assert.rejects(cli.captureResult({
1258
- argumentTokens: ["--lineage=review-1d5aadacc600e167", "--repository-context=rctx1_" + "e".repeat(64)],
1259
- resultDocument: JSON.stringify({ subject_hash: full.subject_hash, inspection: { status: "completed", paths: ["a.ts"] }, findings: [], evidence: ["reviewed the complete frozen candidate scope"] }),
1260
- }));
358
+ const blocked = await __testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify(input) }, process.cwd(), {
359
+ targetStatus: async () => status,
360
+ recover: async () => { throw new Error("must not recover"); },
361
+ } as unknown as import("../lib/native-review-cli.ts").NativeReviewCli, undefined, undefined, interactiveContext(true));
362
+ assert.equal(blocked.outcome, "native-recovery-status-mismatch");
363
+ assert.equal(blocked.mutation_outcome, "none");
1261
364
  }
1262
- });
1263
365
 
1264
- test("captureEvidence stages exact bytes, uses the closed outcome argv, and decodes the native record", async (t) => {
1265
- t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
1266
- const capabilities = v2Fixture<Record<string, unknown>>("capabilities.fixture.json");
1267
- const capabilitiesBody = { ...capabilities, package: { ...(capabilities.package as Record<string, unknown>), version: GENTLE_AI_VERSION } };
1268
- const record = {
1269
- schema: "gentle-ai.review-verification-evidence/v2",
1270
- version: 2,
1271
- lineage_id: "review-evidence-lineage",
1272
- authority_revision: `sha256:${"a".repeat(64)}`,
1273
- target_identity: `sha256:${"b".repeat(64)}`,
1274
- candidate_tree: "c".repeat(40),
1275
- paths_digest: `sha256:${"d".repeat(64)}`,
1276
- paths: ["app.ts"],
1277
- ledger_ids: [],
1278
- raw_payload_sha256: `sha256:${"e".repeat(64)}`,
1279
- raw_payload_bytes: 24,
1280
- outcome: "verification_failed",
1281
- record_digest: `sha256:${"f".repeat(64)}`,
1282
- };
1283
- let staged = "";
1284
- const calls: Array<{ arguments: readonly string[] }> = [];
1285
- const cli = new NativeReviewCliV216(async (request) => {
1286
- calls.push({ arguments: request.arguments });
1287
- if (request.arguments[1] === "capabilities") return { stdout: JSON.stringify(capabilitiesBody), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
1288
- const inputIndex = request.arguments.indexOf("--input");
1289
- assert.ok(inputIndex >= 0);
1290
- staged = readFileSync(request.arguments[inputIndex + 1]!, "utf8");
1291
- return { stdout: JSON.stringify(record), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
1292
- }, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
1293
- const evidence = "focused verification failed\n";
1294
- const captured = await cli.captureEvidence({
1295
- cwd: "/repo",
1296
- lineageId: record.lineage_id,
1297
- targetIdentity: record.target_identity,
1298
- expectedRevision: record.authority_revision,
1299
- outcome: "verification_failed",
1300
- evidenceDocument: evidence,
1301
- });
1302
- assert.equal(staged, evidence);
1303
- assert.equal(captured.recordDigest, record.record_digest);
1304
- assert.equal(captured.outcome, "verification_failed");
1305
- const argv = calls[1]!.arguments;
1306
- assert.deepEqual(argv.slice(0, 2), ["review", "capture-evidence"]);
1307
- assert.equal(argv.includes("--contract"), false);
1308
- assert.equal(argv[argv.indexOf("--outcome") + 1], "verification_failed");
1309
- await assert.rejects(
1310
- cli.captureEvidence({ cwd: "/repo", lineageId: record.lineage_id, targetIdentity: record.target_identity, expectedRevision: record.authority_revision, outcome: "failed" as never, evidenceDocument: evidence }),
1311
- /outcome must be passed, verification_failed, or procedural_tooling_failed/,
1312
- );
1313
- assert.equal(calls.length, 2, "outside-domain outcomes must fail before another native launch");
1314
- });
1315
-
1316
- test("negotiated finalize never emits the retired --result flag", async (t) => {
1317
- t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
1318
- const capabilities = v2Fixture<Record<string, unknown>>("capabilities.fixture.json");
1319
- const capabilitiesBody = { ...capabilities, package: { ...(capabilities.package as Record<string, unknown>), version: GENTLE_AI_VERSION } };
1320
- const finalizeBody = {
1321
- schema: "gentle-ai.review-integration.operation/v2",
1322
- contract: "gentle-ai.review-integration/v2",
1323
- operation: "review.finalize",
1324
- result: { operation: "review/finalize", lineage_id: "review-1d5aadacc600e167", state: "approved", action: "validate delivery", store_revision: "sha256:" + "f".repeat(64) },
1325
- };
1326
- const { adapter, calls } = queuedAdapter([
1327
- { stdout: JSON.stringify(capabilitiesBody) },
1328
- { stdout: JSON.stringify(finalizeBody) },
1329
- ]);
1330
- const cli = new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
1331
-
1332
- await cli.finalize({ cwd: "/repo", lineageId: "review-1d5aadacc600e167", capturedResults: true });
1333
-
1334
- const argv = calls[1]!.arguments;
1335
- assert.equal(argv.includes("--result"), false, "--result is retired; results reach authority through capture-result");
1336
- assert.ok(argv.some((token) => token === "--captured-results" || token.startsWith("--captured-results=")), "finalize must tell the provider to discover the captured results");
1337
- assert.ok(argv.includes("--contract"), "finalize IS negotiated, unlike capture-result");
366
+ let reads = 0;
367
+ const changed = await __testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify(input) }, process.cwd(), {
368
+ targetStatus: async () => (++reads === 1 ? recoveryStatus() : recoveryStatus({ targetIdentity: `sha256:${"b".repeat(64)}` })),
369
+ recover: async () => { throw new Error("must not recover"); },
370
+ } as unknown as import("../lib/native-review-cli.ts").NativeReviewCli, undefined, undefined, interactiveContext(true));
371
+ assert.equal(changed.outcome, "native-recovery-authority-changed");
372
+ assert.equal(changed.mutation_outcome, "none");
373
+ });
374
+
375
+ test("RECOVER, RESET, and RECONCILE keep provider inputs and failures authority-scoped", async () => {
376
+ const recoverMissing = await __testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify({ predecessorLineage: "predecessor", disposition: "not-a-disposition" }) }, process.cwd(), {
377
+ recover: async () => { throw new Error("must not recover"); },
378
+ } as unknown as import("../lib/native-review-cli.ts").NativeReviewCli);
379
+ assert.deepEqual(recoverMissing.missing_input, ["expectedPredecessorRevision", "successorLineage", "disposition", "actor", "reason"]);
380
+
381
+ const resetUnavailable = await __testing.executeReviewControllerOperation({ operation: "reset", input: JSON.stringify({ lineage: "stuck", actor: "maintainer", reason: "incomplete" }) }, process.cwd(), null);
382
+ assert.equal(resetUnavailable.outcome, "native-recovery-unavailable");
383
+
384
+ const reconciliation = { predecessorLineage: "predecessor", expectedPredecessorRevision: "revision-1", successorLineage: "successor", expectedSuccessorRevision: "revision-2", actor: "maintainer", reason: "repair edge" };
385
+ const completed = await __testing.executeReviewControllerOperation({ operation: "reconcile-authority", input: JSON.stringify(reconciliation) }, process.cwd(), {
386
+ reconcileAuthority: async (request: Record<string, unknown>) => ({ record: { schema: "gentle-ai.review-reconcile-audit/v1", successor: request.successorLineage } }),
387
+ } as unknown as import("../lib/native-review-cli.ts").NativeReviewCli);
388
+ assert.equal(completed.mutation_outcome, "committed");
389
+ assert.deepEqual(completed.result, { schema: "gentle-ai.review-reconcile-audit/v1", successor: "successor" });
390
+
391
+ for (const error of [
392
+ new NativeReviewCliError("cancelled", "review/reconcile-authority", true, true, "cancelled"),
393
+ new NativeReviewCliError("non_zero", "review/reconcile-authority", true, true, "partial", undefined, { schema: "gentle-ai.review-reconcile-audit/v1", status: "partial" }),
394
+ ]) {
395
+ const failed = await __testing.executeReviewControllerOperation({ operation: "reconcile-authority", input: JSON.stringify(reconciliation) }, process.cwd(), {
396
+ reconcileAuthority: async () => { throw error; },
397
+ } as unknown as import("../lib/native-review-cli.ts").NativeReviewCli);
398
+ assert.equal(failed.outcome, "native-operation-failed");
399
+ assert.equal(failed.mutation_outcome, "unknown");
400
+ assert.equal(failed.next_action, "review.status");
401
+ }
1338
402
  });
1339
403
 
1340
- // gentle-pi#311 P4-roles: the provider-rendered self-contained role vectors
1341
- // execute EXACTLY as rendered — one CLI invocation, verbatim tokens in
1342
- // provider order, no --cwd, no --contract, no --input. Go materializes the
1343
- // role prompt, runs its own locked-down pi process, and admits the verdict.
1344
- test("captureProviderRole executes the exact rendered vector and decodes only the strict artifact", async () => {
1345
- const artifact = {
1346
- schema: "gentle-ai.review-provider-role-capture/v1",
1347
- lineage_id: "review-1d5aadacc600e167",
1348
- target_identity: `sha256:${"9".repeat(64)}`,
1349
- role: "refuter",
1350
- captured: true,
1351
- };
1352
- const calls: Array<{ arguments: readonly string[] }> = [];
1353
- const cli = new NativeReviewCliV216(async (request) => {
1354
- calls.push({ arguments: request.arguments });
1355
- return { stdout: JSON.stringify(artifact), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
1356
- }, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
1357
- const tokens = [
1358
- "--lineage=review-1d5aadacc600e167",
1359
- `--expected-revision=sha256:${"a".repeat(64)}`,
1360
- `--target=sha256:${"9".repeat(64)}`,
1361
- `--repository-context=rctx1_${"c".repeat(64)}`,
1362
- "--agent=pi",
1363
- "--execute=true",
1364
- ];
1365
- const captured = await cli.captureProviderRole({ captureOperation: "review.capture-refuter", argumentTokens: tokens, cwd: "/repo" });
1366
- assert.equal(captured.role, "refuter");
1367
- assert.equal(captured.lineageId, "review-1d5aadacc600e167");
1368
- assert.deepEqual(calls[0]!.arguments, ["review", "capture-refuter", ...tokens], "the vector runs verbatim: nothing added, removed, or reordered");
1369
-
404
+ test("destructive maintenance remains UI-gated and derives authorization at the public controller boundary", async () => {
405
+ const calls: Array<Record<string, unknown>> = [];
406
+ const native = {
407
+ reclaim: async (request: Record<string, unknown>) => { calls.push(request); return { record: { schema: "gentle-ai.review-reclaim-audit/v1" } }; },
408
+ reconcileAuthority: async (request: Record<string, unknown>) => { calls.push(request); return { record: { schema: "gentle-ai.review-reconcile-audit/v1" } }; },
409
+ } as unknown as import("../lib/native-review-cli.ts").NativeReviewCli;
410
+ const controller = registeredController(native);
411
+ const reset = { operation: "reset", input: JSON.stringify({ repositoryId: "repo", commonDirHash: "a".repeat(64), inventoryHash: "b".repeat(64), confirmation: "DESTROY REVIEW AUTHORITY repo", lineage: "stuck", actor: "maintainer", reason: "incomplete" }) };
412
+ await assert.rejects(controller.execute("headless", reset, undefined, undefined, { ...interactiveContext(true), hasUI: false } as ExtensionContext), /interactive Pi UI.*fails closed/i);
413
+ await assert.rejects(controller.execute("declined", reset, undefined, undefined, interactiveContext(false)), /not explicitly authorized/);
414
+ const approved = await controller.execute("approved", reset, undefined, undefined, interactiveContext(true));
415
+ assert.equal((approved.details as { mutation_outcome?: string }).mutation_outcome, "committed");
416
+ assert.equal(calls.length, 1);
417
+ assert.equal(calls[0]?.lineage, "stuck");
1370
418
  await assert.rejects(
1371
- cli.captureProviderRole({ captureOperation: "review.capture-result", argumentTokens: tokens, cwd: "/repo" }),
1372
- /supports only review\.capture-refuter and review\.capture-validation/,
419
+ controller.execute("missing-challenge", { operation: "reset", input: JSON.stringify({ commonDirHash: "a".repeat(64), inventoryHash: "b".repeat(64), confirmation: "DESTROY REVIEW AUTHORITY repo", lineage: "stuck", actor: "maintainer", reason: "incomplete" }) }, undefined, undefined, interactiveContext(true)),
420
+ /requires an exact string repositoryId/,
1373
421
  );
1374
- assert.equal(calls.length, 1, "an unknown role operation must fail before any native launch");
1375
- });
1376
422
 
1377
- test("captureProviderRole refuses an uncaptured or malformed role artifact", async () => {
1378
- const base = {
1379
- schema: "gentle-ai.review-provider-role-capture/v1",
1380
- lineage_id: "review-1d5aadacc600e167",
1381
- target_identity: `sha256:${"9".repeat(64)}`,
1382
- role: "targeted-validator",
1383
- captured: true,
1384
- };
1385
- for (const body of [
1386
- { ...base, captured: false },
1387
- { ...base, role: "reviewer" },
1388
- { ...base, schema: "gentle-ai.review-result-artifact/v2" },
1389
- { ...base, extra: true },
1390
- ]) {
1391
- const cli = new NativeReviewCliV216(async () => ({ stdout: JSON.stringify(body), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false }), "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
1392
- await assert.rejects(cli.captureProviderRole({ captureOperation: "review.capture-validation", argumentTokens: ["--agent=pi", "--execute=true"], cwd: "/repo" }));
1393
- }
423
+ const reconciliation = await controller.execute("reconcile", { operation: "reconcile-authority", input: JSON.stringify({ predecessorLineage: "predecessor", expectedPredecessorRevision: "revision-1", successorLineage: "successor", expectedSuccessorRevision: "revision-2", actor: "maintainer", reason: "repair edge" }) }, undefined, undefined, interactiveContext(true));
424
+ assert.equal((reconciliation.details as { mutation_outcome?: string }).mutation_outcome, "committed");
425
+ assert.match(String(calls[1]?.maintainerAuthorization), /^gentle-ai\.review-reconcile-authorization\/v1\npredecessor_lineage=predecessor\npredecessor_revision=revision-1\nsuccessor_lineage=successor\nsuccessor_revision=revision-2/);
1394
426
  });
1395
427
 
1396
- // gentle-pi#311 P5: the provider-driven FINALIZE executes the rendered
1397
- // transition tokens verbatim and decodes both answer shapes (the negotiated
1398
- // operation envelope and the plain review/finalize shape a contract-less
1399
- // transition produces).
1400
- test("finalizeTransition runs the provider-rendered tokens verbatim and decodes both answer shapes", async (t) => {
1401
- t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
1402
- const capabilities = v2Fixture<Record<string, unknown>>("capabilities.fixture.json");
1403
- const capabilitiesBody = { ...capabilities, package: { ...(capabilities.package as Record<string, unknown>), version: GENTLE_AI_VERSION } };
1404
- const plainBody = { operation: "review/finalize", lineage_id: "review-1d5aadacc600e167", state: "approved", action: "validate delivery", store_revision: `sha256:${"f".repeat(64)}` };
1405
- const envelopeBody = {
1406
- schema: "gentle-ai.review-integration.operation/v2",
1407
- contract: "gentle-ai.review-integration/v2",
1408
- operation: "review.finalize",
1409
- result: { operation: "review/finalize", lineage_id: "review-1d5aadacc600e167", state: "approved", action: "validate delivery", store_revision: `sha256:${"f".repeat(64)}` },
1410
- };
1411
- const { adapter, calls } = queuedAdapter([
1412
- { stdout: JSON.stringify(capabilitiesBody) },
1413
- { stdout: JSON.stringify(plainBody) },
1414
- { stdout: JSON.stringify(envelopeBody) },
1415
- ]);
1416
- const cli = new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
1417
- const tokens = ["--lineage=review-1d5aadacc600e167", "--captured-results=true"];
1418
- const plain = await cli.finalizeTransition({ cwd: "/repo", argumentTokens: tokens });
1419
- assert.equal(plain.state, "approved");
1420
- assert.deepEqual(calls[1]!.arguments, ["review", "finalize", ...tokens], "no --contract, --cwd, or document flag is invented for a rendered transition");
1421
- const negotiated = await cli.finalizeTransition({ cwd: "/repo", argumentTokens: tokens });
1422
- assert.equal(negotiated.storeRevision, `sha256:${"f".repeat(64)}`);
1423
- await assert.rejects(cli.finalizeTransition({ cwd: "/repo", argumentTokens: [] }), /requires the provider-rendered argument tokens/);
1424
- });
1425
-
1426
- // Field defect (fambig, 2026-08-16): the evidence collect slot renders the
1427
- // exact `review capture-evidence` submission tokens — fix-diff `--target`,
1428
- // `--expected-revision`, and an opaque cwd-independent `--repository-context` —
1429
- // with `{{outcome}}`/`{{input}}` slots. Satisfying the slot means executing
1430
- // those tokens verbatim with only the two slot substitutions, exactly like
1431
- // captureResult; identities are never reconstructed on the client.
1432
- test("captureEvidenceSubmission executes the provider-rendered submission tokens verbatim with slot substitution", async (t) => {
1433
- t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
1434
- const capabilities = v2Fixture<Record<string, unknown>>("capabilities.fixture.json");
1435
- const capabilitiesBody = { ...capabilities, package: { ...(capabilities.package as Record<string, unknown>), version: GENTLE_AI_VERSION } };
1436
- const record = {
1437
- schema: "gentle-ai.review-verification-evidence/v2",
1438
- version: 2,
1439
- lineage_id: "review-evidence-lineage",
1440
- authority_revision: `sha256:${"a".repeat(64)}`,
1441
- target_identity: `sha256:${"b".repeat(64)}`,
1442
- candidate_tree: "c".repeat(40),
1443
- paths_digest: `sha256:${"d".repeat(64)}`,
1444
- paths: ["calc.go"],
1445
- ledger_ids: ["R3-1"],
1446
- raw_payload_sha256: `sha256:${"e".repeat(64)}`,
1447
- raw_payload_bytes: 24,
1448
- outcome: "passed",
1449
- record_digest: `sha256:${"f".repeat(64)}`,
1450
- };
1451
- const argumentTokens = [
1452
- `--lineage=${record.lineage_id}`,
1453
- `--expected-revision=${record.authority_revision}`,
1454
- `--target=${record.target_identity}`,
1455
- `--repository-context=rctx1_${"e".repeat(64)}`,
1456
- "--outcome={{outcome}}",
1457
- "--input={{input}}",
1458
- ];
1459
- let staged = "";
1460
- const calls: Array<{ arguments: readonly string[] }> = [];
1461
- const cli = new NativeReviewCliV216(async (request) => {
1462
- calls.push({ arguments: request.arguments });
1463
- if (request.arguments[1] === "capabilities") return { stdout: JSON.stringify(capabilitiesBody), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
1464
- const inputToken = request.arguments.find((token) => token.startsWith("--input="));
1465
- assert.ok(inputToken !== undefined);
1466
- staged = readFileSync(inputToken.slice("--input=".length), "utf8");
1467
- return { stdout: JSON.stringify(record), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
1468
- }, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
1469
- const evidence = "verification: go test ./... passed\n";
1470
- const captured = await cli.captureEvidenceSubmission({
1471
- argumentTokens,
1472
- outcomeSubstitutionLocation: 4,
1473
- inputSubstitutionLocation: 5,
1474
- outcome: "passed",
1475
- evidenceDocument: evidence,
1476
- });
1477
- assert.equal(staged, evidence, "the evidence bytes must be staged exactly");
1478
- assert.equal(captured.recordDigest, record.record_digest);
1479
- assert.equal(captured.targetIdentity, record.target_identity);
1480
- const argv = calls[1]!.arguments;
1481
- assert.deepEqual(argv.slice(0, 2), ["review", "capture-evidence"]);
1482
- assert.deepEqual(argv.slice(2, 6), argumentTokens.slice(0, 4), "the identity-bearing tokens must pass through verbatim, in provider order");
1483
- assert.equal(argv[6], "--outcome=passed");
1484
- assert.match(String(argv[7]), /^--input=./);
1485
- assert.equal(argv.length, 8);
1486
- assert.equal(argv.includes("--cwd"), false, "the repository context is authoritative and cwd-independent");
1487
- assert.equal(argv.includes("--contract"), false);
428
+ test("RECOVER requires a live UI decision and RECOVER_LOCK requires its owner binding", async () => {
429
+ const input = { predecessorLineage: "predecessor", expectedPredecessorRevision: "revision-1", successorLineage: "successor", disposition: "invalidated", actor: "maintainer", reason: "recover authority" };
430
+ const native = {
431
+ targetStatus: async () => recoveryStatus(),
432
+ recover: async () => ({ record: { schema: "gentle-ai.review-recovery/v1" } }),
433
+ reclaim: async () => ({ record: { schema: "gentle-ai.review-reclaim-audit/v1" } }),
434
+ } as unknown as import("../lib/native-review-cli.ts").NativeReviewCli;
1488
435
  await assert.rejects(
1489
- cli.captureEvidenceSubmission({ cwd: "/repo", argumentTokens, outcomeSubstitutionLocation: 4, inputSubstitutionLocation: 5, outcome: "passed", evidenceDocument: evidence }),
1490
- /repository context or --cwd, never both/,
436
+ __testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify(input) }, process.cwd(), native),
437
+ /fresh explicit authorization through the interactive Pi UI/,
1491
438
  );
1492
439
  await assert.rejects(
1493
- cli.captureEvidenceSubmission({ argumentTokens, outcomeSubstitutionLocation: 4, inputSubstitutionLocation: 5, outcome: "failed" as never, evidenceDocument: evidence }),
1494
- /outcome must be passed, verification_failed, or procedural_tooling_failed/,
440
+ __testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify(input) }, process.cwd(), native, undefined, undefined, interactiveContext(false)),
441
+ /not explicitly authorized/,
1495
442
  );
1496
443
  await assert.rejects(
1497
- cli.captureEvidenceSubmission({ argumentTokens: argumentTokens.slice(0, 4), outcomeSubstitutionLocation: 0, inputSubstitutionLocation: 1, outcome: "passed", evidenceDocument: evidence }),
1498
- /\{\{outcome\}\}/,
444
+ __testing.executeReviewControllerOperation({ operation: "recover-lock", input: JSON.stringify({ lineage: "stuck", actor: "maintainer", reason: "stale" }) }, process.cwd(), native),
445
+ /ownerHash/,
1499
446
  );
1500
- assert.equal(calls.length, 2, "every rejected submission must fail before another native launch");
447
+ const missingReclaim = await __testing.executeReviewControllerOperation({ operation: "recover-lock", input: JSON.stringify({ ownerHash: "a".repeat(64) }) }, process.cwd(), native);
448
+ assert.deepEqual(missingReclaim.missing_input, ["lineage", "actor", "reason"]);
449
+ const recovered = await __testing.executeReviewControllerOperation({ operation: "recover-lock", input: JSON.stringify({ ownerHash: "a".repeat(64), lineage: "stuck", actor: "maintainer", reason: "stale" }) }, process.cwd(), native);
450
+ assert.equal(recovered.mutation_outcome, "committed");
1501
451
  });
1502
452
 
1503
- test("finalizeSubmission executes the rendered finalize submission tokens verbatim with the {{value}} substitution", async (t) => {
1504
- t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
1505
- const capabilities = v2Fixture<Record<string, unknown>>("capabilities.fixture.json");
1506
- const capabilitiesBody = { ...capabilities, package: { ...(capabilities.package as Record<string, unknown>), version: GENTLE_AI_VERSION } };
1507
- const finalizeBody = {
1508
- schema: "gentle-ai.review-integration.operation/v2",
1509
- contract: "gentle-ai.review-integration/v2",
1510
- operation: "review.finalize",
1511
- result: { operation: "review/finalize", lineage_id: "review-1d5aadacc600e167", state: "correction_required", action: "continue the current review state", store_revision: `sha256:${"f".repeat(64)}` },
1512
- };
1513
- const planTokens = [
1514
- "--contract=gentle-ai.review-integration/v2",
1515
- "--lineage=review-1d5aadacc600e167",
1516
- `--expected-revision=sha256:${"a".repeat(64)}`,
1517
- `--target=sha256:${"b".repeat(64)}`,
1518
- `--request-hash=sha256:${"c".repeat(64)}`,
1519
- `--repository-context=rctx1_${"e".repeat(64)}`,
1520
- "--correction-lines={{value}}",
1521
- ];
1522
- const calls: Array<{ arguments: readonly string[] }> = [];
1523
- let staged: string | undefined;
1524
- const cli = new NativeReviewCliV216(async (request) => {
1525
- calls.push({ arguments: request.arguments });
1526
- if (request.arguments[1] === "capabilities") return { stdout: JSON.stringify(capabilitiesBody), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
1527
- const validationToken = request.arguments.find((token) => token.startsWith("--validation="));
1528
- if (validationToken !== undefined) staged = readFileSync(validationToken.slice("--validation=".length), "utf8");
1529
- return { stdout: JSON.stringify(finalizeBody), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
1530
- }, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
1531
-
1532
- // Plan form: literal substitution, every other token verbatim.
1533
- const planned = await cli.finalizeSubmission({ cwd: "/repo", argumentTokens: planTokens, valueSubstitutionLocation: 6, valueLiteral: "2" });
1534
- assert.equal(planned.state, "correction_required");
1535
- const planArgv = calls[1]!.arguments;
1536
- assert.deepEqual(planArgv.slice(0, 2), ["review", "finalize"]);
1537
- assert.deepEqual(planArgv.slice(2, 8), planTokens.slice(0, 6), "identity-bearing tokens must pass through verbatim, in provider order");
1538
- assert.equal(planArgv[8], "--correction-lines=2");
1539
- assert.equal(planArgv.length, 9);
1540
-
1541
- // Validation form: the document is staged to a 0o600 artifact whose path substitutes {{value}}.
1542
- const validationTokens = [...planTokens.slice(0, 6), "--validation={{value}}"];
1543
- const document = JSON.stringify({ targeted_validation_request_hash: `sha256:${"9".repeat(64)}`, original_criteria: { passed: true, evidence: ["ok"] } });
1544
- await cli.finalizeSubmission({ cwd: "/repo", argumentTokens: validationTokens, valueSubstitutionLocation: 6, valueDocument: document });
1545
- assert.equal(staged, document, "the validation document must be staged byte-exact");
1546
-
1547
- // Guards: exactly one substitution form, and a real {{value}} slot.
1548
- await assert.rejects(
1549
- cli.finalizeSubmission({ cwd: "/repo", argumentTokens: planTokens, valueSubstitutionLocation: 6, valueLiteral: "2", valueDocument: document }),
1550
- /exactly one of valueLiteral or valueDocument/,
1551
- );
1552
- await assert.rejects(
1553
- cli.finalizeSubmission({ cwd: "/repo", argumentTokens: planTokens.slice(0, 6), valueSubstitutionLocation: 0, valueLiteral: "2" }),
1554
- /\{\{value\}\}/,
1555
- );
1556
- assert.equal(calls.length, 3, "rejected submissions must fail before another native launch");
453
+ test("REPAIR_LEGACY_ALIAS derives its immutable target from fresh native inventory", async () => {
454
+ const calls: Array<Record<string, unknown>> = [];
455
+ const native = {
456
+ reviewStatus: async () => ({
457
+ repository: "/canonical/repository",
458
+ complete: true,
459
+ entries: [{ version: "legacy-v1", status: "invalid", lineageId: "legacy-alias", revision: SHA, problems: [NATIVE_REVIEW_LEGACY_ALIAS_REPAIR.DIAGNOSTIC] }],
460
+ }),
461
+ repairLegacyAlias: async (request: Record<string, unknown>) => { calls.push(request); return { record: { schema: "gentle-ai.review-repair-audit/v1" } }; },
462
+ } as unknown as import("../lib/native-review-cli.ts").NativeReviewCli;
463
+ const result = await __testing.executeReviewControllerOperation({ operation: "repair-legacy-alias", input: JSON.stringify({ lineage: "legacy-alias", actor: "maintainer", reason: "repair alias" }) }, process.cwd(), native, undefined, undefined, interactiveContext(true));
464
+ assert.equal(result.mutation_outcome, "committed");
465
+ assert.equal(calls[0]?.repository, "/canonical/repository");
466
+ assert.equal(calls[0]?.expectedRevision, SHA);
467
+ assert.match(String(calls[0]?.maintainerAuthorization), /^gentle-ai\.review-legacy-alias-repair-authorization\/v1\nrepository=\/canonical\/repository/);
468
+ const injected = await __testing.executeReviewControllerOperation({ operation: "repair-legacy-alias", input: JSON.stringify({ lineage: "legacy-alias", actor: "maintainer", reason: "repair alias", repository: "/injected" }) }, process.cwd(), native, undefined, undefined, interactiveContext(true));
469
+ assert.equal(injected.outcome, "native-input-invalid");
1557
470
  });