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
@@ -1,9 +1,9 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import { createHash } from "node:crypto";
4
- import { chmodSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
5
5
  import { tmpdir } from "node:os";
6
- import { dirname, join, resolve } from "node:path";
6
+ import { delimiter, dirname, join, resolve } from "node:path";
7
7
  import test from "node:test";
8
8
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
9
9
  import { __testing, createGentleAiExtension } from "../extensions/gentle-ai.ts";
@@ -22,7 +22,6 @@ import { canonicalJsonV1, domainHashV1 } from "../lib/review-canonical.ts";
22
22
  import { CandidateViewRegistry, deriveChangedPathManifest } from "../lib/review-candidate-view.ts";
23
23
  import { inspectLegacyReviewAuthorityV1 } from "../lib/review-legacy-detector.ts";
24
24
  import { resolveRepositoryAuthorityV1 } from "../lib/review-repository.ts";
25
- import { NATIVE_REVIEW_REMEDIATION, classifyNativeReviewRemediation } from "../lib/native-review-remediation.ts";
26
25
  import type { AuthorityRepairAssessmentV1, ReviewStatusV3 } from "../lib/review-integration-v2.ts";
27
26
 
28
27
  interface RegisteredTool {
@@ -46,6 +45,7 @@ interface RegisteredCommandFixture {
46
45
 
47
46
  interface Runtime {
48
47
  controller: RegisteredTool;
48
+ scopeReader: RegisteredTool;
49
49
  toolCall: ToolCallHandler;
50
50
  commands: Map<string, RegisteredCommandFixture>;
51
51
  }
@@ -90,15 +90,23 @@ function runtime(
90
90
  registerCommand(name: string, definition: RegisteredCommandFixture) { commands.set(name, definition); },
91
91
  } as unknown as ExtensionAPI);
92
92
  const controller = tools.get("gentle_review");
93
+ const scopeReader = tools.get("gentle_review_scope");
93
94
  assert.ok(controller);
95
+ assert.ok(scopeReader);
94
96
  assert.ok(toolCall);
95
- return { controller, toolCall, commands };
97
+ return { controller, scopeReader, toolCall, commands };
96
98
  }
97
99
 
98
100
  function context(cwd: string, signal?: AbortSignal): ExtensionContext {
99
101
  return { cwd, hasUI: false, signal, ui: { confirm: async () => true } } as unknown as ExtensionContext;
100
102
  }
101
103
 
104
+ function compactCandidateContextManifest(task: string): { encoded: string; sha256: string } {
105
+ const match = /Frozen changed scope manifest \(gzip\+base64url\): `([A-Za-z0-9_-]+)`\.\nFrozen changed scope manifest SHA-256: `([0-9a-f]{64})`\./.exec(task);
106
+ assert.ok(match, "expected a compact candidate context manifest");
107
+ return { encoded: match[1]!, sha256: match[2]! };
108
+ }
109
+
102
110
  function interactiveContext(cwd: string, signal?: AbortSignal): ExtensionContext {
103
111
  return { cwd, hasUI: true, signal, ui: { confirm: async () => true } } as unknown as ExtensionContext;
104
112
  }
@@ -268,7 +276,7 @@ function fakeNative(overrides: Partial<NativeReviewCli> = {}): NativeReviewCli {
268
276
  const lineageId = request.lineageId ?? "";
269
277
  return lineageId === ""
270
278
  ? candidateStartTargetStatus(request)
271
- : targetStatusFixture({ lineageId });
279
+ : candidateFinalizeTargetStatus(request, lineageId);
272
280
  },
273
281
  ...overrides,
274
282
  };
@@ -400,6 +408,24 @@ function candidateStartTargetStatus(request: Parameters<NonNullable<NativeReview
400
408
  }
401
409
  }
402
410
 
411
+ function candidateFinalizeTargetStatus(request: Parameters<NonNullable<NativeReviewCli["targetStatus"]>>[0], lineageId: string): ReviewStatusV3 {
412
+ let candidate: ReturnType<CandidateViewRegistry["create"]> | undefined;
413
+ try {
414
+ candidate = new CandidateViewRegistry().create({ contributorRoot: request.cwd });
415
+ return targetStatusFixture({
416
+ lineageId,
417
+ baseTree: candidate.baseTree,
418
+ currentCandidateTree: candidate.candidateTree,
419
+ paths: candidate.paths,
420
+ projection: request.projection ?? "workspace",
421
+ });
422
+ } catch {
423
+ return targetStatusFixture({ lineageId });
424
+ } finally {
425
+ candidate?.cleanup();
426
+ }
427
+ }
428
+
403
429
  function bindReviewerManifest(status: ReviewStatusV3, cwd: string, manifestHash = `sha256:${"7".repeat(64)}`): ReviewStatusV3 {
404
430
  const manifest = deriveChangedPathManifest(cwd, status.projection.baseTree, status.projection.currentCandidateTree).map((entry) => ({
405
431
  ...entry,
@@ -555,12 +581,60 @@ test("new ordinary START and native-lineage FINALIZE use exactly one native call
555
581
  }));
556
582
  const start = await controller.execute("start", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
557
583
  assert.deepEqual(start.details, { operation: "start", result: { lineage_id: "native-lineage", state: "reviewing", risk_tier: "medium", selected_lenses: ["review-reliability"], changed_files: 2, original_changed_lines: 7, correction_budget: 4, action: "created", lenses_required: true }, workspace_root: cwd });
558
- const finalize = await controller.execute("finalize", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({ review_result: { lens_results: [{ findings: [], evidence: ["complete candidate reviewed"] }] } }) }, undefined, undefined, context(cwd));
584
+ const finalize = await controller.execute("finalize", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({}) }, undefined, undefined, context(cwd));
559
585
  assert.deepEqual(finalize.details, { operation: "finalize", result: { lineage_id: "native-lineage", state: "approved", action: "approved", store_revision: "r1", receipt_path: "/opaque/receipt" } });
560
586
  assert.equal(starts, 1);
561
587
  assert.equal(finalizes, 1);
562
588
  });
563
589
 
590
+ test("native FINALIZE resolves STATUS and mutation against the verified frozen candidate root", async (t) => {
591
+ const cwd = repository(t);
592
+ writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
593
+ const candidateViews = new CandidateViewRegistry();
594
+ const statusRoots: string[] = [];
595
+ let finalizeRoot: string | undefined;
596
+ const native = fakeNative({
597
+ targetStatus: async (request) => {
598
+ if (request.lineageId === undefined) return candidateStartTargetStatus(request);
599
+ statusRoots.push(request.cwd);
600
+ if (request.cwd === cwd) return targetStatusFixture({ applicability: "unrelated", action: "start" });
601
+ const candidate = candidateViews.resolveForFinalize(request.lineageId);
602
+ return targetStatusFixture({
603
+ lineageId: request.lineageId,
604
+ baseTree: candidate.baseTree,
605
+ currentCandidateTree: candidate.candidateTree,
606
+ paths: candidate.paths,
607
+ });
608
+ },
609
+ start: async () => ({
610
+ lineageId: "candidate-root-finalize",
611
+ state: "reviewing",
612
+ riskLevel: "medium",
613
+ selectedLenses: ["review-reliability"],
614
+ changedFiles: 1,
615
+ changedLines: 1,
616
+ correctionBudget: 1,
617
+ action: "created",
618
+ lensesRequired: true,
619
+ }),
620
+ finalize: async (request) => {
621
+ finalizeRoot = request.cwd;
622
+ return { lineageId: "candidate-root-finalize", state: "approved", action: "approved", storeRevision: "r1" };
623
+ },
624
+ });
625
+ const { controller } = runtime(native, undefined, undefined, undefined, candidateViews);
626
+ await controller.execute("candidate-root-start", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
627
+ const candidateRoot = candidateViews.resolveForFinalize("candidate-root-finalize").root;
628
+ const result = await controller.execute("candidate-root-finalize", {
629
+ operation: "finalize",
630
+ lineageId: "candidate-root-finalize",
631
+ input: JSON.stringify({}),
632
+ }, undefined, undefined, context(cwd));
633
+ assert.deepEqual(statusRoots, [candidateRoot]);
634
+ assert.equal(finalizeRoot, candidateRoot);
635
+ assert.equal((result.details as { result: { state: string } }).result.state, "approved");
636
+ });
637
+
564
638
  test("parent subagent_run mutates single and parallel review actors with one verified controller-owned candidate view", async (t) => {
565
639
  const cwd = repository(t);
566
640
  writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
@@ -750,91 +824,12 @@ test("fresh negotiated registries reconstruct the frozen candidate before FINALI
750
824
  const result = await controller.execute("restarted-finalize", {
751
825
  operation: "finalize",
752
826
  lineageId: "restarted-lineage",
753
- input: JSON.stringify({ review_result: { lens_results: [{ lens: "review-reliability", findings: [], evidence: ["reviewed frozen candidate"] }] } }),
827
+ input: JSON.stringify({}),
754
828
  }, undefined, undefined, context(cwd));
755
829
  assert.equal(finalizedContent, "export const value = 2;\n");
756
830
  assert.equal((result.details as { result: { state: string } }).result.state, "approved");
757
831
  });
758
832
 
759
- test("fresh FINALIZE restores the provider manifest binding and rejects mode drift on the production path", async (t) => {
760
- const cwd = repository(t);
761
- writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
762
- const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
763
- const status = targetStatusFixture({ lineageId: "manifest-lineage", baseTree: frozen.baseTree, currentCandidateTree: frozen.candidateTree, paths: frozen.paths });
764
- bindReviewerManifest(status, cwd);
765
- const collect = status.nextTransition!.collect!;
766
- const input = collect.inputs[0]!;
767
- const changed = input.changedPathManifest![0]!;
768
- status.nextTransition = { ...status.nextTransition!, collect: { inputs: [{
769
- ...input,
770
- changedPathManifest: [{
771
- ...changed,
772
- newMode: "100755",
773
- modeOnly: true,
774
- }],
775
- }] } };
776
- frozen.cleanup();
777
- let finalizes = 0;
778
- const { controller } = runtime(fakeNative({
779
- targetStatus: async () => status,
780
- finalize: async () => {
781
- finalizes += 1;
782
- return { lineageId: "manifest-lineage", state: "approved", action: "approved", storeRevision: "r1" };
783
- },
784
- }), undefined, undefined, undefined, new CandidateViewRegistry());
785
- const result = await controller.execute("manifest-mode-drift", {
786
- operation: "finalize",
787
- lineageId: "manifest-lineage",
788
- input: JSON.stringify({ review_result: { lens_results: [{ lens: "review-reliability", findings: [], evidence: ["reviewed frozen candidate"] }] } }),
789
- }, undefined, undefined, context(cwd));
790
- assert.equal((result.details as { outcome?: string }).outcome, "native-operation-failed");
791
- assert.equal(((result.details as { diagnostics?: { code?: string } }).diagnostics?.code), "manifest-mode-drift");
792
- assert.equal(finalizes, 0, "manifest drift must stop before native FINALIZE");
793
- });
794
-
795
- test("fresh FINALIZE rejects inconsistent provider manifest hashes before production dispatch", async (t) => {
796
- const cwd = repository(t);
797
- writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
798
- const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
799
- const status = bindReviewerManifest(targetStatusFixture({ lineageId: "manifest-hash-lineage", baseTree: frozen.baseTree, currentCandidateTree: frozen.candidateTree, paths: frozen.paths }), cwd);
800
- const first = status.nextTransition!.collect!.inputs[0]!;
801
- const secondSubjectHash = `sha256:${"a".repeat(64)}`;
802
- status.nextTransition = { ...status.nextTransition!, collect: { inputs: [first, {
803
- ...first,
804
- arguments: first.arguments.map((argument) => argument.name === "lens"
805
- ? { ...argument, value: "review-risk", token: "--lens=review-risk" }
806
- : argument.name === "order"
807
- ? { ...argument, value: "1", token: "--order=1" }
808
- : argument.name === "subject-hash"
809
- ? { ...argument, value: secondSubjectHash, token: `--subject-hash=${secondSubjectHash}` }
810
- : argument),
811
- artifactSubject: {
812
- ...first.artifactSubject!,
813
- subjectHash: secondSubjectHash,
814
- changedPathManifestSha256: `sha256:${"b".repeat(64)}`,
815
- lens: "review-risk",
816
- selectedOrder: 1,
817
- },
818
- }] } };
819
- frozen.cleanup();
820
- let finalizes = 0;
821
- const { controller } = runtime(fakeNative({
822
- targetStatus: async () => status,
823
- finalize: async () => {
824
- finalizes += 1;
825
- return { lineageId: "manifest-hash-lineage", state: "approved", action: "approved", storeRevision: "r1" };
826
- },
827
- }), undefined, undefined, undefined, new CandidateViewRegistry());
828
- const result = await controller.execute("manifest-hash-drift", {
829
- operation: "finalize",
830
- lineageId: "manifest-hash-lineage",
831
- input: JSON.stringify({ review_result: { lens_results: [{ lens: "review-reliability", findings: [], evidence: ["reviewed frozen candidate"] }] } }),
832
- }, undefined, undefined, context(cwd));
833
- assert.equal((result.details as { outcome?: string }).outcome, "native-operation-failed");
834
- assert.equal(((result.details as { diagnostics?: { code?: string } }).diagnostics?.code), "manifest-input-divergence");
835
- assert.equal(finalizes, 0);
836
- });
837
-
838
833
  test("forecast-only FINALIZE reconstructs the frozen candidate after a fresh process (#176)", async (t) => {
839
834
  const cwd = repository(t);
840
835
  writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
@@ -1313,113 +1308,480 @@ test("production correction routing rejects a second capture that reuses failed
1313
1308
  execFileSync("chmod", ["-R", "u+w", cwd]);
1314
1309
  });
1315
1310
 
1316
- test("native FINALIZE derives and requires the trusted refuter request before invoking native FINALIZE", async (t) => {
1311
+ // Live-emitter correction shapes (probed 2026-08-16 against BOTH the pinned
1312
+ // 2.2.3 binary and dev 2.4.0-main.b1afef46, scripted correction lane):
1313
+ // - The evidence-collect status at `correction_required` carries the
1314
+ // `validation_request` CONTEXT alongside its single `review.capture-evidence`
1315
+ // input (reason correction_repository_verification_required). The request
1316
+ // context is not an offered validation step.
1317
+ // - The post-evidence targeted-validation status keeps state
1318
+ // `correction_required` (never `validating`), and its
1319
+ // `external.run_targeted_validation` input embeds the identical
1320
+ // validation_request.
1321
+ test("production correction routing accepts the live emitters' early validation-request context and correction_required post-evidence state", async (t) => {
1322
+ for (const [outcome, expectedKind] of [
1323
+ ["passed", "run-targeted-validation"],
1324
+ ["verification_failed", "recapture-required"],
1325
+ ] as const) {
1326
+ await t.test(outcome, async (scenario) => {
1327
+ const cwd = repository(scenario);
1328
+ writeFileSync(join(cwd, "app.ts"), `export const live = ${JSON.stringify(outcome)};\n`);
1329
+ const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
1330
+ const beforeCapture = bindCorrectionCollection(targetStatusFixture({
1331
+ lineageId: `live-correction-${outcome.replaceAll("_", "-")}`,
1332
+ authorityState: "correction_required",
1333
+ baseTree: frozen.baseTree,
1334
+ currentCandidateTree: frozen.candidateTree,
1335
+ paths: frozen.paths,
1336
+ }));
1337
+ // The live emitters publish the request context BEFORE evidence.
1338
+ beforeCapture.validationRequest = bindTargetedValidation(targetStatusFixture({
1339
+ lineageId: beforeCapture.authority!.lineageId,
1340
+ authorityState: "correction_required",
1341
+ baseTree: frozen.baseTree,
1342
+ currentCandidateTree: frozen.candidateTree,
1343
+ paths: frozen.paths,
1344
+ })).validationRequest;
1345
+ const afterCapture = targetStatusFixture({
1346
+ lineageId: beforeCapture.authority!.lineageId,
1347
+ // Live post-evidence state on both emitters.
1348
+ authorityState: "correction_required",
1349
+ baseTree: frozen.baseTree,
1350
+ currentCandidateTree: frozen.candidateTree,
1351
+ paths: frozen.paths,
1352
+ });
1353
+ if (outcome === "passed") bindTargetedValidation(afterCapture);
1354
+ else {
1355
+ bindCorrectionCollection(afterCapture);
1356
+ afterCapture.validationRequest = beforeCapture.validationRequest;
1357
+ }
1358
+ frozen.cleanup();
1359
+ const calls: string[] = [];
1360
+ let statuses = 0;
1361
+ let finalizes = 0;
1362
+ const { controller } = runtime(fakeNative({
1363
+ targetStatus: async () => {
1364
+ calls.push("status");
1365
+ statuses += 1;
1366
+ return statuses === 1 ? beforeCapture : afterCapture;
1367
+ },
1368
+ captureEvidence: async (request) => {
1369
+ calls.push("capture-evidence");
1370
+ assert.equal(request.outcome, outcome);
1371
+ return capturedCorrectionEvidence(beforeCapture, outcome, outcome === "passed" ? "7" : "8");
1372
+ },
1373
+ finalize: async () => {
1374
+ calls.push("finalize");
1375
+ finalizes += 1;
1376
+ return { lineageId: beforeCapture.authority!.lineageId, state: "approved", action: "approved", storeRevision: "r-final" };
1377
+ },
1378
+ }), undefined, undefined, undefined, new CandidateViewRegistry());
1379
+ const validation = outcome === "passed" ? {
1380
+ request_hash: "9".repeat(64), correction_ids: [],
1381
+ original_criteria: { passed: true, evidence: ["acceptance passes"] },
1382
+ correction_regression: { passed: true, evidence: ["regression passes"] },
1383
+ fix_caused_findings: [], follow_ups: [],
1384
+ } : undefined;
1385
+ const result = await controller.execute(`live-correction-${outcome}`, {
1386
+ operation: "finalize",
1387
+ lineageId: beforeCapture.authority!.lineageId,
1388
+ input: JSON.stringify({ final_evidence: `evidence: ${outcome}`, final_verification_outcome: outcome, ...(validation === undefined ? {} : { validation }) }),
1389
+ }, undefined, undefined, context(cwd));
1390
+ const details = result.details as Record<string, unknown>;
1391
+ assert.deepEqual(calls.slice(0, 3), ["status", "capture-evidence", "status"], "the early validation-request context must not block the evidence capture the state demands");
1392
+ assert.equal((details.correction_step as { kind?: string } | undefined)?.kind, expectedKind);
1393
+ assert.equal(finalizes, outcome === "passed" ? 1 : 0);
1394
+ });
1395
+ }
1396
+ });
1397
+
1398
+ // Ordinary final verification (field defect, dev binary 2.4.0-main.b1afef46,
1399
+ // reproduced identically on the pinned 2.2.3 binary): after FINALIZE with
1400
+ // captured results the native state is `validating` and the provider collects
1401
+ // exactly one `review.capture-evidence` record, then offers one execute
1402
+ // `review.finalize --captured-evidence=true` transition. The controller must
1403
+ // follow that transition faithfully — it must never demand its correction-lane
1404
+ // targeted-validation phase, and never substitute the validate gate.
1405
+ function bindFinalVerificationTransition(status: ReviewStatusV3, outcome: "passed" | "verification_failed" | "procedural_tooling_failed"): ReviewStatusV3 {
1406
+ const reasonCode = outcome === "passed"
1407
+ ? "captured_verification_evidence_passed"
1408
+ : outcome === "verification_failed" ? "captured_verification_failed" : "captured_verification_tooling_failed";
1409
+ status.nextTransition = {
1410
+ kind: "execute",
1411
+ reasonCode,
1412
+ execute: {
1413
+ operation: "review.finalize",
1414
+ // The second argument deliberately omits `token` to pin the
1415
+ // provider's published hyphenation fallback (captured_evidence ->
1416
+ // --captured-evidence=true); the host never invents another form.
1417
+ arguments: [
1418
+ { name: "lineage", value: status.authority!.lineageId, token: `--lineage=${status.authority!.lineageId}` },
1419
+ { name: "captured_evidence", value: "true" },
1420
+ ],
1421
+ preconditions: [{ name: "state", value: "validating" }, { name: "verification_outcome", value: outcome }],
1422
+ binding: { targetIdentity: status.targetIdentity, lineageId: status.authority!.lineageId },
1423
+ },
1424
+ };
1425
+ delete status.validationRequest;
1426
+ return status;
1427
+ }
1428
+
1429
+ test("ordinary final verification at validating captures evidence then executes the provider finalize transition, never targeted validation", async (t) => {
1430
+ for (const [outcome, terminalState] of [
1431
+ ["passed", "approved"],
1432
+ ["verification_failed", "escalated"],
1433
+ ["procedural_tooling_failed", "escalated"],
1434
+ ] as const) {
1435
+ await t.test(outcome, async (scenario) => {
1436
+ const cwd = repository(scenario);
1437
+ writeFileSync(join(cwd, "app.ts"), `export const outcome = ${JSON.stringify(outcome)};\n`);
1438
+ const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
1439
+ const beforeCapture = bindCorrectionCollection(targetStatusFixture({
1440
+ lineageId: "final-verification",
1441
+ authorityState: "validating",
1442
+ baseTree: frozen.baseTree,
1443
+ currentCandidateTree: frozen.candidateTree,
1444
+ paths: frozen.paths,
1445
+ }));
1446
+ const afterCapture = bindFinalVerificationTransition(targetStatusFixture({
1447
+ lineageId: "final-verification",
1448
+ authorityState: "validating",
1449
+ baseTree: frozen.baseTree,
1450
+ currentCandidateTree: frozen.candidateTree,
1451
+ paths: frozen.paths,
1452
+ }), outcome);
1453
+ frozen.cleanup();
1454
+ const calls: string[] = [];
1455
+ const transitions: Array<readonly string[]> = [];
1456
+ let statuses = 0;
1457
+ const { controller } = runtime(fakeNative({
1458
+ targetStatus: async () => {
1459
+ calls.push("status");
1460
+ statuses += 1;
1461
+ return statuses === 1 ? beforeCapture : afterCapture;
1462
+ },
1463
+ captureEvidence: async (request) => {
1464
+ calls.push("capture-evidence");
1465
+ assert.equal(request.outcome, outcome);
1466
+ return capturedCorrectionEvidence(beforeCapture, outcome, "6");
1467
+ },
1468
+ finalize: async () => {
1469
+ calls.push("finalize");
1470
+ return { lineageId: "final-verification", state: terminalState, action: "terminal", storeRevision: "r2" };
1471
+ },
1472
+ finalizeTransition: async (request) => {
1473
+ calls.push("finalize-transition");
1474
+ transitions.push(request.argumentTokens);
1475
+ return { lineageId: "final-verification", state: terminalState, action: "terminal", storeRevision: "r2", receiptPath: "/opaque/receipt" };
1476
+ },
1477
+ }), undefined, undefined, undefined, new CandidateViewRegistry());
1478
+ const result = await controller.execute(`final-verification-${outcome}`, {
1479
+ operation: "finalize",
1480
+ lineageId: "final-verification",
1481
+ input: JSON.stringify({ final_evidence: `verification run: ${outcome}`, final_verification_outcome: outcome }),
1482
+ }, undefined, undefined, context(cwd));
1483
+ const details = result.details as Record<string, unknown>;
1484
+ assert.deepEqual(calls, ["status", "capture-evidence", "status", "finalize-transition"], "the controller must follow the provider transition after evidence capture");
1485
+ assert.deepEqual(transitions, [["--lineage=final-verification", "--captured-evidence=true"]]);
1486
+ assert.equal((details.result as { state?: string } | undefined)?.state, terminalState);
1487
+ assert.equal("correction_step" in details, false, "ordinary final verification is not a correction transaction");
1488
+ });
1489
+ }
1490
+ });
1491
+
1492
+ test("ordinary final verification rejects a Pi-authored targeted validation document before any capture", async (t) => {
1317
1493
  const cwd = repository(t);
1494
+ writeFileSync(join(cwd, "app.ts"), "export const value = 1;\n");
1495
+ const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
1496
+ const status = bindCorrectionCollection(targetStatusFixture({
1497
+ lineageId: "final-verification",
1498
+ authorityState: "validating",
1499
+ baseTree: frozen.baseTree,
1500
+ currentCandidateTree: frozen.candidateTree,
1501
+ paths: frozen.paths,
1502
+ }));
1503
+ frozen.cleanup();
1504
+ let captures = 0;
1318
1505
  let finalizes = 0;
1319
1506
  const { controller } = runtime(fakeNative({
1320
- start: async () => ({ lineageId: "native-lineage", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-risk"], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true }),
1321
- finalize: async () => {
1322
- finalizes += 1;
1323
- return { lineageId: "native-lineage", state: "validating", action: "continue", storeRevision: "r1" };
1324
- },
1325
- }));
1326
- await controller.execute("risk-001-start", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
1327
- const proof = "differential-test:candidate still fails";
1328
- const finding = { id: "RISK-001", lens: "review-risk", location: "lib/a.ts:1", severity: "CRITICAL", claim: "Candidate fails", evidence_class: "inferential", causal_disposition: "introduced", proof_refs: [proof] };
1329
- const review_result = { lens_results: [{ lens: "review-risk", findings: [finding], evidence: ["complete candidate reviewed"] }] };
1330
- const required = await controller.execute("risk-001-refuter-request", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({ review_result }) }, undefined, undefined, context(cwd));
1331
- const request = (required.details as { refuter_request?: { request_hash: string; findings: unknown[] } }).refuter_request;
1507
+ targetStatus: async () => status,
1508
+ captureEvidence: async () => { captures += 1; return capturedCorrectionEvidence(status, "passed", "4"); },
1509
+ finalize: async () => { finalizes += 1; return { lineageId: "final-verification", state: "approved", action: "approved", storeRevision: "r1" }; },
1510
+ }), undefined, undefined, undefined, new CandidateViewRegistry());
1511
+ const result = await controller.execute("final-verification-validation-doc", {
1512
+ operation: "finalize",
1513
+ lineageId: "final-verification",
1514
+ input: JSON.stringify({
1515
+ final_evidence: "verification run: passed",
1516
+ final_verification_outcome: "passed",
1517
+ validation: { request_hash: "9".repeat(64), correction_ids: [], original_criteria: { passed: true, evidence: ["passes"] }, correction_regression: { passed: true, evidence: ["passes"] }, fix_caused_findings: [], follow_ups: [] },
1518
+ }),
1519
+ }, undefined, undefined, context(cwd));
1520
+ assert.equal((result.details as { outcome?: string }).outcome, "native-operation-failed");
1521
+ assert.match(JSON.stringify(result.details), /final-verification-provider-owned/);
1522
+ assert.equal(captures, 0, "an inadmissible payload must fail closed before the single evidence capture is consumed");
1332
1523
  assert.equal(finalizes, 0);
1333
- assert.equal((required.details as { outcome?: string }).outcome, "refuter-required");
1334
- assert.equal(typeof request?.request_hash, "string");
1335
- assert.deepEqual(request?.findings.map((row) => (row as { id: string }).id), ["RISK-001"]);
1336
- const retry = await controller.execute("risk-001-refuter-retry", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({
1337
- review_result: { ...review_result, refuter_request_hash: request!.request_hash },
1338
- refuter_batch: { schema: "gentle-ai.refuter-result-batch/v1", request_hash: request!.request_hash, results: [{ finding_id: "RISK-001", outcome: "corroborated", proof_refs: [proof] }] },
1339
- }) }, undefined, undefined, context(cwd));
1340
- assert.equal(finalizes, 1);
1341
- assert.equal((retry.details as { result?: { state?: string } }).result?.state, "validating");
1342
1524
  });
1343
1525
 
1344
- test("native FINALIZE emits exact v2.1.4 process documents and failed verification argv intent", async (t) => {
1526
+ test("ordinary final verification fails closed without substituting a step when the provider offers no finalize transition", async (t) => {
1345
1527
  const cwd = repository(t);
1346
- const requests: Parameters<NativeReviewCli["finalize"]>[0][] = [];
1528
+ writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
1529
+ const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
1530
+ const beforeCapture = bindCorrectionCollection(targetStatusFixture({
1531
+ lineageId: "final-verification",
1532
+ authorityState: "validating",
1533
+ baseTree: frozen.baseTree,
1534
+ currentCandidateTree: frozen.candidateTree,
1535
+ paths: frozen.paths,
1536
+ }));
1537
+ const afterCapture = targetStatusFixture({
1538
+ lineageId: "final-verification",
1539
+ authorityState: "validating",
1540
+ action: "stop",
1541
+ replayability: "manual_action_required",
1542
+ baseTree: frozen.baseTree,
1543
+ currentCandidateTree: frozen.candidateTree,
1544
+ paths: frozen.paths,
1545
+ });
1546
+ frozen.cleanup();
1547
+ let statuses = 0;
1548
+ let finalizes = 0;
1549
+ let transitions = 0;
1550
+ let validates = 0;
1347
1551
  const { controller } = runtime(fakeNative({
1348
- start: async () => ({ lineageId: "native-lineage", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-risk"], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true }),
1349
- finalize: async (request) => {
1350
- requests.push(request);
1552
+ targetStatus: async () => {
1553
+ statuses += 1;
1554
+ return statuses === 1 ? beforeCapture : afterCapture;
1555
+ },
1556
+ captureEvidence: async () => capturedCorrectionEvidence(beforeCapture, "passed", "5"),
1557
+ finalize: async () => { finalizes += 1; return { lineageId: "final-verification", state: "approved", action: "approved", storeRevision: "r1" }; },
1558
+ finalizeTransition: async () => { transitions += 1; return { lineageId: "final-verification", state: "approved", action: "approved", storeRevision: "r1" }; },
1559
+ validate: async () => { validates += 1; return { allowed: true, result: "allow", action: "continue", reason: "ok", gateContext: nativeGateContext() }; },
1560
+ }), undefined, undefined, undefined, new CandidateViewRegistry());
1561
+ const result = await controller.execute("final-verification-no-transition", {
1562
+ operation: "finalize",
1563
+ lineageId: "final-verification",
1564
+ input: JSON.stringify({ final_evidence: "verification run: passed", final_verification_outcome: "passed" }),
1565
+ }, undefined, undefined, context(cwd));
1566
+ const details = result.details as Record<string, unknown>;
1567
+ assert.equal(details.outcome, "final-verification-transition-unavailable");
1568
+ assert.equal(details.mutation_performed, true, "the committed evidence capture must never be reported as zero mutations");
1569
+ assert.equal(details.mutation_outcome, "committed");
1570
+ assert.equal(finalizes, 0, "no substitute finalize form may be invented");
1571
+ assert.equal(transitions, 0);
1572
+ assert.equal(validates, 0, "the validate gate must never be substituted for a missing transition");
1573
+ });
1574
+
1575
+ // gentle-pi#311 P4-roles fixture: one provider-rendered SELF-CONTAINED role
1576
+ // capture vector (binding tokens + --agent=pi --execute=true, no submission).
1577
+ function bindProviderRoleVector(status: ReviewStatusV3, role: "refuter" | "targeted-validator"): ReviewStatusV3 {
1578
+ const authority = status.authority!;
1579
+ const contextHandle = `rctx1_${"c".repeat(64)}`;
1580
+ const requestHash = `sha256:${"9".repeat(64)}`;
1581
+ const argumentsList = [
1582
+ { name: "lineage", value: authority.lineageId, token: `--lineage=${authority.lineageId}` },
1583
+ { name: "expected-revision", value: authority.revision, token: `--expected-revision=${authority.revision}` },
1584
+ { name: "target", value: status.targetIdentity, token: `--target=${status.targetIdentity}` },
1585
+ { name: "repository-context", value: contextHandle, token: `--repository-context=${contextHandle}` },
1586
+ ...(role === "targeted-validator" ? [{ name: "request-hash", value: requestHash, token: `--request-hash=${requestHash}` }] : []),
1587
+ { name: "agent", value: "pi", token: "--agent=pi" },
1588
+ { name: "execute", value: "true", token: "--execute=true" },
1589
+ ];
1590
+ status.nextTransition = {
1591
+ kind: "collect",
1592
+ reasonCode: role === "refuter" ? "provider_refuter_required" : "targeted_validation_required",
1593
+ collect: { inputs: [role === "refuter" ? {
1594
+ name: "provider_refuter",
1595
+ schema: "https://gentle-ai.dev/schema/review/refuter/v1",
1596
+ captureOperation: "review.capture-refuter",
1597
+ arguments: argumentsList,
1598
+ } : {
1599
+ name: "provider_targeted_validator",
1600
+ schema: "https://gentle-ai.dev/schema/review/validator/v1",
1601
+ captureOperation: "review.capture-validation",
1602
+ arguments: argumentsList,
1603
+ validationRequest: {
1604
+ schema: "gentle-ai.review-targeted-validation-request/v1" as const,
1605
+ requestHash,
1606
+ lineageId: authority.lineageId,
1607
+ expectedRevision: authority.revision,
1608
+ targetIdentity: status.targetIdentity,
1609
+ fixFindingIds: ["RISK-001"],
1610
+ projection: "workspace" as const,
1611
+ correctionCandidateTree: status.projection.currentCandidateTree,
1612
+ correctionTargetIdentity: status.targetIdentity,
1613
+ correctionPaths: status.projection.paths,
1614
+ correctionPathsDigest: status.projection.pathsDigest,
1615
+ },
1616
+ }] },
1617
+ };
1618
+ return status;
1619
+ }
1620
+
1621
+ test("provider refuter vector executes exactly as rendered, then STATUS is re-queried", async (t) => {
1622
+ const cwd = repository(t);
1623
+ const executed: Array<{ captureOperation: string; argumentTokens: readonly string[]; cwd: string }> = [];
1624
+ let finalizes = 0;
1625
+ let statusCalls = 0;
1626
+ const roleStatus = bindProviderRoleVector(targetStatusFixture({ lineageId: "native-lineage" }), "refuter");
1627
+ const settledStatus = targetStatusFixture({ lineageId: "native-lineage" });
1628
+ const { controller } = runtime(fakeNative({
1629
+ targetStatus: async () => {
1630
+ statusCalls += 1;
1631
+ return statusCalls === 1 ? roleStatus : settledStatus;
1632
+ },
1633
+ finalize: async () => {
1634
+ finalizes += 1;
1351
1635
  return { lineageId: "native-lineage", state: "approved", action: "approved", storeRevision: "r1" };
1352
1636
  },
1637
+ captureProviderRole: async (request) => {
1638
+ executed.push({ captureOperation: request.captureOperation, argumentTokens: request.argumentTokens, cwd: request.cwd });
1639
+ return { schema: "gentle-ai.review-provider-role-capture/v1", lineageId: "native-lineage", targetIdentity: roleStatus.targetIdentity, role: "refuter", captured: true };
1640
+ },
1353
1641
  }));
1354
- await controller.execute("finalize-v212-start", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
1355
- const finding = { id: "RISK-001", lens: "review-risk", location: "lib/a.ts:1", severity: "CRITICAL", claim: "Candidate fails", evidence_class: "inferential", causal_disposition: "introduced", proof_refs: ["differential-test:candidate still fails"] };
1356
- const review_result = { lens_results: [{ lens: "review-risk", findings: [finding], evidence: ["complete candidate reviewed"] }] };
1357
- const requested = await controller.execute("finalize-v212-request", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({ review_result }) }, undefined, undefined, context(cwd));
1358
- const request_hash = (requested.details as { refuter_request: { request_hash: string } }).refuter_request.request_hash;
1359
- const refuterBatch = { schema: "gentle-ai.refuter-result-batch/v1", request_hash, results: [{ finding_id: "RISK-001", outcome: "inconclusive", proof_refs: ["differential-test:candidate still fails"] }] };
1360
- await controller.execute("finalize-v212", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({
1361
- review_result: { ...review_result, refuter_request_hash: request_hash },
1362
- refuter_batch: refuterBatch,
1363
- validation: { request_hash: "b".repeat(64), correction_ids: ["RISK-001"], original_criteria: { passed: false, evidence: ["acceptance still fails"] }, correction_regression: { passed: true, evidence: ["regression suite passes"] }, fix_caused_findings: [], follow_ups: [{ finding_id: "RISK-001", location: "lib/a.ts:1", summary: "Track the remaining failure", proof_refs: ["differential-test:candidate still fails"] }] },
1364
- final_evidence: " focused verification failed\n\n",
1365
- final_verification_passed: false,
1366
- }) }, undefined, undefined, context(cwd));
1367
- assert.deepEqual(requests, [{
1642
+ const result = await controller.execute("role-vector", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({}) }, undefined, undefined, context(cwd));
1643
+ assert.deepEqual(executed, [{
1644
+ captureOperation: "review.capture-refuter",
1645
+ argumentTokens: [
1646
+ "--lineage=native-lineage",
1647
+ `--expected-revision=sha256:${"a".repeat(64)}`,
1648
+ `--target=sha256:${"a".repeat(64)}`,
1649
+ `--repository-context=rctx1_${"c".repeat(64)}`,
1650
+ "--agent=pi",
1651
+ "--execute=true",
1652
+ ],
1368
1653
  cwd,
1369
- lineageId: "native-lineage",
1370
- lensResults: [{ lens: "review-risk", document: { lens: "risk", findings: [{ ...finding, lens: "risk" }], evidence: ["complete candidate reviewed"] } }],
1371
- refuterDocument: { results: refuterBatch.results },
1372
- validationDocument: { original_criteria: { passed: false, evidence: ["acceptance still fails"] }, correction_regression: { passed: true, evidence: ["regression suite passes"] }, follow_ups: [{ observation: "Track the remaining failure", proof_refs: ["differential-test:candidate still fails"] }] },
1373
- evidenceDocument: " focused verification failed\n\n",
1374
- failed: true,
1375
1654
  }]);
1655
+ assert.equal(finalizes, 0, "role vectors never route through document finalize");
1656
+ assert.equal(statusCalls, 2, "STATUS must be re-queried after the vector executes");
1657
+ const details = result.details as { provider_roles?: { transport?: string; executed_slots?: Array<Record<string, unknown>> } };
1658
+ assert.equal(details.provider_roles?.transport, "go_owned_pi_process");
1659
+ assert.deepEqual(details.provider_roles?.executed_slots?.map((slot) => slot.role), ["refuter"]);
1376
1660
  });
1377
1661
 
1378
- test("native FINALIZE rejects unpublished reviewer enums and empty arrays before native calls", async (t) => {
1662
+ test("provider targeted-validator vector keeps the frozen request hash token verbatim", async (t) => {
1379
1663
  const cwd = repository(t);
1380
- let finalizes = 0;
1381
- const { controller } = runtime(fakeNative({ finalize: async () => {
1382
- finalizes += 1;
1383
- return { lineageId: "native-lineage", state: "validating", action: "continue", storeRevision: "r1" };
1384
- } }));
1385
- const finding = { id: "RISK-001", lens: "review-risk", location: "lib/a.ts:1", severity: "CRITICAL", claim: "Candidate fails", evidence_class: "inferential", causal_disposition: "introduced", proof_refs: ["differential-test:candidate still fails"] };
1386
- for (const lensResult of [
1387
- { lens: "review-unknown", findings: [finding], evidence: ["reviewed"] },
1388
- { lens: "review-risk", findings: [{ ...finding, lens: "review-unknown" }], evidence: ["reviewed"] },
1389
- { lens: "review-risk", findings: [{ ...finding, severity: "INFO" }], evidence: ["reviewed"] },
1390
- { lens: "review-risk", findings: [{ ...finding, evidence_class: "info" }], evidence: ["reviewed"] },
1391
- { lens: "review-risk", findings: [{ ...finding, evidence_class: "unknown" }], evidence: ["reviewed"] },
1392
- { lens: "review-risk", findings: [{ ...finding, causal_disposition: "candidate-caused" }], evidence: ["reviewed"] },
1393
- { lens: "review-risk", findings: [{ ...finding, proof_refs: [] }], evidence: ["reviewed"] },
1394
- { lens: "review-risk", findings: [], evidence: [] },
1395
- ]) {
1396
- await assert.rejects(controller.execute("invalid-reviewer", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({ review_result: { lens_results: [lensResult] } }) }, undefined, undefined, context(cwd)));
1397
- }
1398
- assert.equal(finalizes, 0);
1399
- });
1664
+ const executed: string[][] = [];
1665
+ let statusCalls = 0;
1666
+ const roleStatus = bindProviderRoleVector(targetStatusFixture({ lineageId: "native-lineage", authorityState: "validating" }), "targeted-validator");
1667
+ const { controller } = runtime(fakeNative({
1668
+ targetStatus: async () => {
1669
+ statusCalls += 1;
1670
+ return statusCalls === 1 ? roleStatus : targetStatusFixture({ lineageId: "native-lineage" });
1671
+ },
1672
+ captureProviderRole: async (request) => {
1673
+ executed.push([...request.argumentTokens]);
1674
+ return { schema: "gentle-ai.review-provider-role-capture/v1", lineageId: "native-lineage", targetIdentity: roleStatus.targetIdentity, role: "targeted-validator", captured: true };
1675
+ },
1676
+ }));
1677
+ await controller.execute("validator-vector", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({}) }, undefined, undefined, context(cwd));
1678
+ assert.equal(executed.length, 1);
1679
+ assert.ok(executed[0]!.includes(`--request-hash=sha256:${"9".repeat(64)}`), "the frozen request hash token must pass through verbatim");
1680
+ assert.ok(executed[0]!.includes("--execute=true"));
1681
+ });
1400
1682
 
1401
- test("native FINALIZE validates refuter request binding, completeness, and rows before native calls", async (t) => {
1683
+ test("a failed provider role vector surfaces the typed error and never auto-relaunches", async (t) => {
1684
+ const cwd = repository(t);
1685
+ let captures = 0;
1686
+ let statusCalls = 0;
1687
+ const roleStatus = bindProviderRoleVector(targetStatusFixture({ lineageId: "native-lineage" }), "refuter");
1688
+ const { controller } = runtime(fakeNative({
1689
+ targetStatus: async () => {
1690
+ statusCalls += 1;
1691
+ return roleStatus;
1692
+ },
1693
+ captureProviderRole: async () => {
1694
+ captures += 1;
1695
+ throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.NON_ZERO, NATIVE_REVIEW_OPERATION.CAPTURE_PROVIDER_ROLE, true, "provider refused the role capture");
1696
+ },
1697
+ }));
1698
+ const result = await controller.execute("role-vector-failure", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({}) }, undefined, undefined, context(cwd));
1699
+ const details = result.details as Record<string, unknown>;
1700
+ assert.equal(details.outcome, "provider-role-vector-failed");
1701
+ assert.equal(details.status, "blocked");
1702
+ assert.match(String(details.retry_discipline), /Re-query negotiated STATUS/);
1703
+ assert.equal(captures, 1, "a failed vector must not be relaunched from transcript inference");
1704
+ assert.equal(statusCalls, 1, "the failure envelope is surfaced without a blind relaunch loop");
1705
+ });
1706
+
1707
+ test("negotiated FINALIZE executes the provider-rendered captured-results transition verbatim", async (t) => {
1708
+ const cwd = repository(t);
1709
+ const transitions: Array<{ cwd: string; argumentTokens: readonly string[] }> = [];
1710
+ let finalizes = 0;
1711
+ const status = targetStatusFixture({ lineageId: "native-lineage" });
1712
+ status.nextTransition = {
1713
+ kind: "execute",
1714
+ reasonCode: "captured_results_ready",
1715
+ execute: {
1716
+ operation: "review.finalize",
1717
+ // The second argument deliberately omits `token` to pin the
1718
+ // provider's published hyphenation fallback (captured_results ->
1719
+ // --captured-results=true); the host never invents another form.
1720
+ arguments: [
1721
+ { name: "lineage", value: "native-lineage", token: "--lineage=native-lineage" },
1722
+ { name: "captured_results", value: "true" },
1723
+ ],
1724
+ preconditions: [{ name: "state", value: "reviewing" }],
1725
+ binding: { targetIdentity: status.targetIdentity, lineageId: "native-lineage" },
1726
+ },
1727
+ };
1728
+ const { controller } = runtime(fakeNative({
1729
+ targetStatus: async () => status,
1730
+ finalize: async () => {
1731
+ finalizes += 1;
1732
+ return { lineageId: "native-lineage", state: "approved", action: "approved", storeRevision: "r1" };
1733
+ },
1734
+ finalizeTransition: async (request) => {
1735
+ transitions.push({ cwd: request.cwd, argumentTokens: request.argumentTokens });
1736
+ return { lineageId: "native-lineage", state: "approved", action: "approved", storeRevision: "r1", receiptPath: "/opaque/receipt" };
1737
+ },
1738
+ }));
1739
+ const result = await controller.execute("captured-results-finalize", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({}) }, undefined, undefined, context(cwd));
1740
+ assert.deepEqual(transitions, [{ cwd, argumentTokens: ["--lineage=native-lineage", "--captured-results=true"] }]);
1741
+ assert.equal(finalizes, 0, "the provider-driven lane never assembles a document finalize");
1742
+ assert.deepEqual(result.details, { operation: "finalize", result: { lineage_id: "native-lineage", state: "approved", action: "approved", store_revision: "r1", receipt_path: "/opaque/receipt" } });
1743
+ });
1744
+
1745
+ test("negotiated FINALIZE refuses a finalize transition bound to a different lineage", async (t) => {
1746
+ const cwd = repository(t);
1747
+ let transitions = 0;
1748
+ const status = targetStatusFixture({ lineageId: "native-lineage" });
1749
+ status.nextTransition = {
1750
+ kind: "execute",
1751
+ reasonCode: "captured_results_ready",
1752
+ execute: {
1753
+ operation: "review.finalize",
1754
+ arguments: [{ name: "lineage", value: "other-lineage", token: "--lineage=other-lineage" }],
1755
+ preconditions: [],
1756
+ binding: { targetIdentity: status.targetIdentity, lineageId: "other-lineage" },
1757
+ },
1758
+ };
1759
+ const { controller } = runtime(fakeNative({
1760
+ targetStatus: async () => status,
1761
+ finalizeTransition: async () => {
1762
+ transitions += 1;
1763
+ return { lineageId: "other-lineage", state: "approved", action: "approved", storeRevision: "r1" };
1764
+ },
1765
+ }));
1766
+ const result = await controller.execute("foreign-transition", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({}) }, undefined, undefined, context(cwd));
1767
+ assert.equal((result.details as { outcome?: string }).outcome, "native-operation-failed");
1768
+ assert.match(JSON.stringify(result.details), /finalize-transition-binding-drift/);
1769
+ assert.equal(transitions, 0);
1770
+ });
1771
+
1772
+ test("native FINALIZE rejects the retired Pi-authored payload fields before native calls", async (t) => {
1402
1773
  const cwd = repository(t);
1403
1774
  let finalizes = 0;
1404
1775
  const { controller } = runtime(fakeNative({ finalize: async () => {
1405
1776
  finalizes += 1;
1406
1777
  return { lineageId: "native-lineage", state: "validating", action: "continue", storeRevision: "r1" };
1407
1778
  } }));
1408
- const expectedHash = "a".repeat(64);
1409
- const proof = "differential-test:candidate still fails";
1410
- const finding = { id: "RISK-001", lens: "review-risk", location: "lib/a.ts:1", severity: "CRITICAL", claim: "Candidate fails", evidence_class: "inferential", causal_disposition: "introduced", proof_refs: [proof] };
1411
- const row = { finding_id: finding.id, outcome: "corroborated", proof_refs: [proof] };
1412
- for (const refuter_batch of [
1413
- { schema: "gentle-ai.refuter-result-batch/v1", request_hash: "b".repeat(64), results: [row] },
1414
- { schema: "gentle-ai.refuter-result-batch/v1", request_hash: expectedHash, results: [] },
1415
- { schema: "gentle-ai.refuter-result-batch/v1", request_hash: expectedHash, results: [row, row] },
1416
- { schema: "gentle-ai.refuter-result-batch/v1", request_hash: expectedHash, results: [{ ...row, finding_id: "RISK-002" }] },
1417
- { schema: "gentle-ai.refuter-result-batch/v1", request_hash: expectedHash, results: [{ ...row, proof_refs: [] }] },
1779
+ for (const retired of [
1780
+ { review_result: { lens_results: [{ lens: "review-risk", findings: [], evidence: ["reviewed"] }] } },
1781
+ { refuter_batch: { schema: "gentle-ai.refuter-result-batch/v1", request_hash: "a".repeat(64), results: [] } },
1782
+ { validation_proof: { original_criteria: { passed: true, evidence: ["ok"] }, correction_regression: { passed: true, evidence: ["ok"] } } },
1418
1783
  ]) {
1419
- await assert.rejects(controller.execute("invalid-refuter", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({
1420
- review_result: { lens_results: [{ lens: "review-risk", findings: [finding], evidence: ["reviewed"] }], refuter_request_hash: expectedHash },
1421
- refuter_batch,
1422
- }) }, undefined, undefined, context(cwd)));
1784
+ await assert.rejects(controller.execute("retired-field", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify(retired) }, undefined, undefined, context(cwd)));
1423
1785
  }
1424
1786
  assert.equal(finalizes, 0);
1425
1787
  });
@@ -1452,17 +1814,19 @@ test("controller rejects zero-length final evidence before native calls", async
1452
1814
  assert.equal(finalizes, 0);
1453
1815
  });
1454
1816
 
1455
- test("repeated native FINALIZE keeps initial lenses one-shot", async (t) => {
1817
+ test("native FINALIZE never assembles reviewer documents and forwards only the correction forecast", async (t) => {
1456
1818
  const cwd = repository(t);
1457
1819
  const requests: Parameters<NativeReviewCli["finalize"]>[0][] = [];
1458
1820
  const { controller } = runtime(fakeNative({ finalize: async (request) => {
1459
1821
  requests.push(request);
1460
1822
  return { lineageId: "native-lineage", state: "correction_required", action: "continue correction", storeRevision: `r${requests.length}` };
1461
1823
  } }));
1462
- await controller.execute("initial", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({ review_result: { lens_results: [{ findings: [], evidence: ["complete candidate reviewed"] }] } }) }, undefined, undefined, context(cwd));
1824
+ await controller.execute("initial", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({}) }, undefined, undefined, context(cwd));
1463
1825
  await controller.execute("retry", { operation: "finalize", lineageId: "native-lineage", input: JSON.stringify({ correction_line_forecast: 1 }) }, undefined, undefined, context(cwd));
1464
- assert.equal(requests[0]?.lensResults?.length, 1);
1826
+ assert.equal(requests[0]?.lensResults, undefined, "lens results are admitted natively, never Pi-assembled");
1827
+ assert.equal(requests[0]?.refuterDocument, undefined);
1465
1828
  assert.equal(requests[1]?.lensResults, undefined);
1829
+ assert.equal(requests[1]?.correctionLines, 1);
1466
1830
  });
1467
1831
 
1468
1832
  test("native error has no compact fallback and ambiguous mutation demands target status", async (t) => {
@@ -1510,6 +1874,35 @@ test("native START preserves a candidate-view diagnostic before native invocatio
1510
1874
  assert.equal(starts, 0);
1511
1875
  });
1512
1876
 
1877
+ test("native START returns a structured pre-native candidate-view output-limit diagnostic without calling native START", async (t) => {
1878
+ const cwd = repository(t);
1879
+ let starts = 0;
1880
+ const candidateViews = new CandidateViewRegistry(() => {
1881
+ throw Object.assign(new Error("sensitive stderr and candidate bytes"), { code: "ENOBUFS", stderr: Buffer.from("sensitive stderr and candidate bytes") });
1882
+ });
1883
+ const { controller } = runtime(fakeNative({
1884
+ start: async () => {
1885
+ starts += 1;
1886
+ return { lineageId: "must-not-start", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true };
1887
+ },
1888
+ }), undefined, undefined, undefined, candidateViews);
1889
+ const result = await controller.execute("candidate-view-output-limit", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
1890
+ const details = result.details as Record<string, unknown>;
1891
+ assert.equal(details.outcome, "native-operation-failed");
1892
+ assert.equal(details.lineage_created, false);
1893
+ assert.equal(details.mutation_outcome, "none");
1894
+ assert.deepEqual(details.diagnostics, {
1895
+ phase: "candidate-view",
1896
+ category: "output-limit",
1897
+ git_subcommand: "rev-parse",
1898
+ timeout_ms: 10_000,
1899
+ max_buffer_bytes: 64 * 1024 * 1024,
1900
+ message: "candidate-view Git command rev-parse exceeded the 67108864-byte output limit; inspect the candidate state before any new START",
1901
+ });
1902
+ assert.doesNotMatch(JSON.stringify(details), /sensitive stderr|candidate bytes/);
1903
+ assert.equal(starts, 0);
1904
+ });
1905
+
1513
1906
  test("ambiguous native START failure preserves rebuilt sanitized diagnostics across duplicated module instances", async (t) => {
1514
1907
  const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-native-controller-"));
1515
1908
  t.after(() => rmSync(cwd, { recursive: true, force: true }));
@@ -1607,6 +2000,83 @@ test("native START uses the default policy or a canonical safe policy path, and
1607
2000
  assert.equal(requests.length, 2);
1608
2001
  });
1609
2002
 
2003
+ test("native ordinary START forwards every allowed focus and leaves the native default omitted", async (t) => {
2004
+ const cwd = repository(t);
2005
+ const requests: Parameters<NativeReviewCli["start"]>[0][] = [];
2006
+ const { controller } = runtime(fakeNative({
2007
+ start: async (request) => {
2008
+ requests.push(request);
2009
+ return { lineageId: `native-${request.focus ?? "default"}`, state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 2, changedLines: 7, correctionBudget: 4, action: "created", lensesRequired: true };
2010
+ },
2011
+ targetStatus: async () => targetStatusFixture({ applicability: "unrelated", action: "start" }),
2012
+ }));
2013
+ for (const focus of ["risk", "resilience", "readability", "reliability"] as const) {
2014
+ await controller.execute(`focused-${focus}`, { operation: "start", input: JSON.stringify({ mode: "ordinary", focus }) }, undefined, undefined, context(cwd));
2015
+ }
2016
+ await controller.execute("default-focus", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
2017
+ assert.deepEqual(requests, [
2018
+ ...(["risk", "resilience", "readability", "reliability"] as const).map((focus) => ({ cwd, focus, targetIdentity: `sha256:${"a".repeat(64)}`, projection: "workspace" as const })),
2019
+ { cwd, targetIdentity: `sha256:${"a".repeat(64)}`, projection: "workspace" },
2020
+ ]);
2021
+ });
2022
+
2023
+ test("native ordinary START rejects invalid focus before native calls", async (t) => {
2024
+ const cwd = repository(t);
2025
+ let starts = 0;
2026
+ const { controller } = runtime(fakeNative({
2027
+ start: async () => {
2028
+ starts += 1;
2029
+ return { lineageId: "must-not-start", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true };
2030
+ },
2031
+ targetStatus: async () => targetStatusFixture({ applicability: "unrelated", action: "start" }),
2032
+ }));
2033
+ for (const focus of ["", "risk ", "RISK", "all", 42, null, [], {}]) {
2034
+ const rejected = await controller.execute("invalid-focus", { operation: "start", input: JSON.stringify({ mode: "ordinary", focus }) }, undefined, undefined, context(cwd));
2035
+ assert.deepEqual(rejected.details, {
2036
+ operation: "start",
2037
+ status: "blocked",
2038
+ outcome: "native-start-input-invalid",
2039
+ reason: "focus-invalid",
2040
+ lineage_created: false,
2041
+ mutation_performed: false,
2042
+ mutation_outcome: "none",
2043
+ reset_eligible: false,
2044
+ });
2045
+ }
2046
+ assert.equal(starts, 0);
2047
+ });
2048
+
2049
+ test("native ordinary START rejects malformed input before resolving the review-mode gate", async (t) => {
2050
+ const cwd = repository(t);
2051
+ let reviewModeCalls = 0;
2052
+ let starts = 0;
2053
+ const { controller } = runtime(fakeNative({
2054
+ reviewMode: async () => {
2055
+ reviewModeCalls += 1;
2056
+ throw new Error("review mode must not be resolved for malformed ordinary START input");
2057
+ },
2058
+ start: async () => {
2059
+ starts += 1;
2060
+ return { lineageId: "must-not-start", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true };
2061
+ },
2062
+ }));
2063
+ for (const [input, reason] of [
2064
+ [{ mode: "ordinary", policyHash: "legacy" }, "legacy-policy-hash-unsupported"],
2065
+ [{ mode: "ordinary", focus: "all" }, "focus-invalid"],
2066
+ [{ mode: "ordinary", unexpected: true }, "unknown-field"],
2067
+ [{ mode: "ordinary", policyPath: "outside.json" }, "policy-path-outside-scope"],
2068
+ [{ mode: "ordinary", baseRef: " " }, "base-ref-invalid"],
2069
+ [{ mode: "ordinary", baseRef: "origin/main" }, "committed-only-required"],
2070
+ [{ mode: "ordinary", committedOnly: true }, "committed-only-invalid"],
2071
+ [{ mode: "ordinary", baseRef: "refs/heads/missing", committedOnly: true }, "base-ref-unresolvable"],
2072
+ ] as const) {
2073
+ const rejected = await controller.execute("malformed-start", { operation: "start", input: JSON.stringify(input) }, undefined, undefined, context(cwd));
2074
+ assert.equal((rejected.details as { reason?: unknown }).reason, reason);
2075
+ }
2076
+ assert.equal(reviewModeCalls, 0);
2077
+ assert.equal(starts, 0);
2078
+ });
2079
+
1610
2080
  test("native START preserves the default dirty-inclusive candidate without base flags", async (t) => {
1611
2081
  const cwd = repository(t);
1612
2082
  writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
@@ -1662,6 +2132,47 @@ test("native START binds an acknowledged committed range and native identity to
1662
2132
  }
1663
2133
  });
1664
2134
 
2135
+ test("native START binds a default dirty-inclusive candidate on an unborn repository against Git's empty tree", async (t) => {
2136
+ // Unborn repository: symbolic HEAD, no commits, staged + untracked content.
2137
+ // The default targetStatus helper builds a REAL candidate view from this
2138
+ // repo, so this exercises the real resolveCandidateBase/materializeCandidate
2139
+ // path on an unborn repository, not a synthetic adapter stub.
2140
+ const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-native-controller-unborn-"));
2141
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
2142
+ execFileSync("git", ["init", "-b", "main"], { cwd });
2143
+ writeFileSync(join(cwd, "staged.txt"), "first staged\n");
2144
+ execFileSync("git", ["add", "staged.txt"], { cwd });
2145
+ writeFileSync(join(cwd, "untracked.ts"), "export const untracked = true;\n");
2146
+ const emptyTree = execFileSync("git", ["-C", cwd, "mktree"], { encoding: "utf8", input: "" }).trim();
2147
+ const candidateViews = new CandidateViewRegistry();
2148
+ const requests: Parameters<NativeReviewCli["start"]>[0][] = [];
2149
+ const { controller } = runtime(fakeNative({
2150
+ start: async (request) => {
2151
+ requests.push(request);
2152
+ return { lineageId: "unborn-lineage", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 2, changedLines: 2, correctionBudget: 1, action: "created", lensesRequired: true };
2153
+ },
2154
+ }), undefined, undefined, undefined, candidateViews);
2155
+ const started = await controller.execute("unborn-start", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
2156
+ const view = candidateViews.resolveForLens("unborn-lineage", "review-reliability");
2157
+ try {
2158
+ // The unborn candidate base is Git's empty tree, not a phantom commit.
2159
+ assert.equal(view.baseTree, emptyTree);
2160
+ assert.equal(view.baseCommit, "HEAD");
2161
+ assert.equal(view.committedOnly, false);
2162
+ // Staged and untracked workspace content is preserved in the candidate.
2163
+ assert.deepEqual(view.paths, ["staged.txt", "untracked.ts"]);
2164
+ // No baseRef is sent to native START: the unborn default uses the
2165
+ // workspace projection only, exactly as the provider expects.
2166
+ assert.deepEqual(requests, [{ cwd, targetIdentity: `sha256:${"a".repeat(64)}`, projection: "workspace" }]);
2167
+ const actorBinding = (started.details as { actor_binding: { workspace_root: string; candidate_root: string; candidate_tree: string; candidate_paths: readonly string[] } }).actor_binding;
2168
+ assert.equal(actorBinding.workspace_root, cwd);
2169
+ assert.equal(actorBinding.candidate_tree, view.candidateTree);
2170
+ assert.deepEqual(actorBinding.candidate_paths, view.paths);
2171
+ } finally {
2172
+ view.cleanup();
2173
+ }
2174
+ });
2175
+
1665
2176
  test("native START fails closed before mutation when the workspace target and immutable candidate view differ", async (t) => {
1666
2177
  const cwd = repository(t);
1667
2178
  writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
@@ -1718,6 +2229,52 @@ test("native START re-verifies candidate-view integrity before granting workspac
1718
2229
  assert.equal(starts, 0);
1719
2230
  });
1720
2231
 
2232
+ test("native START preserves an explicit base-resolution timeout diagnostic without native START", async (t) => {
2233
+ const cwd = repository(t);
2234
+ const bin = join(cwd, "test-bin");
2235
+ mkdirSync(bin);
2236
+ const fakeGit = join(bin, "git");
2237
+ writeFileSync(fakeGit, "#!/bin/sh\nexec sleep 1\n");
2238
+ chmodSync(fakeGit, 0o755);
2239
+ const previousPath = process.env.PATH;
2240
+ const previousTimeout = process.env.GENTLE_PI_CANDIDATE_GIT_TIMEOUT_MS;
2241
+ t.after(() => {
2242
+ if (previousPath === undefined) delete process.env.PATH;
2243
+ else process.env.PATH = previousPath;
2244
+ if (previousTimeout === undefined) delete process.env.GENTLE_PI_CANDIDATE_GIT_TIMEOUT_MS;
2245
+ else process.env.GENTLE_PI_CANDIDATE_GIT_TIMEOUT_MS = previousTimeout;
2246
+ });
2247
+ process.env.PATH = `${bin}${delimiter}${previousPath ?? ""}`;
2248
+ process.env.GENTLE_PI_CANDIDATE_GIT_TIMEOUT_MS = "1";
2249
+ let starts = 0;
2250
+ let statuses = 0;
2251
+ const { controller } = runtime(fakeNative({
2252
+ targetStatus: async () => {
2253
+ statuses += 1;
2254
+ throw new Error("target status must not run after base resolution fails");
2255
+ },
2256
+ start: async () => {
2257
+ starts += 1;
2258
+ return { lineageId: "must-not-start", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true };
2259
+ },
2260
+ }), undefined, undefined, undefined, new CandidateViewRegistry());
2261
+ const result = await controller.execute("base-resolution-timeout", { operation: "start", input: JSON.stringify({ mode: "ordinary", baseRef: "refs/heads/main", committedOnly: true }) }, undefined, undefined, context(cwd));
2262
+ const details = result.details as Record<string, unknown>;
2263
+ assert.equal(details.outcome, "native-operation-failed");
2264
+ assert.equal(details.lineage_created, false);
2265
+ assert.equal(details.mutation_outcome, "none");
2266
+ assert.deepEqual(details.diagnostics, {
2267
+ phase: "candidate-view",
2268
+ category: "timeout",
2269
+ git_subcommand: "for-each-ref",
2270
+ timeout_ms: 1,
2271
+ max_buffer_bytes: 64 * 1024 * 1024,
2272
+ message: "candidate-view Git command for-each-ref timed out after 1ms; inspect the candidate state before any new START",
2273
+ });
2274
+ assert.equal(statuses, 0);
2275
+ assert.equal(starts, 0);
2276
+ });
2277
+
1721
2278
  test("native START rejects an unresolvable explicit base before native mutation", async (t) => {
1722
2279
  const cwd = repository(t);
1723
2280
  let starts = 0;
@@ -1852,33 +2409,294 @@ test("native ordinary START blocks unknown input fields before native calls", as
1852
2409
  const { controller } = runtime(fakeNative({
1853
2410
  start: async () => {
1854
2411
  starts += 1;
1855
- return { lineageId: "native-lineage", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 2, changedLines: 7, correctionBudget: 4, action: "created", lensesRequired: true };
2412
+ return { lineageId: "native-lineage", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 2, changedLines: 7, correctionBudget: 4, action: "created", lensesRequired: true };
2413
+ },
2414
+ }));
2415
+ for (const field of ["base_ref", "focus_mode", "unexpected"]) {
2416
+ const rejected = await controller.execute("unknown-start-field", { operation: "start", input: JSON.stringify({ mode: "ordinary", [field]: "origin/main" }) }, undefined, undefined, context(cwd));
2417
+ assert.deepEqual(rejected.details, {
2418
+ operation: "start",
2419
+ status: "blocked",
2420
+ outcome: "native-start-input-invalid",
2421
+ reason: "unknown-field",
2422
+ field,
2423
+ lineage_created: false,
2424
+ mutation_performed: false,
2425
+ mutation_outcome: "none",
2426
+ reset_eligible: false,
2427
+ });
2428
+ }
2429
+ assert.equal(starts, 0);
2430
+ });
2431
+
2432
+ test("ordinary START rejects a legacy policy hash before native availability", async (t) => {
2433
+ const cwd = repository(t);
2434
+ const { controller } = runtime(null);
2435
+ const result = await controller.execute("legacy-start", { operation: "start", input: JSON.stringify({ mode: "ordinary", policyHash: "a".repeat(64) }) }, undefined, undefined, context(cwd));
2436
+ assert.equal((result.details as { outcome?: string }).outcome, "native-start-legacy-policy-hash-unsupported");
2437
+ });
2438
+
2439
+ test("INSPECT reports a missing package-local native binary with fail-closed reinstall guidance", async (t) => {
2440
+ const cwd = repository(t);
2441
+ const rawPath = "/private/workspace/.gentle-ai/gentle-ai";
2442
+ const rawSecret = "package-binary-secret";
2443
+ const { controller } = runtime(fakeNative({
2444
+ targetStatus: async () => {
2445
+ throw new NativeReviewCliError(
2446
+ NATIVE_REVIEW_ERROR_CODE.PACKAGE_BINARY_MISSING,
2447
+ NATIVE_REVIEW_OPERATION.VERSION,
2448
+ false,
2449
+ false,
2450
+ `missing ${rawPath} token=${rawSecret}`,
2451
+ );
2452
+ },
2453
+ }));
2454
+ const response = await controller.execute("inspect-package-binary-missing", { operation: "inspect" }, undefined, undefined, context(cwd));
2455
+ const details = response.details as Record<string, unknown>;
2456
+ assert.equal(details.status, "blocked");
2457
+ assert.equal(details.outcome, "native-status-package-binary-missing");
2458
+ assert.deepEqual(details.diagnostics, {
2459
+ operation: NATIVE_REVIEW_OPERATION.VERSION,
2460
+ error_code: NATIVE_REVIEW_ERROR_CODE.PACKAGE_BINARY_MISSING,
2461
+ timed_out: false,
2462
+ output_limit_exceeded: false,
2463
+ });
2464
+ assert.equal(details.inventory_complete, false);
2465
+ assert.equal(details.mutation_performed, false);
2466
+ assert.equal(details.mutation_outcome, "none");
2467
+ assert.equal(details.next_action, "reinstall-package-local-gentle-ai");
2468
+ assert.doesNotMatch(JSON.stringify(details), new RegExp(`${rawPath}|${rawSecret}`));
2469
+ });
2470
+
2471
+ test("START reports a missing package-local native binary before any native mutation", async (t) => {
2472
+ const cwd = repository(t);
2473
+ let starts = 0;
2474
+ const { controller } = runtime(fakeNative({
2475
+ targetStatus: async () => {
2476
+ throw new NativeReviewCliError(
2477
+ NATIVE_REVIEW_ERROR_CODE.PACKAGE_BINARY_MISSING,
2478
+ NATIVE_REVIEW_OPERATION.VERSION,
2479
+ false,
2480
+ false,
2481
+ "package-local binary missing",
2482
+ );
2483
+ },
2484
+ start: async () => {
2485
+ starts += 1;
2486
+ return { lineageId: "must-not-start", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true };
2487
+ },
2488
+ }));
2489
+ const response = await controller.execute("start-package-binary-missing", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
2490
+ const details = response.details as Record<string, unknown>;
2491
+ assert.equal(details.status, "blocked");
2492
+ assert.equal(details.outcome, "native-status-package-binary-missing");
2493
+ assert.equal(details.inventory_complete, false);
2494
+ assert.equal(details.lineage_created, false);
2495
+ assert.equal(details.mutation_performed, false);
2496
+ assert.equal(details.mutation_outcome, "none");
2497
+ assert.equal(details.next_action, "reinstall-package-local-gentle-ai");
2498
+ assert.equal(starts, 0);
2499
+ });
2500
+
2501
+ test("REPAIR reports a missing package-local native binary without touching authority", async (t) => {
2502
+ const cwd = repository(t);
2503
+ const { controller } = runtime(fakeNative({
2504
+ targetStatus: async () => {
2505
+ throw new NativeReviewCliError(
2506
+ NATIVE_REVIEW_ERROR_CODE.PACKAGE_BINARY_MISSING,
2507
+ NATIVE_REVIEW_OPERATION.VERSION,
2508
+ false,
2509
+ false,
2510
+ "package-local binary missing",
2511
+ );
2512
+ },
2513
+ }));
2514
+ const response = await controller.execute("repair-package-binary-missing", { operation: "repair" }, undefined, undefined, context(cwd));
2515
+ const details = response.details as Record<string, unknown>;
2516
+ assert.equal(details.status, "blocked");
2517
+ assert.equal(details.outcome, "native-status-package-binary-missing");
2518
+ assert.equal(details.inventory_complete, false);
2519
+ assert.equal(details.mutation_performed, false);
2520
+ assert.equal(details.mutation_outcome, "none");
2521
+ assert.equal(details.next_action, "reinstall-package-local-gentle-ai");
2522
+ });
2523
+
2524
+ test("START preserves bounded sanitized version-process diagnostics before native mutation", async (t) => {
2525
+ const cwd = repository(t);
2526
+ const rawPath = "/private/workspace/gentle-ai";
2527
+ const rawSecret = "version-process-secret";
2528
+ const stderr = `version process failed token=${rawSecret}\n${"x".repeat(5_000)}`;
2529
+ const foreignError = Object.assign(new Error(`version process failed at ${rawPath} token=${rawSecret}`), {
2530
+ name: "NativeReviewCliError",
2531
+ code: NATIVE_REVIEW_ERROR_CODE.NON_ZERO,
2532
+ diagnostics: {
2533
+ operation: NATIVE_REVIEW_OPERATION.VERSION,
2534
+ error_code: NATIVE_REVIEW_ERROR_CODE.NON_ZERO,
2535
+ exit_code: 17,
2536
+ timed_out: false,
2537
+ output_limit_exceeded: false,
2538
+ stderr,
2539
+ },
2540
+ });
2541
+ let starts = 0;
2542
+ const { controller } = runtime(fakeNative({
2543
+ targetStatus: async () => { throw foreignError; },
2544
+ start: async () => {
2545
+ starts += 1;
2546
+ return { lineageId: "must-not-start", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true };
2547
+ },
2548
+ }));
2549
+ const response = await controller.execute("start-version-process-failure", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
2550
+ const details = response.details as Record<string, unknown>;
2551
+ const diagnostics = details.diagnostics as { operation?: string; error_code?: string; stderr?: string };
2552
+ assert.equal(details.status, "blocked");
2553
+ assert.equal(details.outcome, "native-operation-failed");
2554
+ assert.equal(diagnostics.operation, NATIVE_REVIEW_OPERATION.VERSION);
2555
+ assert.equal(diagnostics.error_code, NATIVE_REVIEW_ERROR_CODE.NON_ZERO);
2556
+ assert.match(diagnostics.stderr ?? "", /token=\[REDACTED\]/);
2557
+ assert.ok((diagnostics.stderr?.length ?? 0) <= 4_096);
2558
+ assert.equal(details.lineage_created, false);
2559
+ assert.equal(details.mutation_performed, false);
2560
+ assert.equal(details.mutation_outcome, "none");
2561
+ assert.equal(starts, 0);
2562
+ assert.doesNotMatch(JSON.stringify(details), new RegExp(`${rawPath}|${rawSecret}`));
2563
+
2564
+ const inspect = await controller.execute("inspect-version-process-failure", { operation: "inspect" }, undefined, undefined, context(cwd));
2565
+ const inspectDetails = inspect.details as Record<string, unknown>;
2566
+ assert.equal(inspectDetails.status, "blocked");
2567
+ assert.equal(inspectDetails.outcome, "native-status-unavailable");
2568
+ assert.deepEqual(inspectDetails.diagnostics, details.diagnostics);
2569
+ assert.equal(inspectDetails.inventory_complete, false);
2570
+ assert.equal(inspectDetails.mutation_performed, false);
2571
+ assert.equal(inspectDetails.mutation_outcome, "none");
2572
+ assert.doesNotMatch(JSON.stringify(inspectDetails), new RegExp(`${rawPath}|${rawSecret}`));
2573
+ });
2574
+
2575
+ test("INSPECT preserves bounded sanitized review/status process diagnostics and remains fail-closed", async (t) => {
2576
+ const cwd = repository(t);
2577
+ const rawPath = "/private/workspace/review-status";
2578
+ const rawSecret = "review-status-secret";
2579
+ const stderr = `review status failed token=${rawSecret}\n${"x".repeat(5_000)}`;
2580
+ const foreignError = Object.assign(new Error(`review status failed at ${rawPath} token=${rawSecret}`), {
2581
+ name: "NativeReviewCliError",
2582
+ code: NATIVE_REVIEW_ERROR_CODE.NON_ZERO,
2583
+ diagnostics: {
2584
+ operation: NATIVE_REVIEW_OPERATION.STATUS,
2585
+ error_code: NATIVE_REVIEW_ERROR_CODE.NON_ZERO,
2586
+ exit_code: 18,
2587
+ timed_out: false,
2588
+ output_limit_exceeded: false,
2589
+ stderr,
2590
+ },
2591
+ });
2592
+ const { controller } = runtime(fakeNative({
2593
+ targetStatus: async () => { throw foreignError; },
2594
+ }));
2595
+ const response = await controller.execute("inspect-review-status-process-failure", { operation: "inspect" }, undefined, undefined, context(cwd));
2596
+ const details = response.details as Record<string, unknown>;
2597
+ const diagnostics = details.diagnostics as { operation?: string; error_code?: string; stderr?: string };
2598
+ assert.equal(details.status, "blocked");
2599
+ assert.equal(details.outcome, "native-status-unavailable");
2600
+ assert.equal(diagnostics.operation, NATIVE_REVIEW_OPERATION.STATUS);
2601
+ assert.equal(diagnostics.error_code, NATIVE_REVIEW_ERROR_CODE.NON_ZERO);
2602
+ assert.match(diagnostics.stderr ?? "", /token=\[REDACTED\]/);
2603
+ assert.ok((diagnostics.stderr?.length ?? 0) <= 4_096);
2604
+ assert.equal(details.inventory_complete, false);
2605
+ assert.equal(details.mutation_performed, false);
2606
+ assert.equal(details.mutation_outcome, "none");
2607
+ assert.doesNotMatch(JSON.stringify(details), new RegExp(`${rawPath}|${rawSecret}`));
2608
+ });
2609
+
2610
+ test("INSPECT keeps version-incompatible target status failures mapped to native-status-unsupported", async (t) => {
2611
+ const cwd = repository(t);
2612
+ const { controller } = runtime(fakeNative({
2613
+ targetStatus: async () => {
2614
+ throw new NativeReviewCliError(
2615
+ NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE,
2616
+ NATIVE_REVIEW_OPERATION.VERSION,
2617
+ true,
2618
+ false,
2619
+ "native version is incompatible",
2620
+ );
2621
+ },
2622
+ }));
2623
+ const response = await controller.execute("inspect-version-incompatible", { operation: "inspect" }, undefined, undefined, context(cwd));
2624
+ const details = response.details as Record<string, unknown>;
2625
+ assert.equal(details.status, "blocked");
2626
+ assert.equal(details.outcome, "native-status-unsupported");
2627
+ assert.equal(details.inventory_complete, false);
2628
+ assert.equal(details.mutation_performed, false);
2629
+ assert.equal(details.mutation_outcome, undefined);
2630
+ });
2631
+
2632
+ test("INSPECT preserves output-limit diagnostics from native review status and remains fail-closed", async (t) => {
2633
+ const cwd = repository(t);
2634
+ const diagnostics = {
2635
+ operation: NATIVE_REVIEW_OPERATION.STATUS,
2636
+ error_code: NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT,
2637
+ output_limit_exceeded: true,
2638
+ timed_out: false,
2639
+ };
2640
+ const { controller } = runtime(fakeNative({
2641
+ targetStatus: async () => {
2642
+ throw new NativeReviewCliError(
2643
+ NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT,
2644
+ NATIVE_REVIEW_OPERATION.STATUS,
2645
+ true,
2646
+ false,
2647
+ "native process output exceeded limit",
2648
+ diagnostics,
2649
+ );
2650
+ },
2651
+ }));
2652
+ const response = await controller.execute("inspect-output-limit", { operation: "inspect" }, undefined, undefined, context(cwd));
2653
+ const details = response.details as Record<string, unknown>;
2654
+ assert.equal(details.status, "blocked");
2655
+ assert.equal(details.outcome, "native-status-unavailable");
2656
+ assert.deepEqual(details.diagnostics, diagnostics);
2657
+ assert.equal(details.inventory_complete, false);
2658
+ assert.equal(details.next_action, "require-complete-native-authority-inventory");
2659
+ assert.equal(details.mutation_performed, false);
2660
+ assert.equal(details.mutation_outcome, "none");
2661
+ });
2662
+
2663
+ test("START preserves actionable review/status output-limit diagnostics before native mutation", async (t) => {
2664
+ const cwd = repository(t);
2665
+ const diagnostics = {
2666
+ operation: NATIVE_REVIEW_OPERATION.STATUS,
2667
+ error_code: NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT,
2668
+ output_limit_exceeded: true,
2669
+ timed_out: false,
2670
+ max_buffer_bytes: 16 * 1024 * 1024,
2671
+ configuration_hint: "Inspect native review state before any new START; GENTLE_PI_REVIEW_MAX_BUFFER_BYTES accepts a positive decimal up to 67108864.",
2672
+ };
2673
+ let starts = 0;
2674
+ const { controller } = runtime(fakeNative({
2675
+ targetStatus: async () => {
2676
+ throw new NativeReviewCliError(
2677
+ NATIVE_REVIEW_ERROR_CODE.OUTPUT_LIMIT,
2678
+ NATIVE_REVIEW_OPERATION.STATUS,
2679
+ true,
2680
+ false,
2681
+ "native process output exceeded limit",
2682
+ diagnostics,
2683
+ );
2684
+ },
2685
+ start: async () => {
2686
+ starts += 1;
2687
+ return { lineageId: "must-not-start", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true };
1856
2688
  },
1857
2689
  }));
1858
- for (const field of ["base_ref", "unexpected"]) {
1859
- const rejected = await controller.execute("unknown-start-field", { operation: "start", input: JSON.stringify({ mode: "ordinary", [field]: "origin/main" }) }, undefined, undefined, context(cwd));
1860
- assert.deepEqual(rejected.details, {
1861
- operation: "start",
1862
- status: "blocked",
1863
- outcome: "native-start-input-invalid",
1864
- reason: "unknown-field",
1865
- field,
1866
- lineage_created: false,
1867
- mutation_performed: false,
1868
- mutation_outcome: "none",
1869
- reset_eligible: false,
1870
- });
1871
- }
2690
+ const response = await controller.execute("start-status-output-limit", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
2691
+ const details = response.details as Record<string, unknown>;
2692
+ assert.equal(details.status, "blocked");
2693
+ assert.equal(details.outcome, "native-operation-failed");
2694
+ assert.deepEqual(details.diagnostics, diagnostics);
2695
+ assert.equal(details.lineage_created, false);
2696
+ assert.equal(details.mutation_outcome, "none");
1872
2697
  assert.equal(starts, 0);
1873
2698
  });
1874
2699
 
1875
- test("ordinary START fails closed before legacy policy handling when target status is unavailable", async (t) => {
1876
- const cwd = repository(t);
1877
- const { controller } = runtime(null);
1878
- const result = await controller.execute("legacy-start", { operation: "start", input: JSON.stringify({ mode: "ordinary", policyHash: "a".repeat(64) }) }, undefined, undefined, context(cwd));
1879
- assert.equal((result.details as { outcome?: string }).outcome, "native-status-unsupported");
1880
- });
1881
-
1882
2700
  test("general STATUS and INSPECT use negotiated target status without mutation or inventory reads", async (t) => {
1883
2701
  const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-native-controller-"));
1884
2702
  t.after(() => rmSync(cwd, { recursive: true, force: true }));
@@ -1922,7 +2740,7 @@ test("legacy compact FINALIZE is a typed read-only rejection without native fall
1922
2740
  }));
1923
2741
  const result = await controller.execute(
1924
2742
  "legacy-finalize",
1925
- { operation: "finalize", lineageId, input: JSON.stringify({ review_result: { lens_results: [] } }) },
2743
+ { operation: "finalize", lineageId, input: JSON.stringify({}) },
1926
2744
  undefined,
1927
2745
  undefined,
1928
2746
  context(cwd),
@@ -1962,7 +2780,7 @@ test("legacy graph-v1 FINALIZE is a typed read-only rejection without native fal
1962
2780
  }));
1963
2781
  const result = await controller.execute(
1964
2782
  "legacy-graph-finalize",
1965
- { operation: "finalize", lineageId, input: JSON.stringify({ review_result: { lens_results: [] } }) },
2783
+ { operation: "finalize", lineageId, input: JSON.stringify({}) },
1966
2784
  undefined,
1967
2785
  undefined,
1968
2786
  context(cwd),
@@ -2253,7 +3071,7 @@ test("native pre-commit rejects an unproven staged projection before native auth
2253
3071
  }), undefined, undefined, undefined, candidateViews);
2254
3072
  const started = await controller.execute("start", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
2255
3073
  const lineageId = (started.details as { result: { lineage_id: string } }).result.lineage_id;
2256
- await controller.execute("finalize", { operation: "finalize", lineageId, input: JSON.stringify({ review_result: { lens_results: [{ findings: [], evidence: ["candidate reviewed"] }] } }) }, undefined, undefined, context(cwd));
3074
+ await controller.execute("finalize", { operation: "finalize", lineageId, input: JSON.stringify({}) }, undefined, undefined, context(cwd));
2257
3075
  writeFileSync(join(cwd, "app.ts"), "export const value = 3;\n");
2258
3076
  git(cwd, "add", "--", "app.ts", "initially-untracked.ts");
2259
3077
  const result = await controller.execute("validate", { operation: "validate", lineageId, idempotencyKey: "projection-drift", command: "git commit -m native", input: "{}" }, undefined, undefined, context(cwd));
@@ -2276,7 +3094,7 @@ test("native pre-commit binds the exact tracked and initially-untracked projecti
2276
3094
  const { controller, toolCall } = runtime(native, undefined, undefined, undefined, candidateViews);
2277
3095
  const started = await controller.execute("start", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
2278
3096
  const lineageId = (started.details as { result: { lineage_id: string } }).result.lineage_id;
2279
- await controller.execute("finalize", { operation: "finalize", lineageId, input: JSON.stringify({ review_result: { lens_results: [{ findings: [], evidence: ["candidate reviewed"] }] } }) }, undefined, undefined, context(cwd));
3097
+ await controller.execute("finalize", { operation: "finalize", lineageId, input: JSON.stringify({}) }, undefined, undefined, context(cwd));
2280
3098
  git(cwd, "add", "--", "app.ts", "initially-untracked.ts");
2281
3099
  const command = "git commit -m exact-projection";
2282
3100
  const allowed = await controller.execute("validate", { operation: "validate", lineageId, idempotencyKey: "exact-projection", command, input: "{}" }, undefined, undefined, context(cwd));
@@ -3687,7 +4505,7 @@ test("controller preserves every historical response-schema empty-context pre-PR
3687
4505
  }
3688
4506
  });
3689
4507
 
3690
- test("parallel 4R dispatch receives one compact changed scope and blocks oversized scope before any actor", async (t) => {
4508
+ test("parallel 4R dispatch receives readable and compact changed scopes before any actor", async (t) => {
3691
4509
  const cwd = repository(t);
3692
4510
  for (let index = 0; index < 248; index += 1) {
3693
4511
  writeFileSync(join(cwd, `unchanged-${String(index).padStart(3, "0")}.txt`), "base\n");
@@ -3724,9 +4542,24 @@ test("parallel 4R dispatch receives one compact changed scope and blocks oversiz
3724
4542
  start: async () => ({ lineageId: "c4-oversized", state: "reviewing", riskLevel: "medium", selectedLenses: ["review-reliability"], changedFiles: 80, changedLines: 80, correctionBudget: 40, action: "created", lensesRequired: true }),
3725
4543
  }), undefined, undefined, undefined, oversizedViews);
3726
4544
  await oversized.controller.execute("c4-oversized-start", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, context(cwd));
3727
- const rejected = await oversized.toolCall({ toolName: "subagent_run", input: { agent: "review-reliability", task: "Review oversized scope", mode: "task" } }, context(cwd)) as { block?: boolean };
3728
- assert.equal(rejected.block, true, "oversized scope blocks before an actor can launch");
3729
- oversizedViews.resolveForLens("c4-oversized", "review-reliability").cleanup();
4545
+ const oversizedDispatch = { agent: "review-reliability", task: "Review oversized scope", mode: "task" };
4546
+ assert.equal(await oversized.toolCall({ toolName: "subagent_run", input: oversizedDispatch }, context(cwd)), undefined, "a compressible oversized scope reaches the actor through its compact manifest");
4547
+ assert.match(oversizedDispatch.task, /Frozen changed scope manifest \(gzip\+base64url\):/);
4548
+ assert.match(oversizedDispatch.task, /Call `gentle_review_scope`/);
4549
+ assert.ok(Buffer.byteLength(oversizedDispatch.task, "utf8") <= 4_096 + "Review oversized scope".length);
4550
+ const compact = compactCandidateContextManifest(oversizedDispatch.task);
4551
+ const actorEntries: Array<{ path: string; mode: string; gitlinkObjectId?: string }> = [];
4552
+ let cursor: number | undefined = 0;
4553
+ while (cursor !== undefined) {
4554
+ const result = await oversized.scopeReader.execute("actor-scope", { manifest: compact.encoded, sha256: compact.sha256, cursor }, undefined, undefined, context(cwd));
4555
+ const page = result.details as { entries: typeof actorEntries; nextCursor?: number; totalPaths: number };
4556
+ actorEntries.push(...page.entries);
4557
+ cursor = page.nextCursor;
4558
+ }
4559
+ const actorCandidate = oversizedViews.resolveForLens("c4-oversized", "review-reliability");
4560
+ assert.deepEqual(actorEntries.map((entry) => entry.path).sort(), [...actorCandidate.paths].sort(), "the actual actor tool recovers every compressed changed path");
4561
+ assert.equal(actorEntries.some((entry) => entry.path.startsWith("unchanged-")), false, "the actor tool never falls back to the ambient or full candidate tree");
4562
+ actorCandidate.cleanup();
3730
4563
  });
3731
4564
 
3732
4565
  test("INSPECT relays negotiated target status without inventory reconstruction or mutation", async (t) => {
@@ -3754,26 +4587,6 @@ test("INSPECT relays negotiated target status without inventory reconstruction o
3754
4587
  assert.equal(mutations, 0);
3755
4588
  });
3756
4589
 
3757
- test("native remediation classification accepts only invalid legacy, compact, collision, or reset evidence", () => {
3758
- const status = (authorityStatus: NativeReviewStatusResult["status"], entries: NativeReviewStatusResult["entries"]): NativeReviewStatusResult => ({
3759
- repository: "/repo",
3760
- complete: authorityStatus !== "invalid",
3761
- authoritative: authorityStatus !== "invalid",
3762
- status: authorityStatus,
3763
- entries,
3764
- locks: [],
3765
- diagnostics: [],
3766
- raw: {},
3767
- });
3768
- assert.equal(classifyNativeReviewRemediation(status("invalid", [{ version: "legacy-v1", lineageId: "current", path: "/repo/legacy", status: "invalid", problems: [] }]), ["current"]).kind, NATIVE_REVIEW_REMEDIATION.LEGACY);
3769
- assert.equal(classifyNativeReviewRemediation(status("invalid", [{ version: "compact-v2", lineageId: "current", path: "/repo/compact", status: "invalid", problems: [] }]), ["current"]).kind, NATIVE_REVIEW_REMEDIATION.INVALID_OR_MIXED);
3770
- assert.equal(classifyNativeReviewRemediation({ ...status("same-lineage-mixed-collision", []), complete: false, authoritative: false }, ["current"]).kind, NATIVE_REVIEW_REMEDIATION.NONE);
3771
- assert.equal(classifyNativeReviewRemediation(status("reset-in-progress", [])).kind, NATIVE_REVIEW_REMEDIATION.NONE);
3772
- assert.equal(classifyNativeReviewRemediation(status("invalid", [])).kind, NATIVE_REVIEW_REMEDIATION.NONE);
3773
- assert.equal(classifyNativeReviewRemediation({ ...status("invalid", [{ version: "legacy-v1", path: "/repo/legacy", status: "invalid", problems: [] }]), complete: true }).kind, NATIVE_REVIEW_REMEDIATION.NONE);
3774
- assert.equal(classifyNativeReviewRemediation(status("approved", [{ version: "legacy-v1", path: "/repo/legacy", status: "approved", problems: [] }])).kind, NATIVE_REVIEW_REMEDIATION.NONE);
3775
- });
3776
-
3777
4590
  test("native INSPECT never reconstructs reset material from raw Pi corruption", async (t) => {
3778
4591
  const nativeStatus = (cwd: string, entries: NativeReviewStatusResult["entries"]): NativeReviewStatusResult => ({
3779
4592
  repository: cwd,
@@ -4276,3 +5089,635 @@ test("successful /gentle:review-mode disable clears pending authorizations even
4276
5089
  assert.equal(result.block, true);
4277
5090
  assert.match(result.reason, /receipt consumption failed closed/, "re-enabled delivery must perform fresh receipt discovery rather than reuse the pre-disable authorization");
4278
5091
  });
5092
+
5093
+ function gitStdin(cwd: string, arguments_: readonly string[], input: string): string {
5094
+ return execFileSync("git", [...arguments_], { cwd, encoding: "utf8", input }).trim();
5095
+ }
5096
+
5097
+ test("large repository end-to-end: tiny candidate diff reaches START, reviewer dispatch, FINALIZE, and the validation delivery gate", async (t) => {
5098
+ // Build a genuinely large synthetic repository (5000 unchanged entries) from a single
5099
+ // blob via `git mktree`, avoiding thousands of per-file filesystem writes/subprocesses.
5100
+ // The candidate diff is exactly one modified entry — the smallest review scope — so the
5101
+ // frozen candidate view materializes the full 5000-entry tree through the production
5102
+ // checkout-index path while the reviewer dispatch carries only the one changed path.
5103
+ const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-large-repo-e2e-"));
5104
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
5105
+ git(cwd, "init", "-b", "main");
5106
+ const baseBlob = gitStdin(cwd, ["hash-object", "-w", "--stdin"], "base\n");
5107
+ const largeCount = 5_000;
5108
+ const baseTreeInput = Array.from({ length: largeCount }, (_, index) =>
5109
+ `100644 blob ${baseBlob}\tunchanged-${String(index).padStart(4, "0")}.txt`,
5110
+ ).join("\n");
5111
+ const baseTree = gitStdin(cwd, ["mktree"], baseTreeInput);
5112
+ const candidateBlob = gitStdin(cwd, ["hash-object", "-w", "--stdin"], "candidate\n");
5113
+ const candidateTreeInput = Array.from({ length: largeCount }, (_, index) =>
5114
+ index === 0
5115
+ ? `100644 blob ${candidateBlob}\tunchanged-0000.txt`
5116
+ : `100644 blob ${baseBlob}\tunchanged-${String(index).padStart(4, "0")}.txt`,
5117
+ ).join("\n");
5118
+ const candidateTree = gitStdin(cwd, ["mktree"], candidateTreeInput);
5119
+ const commitIdentity = ["-c", "user.name=Large Repo E2E", "-c", "user.email=large@example.invalid"];
5120
+ const baseCommit = git(cwd, ...commitIdentity, "commit-tree", baseTree, "-m", "large base");
5121
+ const candidateCommit = git(cwd, ...commitIdentity, "commit-tree", candidateTree, "-p", baseCommit, "-m", "tiny candidate");
5122
+ git(cwd, "update-ref", "refs/heads/main", candidateCommit);
5123
+
5124
+ const candidateViews = new CandidateViewRegistry();
5125
+ const lenses = ["review-risk", "review-resilience", "review-readability", "review-reliability"] as const;
5126
+ const lineageId = "large-repo-e2e";
5127
+ let starts = 0;
5128
+ let finalizes = 0;
5129
+ let validates = 0;
5130
+ let finalizeRoot: string | undefined;
5131
+ const { controller, toolCall } = runtime(fakeNative({
5132
+ start: async () => {
5133
+ starts += 1;
5134
+ return { lineageId, state: "reviewing", riskLevel: "high", selectedLenses: lenses, changedFiles: 1, changedLines: 1, correctionBudget: 1, action: "created", lensesRequired: true };
5135
+ },
5136
+ finalize: async (request) => {
5137
+ finalizes += 1;
5138
+ finalizeRoot = request.cwd;
5139
+ return { lineageId, state: "approved", action: "approved", storeRevision: "r1" };
5140
+ },
5141
+ validate: async () => {
5142
+ validates += 1;
5143
+ return { allowed: true, result: "allow", action: "continue", reason: "native receipt matches the frozen large-repo candidate", gateContext: nativeGateContext(lineageId, "r1", candidateTree) };
5144
+ },
5145
+ targetStatus: async (request) => {
5146
+ if (request.lineageId === undefined) return candidateStartTargetStatus(request);
5147
+ const statusCandidate = new CandidateViewRegistry().create({ contributorRoot: request.cwd, baseRef: baseCommit });
5148
+ try {
5149
+ return targetStatusFixture({ lineageId, baseTree: statusCandidate.baseTree, currentCandidateTree: statusCandidate.candidateTree, paths: statusCandidate.paths });
5150
+ } finally {
5151
+ statusCandidate.cleanup();
5152
+ }
5153
+ },
5154
+ }), undefined, undefined, undefined, candidateViews);
5155
+
5156
+ // START binds the frozen candidate view for the large repository.
5157
+ const start = await controller.execute("large-repo-start", { operation: "start", input: JSON.stringify({ mode: "ordinary", baseRef: baseCommit, committedOnly: true }) }, undefined, undefined, context(cwd));
5158
+ assert.equal(starts, 1);
5159
+ assert.equal((start.details as { result: { lineage_id: string } }).result.lineage_id, lineageId);
5160
+ const frozen = candidateViews.resolveForLens(lineageId, "review-risk");
5161
+ try {
5162
+ // Immutable candidate identity: the frozen view binds the exact synthetic trees.
5163
+ assert.equal(frozen.baseTree, baseTree);
5164
+ assert.equal(frozen.candidateTree, candidateTree);
5165
+ assert.deepEqual(frozen.paths, ["unchanged-0000.txt"]);
5166
+ assert.equal(readdirSync(frozen.root).filter((entry) => entry !== ".git").length, largeCount);
5167
+ assert.equal(readFileSync(join(frozen.root, "unchanged-0000.txt"), "utf8"), "candidate\n");
5168
+ assert.equal(readFileSync(join(frozen.root, "unchanged-4999.txt"), "utf8"), "base\n");
5169
+
5170
+ // Reviewer dispatch injects the controller-owned candidate view carrying only the
5171
+ // tiny changed scope — never the 5000-entry unchanged repository bulk.
5172
+ const dispatch = { agents: [...lenses], task: "Review the tiny candidate diff in a large repository", mode: "task" };
5173
+ assert.equal(await toolCall({ toolName: "subagent_run", input: dispatch }, context(cwd)), undefined);
5174
+ assert.match(dispatch.task, /## Controller-owned candidate view/);
5175
+ assert.match(dispatch.task, new RegExp(`Frozen candidate tree: \`${candidateTree}\``));
5176
+ assert.match(dispatch.task, /unchanged-0000\.txt/);
5177
+ assert.doesNotMatch(dispatch.task, /unchanged-0001\.txt/);
5178
+ assert.doesNotMatch(dispatch.task, /unchanged-0499\.txt/);
5179
+ assert.ok(Buffer.byteLength(dispatch.task, "utf8") <= 4_096 + "Review the tiny candidate diff in a large repository".length);
5180
+
5181
+ // No live-worktree fallback: a rogue untracked file in the contributor working
5182
+ // directory (invisible to the committedOnly candidate view) cannot reach the dispatch.
5183
+ writeFileSync(join(cwd, "rogue-untracked.ts"), "export const rogue = true;\n");
5184
+ const rogueDispatch = { agent: "review-risk", task: "review after rogue", mode: "task" };
5185
+ assert.equal(await toolCall({ toolName: "subagent_run", input: rogueDispatch }, context(cwd)), undefined);
5186
+ assert.doesNotMatch(rogueDispatch.task, /rogue-untracked\.ts/);
5187
+ assert.match(rogueDispatch.task, new RegExp(`Frozen candidate tree: \`${candidateTree}\``));
5188
+
5189
+ // FINALIZE mutates against the frozen candidate root, not the contributor working directory.
5190
+ const finalize = await controller.execute("large-repo-finalize", { operation: "finalize", lineageId, input: JSON.stringify({}) }, undefined, undefined, context(cwd));
5191
+ assert.equal((finalize.details as { result: { state: string } }).result.state, "approved");
5192
+ assert.equal(finalizes, 1);
5193
+ assert.notEqual(finalizeRoot, cwd);
5194
+ assert.equal(finalizeRoot, frozen.root);
5195
+
5196
+ // Validation delivery gate: stage the frozen candidate tree and authorize delivery.
5197
+ git(cwd, "read-tree", candidateTree);
5198
+ const command = "git commit -m large-repo-delivery";
5199
+ const validated = await controller.execute("large-repo-validate", { operation: "validate", lineageId, idempotencyKey: "large-repo-delivery", command, input: "{}" }, undefined, undefined, context(cwd));
5200
+ assert.ok(validates >= 1);
5201
+ assert.notEqual((validated.details as { authorization?: unknown }).authorization, undefined);
5202
+ assert.equal((validated.details as { result?: { allowed?: boolean } }).result?.allowed, true);
5203
+ // The delivery gate allows the authorized command exactly once; replay is blocked.
5204
+ assert.equal(await toolCall({ toolName: "bash", input: { command } }, interactiveContext(cwd)), undefined);
5205
+ const replay = await toolCall({ toolName: "bash", input: { command } }, context(cwd)) as { block: boolean };
5206
+ assert.equal(replay.block, true);
5207
+ } finally {
5208
+ frozen.cleanup();
5209
+ }
5210
+ });
5211
+
5212
+ // Field defect (fambig, 2026-08-16, dev binary 2.4.0-main): at every
5213
+ // evidence-pending sub-state the provider renders the `review.capture-evidence`
5214
+ // collect slot with the identity native demands — for a correction that is the
5215
+ // FIX-DIFF `--target` identity plus an opaque `--repository-context`, never the
5216
+ // top-level live workspace snapshot identity. Rebinding the capture to
5217
+ // `negotiatedStatus.targetIdentity` failed deterministically with
5218
+ // "verification evidence binding does not match the current authority
5219
+ // revision". Collect satisfaction must execute the slot's rendered submission
5220
+ // tokens verbatim, exactly like captureResult does.
5221
+ const EVIDENCE_SLOT_FIX_TARGET = `sha256:${"e".repeat(64)}`;
5222
+ const EVIDENCE_SLOT_REPOSITORY_CONTEXT = `rctx1_${"f".repeat(64)}`;
5223
+
5224
+ function bindEvidenceSubmissionCollection(status: ReviewStatusV3, fixTargetIdentity = EVIDENCE_SLOT_FIX_TARGET, repositoryContext = EVIDENCE_SLOT_REPOSITORY_CONTEXT): ReviewStatusV3 {
5225
+ const lineageId = status.authority!.lineageId;
5226
+ const revision = status.authority!.revision;
5227
+ const argumentTokens = [
5228
+ `--lineage=${lineageId}`,
5229
+ `--expected-revision=${revision}`,
5230
+ `--target=${fixTargetIdentity}`,
5231
+ `--repository-context=${repositoryContext}`,
5232
+ "--outcome={{outcome}}",
5233
+ "--input={{input}}",
5234
+ ];
5235
+ status.nextTransition = {
5236
+ kind: "collect",
5237
+ reasonCode: "correction_repository_verification_required",
5238
+ collect: { inputs: [{
5239
+ name: "evidence",
5240
+ schema: "https://gentle-ai.dev/schema/review/verification-evidence/v1",
5241
+ captureOperation: "review.capture-evidence",
5242
+ arguments: [
5243
+ { name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
5244
+ { name: "expected-revision", value: revision, token: `--expected-revision=${revision}` },
5245
+ { name: "target", value: fixTargetIdentity, token: `--target=${fixTargetIdentity}` },
5246
+ { name: "repository-context", value: repositoryContext, token: `--repository-context=${repositoryContext}` },
5247
+ ],
5248
+ submissionDescriptor: {
5249
+ operationToken: "capture-evidence",
5250
+ argumentTokens,
5251
+ values: [
5252
+ { slot: "outcome", domain: "verification_outcome", allowedValues: ["passed", "verification_failed", "procedural_tooling_failed"], substitutionLocation: 4 },
5253
+ { slot: "input", domain: "artifact_path_or_stdin", schema: "https://gentle-ai.dev/schema/review/verification-evidence/v1", substitutionLocation: 5 },
5254
+ ],
5255
+ },
5256
+ }] },
5257
+ };
5258
+ delete status.validationRequest;
5259
+ return status;
5260
+ }
5261
+
5262
+ // The record the live binary returns for a slot-bound capture: it binds the
5263
+ // slot's fix-diff target identity, and its paths digest covers the record's
5264
+ // own (fix-diff) paths — NOT the frozen projection digest (captured 2026-08-16,
5265
+ // lineage review-2b6206ed68fb9128: record paths ["calc.go"] against projection
5266
+ // paths ["calc.go", "util.go"]).
5267
+ function capturedSlotEvidence(status: ReviewStatusV3, outcome: "passed" | "verification_failed" | "procedural_tooling_failed", identityDigit: string) {
5268
+ return {
5269
+ ...capturedCorrectionEvidence(status, outcome, identityDigit),
5270
+ targetIdentity: EVIDENCE_SLOT_FIX_TARGET,
5271
+ pathsDigest: `sha256:${identityDigit.repeat(64)}`,
5272
+ };
5273
+ }
5274
+
5275
+ test("correction evidence capture executes the collect slot's rendered submission tokens verbatim", async (t) => {
5276
+ for (const [outcome, expectedKind] of [
5277
+ ["passed", "run-targeted-validation"],
5278
+ ["verification_failed", "recapture-required"],
5279
+ ] as const) {
5280
+ await t.test(outcome, async (scenario) => {
5281
+ const cwd = repository(scenario);
5282
+ writeFileSync(join(cwd, "app.ts"), `export const outcome = ${JSON.stringify(outcome)};\n`);
5283
+ const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
5284
+ const beforeCapture = bindEvidenceSubmissionCollection(targetStatusFixture({
5285
+ lineageId: `slot-correction-${outcome.replaceAll("_", "-")}`,
5286
+ authorityState: "correction_required",
5287
+ baseTree: frozen.baseTree,
5288
+ currentCandidateTree: frozen.candidateTree,
5289
+ paths: frozen.paths,
5290
+ }));
5291
+ const expectedTokens = beforeCapture.nextTransition!.collect!.inputs[0]!.submissionDescriptor!.argumentTokens;
5292
+ const afterCapture = targetStatusFixture({
5293
+ lineageId: beforeCapture.authority!.lineageId,
5294
+ authorityState: outcome === "passed" ? "validating" : "correction_required",
5295
+ baseTree: frozen.baseTree,
5296
+ currentCandidateTree: frozen.candidateTree,
5297
+ paths: frozen.paths,
5298
+ });
5299
+ if (outcome === "passed") bindTargetedValidation(afterCapture);
5300
+ else bindEvidenceSubmissionCollection(afterCapture);
5301
+ frozen.cleanup();
5302
+ const misboundCaptures: Array<Record<string, unknown>> = [];
5303
+ const submissions: Array<Record<string, unknown>> = [];
5304
+ let statuses = 0;
5305
+ let finalizes = 0;
5306
+ const { controller } = runtime(fakeNative({
5307
+ targetStatus: async () => {
5308
+ statuses += 1;
5309
+ return statuses === 1 ? beforeCapture : afterCapture;
5310
+ },
5311
+ captureEvidence: async (request) => {
5312
+ misboundCaptures.push(request as unknown as Record<string, unknown>);
5313
+ throw new Error("misbound capture: the collect slot's rendered submission tokens were bypassed");
5314
+ },
5315
+ captureEvidenceSubmission: async (request: Record<string, unknown>) => {
5316
+ submissions.push(request);
5317
+ return capturedSlotEvidence(beforeCapture, outcome, outcome === "passed" ? "1" : "2");
5318
+ },
5319
+ finalize: async () => {
5320
+ finalizes += 1;
5321
+ return { lineageId: beforeCapture.authority!.lineageId, state: "approved", action: "approved", storeRevision: "r-final" };
5322
+ },
5323
+ } as unknown as Partial<NativeReviewCli>), undefined, undefined, undefined, new CandidateViewRegistry());
5324
+ const validation = outcome === "passed" ? {
5325
+ request_hash: "9".repeat(64), correction_ids: [],
5326
+ original_criteria: { passed: true, evidence: ["acceptance passes"] },
5327
+ correction_regression: { passed: true, evidence: ["regression passes"] },
5328
+ fix_caused_findings: [], follow_ups: [],
5329
+ } : undefined;
5330
+ const result = await controller.execute(`slot-correction-${outcome}`, {
5331
+ operation: "finalize",
5332
+ lineageId: beforeCapture.authority!.lineageId,
5333
+ input: JSON.stringify({ final_evidence: `evidence: ${outcome}`, final_verification_outcome: outcome, ...(validation === undefined ? {} : { validation }) }),
5334
+ }, undefined, undefined, context(cwd));
5335
+ const details = result.details as Record<string, unknown>;
5336
+ assert.deepEqual(misboundCaptures, [], "capture-evidence must never rebind to the top-level live workspace identity");
5337
+ assert.equal(submissions.length, 1, "the slot submission must be executed exactly once");
5338
+ const submission = submissions[0]!;
5339
+ assert.deepEqual(submission.argumentTokens, expectedTokens, "the provider-rendered submission tokens must be passed verbatim, in provider order");
5340
+ assert.equal(submission.outcomeSubstitutionLocation, 4);
5341
+ assert.equal(submission.inputSubstitutionLocation, 5);
5342
+ assert.equal(submission.cwd, undefined, "the slot's --repository-context is authoritative; never pass --cwd alongside it");
5343
+ assert.equal(submission.outcome, outcome);
5344
+ assert.equal(submission.evidenceDocument, `evidence: ${outcome}`);
5345
+ assert.equal((details.correction_step as { kind?: string } | undefined)?.kind, expectedKind);
5346
+ assert.equal(finalizes, outcome === "passed" ? 1 : 0);
5347
+ });
5348
+ }
5349
+ });
5350
+
5351
+ test("ordinary final verification at validating captures evidence through the slot's rendered submission tokens", async (t) => {
5352
+ const cwd = repository(t);
5353
+ writeFileSync(join(cwd, "app.ts"), "export const finalSlot = true;\n");
5354
+ const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
5355
+ const beforeCapture = bindEvidenceSubmissionCollection(targetStatusFixture({
5356
+ lineageId: "final-verification-slot",
5357
+ authorityState: "validating",
5358
+ baseTree: frozen.baseTree,
5359
+ currentCandidateTree: frozen.candidateTree,
5360
+ paths: frozen.paths,
5361
+ }));
5362
+ const expectedTokens = beforeCapture.nextTransition!.collect!.inputs[0]!.submissionDescriptor!.argumentTokens;
5363
+ const afterCapture = bindFinalVerificationTransition(targetStatusFixture({
5364
+ lineageId: "final-verification-slot",
5365
+ authorityState: "validating",
5366
+ baseTree: frozen.baseTree,
5367
+ currentCandidateTree: frozen.candidateTree,
5368
+ paths: frozen.paths,
5369
+ }), "passed");
5370
+ frozen.cleanup();
5371
+ const misboundCaptures: Array<Record<string, unknown>> = [];
5372
+ const submissions: Array<Record<string, unknown>> = [];
5373
+ const transitions: Array<readonly string[]> = [];
5374
+ let statuses = 0;
5375
+ const { controller } = runtime(fakeNative({
5376
+ targetStatus: async () => {
5377
+ statuses += 1;
5378
+ return statuses === 1 ? beforeCapture : afterCapture;
5379
+ },
5380
+ captureEvidence: async (request) => {
5381
+ misboundCaptures.push(request as unknown as Record<string, unknown>);
5382
+ throw new Error("misbound capture: the collect slot's rendered submission tokens were bypassed");
5383
+ },
5384
+ captureEvidenceSubmission: async (request: Record<string, unknown>) => {
5385
+ submissions.push(request);
5386
+ return capturedSlotEvidence(beforeCapture, "passed", "3");
5387
+ },
5388
+ finalizeTransition: async (request) => {
5389
+ transitions.push(request.argumentTokens);
5390
+ return { lineageId: "final-verification-slot", state: "approved", action: "terminal", storeRevision: "r2", receiptPath: "/opaque/receipt" };
5391
+ },
5392
+ } as unknown as Partial<NativeReviewCli>), undefined, undefined, undefined, new CandidateViewRegistry());
5393
+ const result = await controller.execute("final-verification-slot", {
5394
+ operation: "finalize",
5395
+ lineageId: "final-verification-slot",
5396
+ input: JSON.stringify({ final_evidence: "verification run: passed", final_verification_outcome: "passed" }),
5397
+ }, undefined, undefined, context(cwd));
5398
+ const details = result.details as Record<string, unknown>;
5399
+ assert.deepEqual(misboundCaptures, [], "capture-evidence must never rebind to the top-level live workspace identity");
5400
+ assert.equal(submissions.length, 1);
5401
+ assert.deepEqual(submissions[0]!.argumentTokens, expectedTokens);
5402
+ assert.equal(submissions[0]!.cwd, undefined, "the slot's --repository-context is authoritative; never pass --cwd alongside it");
5403
+ assert.deepEqual(transitions, [["--lineage=final-verification-slot", "--captured-evidence=true"]]);
5404
+ assert.equal((details.result as { state?: string } | undefined)?.state, "approved");
5405
+ });
5406
+
5407
+ // Field defect amplifier (fambig, 2026-08-16): an envelope-less mutating
5408
+ // failure is stamped mutationOutcome "unknown", and the reconciler KEPT
5409
+ // "unknown" even after fresh STATUS proved the authority revision identical to
5410
+ // the pre-operation revision — reporting a replay prohibition for an operation
5411
+ // that provably never mutated. A proven non-mutation must be reported as
5412
+ // mutation_outcome none with the proof named; a genuinely ambiguous result
5413
+ // (revision moved, or STATUS unavailable) stays fail-closed unknown.
5414
+ test("finalize reconciliation downgrades an envelope-less unknown to proven non-mutation when the authority revision is unchanged", async (t) => {
5415
+ const cwd = repository(t);
5416
+ let statuses = 0;
5417
+ const { controller } = runtime(fakeNative({
5418
+ targetStatus: async () => {
5419
+ statuses += 1;
5420
+ return targetStatusFixture({ lineageId: "unchanged-revision" });
5421
+ },
5422
+ finalize: async () => {
5423
+ throw Object.assign(new Error("stderr-only native failure without a typed envelope"), { mutationOutcome: "unknown", nextAction: "review.status" });
5424
+ },
5425
+ }));
5426
+ const details = (await controller.execute("proven-none", { operation: "finalize", lineageId: "unchanged-revision", input: "{}" }, undefined, undefined, context(cwd))).details as Record<string, unknown>;
5427
+ assert.equal(details.outcome, "native-mutation-status-reconciled");
5428
+ assert.equal(details.mutation_performed, false, "a reconciled unchanged revision proves the operation did not mutate");
5429
+ assert.equal(details.mutation_outcome, "none");
5430
+ assert.match(String(details.mutation_outcome_reason), /revision unchanged/i, "the downgrade must name its proof");
5431
+ assert.equal("replayability" in details, false, "a proven non-mutation must not claim replay prohibition");
5432
+ assert.equal(details.next_action, "finalize");
5433
+ assert.equal(statuses, 2);
5434
+ });
5435
+
5436
+ test("finalize reconciliation preserves fail-closed unknown when the reconciled authority revision moved", async (t) => {
5437
+ const cwd = repository(t);
5438
+ const moved = targetStatusFixture({ lineageId: "moved-revision" });
5439
+ moved.authority!.revision = `sha256:${"b".repeat(64)}`;
5440
+ let statuses = 0;
5441
+ const { controller } = runtime(fakeNative({
5442
+ targetStatus: async () => {
5443
+ statuses += 1;
5444
+ return statuses === 1 ? targetStatusFixture({ lineageId: "moved-revision" }) : moved;
5445
+ },
5446
+ finalize: async () => {
5447
+ throw Object.assign(new Error("stderr-only native failure without a typed envelope"), { mutationOutcome: "unknown", nextAction: "review.status" });
5448
+ },
5449
+ }));
5450
+ const details = (await controller.execute("kept-unknown", { operation: "finalize", lineageId: "moved-revision", input: "{}" }, undefined, undefined, context(cwd))).details as Record<string, unknown>;
5451
+ assert.equal(details.outcome, "native-mutation-status-reconciled");
5452
+ assert.equal(details.mutation_outcome, "unknown", "a moved revision is genuinely ambiguous and must stay fail-closed");
5453
+ assert.equal(details.replayability, "not_replayable");
5454
+ });
5455
+
5456
+ // Same misbinding class, remaining finalize-form slots (live smoke,
5457
+ // 2026-08-16, dev binary 2.4.0-main): the correction PLAN and TARGETED
5458
+ // VALIDATION collect slots render `finalize` submission descriptors whose
5459
+ // tokens carry --contract/--lineage/--expected-revision/--target/
5460
+ // --request-hash/--repository-context plus one {{value}} slot. The legacy
5461
+ // reconstructed `finalize --correction-lines/--validation` argv fails on the
5462
+ // live emitter with "reconcile compact predecessor effects: repository
5463
+ // context effect binding or payload does not match committed intent". Collect
5464
+ // satisfaction must execute the rendered tokens verbatim.
5465
+ function bindPlanSubmissionCollection(status: ReviewStatusV3, repositoryContext = EVIDENCE_SLOT_REPOSITORY_CONTEXT): ReviewStatusV3 {
5466
+ const lineageId = status.authority!.lineageId;
5467
+ const revision = status.authority!.revision;
5468
+ status.nextTransition = {
5469
+ kind: "collect",
5470
+ reasonCode: "correction_plan_required",
5471
+ collect: { inputs: [{
5472
+ name: "correction_lines",
5473
+ schema: "gentle-ai.review-correction-plan/v1",
5474
+ captureOperation: "external.plan_correction",
5475
+ arguments: [
5476
+ { name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
5477
+ { name: "expected-revision", value: revision, token: `--expected-revision=${revision}` },
5478
+ { name: "target", value: status.targetIdentity, token: `--target=${status.targetIdentity}` },
5479
+ ],
5480
+ submissionDescriptor: {
5481
+ operationToken: "finalize",
5482
+ argumentTokens: [
5483
+ "--contract=gentle-ai.review-integration/v2",
5484
+ `--lineage=${lineageId}`,
5485
+ `--expected-revision=${revision}`,
5486
+ `--target=${status.targetIdentity}`,
5487
+ `--request-hash=sha256:${"c".repeat(64)}`,
5488
+ `--repository-context=${repositoryContext}`,
5489
+ "--correction-lines={{value}}",
5490
+ ],
5491
+ value: { slot: "correction_lines", domain: "positive_correction_lines", minimum: 1, maximum: 9, substitutionLocation: 6 },
5492
+ },
5493
+ }] },
5494
+ };
5495
+ delete status.validationRequest;
5496
+ return status;
5497
+ }
5498
+
5499
+ test("correction plan forecast executes the collect slot's rendered finalize submission tokens verbatim", async (t) => {
5500
+ const cwd = repository(t);
5501
+ writeFileSync(join(cwd, "app.ts"), "export const plan = true;\n");
5502
+ const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
5503
+ const status = bindPlanSubmissionCollection(targetStatusFixture({
5504
+ lineageId: "plan-submission",
5505
+ authorityState: "correction_required",
5506
+ baseTree: frozen.baseTree,
5507
+ currentCandidateTree: frozen.candidateTree,
5508
+ paths: frozen.paths,
5509
+ }));
5510
+ const expectedTokens = status.nextTransition!.collect!.inputs[0]!.submissionDescriptor!.argumentTokens;
5511
+ frozen.cleanup();
5512
+ const legacyFinalizes: Array<Record<string, unknown>> = [];
5513
+ const submissions: Array<Record<string, unknown>> = [];
5514
+ const { controller } = runtime(fakeNative({
5515
+ targetStatus: async () => status,
5516
+ finalize: async (request) => {
5517
+ legacyFinalizes.push(request as unknown as Record<string, unknown>);
5518
+ throw new Error("misbound plan: the collect slot's rendered finalize submission tokens were bypassed");
5519
+ },
5520
+ finalizeSubmission: async (request: Record<string, unknown>) => {
5521
+ submissions.push(request);
5522
+ return { lineageId: "plan-submission", state: "correction_required", action: "continue the current review state", storeRevision: "r-plan" };
5523
+ },
5524
+ } as unknown as Partial<NativeReviewCli>), undefined, undefined, undefined, new CandidateViewRegistry());
5525
+ const planned = await controller.execute("plan-submission", {
5526
+ operation: "finalize",
5527
+ lineageId: "plan-submission",
5528
+ input: JSON.stringify({ correction_line_forecast: 2 }),
5529
+ }, undefined, undefined, context(cwd));
5530
+ const details = planned.details as Record<string, unknown>;
5531
+ assert.deepEqual(legacyFinalizes, [], "the legacy reconstructed --correction-lines argv must never run when the slot renders submission tokens");
5532
+ assert.equal(submissions.length, 1);
5533
+ assert.deepEqual(submissions[0]!.argumentTokens, expectedTokens, "the provider-rendered submission tokens must be passed verbatim, in provider order");
5534
+ assert.equal(submissions[0]!.valueSubstitutionLocation, 6);
5535
+ assert.equal(submissions[0]!.valueLiteral, "2");
5536
+ assert.equal((details.result as { state?: string } | undefined)?.state, "correction_required");
5537
+
5538
+ // An out-of-bounds forecast fails closed before any native launch.
5539
+ const outOfBounds = await controller.execute("plan-submission-oob", {
5540
+ operation: "finalize",
5541
+ lineageId: "plan-submission",
5542
+ input: JSON.stringify({ correction_line_forecast: 99 }),
5543
+ }, undefined, undefined, context(cwd));
5544
+ assert.equal((outOfBounds.details as { outcome?: string }).outcome, "native-operation-failed");
5545
+ assert.match(JSON.stringify(outOfBounds.details), /correction-forecast-out-of-bounds/);
5546
+ assert.equal(submissions.length, 1);
5547
+ assert.deepEqual(legacyFinalizes, []);
5548
+ execFileSync("chmod", ["-R", "u+w", cwd]);
5549
+ });
5550
+
5551
+ function bindTargetedValidationSubmission(status: ReviewStatusV3): ReviewStatusV3 {
5552
+ bindTargetedValidation(status);
5553
+ const input = status.nextTransition!.collect!.inputs[0]! as { submissionDescriptor?: unknown };
5554
+ const lineageId = status.authority!.lineageId;
5555
+ const revision = status.authority!.revision;
5556
+ input.submissionDescriptor = {
5557
+ operationToken: "finalize",
5558
+ argumentTokens: [
5559
+ "--contract=gentle-ai.review-integration/v2",
5560
+ `--lineage=${lineageId}`,
5561
+ `--expected-revision=${revision}`,
5562
+ `--target=${status.targetIdentity}`,
5563
+ `--request-hash=${status.validationRequest!.requestHash}`,
5564
+ `--repository-context=${EVIDENCE_SLOT_REPOSITORY_CONTEXT}`,
5565
+ "--validation={{value}}",
5566
+ ],
5567
+ value: { slot: "validation", domain: "artifact_path_or_stdin", substitutionLocation: 6 },
5568
+ };
5569
+ return status;
5570
+ }
5571
+
5572
+ test("targeted validation executes the collect slot's rendered finalize submission tokens verbatim", async (t) => {
5573
+ const cwd = repository(t);
5574
+ writeFileSync(join(cwd, "app.ts"), "export const validated = true;\n");
5575
+ const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
5576
+ const beforeCapture = bindEvidenceSubmissionCollection(targetStatusFixture({
5577
+ lineageId: "validation-submission",
5578
+ authorityState: "correction_required",
5579
+ baseTree: frozen.baseTree,
5580
+ currentCandidateTree: frozen.candidateTree,
5581
+ paths: frozen.paths,
5582
+ }));
5583
+ const afterCapture = bindTargetedValidationSubmission(targetStatusFixture({
5584
+ lineageId: "validation-submission",
5585
+ authorityState: "validating",
5586
+ baseTree: frozen.baseTree,
5587
+ currentCandidateTree: frozen.candidateTree,
5588
+ paths: frozen.paths,
5589
+ }));
5590
+ const expectedTokens = (afterCapture.nextTransition!.collect!.inputs[0]! as { submissionDescriptor: { argumentTokens: readonly string[] } }).submissionDescriptor.argumentTokens;
5591
+ frozen.cleanup();
5592
+ const legacyFinalizes: Array<Record<string, unknown>> = [];
5593
+ const submissions: Array<Record<string, unknown>> = [];
5594
+ let statuses = 0;
5595
+ const { controller } = runtime(fakeNative({
5596
+ targetStatus: async () => {
5597
+ statuses += 1;
5598
+ return statuses === 1 ? beforeCapture : afterCapture;
5599
+ },
5600
+ captureEvidenceSubmission: async () => capturedSlotEvidence(beforeCapture, "passed", "7"),
5601
+ finalize: async (request) => {
5602
+ legacyFinalizes.push(request as unknown as Record<string, unknown>);
5603
+ throw new Error("misbound validation: the collect slot's rendered finalize submission tokens were bypassed");
5604
+ },
5605
+ finalizeSubmission: async (request: Record<string, unknown>) => {
5606
+ submissions.push(request);
5607
+ return { lineageId: "validation-submission", state: "approved", action: "approved", storeRevision: "r-approved" };
5608
+ },
5609
+ } as unknown as Partial<NativeReviewCli>), undefined, undefined, undefined, new CandidateViewRegistry());
5610
+ const completed = await controller.execute("validation-submission", {
5611
+ operation: "finalize",
5612
+ lineageId: "validation-submission",
5613
+ input: JSON.stringify({
5614
+ final_evidence: "evidence: passed",
5615
+ final_verification_outcome: "passed",
5616
+ validation: {
5617
+ request_hash: "9".repeat(64), correction_ids: [],
5618
+ original_criteria: { passed: true, evidence: ["acceptance passes"] },
5619
+ correction_regression: { passed: true, evidence: ["regression passes"] },
5620
+ fix_caused_findings: [], follow_ups: [],
5621
+ },
5622
+ }),
5623
+ }, undefined, undefined, context(cwd));
5624
+ const details = completed.details as Record<string, unknown>;
5625
+ assert.deepEqual(legacyFinalizes, [], "the legacy reconstructed --validation argv must never run when the slot renders submission tokens");
5626
+ assert.equal(submissions.length, 1);
5627
+ assert.deepEqual(submissions[0]!.argumentTokens, expectedTokens, "the provider-rendered submission tokens must be passed verbatim, in provider order");
5628
+ assert.equal(submissions[0]!.valueSubstitutionLocation, 6);
5629
+ const staged = JSON.parse(String(submissions[0]!.valueDocument)) as Record<string, unknown>;
5630
+ assert.equal(staged.targeted_validation_request_hash, afterCapture.validationRequest!.requestHash);
5631
+ assert.equal(staged.correction_target_identity, afterCapture.validationRequest!.correctionTargetIdentity);
5632
+ assert.deepEqual(staged.original_criteria, { passed: true, evidence: ["acceptance passes"] });
5633
+ assert.equal((details.result as { state?: string } | undefined)?.state, "approved");
5634
+ });
5635
+
5636
+ // Live smoke root cause (2026-08-16, dev binary 2.4.0-main): status/v5 mints
5637
+ // the opaque --repository-context handle BOUND TO THE STATUS QUERY ROOT, and
5638
+ // every rendered payload embeds it. The finalize lane queries negotiated
5639
+ // STATUS from a frozen candidate-view root, so every rendered submission it
5640
+ // executed carried a context bound to the wrong root and failed the live
5641
+ // emitter's committed-intent reconciliation ("repository context effect
5642
+ // binding or payload does not match committed intent"). On v5 the lane must
5643
+ // rebind its negotiated STATUS to the workspace root before executing any
5644
+ // rendered payload; pinned pre-v5 emitters keep the frozen-view status.
5645
+ test("status/v5 finalize lane rebinds negotiated status to the workspace root before executing rendered payloads", async (t) => {
5646
+ const cwd = repository(t);
5647
+ writeFileSync(join(cwd, "app.ts"), "export const rebind = true;\n");
5648
+ const frozen = new CandidateViewRegistry().create({ contributorRoot: cwd });
5649
+ const viewContext = `rctx1_${"a".repeat(64)}`;
5650
+ const workspaceContext = `rctx1_${"b".repeat(64)}`;
5651
+ const makeBefore = (repositoryContext: string) => {
5652
+ const status = bindEvidenceSubmissionCollection(targetStatusFixture({
5653
+ lineageId: "v5-rebind",
5654
+ authorityState: "correction_required",
5655
+ baseTree: frozen.baseTree,
5656
+ currentCandidateTree: frozen.candidateTree,
5657
+ paths: frozen.paths,
5658
+ }), EVIDENCE_SLOT_FIX_TARGET, repositoryContext);
5659
+ (status.raw as Record<string, unknown>).schema = "gentle-ai.review-integration.status/v5";
5660
+ return status;
5661
+ };
5662
+ const viewBefore = makeBefore(viewContext);
5663
+ const workspaceBefore = makeBefore(workspaceContext);
5664
+ const workspaceAfter = bindTargetedValidationSubmission(targetStatusFixture({
5665
+ lineageId: "v5-rebind",
5666
+ authorityState: "validating",
5667
+ baseTree: frozen.baseTree,
5668
+ currentCandidateTree: frozen.candidateTree,
5669
+ paths: frozen.paths,
5670
+ }));
5671
+ (workspaceAfter.raw as Record<string, unknown>).schema = "gentle-ai.review-integration.status/v5";
5672
+ // Live post-evidence shape (2026-08-16): the validation request binds the
5673
+ // FROZEN authority target identity while the top-level target identity is
5674
+ // the live workspace snapshot (which now contains the fix).
5675
+ workspaceAfter.authorityTargetIdentity = workspaceAfter.validationRequest!.targetIdentity;
5676
+ workspaceAfter.targetIdentity = `sha256:${"d".repeat(64)}`;
5677
+ const expectedEvidenceTokens = workspaceBefore.nextTransition!.collect!.inputs[0]!.submissionDescriptor!.argumentTokens;
5678
+ const expectedValidationTokens = (workspaceAfter.nextTransition!.collect!.inputs[0]! as { submissionDescriptor: { argumentTokens: readonly string[] } }).submissionDescriptor.argumentTokens;
5679
+ frozen.cleanup();
5680
+ const statusRoots: string[] = [];
5681
+ const evidenceSubmissions: Array<Record<string, unknown>> = [];
5682
+ const finalizeSubmissions: Array<Record<string, unknown>> = [];
5683
+ let workspaceStatuses = 0;
5684
+ const { controller } = runtime(fakeNative({
5685
+ targetStatus: async (request) => {
5686
+ statusRoots.push(request.cwd);
5687
+ if (request.cwd !== cwd) return viewBefore;
5688
+ workspaceStatuses += 1;
5689
+ return workspaceStatuses === 1 ? workspaceBefore : workspaceAfter;
5690
+ },
5691
+ captureEvidenceSubmission: async (request: Record<string, unknown>) => {
5692
+ evidenceSubmissions.push(request);
5693
+ return capturedSlotEvidence(workspaceBefore, "passed", "8");
5694
+ },
5695
+ finalizeSubmission: async (request: Record<string, unknown>) => {
5696
+ finalizeSubmissions.push(request);
5697
+ return { lineageId: "v5-rebind", state: "approved", action: "approved", storeRevision: "r-approved" };
5698
+ },
5699
+ } as unknown as Partial<NativeReviewCli>), undefined, undefined, undefined, new CandidateViewRegistry());
5700
+ const completed = await controller.execute("v5-rebind", {
5701
+ operation: "finalize",
5702
+ lineageId: "v5-rebind",
5703
+ input: JSON.stringify({
5704
+ final_evidence: "evidence: passed",
5705
+ final_verification_outcome: "passed",
5706
+ validation: {
5707
+ request_hash: "9".repeat(64), correction_ids: [],
5708
+ original_criteria: { passed: true, evidence: ["acceptance passes"] },
5709
+ correction_regression: { passed: true, evidence: ["regression passes"] },
5710
+ fix_caused_findings: [], follow_ups: [],
5711
+ },
5712
+ }),
5713
+ }, undefined, undefined, context(cwd));
5714
+ const details = completed.details as Record<string, unknown>;
5715
+ assert.notEqual(statusRoots[0], cwd, "the first status query still freezes through the candidate view root");
5716
+ assert.equal(statusRoots[1], cwd, "a v5 status must be rebound to the workspace root before any rendered payload executes");
5717
+ assert.equal(evidenceSubmissions.length, 1);
5718
+ assert.deepEqual(evidenceSubmissions[0]!.argumentTokens, expectedEvidenceTokens, "the evidence submission must carry the workspace-root-minted tokens");
5719
+ assert.equal(finalizeSubmissions.length, 1);
5720
+ assert.deepEqual(finalizeSubmissions[0]!.argumentTokens, expectedValidationTokens, "the validation submission must carry the workspace-root-minted tokens");
5721
+ assert.equal(statusRoots.filter((root) => root === cwd).length, 2, "post-evidence status must also be workspace-bound");
5722
+ assert.equal((details.result as { state?: string } | undefined)?.state, "approved");
5723
+ });