gentle-pi 2.1.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.md +70 -12
  2. package/assets/agents/gentle-ai-worker.md +7 -3
  3. package/assets/agents/jd-fix-agent.md +1 -1
  4. package/assets/agents/jd-judge-a.md +3 -1
  5. package/assets/agents/jd-judge-b.md +3 -1
  6. package/assets/agents/review-readability.md +4 -1
  7. package/assets/agents/review-reliability.md +4 -1
  8. package/assets/agents/review-resilience.md +4 -1
  9. package/assets/agents/review-risk.md +4 -1
  10. package/assets/agents/sdd-apply.md +6 -1
  11. package/assets/agents/sdd-archive.md +6 -1
  12. package/assets/agents/sdd-design.md +6 -1
  13. package/assets/agents/sdd-explore.md +6 -2
  14. package/assets/agents/sdd-init.md +10 -2
  15. package/assets/agents/sdd-onboard.md +6 -1
  16. package/assets/agents/sdd-proposal.md +6 -1
  17. package/assets/agents/sdd-spec.md +6 -1
  18. package/assets/agents/sdd-status.md +6 -1
  19. package/assets/agents/sdd-sync.md +6 -1
  20. package/assets/agents/sdd-tasks.md +6 -1
  21. package/assets/agents/sdd-verify.md +6 -1
  22. package/assets/chains/4r-review.chain.md +2 -0
  23. package/assets/chains/sdd-full.chain.md +1 -1
  24. package/assets/chains/sdd-plan.chain.md +1 -1
  25. package/assets/chains/sdd-verify.chain.md +1 -1
  26. package/assets/orchestrator-delegation.md +246 -67
  27. package/assets/orchestrator.md +7 -14
  28. package/assets/sdd-orchestrator-workflow.md +154 -9
  29. package/assets/support/sdd-status-contract.md +19 -1
  30. package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
  31. package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
  32. package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
  33. package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
  34. package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
  35. package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
  36. package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
  37. package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
  38. package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
  39. package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
  40. package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
  41. package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
  42. package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
  43. package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
  44. package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
  45. package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
  46. package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
  47. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
  48. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
  49. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
  50. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
  51. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
  52. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
  53. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
  54. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
  55. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
  56. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
  57. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
  58. package/docs/native-authority-architecture.md +5 -5
  59. package/docs/review-integration.md +22 -2
  60. package/extensions/gentle-ai.ts +1595 -201
  61. package/extensions/sdd-init.ts +19 -6
  62. package/extensions/skill-registry.ts +10 -2
  63. package/extensions/startup-banner.ts +10 -4
  64. package/lib/gentle-ai-binary.ts +173 -2
  65. package/lib/git-commit-transaction.ts +77 -17
  66. package/lib/native-review-cli.ts +528 -65
  67. package/lib/provider-contract-bundle.ts +704 -0
  68. package/lib/review-candidate-view.ts +527 -18
  69. package/lib/review-compact-contract.ts +59 -248
  70. package/lib/review-host-relay.ts +436 -0
  71. package/lib/review-integration-v2.ts +537 -36
  72. package/lib/review-relay-contract.ts +16 -0
  73. package/lib/sdd-preflight.ts +53 -1
  74. package/package.json +5 -2
  75. package/runtime/gentle-ai-binary.mjs +173 -2
  76. package/runtime/git-commit-transaction.mjs +75 -15
  77. package/runtime/native-review-cli.mjs +524 -61
  78. package/runtime/review-integration-v2.mjs +536 -35
  79. package/runtime/review-relay-contract.mjs +17 -0
  80. package/scripts/build-git-commit-transaction-runner.mjs +1 -0
  81. package/scripts/check-provider-contract.mjs +138 -0
  82. package/scripts/gentle-ai-installer.mjs +23 -13
  83. package/scripts/maintainer/provider-relay-matrix.mjs +219 -0
  84. package/scripts/mirror-provider-contract.mjs +143 -0
  85. package/scripts/test-packed-runner.mjs +16 -2
  86. package/scripts/verify-package-files.mjs +110 -33
  87. package/skills/_shared/review-ledger-contract.md +4 -6
  88. package/skills/gentle-ai/SKILL.md +4 -4
  89. package/skills/issue-creation/SKILL.md +94 -168
  90. package/skills/judgment-day/SKILL.md +7 -1
  91. package/skills/judgment-day/references/prompts-and-formats.md +2 -0
  92. package/skills/rdd-defect-workflow/SKILL.md +54 -0
  93. package/tests/background-subagents.test.ts +771 -0
  94. package/tests/crosslane/cross-lane.mjs +1169 -0
  95. package/tests/delegated-key-learnings-contract.test.ts +238 -0
  96. package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
  97. package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
  98. package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
  99. package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
  100. package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
  101. package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
  102. package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
  103. package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
  104. package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
  105. package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
  106. package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
  107. package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
  108. package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
  109. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
  110. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
  111. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
  112. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
  113. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
  114. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
  115. package/tests/gentle-ai-binary.test.ts +1 -1
  116. package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
  117. package/tests/gentle-ai-dev-binary.test.ts +336 -0
  118. package/tests/gentle-ai-installer.test.ts +46 -46
  119. package/tests/git-commit-transaction.test.ts +229 -1
  120. package/tests/maintainer/provider-relay.maintest.ts +265 -0
  121. package/tests/native-review-capability-contract.test.ts +48 -2
  122. package/tests/native-review-cli.test.ts +56 -0
  123. package/tests/native-review-consent.test.ts +164 -3
  124. package/tests/native-review-parity-runtime.test.ts +37 -0
  125. package/tests/native-review-parity.test.ts +218 -15
  126. package/tests/native-sdd-attempt-authority.test.ts +235 -0
  127. package/tests/orchestrator-budget.test.ts +30 -5
  128. package/tests/package-manifest.test.ts +98 -72
  129. package/tests/provider-contract-bundle.test.ts +385 -0
  130. package/tests/provider-contract-mirror.test.ts +206 -0
  131. package/tests/provider-defect-handoff.test.ts +355 -0
  132. package/tests/review-actor-tool-deny.test.ts +12 -13
  133. package/tests/review-candidate-view.test.ts +489 -9
  134. package/tests/review-compact-contract.test.ts +52 -119
  135. package/tests/review-controller-native-recovery.test.ts +643 -47
  136. package/tests/review-controller-native-routing.test.ts +1667 -222
  137. package/tests/review-controller-workspace-root.test.ts +17 -2
  138. package/tests/review-corrected-finalize-binding.test.ts +175 -0
  139. package/tests/review-dispatch-hydration-gap.test.ts +197 -0
  140. package/tests/review-host-relay-routing.test.ts +317 -0
  141. package/tests/review-host-relay.test.ts +520 -0
  142. package/tests/review-integration-v2-forward.test.ts +631 -0
  143. package/tests/review-integration-v2.test.ts +114 -0
  144. package/tests/review-ledger-contract.test.ts +12 -28
  145. package/tests/review-recovered-lineage-routing.test.ts +246 -0
  146. package/tests/review-relay-transport-agent.test.ts +249 -0
  147. package/tests/runtime-harness.mjs +242 -14
  148. package/tests/sdd-agent-tools.test.ts +18 -33
  149. package/tests/skill-collision-prefixes.test.ts +1 -0
  150. package/tests/skill-registry.test.ts +50 -1
  151. package/tests/verify-package-files.test.ts +62 -0
  152. package/assets/agents/review-refuter.md +0 -40
  153. package/assets/agents/review-validator.md +0 -23
  154. package/lib/native-review-remediation.ts +0 -49
  155. package/lib/review-compact.ts +0 -947
  156. package/lib/review-refuter-adapter.ts +0 -129
  157. package/lib/review-runtime-contract.ts +0 -68
  158. package/prompts/gcl.md +0 -54
  159. package/prompts/gis.md +0 -25
  160. package/prompts/gpr.md +0 -41
  161. package/prompts/gwr.md +0 -31
  162. package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
  163. package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
  164. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
  165. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
  166. package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
  167. package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
  168. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
  169. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
  170. package/tests/review-compact.test.ts +0 -243
  171. package/tests/review-refuter-adapter.test.ts +0 -89
@@ -85,7 +85,7 @@ function fakeNative(overrides: Partial<NativeReviewCli> = {}): NativeReviewCli {
85
85
  reviewStatus: async () => ({ schema: "gentle-ai.review-authority-status/v1", repository: "/repo", complete: true, authoritative: true, status: "clean", entries: [], locks: [], diagnostics: [], raw: { schema: "gentle-ai.review-authority-status/v1", operation: "review/status", repository: "/repo", complete: true, authoritative: true, status: "clean", entries: [], locks: [], diagnostics: [] } }),
86
86
  targetStatus: async (request) => request.lineageId === undefined
87
87
  ? candidateStartTargetStatus(request)
88
- : targetStatusFixture({ lineageId: request.lineageId }),
88
+ : candidateFinalizeTargetStatus(request, request.lineageId),
89
89
  ...overrides,
90
90
  };
91
91
  }
@@ -202,6 +202,21 @@ function candidateStartTargetStatus(request: Parameters<NonNullable<NativeReview
202
202
  }
203
203
  }
204
204
 
205
+ function candidateFinalizeTargetStatus(request: Parameters<NonNullable<NativeReviewCli["targetStatus"]>>[0], lineageId: string): ReviewStatusV3 {
206
+ let candidate: ReturnType<CandidateViewRegistry["create"]> | undefined;
207
+ try {
208
+ candidate = new CandidateViewRegistry().create({ contributorRoot: request.cwd });
209
+ return targetStatusFixture({
210
+ lineageId,
211
+ baseTree: candidate.baseTree,
212
+ currentCandidateTree: candidate.candidateTree,
213
+ paths: candidate.paths,
214
+ });
215
+ } finally {
216
+ candidate?.cleanup();
217
+ }
218
+ }
219
+
205
220
  test("INSPECT and STATUS operate on the explicit workspace root while the session cwd stays elsewhere", async (t) => {
206
221
  const sessionCwd = repository(t);
207
222
  const worktree = addWorktree(t, sessionCwd, "feat-binding");
@@ -351,7 +366,7 @@ test("FINALIZE fails closed when the frozen projection belongs to a different wo
351
366
  },
352
367
  }), candidateViews);
353
368
  await controller.execute("start-finalize-b", { operation: "start", workspaceRoot: worktree, input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(sessionCwd));
354
- const input = JSON.stringify({ review_result: { lens_results: [{ lens: "review-reliability", findings: [], evidence: ["reviewed frozen candidate"] }] } });
369
+ const input = JSON.stringify({});
355
370
  const mismatch = await controller.execute("finalize-wrong-root", { operation: "finalize", lineageId: "finalize-lineage", input }, undefined, undefined, context(sessionCwd));
356
371
  assert.equal((mismatch.details as { outcome?: string }).outcome, "native-operation-failed");
357
372
  assert.equal(finalizes, 0);
@@ -0,0 +1,175 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import test from "node:test";
7
+ import { __testing } from "../extensions/gentle-ai.ts";
8
+ import type { NativeReviewCli } from "../lib/native-review-cli.ts";
9
+ import { CandidateViewRegistry } from "../lib/review-candidate-view.ts";
10
+ import type { ReviewStatusV3 } from "../lib/review-integration-v2.ts";
11
+
12
+ // Field defect (Engram #12547, lineage review-43762fca1bba0bf3): a MEDIUM
13
+ // lineage detected a reliability defect, corrected it inside budget, captured
14
+ // evidence and the native targeted validator. Negotiated STATUS then returned
15
+ // `captured_provider_targeted_validation_ready` with an execute
16
+ // `review.finalize` transition, and running that exact transition failed twice
17
+ // with `candidate-target-projection-drift`, mutation none, so no receipt was
18
+ // ever minted.
19
+ //
20
+ // Measured on a faithful live reproduction (medium candidate, real binary,
21
+ // START through the controller so the registry holds the START-time view):
22
+ //
23
+ // START candidate tree: 4c24487f850050af0a0944d37d4dbb15287ab3da
24
+ // corrected candidate tree: 777f48fdc4e6ed9abd325d6b4c49e6d06036c906
25
+ // FINALIZE (no documents) -> candidate-target-projection-drift, mutation none
26
+ // registry branch taken: ["resolveForFinalize"]
27
+ //
28
+ // Root cause: the corrected tree is tolerated only while
29
+ // `correctionCompletion` (validation AND final_evidence) or `validationAttempt`
30
+ // (final_evidence without a forecast) hold. A FINALIZE that merely follows the
31
+ // provider's own execute transition carries no documents, so both are false,
32
+ // the START-time view is resolved, and it is compared against the corrected
33
+ // candidate the provider itself authorized.
34
+
35
+ const SHA = `sha256:${"1".repeat(64)}`;
36
+ const DEFECT = "export function lastComponent(input) {\n const parts = input.split(\"/\");\n return parts[parts.length - 2];\n}\n";
37
+ const CORRECTED = "export function lastComponent(input) {\n const parts = input.split(\"/\");\n return parts[parts.length - 1];\n}\n";
38
+
39
+ function git(cwd: string, ...arguments_: string[]): string {
40
+ return execFileSync("git", arguments_, { cwd, encoding: "utf8" }).trim();
41
+ }
42
+
43
+ function repository(t: test.TestContext): string {
44
+ const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-corrected-finalize-"));
45
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
46
+ git(cwd, "init", "-b", "main");
47
+ git(cwd, "config", "user.name", "Corrected Test");
48
+ git(cwd, "config", "user.email", "corrected@example.invalid");
49
+ mkdirSync(join(cwd, "src"), { recursive: true });
50
+ writeFileSync(join(cwd, "src", "parse.js"), "export function parsePath(input) {\n return input.split(\"/\");\n}\n");
51
+ git(cwd, "add", "-A");
52
+ git(cwd, "commit", "-m", "base");
53
+ writeFileSync(join(cwd, "src", "parse.js"), DEFECT);
54
+ return cwd;
55
+ }
56
+
57
+ /** The live frozen identity of the current workspace candidate. */
58
+ function liveIdentity(cwd: string): { baseTree: string; candidateTree: string; paths: readonly string[] } {
59
+ const probe = new CandidateViewRegistry();
60
+ const view = probe.create({ contributorRoot: cwd });
61
+ try {
62
+ return { baseTree: view.baseTree, candidateTree: view.candidateTree, paths: view.paths };
63
+ } finally {
64
+ probe.cleanup(view.token);
65
+ }
66
+ }
67
+
68
+ function correctedStatus(lineageId: string, identity: { baseTree: string; candidateTree: string; paths: readonly string[] }): ReviewStatusV3 {
69
+ return {
70
+ contract: "gentle-ai.review-integration/v2",
71
+ applicability: "current_target",
72
+ authority: { version: "compact-v2", lineageId, state: "correction_required", generation: 1, revision: SHA },
73
+ receipt: { status: "expected_missing" },
74
+ action: "finalize",
75
+ replayability: "not_replayable",
76
+ frozen: { tier: "medium", originalChangedLines: 4, correctionBudget: 2 },
77
+ targetIdentity: SHA,
78
+ projection: {
79
+ schema: "gentle-ai.review-integration.projection/v1",
80
+ kind: "current-changes",
81
+ projection: "workspace",
82
+ baseTree: identity.baseTree,
83
+ initialReviewTree: identity.candidateTree,
84
+ currentCandidateTree: identity.candidateTree,
85
+ pathsDigest: SHA,
86
+ paths: [...identity.paths],
87
+ intendedUntracked: [],
88
+ intendedUntrackedProof: SHA,
89
+ initialSnapshotIdentity: SHA,
90
+ currentSnapshotIdentity: SHA,
91
+ },
92
+ candidates: [],
93
+ // The provider's own answer after it admitted the captured validator.
94
+ nextTransition: {
95
+ kind: "execute",
96
+ reasonCode: "captured_provider_targeted_validation_ready",
97
+ execute: {
98
+ operation: "review.finalize",
99
+ arguments: [
100
+ { name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
101
+ { name: "captured-evidence", value: "true", token: "--captured-evidence=true" },
102
+ ],
103
+ binding: { lineageId },
104
+ },
105
+ },
106
+ raw: { schema: "gentle-ai.review-integration.status/v5", action: "finalize", lineage_id: lineageId },
107
+ } as unknown as ReviewStatusV3;
108
+ }
109
+
110
+ function approvingNative(status: ReviewStatusV3): { native: NativeReviewCli; transitions: number } {
111
+ const state = { transitions: 0 };
112
+ const native = {
113
+ targetStatus: async () => status,
114
+ finalizeTransition: async () => {
115
+ state.transitions += 1;
116
+ return { lineageId: status.authority!.lineageId, state: "approved", action: "approved", storeRevision: "r2" };
117
+ },
118
+ finalize: async () => { throw new Error("the provider transition must be executed, not raw finalize"); },
119
+ } as unknown as NativeReviewCli;
120
+ return { native, get transitions() { return state.transitions; } } as never;
121
+ }
122
+
123
+ /** Binds the START-time immutable reviewer view, as a live session does. */
124
+ function sessionWithStartBinding(cwd: string, lineageId: string): { registry: CandidateViewRegistry; startTree: string } {
125
+ const registry = new CandidateViewRegistry();
126
+ const view = registry.createOrReuse({ contributorRoot: cwd });
127
+ registry.bindCurrent({ token: view.token, lineageId, selectedLenses: ["review-reliability"] });
128
+ return { registry, startTree: view.candidateTree };
129
+ }
130
+
131
+ test("FINALIZE follows the provider transition after a correction instead of drifting on the START view", async (t) => {
132
+ const cwd = repository(t);
133
+ const lineageId = "review-corrected-lineage";
134
+ const { registry, startTree } = sessionWithStartBinding(cwd, lineageId);
135
+ t.after(() => { try { registry.cleanup(registry.resolveForFinalize(lineageId).token); } catch { /* already cleaned */ } });
136
+
137
+ // The bounded correction lands: the candidate identity legitimately moves.
138
+ writeFileSync(join(cwd, "src", "parse.js"), CORRECTED);
139
+ const corrected = liveIdentity(cwd);
140
+ assert.notEqual(corrected.candidateTree, startTree, "the correction must move the candidate tree");
141
+
142
+ const harness = approvingNative(correctedStatus(lineageId, corrected));
143
+ const result = await __testing.executeReviewControllerOperation(
144
+ // Exactly the reporter's call: follow the provider transition, no documents.
145
+ { operation: "finalize", lineageId, input: JSON.stringify({}) },
146
+ cwd, new Map(), harness.native, undefined, undefined, undefined, registry,
147
+ ) as Record<string, unknown>;
148
+
149
+ assert.notEqual(
150
+ (result.diagnostics as { code?: string } | undefined)?.code,
151
+ "candidate-target-projection-drift",
152
+ "a corrected candidate the provider authorized must not read as reviewer-view drift",
153
+ );
154
+ assert.equal(harness.transitions, 1, "the provider's own finalize transition must run");
155
+ assert.equal((result.result as { state?: string } | undefined)?.state, "approved");
156
+ });
157
+
158
+ test("FINALIZE still fails closed when the live candidate does not match the provider projection", async (t) => {
159
+ const cwd = repository(t);
160
+ const lineageId = "review-drifted-lineage";
161
+ const { registry } = sessionWithStartBinding(cwd, lineageId);
162
+ t.after(() => { try { registry.cleanup(registry.resolveForFinalize(lineageId).token); } catch { /* already cleaned */ } });
163
+
164
+ // The provider describes a candidate this workspace cannot reproduce.
165
+ const identity = liveIdentity(cwd);
166
+ const harness = approvingNative(correctedStatus(lineageId, { ...identity, candidateTree: identity.baseTree }));
167
+ const result = await __testing.executeReviewControllerOperation(
168
+ { operation: "finalize", lineageId, input: JSON.stringify({}) },
169
+ cwd, new Map(), harness.native, undefined, undefined, undefined, registry,
170
+ ) as Record<string, unknown>;
171
+
172
+ assert.equal(result.status, "blocked", "an unverifiable candidate must never mint a receipt");
173
+ assert.equal(result.mutation_outcome, "none");
174
+ assert.equal(harness.transitions, 0, "no provider transition may run for an unverifiable candidate");
175
+ });
@@ -0,0 +1,197 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import test from "node:test";
7
+ import { __testing } from "../extensions/gentle-ai.ts";
8
+ import type { NativeReviewCli } from "../lib/native-review-cli.ts";
9
+ import { CandidateViewError, CandidateViewRegistry, injectReviewCandidateView } from "../lib/review-candidate-view.ts";
10
+ import type { ReviewCollectInputV3, ReviewStatusV3 } from "../lib/review-integration-v2.ts";
11
+
12
+ // Field report (2026-08-16, gentle-pi main 402f9f77 + gentle-ai
13
+ // 2.4.0-main.20278905): after #340 the FINALIZE routing defect was fixed but
14
+ // the Pi relay's candidate-view registration still refused for an externally
15
+ // recovered successor. Reproduced against the live binary: hydration itself
16
+ // works for that shape (a dirty LINKED worktree hydrates fine) — the gap is
17
+ // that hydration was wired into the STATUS controller operation ONLY, while
18
+ // the maintainer's flow is `finalize` (now correctly blocked with
19
+ // review.capture-result) followed by a reviewer dispatch. The FINALIZE lane
20
+ // decodes the same authoritative status and never hydrated from it.
21
+ //
22
+ // Second defect in the same area: every hydration failure was swallowed by a
23
+ // bare catch, so the later dispatch refusal claimed no binding existed
24
+ // without ever admitting hydration had been attempted and why it failed.
25
+
26
+ const SHA = `sha256:${"1".repeat(64)}`;
27
+
28
+ function git(cwd: string, ...arguments_: string[]): string {
29
+ return execFileSync("git", arguments_, { cwd, encoding: "utf8" }).trim();
30
+ }
31
+
32
+ /** A LINKED worktree (not the primary checkout) with dirty tracked files. */
33
+ function linkedDirtyWorktree(t: test.TestContext): string {
34
+ const root = mkdtempSync(join(tmpdir(), "gentle-pi-hydration-gap-"));
35
+ t.after(() => rmSync(root, { recursive: true, force: true }));
36
+ const primary = join(root, "primary");
37
+ const linked = join(root, "linked");
38
+ mkdirSync(primary, { recursive: true });
39
+ git(primary, "init", "-b", "main");
40
+ git(primary, "config", "user.name", "Hydration Test");
41
+ git(primary, "config", "user.email", "hydration@example.invalid");
42
+ mkdirSync(join(primary, "internal"), { recursive: true });
43
+ writeFileSync(join(primary, "internal", "parser.go"), "package internal\n\nfunc Parse(s string) string {\n\treturn s\n}\n");
44
+ git(primary, "add", "-A");
45
+ git(primary, "commit", "-m", "base");
46
+ git(primary, "worktree", "add", linked, "-b", "feature/fence-info");
47
+ // Uncommitted tracked modification, exactly the field shape.
48
+ writeFileSync(join(linked, "internal", "parser.go"), "package internal\n\nimport \"strings\"\n\nfunc Parse(s string) string {\n\treturn strings.TrimSpace(s)\n}\n");
49
+ return linked;
50
+ }
51
+
52
+ function reviewerResultCollectInput(lineageId: string, lens: string): ReviewCollectInputV3 {
53
+ return {
54
+ name: "reviewer_result",
55
+ schema: "https://gentle-ai.dev/schema/review/reviewer/v1",
56
+ captureOperation: "review.capture-result",
57
+ arguments: [
58
+ { name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
59
+ { name: "expected-revision", value: SHA, token: `--expected-revision=${SHA}` },
60
+ { name: "lens", value: lens, token: `--lens=${lens}` },
61
+ { name: "order", value: "0", token: "--order=0" },
62
+ ],
63
+ artifactSubject: {
64
+ schema: "gentle-ai.review-artifact-subject/v2",
65
+ subjectHash: SHA,
66
+ lineageId,
67
+ authorityRevision: SHA,
68
+ targetIdentity: SHA,
69
+ baseTree: "3".repeat(40),
70
+ candidateTree: "4".repeat(40),
71
+ changedPathManifestSha256: SHA,
72
+ lens,
73
+ selectedOrder: 0,
74
+ },
75
+ } as unknown as ReviewCollectInputV3;
76
+ }
77
+
78
+ function successorStatus(lineageId: string, projection: { baseTree: string; currentCandidateTree: string; paths: readonly string[] }): ReviewStatusV3 {
79
+ return {
80
+ contract: "gentle-ai.review-integration/v2",
81
+ applicability: "current_target",
82
+ authority: { version: "compact-v2", lineageId, state: "reviewer_results_required", generation: 2, revision: SHA },
83
+ receipt: { status: "expected_missing" },
84
+ action: "finalize",
85
+ replayability: "not_replayable",
86
+ targetIdentity: SHA,
87
+ projection: {
88
+ schema: "gentle-ai.review-integration.projection/v1",
89
+ kind: "current-changes",
90
+ projection: "workspace",
91
+ baseTree: projection.baseTree,
92
+ initialReviewTree: projection.currentCandidateTree,
93
+ currentCandidateTree: projection.currentCandidateTree,
94
+ pathsDigest: SHA,
95
+ paths: [...projection.paths],
96
+ intendedUntracked: [],
97
+ intendedUntrackedProof: SHA,
98
+ initialSnapshotIdentity: SHA,
99
+ currentSnapshotIdentity: SHA,
100
+ },
101
+ candidates: [],
102
+ nextTransition: { kind: "collect", reasonCode: "reviewer_results_required", collect: { inputs: [reviewerResultCollectInput(lineageId, "review-reliability")] } },
103
+ raw: { schema: "gentle-ai.review-integration.status/v5", action: "finalize", lineage_id: lineageId },
104
+ } as unknown as ReviewStatusV3;
105
+ }
106
+
107
+ /** The live frozen identity of the dirty linked worktree candidate. */
108
+ function liveProjection(cwd: string): { baseTree: string; currentCandidateTree: string; paths: readonly string[] } {
109
+ const probe = new CandidateViewRegistry();
110
+ const view = probe.create({ contributorRoot: cwd });
111
+ try {
112
+ return { baseTree: view.baseTree, currentCandidateTree: view.candidateTree, paths: view.paths };
113
+ } finally {
114
+ probe.cleanup(view.token);
115
+ }
116
+ }
117
+
118
+ function statusNative(status: ReviewStatusV3): NativeReviewCli {
119
+ return {
120
+ targetStatus: async () => status,
121
+ finalize: async () => { throw new Error("native finalize must not run while reviewer results are outstanding"); },
122
+ } as unknown as NativeReviewCli;
123
+ }
124
+
125
+ async function runController(parameters: Record<string, unknown>, cwd: string, native: NativeReviewCli, candidateViews: CandidateViewRegistry): Promise<Record<string, unknown>> {
126
+ return await __testing.executeReviewControllerOperation(
127
+ parameters, cwd, new Map(), native, undefined, undefined, undefined, candidateViews,
128
+ ) as Record<string, unknown>;
129
+ }
130
+
131
+ test("FINALIZE hydrates the dispatch binding for an externally recovered successor in a dirty linked worktree", async (t) => {
132
+ const cwd = linkedDirtyWorktree(t);
133
+ const lineageId = "recovered-dirty-successor";
134
+ const registry = new CandidateViewRegistry();
135
+ const native = statusNative(successorStatus(lineageId, liveProjection(cwd)));
136
+
137
+ // The maintainer's flow: FINALIZE first (correctly blocked since #340),
138
+ // then a reviewer dispatch. FINALIZE never went through the STATUS
139
+ // controller operation, so nothing had hydrated the registry.
140
+ const finalize = await runController({ operation: "finalize", lineageId, input: JSON.stringify({}) }, cwd, native, registry);
141
+ assert.equal(finalize.outcome, "reviewer-results-required");
142
+ assert.equal(registry.hasCurrentBinding(), true, "FINALIZE must hydrate the dispatch binding from the status it decoded");
143
+ assert.deepEqual(finalize.dispatch_binding, { hydrated: true, lineage_id: lineageId, lenses: ["review-reliability"] });
144
+
145
+ try {
146
+ const dispatch: Record<string, unknown> = { agent: "review-reliability", task: "review the recovered successor", mode: "task" };
147
+ assert.doesNotThrow(() => injectReviewCandidateView(dispatch, registry));
148
+ assert.match(String(dispatch.task), new RegExp(lineageId));
149
+ } finally {
150
+ registry.cleanup(registry.resolveCurrentForLens("review-reliability").token);
151
+ }
152
+ });
153
+
154
+ test("a swallowed hydration failure becomes an observable typed dispatch refusal", async (t) => {
155
+ const cwd = linkedDirtyWorktree(t);
156
+ const lineageId = "drifted-successor";
157
+ const registry = new CandidateViewRegistry();
158
+ // Realistic drift: the frozen candidate names the pristine HEAD tree while
159
+ // the live worktree carries the uncommitted modification. Hydration
160
+ // legitimately fails, and that failure must stop being invisible.
161
+ const live = liveProjection(cwd);
162
+ const drifted = { ...live, currentCandidateTree: live.baseTree };
163
+ const finalize = await runController({ operation: "finalize", lineageId, input: JSON.stringify({}) }, cwd, statusNative(successorStatus(lineageId, drifted)), registry);
164
+
165
+ assert.equal(finalize.outcome, "reviewer-results-required");
166
+ assert.equal(registry.hasCurrentBinding(), false);
167
+ const binding = finalize.dispatch_binding as { hydrated: boolean; lineage_id: string; reason: string; message: string };
168
+ assert.equal(binding.hydrated, false, "a failed hydration must be reported, never swallowed");
169
+ assert.equal(binding.lineage_id, lineageId);
170
+ assert.ok(typeof binding.reason === "string" && binding.reason.length > 0, "the failure carries a typed reason");
171
+ assert.match(binding.message, /projection|live candidate/i);
172
+
173
+ // The dispatch refusal must now admit that hydration was attempted.
174
+ assert.throws(
175
+ () => injectReviewCandidateView({ agent: "review-reliability", task: "review", mode: "task" }, registry),
176
+ (error: unknown) => error instanceof CandidateViewError
177
+ && error.reason === "current-binding-hydration-failed"
178
+ && /hydrat/i.test(error.message)
179
+ && error.message.includes(lineageId),
180
+ );
181
+ });
182
+
183
+ test("STATUS keeps hydrating and stays read-only when hydration fails", async (t) => {
184
+ const cwd = linkedDirtyWorktree(t);
185
+ const lineageId = "status-drifted-successor";
186
+ const registry = new CandidateViewRegistry();
187
+ const statusLive = liveProjection(cwd);
188
+ const drifted = { ...statusLive, currentCandidateTree: statusLive.baseTree };
189
+ // STATUS must never fail because hydration failed.
190
+ const envelope = await runController({ operation: "status", lineageId }, cwd, statusNative(successorStatus(lineageId, drifted)), registry);
191
+ assert.equal(envelope.operation, "status");
192
+ assert.equal(registry.hasCurrentBinding(), false);
193
+ assert.throws(
194
+ () => injectReviewCandidateView({ agent: "review-reliability", task: "review", mode: "task" }, registry),
195
+ (error: unknown) => error instanceof CandidateViewError && error.reason === "current-binding-hydration-failed",
196
+ );
197
+ });