gentle-pi 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -80
- package/assets/agents/review-readability.md +0 -2
- package/assets/agents/review-reliability.md +0 -2
- package/assets/agents/review-resilience.md +0 -2
- package/assets/agents/review-risk.md +0 -2
- package/assets/agents/sdd-apply.md +5 -3
- package/assets/agents/sdd-proposal.md +2 -0
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-status.md +4 -4
- package/assets/agents/sdd-tasks.md +2 -3
- package/assets/agents/sdd-verify.md +21 -1
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +54 -248
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +22 -38
- package/assets/sdd-orchestrator-workflow.md +30 -37
- package/assets/support/sdd-status-contract.md +7 -7
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
- package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
- package/docs/native-authority-architecture.md +9 -11
- package/docs/review-integration.md +27 -373
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +1778 -3673
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +4 -8
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +372 -940
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/review-candidate-view.ts +341 -132
- package/lib/review-host-relay.ts +210 -68
- package/lib/review-integration-v2.ts +839 -307
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/sdd-preflight.ts +177 -66
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -82
- package/runtime/gentle-ai-binary.mjs +1 -1
- package/runtime/native-review-cli.mjs +322 -890
- package/runtime/review-integration-v2.mjs +789 -257
- package/runtime/review-relay-contract.mjs +1 -1
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
- package/scripts/gentle-ai-installer.mjs +75 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
- package/scripts/test-packed-runner.mjs +4 -7
- package/scripts/verify-package-files.mjs +12 -13
- package/skills/_shared/review-ledger-contract.md +8 -14
- package/skills/chained-pr/SKILL.md +3 -0
- package/skills/cognitive-doc-design/SKILL.md +1 -1
- package/skills/comment-writer/SKILL.md +1 -1
- package/skills/gentle-ai/SKILL.md +7 -74
- package/skills/judgment-day/SKILL.md +5 -7
- package/skills/rdd-defect-workflow/SKILL.md +3 -3
- package/skills/release/SKILL.md +3 -3
- package/skills/skill-registry/SKILL.md +1 -1
- package/skills/work-unit-commits/SKILL.md +3 -1
- package/tests/artifact-language.test.ts +24 -11
- package/tests/ask-user-choice.test.ts +264 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +15 -1168
- package/tests/delegated-key-learnings-contract.test.ts +8 -6
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
- package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
- package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary.test.ts +51 -228
- package/tests/gentle-ai-installer.test.ts +137 -47
- package/tests/gentle-ai.test.ts +820 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/maintainer/provider-relay.maintest.ts +340 -4
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +28 -1
- package/tests/native-review-cli.test.ts +586 -962
- package/tests/native-review-consent.test.ts +150 -66
- package/tests/native-review-parity-runtime.test.ts +80 -359
- package/tests/native-review-parity.test.ts +674 -925
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +132 -88
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +37 -35
- package/tests/provider-defect-handoff.test.ts +15 -118
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +409 -3
- package/tests/review-compact-contract.test.ts +4 -30
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +363 -1450
- package/tests/review-controller-native-routing.test.ts +1251 -5642
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +222 -71
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +112 -153
- package/tests/review-dispatch-hydration-gap.test.ts +1 -53
- package/tests/review-gate.test.ts +0 -45
- package/tests/review-host-relay-restart-parity.test.ts +360 -0
- package/tests/review-host-relay-routing.test.ts +117 -82
- package/tests/review-host-relay.test.ts +242 -8
- package/tests/review-integration-v2-forward.test.ts +576 -553
- package/tests/review-integration-v2.test.ts +239 -165
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +97 -35
- package/tests/review-recovered-lineage-routing.test.ts +0 -47
- package/tests/review-relay-transport-agent.test.ts +119 -46
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +361 -196
- package/tests/sdd-agent-tools.test.ts +36 -0
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +5 -8
- package/tests/writer-edit-surface-scope.test.ts +230 -0
- package/themes/Gentleman-Cute.json +94 -0
- package/themes/Gentleman-Sexy.json +92 -0
- package/lib/git-commit-transaction.ts +0 -861
- package/runtime/git-commit-transaction.mjs +0 -862
- package/scripts/run-git-commit-transaction.mjs +0 -35
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
- package/tests/git-commit-transaction.test.ts +0 -530
|
@@ -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
|
+
});
|