gentle-pi 2.1.2 → 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 (228) hide show
  1. package/README.md +108 -79
  2. package/assets/agents/gentle-ai-worker.md +7 -3
  3. package/assets/agents/jd-fix-agent.md +1 -1
  4. package/assets/agents/jd-judge-a.md +3 -1
  5. package/assets/agents/jd-judge-b.md +3 -1
  6. package/assets/agents/review-readability.md +4 -3
  7. package/assets/agents/review-reliability.md +4 -3
  8. package/assets/agents/review-resilience.md +4 -3
  9. package/assets/agents/review-risk.md +4 -3
  10. package/assets/agents/sdd-apply.md +11 -4
  11. package/assets/agents/sdd-archive.md +6 -1
  12. package/assets/agents/sdd-design.md +6 -1
  13. package/assets/agents/sdd-explore.md +6 -2
  14. package/assets/agents/sdd-init.md +10 -2
  15. package/assets/agents/sdd-onboard.md +6 -1
  16. package/assets/agents/sdd-proposal.md +8 -1
  17. package/assets/agents/sdd-research.md +54 -0
  18. package/assets/agents/sdd-spec.md +6 -1
  19. package/assets/agents/sdd-status.md +10 -5
  20. package/assets/agents/sdd-sync.md +6 -1
  21. package/assets/agents/sdd-tasks.md +8 -4
  22. package/assets/agents/sdd-verify.md +27 -2
  23. package/assets/chains/4r-review.chain.md +2 -0
  24. package/assets/chains/sdd-full.chain.md +2 -2
  25. package/assets/chains/sdd-plan.chain.md +1 -1
  26. package/assets/chains/sdd-verify.chain.md +2 -2
  27. package/assets/orchestrator-delegation.md +145 -160
  28. package/assets/orchestrator-memory.md +2 -0
  29. package/assets/orchestrator.md +25 -48
  30. package/assets/sdd-orchestrator-workflow.md +163 -25
  31. package/assets/support/sdd-status-contract.md +24 -6
  32. package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
  33. package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
  34. package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
  35. package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
  36. package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
  37. package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
  38. package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
  39. package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
  40. package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
  41. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  42. package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
  43. package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
  44. package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
  45. package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
  46. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  47. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  48. package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
  49. package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
  50. package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
  51. package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
  52. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
  53. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
  54. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
  55. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
  56. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
  57. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
  58. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
  59. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
  60. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
  61. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
  62. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
  63. package/docs/native-authority-architecture.md +14 -16
  64. package/docs/review-integration.md +27 -353
  65. package/extensions/ask-user-choice.ts +151 -0
  66. package/extensions/gentle-ai.ts +2710 -3211
  67. package/extensions/quiet-tools.ts +515 -32
  68. package/extensions/sdd-init.ts +21 -12
  69. package/extensions/skill-registry.ts +10 -2
  70. package/extensions/startup-banner.ts +10 -4
  71. package/lib/gentle-ai-binary.ts +173 -2
  72. package/lib/gentle-ai-renderer.ts +70 -0
  73. package/lib/model-routing-authority.ts +133 -0
  74. package/lib/native-review-cli.ts +676 -781
  75. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  76. package/lib/provider-contract-bundle.ts +704 -0
  77. package/lib/review-candidate-view.ts +836 -118
  78. package/lib/review-compact-contract.ts +59 -248
  79. package/lib/review-host-relay.ts +578 -0
  80. package/lib/review-integration-v2.ts +1254 -221
  81. package/lib/review-last-event-controller.ts +35 -0
  82. package/lib/review-relay-contract.ts +16 -0
  83. package/lib/sdd-preflight.ts +230 -67
  84. package/lib/sdd-status.ts +66 -111
  85. package/lib/terminal-theme.ts +1 -1
  86. package/package.json +83 -79
  87. package/runtime/gentle-ai-binary.mjs +174 -3
  88. package/runtime/native-review-cli.mjs +610 -715
  89. package/runtime/review-integration-v2.mjs +1223 -190
  90. package/runtime/review-relay-contract.mjs +17 -0
  91. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +5 -5
  92. package/scripts/check-provider-contract.mjs +138 -0
  93. package/scripts/gentle-ai-installer.mjs +85 -22
  94. package/scripts/maintainer/provider-relay-matrix.mjs +403 -0
  95. package/scripts/mirror-provider-contract.mjs +143 -0
  96. package/scripts/test-packed-runner.mjs +20 -9
  97. package/scripts/verify-package-files.mjs +119 -43
  98. package/skills/_shared/review-ledger-contract.md +11 -19
  99. package/skills/chained-pr/SKILL.md +3 -0
  100. package/skills/cognitive-doc-design/SKILL.md +1 -1
  101. package/skills/comment-writer/SKILL.md +1 -1
  102. package/skills/gentle-ai/SKILL.md +7 -74
  103. package/skills/issue-creation/SKILL.md +94 -168
  104. package/skills/judgment-day/SKILL.md +9 -5
  105. package/skills/judgment-day/references/prompts-and-formats.md +2 -0
  106. package/skills/rdd-defect-workflow/SKILL.md +54 -0
  107. package/skills/release/SKILL.md +3 -3
  108. package/skills/skill-registry/SKILL.md +1 -1
  109. package/skills/work-unit-commits/SKILL.md +3 -1
  110. package/tests/artifact-language.test.ts +24 -11
  111. package/tests/ask-user-choice.test.ts +264 -0
  112. package/tests/background-subagents.test.ts +771 -0
  113. package/tests/codegraph-tools.test.ts +3 -3
  114. package/tests/crosslane/cross-lane.mjs +16 -0
  115. package/tests/delegated-key-learnings-contract.test.ts +240 -0
  116. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  117. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  118. package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
  119. package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
  120. package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
  121. package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
  122. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  123. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  124. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  125. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  126. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  127. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  128. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  129. package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
  130. package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
  131. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  132. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  133. package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
  134. package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
  135. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  136. package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
  137. package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
  138. package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
  139. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  140. package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
  141. package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
  142. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
  143. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
  144. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
  145. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
  146. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
  147. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
  148. package/tests/gentle-ai-binary.test.ts +83 -13
  149. package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
  150. package/tests/gentle-ai-dev-binary.test.ts +159 -0
  151. package/tests/gentle-ai-installer.test.ts +138 -48
  152. package/tests/gentle-ai.test.ts +820 -131
  153. package/tests/gentle-theme.test.ts +133 -0
  154. package/tests/maintainer/provider-relay.maintest.ts +601 -0
  155. package/tests/model-routing-authority.test.ts +257 -0
  156. package/tests/native-review-capability-contract.test.ts +75 -2
  157. package/tests/native-review-cli.test.ts +588 -908
  158. package/tests/native-review-consent.test.ts +283 -38
  159. package/tests/native-review-parity-runtime.test.ts +108 -350
  160. package/tests/native-review-parity.test.ts +684 -732
  161. package/tests/native-sdd-attempt-authority.test.ts +235 -0
  162. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  163. package/tests/orchestrator-budget.test.ts +158 -89
  164. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  165. package/tests/package-manifest.test.ts +128 -100
  166. package/tests/provider-contract-bundle.test.ts +385 -0
  167. package/tests/provider-contract-mirror.test.ts +206 -0
  168. package/tests/provider-defect-handoff.test.ts +252 -0
  169. package/tests/quiet-tool-rendering.test.ts +1055 -28
  170. package/tests/review-actor-tool-deny.test.ts +12 -13
  171. package/tests/review-authority-recovery-docs.test.ts +1 -2
  172. package/tests/review-candidate-view.test.ts +898 -12
  173. package/tests/review-compact-contract.test.ts +29 -122
  174. package/tests/review-controller-lock-status.test.ts +2 -2
  175. package/tests/review-controller-native-recovery.test.ts +366 -857
  176. package/tests/review-controller-native-routing.test.ts +1253 -4199
  177. package/tests/review-controller-retired-ops.test.ts +1 -1
  178. package/tests/review-controller-workspace-root.test.ts +236 -70
  179. package/tests/review-controller.test.ts +26 -816
  180. package/tests/review-corrected-finalize-binding.test.ts +134 -0
  181. package/tests/review-dispatch-hydration-gap.test.ts +145 -0
  182. package/tests/review-gate.test.ts +0 -45
  183. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  184. package/tests/review-host-relay-routing.test.ts +352 -0
  185. package/tests/review-host-relay.test.ts +754 -0
  186. package/tests/review-integration-v2-forward.test.ts +654 -0
  187. package/tests/review-integration-v2.test.ts +317 -129
  188. package/tests/review-last-event-closure.test.ts +408 -0
  189. package/tests/review-ledger-contract.test.ts +106 -60
  190. package/tests/review-recovered-lineage-routing.test.ts +199 -0
  191. package/tests/review-relay-transport-agent.test.ts +322 -0
  192. package/tests/review-snapshot.test.ts +3 -2
  193. package/tests/runtime-harness.mjs +573 -180
  194. package/tests/sdd-agent-tools.test.ts +53 -32
  195. package/tests/sdd-preflight.test.ts +81 -15
  196. package/tests/sdd-status.test.ts +109 -110
  197. package/tests/skill-collision-prefixes.test.ts +6 -8
  198. package/tests/skill-registry.test.ts +50 -1
  199. package/tests/verify-package-files.test.ts +62 -0
  200. package/tests/writer-edit-surface-scope.test.ts +230 -0
  201. package/themes/Gentleman-Cute.json +94 -0
  202. package/themes/Gentleman-Sexy.json +92 -0
  203. package/assets/agents/review-refuter.md +0 -40
  204. package/assets/agents/review-validator.md +0 -23
  205. package/lib/git-commit-transaction.ts +0 -801
  206. package/lib/native-review-remediation.ts +0 -49
  207. package/lib/review-compact.ts +0 -947
  208. package/lib/review-refuter-adapter.ts +0 -129
  209. package/lib/review-runtime-contract.ts +0 -68
  210. package/prompts/gcl.md +0 -54
  211. package/prompts/gis.md +0 -25
  212. package/prompts/gpr.md +0 -41
  213. package/prompts/gwr.md +0 -31
  214. package/runtime/git-commit-transaction.mjs +0 -802
  215. package/scripts/run-git-commit-transaction.mjs +0 -35
  216. package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
  217. package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
  218. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
  219. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
  220. package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
  221. package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
  222. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
  223. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
  224. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  225. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  226. package/tests/git-commit-transaction.test.ts +0 -302
  227. package/tests/review-compact.test.ts +0 -243
  228. package/tests/review-refuter-adapter.test.ts +0 -89
@@ -1,961 +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,
12
+ nativeReviewAbandonAuthorization,
14
13
  nativeReviewLegacyAliasRepairAuthorization,
14
+ nativeReviewLegacyQuarantineAuthorization,
15
+ nativeReviewReconcileAuthorization,
15
16
  type ExecFileAdapter,
16
- type NativeReviewCli,
17
17
  } from "../lib/native-review-cli.ts";
18
- import { GENTLE_AI_VERSION } from "../lib/gentle-ai-binary.ts";
19
18
 
20
- const v2FixtureRoot = join(process.cwd(), "contracts", "review-integration", "v2", "fixtures");
21
- const v2Fixture = <T = unknown>(name: string): T => JSON.parse(readFileSync(join(v2FixtureRoot, name), "utf8")) as T;
19
+ interface QueuedResult { stdout: string; exitCode?: number; }
22
20
 
23
- // Queued-adapter clients never execute a real process; a fixed absolute
24
- // package-local path keeps these tests independent of an installed binary.
25
- class NativeReviewCliV214 extends NativeReviewCliV214Production {
26
- constructor(...parameters: ConstructorParameters<typeof NativeReviewCliV214Production>) {
27
- const [adapter, executable, ...rest] = parameters;
28
- super(adapter, executable ?? "/package/.gentle-ai/gentle-ai", ...rest);
29
- }
30
- }
31
-
32
- interface QueuedResult { stdout: string; stderr?: string; exitCode?: number; }
33
-
34
- function queuedAdapter(results: QueuedResult[]): { adapter: ExecFileAdapter; calls: Array<{ file: string; arguments: readonly string[]; cwd: string }> } {
35
- 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 }> = [];
36
27
  return {
37
28
  calls,
38
29
  adapter: async (request) => {
39
- calls.push({ file: request.file, arguments: request.arguments, cwd: request.cwd });
40
- const result = results.shift();
41
- if (!result) throw new Error("unexpected native invocation");
42
- return { stdout: result.stdout, stderr: result.stderr ?? "", exitCode: result.exitCode ?? 0, signal: null, timedOut: false, outputLimitExceeded: false };
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 };
43
34
  },
44
35
  };
45
36
  }
46
37
 
47
- const VERSION_219 = { stdout: "gentle-ai 2.1.9\n" };
48
- const VERSION_218 = { stdout: "gentle-ai 2.1.8\n" };
49
- const VERSION_220 = { stdout: "gentle-ai 2.2.0\n" };
50
- const RECLAIM_RECORD = { schema: "gentle-ai.review-reclaim-audit/v1", lineage: "stuck-lineage", actor: "maintainer", reason: "incomplete entry" };
51
- const RECOVER_RECORD = { schema: "gentle-ai.review-recovery/v1", predecessor_lineage: "broken", successor_lineage: "successor" };
52
- const RECONCILE_RECORD = { schema: "gentle-ai.review-reconcile-audit/v1", predecessor_lineage: "predecessor", successor_lineage: "successor", outcome: "quarantined" };
53
- const RECONCILE_RESULT = { operation: "review/reconcile-authority", record: RECONCILE_RECORD };
54
- const ABANDON_RECORD = { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: "pristine", status: "committed" };
55
- const LEGACY_QUARANTINE_RECORD = { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: "legacy", status: "committed" };
56
- const LEGACY_FREEZE_DIAGNOSTIC = "historical findings freeze changed unrelated transaction state";
57
- const LEGACY_FREEZE_DISPOSITION = "quarantine-malformed-freeze-event";
58
- const COMBINED_RECONCILE_ANOMALIES = "unchanged_target,malformed_recovery_authorization";
59
- const LEGACY_ALIAS_DIAGNOSTIC = "unsupported historical v1 operation alias";
60
- const LEGACY_ALIAS_DISPOSITION = "quarantine-approved-historical-alias";
61
- const LEGACY_ALIAS_RECORD = { schema: "gentle-ai.review-reclaim-audit/v1", lineage_id: "legacy-alias", status: "committed" };
62
- const LEGACY_ALIAS_AUTHORIZATION = [
63
- "gentle-ai.review-legacy-alias-repair-authorization/v1",
64
- "repository=/repo",
65
- "lineage=legacy-alias",
66
- `revision=sha256:${"c".repeat(64)}`,
67
- `diagnostic=${LEGACY_ALIAS_DIAGNOSTIC}`,
68
- `disposition=${LEGACY_ALIAS_DISPOSITION}`,
69
- "actor=maintainer",
70
- "reason=quarantine approved historical alias",
71
- ].join("\n");
72
- const ABANDON_AUTHORIZATION = [
73
- "gentle-ai.review-abandon-authorization/v1",
74
- "lineage=pristine",
75
- "revision=revision",
76
- "snapshot_identity=sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
77
- "actor=maintainer",
78
- "reason=retire pristine lineage",
79
- ].join("\n");
80
- const LEGACY_QUARANTINE_AUTHORIZATION = [
81
- "gentle-ai.review-legacy-quarantine-authorization/v1",
82
- "repository=/repo",
83
- "lineage=legacy",
84
- "revision=sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
85
- `diagnostic=${LEGACY_FREEZE_DIAGNOSTIC}`,
86
- `disposition=${LEGACY_FREEZE_DISPOSITION}`,
87
- "actor=maintainer",
88
- "reason=quarantine malformed legacy freeze",
89
- ].join("\n");
90
- const RECONCILE_AUTHORIZATION = [
91
- "gentle-ai.review-reconcile-authorization/v1",
92
- "predecessor_lineage=predecessor",
93
- "predecessor_revision=predecessor-revision",
94
- "successor_lineage=successor",
95
- "successor_revision=successor-revision",
96
- "actor=maintainer",
97
- "reason=invalid recovery edge",
98
- ].join("\n");
99
- const COMBINED_RECONCILE_AUTHORIZATION = `${RECONCILE_AUTHORIZATION}\nanomalies=${COMBINED_RECONCILE_ANOMALIES}`;
100
-
101
- function scratchDir(prefix: string): string {
102
- const dir = mkdtempSync(join(tmpdir(), prefix));
103
- test.after(() => rmSync(dir, { recursive: true, force: true }));
104
- return dir;
105
- }
106
-
107
- interface RecordedNativeCall { operation: "reclaim" | "recover" | "reconcileAuthority" | "abandon" | "quarantineLegacy"; request: Record<string, unknown>; }
108
-
109
- function fakeRecoveryNative(record: Record<string, unknown>): { native: NativeReviewCli; calls: RecordedNativeCall[] } {
110
- const calls: RecordedNativeCall[] = [];
111
- const native = {
112
- async reclaim(request: Record<string, unknown>) {
113
- calls.push({ operation: "reclaim", request });
114
- return { record };
115
- },
116
- async recover(request: Record<string, unknown>) {
117
- calls.push({ operation: "recover", request });
118
- return { record };
119
- },
120
- async reconcileAuthority(request: Record<string, unknown>) {
121
- calls.push({ operation: "reconcileAuthority", request });
122
- return { record };
123
- },
124
- async abandon(request: Record<string, unknown>) {
125
- calls.push({ operation: "abandon", request });
126
- return { record };
127
- },
128
- async quarantineLegacy(request: Record<string, unknown>) {
129
- calls.push({ operation: "quarantineLegacy", request });
130
- return { record };
131
- },
132
- async targetStatus(request: { lineageId?: string }) {
133
- return {
134
- action: "recover",
135
- actionDisposition: "invalidated",
136
- authority: { lineageId: request.lineageId ?? "broken", revision: "rev-1" },
137
- } as unknown;
138
- },
139
- } as unknown as NativeReviewCli;
140
- return { native, calls };
141
- }
142
-
143
- async function runControllerOperation(
144
- parameters: Record<string, unknown>,
145
- native: NativeReviewCli | null,
146
- pendingAuthorizations: Map<string, unknown> = new Map(),
147
- signal?: AbortSignal,
148
- ): Promise<Record<string, unknown>> {
149
- const cwd = scratchDir("gentle-pi-native-recovery-");
150
- return await __testing.executeReviewControllerOperation(
151
- parameters,
152
- cwd,
153
- pendingAuthorizations as Map<string, never>,
154
- native,
155
- signal,
156
- );
38
+ function client(adapter: ExecFileAdapter): NativeReviewCliV216 {
39
+ return new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai");
157
40
  }
158
41
 
159
- test("native reclaim wrapper issues the exact review reclaim command and returns the audit record", async () => {
160
- const { adapter, calls } = queuedAdapter([VERSION_219, { stdout: JSON.stringify(RECLAIM_RECORD) }]);
161
- const cli = new NativeReviewCliV214(adapter);
162
- const result = await cli.reclaim!({ cwd: "/repo", lineage: "stuck-lineage", actor: "maintainer", reason: "incomplete entry" });
163
- assert.deepEqual(result.record, RECLAIM_RECORD);
164
- assert.deepEqual(calls[1]?.arguments, ["review", "reclaim", "--cwd", "/repo", "--lineage", "stuck-lineage", "--actor", "maintainer", "--reason", "incomplete entry"]);
165
- });
42
+ const SHA = `sha256:${"a".repeat(64)}`;
166
43
 
167
- test("native recover wrapper issues the exact review recover command including the authorization binding", async () => {
168
- const { adapter, calls } = queuedAdapter([VERSION_219, { stdout: JSON.stringify(RECOVER_RECORD) }]);
169
- const cli = new NativeReviewCliV214(adapter);
170
- 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({
171
55
  cwd: "/repo",
172
- predecessorLineage: "broken",
173
- expectedPredecessorRevision: "rev-1",
174
- successorLineage: "successor",
56
+ predecessorLineage: "stuck",
57
+ expectedPredecessorRevision: "revision-1",
58
+ successorLineage: "recovered",
175
59
  disposition: "invalidated",
176
60
  actor: "maintainer",
177
- reason: "invalid authority",
178
- maintainerAuthorization: "binding",
179
- });
180
- assert.deepEqual(result.record, RECOVER_RECORD);
181
- assert.deepEqual(calls[1]?.arguments, [
182
- "review", "recover", "--cwd", "/repo",
183
- "--predecessor-lineage", "broken",
184
- "--expected-predecessor-revision", "rev-1",
185
- "--successor-lineage", "successor",
186
- "--disposition", "invalidated",
187
- "--actor", "maintainer",
188
- "--reason", "invalid authority",
189
- "--maintainer-authorization", "binding",
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"],
190
67
  ]);
191
68
  });
192
69
 
193
- test("native reconcile-authority wrapper binds the exact target revisions and authorization without a shell", async () => {
194
- const { adapter, calls } = queuedAdapter([VERSION_219, { stdout: JSON.stringify(RECONCILE_RESULT) }]);
195
- const cli = new NativeReviewCliV214(adapter);
196
- const result = await cli.reconcileAuthority!({
197
- cwd: "/repo with spaces",
198
- predecessorLineage: "predecessor",
199
- expectedPredecessorRevision: "predecessor-revision",
200
- successorLineage: "successor",
201
- expectedSuccessorRevision: "successor-revision",
202
- actor: "maintainer",
203
- reason: "invalid recovery edge",
204
- maintainerAuthorization: RECONCILE_AUTHORIZATION,
205
- });
206
- assert.deepEqual(result.record, RECONCILE_RECORD);
207
- assert.deepEqual(calls[1]?.arguments, [
208
- "review", "reconcile-authority", "--cwd", "/repo with spaces",
209
- "--predecessor-lineage", "predecessor",
210
- "--expected-predecessor-revision", "predecessor-revision",
211
- "--successor-lineage", "successor",
212
- "--expected-successor-revision", "successor-revision",
213
- "--actor", "maintainer",
214
- "--reason", "invalid recovery edge",
215
- "--maintainer-authorization", RECONCILE_AUTHORIZATION,
216
- ]);
217
- });
218
-
219
- 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 () => {
220
71
  const request = {
221
72
  cwd: "/repo",
222
- predecessorLineage: "predecessor",
223
- expectedPredecessorRevision: "predecessor-revision",
224
- successorLineage: "successor",
225
- expectedSuccessorRevision: "successor-revision",
73
+ lineage: "abandoned",
74
+ expectedRevision: "revision-1",
75
+ snapshotIdentity: SHA,
76
+ capturedLensResults: ["00-risk.json"],
77
+ findingsPresent: true,
78
+ evidenceRecordsPresent: false,
226
79
  actor: "maintainer",
227
- reason: "invalid recovery edge",
228
- maintainerAuthorization: RECONCILE_AUTHORIZATION,
80
+ reason: "discard candidate",
229
81
  };
230
- const legacy = queuedAdapter([VERSION_218, { stdout: JSON.stringify(RECONCILE_RECORD) }]);
231
- assert.deepEqual((await new NativeReviewCliV214(legacy.adapter).reconcileAuthority!(request)).record, RECONCILE_RECORD);
232
-
233
- for (const [version, response] of [
234
- [VERSION_218, { schema: "gentle-ai.review-reconcile-audit/v1", predecessor_lineage: "predecessor" }],
235
- [VERSION_219, RECONCILE_RECORD],
236
- ] as const) {
237
- const queue = queuedAdapter([version, { stdout: JSON.stringify(response) }]);
238
- await assert.rejects(
239
- () => new NativeReviewCliV214(queue.adapter).reconcileAuthority!(request),
240
- (error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE,
241
- );
242
- }
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"]);
243
86
  });
244
87
 
245
- test("native v2.1.9 maintenance wrappers use exact argv and published authorization bindings", async () => {
246
- const { adapter, calls } = queuedAdapter([
247
- VERSION_219, { stdout: JSON.stringify({ operation: "review/abandon", record: ABANDON_RECORD }) },
248
- VERSION_219, { stdout: JSON.stringify({ operation: "review/quarantine-legacy", record: LEGACY_QUARANTINE_RECORD }) },
249
- VERSION_219, { stdout: JSON.stringify({ operation: "review/reconcile-authority", record: RECONCILE_RECORD }) },
250
- ]);
251
- const cli = new NativeReviewCliV214(adapter);
252
- assert.deepEqual((await cli.abandon!({ cwd: "/repo", lineage: "pristine", expectedRevision: "revision", snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", actor: "maintainer", reason: "retire pristine lineage", maintainerAuthorization: ABANDON_AUTHORIZATION })).record, ABANDON_RECORD);
253
- 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);
254
- 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);
255
- assert.deepEqual(calls.filter((call) => call.arguments[0] === "review").map((call) => call.arguments), [
256
- ["review", "abandon", "--cwd", "/repo", "--lineage", "pristine", "--expected-revision", "revision", "--actor", "maintainer", "--reason", "retire pristine lineage", "--maintainer-authorization", ABANDON_AUTHORIZATION],
257
- ["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],
258
- ["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],
259
- ]);
260
- });
261
-
262
- test("native v2.2.0 repair-legacy-alias uses the exact fixed binding and preserves idempotent audit records", async () => {
263
- const { adapter, calls } = queuedAdapter([
264
- VERSION_220,
265
- { stdout: JSON.stringify({ operation: "review/repair-legacy-alias", record: LEGACY_ALIAS_RECORD }) },
266
- ]);
267
- const cli = new NativeReviewCliV214(adapter);
88
+ test("native quarantine retains published legacy-v1 maintenance compatibility", async () => {
268
89
  const request = {
269
90
  cwd: "/repo",
270
91
  repository: "/repo",
271
- lineage: "legacy-alias",
272
- expectedRevision: `sha256:${"c".repeat(64)}`,
273
- diagnostic: LEGACY_ALIAS_DIAGNOSTIC,
274
- 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,
275
96
  actor: "maintainer",
276
- reason: "quarantine approved historical alias",
277
- maintainerAuthorization: LEGACY_ALIAS_AUTHORIZATION,
97
+ reason: "quarantine malformed freeze",
278
98
  };
279
- assert.equal(nativeReviewLegacyAliasRepairAuthorization(request), LEGACY_ALIAS_AUTHORIZATION);
280
- assert.deepEqual((await cli.repairLegacyAlias!(request)).record, LEGACY_ALIAS_RECORD);
281
- assert.deepEqual(calls[1]?.arguments, [
282
- "review", "repair-legacy-alias", "--cwd", "/repo", "--lineage", "legacy-alias",
283
- "--expected-revision", `sha256:${"c".repeat(64)}`,
284
- "--diagnostic", LEGACY_ALIAS_DIAGNOSTIC,
285
- "--disposition", LEGACY_ALIAS_DISPOSITION,
286
- "--actor", "maintainer", "--reason", "quarantine approved historical alias",
287
- "--maintainer-authorization", LEGACY_ALIAS_AUTHORIZATION,
288
- ]);
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]);
289
103
  });
290
104
 
291
- // Task 11.1 (migrate-review-integration-v2): `repairLegacyAlias` above stays
292
- // unnegotiated and carries no --contract (asserted at :281-288). The net-new
293
- // negotiated `repair()` is a DIFFERENT operation and must carry --contract on
294
- // every invocation, including the capabilities preflight `negotiated()` runs
295
- // first. A non-eligible preflight assessment lets this test stop after one
296
- // repair call, without needing a second queued execute-mode response.
297
- test("negotiated repair carries --contract on every invocation, unlike repair-legacy-alias", async (t) => {
298
- t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
299
- const capabilities = v2Fixture<Record<string, unknown>>("capabilities.fixture.json");
300
- const executableDigest = "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705";
301
- const capabilitiesBody = { ...capabilities, package: { ...(capabilities.package as Record<string, unknown>), version: GENTLE_AI_VERSION } };
302
- const preflightResult = {
303
- schema: "gentle-ai.review-integration.repair/v2",
304
- contract: "gentle-ai.review-integration/v2",
305
- operation: "review.repair",
306
- mode: "preflight",
307
- assessment: {
308
- schema: "gentle-ai.review-authority-repair-assessment/v1",
309
- status: "unsupported",
310
- counts: { lineages: 0, compact_lineages: 0, legacy_lineages: 0, events: 0, bytes: 0, eligible_candidates: 0, unsupported_lineages: 0, conflicts: 0 },
311
- supported_operations: ["review/complete-fix", "review/validate-fix"],
312
- authorization_schema: "gentle-ai.review-repair-authorization/v1",
313
- },
314
- required_inputs: [],
315
- };
316
- const { adapter, calls } = queuedAdapter([
317
- { stdout: JSON.stringify(capabilitiesBody) },
318
- { stdout: JSON.stringify(preflightResult) },
319
- ]);
320
- const cli = new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => executableDigest);
321
- const result = await cli.repair!({ cwd: "/repo", actor: "maintainer", reason: "quarantine approved historical alias", maintainerAuthorization: "irrelevant-for-non-eligible-preflight" });
322
- assert.equal(result.mode, "preflight");
323
- assert.equal(result.assessment.status, "unsupported");
324
- assert.equal(calls.length, 2, "a non-eligible preflight must never issue an execute-mode call");
325
- assert.deepEqual(calls[0]?.arguments, ["review", "capabilities", "--contract", "gentle-ai.review-integration/v2"]);
326
- assert.deepEqual(calls[1]?.arguments, ["review", "repair", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo", "--mode", "preflight"]);
327
- for (const call of calls) assert.ok(call.arguments.includes("--contract"), "every negotiated repair() invocation must carry --contract");
328
- });
329
-
330
- 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 () => {
331
106
  const request = {
332
- cwd: "/repo", repository: "/repo", lineage: "legacy-alias", expectedRevision: `sha256:${"c".repeat(64)}`,
333
- diagnostic: LEGACY_ALIAS_DIAGNOSTIC, disposition: LEGACY_ALIAS_DISPOSITION, actor: "maintainer", reason: "quarantine approved historical alias", maintainerAuthorization: LEGACY_ALIAS_AUTHORIZATION,
334
- };
335
- const stale = queuedAdapter([]);
336
- await assert.rejects(() => new NativeReviewCliV214(stale.adapter).repairLegacyAlias!({ ...request, expectedRevision: `sha256:${"d".repeat(64)}` }), /exact repository, lineage, revision/);
337
- assert.equal(stale.calls.length, 0);
338
- for (const result of [
339
- { stdout: JSON.stringify({ operation: "review/repair-legacy-alias" }) },
340
- { stdout: JSON.stringify({ operation: "review/repair-legacy-alias", record: LEGACY_ALIAS_RECORD }), stderr: "interrupted", exitCode: 1 },
341
- ]) {
342
- const queue = queuedAdapter([VERSION_220, result]);
343
- await assert.rejects(
344
- () => new NativeReviewCliV214(queue.adapter).repairLegacyAlias!(request),
345
- (error: unknown) => error instanceof NativeReviewCliError
346
- && (result.exitCode === 1 ? error.auditRecord?.schema === LEGACY_ALIAS_RECORD.schema : error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE),
347
- );
348
- }
349
- });
350
-
351
- test("native v2.1.9 maintenance wrappers fail closed before launch for unsupported versions and invalid bindings", async () => {
352
- const unsupported = queuedAdapter([{ stdout: "gentle-ai 2.1.8\n" }]);
353
- await assert.rejects(() => new NativeReviewCliV214(unsupported.adapter).abandon!({ cwd: "/repo", lineage: "pristine", expectedRevision: "revision", snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", actor: "maintainer", reason: "retire pristine lineage", maintainerAuthorization: ABANDON_AUTHORIZATION }), NativeReviewCliError);
354
- const invalid = queuedAdapter([]);
355
- 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);
356
- assert.equal(invalid.calls.length, 0);
357
- });
358
-
359
- test("native v2.1.9 maintenance wrappers preserve only valid prepared audit records on partial failures", async () => {
360
- for (const [operation, invoke, result] of [
361
- ["review/abandon", (cli: NativeReviewCliV214) => cli.abandon!({ cwd: "/repo", lineage: "pristine", expectedRevision: "revision", snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", actor: "maintainer", reason: "retire pristine lineage", maintainerAuthorization: ABANDON_AUTHORIZATION }), ABANDON_RECORD],
362
- ["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],
363
- ] as const) {
364
- const queue = queuedAdapter([VERSION_219, { stdout: JSON.stringify({ operation, record: result }), stderr: "quarantine interrupted", exitCode: 1 }]);
365
- 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);
366
- }
367
- const malformed = queuedAdapter([VERSION_219, { stdout: JSON.stringify({ operation: "review/abandon" }), exitCode: 1 }]);
368
- await assert.rejects(() => new NativeReviewCliV214(malformed.adapter).abandon!({ cwd: "/repo", lineage: "pristine", expectedRevision: "revision", snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", actor: "maintainer", reason: "retire pristine lineage", maintainerAuthorization: ABANDON_AUTHORIZATION }), (error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE && error.auditRecord === undefined);
369
- });
370
-
371
- test("native abandon forwards cancellation and preserves the unknown mutation outcome", async () => {
372
- const controller = new AbortController();
373
- let calls = 0;
374
- const adapter: ExecFileAdapter = async (request) => {
375
- calls += 1;
376
- if (calls === 1) return { ...VERSION_219, stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
377
- assert.equal(request.signal, controller.signal);
378
- const error = new Error("cancelled");
379
- error.name = "AbortError";
380
- 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",
381
114
  };
382
- await assert.rejects(() => new NativeReviewCliV214(adapter).abandon!({ cwd: "/repo", lineage: "pristine", expectedRevision: "revision", snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", actor: "maintainer", reason: "retire pristine lineage", maintainerAuthorization: ABANDON_AUTHORIZATION, signal: controller.signal }), (error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.CANCELLED && error.mutationOutcome === "unknown" && error.nextAction === "review.status");
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"]);
383
119
  });
384
120
 
385
- test("native reconcile-authority refuses a mismatched authorization before process launch", async () => {
386
- const { adapter, calls } = queuedAdapter([]);
387
- const cli = new NativeReviewCliV214(adapter);
121
+ test("native maintenance rejects tampered authority authorization before launch", async () => {
122
+ const queue = queuedAdapter([]);
388
123
  await assert.rejects(
389
- cli.reconcileAuthority!({
124
+ () => client(queue.adapter).reconcileAuthority({
390
125
  cwd: "/repo",
391
126
  predecessorLineage: "predecessor",
392
127
  expectedPredecessorRevision: "predecessor-revision",
393
128
  successorLineage: "successor",
394
- expectedSuccessorRevision: "changed-revision",
129
+ expectedSuccessorRevision: "successor-revision",
395
130
  actor: "maintainer",
396
- reason: "invalid recovery edge",
397
- maintainerAuthorization: RECONCILE_AUTHORIZATION,
131
+ reason: "repair authority edge",
132
+ maintainerAuthorization: "tampered",
398
133
  }),
399
134
  /exact target and revision binding/,
400
135
  );
401
- assert.equal(calls.length, 0);
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)}`,
176
+ };
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
+ ]);
402
184
  });
403
185
 
404
- test("native reconcile-authority forwards cancellation and preserves unknown mutation outcome", async () => {
405
- const controller = new AbortController();
406
- let calls = 0;
407
- const adapter: ExecFileAdapter = async (request) => {
408
- calls += 1;
409
- if (calls === 1) return { ...VERSION_219, stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
410
- assert.equal(request.signal, controller.signal);
411
- const error = new Error("cancelled");
412
- error.name = "AbortError";
413
- throw error;
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",
414
196
  };
415
- const cli = new NativeReviewCliV214(adapter);
416
- await assert.rejects(
417
- cli.reconcileAuthority!({
418
- cwd: "/repo",
419
- predecessorLineage: "predecessor",
420
- expectedPredecessorRevision: "predecessor-revision",
421
- successorLineage: "successor",
422
- expectedSuccessorRevision: "successor-revision",
423
- actor: "maintainer",
424
- reason: "invalid recovery edge",
425
- maintainerAuthorization: RECONCILE_AUTHORIZATION,
426
- signal: controller.signal,
427
- }),
428
- (error: unknown) => error instanceof NativeReviewCliError
429
- && error.code === NATIVE_REVIEW_ERROR_CODE.CANCELLED
430
- && error.operation === "review/reconcile-authority"
431
- && error.mutationOutcome === "unknown",
432
- );
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);
433
204
  });
434
205
 
435
- test("native reconcile-authority preserves the prepared audit record on partial failure", async () => {
436
- const { adapter } = queuedAdapter([VERSION_219, { stdout: JSON.stringify(RECONCILE_RESULT), stderr: "quarantine interrupted", exitCode: 1 }]);
437
- const cli = new NativeReviewCliV214(adapter);
438
- await assert.rejects(
439
- cli.reconcileAuthority!({
440
- cwd: "/repo",
441
- predecessorLineage: "predecessor",
442
- expectedPredecessorRevision: "predecessor-revision",
443
- successorLineage: "successor",
444
- expectedSuccessorRevision: "successor-revision",
445
- actor: "maintainer",
446
- reason: "invalid recovery edge",
447
- maintainerAuthorization: RECONCILE_AUTHORIZATION,
448
- }),
449
- (error: unknown) => error instanceof NativeReviewCliError
450
- && error.mutationOutcome === "unknown"
451
- && error.nextAction === "review.status"
452
- && error.auditRecord?.schema === RECONCILE_RECORD.schema,
453
- );
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);
454
213
  });
455
214
 
456
- test("native recovery wrappers accept the published 2.1.9 contract and refuse older recovery binaries", async () => {
457
- const { adapter } = queuedAdapter([{ stdout: "gentle-ai 2.1.7\n" }]);
458
- const cli = new NativeReviewCliV214(adapter);
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" } }) }]);
459
221
  await assert.rejects(
460
- cli.reclaim!({ cwd: "/repo", lineage: "stuck", actor: "maintainer", reason: "incomplete" }),
461
- (error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE,
222
+ () => client(queue.adapter).abandon({ ...request, maintainerAuthorization: nativeReviewAbandonAuthorization(request) }),
223
+ (error: unknown) => error instanceof NativeReviewCliError && error.mutationOutcome === "unknown" && error.auditRecord?.status === "partial",
462
224
  );
225
+ assert.equal(queue.calls.length, 1);
463
226
  });
464
227
 
465
- test("RESET maps to native review reclaim with the exact audited inputs and clears pending authorizations", async () => {
466
- const { native, calls } = fakeRecoveryNative(RECLAIM_RECORD);
467
- const pending = new Map<string, unknown>([["stale", { command: "git push" }]]);
468
- const details = await runControllerOperation({
469
- operation: "reset",
470
- input: JSON.stringify({
471
- repositoryId: "repo-id",
472
- commonDirHash: "c".repeat(64),
473
- inventoryHash: "d".repeat(64),
474
- confirmation: "DESTROY REVIEW AUTHORITY repo-id",
475
- lineage: "stuck-lineage",
476
- actor: "maintainer",
477
- reason: "incomplete entry",
478
- }),
479
- }, native, pending);
480
- assert.equal(details.operation, "reset");
481
- assert.equal(details.native_operation, "review reclaim");
482
- assert.equal(details.mutation_performed, true);
483
- assert.equal(details.mutation_outcome, "committed");
484
- assert.deepEqual(details.result, RECLAIM_RECORD);
485
- assert.equal(details.next_action, "inspect");
486
- assert.equal(calls.length, 1);
487
- assert.equal(calls[0]?.operation, "reclaim");
488
- assert.equal(calls[0]?.request.lineage, "stuck-lineage");
489
- assert.equal(calls[0]?.request.actor, "maintainer");
490
- assert.equal(calls[0]?.request.reason, "incomplete entry");
491
- assert.equal(pending.size, 0);
492
- });
493
-
494
- test("RESET without the native reclaim inputs returns a structured request instead of inventing values", async () => {
495
- const { native, calls } = fakeRecoveryNative(RECLAIM_RECORD);
496
- const pending = new Map<string, unknown>([["stale", { command: "git push" }]]);
497
- const details = await runControllerOperation({
498
- operation: "reset",
499
- input: JSON.stringify({
500
- repositoryId: "repo-id",
501
- commonDirHash: "c".repeat(64),
502
- inventoryHash: "d".repeat(64),
503
- confirmation: "DESTROY REVIEW AUTHORITY repo-id",
504
- }),
505
- }, native, pending);
506
- assert.equal(details.status, "blocked");
507
- assert.equal(details.outcome, "native-input-required");
508
- assert.equal(details.native_operation, "review reclaim");
509
- assert.deepEqual(details.missing_input, ["lineage", "actor", "reason"]);
510
- assert.equal(details.mutation_performed, false);
511
- assert.equal(details.mutation_outcome, "none");
512
- assert.equal(calls.length, 0);
513
- assert.equal(pending.size, 1);
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");
514
234
  });
515
235
 
516
- test("RESET without a native client fails closed as unavailable", async () => {
517
- const details = await runControllerOperation({
518
- operation: "reset",
519
- input: JSON.stringify({ lineage: "stuck", actor: "maintainer", reason: "incomplete" }),
520
- }, null);
521
- assert.equal(details.status, "blocked");
522
- assert.equal(details.outcome, "native-recovery-unavailable");
523
- assert.equal(details.native_operation, "review reclaim");
524
- assert.equal(details.mutation_performed, false);
525
- });
526
-
527
- test("RECOVER maps to native review recover with the successor authority binding", async () => {
528
- const { native, calls } = fakeRecoveryNative(RECOVER_RECORD);
529
- const details = await runControllerOperation({
530
- operation: "recover",
531
- input: JSON.stringify({
532
- repositoryId: "repo-id",
533
- commonDirHash: "c".repeat(64),
534
- inventoryHash: "d".repeat(64),
535
- confirmation: "DESTROY REVIEW AUTHORITY repo-id",
536
- predecessorLineage: "broken",
537
- expectedPredecessorRevision: "rev-1",
538
- successorLineage: "successor",
539
- disposition: "invalidated",
540
- actor: "maintainer",
541
- reason: "invalid authority",
542
- maintainerAuthorization: "binding",
543
- }),
544
- }, native);
545
- assert.equal(details.native_operation, "review recover");
546
- assert.equal(details.mutation_performed, true);
547
- assert.deepEqual(details.result, RECOVER_RECORD);
548
- assert.equal(calls.length, 1);
549
- assert.equal(calls[0]?.operation, "recover");
550
- assert.equal(calls[0]?.request.predecessorLineage, "broken");
551
- assert.equal(calls[0]?.request.expectedPredecessorRevision, "rev-1");
552
- assert.equal(calls[0]?.request.successorLineage, "successor");
553
- assert.equal(calls[0]?.request.disposition, "invalidated");
554
- assert.equal(calls[0]?.request.maintainerAuthorization, "binding");
555
- });
556
-
557
- test("RECOVER surfaces every missing successor input including an unsupported disposition", async () => {
558
- const { native, calls } = fakeRecoveryNative(RECOVER_RECORD);
559
- const details = await runControllerOperation({
560
- operation: "recover",
561
- input: JSON.stringify({
562
- repositoryId: "repo-id",
563
- commonDirHash: "c".repeat(64),
564
- inventoryHash: "d".repeat(64),
565
- confirmation: "DESTROY REVIEW AUTHORITY repo-id",
566
- predecessorLineage: "broken",
567
- disposition: "not-a-disposition",
568
- }),
569
- }, native);
570
- assert.equal(details.outcome, "native-input-required");
571
- assert.equal(details.native_operation, "review recover");
572
- assert.deepEqual(details.missing_input, ["expectedPredecessorRevision", "successorLineage", "disposition", "actor", "reason"]);
573
- assert.equal(calls.length, 0);
574
- });
575
-
576
- test("RECONCILE_AUTHORITY routes one exact native mutation and returns its audit record", async () => {
577
- const { native, calls } = fakeRecoveryNative(RECONCILE_RECORD);
578
- const pending = new Map<string, unknown>([["stale", { command: "git push" }]]);
579
- const details = await runControllerOperation({
580
- operation: "reconcile-authority",
581
- input: JSON.stringify({
582
- predecessorLineage: "predecessor",
583
- expectedPredecessorRevision: "predecessor-revision",
584
- successorLineage: "successor",
585
- expectedSuccessorRevision: "successor-revision",
586
- actor: "maintainer",
587
- reason: "invalid recovery edge",
588
- }),
589
- }, native, pending);
590
- assert.equal(details.operation, "reconcile-authority");
591
- assert.equal(details.native_operation, "review reconcile-authority");
592
- assert.equal(details.mutation_performed, true);
593
- assert.equal(details.mutation_outcome, "committed");
594
- assert.deepEqual(details.result, RECONCILE_RECORD);
595
- assert.equal(details.next_action, "inspect");
596
- assert.equal(calls.length, 1);
597
- assert.equal(calls[0]?.operation, "reconcileAuthority");
598
- assert.equal(calls[0]?.request.expectedPredecessorRevision, "predecessor-revision");
599
- assert.equal(calls[0]?.request.expectedSuccessorRevision, "successor-revision");
600
- assert.equal(calls[0]?.request.maintainerAuthorization, RECONCILE_AUTHORIZATION);
601
- assert.equal(pending.size, 0);
602
- });
603
-
604
- test("RECONCILE_AUTHORITY requests every exact native binding before authorization or mutation", async () => {
605
- const { native, calls } = fakeRecoveryNative(RECONCILE_RECORD);
606
- const details = await runControllerOperation({ operation: "reconcile-authority", input: "{}" }, native);
607
- assert.equal(details.status, "blocked");
608
- assert.equal(details.outcome, "native-input-required");
609
- assert.deepEqual(details.missing_input, ["predecessorLineage", "expectedPredecessorRevision", "successorLineage", "expectedSuccessorRevision", "actor", "reason"]);
610
- assert.equal(details.mutation_performed, false);
611
- assert.equal(details.mutation_outcome, "none");
612
- assert.equal(calls.length, 0);
613
- });
614
-
615
- test("RECONCILE_AUTHORITY returns a typed fail-closed envelope for native cancellation", async () => {
236
+ test("RECOVER rejects caller-authored authorization before status, UI, or native mutation", async () => {
237
+ let statusCalls = 0;
616
238
  const native = {
617
- async reconcileAuthority() {
618
- throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.CANCELLED, "review/reconcile-authority", true, true, "native process was cancelled");
619
- },
620
- } as unknown as NativeReviewCli;
621
- const details = await runControllerOperation({
622
- operation: "reconcile-authority",
623
- input: JSON.stringify({
624
- predecessorLineage: "predecessor",
625
- expectedPredecessorRevision: "predecessor-revision",
626
- successorLineage: "successor",
627
- expectedSuccessorRevision: "successor-revision",
628
- actor: "maintainer",
629
- reason: "invalid recovery edge",
630
- }),
631
- }, native);
632
- assert.equal(details.status, "blocked");
633
- assert.equal(details.outcome, "native-operation-failed");
634
- assert.equal(details.mutation_outcome, "unknown");
635
- assert.equal(details.replayability, "status_required");
636
- assert.equal(details.next_action, "review.status");
637
- assert.deepEqual(details.diagnostics, {
638
- operation: "review/reconcile-authority",
639
- error_code: "cancelled",
640
- timed_out: false,
641
- output_limit_exceeded: false,
642
- });
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");
643
248
  });
644
249
 
645
- test("RECONCILE_AUTHORITY relays a partial-failure audit record without weakening status reconciliation", async () => {
646
- const native = {
647
- async reconcileAuthority() {
648
- throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, "review/reconcile-authority", true, true, "partial failure", undefined, RECONCILE_RECORD);
649
- },
650
- } as unknown as NativeReviewCli;
651
- const details = await runControllerOperation({
652
- operation: "reconcile-authority",
653
- input: JSON.stringify({ predecessorLineage: "predecessor", expectedPredecessorRevision: "predecessor-revision", successorLineage: "successor", expectedSuccessorRevision: "successor-revision", actor: "maintainer", reason: "invalid recovery edge" }),
654
- }, native);
655
- assert.equal(details.mutation_outcome, "unknown");
656
- assert.equal(details.next_action, "review.status");
657
- assert.deepEqual(details.native_audit_record, RECONCILE_RECORD);
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");
658
256
  });
659
257
 
660
- test("RECOVER_LOCK still requires the exact ownerHash before routing to native reclaim", async () => {
661
- const { native, calls } = fakeRecoveryNative(RECLAIM_RECORD);
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([]);
662
271
  await assert.rejects(
663
- runControllerOperation({ operation: "recover-lock", input: JSON.stringify({ lineage: "stuck", actor: "maintainer", reason: "stale lock" }) }, native),
664
- /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/,
665
274
  );
666
- assert.equal(calls.length, 0);
667
- const details = await runControllerOperation({
668
- operation: "recover-lock",
669
- input: JSON.stringify({ ownerHash: "a".repeat(64), lineage: "stuck", actor: "maintainer", reason: "stale lock" }),
670
- }, native);
671
- assert.equal(details.native_operation, "review reclaim");
672
- assert.equal(details.mutation_performed, true);
673
- assert.deepEqual(details.result, RECLAIM_RECORD);
674
- assert.equal(calls.length, 1);
675
- assert.equal(calls[0]?.operation, "reclaim");
676
- assert.equal(calls[0]?.request.lineage, "stuck");
275
+ assert.equal(queue.calls.length, 0);
677
276
  });
678
277
 
679
- test("RECOVER_LOCK without the native reclaim inputs requests them explicitly", async () => {
680
- const { native, calls } = fakeRecoveryNative(RECLAIM_RECORD);
681
- const details = await runControllerOperation({
682
- operation: "recover-lock",
683
- input: JSON.stringify({ ownerHash: "a".repeat(64) }),
684
- }, native);
685
- assert.equal(details.outcome, "native-input-required");
686
- assert.deepEqual(details.missing_input, ["lineage", "actor", "reason"]);
687
- assert.equal(calls.length, 0);
688
- });
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
+ }
689
293
 
690
- test("destructive RESET still fails closed without fresh interactive authorization", async () => {
691
- const tools = new Map<string, { execute: (id: string, params: unknown, signal: undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<unknown> }>();
692
- 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 })({
693
297
  on() {},
694
- registerTool(definition: { name: string; execute: never }) {
695
- tools.set(definition.name, definition as unknown as { execute: (id: string, params: unknown, signal: undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<unknown> });
696
- },
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); },
697
299
  registerCommand() {},
698
- } as unknown as ExtensionAPI;
699
- const { native, calls } = fakeRecoveryNative(RECLAIM_RECORD);
700
- createGentleAiExtension({ nativeReviewCli: native })(pi);
300
+ } as unknown as ExtensionAPI);
701
301
  const controller = tools.get("gentle_review");
702
302
  assert.ok(controller);
703
- const cwd = scratchDir("gentle-pi-native-recovery-headless-");
704
- const ctx = { cwd, hasUI: false, ui: { confirm: async () => true } } as unknown as ExtensionContext;
303
+ return controller;
304
+ }
305
+
306
+ function interactiveContext(confirm: boolean): ExtensionContext {
307
+ return { cwd: process.cwd(), hasUI: true, ui: { confirm: async () => confirm } } as unknown as ExtensionContext;
308
+ }
309
+
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;
322
+ };
705
323
  await assert.rejects(
706
- controller.execute("headless-reset", {
707
- operation: "reset",
708
- input: JSON.stringify({
709
- repositoryId: "repo-id",
710
- commonDirHash: "c".repeat(64),
711
- inventoryHash: "d".repeat(64),
712
- confirmation: "DESTROY REVIEW AUTHORITY repo-id",
713
- lineage: "stuck",
714
- actor: "maintainer",
715
- reason: "incomplete",
716
- }),
717
- }, undefined, undefined, ctx),
718
- /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",
719
326
  );
720
- assert.equal(calls.length, 0);
721
- });
722
-
723
- test("RECONCILE_AUTHORITY requires fresh Pi approval for the exact seven-line binding", async () => {
724
- const tools = new Map<string, { execute: (id: string, params: unknown, signal: undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<unknown> }>();
725
- const pi = {
726
- on() {},
727
- registerTool(definition: { name: string; execute: never }) { tools.set(definition.name, definition as never); },
728
- registerCommand() {},
729
- } as unknown as ExtensionAPI;
730
- const { native, calls } = fakeRecoveryNative(RECONCILE_RECORD);
731
- createGentleAiExtension({ nativeReviewCli: native })(pi);
732
- const controller = tools.get("gentle_review");
733
- assert.ok(controller);
734
- const cwd = scratchDir("gentle-pi-native-reconcile-authorization-");
735
- const parameters = {
736
- operation: "reconcile-authority",
737
- input: JSON.stringify({
738
- predecessorLineage: "predecessor",
739
- expectedPredecessorRevision: "predecessor-revision",
740
- successorLineage: "successor",
741
- expectedSuccessorRevision: "successor-revision",
742
- actor: "maintainer",
743
- reason: "invalid recovery edge",
744
- }),
327
+ const reconcile = {
328
+ cwd: "/repo", predecessorLineage: "predecessor", expectedPredecessorRevision: "revision-1",
329
+ successorLineage: "successor", expectedSuccessorRevision: "revision-2", actor: "maintainer", reason: "repair edge",
745
330
  };
746
331
  await assert.rejects(
747
- controller.execute("headless-reconcile", parameters, undefined, undefined, { cwd, hasUI: false, ui: { confirm: async () => true } } as unknown as ExtensionContext),
748
- /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",
749
334
  );
750
- let prompt = "";
751
- const approved = await controller.execute("approved-reconcile", parameters, undefined, undefined, {
752
- cwd,
753
- hasUI: true,
754
- ui: { confirm: async (_title: string, message: string) => { prompt = message; return true; } },
755
- } as unknown as ExtensionContext) as { details: Record<string, unknown> };
756
- assert.match(prompt, /predecessor_revision=predecessor-revision/);
757
- assert.match(prompt, /successor_revision=successor-revision/);
758
- assert.equal(approved.details.mutation_outcome, "committed");
759
- assert.equal(calls.length, 1);
760
335
  });
761
336
 
762
- test("published maintenance controller actions require exact inputs and fresh UI approval", async () => {
763
- const tools = new Map<string, { execute: (id: string, params: unknown, signal: undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<unknown> }>();
764
- const pi = { on() {}, registerTool(definition: { name: string; execute: never }) { tools.set(definition.name, definition as never); }, registerCommand() {} } as unknown as ExtensionAPI;
765
- const { native, calls } = fakeRecoveryNative(ABANDON_RECORD);
766
- createGentleAiExtension({ nativeReviewCli: native })(pi);
767
- const controller = tools.get("gentle_review");
768
- assert.ok(controller);
769
- const cwd = scratchDir("gentle-pi-v219-maintenance-");
770
- const abandon = { operation: "abandon", input: JSON.stringify({ lineage: "pristine", expectedRevision: "revision", snapshotIdentity: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", actor: "maintainer", reason: "retire pristine lineage" }) };
771
- 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);
772
- await assert.rejects(controller.execute("denied-abandon", abandon, undefined, undefined, { cwd, hasUI: true, ui: { confirm: async () => false } } as unknown as ExtensionContext), /not explicitly authorized/);
773
- const approved = await controller.execute("approved-abandon", abandon, undefined, undefined, { cwd, hasUI: true, ui: { confirm: async () => true } } as unknown as ExtensionContext) as { details: Record<string, unknown> };
774
- assert.equal(approved.details.mutation_outcome, "committed");
775
- assert.equal(calls[0]?.operation, "abandon");
776
- 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> };
777
- assert.equal(legacy.details.mutation_outcome, "committed");
778
- assert.equal(calls[1]?.operation, "quarantineLegacy");
779
- for (const input of [
780
- { 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" }) },
781
- { 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" }) },
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" }),
357
+ ]) {
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");
364
+ }
365
+
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" }),
782
394
  ]) {
783
- const details = await runControllerOperation(input, native);
784
- assert.equal(details.outcome, "native-input-invalid");
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");
785
401
  }
786
- assert.equal(calls.length, 2);
787
402
  });
788
403
 
789
- test("REPAIR_LEGACY_ALIAS derives fixed inputs from fresh inventory and requires fresh UI approval", async () => {
790
- const tools = new Map<string, { execute: (id: string, params: unknown, signal: undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<unknown> }>();
791
- const pi = { on() {}, registerTool(definition: { name: string; execute: never }) { tools.set(definition.name, definition as never); }, registerCommand() {} } as unknown as ExtensionAPI;
792
- const calls: Record<string, unknown>[] = [];
404
+ test("destructive maintenance remains UI-gated and derives authorization at the public controller boundary", async () => {
405
+ const calls: Array<Record<string, unknown>> = [];
793
406
  const native = {
794
- async reviewStatus() {
795
- return {
796
- repository: "/canonical/repository",
797
- complete: true,
798
- entries: [{ version: "legacy-v1", status: "invalid", lineageId: "legacy-alias", revision: `sha256:${"c".repeat(64)}`, problems: [LEGACY_ALIAS_DIAGNOSTIC] }],
799
- };
800
- },
801
- async repairLegacyAlias(request: Record<string, unknown>) {
802
- calls.push(request);
803
- return { record: LEGACY_ALIAS_RECORD };
804
- },
805
- } as unknown as NativeReviewCli;
806
- createGentleAiExtension({ nativeReviewCli: native })(pi);
807
- const controller = tools.get("gentle_review");
808
- assert.ok(controller);
809
- const cwd = scratchDir("gentle-pi-v2110-alias-repair-");
810
- const parameters = { operation: "repair-legacy-alias", input: JSON.stringify({ lineage: "legacy-alias", actor: "maintainer", reason: "quarantine approved historical alias" }) };
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");
811
418
  await assert.rejects(
812
- controller.execute("headless-alias-repair", parameters, undefined, undefined, { cwd, hasUI: false, ui: { confirm: async () => true } } as unknown as ExtensionContext),
813
- /interactive Pi UI.*fails closed/i,
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/,
814
421
  );
815
- assert.equal(calls.length, 0);
816
- let prompt = "";
817
- const approved = await controller.execute("approved-alias-repair", parameters, undefined, undefined, {
818
- cwd,
819
- hasUI: true,
820
- ui: { confirm: async (_title: string, message: string) => { prompt = message; return true; } },
821
- } as unknown as ExtensionContext) as { details: Record<string, unknown> };
822
- assert.match(prompt, /repository=\/canonical\/repository/);
823
- assert.match(prompt, /revision=sha256:c{64}/);
824
- assert.equal(approved.details.mutation_outcome, "committed");
825
- assert.equal(calls[0]?.repository, "/canonical/repository");
826
- assert.equal(calls[0]?.diagnostic, LEGACY_ALIAS_DIAGNOSTIC);
827
- assert.equal(calls[0]?.disposition, LEGACY_ALIAS_DISPOSITION);
828
- const injected = await runControllerOperation({ operation: "repair-legacy-alias", input: JSON.stringify({ lineage: "legacy-alias", actor: "maintainer", reason: "no-op", repository: "/attacker" }) }, native);
829
- assert.equal(injected.outcome, "native-input-invalid");
830
- await assert.rejects(runControllerOperation({ operation: "dispose-result", input: "{}" }, native), /operation/);
831
- });
832
422
 
833
- // The capture-result gap, found by benchmarking Pi's client against the real
834
- // binary: `finalize()` emitted `--result <file>` per lens, a flag gentle-ai
835
- // retired because "a reviewer result supplied this way carries no
836
- // provider-owned admission, so it cannot prove the lens inspected the frozen
837
- // candidate". There was no capture-result surface at all, so Pi could only
838
- // finalize a zero-lens low-risk candidate. The suite never caught it because
839
- // it mocks the finalize response.
840
- //
841
- // Two inverse contracts meet here. `repair()` above MUST carry --contract.
842
- // `capture-result` MUST NOT: it is an additive headless command, not a
843
- // negotiated repository operation, and the provider's own tokens already
844
- // carry the repository context -- it accepts that or --cwd, never both. So
845
- // Pi passes the transition's tokens through verbatim and adds only --input.
846
- test("captureResult passes the provider tokens through verbatim and carries no --contract", async (t) => {
847
- t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
848
- const manifest = {
849
- schema: "gentle-ai.review-result-artifact/v2",
850
- capability: "review.native_result_artifact",
851
- subject_hash: "sha256:" + "a".repeat(64),
852
- admission_decision: "completed",
853
- lens: "review-reliability",
854
- reference: "rref1_" + "b".repeat(64),
855
- };
856
- const { adapter, calls } = queuedAdapter([{ stdout: JSON.stringify(manifest) }]);
857
- const cli = new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
858
-
859
- const tokens = [
860
- "--lineage=review-1d5aadacc600e167",
861
- "--expected-revision=sha256:" + "c".repeat(64),
862
- "--target=sha256:" + "d".repeat(64),
863
- "--repository-context=rctx1_" + "e".repeat(64),
864
- "--lens=review-reliability",
865
- "--order=0",
866
- "--subject-hash=" + manifest.subject_hash,
867
- ];
868
- 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"] }) });
869
-
870
- assert.equal(captured.subjectHash, manifest.subject_hash);
871
- assert.equal(captured.admissionDecision, "completed");
872
-
873
- // Exactly one invocation: capture-result is headless and never negotiates,
874
- // so it must not drag a capabilities preflight along with it.
875
- assert.equal(calls.length, 1);
876
- const argv = calls[0]!.arguments;
877
- assert.deepEqual(argv.slice(0, 2), ["review", "capture-result"]);
878
- assert.equal(argv.includes("--contract"), false, "capture-result accepts no --contract");
879
- assert.equal(argv.includes("--cwd"), false, "the provider tokens already carry the repository context");
880
- // Tokens pass through in order, untouched, and --input is the only addition.
881
- assert.deepEqual(argv.slice(2, 2 + tokens.length), tokens);
882
- assert.equal(argv.at(-2), "--input");
883
- assert.match(argv.at(-1) as string, /\S/);
884
- assert.equal(argv.length, 2 + tokens.length + 2);
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/);
885
426
  });
886
427
 
887
- test("captureEvidence stages exact bytes, uses the closed outcome argv, and decodes the native record", async (t) => {
888
- t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
889
- const capabilities = v2Fixture<Record<string, unknown>>("capabilities.fixture.json");
890
- const capabilitiesBody = { ...capabilities, package: { ...(capabilities.package as Record<string, unknown>), version: GENTLE_AI_VERSION } };
891
- const record = {
892
- schema: "gentle-ai.review-verification-evidence/v2",
893
- version: 2,
894
- lineage_id: "review-evidence-lineage",
895
- authority_revision: `sha256:${"a".repeat(64)}`,
896
- target_identity: `sha256:${"b".repeat(64)}`,
897
- candidate_tree: "c".repeat(40),
898
- paths_digest: `sha256:${"d".repeat(64)}`,
899
- paths: ["app.ts"],
900
- ledger_ids: [],
901
- raw_payload_sha256: `sha256:${"e".repeat(64)}`,
902
- raw_payload_bytes: 24,
903
- outcome: "verification_failed",
904
- record_digest: `sha256:${"f".repeat(64)}`,
905
- };
906
- let staged = "";
907
- const calls: Array<{ arguments: readonly string[] }> = [];
908
- const cli = new NativeReviewCliV216(async (request) => {
909
- calls.push({ arguments: request.arguments });
910
- if (request.arguments[1] === "capabilities") return { stdout: JSON.stringify(capabilitiesBody), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
911
- const inputIndex = request.arguments.indexOf("--input");
912
- assert.ok(inputIndex >= 0);
913
- staged = readFileSync(request.arguments[inputIndex + 1]!, "utf8");
914
- return { stdout: JSON.stringify(record), stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
915
- }, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
916
- const evidence = "focused verification failed\n";
917
- const captured = await cli.captureEvidence({
918
- cwd: "/repo",
919
- lineageId: record.lineage_id,
920
- targetIdentity: record.target_identity,
921
- expectedRevision: record.authority_revision,
922
- outcome: "verification_failed",
923
- evidenceDocument: evidence,
924
- });
925
- assert.equal(staged, evidence);
926
- assert.equal(captured.recordDigest, record.record_digest);
927
- assert.equal(captured.outcome, "verification_failed");
928
- const argv = calls[1]!.arguments;
929
- assert.deepEqual(argv.slice(0, 2), ["review", "capture-evidence"]);
930
- assert.equal(argv.includes("--contract"), false);
931
- assert.equal(argv[argv.indexOf("--outcome") + 1], "verification_failed");
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;
932
435
  await assert.rejects(
933
- cli.captureEvidence({ cwd: "/repo", lineageId: record.lineage_id, targetIdentity: record.target_identity, expectedRevision: record.authority_revision, outcome: "failed" as never, evidenceDocument: evidence }),
934
- /outcome must be passed, verification_failed, or procedural_tooling_failed/,
436
+ __testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify(input) }, process.cwd(), native),
437
+ /fresh explicit authorization through the interactive Pi UI/,
935
438
  );
936
- assert.equal(calls.length, 2, "outside-domain outcomes must fail before another native launch");
439
+ await assert.rejects(
440
+ __testing.executeReviewControllerOperation({ operation: "recover", input: JSON.stringify(input) }, process.cwd(), native, undefined, undefined, interactiveContext(false)),
441
+ /not explicitly authorized/,
442
+ );
443
+ await assert.rejects(
444
+ __testing.executeReviewControllerOperation({ operation: "recover-lock", input: JSON.stringify({ lineage: "stuck", actor: "maintainer", reason: "stale" }) }, process.cwd(), native),
445
+ /ownerHash/,
446
+ );
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");
937
451
  });
938
452
 
939
- test("negotiated finalize never emits the retired --result flag", async (t) => {
940
- t.after(() => clearNativeReviewCapabilitiesCacheForTesting());
941
- const capabilities = v2Fixture<Record<string, unknown>>("capabilities.fixture.json");
942
- const capabilitiesBody = { ...capabilities, package: { ...(capabilities.package as Record<string, unknown>), version: GENTLE_AI_VERSION } };
943
- const finalizeBody = {
944
- schema: "gentle-ai.review-integration.operation/v2",
945
- contract: "gentle-ai.review-integration/v2",
946
- operation: "review.finalize",
947
- result: { operation: "review/finalize", lineage_id: "review-1d5aadacc600e167", state: "approved", action: "validate delivery", store_revision: "sha256:" + "f".repeat(64) },
948
- };
949
- const { adapter, calls } = queuedAdapter([
950
- { stdout: JSON.stringify(capabilitiesBody) },
951
- { stdout: JSON.stringify(finalizeBody) },
952
- ]);
953
- const cli = new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705");
954
-
955
- await cli.finalize({ cwd: "/repo", lineageId: "review-1d5aadacc600e167", capturedResults: true });
956
-
957
- const argv = calls[1]!.arguments;
958
- assert.equal(argv.includes("--result"), false, "--result is retired; results reach authority through capture-result");
959
- assert.ok(argv.some((token) => token === "--captured-results" || token.startsWith("--captured-results=")), "finalize must tell the provider to discover the captured results");
960
- assert.ok(argv.includes("--contract"), "finalize IS negotiated, unlike capture-result");
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");
961
470
  });