gentle-pi 1.2.0 → 2.0.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 +40 -9
- package/assets/agents/review-readability.md +0 -1
- package/assets/agents/review-reliability.md +0 -1
- package/assets/agents/review-resilience.md +0 -1
- package/assets/agents/review-risk.md +0 -1
- package/contracts/review-integration/v1/fixtures/binding-revision-conflict.fixture.json +25 -0
- package/contracts/review-integration/v1/fixtures/capabilities-v1.2.fixture.json +263 -0
- package/contracts/review-integration/v1/fixtures/capabilities-v1.3.fixture.json +301 -0
- package/contracts/review-integration/v1/fixtures/capabilities-v1.4.fixture.json +312 -0
- package/contracts/review-integration/v1/fixtures/capabilities-v1.5.fixture.json +321 -0
- package/contracts/review-integration/v1/fixtures/consent.fixture.json +36 -0
- package/contracts/review-integration/v1/fixtures/final-verification-incident.fixture.json +1 -0
- package/contracts/review-integration/v1/fixtures/operation.fixture.json +4 -0
- package/contracts/review-integration/v1/fixtures/repair-preflight.fixture.json +54 -0
- package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +97 -0
- package/contracts/review-integration/v1/fixtures/start.fixture.json +25 -1
- package/contracts/review-integration/v1/fixtures/status-ambiguous.fixture.json +4 -0
- package/contracts/review-integration/v1/fixtures/status-corrupted.fixture.json +4 -0
- package/contracts/review-integration/v1/fixtures/status-recover.fixture.json +4 -0
- package/contracts/review-integration/v1/fixtures/status-unrelated.fixture.json +4 -0
- package/contracts/review-integration/v1/fixtures/status-v2-ambiguous.fixture.json +110 -0
- package/contracts/review-integration/v1/fixtures/status-v2-corrupted.fixture.json +107 -0
- package/contracts/review-integration/v1/fixtures/status-v2-final-verification-retry.fixture.json +192 -0
- package/contracts/review-integration/v1/fixtures/status-v2-recover.fixture.json +130 -0
- package/contracts/review-integration/v1/fixtures/status-v2-repair.fixture.json +161 -0
- package/contracts/review-integration/v1/fixtures/status-v2-unrelated.fixture.json +103 -0
- package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +193 -0
- package/contracts/review-integration/v1/fixtures/status.fixture.json +19 -5
- package/contracts/review-integration/v1/fixtures/verification-evidence.fixture.json +1 -0
- package/contracts/review-integration/v1/schemas/admitted-result.schema.json +35 -0
- package/contracts/review-integration/v1/schemas/artifact-subject.schema.json +33 -0
- package/contracts/review-integration/v1/schemas/authority-repair-assessment.schema.json +247 -0
- package/contracts/review-integration/v1/schemas/capabilities-v1.2.schema.json +249 -0
- package/contracts/review-integration/v1/schemas/capabilities-v1.3.schema.json +429 -0
- package/contracts/review-integration/v1/schemas/capabilities-v1.4.schema.json +432 -0
- package/contracts/review-integration/v1/schemas/capabilities-v1.5.schema.json +154 -0
- package/contracts/review-integration/v1/schemas/consent.schema.json +86 -0
- package/contracts/review-integration/v1/schemas/failure.schema.json +54 -8
- package/contracts/review-integration/v1/schemas/final-verification-incident.schema.json +51 -0
- package/contracts/review-integration/v1/schemas/operation.schema.json +204 -31
- package/contracts/review-integration/v1/schemas/repair.schema.json +251 -0
- package/contracts/review-integration/v1/schemas/result-artifact-v2.schema.json +38 -0
- package/contracts/review-integration/v1/schemas/start-v2.schema.json +216 -0
- package/contracts/review-integration/v1/schemas/start.schema.json +79 -3
- package/contracts/review-integration/v1/schemas/status-v2.schema.json +313 -0
- package/contracts/review-integration/v1/schemas/status.schema.json +37 -10
- package/contracts/review-integration/v1/schemas/targeted-validation-request.schema.json +46 -0
- package/contracts/review-integration/v1/schemas/verification-evidence.schema.json +64 -0
- package/contracts/review-integration/v2/fixtures/capabilities.fixture.json +111 -0
- package/contracts/review-integration/v2/fixtures/consent.fixture.json +36 -0
- package/contracts/review-integration/v2/fixtures/start.fixture.json +93 -0
- package/contracts/review-integration/v2/fixtures/status.fixture.json +113 -0
- package/contracts/review-integration/v2/schemas/admitted-result.schema.json +14 -0
- package/contracts/review-integration/v2/schemas/artifact-subject.schema.json +36 -0
- package/contracts/review-integration/v2/schemas/capabilities.schema.json +111 -0
- package/contracts/review-integration/v2/schemas/consent.schema.json +70 -0
- package/contracts/review-integration/v2/schemas/failure.schema.json +35 -0
- package/contracts/review-integration/v2/schemas/operation.schema.json +46 -0
- package/contracts/review-integration/v2/schemas/repair.schema.json +22 -0
- package/contracts/review-integration/v2/schemas/start.schema.json +106 -0
- package/contracts/review-integration/v2/schemas/status.schema.json +104 -0
- package/docs/native-authority-architecture.md +18 -3
- package/docs/review-integration.md +175 -27
- package/extensions/gentle-ai.ts +414 -19
- package/lib/gentle-ai-binary.ts +1 -1
- package/lib/native-review-cli.ts +679 -57
- package/lib/review-candidate-view.ts +131 -1
- package/lib/review-compact-contract.ts +12 -3
- package/lib/review-consent-latch.ts +51 -0
- package/lib/review-correction-lifecycle.ts +170 -0
- package/lib/review-integration-v2.ts +1563 -0
- package/package.json +15 -11
- package/runtime/gentle-ai-binary.mjs +1 -1
- package/runtime/native-review-cli.mjs +663 -41
- package/runtime/review-integration-v2.mjs +1564 -0
- package/scripts/build-git-commit-transaction-runner.mjs +1 -1
- package/scripts/gentle-ai-installer.mjs +16 -11
- package/scripts/install-gentle-ai.mjs +7 -3
- package/scripts/test-packed-runner.mjs +2 -2
- package/scripts/verify-package-files.mjs +216 -64
- package/skills/release/SKILL.md +38 -11
- package/tests/devbinary/native-review-parity.devtest.ts +348 -0
- package/tests/gentle-ai-binary.test.ts +8 -1
- package/tests/gentle-ai-installer.test.ts +24 -21
- package/tests/native-binary-gate.test.ts +145 -0
- package/tests/native-review-capability-contract.test.ts +87 -0
- package/tests/native-review-cli.test.ts +44 -1
- package/tests/native-review-parity-runtime.test.ts +70 -23
- package/tests/native-review-parity.test.ts +673 -0
- package/tests/native-risk-evidence-parity.test.ts +91 -0
- package/tests/package-manifest.test.ts +72 -7
- package/tests/review-actor-tool-deny.test.ts +20 -5
- package/tests/review-candidate-view.test.ts +331 -1
- package/tests/review-compact-contract.test.ts +5 -0
- package/tests/review-consent-latch.test.ts +67 -0
- package/tests/review-controller-native-recovery.test.ts +180 -5
- package/tests/review-controller-native-routing.test.ts +385 -12
- package/tests/review-controller-workspace-root.test.ts +26 -6
- package/tests/review-correction-lifecycle.test.ts +174 -0
- package/tests/review-integration-v2.test.ts +539 -0
- package/tests/runtime-harness.mjs +187 -0
- package/tests/support/native-binary-gate.ts +86 -0
- package/tests/verify-package-files.test.ts +119 -0
- package/lib/review-integration-v1.ts +0 -795
- package/runtime/review-integration-v1.mjs +0 -796
- package/tests/native-review-integration-v1.test.ts +0 -102
- package/tests/review-integration-v1.test.ts +0 -530
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { NATIVE_CLI_CONTRACTS } from "../lib/native-review-cli.ts";
|
|
4
|
+
|
|
5
|
+
// Organic RDD parity: the four organic-parity capability columns exist on every
|
|
6
|
+
// shipped NATIVE_CLI_CONTRACTS row. Every row through the pinned "2.1.11"
|
|
7
|
+
// reports them false. "2.2.0" is the first capability-true row and it lights
|
|
8
|
+
// only what was demonstrated to reach a negotiated consumer, per Design
|
|
9
|
+
// Decision #1 ("Future row policy").
|
|
10
|
+
//
|
|
11
|
+
// A row describes what the NATIVE CLI puts on the wire, never what Pi manages
|
|
12
|
+
// to show. `riskEvidence` and `hint` stay false on 2.2.0 even though a Pi
|
|
13
|
+
// operator now sees both, because Pi derives them from `risk_reasons` and
|
|
14
|
+
// `changed_files` rather than receiving them: the closed `start/v2` schema
|
|
15
|
+
// omits the fields. Lighting the row would tell a future reader the envelope
|
|
16
|
+
// carries something it does not.
|
|
17
|
+
|
|
18
|
+
const ORGANIC_PARITY_CAPABILITIES = ["mode", "riskEvidence", "hint", "delivery"] as const;
|
|
19
|
+
const DARK_VERSIONS = ["2.1.4", "2.1.5", "2.1.6", "2.1.7", "2.1.8", "2.1.9", "2.1.10", "2.1.11"] as const;
|
|
20
|
+
|
|
21
|
+
test("every row through the pinned 2.1.11 reports the organic-parity capability columns false", () => {
|
|
22
|
+
for (const version of DARK_VERSIONS) {
|
|
23
|
+
const contract = NATIVE_CLI_CONTRACTS[version] as Record<string, boolean>;
|
|
24
|
+
for (const capability of ORGANIC_PARITY_CAPABILITIES) {
|
|
25
|
+
assert.equal(
|
|
26
|
+
Object.hasOwn(contract, capability) ? contract[capability] : false,
|
|
27
|
+
false,
|
|
28
|
+
`${version}.${capability} must be false or absent`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("the 2.1.11 row explicitly reports all four organic-parity capabilities false", () => {
|
|
35
|
+
const contract = NATIVE_CLI_CONTRACTS["2.1.11"] as Record<string, boolean>;
|
|
36
|
+
assert.equal(contract.mode, false);
|
|
37
|
+
assert.equal(contract.riskEvidence, false);
|
|
38
|
+
assert.equal(contract.hint, false);
|
|
39
|
+
assert.equal(contract.delivery, false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("2.2.0 lights only the two capabilities its negotiated envelope demonstrably carries", () => {
|
|
43
|
+
// Ground-truthed against the released v2.2.0 binary: the kill switch and
|
|
44
|
+
// delivery disposition reach the negotiated path, while `risk_evidence` and
|
|
45
|
+
// `hint` appear only in the plain envelope. See
|
|
46
|
+
// tests/native-risk-evidence-parity.test.ts for the captured evidence.
|
|
47
|
+
const contract = NATIVE_CLI_CONTRACTS["2.2.0"] as Record<string, boolean>;
|
|
48
|
+
assert.equal(contract.mode, true);
|
|
49
|
+
assert.equal(contract.delivery, true);
|
|
50
|
+
assert.equal(contract.riskEvidence, false);
|
|
51
|
+
assert.equal(contract.hint, false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("2.2.1 repeats 2.2.0 because the lane Pi speaks did not move", () => {
|
|
55
|
+
// Ground-truthed against the released v2.2.1 binary. On
|
|
56
|
+
// review-integration/v1 it advertises capabilities/v1.5 (protocol minor 5),
|
|
57
|
+
// but the negotiated start envelope is still the closed `start/v2`, so
|
|
58
|
+
// `risk_evidence` and `hint` still cannot arrive. v2.2.1 does publish a
|
|
59
|
+
// second contract, review-integration/v2, whose `start/v3` carries frozen
|
|
60
|
+
// base/candidate trees -- but Pi does not negotiate it yet, and a row
|
|
61
|
+
// describes the lane in use, not the lane available.
|
|
62
|
+
const contract = NATIVE_CLI_CONTRACTS["2.2.1"] as Record<string, boolean>;
|
|
63
|
+
assert.equal(contract.mode, true);
|
|
64
|
+
assert.equal(contract.delivery, true);
|
|
65
|
+
assert.equal(contract.riskEvidence, false);
|
|
66
|
+
assert.equal(contract.hint, false);
|
|
67
|
+
assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["2.2.0"] as Record<string, boolean>);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("2.2.2 repeats 2.2.1 because the lane Pi speaks still did not move", () => {
|
|
71
|
+
// Ground-truthed against the released v2.2.2 binary: on
|
|
72
|
+
// review-integration/v1 it advertises capabilities/v1.5 and the negotiated
|
|
73
|
+
// start envelope is still the closed `start/v2`, so risk_evidence and hint
|
|
74
|
+
// still cannot arrive on the lane Pi negotiates.
|
|
75
|
+
const contract = NATIVE_CLI_CONTRACTS["2.2.2"] as Record<string, boolean>;
|
|
76
|
+
assert.equal(contract.mode, true);
|
|
77
|
+
assert.equal(contract.delivery, true);
|
|
78
|
+
assert.equal(contract.riskEvidence, false);
|
|
79
|
+
assert.equal(contract.hint, false);
|
|
80
|
+
assert.deepEqual(contract, NATIVE_CLI_CONTRACTS["2.2.1"] as Record<string, boolean>);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("no shipped version key was added beyond the pin bump", () => {
|
|
84
|
+
// Rows are promises to consumers, so a new key only ever appears in a
|
|
85
|
+
// dedicated commit alongside a pin bump, never as a side effect.
|
|
86
|
+
assert.deepEqual(Object.keys(NATIVE_CLI_CONTRACTS), [...DARK_VERSIONS, "2.2.0", "2.2.1", "2.2.2"]);
|
|
87
|
+
});
|
|
@@ -676,7 +676,7 @@ test("native review status decodes 2.1.8 released lock residue and keeps unknown
|
|
|
676
676
|
// schema-incompatible on every repository that ever completed a review.
|
|
677
677
|
//
|
|
678
678
|
// Lock status stays a CLOSED enum extended by exactly `released` — unlike
|
|
679
|
-
// the cause_category widening in review-integration-
|
|
679
|
+
// the cause_category widening in review-integration-v2 (diagnostic
|
|
680
680
|
// metadata nothing routes on), lock status routes blocking behavior in the
|
|
681
681
|
// controller, so an unknown future status must keep failing closed rather
|
|
682
682
|
// than being silently classified as blocking or non-blocking.
|
|
@@ -829,6 +829,49 @@ test("native SDD readiness requires an unblocked post-review action with publish
|
|
|
829
829
|
}
|
|
830
830
|
});
|
|
831
831
|
|
|
832
|
+
// Phase 13.13 (migrate-review-integration-v2): with the kill switch off,
|
|
833
|
+
// gentle-ai's gate result can never become "allow" (delivery follows ordinary
|
|
834
|
+
// repository policy instead), so gating `ready` on result==="allow" alone
|
|
835
|
+
// deadlocked readiness forever. `delivery` also arrived on the reviewGate
|
|
836
|
+
// struct itself (json:"delivery,omitempty") without a corresponding optional
|
|
837
|
+
// key on the decoder, so that payload was previously rejected outright rather
|
|
838
|
+
// than merely read as not-ready.
|
|
839
|
+
test("native SDD readiness unblocks on disabled/unmanaged delivery instead of deadlocking on a permanently non-allow gate result", async () => {
|
|
840
|
+
const openspec = JSON.parse(await fixture("sdd-status")) as Record<string, unknown>;
|
|
841
|
+
|
|
842
|
+
for (const nextRecommended of ["verify", "archive"] as const) {
|
|
843
|
+
const body = {
|
|
844
|
+
...openspec,
|
|
845
|
+
nextRecommended,
|
|
846
|
+
blockedReasons: [],
|
|
847
|
+
reviewGate: { result: "invalidated", reason: "review-driven development is disabled and no receipt governs this candidate, so delivery follows ordinary repository policy", delivery: "disabled/unmanaged" },
|
|
848
|
+
};
|
|
849
|
+
const queue = queuedAdapter([VERSION, { stdout: JSON.stringify(body) }]);
|
|
850
|
+
const status = await new NativeReviewCliV213(queue.adapter).sddStatus({ cwd: "/repo", change: "native-review-authority-parity" });
|
|
851
|
+
assert.equal(status.ready, true, `${nextRecommended} must be ready under disabled/unmanaged delivery even though result is not "allow"`);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Blocked reasons still take priority over a legitimate unmanaged delivery.
|
|
855
|
+
const stillBlocked = {
|
|
856
|
+
...openspec, nextRecommended: "verify", blockedReasons: ["stale authority"],
|
|
857
|
+
reviewGate: { result: "invalidated", reason: "delivery follows ordinary repository policy", delivery: "disabled/unmanaged" },
|
|
858
|
+
};
|
|
859
|
+
const blockedQueue = queuedAdapter([VERSION, { stdout: JSON.stringify(stillBlocked) }]);
|
|
860
|
+
assert.equal((await new NativeReviewCliV213(blockedQueue.adapter).sddStatus({ cwd: "/repo", change: "native-review-authority-parity" })).ready, false);
|
|
861
|
+
|
|
862
|
+
// An unrecognized delivery value must still fail closed (exact enum, not a
|
|
863
|
+
// free string).
|
|
864
|
+
const unknownDelivery = {
|
|
865
|
+
...openspec, nextRecommended: "verify", blockedReasons: [],
|
|
866
|
+
reviewGate: { result: "invalidated", reason: "delivery follows ordinary repository policy", delivery: "unmanaged" },
|
|
867
|
+
};
|
|
868
|
+
const unknownQueue = queuedAdapter([VERSION, { stdout: JSON.stringify(unknownDelivery) }]);
|
|
869
|
+
await assert.rejects(
|
|
870
|
+
() => new NativeReviewCliV213(unknownQueue.adapter).sddStatus({ cwd: "/repo", change: "native-review-authority-parity" }),
|
|
871
|
+
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE,
|
|
872
|
+
);
|
|
873
|
+
});
|
|
874
|
+
|
|
832
875
|
test("native client decodes the exact v2.1.3 Engram artifact map", async () => {
|
|
833
876
|
const queue = queuedAdapter([VERSION, { stdout: await fixture("sdd-status-engram") }]);
|
|
834
877
|
const status = await new NativeReviewCliV213(queue.adapter).sddStatus({ cwd: "/repo", change: "native-review-authority-parity" });
|
|
@@ -14,6 +14,7 @@ import { NativeReviewCliV216 } from "../lib/native-review-cli.ts";
|
|
|
14
14
|
import { NativeReviewCliV216 as RuntimeNativeReviewCliV216 } from "../runtime/native-review-cli.mjs";
|
|
15
15
|
import { CandidateViewRegistry } from "../lib/review-candidate-view.ts";
|
|
16
16
|
import { resolveGentleAiReleaseAsset } from "../scripts/gentle-ai-installer.mjs";
|
|
17
|
+
import { requireNativeBinary } from "./support/native-binary-gate.ts";
|
|
17
18
|
|
|
18
19
|
const execFileAsync = promisify(execFile);
|
|
19
20
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -27,7 +28,9 @@ const resolvedBinary = (() => {
|
|
|
27
28
|
return undefined;
|
|
28
29
|
}
|
|
29
30
|
})();
|
|
30
|
-
const
|
|
31
|
+
const nativeBinaryGate = requireNativeBinary({ resolvedBinary, digestsPinned: true, env: process.env });
|
|
32
|
+
if (!nativeBinaryGate.run) console.log(`native-review-parity-runtime: ${nativeBinaryGate.reason}`);
|
|
33
|
+
const test = nativeBinaryGate.run ? baseTest : baseTest.skip;
|
|
31
34
|
const binary = resolvedBinary ?? "";
|
|
32
35
|
const OFFICIAL_BINARY_SHA256 = resolveGentleAiReleaseAsset(process.platform, process.arch).binarySha256;
|
|
33
36
|
const REVIEWED_PATHS = ["tracked.txt", "initially-untracked.txt"] as const;
|
|
@@ -153,16 +156,51 @@ function authorityInventory(status: Record<string, unknown>): ReviewAuthorityEnt
|
|
|
153
156
|
});
|
|
154
157
|
}
|
|
155
158
|
|
|
159
|
+
// Every reviewer result reaches authority through `review capture-result`.
|
|
160
|
+
// `review finalize --result` was removed because a result handed over that way
|
|
161
|
+
// carries no provider-owned admission, so it cannot prove the lens inspected
|
|
162
|
+
// the frozen candidate. The bindings are never guessed here: each one comes
|
|
163
|
+
// from the collect transition the provider itself publishes, which is also
|
|
164
|
+
// what the removal message tells a caller to do.
|
|
165
|
+
//
|
|
166
|
+
// Artifacts stay outside the repository on purpose. Writing them inside changes
|
|
167
|
+
// the workspace snapshot between deriving a target and using it, and authority
|
|
168
|
+
// correctly rejects the stale target that results.
|
|
169
|
+
async function nextTransition(repository: string): Promise<Record<string, unknown>> {
|
|
170
|
+
const status = JSON.parse((await run(binary, ["review", "status", "--contract", "gentle-ai.review-integration/v2", "--cwd", repository, "--projection", "workspace", "--next-transition"], repository)).stdout) as Record<string, unknown>;
|
|
171
|
+
return status.next_transition as Record<string, unknown>;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function captureSelectedLenses(repository: string, artifacts: string, evidenceName: string): Promise<void> {
|
|
175
|
+
for (let guard = 0; guard < 8; guard += 1) {
|
|
176
|
+
const transition = await nextTransition(repository);
|
|
177
|
+
const collect = transition.collect as { inputs?: readonly Record<string, unknown>[] } | undefined;
|
|
178
|
+
if (transition.kind !== "collect" || collect?.inputs === undefined) return;
|
|
179
|
+
for (const [index, input] of collect.inputs.entries()) {
|
|
180
|
+
const subject = input.artifact_subject as { subject_hash: string };
|
|
181
|
+
const manifest = input.changed_path_manifest as readonly { path: string }[];
|
|
182
|
+
const result = join(artifacts, `${evidenceName}-capture-${index}.json`);
|
|
183
|
+
await writeFile(result, JSON.stringify({
|
|
184
|
+
subject_hash: subject.subject_hash,
|
|
185
|
+
inspection: { status: "completed", paths: manifest.map((entry) => entry.path) },
|
|
186
|
+
findings: [],
|
|
187
|
+
evidence: ["reviewed the complete frozen candidate scope"],
|
|
188
|
+
}));
|
|
189
|
+
// The published tokens already carry the repository context, and
|
|
190
|
+
// capture-result accepts that or --cwd, never both.
|
|
191
|
+
const tokens = (input.arguments as readonly { token: string }[]).map((argument) => argument.token);
|
|
192
|
+
const captured = await run(binary, ["review", "capture-result", ...tokens, "--input", result], repository);
|
|
193
|
+
assert.equal(captured.exitCode, 0, `capture-result failed: ${captured.stderr}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
assert.fail("collect transitions did not converge");
|
|
197
|
+
}
|
|
198
|
+
|
|
156
199
|
async function finalizeEmptyReview(repository: string, artifacts: string, started: ReviewStart, evidenceName: string): Promise<CommandResult> {
|
|
157
200
|
const evidence = join(artifacts, evidenceName);
|
|
158
201
|
await writeFile(evidence, `evidence for ${started.lineage_id}\n`);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const result = join(artifacts, `${evidenceName}-lens-${index}.json`);
|
|
162
|
-
await writeFile(result, JSON.stringify({ findings: [], evidence: ["reviewed frozen candidate"] }));
|
|
163
|
-
resultFiles.push(result);
|
|
164
|
-
}
|
|
165
|
-
return run(binary, ["review", "finalize", "--cwd", repository, "--lineage", started.lineage_id, ...resultFiles.flatMap((result) => ["--result", result]), "--evidence", evidence], repository);
|
|
202
|
+
await captureSelectedLenses(repository, artifacts, evidenceName);
|
|
203
|
+
return run(binary, ["review", "finalize", "--cwd", repository, "--lineage", started.lineage_id, "--captured-results=true", "--evidence", evidence], repository);
|
|
166
204
|
}
|
|
167
205
|
|
|
168
206
|
test("official pinned package runtime authorizes an unchanged linked-view candidate and denies a changed staging tree", async (t) => {
|
|
@@ -206,13 +244,10 @@ test("official pinned package runtime authorizes an unchanged linked-view candid
|
|
|
206
244
|
|
|
207
245
|
const evidence = join(artifacts, "final-evidence.txt");
|
|
208
246
|
await writeFile(evidence, "linked-view parity probe\n");
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
resultFiles.push(result);
|
|
214
|
-
}
|
|
215
|
-
await run(binary, ["review", "finalize", "--cwd", view, "--lineage", started.lineage_id, ...resultFiles.flatMap((result) => ["--result", result]), "--evidence", evidence], view);
|
|
247
|
+
// The linked view is the root authority froze, so its collect bindings are
|
|
248
|
+
// derived from the view, not from the contributor repository.
|
|
249
|
+
await captureSelectedLenses(view, artifacts, "final-evidence.txt");
|
|
250
|
+
await run(binary, ["review", "finalize", "--cwd", view, "--lineage", started.lineage_id, "--captured-results=true", "--evidence", evidence], view);
|
|
216
251
|
|
|
217
252
|
await run("git", ["add", "--", ...REVIEWED_PATHS], repository);
|
|
218
253
|
await assertPublishedProjection(repository, candidateTree);
|
|
@@ -306,20 +341,29 @@ test("official pinned package runtime keeps frozen candidate lineages and receip
|
|
|
306
341
|
assert.equal(competingStartResult.lineage_id, first.lineage_id);
|
|
307
342
|
assert.equal(competingStartResult.state, "approved");
|
|
308
343
|
assert.deepEqual(authorityAfterCompetingStart, authorityBeforeCompetingStart, "a blocked scope action must not mutate approved authority");
|
|
344
|
+
// A plain START over the moved candidate does NOT mint a fresh lineage. An
|
|
345
|
+
// approved lineage whose scope changed is recovered, keeping the operator
|
|
346
|
+
// pointed at the receipt that already exists instead of quietly abandoning
|
|
347
|
+
// it, and the recovery itself needs a maintainer to authorize it. Pi mirrors
|
|
348
|
+
// this because the experience must be the one gentle-ai defines, not a
|
|
349
|
+
// friendlier one Pi invents.
|
|
309
350
|
const second = JSON.parse((await run(binary, ["review", "start", "--cwd", repository], repository)).stdout) as ReviewStart;
|
|
310
351
|
const secondInventory = authorityInventory(await reviewStatus(repository));
|
|
311
|
-
assert.equal(second.action, "
|
|
312
|
-
assert.equal(second.state, "
|
|
313
|
-
assert.
|
|
352
|
+
assert.equal(second.action, "recover");
|
|
353
|
+
assert.equal(second.state, "approved");
|
|
354
|
+
assert.equal(second.lineage_id, first.lineage_id, "a moved candidate recovers the approved lineage rather than replacing it");
|
|
314
355
|
const secondReplay = JSON.parse((await run(binary, ["review", "start", "--cwd", repository], repository)).stdout) as ReviewStart;
|
|
315
356
|
const secondReplayInventory = authorityInventory(await reviewStatus(repository));
|
|
357
|
+
assert.equal(secondReplay.action, "recover", "recovery is idempotent");
|
|
316
358
|
assert.equal(secondReplay.lineage_id, second.lineage_id, "replaying the second START must reuse its lineage");
|
|
317
359
|
assert.deepEqual(secondReplayInventory, secondInventory, "replaying the second START must not duplicate durable authority");
|
|
318
|
-
await
|
|
360
|
+
const recoveryTransition = await nextTransition(repository);
|
|
361
|
+
assert.equal(recoveryTransition.kind, "collect");
|
|
362
|
+
assert.equal(recoveryTransition.reason_code, "recovery_authorization_required", "recovery must ask a maintainer, never proceed on its own");
|
|
319
363
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
364
|
+
// No receipt covers the moved candidate, so delivery is denied rather than
|
|
365
|
+
// waved through, and the denial names `gentle-ai review recover` with the
|
|
366
|
+
// inputs it needs. A blocked gate that names its exit is the whole point.
|
|
323
367
|
await assertScopeChanged(repository, first.lineage_id);
|
|
324
368
|
|
|
325
369
|
await writeFile(join(repository, "tracked.txt"), "candidate one\n");
|
|
@@ -379,7 +423,10 @@ test("registered gentle_review START materializes a safe internal skill symlink
|
|
|
379
423
|
let returned: { details?: unknown } | undefined;
|
|
380
424
|
let thrown: unknown;
|
|
381
425
|
try {
|
|
382
|
-
|
|
426
|
+
// A headless context still receives notices, so the stub carries notify.
|
|
427
|
+
// Without it the consent path throws on a real ExtensionContext member,
|
|
428
|
+
// and the failure reads as a START problem rather than a stub gap.
|
|
429
|
+
returned = await controller.execute("issue-146-start", { operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, undefined, undefined, { cwd: repository, hasUI: false, ui: { confirm: async () => true, notify: () => {} } } as unknown as ExtensionContext);
|
|
383
430
|
} catch (caught) {
|
|
384
431
|
thrown = caught;
|
|
385
432
|
}
|