gentle-pi 2.2.0 → 2.4.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 (160) hide show
  1. package/README.md +76 -79
  2. package/assets/agents/review-readability.md +0 -2
  3. package/assets/agents/review-reliability.md +0 -2
  4. package/assets/agents/review-resilience.md +0 -2
  5. package/assets/agents/review-risk.md +0 -2
  6. package/assets/agents/sdd-apply.md +5 -3
  7. package/assets/agents/sdd-explore.md +1 -0
  8. package/assets/agents/sdd-proposal.md +2 -0
  9. package/assets/agents/sdd-research.md +54 -0
  10. package/assets/agents/sdd-status.md +4 -4
  11. package/assets/agents/sdd-tasks.md +2 -3
  12. package/assets/agents/sdd-verify.md +21 -1
  13. package/assets/chains/sdd-full.chain.md +1 -1
  14. package/assets/chains/sdd-verify.chain.md +1 -1
  15. package/assets/orchestrator-delegation.md +56 -249
  16. package/assets/orchestrator-memory.md +2 -0
  17. package/assets/orchestrator.md +20 -40
  18. package/assets/sdd-orchestrator-workflow.md +30 -37
  19. package/assets/support/sdd-status-contract.md +7 -7
  20. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  21. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  22. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  23. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  24. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
  25. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
  26. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
  27. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
  28. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
  29. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
  30. package/docs/native-authority-architecture.md +9 -11
  31. package/docs/review-integration.md +27 -373
  32. package/extensions/ask-user-choice.ts +151 -0
  33. package/extensions/gentle-ai.ts +2349 -3785
  34. package/extensions/quiet-tools.ts +515 -32
  35. package/extensions/sdd-init.ts +4 -8
  36. package/lib/agent-home.ts +8 -0
  37. package/lib/gentle-ai-renderer.ts +70 -0
  38. package/lib/model-routing-authority.ts +133 -0
  39. package/lib/native-review-cli.ts +387 -940
  40. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  41. package/lib/provider-contract-bundle.ts +88 -6
  42. package/lib/review-candidate-view.ts +341 -132
  43. package/lib/review-host-relay.ts +324 -96
  44. package/lib/review-integration-v2.ts +868 -307
  45. package/lib/review-last-event-controller.ts +35 -0
  46. package/lib/sdd-preflight.ts +179 -68
  47. package/lib/sdd-status.ts +66 -111
  48. package/lib/terminal-theme.ts +1 -1
  49. package/package.json +83 -82
  50. package/runtime/gentle-ai-binary.mjs +1 -1
  51. package/runtime/native-review-cli.mjs +337 -890
  52. package/runtime/review-integration-v2.mjs +816 -255
  53. package/runtime/review-relay-contract.mjs +1 -1
  54. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
  55. package/scripts/gentle-ai-installer.mjs +75 -22
  56. package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
  57. package/scripts/test-packed-runner.mjs +4 -7
  58. package/scripts/verify-package-files.mjs +23 -23
  59. package/skills/_shared/review-ledger-contract.md +14 -14
  60. package/skills/chained-pr/SKILL.md +3 -0
  61. package/skills/cognitive-doc-design/SKILL.md +1 -1
  62. package/skills/comment-writer/SKILL.md +1 -1
  63. package/skills/gentle-ai/SKILL.md +7 -74
  64. package/skills/issue-creation/SKILL.md +53 -93
  65. package/skills/judgment-day/SKILL.md +5 -7
  66. package/skills/rdd-defect-workflow/SKILL.md +3 -3
  67. package/skills/release/SKILL.md +3 -3
  68. package/skills/skill-registry/SKILL.md +1 -1
  69. package/skills/work-unit-commits/SKILL.md +3 -1
  70. package/tests/artifact-language.test.ts +24 -11
  71. package/tests/ask-user-choice.test.ts +264 -0
  72. package/tests/codegraph-tools.test.ts +3 -3
  73. package/tests/crosslane/cross-lane.mjs +15 -1168
  74. package/tests/delegated-key-learnings-contract.test.ts +8 -6
  75. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  76. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  77. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  78. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  79. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  80. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  81. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  82. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  83. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  84. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  85. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  86. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  87. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  88. package/tests/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
  89. package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
  90. package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
  91. package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
  92. package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
  93. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
  94. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
  95. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -0
  96. package/tests/gentle-ai-binary.test.ts +83 -13
  97. package/tests/gentle-ai-dev-binary.test.ts +51 -228
  98. package/tests/gentle-ai-installer.test.ts +137 -47
  99. package/tests/gentle-ai.test.ts +821 -131
  100. package/tests/gentle-theme.test.ts +133 -0
  101. package/tests/issue-creation-skill.test.ts +103 -0
  102. package/tests/maintainer/provider-relay.maintest.ts +340 -4
  103. package/tests/model-routing-authority.test.ts +257 -0
  104. package/tests/native-review-capability-contract.test.ts +45 -1
  105. package/tests/native-review-cli.test.ts +586 -962
  106. package/tests/native-review-consent.test.ts +150 -66
  107. package/tests/native-review-parity-runtime.test.ts +80 -359
  108. package/tests/native-review-parity.test.ts +674 -925
  109. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  110. package/tests/orchestrator-budget.test.ts +132 -88
  111. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  112. package/tests/package-manifest.test.ts +152 -36
  113. package/tests/provider-contract-bundle.test.ts +76 -0
  114. package/tests/provider-contract-mirror.test.ts +19 -0
  115. package/tests/provider-defect-handoff.test.ts +15 -118
  116. package/tests/quiet-tool-rendering.test.ts +1055 -28
  117. package/tests/review-agent-end-preflight.test.ts +408 -0
  118. package/tests/review-authority-recovery-docs.test.ts +1 -2
  119. package/tests/review-candidate-view.test.ts +409 -3
  120. package/tests/review-compact-contract.test.ts +4 -30
  121. package/tests/review-contract-prompt.test.ts +139 -0
  122. package/tests/review-controller-lock-status.test.ts +2 -2
  123. package/tests/review-controller-native-recovery.test.ts +363 -1450
  124. package/tests/review-controller-native-routing.test.ts +1251 -5642
  125. package/tests/review-controller-retired-ops.test.ts +1 -1
  126. package/tests/review-controller-workspace-root.test.ts +222 -71
  127. package/tests/review-controller.test.ts +26 -816
  128. package/tests/review-corrected-finalize-binding.test.ts +112 -153
  129. package/tests/review-dispatch-hydration-gap.test.ts +1 -53
  130. package/tests/review-gate.test.ts +0 -45
  131. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  132. package/tests/review-host-relay-routing.test.ts +328 -91
  133. package/tests/review-host-relay.test.ts +436 -14
  134. package/tests/review-integration-v2-forward.test.ts +623 -553
  135. package/tests/review-integration-v2.test.ts +239 -165
  136. package/tests/review-last-event-closure.test.ts +408 -0
  137. package/tests/review-ledger-contract.test.ts +97 -35
  138. package/tests/review-recovered-lineage-routing.test.ts +0 -47
  139. package/tests/review-relay-transport-agent.test.ts +119 -46
  140. package/tests/review-snapshot.test.ts +3 -2
  141. package/tests/runtime-harness.mjs +362 -196
  142. package/tests/sdd-agent-tools.test.ts +36 -0
  143. package/tests/sdd-preflight.test.ts +81 -15
  144. package/tests/sdd-status.test.ts +109 -110
  145. package/tests/skill-collision-prefixes.test.ts +5 -8
  146. package/tests/writer-edit-surface-scope.test.ts +366 -0
  147. package/themes/Gentleman-Cute.json +94 -0
  148. package/themes/Gentleman-Sexy.json +92 -0
  149. package/lib/git-commit-transaction.ts +0 -861
  150. package/runtime/git-commit-transaction.mjs +0 -862
  151. package/scripts/run-git-commit-transaction.mjs +0 -35
  152. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  153. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  154. package/tests/git-commit-transaction.test.ts +0 -530
  155. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
  156. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
  157. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
  158. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
  159. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
  160. /package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/schemas/targeted-validator.schema.json +0 -0
@@ -1,175 +1,134 @@
1
1
  import assert from "node:assert/strict";
2
- import { execFileSync } from "node:child_process";
3
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
- import { tmpdir } from "node:os";
2
+ import { readFileSync } from "node:fs";
5
3
  import { join } from "node:path";
6
4
  import test from "node:test";
7
5
  import { __testing } from "../extensions/gentle-ai.ts";
8
6
  import type { NativeReviewCli } from "../lib/native-review-cli.ts";
9
- import { CandidateViewRegistry } from "../lib/review-candidate-view.ts";
10
- import type { ReviewStatusV3 } from "../lib/review-integration-v2.ts";
7
+ import { assertReviewLastEventClosureBinding, decodeReviewLastEventClosureV1, type ReviewStatusV3 } from "../lib/review-integration-v2.ts";
11
8
 
12
- // Field defect (Engram #12547, lineage review-43762fca1bba0bf3): a MEDIUM
13
- // lineage detected a reliability defect, corrected it inside budget, captured
14
- // evidence and the native targeted validator. Negotiated STATUS then returned
15
- // `captured_provider_targeted_validation_ready` with an execute
16
- // `review.finalize` transition, and running that exact transition failed twice
17
- // with `candidate-target-projection-drift`, mutation none, so no receipt was
18
- // ever minted.
19
- //
20
- // Measured on a faithful live reproduction (medium candidate, real binary,
21
- // START through the controller so the registry holds the START-time view):
22
- //
23
- // START candidate tree: 4c24487f850050af0a0944d37d4dbb15287ab3da
24
- // corrected candidate tree: 777f48fdc4e6ed9abd325d6b4c49e6d06036c906
25
- // FINALIZE (no documents) -> candidate-target-projection-drift, mutation none
26
- // registry branch taken: ["resolveForFinalize"]
27
- //
28
- // Root cause: the corrected tree is tolerated only while
29
- // `correctionCompletion` (validation AND final_evidence) or `validationAttempt`
30
- // (final_evidence without a forecast) hold. A FINALIZE that merely follows the
31
- // provider's own execute transition carries no documents, so both are false,
32
- // the START-time view is resolved, and it is compared against the corrected
33
- // candidate the provider itself authorized.
9
+ const CAPTURED_FIXTURES = join(process.cwd(), "tests", "fixtures", "devbinary");
34
10
 
35
- const SHA = `sha256:${"1".repeat(64)}`;
36
- const DEFECT = "export function lastComponent(input) {\n const parts = input.split(\"/\");\n return parts[parts.length - 2];\n}\n";
37
- const CORRECTED = "export function lastComponent(input) {\n const parts = input.split(\"/\");\n return parts[parts.length - 1];\n}\n";
38
-
39
- function git(cwd: string, ...arguments_: string[]): string {
40
- return execFileSync("git", arguments_, { cwd, encoding: "utf8" }).trim();
41
- }
42
-
43
- function repository(t: test.TestContext): string {
44
- const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-corrected-finalize-"));
45
- t.after(() => rmSync(cwd, { recursive: true, force: true }));
46
- git(cwd, "init", "-b", "main");
47
- git(cwd, "config", "user.name", "Corrected Test");
48
- git(cwd, "config", "user.email", "corrected@example.invalid");
49
- mkdirSync(join(cwd, "src"), { recursive: true });
50
- writeFileSync(join(cwd, "src", "parse.js"), "export function parsePath(input) {\n return input.split(\"/\");\n}\n");
51
- git(cwd, "add", "-A");
52
- git(cwd, "commit", "-m", "base");
53
- writeFileSync(join(cwd, "src", "parse.js"), DEFECT);
54
- return cwd;
11
+ function captured(name: string): Record<string, unknown> {
12
+ return JSON.parse(readFileSync(join(CAPTURED_FIXTURES, name), "utf8")) as Record<string, unknown>;
55
13
  }
56
14
 
57
- /** The live frozen identity of the current workspace candidate. */
58
- function liveIdentity(cwd: string): { baseTree: string; candidateTree: string; paths: readonly string[] } {
59
- const probe = new CandidateViewRegistry();
60
- const view = probe.create({ contributorRoot: cwd });
61
- try {
62
- return { baseTree: view.baseTree, candidateTree: view.candidateTree, paths: view.paths };
63
- } finally {
64
- probe.cleanup(view.token);
65
- }
66
- }
15
+ test("captured correction-required result preserves the provider status continuation", () => {
16
+ const fixture = captured("last-event-capture-result-correction-required.captured.json");
17
+ const closure = decodeReviewLastEventClosureV1(fixture);
18
+ assert.equal(closure.operation, "review/capture-result");
19
+ assert.equal(closure.state, "correction_required");
20
+ assert.match(closure.storeRevision, /^sha256:[a-f0-9]{64}$/);
21
+ assert.equal(closure.statusContinuation?.operation, "review.status");
22
+ assert.deepEqual(
23
+ closure.statusContinuation?.arguments.map((argument) => argument.token),
24
+ [
25
+ "--cwd=/tmp/repository",
26
+ "--contract=gentle-ai.review-integration/v2",
27
+ "--next-transition=true",
28
+ "--lineage=review-afeed0d5a9aa24a1",
29
+ "--agent=pi",
30
+ ],
31
+ );
32
+ assert.deepEqual(closure.statusContinuation?.raw, fixture.status_continuation);
33
+ });
67
34
 
68
- function correctedStatus(lineageId: string, identity: { baseTree: string; candidateTree: string; paths: readonly string[] }): ReviewStatusV3 {
69
- return {
70
- contract: "gentle-ai.review-integration/v2",
71
- applicability: "current_target",
72
- authority: { version: "compact-v2", lineageId, state: "correction_required", generation: 1, revision: SHA },
73
- receipt: { status: "expected_missing" },
74
- action: "finalize",
75
- replayability: "not_replayable",
76
- frozen: { tier: "medium", originalChangedLines: 4, correctionBudget: 2 },
77
- targetIdentity: SHA,
78
- projection: {
79
- schema: "gentle-ai.review-integration.projection/v1",
80
- kind: "current-changes",
81
- projection: "workspace",
82
- baseTree: identity.baseTree,
83
- initialReviewTree: identity.candidateTree,
84
- currentCandidateTree: identity.candidateTree,
85
- pathsDigest: SHA,
86
- paths: [...identity.paths],
87
- intendedUntracked: [],
88
- intendedUntrackedProof: SHA,
89
- initialSnapshotIdentity: SHA,
90
- currentSnapshotIdentity: SHA,
35
+ test("correction status continuation stays bound to its enclosing closure and provider target", () => {
36
+ const mismatch = `sha256:${"b".repeat(64)}`;
37
+ const cases = [
38
+ {
39
+ name: "missing binding lineage",
40
+ mutate: (binding: Record<string, unknown>) => { delete binding.lineage_id; },
41
+ error: /lineage does not match its enclosing closure/,
91
42
  },
92
- candidates: [],
93
- // The provider's own answer after it admitted the captured validator.
94
- nextTransition: {
95
- kind: "execute",
96
- reasonCode: "captured_provider_targeted_validation_ready",
97
- execute: {
98
- operation: "review.finalize",
99
- arguments: [
100
- { name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
101
- { name: "captured-evidence", value: "true", token: "--captured-evidence=true" },
102
- ],
103
- binding: { lineageId },
104
- },
43
+ {
44
+ name: "different binding lineage",
45
+ mutate: (binding: Record<string, unknown>) => { binding.lineage_id = "review-different"; },
46
+ error: /lineage does not match its enclosing closure/,
105
47
  },
106
- raw: { schema: "gentle-ai.review-integration.status/v5", action: "finalize", lineage_id: lineageId },
107
- } as unknown as ReviewStatusV3;
108
- }
109
-
110
- function approvingNative(status: ReviewStatusV3): { native: NativeReviewCli; transitions: number } {
111
- const state = { transitions: 0 };
112
- const native = {
113
- targetStatus: async () => status,
114
- finalizeTransition: async () => {
115
- state.transitions += 1;
116
- return { lineageId: status.authority!.lineageId, state: "approved", action: "approved", storeRevision: "r2" };
48
+ {
49
+ name: "missing binding revision",
50
+ mutate: (binding: Record<string, unknown>) => { delete binding.revision; },
51
+ error: /revision does not match its enclosing closure/,
117
52
  },
118
- finalize: async () => { throw new Error("the provider transition must be executed, not raw finalize"); },
119
- } as unknown as NativeReviewCli;
120
- return { native, get transitions() { return state.transitions; } } as never;
121
- }
122
-
123
- /** Binds the START-time immutable reviewer view, as a live session does. */
124
- function sessionWithStartBinding(cwd: string, lineageId: string): { registry: CandidateViewRegistry; startTree: string } {
125
- const registry = new CandidateViewRegistry();
126
- const view = registry.createOrReuse({ contributorRoot: cwd });
127
- registry.bindCurrent({ token: view.token, lineageId, selectedLenses: ["review-reliability"] });
128
- return { registry, startTree: view.candidateTree };
129
- }
130
-
131
- test("FINALIZE follows the provider transition after a correction instead of drifting on the START view", async (t) => {
132
- const cwd = repository(t);
133
- const lineageId = "review-corrected-lineage";
134
- const { registry, startTree } = sessionWithStartBinding(cwd, lineageId);
135
- t.after(() => { try { registry.cleanup(registry.resolveForFinalize(lineageId).token); } catch { /* already cleaned */ } });
136
-
137
- // The bounded correction lands: the candidate identity legitimately moves.
138
- writeFileSync(join(cwd, "src", "parse.js"), CORRECTED);
139
- const corrected = liveIdentity(cwd);
140
- assert.notEqual(corrected.candidateTree, startTree, "the correction must move the candidate tree");
53
+ {
54
+ name: "different binding revision",
55
+ mutate: (binding: Record<string, unknown>) => { binding.revision = mismatch; },
56
+ error: /revision does not match its enclosing closure/,
57
+ },
58
+ ];
59
+ for (const scenario of cases) {
60
+ const fixture = captured("last-event-capture-result-correction-required.captured.json");
61
+ const continuation = fixture.status_continuation as Record<string, unknown>;
62
+ scenario.mutate(continuation.binding as Record<string, unknown>);
63
+ assert.throws(() => decodeReviewLastEventClosureV1(fixture), scenario.error, scenario.name);
64
+ }
141
65
 
142
- const harness = approvingNative(correctedStatus(lineageId, corrected));
143
- const result = await __testing.executeReviewControllerOperation(
144
- // Exactly the reporter's call: follow the provider transition, no documents.
145
- { operation: "finalize", lineageId, input: JSON.stringify({}) },
146
- cwd, new Map(), harness.native, undefined, undefined, undefined, registry,
147
- ) as Record<string, unknown>;
66
+ const argumentMismatch = captured("last-event-capture-result-correction-required.captured.json");
67
+ const continuation = argumentMismatch.status_continuation as Record<string, unknown>;
68
+ const argumentsList = continuation.arguments as Array<Record<string, unknown>>;
69
+ const lineageArgument = argumentsList.find((argument) => argument.name === "lineage")!;
70
+ lineageArgument.value = "review-different";
71
+ lineageArgument.token = "--lineage=review-different";
72
+ assert.throws(() => decodeReviewLastEventClosureV1(argumentMismatch), /lineage argument does not match its enclosing closure/);
148
73
 
149
- assert.notEqual(
150
- (result.diagnostics as { code?: string } | undefined)?.code,
151
- "candidate-target-projection-drift",
152
- "a corrected candidate the provider authorized must not read as reviewer-view drift",
74
+ const closure = decodeReviewLastEventClosureV1(captured("last-event-capture-result-correction-required.captured.json"));
75
+ assert.throws(
76
+ () => assertReviewLastEventClosureBinding(closure, { lineageId: closure.lineageId, targetIdentity: mismatch }),
77
+ /status continuation target does not match its provider binding/,
153
78
  );
154
- assert.equal(harness.transitions, 1, "the provider's own finalize transition must run");
155
- assert.equal((result.result as { state?: string } | undefined)?.state, "approved");
156
79
  });
157
80
 
158
- test("FINALIZE still fails closed when the live candidate does not match the provider projection", async (t) => {
159
- const cwd = repository(t);
160
- const lineageId = "review-drifted-lineage";
161
- const { registry } = sessionWithStartBinding(cwd, lineageId);
162
- t.after(() => { try { registry.cleanup(registry.resolveForFinalize(lineageId).token); } catch { /* already cleaned */ } });
81
+ test("correction-required result and refuter closures require one status continuation", () => {
82
+ for (const name of [
83
+ "last-event-capture-result-correction-required.captured.json",
84
+ "last-event-capture-refuter-correction-required.captured.json",
85
+ ]) {
86
+ const fixture = captured(name);
87
+ delete fixture.status_continuation;
88
+ assert.throws(() => decodeReviewLastEventClosureV1(fixture), /requires status_continuation/, name);
89
+ }
90
+ });
163
91
 
164
- // The provider describes a candidate this workspace cannot reproduce.
165
- const identity = liveIdentity(cwd);
166
- const harness = approvingNative(correctedStatus(lineageId, { ...identity, candidateTree: identity.baseTree }));
167
- const result = await __testing.executeReviewControllerOperation(
168
- { operation: "finalize", lineageId, input: JSON.stringify({}) },
169
- cwd, new Map(), harness.native, undefined, undefined, undefined, registry,
170
- ) as Record<string, unknown>;
92
+ test("closures outside result or refuter correction-required forbid a status continuation", () => {
93
+ const approved = captured("last-event-capture-result-approved.captured.json");
94
+ approved.status_continuation = captured("last-event-capture-result-correction-required.captured.json").status_continuation;
95
+ assert.throws(() => decodeReviewLastEventClosureV1(approved), /status_continuation is only valid/, "approved result closure");
96
+ });
171
97
 
172
- assert.equal(result.status, "blocked", "an unverifiable candidate must never mint a receipt");
98
+ test("current capture fails closed when fresh STATUS no longer offers the corrected candidate binding", async () => {
99
+ const lineageId = "corrected-candidate";
100
+ const sha = `sha256:${"a".repeat(64)}`;
101
+ const binding = {
102
+ name: "provider_targeted_validator",
103
+ schema: "https://gentle-ai.dev/schema/review/targeted-validator/v1",
104
+ captureOperation: "review.capture-validation",
105
+ arguments: [
106
+ { name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
107
+ { name: "target", value: sha, token: `--target=${sha}` },
108
+ { name: "agent", value: "pi", token: "--agent=pi" },
109
+ { name: "execute", value: "true", token: "--execute=true" },
110
+ ],
111
+ };
112
+ let captureCalls = 0;
113
+ const native = {
114
+ targetStatus: async () => ({
115
+ contract: "gentle-ai.review-integration/v2",
116
+ applicability: "current_target",
117
+ authority: { version: "compact-v2", lineageId, state: "correction_required", generation: 1, revision: sha },
118
+ receipt: { status: "expected_missing" },
119
+ action: "stop",
120
+ replayability: "not_replayable",
121
+ targetIdentity: sha,
122
+ projection: { schema: "gentle-ai.review-candidate-projection/v1", kind: "current-changes", projection: "workspace", baseTree: "b".repeat(40), initialReviewTree: "b".repeat(40), currentCandidateTree: "c".repeat(40), pathsDigest: sha, paths: ["corrected.ts"], intendedUntracked: [], intendedUntrackedProof: sha, initialSnapshotIdentity: sha, currentSnapshotIdentity: sha },
123
+ repair: { schema: "gentle-ai.review-authority-repair-assessment/v1", status: "unsupported", counts: { lineages: 0, compactLineages: 0, legacyLineages: 0, events: 0, bytes: 0, eligibleCandidates: 0, unsupportedLineages: 0, conflicts: 0 }, supportedOperations: [], authorizationSchema: "gentle-ai.review-repair-authorization/v1" },
124
+ candidates: [],
125
+ nextTransition: { kind: "stop", reasonCode: "corrected_candidate_unavailable" },
126
+ raw: { schema: "gentle-ai.review-integration.status/v5" },
127
+ } as unknown as ReviewStatusV3),
128
+ captureProviderRole: async () => { captureCalls += 1; throw new Error("must not capture"); },
129
+ } as unknown as NativeReviewCli;
130
+ const result = await __testing.executeReviewCaptureOperation({ lineageId, collectBinding: JSON.stringify(binding) }, process.cwd(), native);
131
+ assert.equal(result.outcome, "capture-binding-rejected");
173
132
  assert.equal(result.mutation_outcome, "none");
174
- assert.equal(harness.transitions, 0, "no provider transition may run for an unverifiable candidate");
133
+ assert.equal(captureCalls, 0);
175
134
  });
@@ -124,62 +124,10 @@ function statusNative(status: ReviewStatusV3): NativeReviewCli {
124
124
 
125
125
  async function runController(parameters: Record<string, unknown>, cwd: string, native: NativeReviewCli, candidateViews: CandidateViewRegistry): Promise<Record<string, unknown>> {
126
126
  return await __testing.executeReviewControllerOperation(
127
- parameters, cwd, new Map(), native, undefined, undefined, undefined, candidateViews,
127
+ parameters, cwd, native, undefined, candidateViews,
128
128
  ) as Record<string, unknown>;
129
129
  }
130
130
 
131
- test("FINALIZE hydrates the dispatch binding for an externally recovered successor in a dirty linked worktree", async (t) => {
132
- const cwd = linkedDirtyWorktree(t);
133
- const lineageId = "recovered-dirty-successor";
134
- const registry = new CandidateViewRegistry();
135
- const native = statusNative(successorStatus(lineageId, liveProjection(cwd)));
136
-
137
- // The maintainer's flow: FINALIZE first (correctly blocked since #340),
138
- // then a reviewer dispatch. FINALIZE never went through the STATUS
139
- // controller operation, so nothing had hydrated the registry.
140
- const finalize = await runController({ operation: "finalize", lineageId, input: JSON.stringify({}) }, cwd, native, registry);
141
- assert.equal(finalize.outcome, "reviewer-results-required");
142
- assert.equal(registry.hasCurrentBinding(), true, "FINALIZE must hydrate the dispatch binding from the status it decoded");
143
- assert.deepEqual(finalize.dispatch_binding, { hydrated: true, lineage_id: lineageId, lenses: ["review-reliability"] });
144
-
145
- try {
146
- const dispatch: Record<string, unknown> = { agent: "review-reliability", task: "review the recovered successor", mode: "task" };
147
- assert.doesNotThrow(() => injectReviewCandidateView(dispatch, registry));
148
- assert.match(String(dispatch.task), new RegExp(lineageId));
149
- } finally {
150
- registry.cleanup(registry.resolveCurrentForLens("review-reliability").token);
151
- }
152
- });
153
-
154
- test("a swallowed hydration failure becomes an observable typed dispatch refusal", async (t) => {
155
- const cwd = linkedDirtyWorktree(t);
156
- const lineageId = "drifted-successor";
157
- const registry = new CandidateViewRegistry();
158
- // Realistic drift: the frozen candidate names the pristine HEAD tree while
159
- // the live worktree carries the uncommitted modification. Hydration
160
- // legitimately fails, and that failure must stop being invisible.
161
- const live = liveProjection(cwd);
162
- const drifted = { ...live, currentCandidateTree: live.baseTree };
163
- const finalize = await runController({ operation: "finalize", lineageId, input: JSON.stringify({}) }, cwd, statusNative(successorStatus(lineageId, drifted)), registry);
164
-
165
- assert.equal(finalize.outcome, "reviewer-results-required");
166
- assert.equal(registry.hasCurrentBinding(), false);
167
- const binding = finalize.dispatch_binding as { hydrated: boolean; lineage_id: string; reason: string; message: string };
168
- assert.equal(binding.hydrated, false, "a failed hydration must be reported, never swallowed");
169
- assert.equal(binding.lineage_id, lineageId);
170
- assert.ok(typeof binding.reason === "string" && binding.reason.length > 0, "the failure carries a typed reason");
171
- assert.match(binding.message, /projection|live candidate/i);
172
-
173
- // The dispatch refusal must now admit that hydration was attempted.
174
- assert.throws(
175
- () => injectReviewCandidateView({ agent: "review-reliability", task: "review", mode: "task" }, registry),
176
- (error: unknown) => error instanceof CandidateViewError
177
- && error.reason === "current-binding-hydration-failed"
178
- && /hydrat/i.test(error.message)
179
- && error.message.includes(lineageId),
180
- );
181
- });
182
-
183
131
  test("STATUS keeps hydrating and stays read-only when hydration fails", async (t) => {
184
132
  const cwd = linkedDirtyWorktree(t);
185
133
  const lineageId = "status-drifted-successor";
@@ -4,7 +4,6 @@ import { chmodSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:f
4
4
  import { tmpdir } from "node:os";
5
5
  import { join } from "node:path";
6
6
  import test from "node:test";
7
- import { __testing } from "../extensions/gentle-ai.ts";
8
7
  import {
9
8
  EXTERNAL_RELEASE_EVIDENCE,
10
9
  GATE_RESULT,
@@ -175,14 +174,6 @@ function temporaryAuthority(t: test.TestContext): GateRepository & {
175
174
  return { ...repository, store, receipt: receiptFor(state), authoritativeReceipt: store.createAuthoritativeReceipt("approved-lineage") };
176
175
  }
177
176
 
178
- test("lifecycle command classification identifies gates but never runs review routing", () => {
179
- assert.equal(__testing.classifyReviewEvent("git commit -m fix"), "pre-commit");
180
- assert.equal(__testing.classifyReviewEvent("git -C /repo push origin main"), "pre-push");
181
- assert.equal(__testing.classifyReviewEvent("gh pr create --draft"), "pre-pr");
182
- assert.equal(__testing.classifyReviewEvent("gh release create v1.2.3"), "pre-release");
183
- assert.equal(__testing.classifyReviewEvent("git status"), null);
184
- });
185
-
186
177
  test("unbranded receipts are rejected before lifecycle gate evaluation", (t) => {
187
178
  const { repository, finalTree, store, receipt } = temporaryAuthority(t);
188
179
  execFileSync("git", ["read-tree", finalTree], { cwd: repository });
@@ -589,42 +580,6 @@ test("scope child claim and parent gate journal publish atomically across faults
589
580
  assert.equal(store.read(receipt.body.lineage_id).child_claims?.length, 1);
590
581
  });
591
582
 
592
- test("receipt gate cannot bypass independent dangerous-command safety", async () => {
593
- let safetyCalls = 0;
594
- let gateCalls = 0;
595
- const safetyBlock = { block: true, reason: "dangerous command denied" };
596
- const result = await __testing.enforceReviewGateAndCommandSafety(
597
- "git push origin main",
598
- () => {
599
- gateCalls += 1;
600
- return undefined;
601
- },
602
- async () => {
603
- safetyCalls += 1;
604
- return safetyBlock;
605
- },
606
- );
607
- assert.deepEqual(result, safetyBlock);
608
- assert.equal(safetyCalls, 1);
609
- assert.equal(gateCalls, 0);
610
-
611
- const gateBlock = { block: true, reason: "exact receipt required" };
612
- const blocked = await __testing.enforceReviewGateAndCommandSafety(
613
- "git push origin main",
614
- () => {
615
- gateCalls += 1;
616
- return gateBlock;
617
- },
618
- async () => {
619
- safetyCalls += 1;
620
- return undefined;
621
- },
622
- );
623
- assert.deepEqual(blocked, gateBlock);
624
- assert.equal(safetyCalls, 2);
625
- assert.equal(gateCalls, 1);
626
- });
627
-
628
583
  function releaseTarget(repository: GateRepository): GateTargetV1 {
629
584
  return {
630
585
  kind: GATE_TARGET_KIND.RELEASE,