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.
- package/README.md +76 -79
- 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-explore.md +1 -0
- 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 +56 -249
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +20 -40
- 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/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
- 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 +2349 -3785
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +4 -8
- package/lib/agent-home.ts +8 -0
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +387 -940
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/provider-contract-bundle.ts +88 -6
- package/lib/review-candidate-view.ts +341 -132
- package/lib/review-host-relay.ts +324 -96
- package/lib/review-integration-v2.ts +868 -307
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/sdd-preflight.ts +179 -68
- 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 +337 -890
- package/runtime/review-integration-v2.mjs +816 -255
- 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 +23 -23
- package/skills/_shared/review-ledger-contract.md +14 -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/issue-creation/SKILL.md +53 -93
- 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/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
- package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
- package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -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 +821 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/issue-creation-skill.test.ts +103 -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 +45 -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 +152 -36
- package/tests/provider-contract-bundle.test.ts +76 -0
- package/tests/provider-contract-mirror.test.ts +19 -0
- package/tests/provider-defect-handoff.test.ts +15 -118
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-agent-end-preflight.test.ts +408 -0
- 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-contract-prompt.test.ts +139 -0
- 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 +328 -91
- package/tests/review-host-relay.test.ts +436 -14
- package/tests/review-integration-v2-forward.test.ts +623 -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 +362 -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 +366 -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
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
- /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,1169 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// through live scratch-repository lifecycles.
|
|
4
|
-
//
|
|
5
|
-
// pnpm test:cross-lane # requires the dev-binary override
|
|
6
|
-
// pnpm test:cross-lane --with-model # adds the real pi reviewer run
|
|
7
|
-
//
|
|
8
|
-
// The battery exists because the pinned decoder lane never sees new envelope
|
|
9
|
-
// schemas and the controller sequencing was never driven through a full
|
|
10
|
-
// lifecycle before merge. Three check groups:
|
|
11
|
-
// 1. Full direct-lane lifecycles against the override binary through
|
|
12
|
-
// runtime/*.mjs (low to gate allow; medium consent/v3 granted).
|
|
13
|
-
// 2. Controller sequencing: at every step the client's decoded offered
|
|
14
|
-
// next step must equal the native transition, and correction evidence
|
|
15
|
-
// must be collected before targeted validation is ever offered
|
|
16
|
-
// (the validate-before-evidence class, pending fix/validate-before-evidence).
|
|
17
|
-
// 3. Forward-decoder freshness: every live envelope captured from the
|
|
18
|
-
// override binary must decode without unknown-key rejection - the
|
|
19
|
-
// early warning that gentle-ai main grew a field gentle-pi lacks.
|
|
20
|
-
//
|
|
21
|
-
// Excluded from `pnpm test` by construction: the default suite globs
|
|
22
|
-
// tests/*.test.ts only.
|
|
23
|
-
import { execFileSync } from "node:child_process";
|
|
24
|
-
import { createHash } from "node:crypto";
|
|
25
|
-
import { chmodSync, mkdtempSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
26
|
-
import { tmpdir } from "node:os";
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
27
3
|
import { dirname, join } from "node:path";
|
|
28
|
-
|
|
29
|
-
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
decodeReviewConsentV3,
|
|
42
|
-
decodeReviewOperationV2,
|
|
43
|
-
decodeReviewResultArtifactV2,
|
|
44
|
-
decodeReviewStartV3,
|
|
45
|
-
decodeReviewStatusV3,
|
|
46
|
-
} from "../../runtime/review-integration-v2.mjs";
|
|
47
|
-
// The recovered-successor checks drive the CONTROLLER (not just the runtime
|
|
48
|
-
// adapter) against the live binary; Node's default type stripping loads the
|
|
49
|
-
// authored TypeScript directly.
|
|
50
|
-
import { __testing } from "../../extensions/gentle-ai.ts";
|
|
51
|
-
import { CandidateViewRegistry, injectReviewCandidateView } from "../../lib/review-candidate-view.ts";
|
|
52
|
-
|
|
53
|
-
const WITH_MODEL = process.argv.includes("--with-model");
|
|
54
|
-
const CONTRACT = "gentle-ai.review-integration/v2";
|
|
55
|
-
const KNOWN_RED_SEQUENCING = "known-red pending fix/validate-before-evidence";
|
|
56
|
-
|
|
57
|
-
// A schema-incompatible failure mid-lifecycle means the forward decoder lags
|
|
58
|
-
// a field gentle-ai main already emits: the exact parity gap this battery
|
|
59
|
-
// exists to surface before merge. The sequencing check (the
|
|
60
|
-
// validate-before-evidence class, pending fix/validate-before-evidence)
|
|
61
|
-
// stays unevaluated until decoder parity lands.
|
|
62
|
-
function knownRedParity(message) {
|
|
63
|
-
if (!message.includes("schema incompatible")) return message;
|
|
64
|
-
return `known-red pending gentle-pi decoder parity with gentle-ai main: ${message}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const checks = [];
|
|
68
|
-
const rawEnvelopes = [];
|
|
69
|
-
|
|
70
|
-
function describeError(error) {
|
|
71
|
-
if (!(error instanceof Error)) return String(error);
|
|
72
|
-
const parts = [error.message];
|
|
73
|
-
let cause = error.cause;
|
|
74
|
-
while (cause !== undefined && cause !== null) {
|
|
75
|
-
parts.push(cause instanceof Error ? cause.message : String(cause));
|
|
76
|
-
cause = cause instanceof Error ? cause.cause : undefined;
|
|
77
|
-
}
|
|
78
|
-
let described = parts.join(" <- ");
|
|
79
|
-
if (described.includes("schema incompatible")) {
|
|
80
|
-
described += " (the decoder freshness lane below names the exact rejected field)";
|
|
81
|
-
}
|
|
82
|
-
return described;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function report(name, status, note) {
|
|
86
|
-
checks.push({ name, status, note });
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function pass(name, note) {
|
|
90
|
-
report(name, "PASS", note);
|
|
91
|
-
}
|
|
92
|
-
function fail(name, note) {
|
|
93
|
-
report(name, "FAIL", note);
|
|
94
|
-
}
|
|
95
|
-
function skip(name, note) {
|
|
96
|
-
report(name, "SKIP", note);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// --- binary resolution: the dev-binary override is this battery's reason to exist ---
|
|
100
|
-
|
|
101
|
-
function resolveBatteryBinary() {
|
|
102
|
-
const fromEnv = process.env[GENTLE_AI_DEV_BINARY_ENV];
|
|
103
|
-
if (fromEnv !== undefined && fromEnv.length > 0) return fromEnv;
|
|
104
|
-
const override = resolveGentleAiDevBinaryOverride();
|
|
105
|
-
if (override !== undefined) {
|
|
106
|
-
// Project the registration into the env override so every downstream
|
|
107
|
-
// dev-mode relaxation (version banner, capability floor) is active.
|
|
108
|
-
process.env[GENTLE_AI_DEV_BINARY_ENV] = override.path;
|
|
109
|
-
return override.path;
|
|
110
|
-
}
|
|
111
|
-
throw new Error(
|
|
112
|
-
`no dev binary override: set ${GENTLE_AI_DEV_BINARY_ENV}=<absolute path> or register one with /gentle:dev-binary <path>`,
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// --- raw native access (the battery's independent view of the binary) ---
|
|
117
|
-
|
|
118
|
-
function rawInvoke(binary, cwd, args) {
|
|
119
|
-
const stdout = execFileSync(binary, args, {
|
|
120
|
-
cwd,
|
|
121
|
-
encoding: "utf8",
|
|
122
|
-
env: gentleAiProcessEnvironment(),
|
|
123
|
-
});
|
|
124
|
-
const body = JSON.parse(stdout);
|
|
125
|
-
if (body !== null && typeof body === "object" && typeof body.schema === "string") {
|
|
126
|
-
rawEnvelopes.push({ schema: body.schema, source: args.slice(0, 2).join(" "), body });
|
|
127
|
-
}
|
|
128
|
-
return body;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function rawStatus(binary, cwd) {
|
|
132
|
-
return rawInvoke(binary, cwd, [
|
|
133
|
-
"review", "status", "--contract", CONTRACT, "--cwd", cwd,
|
|
134
|
-
"--projection", "workspace", "--next-transition",
|
|
135
|
-
]);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// --- scratch repositories ---
|
|
139
|
-
|
|
140
|
-
function git(cwd, ...args) {
|
|
141
|
-
execFileSync("git", args, { cwd, encoding: "utf8" });
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function scratchRepo(root, name) {
|
|
145
|
-
const cwd = join(root, name);
|
|
146
|
-
mkdirSync(cwd, { recursive: true });
|
|
147
|
-
git(cwd, "init", "-q", "-b", "main");
|
|
148
|
-
git(cwd, "config", "user.email", "crosslane@example.com");
|
|
149
|
-
git(cwd, "config", "user.name", "Cross Lane Battery");
|
|
150
|
-
git(cwd, "commit", "-q", "--allow-empty", "-m", "chore: root");
|
|
151
|
-
return cwd;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// A LINKED git worktree (not the primary checkout) of a fresh scratch repo:
|
|
155
|
-
// the field shape the reported defect was hit in.
|
|
156
|
-
function scratchLinkedWorktree(root, name) {
|
|
157
|
-
const primary = scratchRepo(root, `${name}-primary`);
|
|
158
|
-
const linked = join(root, `${name}-linked`);
|
|
159
|
-
git(primary, "worktree", "add", "-q", linked, "-b", `feature/${name}`);
|
|
160
|
-
git(linked, "config", "user.email", "crosslane@example.com");
|
|
161
|
-
git(linked, "config", "user.name", "Cross Lane Battery");
|
|
162
|
-
return linked;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function write(cwd, name, content) {
|
|
166
|
-
const path = join(cwd, name);
|
|
167
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
168
|
-
writeFileSync(path, content);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function commitAll(cwd, message) {
|
|
172
|
-
git(cwd, "add", "-A");
|
|
173
|
-
git(cwd, "commit", "-q", "-m", message);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// --- transition parity: the controller's offered next step vs the native one ---
|
|
177
|
-
|
|
178
|
-
function transitionSummary(kind, reasonCode, executeOperation, collectOperations) {
|
|
179
|
-
return JSON.stringify({ kind, reasonCode, executeOperation, collectOperations });
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function summarizeDecoded(transition) {
|
|
183
|
-
return transitionSummary(
|
|
184
|
-
transition?.kind ?? "(none)",
|
|
185
|
-
transition?.reasonCode ?? "(none)",
|
|
186
|
-
transition?.execute?.operation,
|
|
187
|
-
transition?.collect?.inputs.map((input) => input.captureOperation),
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function summarizeRaw(transition) {
|
|
192
|
-
return transitionSummary(
|
|
193
|
-
transition?.kind ?? "(none)",
|
|
194
|
-
transition?.reason_code ?? "(none)",
|
|
195
|
-
transition?.execute?.operation,
|
|
196
|
-
transition?.collect?.inputs.map((input) => input.capture_operation),
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function assertOfferedStepMatchesNative(step, decodedStatus, rawDocument) {
|
|
201
|
-
const offered = summarizeDecoded(decodedStatus.nextTransition);
|
|
202
|
-
const native = summarizeRaw(rawDocument.next_transition);
|
|
203
|
-
if (offered !== native) {
|
|
204
|
-
throw new Error(`${step}: controller offered ${offered} but native transition is ${native}`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function executeTokens(transition) {
|
|
209
|
-
return transition.execute.arguments.map(
|
|
210
|
-
(argument) => argument.token ?? `--${argument.name.replaceAll("_", "-")}=${argument.value}`,
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function rawArgumentValues(input) {
|
|
215
|
-
const values = {};
|
|
216
|
-
for (const argument of input.arguments ?? []) values[argument.name] = argument.value;
|
|
217
|
-
return values;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function substitute(tokens, slots) {
|
|
221
|
-
return tokens.map((token) => {
|
|
222
|
-
let out = token;
|
|
223
|
-
for (const [slot, value] of Object.entries(slots)) out = out.replaceAll(`{{${slot}}}`, value);
|
|
224
|
-
return out;
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// --- checks ---
|
|
229
|
-
|
|
230
|
-
async function lowLifecycle(binary, cli, root) {
|
|
231
|
-
const cwd = scratchRepo(root, "pi-low");
|
|
232
|
-
write(cwd, "docs/ordinary-guide.md", "# Ordinary guide\n\nline one\n");
|
|
233
|
-
commitAll(cwd, "docs: guide");
|
|
234
|
-
write(cwd, "docs/ordinary-guide.md", "# Ordinary guide\n\nline one\nline two, purely passive documentation\n");
|
|
235
|
-
|
|
236
|
-
let raw = rawStatus(binary, cwd);
|
|
237
|
-
let status = await cli.targetStatus({ cwd });
|
|
238
|
-
assertOfferedStepMatchesNative("pre-start", status, raw);
|
|
239
|
-
if (status.nextTransition?.execute?.operation !== "review.start") {
|
|
240
|
-
throw new Error(`expected review.start, got ${summarizeDecoded(status.nextTransition)}`);
|
|
241
|
-
}
|
|
242
|
-
const start = await cli.start({ cwd, targetIdentity: status.targetIdentity, projection: "workspace" });
|
|
243
|
-
if (start.riskLevel !== "low" || start.state !== "reviewing" || start.lensesRequired) {
|
|
244
|
-
throw new Error(`low start decoded riskLevel=${start.riskLevel} state=${start.state} lensesRequired=${start.lensesRequired}`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
raw = rawStatus(binary, cwd);
|
|
248
|
-
status = await cli.targetStatus({ cwd });
|
|
249
|
-
assertOfferedStepMatchesNative("pre-finalize", status, raw);
|
|
250
|
-
if (status.nextTransition?.execute?.operation !== "review.finalize") {
|
|
251
|
-
throw new Error(`expected review.finalize, got ${summarizeDecoded(status.nextTransition)}`);
|
|
252
|
-
}
|
|
253
|
-
const finalize = await cli.finalizeTransition({ cwd, argumentTokens: executeTokens(status.nextTransition) });
|
|
254
|
-
if (finalize.state !== "approved") throw new Error(`finalize state=${finalize.state}`);
|
|
255
|
-
|
|
256
|
-
git(cwd, "add", "-A");
|
|
257
|
-
const validate = await cli.validate({ cwd, gate: "pre-commit", lineageId: finalize.lineageId });
|
|
258
|
-
if (!validate.allowed || validate.result !== "allow") {
|
|
259
|
-
throw new Error(`gate result=${validate.result} allowed=${validate.allowed}`);
|
|
260
|
-
}
|
|
261
|
-
return "start (low, zero lenses) -> finalize approved -> pre-commit validate allow, offered step matched native at every hop";
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
async function mediumConsent(binary, cli, root) {
|
|
265
|
-
const cwd = scratchRepo(root, "pi-medium");
|
|
266
|
-
write(cwd, "src/mul.js", "export function mul(a, b) {\n return a * b;\n}\n");
|
|
267
|
-
commitAll(cwd, "feat: mul");
|
|
268
|
-
write(cwd, "src/mul.js", "export function mul(a, b) {\n return a * b;\n}\nexport function twice(a) {\n return a + a;\n}\n");
|
|
269
|
-
|
|
270
|
-
const raw = rawStatus(binary, cwd);
|
|
271
|
-
const status = await cli.targetStatus({ cwd });
|
|
272
|
-
assertOfferedStepMatchesNative("pre-start", status, raw);
|
|
273
|
-
let consent;
|
|
274
|
-
try {
|
|
275
|
-
await cli.start({ cwd, targetIdentity: status.targetIdentity, projection: "workspace" });
|
|
276
|
-
throw new Error("medium start unexpectedly proceeded without surfacing consent");
|
|
277
|
-
} catch (error) {
|
|
278
|
-
if (!(error instanceof NativeReviewConsentRequiredError)) throw error;
|
|
279
|
-
consent = error.consent;
|
|
280
|
-
}
|
|
281
|
-
if (consent.schema !== "gentle-ai.review-integration.consent/v3") {
|
|
282
|
-
throw new Error(`consent schema=${consent.schema}`);
|
|
283
|
-
}
|
|
284
|
-
rawEnvelopes.push({ schema: consent.schema, source: "review start (consent)", body: consent.raw });
|
|
285
|
-
const answer = await cli.answerConsent({ cwd, consent, answer: "granted" });
|
|
286
|
-
if (answer.kind !== "started") throw new Error(`granted answer kind=${answer.kind}`);
|
|
287
|
-
const granted = answer.start;
|
|
288
|
-
if (granted.state !== "reviewing" || granted.riskLevel !== "medium" || granted.selectedLenses.length !== 1) {
|
|
289
|
-
throw new Error(`granted state=${granted.state} risk=${granted.riskLevel} lenses=${granted.selectedLenses.length}`);
|
|
290
|
-
}
|
|
291
|
-
return { cwd, note: "consent/v3 surfaced through the direct decoder lane; granted answer created a reviewing medium lineage" };
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// sequencingLifecycle drives a scripted correction on its own medium lineage
|
|
295
|
-
// and checks, at every step, that the client's decoded offered step equals
|
|
296
|
-
// the native transition - including the evidence-before-validation ordering.
|
|
297
|
-
async function sequencingLifecycle(binary, cli, root) {
|
|
298
|
-
const cwd = scratchRepo(root, "pi-sequencing");
|
|
299
|
-
const base = "export function greet(name) {\n return \"hi \" + name;\n}\n";
|
|
300
|
-
write(cwd, "src/greet.js", base);
|
|
301
|
-
commitAll(cwd, "feat: greet");
|
|
302
|
-
write(cwd, "src/greet.js", `${base}export function shout(name) {\n return name.toUpperCase() + "!";\n}\n`);
|
|
303
|
-
|
|
304
|
-
let raw = rawStatus(binary, cwd);
|
|
305
|
-
let status = await cli.targetStatus({ cwd });
|
|
306
|
-
assertOfferedStepMatchesNative("pre-start", status, raw);
|
|
307
|
-
let consent;
|
|
308
|
-
try {
|
|
309
|
-
await cli.start({ cwd, targetIdentity: status.targetIdentity, projection: "workspace" });
|
|
310
|
-
throw new Error("medium start unexpectedly proceeded without surfacing consent");
|
|
311
|
-
} catch (error) {
|
|
312
|
-
if (!(error instanceof NativeReviewConsentRequiredError)) throw error;
|
|
313
|
-
consent = error.consent;
|
|
314
|
-
}
|
|
315
|
-
const sequencingAnswer = await cli.answerConsent({ cwd, consent, answer: "granted" });
|
|
316
|
-
if (sequencingAnswer.kind !== "started") throw new Error(`granted answer kind=${sequencingAnswer.kind}`);
|
|
317
|
-
|
|
318
|
-
// Reviewer slot: capture one deterministic candidate-causal blocker.
|
|
319
|
-
raw = rawStatus(binary, cwd);
|
|
320
|
-
status = await cli.targetStatus({ cwd });
|
|
321
|
-
assertOfferedStepMatchesNative("reviewer-slot", status, raw);
|
|
322
|
-
const reviewerInput = status.nextTransition?.collect?.inputs[0];
|
|
323
|
-
if (reviewerInput?.captureOperation !== "review.capture-result") {
|
|
324
|
-
throw new Error(`expected review.capture-result, got ${summarizeDecoded(status.nextTransition)}`);
|
|
325
|
-
}
|
|
326
|
-
const args = rawArgumentValues(raw.next_transition.collect.inputs[0]);
|
|
327
|
-
const reviewerFile = join(root, "pi-reviewer.json");
|
|
328
|
-
writeFileSync(reviewerFile, JSON.stringify({
|
|
329
|
-
subject_hash: args["subject-hash"],
|
|
330
|
-
inspection: { status: "completed", paths: ["src/greet.js"] },
|
|
331
|
-
evidence: ["shout calls toUpperCase without a nullish guard; introduced by the candidate hunk"],
|
|
332
|
-
findings: [{
|
|
333
|
-
claim: "shout calls toUpperCase on its argument without a null/undefined guard",
|
|
334
|
-
severity: "BLOCKER",
|
|
335
|
-
evidence_class: "deterministic",
|
|
336
|
-
causal_disposition: "introduced",
|
|
337
|
-
lens: "review-reliability",
|
|
338
|
-
location: "src/greet.js:5",
|
|
339
|
-
proof_refs: ["src/greet.js:4-6 calls name.toUpperCase() with no nullish guard in the candidate tree"],
|
|
340
|
-
}],
|
|
341
|
-
}));
|
|
342
|
-
rawInvoke(binary, cwd, [
|
|
343
|
-
"review", "capture-result",
|
|
344
|
-
"--lineage", args.lineage,
|
|
345
|
-
"--expected-revision", args["expected-revision"],
|
|
346
|
-
"--target", args.target,
|
|
347
|
-
"--repository-context", args["repository-context"],
|
|
348
|
-
"--lens", args.lens,
|
|
349
|
-
"--order", args.order,
|
|
350
|
-
"--subject-hash", args["subject-hash"],
|
|
351
|
-
"--input", reviewerFile,
|
|
352
|
-
]);
|
|
353
|
-
|
|
354
|
-
// Finalize into correction_required through the client.
|
|
355
|
-
raw = rawStatus(binary, cwd);
|
|
356
|
-
status = await cli.targetStatus({ cwd });
|
|
357
|
-
assertOfferedStepMatchesNative("post-capture", status, raw);
|
|
358
|
-
if (status.nextTransition?.execute?.operation !== "review.finalize") {
|
|
359
|
-
throw new Error(`expected review.finalize, got ${summarizeDecoded(status.nextTransition)}`);
|
|
360
|
-
}
|
|
361
|
-
const finalize = await cli.finalizeTransition({ cwd, argumentTokens: executeTokens(status.nextTransition) });
|
|
362
|
-
if (finalize.state !== "correction_required") throw new Error(`finalize state=${finalize.state}`);
|
|
363
|
-
|
|
364
|
-
// Correction plan forecast is submitted BEFORE editing.
|
|
365
|
-
raw = rawStatus(binary, cwd);
|
|
366
|
-
status = await cli.targetStatus({ cwd });
|
|
367
|
-
assertOfferedStepMatchesNative("correction-plan", status, raw);
|
|
368
|
-
const planInput = raw.next_transition?.collect?.inputs[0];
|
|
369
|
-
if (planInput?.capture_operation !== "external.plan_correction") {
|
|
370
|
-
throw new Error(`expected external.plan_correction, got ${summarizeRaw(raw.next_transition)}`);
|
|
371
|
-
}
|
|
372
|
-
const planTokens = substitute(planInput.submission.argument_tokens, { value: "2" });
|
|
373
|
-
rawInvoke(binary, root, ["review", planInput.submission.operation_token, ...planTokens]);
|
|
374
|
-
|
|
375
|
-
// Bounded fix edit.
|
|
376
|
-
write(cwd, "src/greet.js", `${base}export function shout(name) {\n if (name == null) return "!";\n return name.toUpperCase() + "!";\n}\n`);
|
|
377
|
-
|
|
378
|
-
// THE sequencing class check: correction evidence must be collected
|
|
379
|
-
// before any targeted validation is offered.
|
|
380
|
-
raw = rawStatus(binary, cwd);
|
|
381
|
-
status = await cli.targetStatus({ cwd });
|
|
382
|
-
assertOfferedStepMatchesNative("post-fix", status, raw);
|
|
383
|
-
const inputs = status.nextTransition?.kind === "collect" ? status.nextTransition.collect?.inputs ?? [] : [];
|
|
384
|
-
// An offered validation STEP is a targeted-validation collect input. The
|
|
385
|
-
// bare `validation_request` field is descriptive context both live
|
|
386
|
-
// emitters (pinned 2.2.3 and 2.4.0-main, probed 2026-08-16) publish
|
|
387
|
-
// alongside the evidence collect at `correction_required`.
|
|
388
|
-
const validationOffered =
|
|
389
|
-
inputs.some((input) => input.captureOperation === "external.run_targeted_validation" || input.captureOperation === "review.capture-validation");
|
|
390
|
-
const evidenceInputs = inputs.filter((input) => input.captureOperation === "review.capture-evidence");
|
|
391
|
-
if (validationOffered) {
|
|
392
|
-
throw new Error(`${KNOWN_RED_SEQUENCING}: targeted validation was offered before correction evidence was captured`);
|
|
393
|
-
}
|
|
394
|
-
if (evidenceInputs.length !== 1) {
|
|
395
|
-
throw new Error(`${KNOWN_RED_SEQUENCING}: expected exactly one review.capture-evidence input before validation, got ${summarizeDecoded(status.nextTransition)}`);
|
|
396
|
-
}
|
|
397
|
-
pass("sequencing: evidence collected before targeted validation", "correction status offers exactly one review.capture-evidence and no validation until evidence lands");
|
|
398
|
-
|
|
399
|
-
const evidenceArgs = rawArgumentValues(raw.next_transition.collect.inputs[0]);
|
|
400
|
-
const evidence = await cli.captureEvidence({
|
|
401
|
-
cwd,
|
|
402
|
-
lineageId: evidenceArgs.lineage,
|
|
403
|
-
targetIdentity: evidenceArgs.target,
|
|
404
|
-
expectedRevision: evidenceArgs["expected-revision"],
|
|
405
|
-
outcome: "passed",
|
|
406
|
-
evidenceDocument: `cross-lane battery: node --check src/greet.js passed; shout(null) now returns "!" instead of throwing\n`,
|
|
407
|
-
});
|
|
408
|
-
if (evidence.outcome !== "passed") throw new Error(`evidence outcome=${evidence.outcome}`);
|
|
409
|
-
|
|
410
|
-
// Only now may targeted validation be offered.
|
|
411
|
-
raw = rawStatus(binary, cwd);
|
|
412
|
-
status = await cli.targetStatus({ cwd });
|
|
413
|
-
assertOfferedStepMatchesNative("post-evidence", status, raw);
|
|
414
|
-
const validationInput = raw.next_transition?.collect?.inputs[0];
|
|
415
|
-
if (
|
|
416
|
-
validationInput?.capture_operation !== "external.run_targeted_validation" &&
|
|
417
|
-
validationInput?.capture_operation !== "review.capture-validation"
|
|
418
|
-
) {
|
|
419
|
-
throw new Error(`expected targeted validation after evidence, got ${summarizeRaw(raw.next_transition)}`);
|
|
420
|
-
}
|
|
421
|
-
if (status.validationRequest === undefined) {
|
|
422
|
-
throw new Error("decoded status is missing the validation request after evidence capture");
|
|
423
|
-
}
|
|
424
|
-
const validatorFile = join(root, "pi-validator.json");
|
|
425
|
-
writeFileSync(validatorFile, JSON.stringify({
|
|
426
|
-
targeted_validation_request_hash: status.validationRequest.requestHash,
|
|
427
|
-
correction_target_identity: status.validationRequest.correctionTargetIdentity,
|
|
428
|
-
original_criteria: { passed: true, evidence: ["frozen correction tree guards name == null before toUpperCase per the embedded diff"] },
|
|
429
|
-
correction_regression: { passed: true, evidence: ["greet() is untouched by the correction diff; only shout gained the guard"] },
|
|
430
|
-
follow_ups: [],
|
|
431
|
-
}));
|
|
432
|
-
const validationTokens = substitute(validationInput.submission.argument_tokens, { value: validatorFile });
|
|
433
|
-
const approved = rawInvoke(binary, root, ["review", validationInput.submission.operation_token, ...validationTokens]);
|
|
434
|
-
const approvedState = approved?.result?.state ?? approved?.state;
|
|
435
|
-
if (approvedState !== "approved") throw new Error(`validation finalize state=${approvedState}`);
|
|
436
|
-
return "offered step matched native at every hop; plan -> fix -> evidence -> targeted validation -> approved receipt";
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// abandonLifecycle drives the audited abandon end-to-end through the real
|
|
440
|
-
// adapter runtime against the real binary: start a low lineage, abandon it
|
|
441
|
-
// with the adapter-built maintainer authorization, and confirm the native
|
|
442
|
-
// gate accepted the binding, committed a quarantine record, and no longer
|
|
443
|
-
// offers the lineage as live authority. RED-provable: the check asserts the
|
|
444
|
-
// adapter-built binding is exactly the nine-line
|
|
445
|
-
// gentle-ai.review-abandon-authorization/v2 discarded-work binding, so a
|
|
446
|
-
// v1-emitting builder (the drift class that escaped to a live Pi session)
|
|
447
|
-
// fails this check before the binary is even invoked - and would be refused
|
|
448
|
-
// by the native gate anyway.
|
|
449
|
-
async function abandonLifecycle(binary, cli, root) {
|
|
450
|
-
const cwd = scratchRepo(root, "pi-abandon");
|
|
451
|
-
write(cwd, "docs/abandon-guide.md", "# Abandon guide\n\nline one\n");
|
|
452
|
-
commitAll(cwd, "docs: abandon guide");
|
|
453
|
-
write(cwd, "docs/abandon-guide.md", "# Abandon guide\n\nline one\nline two, purely passive documentation\n");
|
|
454
|
-
|
|
455
|
-
let raw = rawStatus(binary, cwd);
|
|
456
|
-
let status = await cli.targetStatus({ cwd });
|
|
457
|
-
assertOfferedStepMatchesNative("pre-start", status, raw);
|
|
458
|
-
const start = await cli.start({ cwd, targetIdentity: status.targetIdentity, projection: "workspace" });
|
|
459
|
-
if (start.state !== "reviewing") throw new Error(`abandon precondition start state=${start.state}`);
|
|
460
|
-
|
|
461
|
-
raw = rawStatus(binary, cwd);
|
|
462
|
-
if (raw.authority?.lineage_id !== start.lineageId || typeof raw.authority?.revision !== "string") {
|
|
463
|
-
throw new Error(`live authority missing for started lineage ${start.lineageId}: ${JSON.stringify(raw.authority)}`);
|
|
464
|
-
}
|
|
465
|
-
// A fresh lineage before any capture carries the smallest discarded-work
|
|
466
|
-
// summary the v2 binding names: no captured lens results, no findings,
|
|
467
|
-
// no evidence records.
|
|
468
|
-
const request = {
|
|
469
|
-
cwd,
|
|
470
|
-
lineage: raw.authority.lineage_id,
|
|
471
|
-
expectedRevision: raw.authority.revision,
|
|
472
|
-
snapshotIdentity: raw.projection.initial_snapshot_identity,
|
|
473
|
-
capturedLensResults: [],
|
|
474
|
-
findingsPresent: false,
|
|
475
|
-
evidenceRecordsPresent: false,
|
|
476
|
-
actor: "cross-lane-battery",
|
|
477
|
-
reason: "operator_disposition",
|
|
478
|
-
};
|
|
479
|
-
const authorization = nativeReviewAbandonAuthorization(request);
|
|
480
|
-
const expectedBinding = [
|
|
481
|
-
"gentle-ai.review-abandon-authorization/v2",
|
|
482
|
-
`lineage=${request.lineage}`,
|
|
483
|
-
`revision=${request.expectedRevision}`,
|
|
484
|
-
`snapshot_identity=${request.snapshotIdentity}`,
|
|
485
|
-
"reason=operator_disposition",
|
|
486
|
-
"captured_lens_results=",
|
|
487
|
-
"findings_present=false",
|
|
488
|
-
"evidence_records_present=false",
|
|
489
|
-
"actor=cross-lane-battery",
|
|
490
|
-
].join("\n");
|
|
491
|
-
if (authorization !== expectedBinding) {
|
|
492
|
-
throw new Error(
|
|
493
|
-
`adapter built ${authorization.split("\n")[0]} instead of the exact nine-line gentle-ai.review-abandon-authorization/v2 binding: the audited-abandon drift class (v1 emission) is back`,
|
|
494
|
-
);
|
|
495
|
-
}
|
|
496
|
-
const result = await cli.abandon({ ...request, maintainerAuthorization: authorization });
|
|
497
|
-
if (result.record?.status !== "committed") throw new Error(`abandon record status=${result.record?.status}`);
|
|
498
|
-
if (result.record?.abandonment?.schema !== "gentle-ai.review-abandon-authorization/v2") {
|
|
499
|
-
throw new Error(`abandon record binding schema=${result.record?.abandonment?.schema}`);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// The abandoned lineage must no longer be offered as live authority.
|
|
503
|
-
raw = rawStatus(binary, cwd);
|
|
504
|
-
status = await cli.targetStatus({ cwd });
|
|
505
|
-
assertOfferedStepMatchesNative("post-abandon", status, raw);
|
|
506
|
-
if (raw.authority !== null && raw.authority !== undefined) {
|
|
507
|
-
throw new Error(`post-abandon status still reports live authority ${JSON.stringify(raw.authority)}`);
|
|
508
|
-
}
|
|
509
|
-
if (status.nextTransition?.execute?.operation !== "review.start") {
|
|
510
|
-
throw new Error(`post-abandon expected a fresh review.start, got ${summarizeDecoded(status.nextTransition)}`);
|
|
511
|
-
}
|
|
512
|
-
return "adapter-built nine-line v2 binding accepted natively; quarantine record committed; post-abandon status offers only a fresh start";
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// recoveredSuccessorLifecycle reproduces the maintainer's live scenario
|
|
516
|
-
// (2026-08-16, Engram #12461/#12466): a lineage recovered EXTERNALLY through
|
|
517
|
-
// the native CLI, then driven by the Pi controller from STATUS alone.
|
|
518
|
-
// 1. approve a low documentation lineage;
|
|
519
|
-
// 2. change the scope with a code edit (medium risk);
|
|
520
|
-
// 3. native `review recover --disposition scope_changed` with the explicit
|
|
521
|
-
// LF-only gentle-ai.review-recovery-authorization/v1 binding (an ACTIVE
|
|
522
|
-
// reviewing predecessor refuses recovery, so approval comes first);
|
|
523
|
-
// 4. defect A: the controller's dispatch binding must hydrate from the
|
|
524
|
-
// STATUS the controller itself decodes — before that STATUS, dispatch
|
|
525
|
-
// refuses with current-binding-missing;
|
|
526
|
-
// 5. defect B: finalize at reviewer_results_required must surface the
|
|
527
|
-
// provider-offered review.capture-result step, never the correction
|
|
528
|
-
// evidence-first-ordering lane;
|
|
529
|
-
// 6. the drive completes to one really captured lens.
|
|
530
|
-
async function recoveredSuccessorLifecycle(binary, cli, root) {
|
|
531
|
-
const cwd = scratchRepo(root, "pi-recovered");
|
|
532
|
-
write(cwd, "docs/recover-guide.md", "# Recover guide\n\nline one\n");
|
|
533
|
-
write(cwd, "src/mul.js", "export function mul(a, b) {\n return a * b;\n}\n");
|
|
534
|
-
commitAll(cwd, "feat: base");
|
|
535
|
-
write(cwd, "docs/recover-guide.md", "# Recover guide\n\nline one\nline two, purely passive documentation\n");
|
|
536
|
-
|
|
537
|
-
// Low predecessor to approval.
|
|
538
|
-
let raw = rawStatus(binary, cwd);
|
|
539
|
-
let status = await cli.targetStatus({ cwd });
|
|
540
|
-
assertOfferedStepMatchesNative("pre-start", status, raw);
|
|
541
|
-
const start = await cli.start({ cwd, targetIdentity: status.targetIdentity, projection: "workspace" });
|
|
542
|
-
if (start.riskLevel !== "low" || start.state !== "reviewing") throw new Error(`predecessor start risk=${start.riskLevel} state=${start.state}`);
|
|
543
|
-
status = await cli.targetStatus({ cwd });
|
|
544
|
-
if (status.nextTransition?.execute?.operation !== "review.finalize") {
|
|
545
|
-
throw new Error(`expected review.finalize, got ${summarizeDecoded(status.nextTransition)}`);
|
|
546
|
-
}
|
|
547
|
-
const finalize = await cli.finalizeTransition({ cwd, argumentTokens: executeTokens(status.nextTransition) });
|
|
548
|
-
if (finalize.state !== "approved") throw new Error(`predecessor finalize state=${finalize.state}`);
|
|
549
|
-
|
|
550
|
-
// External scope change: code joins the approved documentation change.
|
|
551
|
-
write(cwd, "src/mul.js", "export function mul(a, b) {\n return a * b;\n}\nexport function twice(a) {\n return a + a;\n}\n");
|
|
552
|
-
raw = rawStatus(binary, cwd);
|
|
553
|
-
const successor = "recovered-successor-crosslane";
|
|
554
|
-
const authorization = [
|
|
555
|
-
"gentle-ai.review-recovery-authorization/v1",
|
|
556
|
-
`predecessor_lineage=${finalize.lineageId}`,
|
|
557
|
-
`predecessor_revision=${finalize.storeRevision}`,
|
|
558
|
-
`target_identity=${raw.target_identity}`,
|
|
559
|
-
"actor=cross-lane-battery",
|
|
560
|
-
"reason=scope changed after approval",
|
|
561
|
-
].join("\n");
|
|
562
|
-
rawInvoke(binary, cwd, [
|
|
563
|
-
"review", "recover", "--cwd", cwd,
|
|
564
|
-
"--predecessor-lineage", finalize.lineageId,
|
|
565
|
-
"--expected-predecessor-revision", finalize.storeRevision,
|
|
566
|
-
"--successor-lineage", successor,
|
|
567
|
-
"--disposition", "scope_changed",
|
|
568
|
-
"--actor", "cross-lane-battery",
|
|
569
|
-
"--reason", "scope changed after approval",
|
|
570
|
-
"--maintainer-authorization", authorization,
|
|
571
|
-
]);
|
|
572
|
-
|
|
573
|
-
const registry = new CandidateViewRegistry();
|
|
574
|
-
try {
|
|
575
|
-
// Defect A: before the controller decodes the successor's STATUS, the
|
|
576
|
-
// dispatch registry knows nothing — the refusal is the pre-fix shape.
|
|
577
|
-
let refused = false;
|
|
578
|
-
try {
|
|
579
|
-
injectReviewCandidateView({ agent: "review-reliability", task: "probe", mode: "task" }, registry);
|
|
580
|
-
} catch (error) {
|
|
581
|
-
refused = /no current controller-owned candidate view lineage binding/.test(String(error instanceof Error ? error.message : error));
|
|
582
|
-
}
|
|
583
|
-
if (!refused) throw new Error("pre-STATUS dispatch unexpectedly resolved a binding for the recovered successor");
|
|
584
|
-
await __testing.executeReviewControllerOperation({ operation: "status", lineageId: successor }, cwd, new Map(), cli, undefined, undefined, undefined, registry);
|
|
585
|
-
if (!registry.hasCurrentBinding()) {
|
|
586
|
-
throw new Error("controller STATUS did not hydrate the candidate-view dispatch binding for the recovered successor");
|
|
587
|
-
}
|
|
588
|
-
const dispatch = { agent: "review-reliability", task: "review the recovered successor", mode: "task" };
|
|
589
|
-
injectReviewCandidateView(dispatch, registry);
|
|
590
|
-
if (!dispatch.task.includes(successor)) throw new Error("hydrated dispatch context is not bound to the recovered successor lineage");
|
|
591
|
-
pass(
|
|
592
|
-
"recovered binding: STATUS hydrates controller dispatch",
|
|
593
|
-
"external scope_changed successor driven from STATUS alone: pre-STATUS dispatch refused, post-STATUS dispatch injected the successor candidate context (a controller without STATUS hydration fails here)",
|
|
594
|
-
);
|
|
595
|
-
|
|
596
|
-
// Defect B: finalize must follow the provider transition for reviewer
|
|
597
|
-
// results and never the correction evidence-first-ordering lane. On a
|
|
598
|
-
// provider that admits the pi transport the correct route is the host
|
|
599
|
-
// relay; the pi-subprocess hop is stubbed to keep this check free.
|
|
600
|
-
let relaySlots = 0;
|
|
601
|
-
__testing.setReviewHostRelayRunnerForTesting(async (request) => {
|
|
602
|
-
relaySlots += 1;
|
|
603
|
-
return { promptByteLength: request.captureArgumentTokens.length, resultByteLength: 0, submission: "{}" };
|
|
604
|
-
});
|
|
605
|
-
let finalizeEnvelope;
|
|
606
|
-
try {
|
|
607
|
-
finalizeEnvelope = await __testing.executeReviewControllerOperation({ operation: "finalize", lineageId: successor, input: JSON.stringify({ reviewer_run_acknowledged: true }) }, cwd, new Map(), cli, undefined, undefined, undefined, registry);
|
|
608
|
-
} finally {
|
|
609
|
-
__testing.setReviewHostRelayRunnerForTesting();
|
|
610
|
-
}
|
|
611
|
-
const routedToRelay = relaySlots > 0 && finalizeEnvelope.host_relay?.transport === "pi_host_relay";
|
|
612
|
-
const routedToBlockedCapture = finalizeEnvelope.outcome === "reviewer-results-required"
|
|
613
|
-
&& /capture the reviewer result first/i.test(String(finalizeEnvelope.reason))
|
|
614
|
-
&& finalizeEnvelope.mutation_performed === false;
|
|
615
|
-
if (!routedToRelay && !routedToBlockedCapture) {
|
|
616
|
-
throw new Error(`finalize routed to ${String(finalizeEnvelope.outcome ?? finalizeEnvelope.status)} instead of the provider reviewer-result step`);
|
|
617
|
-
}
|
|
618
|
-
if (JSON.stringify(finalizeEnvelope).includes("evidence-first-ordering")) {
|
|
619
|
-
throw new Error("finalize still leaked the correction evidence-first-ordering lane");
|
|
620
|
-
}
|
|
621
|
-
pass(
|
|
622
|
-
"recovered routing: finalize offers capture-result, never evidence ordering",
|
|
623
|
-
routedToRelay
|
|
624
|
-
? "finalize followed the provider transition into the pi host relay for the outstanding reviewer result. Accepting either provider-correct route (relay when the provider admits the pi transport, blocked capture-result otherwise) is NOT a relaxation of the evidence-ordering guard: this check still fails on any evidence-first-ordering leak in the envelope, and on any route that is neither of the two"
|
|
625
|
-
: "document-free finalize at reviewer_results_required returned the actionable review.capture-result block with zero mutations. Accepting either provider-correct route (relay when the provider admits the pi transport, blocked capture-result otherwise) is NOT a relaxation of the evidence-ordering guard: this check still fails on any evidence-first-ordering leak in the envelope, and on any route that is neither of the two",
|
|
626
|
-
);
|
|
627
|
-
|
|
628
|
-
// Complete the drive to one really captured lens through the exact
|
|
629
|
-
// provider collect input.
|
|
630
|
-
raw = rawStatus(binary, cwd);
|
|
631
|
-
const input = raw.next_transition?.collect?.inputs?.[0];
|
|
632
|
-
if (input?.capture_operation !== "review.capture-result") throw new Error(`expected review.capture-result, got ${summarizeRaw(raw.next_transition)}`);
|
|
633
|
-
const args = rawArgumentValues(input);
|
|
634
|
-
const reviewerFile = join(root, "pi-recovered-reviewer.json");
|
|
635
|
-
writeFileSync(reviewerFile, JSON.stringify({
|
|
636
|
-
subject_hash: args["subject-hash"],
|
|
637
|
-
inspection: { status: "completed", paths: (input.changed_path_manifest ?? []).map((entry) => entry.path) },
|
|
638
|
-
evidence: ["twice(a) returns a + a: pure arithmetic introduced by the candidate hunk with no external effects"],
|
|
639
|
-
findings: [],
|
|
640
|
-
}));
|
|
641
|
-
const artifact = rawInvoke(binary, cwd, [
|
|
642
|
-
"review", "capture-result",
|
|
643
|
-
"--lineage", args.lineage,
|
|
644
|
-
"--expected-revision", args["expected-revision"],
|
|
645
|
-
"--target", args.target,
|
|
646
|
-
"--repository-context", args["repository-context"],
|
|
647
|
-
"--lens", args.lens,
|
|
648
|
-
"--order", args.order,
|
|
649
|
-
"--subject-hash", args["subject-hash"],
|
|
650
|
-
"--input", reviewerFile,
|
|
651
|
-
]);
|
|
652
|
-
if (artifact?.schema !== "gentle-ai.review-result-artifact/v2") throw new Error(`successor lens capture returned schema=${artifact?.schema}`);
|
|
653
|
-
return "low predecessor approved -> native scope_changed recover (explicit v1 binding) -> controller drove the successor from STATUS alone to one captured lens";
|
|
654
|
-
} finally {
|
|
655
|
-
try {
|
|
656
|
-
registry.cleanup(registry.resolveCurrentForLens("review-reliability").token);
|
|
657
|
-
} catch {
|
|
658
|
-
// No hydrated view to clean when the check failed before binding.
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// externallyRecoveredSuccessor performs the shared setup both recovered-
|
|
664
|
-
// lineage checks need: approve a predecessor in `cwd`, change the scope, and
|
|
665
|
-
// create a successor through the EXTERNAL native `review recover` command
|
|
666
|
-
// with its explicit LF-only v1 binding. Returns the successor lineage id.
|
|
667
|
-
async function externallyRecoveredSuccessor(binary, cli, cwd, successor) {
|
|
668
|
-
let raw = rawStatus(binary, cwd);
|
|
669
|
-
let status = await cli.targetStatus({ cwd });
|
|
670
|
-
assertOfferedStepMatchesNative("pre-start", status, raw);
|
|
671
|
-
const start = await cli.start({ cwd, targetIdentity: status.targetIdentity, projection: "workspace" });
|
|
672
|
-
if (start.state !== "reviewing") throw new Error(`predecessor start state=${start.state}`);
|
|
673
|
-
status = await cli.targetStatus({ cwd });
|
|
674
|
-
if (status.nextTransition?.execute?.operation !== "review.finalize") {
|
|
675
|
-
throw new Error(`expected review.finalize, got ${summarizeDecoded(status.nextTransition)}`);
|
|
676
|
-
}
|
|
677
|
-
const finalize = await cli.finalizeTransition({ cwd, argumentTokens: executeTokens(status.nextTransition) });
|
|
678
|
-
if (finalize.state !== "approved") throw new Error(`predecessor finalize state=${finalize.state}`);
|
|
679
|
-
// The caller has already staged its scope change in the worktree.
|
|
680
|
-
write(cwd, "src/mul.js", "export function mul(a, b) {\n return a * b;\n}\nexport function twice(a) {\n return a + a;\n}\n");
|
|
681
|
-
raw = rawStatus(binary, cwd);
|
|
682
|
-
const authorization = [
|
|
683
|
-
"gentle-ai.review-recovery-authorization/v1",
|
|
684
|
-
`predecessor_lineage=${finalize.lineageId}`,
|
|
685
|
-
`predecessor_revision=${finalize.storeRevision}`,
|
|
686
|
-
`target_identity=${raw.target_identity}`,
|
|
687
|
-
"actor=cross-lane-battery",
|
|
688
|
-
"reason=scope changed after approval",
|
|
689
|
-
].join("\n");
|
|
690
|
-
rawInvoke(binary, cwd, [
|
|
691
|
-
"review", "recover", "--cwd", cwd,
|
|
692
|
-
"--predecessor-lineage", finalize.lineageId,
|
|
693
|
-
"--expected-predecessor-revision", finalize.storeRevision,
|
|
694
|
-
"--successor-lineage", successor,
|
|
695
|
-
"--disposition", "scope_changed",
|
|
696
|
-
"--actor", "cross-lane-battery",
|
|
697
|
-
"--reason", "scope changed after approval",
|
|
698
|
-
"--maintainer-authorization", authorization,
|
|
699
|
-
]);
|
|
700
|
-
return successor;
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
// recoveredSuccessorFieldFlow reproduces the FIELD-REPORTED flow (2026-08-16,
|
|
704
|
-
// gentle-pi 402f9f77 + gentle-ai 2.4.0-main): the candidate lives in a LINKED
|
|
705
|
-
// git worktree with UNCOMMITTED tracked modifications, the successor came
|
|
706
|
-
// from an external native recover, and the session drives `finalize` FIRST
|
|
707
|
-
// and dispatches the reviewer straight after — never calling the STATUS
|
|
708
|
-
// controller operation. Hydration wired only into STATUS leaves that flow
|
|
709
|
-
// refusing, which is exactly what the maintainer hit after #340.
|
|
710
|
-
//
|
|
711
|
-
// Residual gap, honestly stated: the battery cannot age a lineage by days or
|
|
712
|
-
// span real OS processes; it reproduces linked-worktree placement, dirty
|
|
713
|
-
// tracked files, external recovery, and a FRESH controller registry (the
|
|
714
|
-
// property a new process actually contributes), not wall-clock age.
|
|
715
|
-
async function recoveredSuccessorFieldFlow(binary, cli, root) {
|
|
716
|
-
const cwd = scratchLinkedWorktree(root, "pi-recovered-linked");
|
|
717
|
-
write(cwd, "docs/recover-guide.md", "# Recover guide\n\nline one\n");
|
|
718
|
-
write(cwd, "src/mul.js", "export function mul(a, b) {\n return a * b;\n}\n");
|
|
719
|
-
commitAll(cwd, "feat: base");
|
|
720
|
-
write(cwd, "docs/recover-guide.md", "# Recover guide\n\nline one\nline two, purely passive documentation\n");
|
|
721
|
-
const successor = await externallyRecoveredSuccessor(binary, cli, cwd, "recovered-linked-successor");
|
|
722
|
-
// The tracked scope change stays UNCOMMITTED, like the reported worktree.
|
|
723
|
-
const dirty = execFileSync("git", ["status", "--porcelain"], { cwd, encoding: "utf8" });
|
|
724
|
-
if (!/^ M src\/mul\.js$/m.test(dirty)) throw new Error(`expected an uncommitted tracked modification, got ${JSON.stringify(dirty)}`);
|
|
725
|
-
|
|
726
|
-
// A brand-new registry stands in for the fresh Pi session.
|
|
727
|
-
const registry = new CandidateViewRegistry();
|
|
728
|
-
try {
|
|
729
|
-
// The pi-subprocess hop is stubbed: this check is about the dispatch
|
|
730
|
-
// binding the finalize-first flow leaves behind, on whichever route the
|
|
731
|
-
// provider offers (host relay when it admits the pi transport).
|
|
732
|
-
__testing.setReviewHostRelayRunnerForTesting(async (request) => ({ promptByteLength: request.captureArgumentTokens.length, resultByteLength: 0, submission: "{}" }));
|
|
733
|
-
let finalizeEnvelope;
|
|
734
|
-
try {
|
|
735
|
-
finalizeEnvelope = await __testing.executeReviewControllerOperation({ operation: "finalize", lineageId: successor, input: JSON.stringify({ reviewer_run_acknowledged: true }) }, cwd, new Map(), cli, undefined, undefined, undefined, registry);
|
|
736
|
-
} finally {
|
|
737
|
-
__testing.setReviewHostRelayRunnerForTesting();
|
|
738
|
-
}
|
|
739
|
-
const routed = finalizeEnvelope.outcome === "reviewer-results-required" || finalizeEnvelope.host_relay?.transport === "pi_host_relay";
|
|
740
|
-
if (!routed) {
|
|
741
|
-
throw new Error(`finalize routed to ${String(finalizeEnvelope.outcome ?? finalizeEnvelope.status)} instead of the provider reviewer-result step`);
|
|
742
|
-
}
|
|
743
|
-
if (JSON.stringify(finalizeEnvelope).includes("evidence-first-ordering")) {
|
|
744
|
-
throw new Error("finalize leaked the correction evidence-first-ordering lane");
|
|
745
|
-
}
|
|
746
|
-
const binding = finalizeEnvelope.dispatch_binding;
|
|
747
|
-
if (binding === undefined) throw new Error("the blocked finalize envelope does not report a dispatch-binding hydration outcome");
|
|
748
|
-
if (binding.hydrated !== true) {
|
|
749
|
-
throw new Error(`finalize reported hydration failure ${String(binding.reason)}: ${String(binding.message)}`);
|
|
750
|
-
}
|
|
751
|
-
if (!registry.hasCurrentBinding()) throw new Error("finalize did not hydrate the candidate-view dispatch binding");
|
|
752
|
-
// The reviewer dispatch the maintainer runs next must resolve.
|
|
753
|
-
const dispatch = { agent: "review-reliability", task: "review the recovered successor", mode: "task" };
|
|
754
|
-
injectReviewCandidateView(dispatch, registry);
|
|
755
|
-
if (!dispatch.task.includes(successor)) throw new Error("hydrated dispatch context is not bound to the recovered successor lineage");
|
|
756
|
-
return "linked worktree + uncommitted tracked changes + external recover: finalize-first (no STATUS call) hydrated the dispatch binding and the reviewer dispatch resolved, on whichever provider-correct route was offered, and the envelope still carries no evidence-first-ordering leak; residual gap: the battery cannot age a lineage by days or span OS processes, only a fresh registry";
|
|
757
|
-
} finally {
|
|
758
|
-
try {
|
|
759
|
-
registry.cleanup(registry.resolveCurrentForLens("review-reliability").token);
|
|
760
|
-
} catch {
|
|
761
|
-
// No hydrated view to clean when the check failed before binding.
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// relayMaterializeSlotLifecycle covers the shape every earlier check missed:
|
|
767
|
-
// the provider's MATERIALIZE-marked host-relay slot (agent=pi,
|
|
768
|
-
// materialize=true, provider submission) on an externally recovered lineage.
|
|
769
|
-
// Measured root cause (third field report): the adapter's negotiated STATUS
|
|
770
|
-
// never named its agent, so the provider only ever returned a bare
|
|
771
|
-
// capture-result input, reviewHostRelaySlots() saw zero slots, and the relay
|
|
772
|
-
// never ran. This check fails on any build that drops `--agent pi`.
|
|
773
|
-
//
|
|
774
|
-
// Residual gap, stated honestly: the locked-down pi subprocess and the final
|
|
775
|
-
// provider submit leg are NOT executed here — those cost model spend. The
|
|
776
|
-
// check drives everything up to and including the REAL materialize leg
|
|
777
|
-
// against the real binary (the provider-issued tokens must actually produce
|
|
778
|
-
// prompt bytes), and stubs only the pi-subprocess hop.
|
|
779
|
-
// correctedLifecycleThroughAdapter drives the shape three field defects in a
|
|
780
|
-
// row escaped through: a MEDIUM candidate taken all the way through detection,
|
|
781
|
-
// bounded correction, evidence and targeted validation to an approved receipt
|
|
782
|
-
// THROUGH THE ADAPTER — not the clean-approval path, and not raw CLI.
|
|
783
|
-
//
|
|
784
|
-
// Field defect it locks down (Engram #12547): after an admitted correction the
|
|
785
|
-
// candidate identity legitimately moves, and a FINALIZE that merely follows the
|
|
786
|
-
// provider's own execute transition carries no documents. That made the
|
|
787
|
-
// adapter resolve the START-time reviewer view and report
|
|
788
|
-
// `candidate-target-projection-drift`, so no corrected lineage could ever reach
|
|
789
|
-
// a receipt through Pi.
|
|
790
|
-
//
|
|
791
|
-
// The controller-driven client is built from lib/*.ts on purpose: the battery's
|
|
792
|
-
// shared `cli` comes from runtime/*.mjs, and mixing that module instance with
|
|
793
|
-
// the extension's lib/*.ts instance breaks `instanceof` across the boundary,
|
|
794
|
-
// which silently turns the consent path into a generic failure.
|
|
795
|
-
async function correctedLifecycleThroughAdapter(binary, root) {
|
|
796
|
-
const { createNativeReviewCli } = await import("../../lib/native-review-cli.ts");
|
|
797
|
-
const cli = createNativeReviewCli(undefined, binary);
|
|
798
|
-
const cwd = scratchRepo(root, "pi-corrected");
|
|
799
|
-
const base = "export function parsePath(input) {\n return input.split(\"/\");\n}\n";
|
|
800
|
-
write(cwd, "src/parse.js", base);
|
|
801
|
-
commitAll(cwd, "feat: parse");
|
|
802
|
-
// The intentional reliability defect: the last component is omitted.
|
|
803
|
-
write(cwd, "src/parse.js", `${base}export function lastComponent(input) {\n const parts = input.split("/");\n return parts[parts.length - 2];\n}\n`);
|
|
804
|
-
|
|
805
|
-
const registry = new CandidateViewRegistry();
|
|
806
|
-
const context = { cwd, hasUI: false, ui: { confirm: async () => true, notify: () => {} } };
|
|
807
|
-
// The real tool is used, not the bare entry point: the pending-consent map
|
|
808
|
-
// lives for the tool's lifetime, so START and ANSWER-CONSENT share it.
|
|
809
|
-
const { createGentleAiExtension } = await import("../../extensions/gentle-ai.ts");
|
|
810
|
-
const tools = new Map();
|
|
811
|
-
createGentleAiExtension({ nativeReviewCli: cli, candidateViews: registry })({
|
|
812
|
-
on() {}, registerTool(definition) { tools.set(definition.name, definition); }, registerCommand() {},
|
|
813
|
-
});
|
|
814
|
-
const tool = tools.get("gentle_review");
|
|
815
|
-
let call = 0;
|
|
816
|
-
const controller = async (parameters) => (await tool.execute(`corrected-${call++}`, parameters, undefined, undefined, context)).details;
|
|
817
|
-
let boundLineageId;
|
|
818
|
-
try {
|
|
819
|
-
// START through the controller so this session holds the START-time
|
|
820
|
-
// immutable reviewer view — the state the defect needs.
|
|
821
|
-
let envelope = await controller({ operation: "start", input: JSON.stringify({ mode: "ordinary" }) });
|
|
822
|
-
if (envelope.consent_binding === undefined) throw new Error(`medium START did not surface consent: ${String(envelope.outcome ?? envelope.status)}`);
|
|
823
|
-
envelope = await controller({ operation: "answer-consent", input: JSON.stringify({ consentBinding: envelope.consent_binding, answer: "granted" }) });
|
|
824
|
-
const lineageId = envelope.result?.lineage_id;
|
|
825
|
-
boundLineageId = lineageId;
|
|
826
|
-
if (envelope.result?.state !== "reviewing" || lineageId === undefined) throw new Error(`granted START state=${String(envelope.result?.state)}`);
|
|
827
|
-
if (!registry.hasCurrentBinding()) throw new Error("START did not bind the immutable reviewer view for this session");
|
|
828
|
-
const startTree = rawStatus(binary, cwd).projection.current_candidate_tree;
|
|
829
|
-
|
|
830
|
-
// Reviewer slot: one deterministic candidate-caused BLOCKER.
|
|
831
|
-
let raw = rawStatus(binary, cwd);
|
|
832
|
-
let slot = raw.next_transition?.collect?.inputs?.[0];
|
|
833
|
-
if (slot?.capture_operation !== "review.capture-result") throw new Error(`expected review.capture-result, got ${summarizeRaw(raw.next_transition)}`);
|
|
834
|
-
let args = rawArgumentValues(slot);
|
|
835
|
-
const reviewerFile = join(root, "pi-corrected-reviewer.json");
|
|
836
|
-
writeFileSync(reviewerFile, JSON.stringify({
|
|
837
|
-
subject_hash: args["subject-hash"],
|
|
838
|
-
inspection: { status: "completed", paths: ["src/parse.js"] },
|
|
839
|
-
evidence: ["lastComponent indexes parts.length - 2 and omits the last component; introduced by the candidate hunk"],
|
|
840
|
-
findings: [{
|
|
841
|
-
claim: "lastComponent returns the second-to-last path component instead of the last",
|
|
842
|
-
severity: "BLOCKER",
|
|
843
|
-
evidence_class: "deterministic",
|
|
844
|
-
causal_disposition: "introduced",
|
|
845
|
-
lens: args.lens,
|
|
846
|
-
location: "src/parse.js:6",
|
|
847
|
-
proof_refs: ["src/parse.js:4-7 indexes parts.length - 2 in the candidate tree"],
|
|
848
|
-
}],
|
|
849
|
-
}));
|
|
850
|
-
rawInvoke(binary, cwd, ["review", "capture-result", "--lineage", args.lineage, "--expected-revision", args["expected-revision"],
|
|
851
|
-
"--target", args.target, "--repository-context", args["repository-context"], "--lens", args.lens,
|
|
852
|
-
"--order", args.order, "--subject-hash", args["subject-hash"], "--input", reviewerFile]);
|
|
853
|
-
|
|
854
|
-
envelope = await controller({ operation: "finalize", lineageId, input: JSON.stringify({}) });
|
|
855
|
-
if (envelope.result?.state !== "correction_required") throw new Error(`finalize after capture state=${String(envelope.result?.state ?? envelope.outcome)}`);
|
|
856
|
-
|
|
857
|
-
// Bounded correction: forecast BEFORE editing, then the edit.
|
|
858
|
-
raw = rawStatus(binary, cwd);
|
|
859
|
-
const planInput = raw.next_transition?.collect?.inputs?.[0];
|
|
860
|
-
if (planInput?.capture_operation !== "external.plan_correction") throw new Error(`expected external.plan_correction, got ${summarizeRaw(raw.next_transition)}`);
|
|
861
|
-
const bounds = planInput.submission?.values?.[0] ?? {};
|
|
862
|
-
envelope = await controller({ operation: "finalize", lineageId, input: JSON.stringify({ correction_line_forecast: bounds.minimum ?? 1 }) });
|
|
863
|
-
if (envelope.result?.state !== "correction_required") throw new Error(`correction forecast rejected: ${JSON.stringify(envelope.diagnostics ?? envelope.outcome)}`);
|
|
864
|
-
write(cwd, "src/parse.js", `${base}export function lastComponent(input) {\n const parts = input.split("/");\n return parts[parts.length - 1];\n}\n`);
|
|
865
|
-
const correctedTree = rawStatus(binary, cwd).projection.current_candidate_tree;
|
|
866
|
-
if (correctedTree === startTree) throw new Error("the correction did not move the candidate identity");
|
|
867
|
-
|
|
868
|
-
// THE REGRESSION PROBE: a FINALIZE that just follows the provider
|
|
869
|
-
// transition carries no documents. Whatever the provider answers is
|
|
870
|
-
// fine; what must never come back is adapter-side reviewer-view drift.
|
|
871
|
-
envelope = await controller({ operation: "finalize", lineageId, input: JSON.stringify({}) });
|
|
872
|
-
if (envelope.diagnostics?.code === "candidate-target-projection-drift") {
|
|
873
|
-
throw new Error("document-free FINALIZE on the corrected candidate still reports candidate-target-projection-drift");
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
// Correction evidence, then targeted validation, to the receipt.
|
|
877
|
-
envelope = await controller({ operation: "finalize", lineageId, input: JSON.stringify({ final_evidence: "node --check src/parse.js passed; lastComponent now returns the final component", final_verification_passed: true }) });
|
|
878
|
-
if (envelope.diagnostics?.code === "candidate-target-projection-drift") throw new Error("evidence FINALIZE reported candidate-target-projection-drift");
|
|
879
|
-
raw = rawStatus(binary, cwd);
|
|
880
|
-
const validationInput = raw.next_transition?.collect?.inputs?.[0];
|
|
881
|
-
if (validationInput?.capture_operation !== "external.run_targeted_validation") {
|
|
882
|
-
return `corrected lineage reached ${String(raw.authority?.state)} through the adapter with no candidate-target-projection-drift at any step; residual gap: this provider renders targeted validation as the Go-owned ${String(validationInput?.capture_operation)} vector, which costs a model run, so the battery stops before the receipt`;
|
|
883
|
-
}
|
|
884
|
-
// The evidence-only FINALIZE above already advanced the provider to
|
|
885
|
-
// targeted validation, so the receipt is completed through the exact
|
|
886
|
-
// provider-rendered submission (the same way the sequencing check does).
|
|
887
|
-
// Re-calling FINALIZE with final_evidence AND validation re-enters
|
|
888
|
-
// evidence capture on this provider; that lane question is pre-existing
|
|
889
|
-
// and out of scope for this defect, and is noted rather than papered over.
|
|
890
|
-
const request = raw.validation_request ?? validationInput.validation_request;
|
|
891
|
-
const validatorFile = join(root, "pi-corrected-validator.json");
|
|
892
|
-
writeFileSync(validatorFile, JSON.stringify({
|
|
893
|
-
targeted_validation_request_hash: request.request_hash,
|
|
894
|
-
correction_target_identity: request.correction_target_identity,
|
|
895
|
-
original_criteria: { passed: true, evidence: ["frozen correction tree returns parts[parts.length - 1]"] },
|
|
896
|
-
correction_regression: { passed: true, evidence: ["parsePath is untouched by the correction diff"] },
|
|
897
|
-
follow_ups: [],
|
|
898
|
-
}));
|
|
899
|
-
const submitted = rawInvoke(binary, root, ["review", validationInput.submission.operation_token,
|
|
900
|
-
...substitute(validationInput.submission.argument_tokens, { value: validatorFile })]);
|
|
901
|
-
const finalState = submitted?.result?.state ?? submitted?.state ?? rawStatus(binary, cwd).authority?.state;
|
|
902
|
-
if (finalState !== "approved") throw new Error(`corrected lineage ended at ${String(finalState)} instead of approved`);
|
|
903
|
-
// The receipt must be the adapter-validatable one for the corrected tree.
|
|
904
|
-
const gate = await controller({ operation: "status", lineageId });
|
|
905
|
-
if (gate.result?.authority?.state !== "approved") throw new Error(`adapter status does not see the approved corrected lineage: ${String(gate.result?.authority?.state)}`);
|
|
906
|
-
return "medium candidate driven through the adapter: BLOCKER detected -> bounded correction -> document-free provider-transition FINALIZE with no candidate-target-projection-drift -> correction evidence -> targeted validation -> approved receipt the adapter can see. Residual gap: the final targeted-validation document is submitted through the exact provider-rendered vector, because a combined evidence+validation FINALIZE re-enters evidence capture on this provider (pre-existing lane question, not this defect)";
|
|
907
|
-
} finally {
|
|
908
|
-
// Candidate views are materialized read-only; leaving one behind makes
|
|
909
|
-
// the battery's own root cleanup fail with EACCES.
|
|
910
|
-
const resolvers = [
|
|
911
|
-
() => registry.resolveCurrentForLens("review-reliability"),
|
|
912
|
-
...(boundLineageId === undefined ? [] : [() => registry.resolveForFinalize(boundLineageId)]),
|
|
913
|
-
];
|
|
914
|
-
for (const resolve of resolvers) {
|
|
915
|
-
try { registry.cleanup(resolve().token); } catch { /* nothing bound to clean */ }
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
async function relayMaterializeSlotLifecycle(binary, cli, root) {
|
|
921
|
-
const cwd = scratchLinkedWorktree(root, "pi-relay-slot");
|
|
922
|
-
write(cwd, "docs/relay-guide.md", "# Relay guide\n\nline one\n");
|
|
923
|
-
write(cwd, "src/mul.js", "export function mul(a, b) {\n return a * b;\n}\n");
|
|
924
|
-
commitAll(cwd, "feat: base");
|
|
925
|
-
write(cwd, "docs/relay-guide.md", "# Relay guide\n\nline one\nline two, purely passive documentation\n");
|
|
926
|
-
const successor = await externallyRecoveredSuccessor(binary, cli, cwd, "relay-materialize-successor");
|
|
927
|
-
|
|
928
|
-
const relayed = [];
|
|
929
|
-
let materializedBytes = 0;
|
|
930
|
-
const registry = new CandidateViewRegistry();
|
|
931
|
-
__testing.setReviewHostRelayRunnerForTesting(async (request) => {
|
|
932
|
-
relayed.push(request);
|
|
933
|
-
// The REAL materialize leg: the provider-issued tokens must produce a
|
|
934
|
-
// non-empty opaque prompt from the real binary. No model spend.
|
|
935
|
-
const prompt = execFileSync(binary, ["review", "capture-result", ...request.captureArgumentTokens], {
|
|
936
|
-
cwd,
|
|
937
|
-
env: gentleAiProcessEnvironment(),
|
|
938
|
-
maxBuffer: 64 * 1024 * 1024,
|
|
939
|
-
});
|
|
940
|
-
materializedBytes = prompt.length;
|
|
941
|
-
if (materializedBytes === 0) throw new Error("provider materialize produced no prompt bytes");
|
|
942
|
-
return { promptByteLength: materializedBytes, resultByteLength: 0, submission: "{}" };
|
|
943
|
-
});
|
|
944
|
-
try {
|
|
945
|
-
const envelope = await __testing.executeReviewControllerOperation(
|
|
946
|
-
{ operation: "finalize", lineageId: successor, input: JSON.stringify({ reviewer_run_acknowledged: true }) },
|
|
947
|
-
cwd, new Map(), cli, undefined, undefined, undefined, registry,
|
|
948
|
-
);
|
|
949
|
-
if (relayed.length !== 1) {
|
|
950
|
-
throw new Error(`the provider materialize slot never reached the host relay (relayed ${relayed.length}); outcome=${String(envelope.outcome ?? envelope.status)}`);
|
|
951
|
-
}
|
|
952
|
-
const tokens = relayed[0].captureArgumentTokens;
|
|
953
|
-
if (!tokens.includes("--agent=pi") || !tokens.includes("--materialize=true")) {
|
|
954
|
-
throw new Error(`relay slot is missing the provider transport tokens: ${tokens.join(" ")}`);
|
|
955
|
-
}
|
|
956
|
-
if (relayed[0].submission?.operationToken !== "capture-result") {
|
|
957
|
-
throw new Error("relay slot carries no provider submission completing form");
|
|
958
|
-
}
|
|
959
|
-
const hostRelay = envelope.host_relay;
|
|
960
|
-
if (hostRelay?.transport !== "pi_host_relay" || hostRelay.captured_slots?.length !== 1) {
|
|
961
|
-
throw new Error(`controller envelope did not report one relayed slot: ${JSON.stringify(hostRelay)}`);
|
|
962
|
-
}
|
|
963
|
-
return `provider offered the materialize slot to the adapter and it reached the relay verbatim (agent=pi, materialize=true, submission=capture-result); the real materialize leg returned ${materializedBytes} prompt bytes. Residual gap: the locked-down pi subprocess and the provider submit leg are not executed here (model spend)`;
|
|
964
|
-
} finally {
|
|
965
|
-
__testing.setReviewHostRelayRunnerForTesting();
|
|
966
|
-
try {
|
|
967
|
-
registry.cleanup(registry.resolveCurrentForLens("review-reliability").token);
|
|
968
|
-
} catch {
|
|
969
|
-
// No hydrated view to clean when the relay lane bound nothing.
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
async function modelReview(binary, cli, cwd) {
|
|
975
|
-
const raw = rawStatus(binary, cwd);
|
|
976
|
-
const input = raw.next_transition?.collect?.inputs?.[0];
|
|
977
|
-
if (input?.capture_operation !== "review.capture-result") {
|
|
978
|
-
throw new Error(`expected review.capture-result, got ${summarizeRaw(raw.next_transition)}`);
|
|
979
|
-
}
|
|
980
|
-
const args = rawArgumentValues(input);
|
|
981
|
-
const artifact = rawInvoke(binary, cwd, [
|
|
982
|
-
"review", "capture-result",
|
|
983
|
-
"--lineage", args.lineage,
|
|
984
|
-
"--expected-revision", args["expected-revision"],
|
|
985
|
-
"--target", args.target,
|
|
986
|
-
"--repository-context", args["repository-context"],
|
|
987
|
-
"--lens", args.lens,
|
|
988
|
-
"--order", args.order,
|
|
989
|
-
"--subject-hash", args["subject-hash"],
|
|
990
|
-
"--agent", "pi",
|
|
991
|
-
]);
|
|
992
|
-
if (artifact?.schema !== "gentle-ai.review-result-artifact/v2") {
|
|
993
|
-
throw new Error(`model capture returned schema=${artifact?.schema}`);
|
|
994
|
-
}
|
|
995
|
-
return "Go-owned locked-down pi reviewer captured a native result artifact";
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
// --- forward-decoder freshness over every captured live envelope ---
|
|
999
|
-
|
|
1000
|
-
function decoderFor(schema, binaryDigest) {
|
|
1001
|
-
switch (schema) {
|
|
1002
|
-
case "gentle-ai.review-integration.status/v3":
|
|
1003
|
-
case "gentle-ai.review-integration.status/v4":
|
|
1004
|
-
case "gentle-ai.review-integration.status/v5":
|
|
1005
|
-
return (body) => decodeReviewStatusV3(body);
|
|
1006
|
-
case "gentle-ai.review-integration.start/v3":
|
|
1007
|
-
return (body) => decodeReviewStartV3(body);
|
|
1008
|
-
case "gentle-ai.review-integration.consent/v3":
|
|
1009
|
-
return (body) => decodeReviewConsentV3(body);
|
|
1010
|
-
case "gentle-ai.review-integration.operation/v2":
|
|
1011
|
-
return (body) => decodeReviewOperationV2(body);
|
|
1012
|
-
case "gentle-ai.review-result-artifact/v2":
|
|
1013
|
-
return (body) => decodeReviewResultArtifactV2(body);
|
|
1014
|
-
case "gentle-ai.review-integration.capabilities/v2":
|
|
1015
|
-
case "gentle-ai.review-integration.capabilities/v2.1":
|
|
1016
|
-
case "gentle-ai.review-integration.capabilities/v2.2":
|
|
1017
|
-
return (body) => decodeReviewCapabilitiesV2(body, binaryDigest);
|
|
1018
|
-
default:
|
|
1019
|
-
return undefined;
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
function decoderFreshness(binary) {
|
|
1024
|
-
const digest = `sha256:${createHash("sha256").update(readFileSync(binary)).digest("hex")}`;
|
|
1025
|
-
const outcomes = new Map();
|
|
1026
|
-
for (const envelope of rawEnvelopes) {
|
|
1027
|
-
const state = outcomes.get(envelope.schema) ?? { total: 0, failures: [] };
|
|
1028
|
-
state.total += 1;
|
|
1029
|
-
const decoder = decoderFor(envelope.schema, digest);
|
|
1030
|
-
if (decoder === undefined) {
|
|
1031
|
-
state.failures.push(`no forward decoder maps ${envelope.schema} (${envelope.source})`);
|
|
1032
|
-
} else {
|
|
1033
|
-
try {
|
|
1034
|
-
decoder(envelope.body);
|
|
1035
|
-
} catch (error) {
|
|
1036
|
-
state.failures.push(`${envelope.source}: ${describeError(error)}`);
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
outcomes.set(envelope.schema, state);
|
|
1040
|
-
}
|
|
1041
|
-
for (const schema of [...outcomes.keys()].sort()) {
|
|
1042
|
-
const state = outcomes.get(schema);
|
|
1043
|
-
const unique = [...new Set(state.failures)].slice(0, 3);
|
|
1044
|
-
if (state.failures.length === 0) {
|
|
1045
|
-
pass(`decoder freshness: ${schema}`, `${state.total} live envelope(s) decoded without unknown-key rejection`);
|
|
1046
|
-
} else {
|
|
1047
|
-
fail(`decoder freshness: ${schema}`, `${state.failures.length}/${state.total} rejected: ${unique.join(" | ")}`);
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
// Candidate views are materialized read-only, so a leaked one makes a plain
|
|
1053
|
-
// rmSync fail with EACCES — and losing the battery's results table to a scratch
|
|
1054
|
-
// permission is worse than leaving bytes in /tmp. Make everything writable
|
|
1055
|
-
// first, then remove, and never let cleanup mask the run's outcome.
|
|
1056
|
-
function removeScratchRoot(root) {
|
|
1057
|
-
const makeWritable = (path) => {
|
|
1058
|
-
let entry;
|
|
1059
|
-
try { entry = statSync(path); } catch { return; }
|
|
1060
|
-
try { chmodSync(path, entry.isDirectory() ? 0o700 : 0o600); } catch { /* best effort */ }
|
|
1061
|
-
if (!entry.isDirectory()) return;
|
|
1062
|
-
let children = [];
|
|
1063
|
-
try { children = readdirSync(path); } catch { return; }
|
|
1064
|
-
for (const child of children) makeWritable(join(path, child));
|
|
1065
|
-
};
|
|
1066
|
-
makeWritable(root);
|
|
1067
|
-
try {
|
|
1068
|
-
rmSync(root, { recursive: true, force: true });
|
|
1069
|
-
} catch (error) {
|
|
1070
|
-
console.log(`note: scratch root ${root} could not be fully removed (${error.code ?? "unknown"}); results below are unaffected`);
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
// --- driver ---
|
|
1075
|
-
|
|
1076
|
-
async function main() {
|
|
1077
|
-
const binary = resolveBatteryBinary();
|
|
1078
|
-
console.log(`cross-lane battery (Pi direct lane)`);
|
|
1079
|
-
console.log(`binary: ${binary}`);
|
|
1080
|
-
const root = mkdtempSync(join(tmpdir(), "gentle-pi-crosslane-"));
|
|
1081
|
-
const cli = createNativeReviewCli(undefined, binary);
|
|
1082
|
-
try {
|
|
1083
|
-
// Capture one live capabilities envelope for the freshness lane.
|
|
1084
|
-
rawInvoke(binary, root, ["review", "capabilities", "--contract", CONTRACT]);
|
|
1085
|
-
|
|
1086
|
-
try {
|
|
1087
|
-
pass("low lifecycle to gate allow", await lowLifecycle(binary, cli, root));
|
|
1088
|
-
} catch (error) {
|
|
1089
|
-
fail("low lifecycle to gate allow", knownRedParity(describeError(error)));
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
let mediumRepo;
|
|
1093
|
-
try {
|
|
1094
|
-
const outcome = await mediumConsent(binary, cli, root);
|
|
1095
|
-
mediumRepo = outcome.cwd;
|
|
1096
|
-
pass("medium consent/v3 granted round-trip", outcome.note);
|
|
1097
|
-
} catch (error) {
|
|
1098
|
-
fail("medium consent/v3 granted round-trip", describeError(error));
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
try {
|
|
1102
|
-
pass("sequencing lifecycle to approved receipt", await sequencingLifecycle(binary, cli, root));
|
|
1103
|
-
} catch (error) {
|
|
1104
|
-
const message = describeError(error);
|
|
1105
|
-
const name = message.startsWith(KNOWN_RED_SEQUENCING)
|
|
1106
|
-
? "sequencing: evidence collected before targeted validation"
|
|
1107
|
-
: "sequencing lifecycle to approved receipt";
|
|
1108
|
-
fail(name, knownRedParity(message));
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
try {
|
|
1112
|
-
pass("audited abandon end-to-end (v2 discarded-work binding)", await abandonLifecycle(binary, cli, root));
|
|
1113
|
-
} catch (error) {
|
|
1114
|
-
fail("audited abandon end-to-end (v2 discarded-work binding)", knownRedParity(describeError(error)));
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
try {
|
|
1118
|
-
pass("external native recover to captured successor lens", await recoveredSuccessorLifecycle(binary, cli, root));
|
|
1119
|
-
} catch (error) {
|
|
1120
|
-
fail("external native recover to captured successor lens", knownRedParity(describeError(error)));
|
|
1121
|
-
}
|
|
1122
|
-
|
|
1123
|
-
try {
|
|
1124
|
-
pass("recovered field flow: linked dirty worktree, finalize-first dispatch", await recoveredSuccessorFieldFlow(binary, cli, root));
|
|
1125
|
-
} catch (error) {
|
|
1126
|
-
fail("recovered field flow: linked dirty worktree, finalize-first dispatch", knownRedParity(describeError(error)));
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
try {
|
|
1130
|
-
pass("relay materialize slot on an externally recovered lineage", await relayMaterializeSlotLifecycle(binary, cli, root));
|
|
1131
|
-
} catch (error) {
|
|
1132
|
-
fail("relay materialize slot on an externally recovered lineage", knownRedParity(describeError(error)));
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
try {
|
|
1136
|
-
pass("corrected lifecycle through the adapter to an approved receipt", await correctedLifecycleThroughAdapter(binary, root));
|
|
1137
|
-
} catch (error) {
|
|
1138
|
-
fail("corrected lifecycle through the adapter to an approved receipt", knownRedParity(describeError(error)));
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
if (WITH_MODEL && mediumRepo !== undefined) {
|
|
1142
|
-
try {
|
|
1143
|
-
pass("medium reviewer model run (pi)", await modelReview(binary, cli, mediumRepo));
|
|
1144
|
-
} catch (error) {
|
|
1145
|
-
fail("medium reviewer model run (pi)", describeError(error));
|
|
1146
|
-
}
|
|
1147
|
-
} else {
|
|
1148
|
-
skip("medium reviewer model run (pi)", "pass --with-model to run the Go-owned pi reviewer (model spend)");
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
decoderFreshness(binary);
|
|
1152
|
-
} finally {
|
|
1153
|
-
removeScratchRoot(root);
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
const nameWidth = Math.max(...checks.map((check) => check.name.length), "check".length);
|
|
1157
|
-
console.log("");
|
|
1158
|
-
console.log(`${"check".padEnd(nameWidth)} status note`);
|
|
1159
|
-
let failed = 0;
|
|
1160
|
-
for (const check of checks) {
|
|
1161
|
-
console.log(`${check.name.padEnd(nameWidth)} ${check.status.padEnd(6)} ${check.note}`);
|
|
1162
|
-
if (check.status === "FAIL") failed += 1;
|
|
1163
|
-
}
|
|
1164
|
-
console.log("");
|
|
1165
|
-
console.log(`total: ${checks.length} checks, ${failed} failed`);
|
|
1166
|
-
if (failed > 0) process.exitCode = 1;
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
await main();
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { decodeReviewLastEventClosureV1 } from "../../runtime/review-integration-v2.mjs";
|
|
6
|
+
|
|
7
|
+
const ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
8
|
+
const fixturePath = join(ROOT, "tests", "fixtures", "devbinary", "last-event-capture-result-approved.captured.json");
|
|
9
|
+
const fixture = JSON.parse(await readFile(fixturePath, "utf8"));
|
|
10
|
+
const closure = decodeReviewLastEventClosureV1(fixture);
|
|
11
|
+
|
|
12
|
+
assert.equal(closure.operation, "review/capture-result");
|
|
13
|
+
assert.equal(closure.state, "approved");
|
|
14
|
+
assert.match(closure.storeRevision, /^sha256:[a-f0-9]{64}$/);
|
|
15
|
+
assert.notEqual(closure.operation, "review.finalize");
|
|
16
|
+
console.log("cross-lane last-event closure parity passed");
|