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
@@ -0,0 +1,360 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import { mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ import test from "node:test";
8
+
9
+ // gentle-pi#311 P6 — restart parity for the provider-relay capture lane.
10
+ //
11
+ // P1-P5 proved same-process STATUS re-query after a relay transport failure
12
+ // (tests/review-host-relay-routing.test.ts): the capture tool re-queries STATUS
13
+ // in-process and directs the operator to relaunch only if the exact same bound
14
+ // slot is reoffered. P6 closes the remaining gap: the binding must survive a
15
+ // TRUE controller-process restart, and the retry discipline must be provable,
16
+ // not merely asserted.
17
+ //
18
+ // Each scenario runs the controller in its own child process with a fresh
19
+ // module load of extensions/gentle-ai.ts. Module-level state and every closure
20
+ // restart from scratch. The truthful restart protocol uses the public surface:
21
+ // 1. Process A: INSPECT exposes one opaque collectBinding, which is copied
22
+ // unchanged to exactly one gentle_review_capture invocation. The relay
23
+ // transport fails without capture or source/authority mutation.
24
+ // 2. Process B (fresh): INSPECT queries fresh provider STATUS and exposes the
25
+ // pending opaque collectBinding; it invokes neither capture nor the relay.
26
+ // 3. The parent compares Process B's provider-returned binding to Process A's
27
+ // observed binding without parsing or rebuilding it.
28
+ // 4. Only when equal, fresh Process C copies its newly reoffered binding to
29
+ // one capture invocation.
30
+ // 5. Drifted and missing reoffers expose no eligible retry; the parent does
31
+ // not spawn a capture process.
32
+ //
33
+ // The provider (native targetStatus) is stubbed from a JSON file the parent
34
+ // controls; the provider's own durability is proven elsewhere
35
+ // (native-review-parity-runtime.test.ts drives the real binary). What is under
36
+ // test is the controller's restart safety.
37
+
38
+ const SHA = `sha256:${"1".repeat(64)}`;
39
+ const TREE = "2".repeat(40);
40
+ const LINEAGE = "relay-lineage";
41
+ const LENS = "review-reliability";
42
+ const ORDER = 0;
43
+
44
+ function bindingArguments(lineageId, lens, order) {
45
+ return [
46
+ { name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
47
+ { name: "expected-revision", value: SHA, token: `--expected-revision=${SHA}` },
48
+ { name: "target", value: SHA, token: `--target=${SHA}` },
49
+ { name: "repository-context", value: `rctx1_${"e".repeat(64)}`, token: `--repository-context=rctx1_${"e".repeat(64)}` },
50
+ { name: "lens", value: lens, token: `--lens=${lens}` },
51
+ { name: "order", value: String(order), token: `--order=${order}` },
52
+ { name: "subject-hash", value: `sha256:${String(order).repeat(64)}`, token: `--subject-hash=sha256:${String(order).repeat(64)}` },
53
+ ];
54
+ }
55
+
56
+ function providerSubmission(lineageId, lens, order) {
57
+ const bindingTokens = bindingArguments(lineageId, lens, order).map((a) => a.token);
58
+ return {
59
+ operationToken: "capture-result",
60
+ argumentTokens: [...bindingTokens, "--input={{value}}"],
61
+ values: [{ slot: "reviewer_result", domain: "artifact_path_or_stdin", substitutionLocation: bindingTokens.length }],
62
+ };
63
+ }
64
+
65
+ function relayCollectInput(lineageId, lens, order) {
66
+ return {
67
+ name: "reviewer_result",
68
+ schema: "https://gentle-ai.dev/schema/review/reviewer/v1",
69
+ captureOperation: "review.capture-result",
70
+ arguments: [
71
+ ...bindingArguments(lineageId, lens, order),
72
+ { name: "agent", value: "pi", token: "--agent=pi" },
73
+ { name: "materialize", value: "true", token: "--materialize=true" },
74
+ ],
75
+ submission: providerSubmission(lineageId, lens, order),
76
+ };
77
+ }
78
+
79
+ function rawNextTransition(inputs) {
80
+ return inputs.length === 0 ? undefined : {
81
+ kind: "collect",
82
+ reason_code: "reviewer_results_required",
83
+ collect: { inputs: inputs.map((input) => ({
84
+ name: input.name,
85
+ schema: input.schema,
86
+ capture_operation: input.captureOperation,
87
+ arguments: input.arguments,
88
+ submission: input.submission,
89
+ })) },
90
+ };
91
+ }
92
+
93
+ function collectStatus(lineageId, inputs) {
94
+ const transition = rawNextTransition(inputs);
95
+ return {
96
+ contract: "gentle-ai.review-integration/v2",
97
+ applicability: "current_target",
98
+ authority: { version: "compact-v2", lineageId, state: "reviewing", generation: 1, revision: SHA },
99
+ receipt: { status: "none" },
100
+ action: "stop",
101
+ replayability: "not_replayable",
102
+ targetIdentity: SHA,
103
+ projection: {
104
+ schema: "gentle-ai.review-candidate-projection/v1", kind: "current-changes", projection: "workspace",
105
+ baseTree: TREE, initialReviewTree: TREE, currentCandidateTree: TREE,
106
+ pathsDigest: SHA, paths: ["app.ts"], intendedUntracked: [], intendedUntrackedProof: SHA,
107
+ initialSnapshotIdentity: SHA, currentSnapshotIdentity: SHA,
108
+ },
109
+ candidates: [],
110
+ ...(transition === undefined ? {} : { nextTransition: { kind: "collect", reasonCode: "reviewer_results_required", collect: { inputs: [...inputs] } } }),
111
+ raw: {
112
+ schema: "gentle-ai.review-integration.status/v5", contract: "gentle-ai.review-integration/v2",
113
+ action: "stop", lineage_id: lineageId, target_identity: SHA,
114
+ ...(transition === undefined ? {} : { next_transition: transition }),
115
+ },
116
+ };
117
+ }
118
+
119
+ function repository(t) {
120
+ const cwd = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-relay-restart-")));
121
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
122
+ execFileSync("git", ["init", "-b", "main"], { cwd });
123
+ writeFileSync(join(cwd, "app.ts"), "export const value = 1;\n");
124
+ execFileSync("git", ["add", "."], { cwd });
125
+ execFileSync("git", ["-c", "user.name=Relay Restart Test", "-c", "user.email=relay-restart@example.invalid", "commit", "-m", "initial"], { cwd });
126
+ return cwd;
127
+ }
128
+
129
+ function restartWorkerSource() {
130
+ const extensionUrl = pathToFileURL(join(import.meta.dirname, "..", "extensions", "gentle-ai.ts")).href;
131
+ const relayUrl = pathToFileURL(join(import.meta.dirname, "..", "lib", "review-host-relay.ts")).href;
132
+ const terminalSubmission = JSON.stringify({
133
+ schema: "gentle-ai.review-last-event-closure/v1",
134
+ operation: "review/capture-result",
135
+ lineage_id: LINEAGE,
136
+ state: "approved",
137
+ store_revision: SHA,
138
+ action: "native last event closed the review",
139
+ });
140
+ return `
141
+ import { readFile, writeFile } from "node:fs/promises";
142
+
143
+ const [cwd, statusFile, mode, outFile] = process.argv.slice(-4);
144
+ const { __testing } = await import(${JSON.stringify(extensionUrl)});
145
+ const { ReviewHostRelayError, REVIEW_HOST_RELAY_FAILURE } = await import(${JSON.stringify(relayUrl)});
146
+ const statusQueue = JSON.parse(await readFile(statusFile, "utf8"));
147
+ const relayRequests = [];
148
+ const statusCalls = [];
149
+ let captureCalls = 0;
150
+
151
+ __testing.setReviewHostRelayRunnerForTesting(async (request) => {
152
+ relayRequests.push({
153
+ captureArgumentTokens: request.captureArgumentTokens,
154
+ submission: request.submission,
155
+ });
156
+ if (mode === "capture-fail") {
157
+ throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_FAILED, "pi", "pi subprocess failed", { exitCode: 4 });
158
+ }
159
+ if (mode === "capture-ambiguous") {
160
+ throw Object.assign(new Error("capture response lost"), { mutationOutcome: "unknown", nextAction: "review.status" });
161
+ }
162
+ if (mode === "capture-terminal") {
163
+ return { promptByteLength: 64, resultByteLength: 32, submission: ${JSON.stringify(terminalSubmission)} };
164
+ }
165
+ return { promptByteLength: 64, resultByteLength: 32, submission: "{\\"admission_decision\\":\\"completed\\"}" };
166
+ });
167
+
168
+ const nativeReviewCli = {
169
+ targetStatus: async (request) => {
170
+ statusCalls.push({ cwd: request.cwd, ...(request.lineageId === undefined ? {} : { lineageId: request.lineageId }) });
171
+ const next = statusQueue.shift();
172
+ if (next === undefined) throw new Error("status queue exhausted");
173
+ return next;
174
+ },
175
+ };
176
+
177
+ let result;
178
+ let inspectResult;
179
+ let captureBinding;
180
+ let error;
181
+ try {
182
+ inspectResult = await __testing.executeReviewControllerOperation({ operation: "inspect" }, cwd, nativeReviewCli);
183
+ if (mode === "inspect") {
184
+ result = inspectResult;
185
+ } else {
186
+ const bindings = inspectResult.collectBindings;
187
+ if (!Array.isArray(bindings) || bindings.length !== 1 || typeof bindings[0]?.collectBinding !== "string") {
188
+ throw new Error("INSPECT did not expose exactly one public collectBinding");
189
+ }
190
+ captureBinding = bindings[0].collectBinding;
191
+ captureCalls += 1;
192
+ result = await __testing.executeReviewCaptureOperation(
193
+ { lineageId: "relay-lineage", collectBinding: captureBinding, reviewerRunAcknowledged: true },
194
+ cwd,
195
+ nativeReviewCli,
196
+ );
197
+ }
198
+ } catch (caught) {
199
+ error = caught instanceof Error ? { name: caught.name, message: caught.message } : String(caught);
200
+ }
201
+
202
+ await writeFile(outFile, JSON.stringify({ result, inspectResult, captureBinding, error, relayRequests, statusCalls, captureCalls }, null, 2));
203
+ `;
204
+ }
205
+
206
+ function runWorker(t, cwd, statuses, mode) {
207
+ const scratch = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-relay-restart-run-")));
208
+ t.after(() => rmSync(scratch, { recursive: true, force: true }));
209
+ const statusFile = join(scratch, "statuses.json");
210
+ const outFile = join(scratch, "out.json");
211
+ writeFileSync(statusFile, JSON.stringify(statuses));
212
+ const out = execFileSync(process.execPath, ["--experimental-strip-types", "--input-type=module", "--eval", restartWorkerSource(), cwd, statusFile, mode, outFile], {
213
+ encoding: "utf8", env: process.env, timeout: 15_000,
214
+ });
215
+ assert.equal(out.length, 0, `worker should write nothing to stdout, got: ${out}`);
216
+ return JSON.parse(readFileSync(outFile, "utf8"));
217
+ }
218
+
219
+ function inspectCollectBinding(inspectResult) {
220
+ const bindings = inspectResult?.collectBindings;
221
+ if (!Array.isArray(bindings) || bindings.length !== 1) return undefined;
222
+ return typeof bindings[0]?.collectBinding === "string" ? bindings[0].collectBinding : undefined;
223
+ }
224
+
225
+ const PENDING = collectStatus(LINEAGE, [relayCollectInput(LINEAGE, LENS, ORDER)]);
226
+ const CONVERGED = collectStatus(LINEAGE, []);
227
+
228
+ test("a fresh controller process reoffers the exact public binding before one capture closes", async (t) => {
229
+ const cwd = repository(t);
230
+
231
+ // Process A copies its own public binding to capture. Transport fails without
232
+ // a capture mutation, and the capture path does not automatically re-query.
233
+ const failure = runWorker(t, cwd, [PENDING, PENDING], "capture-fail");
234
+ assert.equal(failure.error, undefined, `failure run threw: ${JSON.stringify(failure.error)}`);
235
+ assert.equal(failure.captureCalls, 1, "Process A invokes one capture");
236
+ assert.equal(failure.relayRequests.length, 1, "Process A observes exactly one pending slot");
237
+ assert.equal(failure.statusCalls.length, 2, "Process A performs INSPECT and the selected capture STATUS only");
238
+ assert.deepEqual(failure.statusCalls, [{ cwd }, { cwd, lineageId: LINEAGE }]);
239
+ assert.equal(failure.result.tool, "gentle_review_capture");
240
+ assert.equal(failure.result.status, "blocked");
241
+ assert.equal(failure.result.outcome, "pi-host-relay-transport-failure");
242
+ assert.deepEqual(failure.result.failure, { kind: "pi-failed", stage: "pi", exit_code: 4, timed_out: false });
243
+ assert.equal((failure.result.captured_slots ?? []).length, 0, "transport failure captures nothing");
244
+ assert.equal(failure.result.mutation_performed, false);
245
+ assert.match(String(failure.result.next_action), /fresh STATUS/);
246
+ assert.match(String(failure.result.next_action), /exact reoffered one-slot binding/);
247
+ const observedBinding = failure.captureBinding;
248
+ assert.equal(observedBinding, inspectCollectBinding(failure.inspectResult), "Process A copies the public binding without rebuilding it");
249
+ const observedRelayRequest = failure.relayRequests[0];
250
+
251
+ // Process B is a true restart. It only exposes the provider-owned public
252
+ // binding, so the parent can decide whether a retry is safe.
253
+ const inspect = runWorker(t, cwd, [PENDING], "inspect");
254
+ assert.equal(inspect.error, undefined, `inspect run threw: ${JSON.stringify(inspect.error)}`);
255
+ assert.equal(inspect.captureCalls, 0, "INSPECT never invokes capture");
256
+ assert.equal(inspect.relayRequests.length, 0, "INSPECT never launches the relay");
257
+ assert.equal(inspect.statusCalls.length, 1, "INSPECT queries STATUS exactly once");
258
+ assert.deepEqual(inspect.statusCalls, [{ cwd }]);
259
+ assert.equal(inspect.result.operation, "inspect");
260
+ const reofferedBinding = inspectCollectBinding(inspect.result);
261
+ assert.ok(reofferedBinding !== undefined, "INSPECT exposed one provider-returned pending binding");
262
+ assert.equal(reofferedBinding, observedBinding, "the fresh reoffer is byte-identical to the observed opaque binding");
263
+
264
+ // Process C runs only after the parent confirms equality. It copies its fresh
265
+ // reoffer to one capture and receives the terminal native closure directly.
266
+ const relaunch = runWorker(t, cwd, [PENDING, PENDING], "capture-terminal");
267
+ assert.equal(relaunch.error, undefined, `relaunch threw: ${JSON.stringify(relaunch.error)}`);
268
+ assert.equal(relaunch.captureCalls, 1, "Process C invokes exactly one capture");
269
+ assert.equal(relaunch.relayRequests.length, 1, "Process C launches one relay from the fresh reoffer");
270
+ assert.deepEqual(relaunch.statusCalls, [{ cwd }, { cwd, lineageId: LINEAGE }], "terminal capture performs no post-success STATUS");
271
+ assert.equal(relaunch.captureBinding, observedBinding, "Process C copies the exact reoffered binding");
272
+ assert.deepEqual(relaunch.relayRequests[0], observedRelayRequest, "the relay receives the same provider-owned slot after restart");
273
+ assert.deepEqual(relaunch.relayRequests[0].submission, providerSubmission(LINEAGE, LENS, ORDER));
274
+ assert.equal(relaunch.result.tool, "gentle_review_capture");
275
+ assert.equal(relaunch.result.status, "closed");
276
+ assert.equal(relaunch.result.outcome, "native-last-event-closure");
277
+ assert.equal(relaunch.result.closure.operation, "review/capture-result");
278
+
279
+ // A nonterminal result also stops at the one capture; it does not follow the
280
+ // next lifecycle transition on its own.
281
+ const nonterminal = runWorker(t, cwd, [PENDING, PENDING], "capture-nonterminal");
282
+ assert.equal(nonterminal.error, undefined, `nonterminal capture threw: ${JSON.stringify(nonterminal.error)}`);
283
+ assert.equal(nonterminal.captureCalls, 1);
284
+ assert.equal(nonterminal.relayRequests.length, 1);
285
+ assert.deepEqual(nonterminal.statusCalls, [{ cwd }, { cwd, lineageId: LINEAGE }], "nonterminal capture does not auto-follow STATUS");
286
+ assert.equal(nonterminal.result.status, "captured");
287
+ assert.equal(nonterminal.result.outcome, "native-reviewer-result-captured");
288
+
289
+ // A response with an unknown mutation outcome reconciles once through STATUS
290
+ // without replaying the capture or relay.
291
+ const ambiguous = runWorker(t, cwd, [PENDING, PENDING, CONVERGED], "capture-ambiguous");
292
+ assert.equal(ambiguous.error, undefined, `ambiguous capture threw: ${JSON.stringify(ambiguous.error)}`);
293
+ assert.equal(ambiguous.captureCalls, 1, "ambiguous capture is never replayed");
294
+ assert.equal(ambiguous.relayRequests.length, 1, "ambiguity does not relaunch the relay");
295
+ assert.deepEqual(ambiguous.statusCalls, [{ cwd }, { cwd, lineageId: LINEAGE }, { cwd, lineageId: LINEAGE }], "one capture STATUS plus one reconciliation STATUS");
296
+ assert.equal(ambiguous.result.tool, "gentle_review_capture");
297
+ assert.equal(ambiguous.result.status, "reconciled");
298
+ assert.equal(ambiguous.result.outcome, "native-capture-outcome-unknown");
299
+ });
300
+
301
+ test("every provider-owned binding drift forbids a capture after restart", async (t) => {
302
+ const cwd = repository(t);
303
+
304
+ // Process A observes the original public binding and suffers a transport
305
+ // failure. The parent retains the opaque binding only for equality checks.
306
+ const failure = runWorker(t, cwd, [PENDING, PENDING], "capture-fail");
307
+ assert.equal(failure.captureCalls, 1);
308
+ assert.equal(failure.relayRequests.length, 1);
309
+ assert.equal(failure.result.outcome, "pi-host-relay-transport-failure");
310
+ const observedBinding = failure.captureBinding;
311
+ const baseInput = relayCollectInput(LINEAGE, LENS, ORDER);
312
+ const driftArgument = (name, value, token) => {
313
+ const input = structuredClone(baseInput);
314
+ const argument = input.arguments.find((candidate) => candidate.name === name);
315
+ assert.ok(argument !== undefined);
316
+ const originalToken = argument.token;
317
+ Object.assign(argument, { value, token });
318
+ input.submission.argumentTokens = input.submission.argumentTokens.map((candidate) => candidate === originalToken ? token : candidate);
319
+ return input;
320
+ };
321
+ const driftedSubmission = structuredClone(baseInput);
322
+ driftedSubmission.submission.values[0]!.slot = "drifted_reviewer_result";
323
+ const driftSha = `sha256:${"f".repeat(64)}`;
324
+ const driftCases = [
325
+ { field: "submission", input: driftedSubmission },
326
+ { field: "captureArgumentTokens", input: driftArgument("expected-revision", driftSha, `--expected-revision=${driftSha}`) },
327
+ { field: "order", input: driftArgument("order", String(ORDER + 1), `--order=${ORDER + 1}`) },
328
+ { field: "subjectHash", input: driftArgument("subject-hash", driftSha, `--subject-hash=${driftSha}`) },
329
+ { field: "lens", input: driftArgument("lens", "review-risk", "--lens=review-risk") },
330
+ ];
331
+
332
+ for (const { field, input } of driftCases) {
333
+ // Process B is fresh and exposes one independently drifted public binding.
334
+ const inspect = runWorker(t, cwd, [collectStatus(LINEAGE, [input])], "inspect");
335
+ assert.equal(inspect.captureCalls, 0, `${field}: INSPECT never invokes capture`);
336
+ assert.equal(inspect.relayRequests.length, 0, `${field}: INSPECT never launches the relay`);
337
+ assert.deepEqual(inspect.statusCalls, [{ cwd }], `${field}: INSPECT queries STATUS once`);
338
+ const reofferedBinding = inspectCollectBinding(inspect.result);
339
+ assert.ok(reofferedBinding !== undefined, `${field}: INSPECT exposes the drifted binding for comparison`);
340
+ assert.notEqual(reofferedBinding, observedBinding, `${field}: the drifted reoffer must not equal the observed binding`);
341
+ }
342
+ });
343
+
344
+ test("a missing restarted reoffer exposes no public binding and forbids capture", async (t) => {
345
+ const cwd = repository(t);
346
+
347
+ // Process A observes the original binding and suffers a transport failure.
348
+ const failure = runWorker(t, cwd, [PENDING, PENDING], "capture-fail");
349
+ assert.equal(failure.captureCalls, 1);
350
+ assert.equal(failure.relayRequests.length, 1);
351
+ assert.equal(failure.result.outcome, "pi-host-relay-transport-failure");
352
+
353
+ // Process B is fresh and returns a converged STATUS with no public binding.
354
+ // The parent confirms the absence, so it does not launch another process.
355
+ const inspect = runWorker(t, cwd, [CONVERGED], "inspect");
356
+ assert.equal(inspect.captureCalls, 0, "INSPECT never invokes capture");
357
+ assert.equal(inspect.relayRequests.length, 0, "INSPECT never launches the relay");
358
+ assert.deepEqual(inspect.statusCalls, [{ cwd }], "INSPECT queries STATUS once");
359
+ assert.equal(inspectCollectBinding(inspect.result), undefined, "INSPECT exposed no matching public binding");
360
+ });