gentle-pi 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -80
- package/assets/agents/review-readability.md +0 -2
- package/assets/agents/review-reliability.md +0 -2
- package/assets/agents/review-resilience.md +0 -2
- package/assets/agents/review-risk.md +0 -2
- package/assets/agents/sdd-apply.md +5 -3
- package/assets/agents/sdd-proposal.md +2 -0
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-status.md +4 -4
- package/assets/agents/sdd-tasks.md +2 -3
- package/assets/agents/sdd-verify.md +21 -1
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +54 -248
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +22 -38
- package/assets/sdd-orchestrator-workflow.md +30 -37
- package/assets/support/sdd-status-contract.md +7 -7
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
- package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
- package/docs/native-authority-architecture.md +9 -11
- package/docs/review-integration.md +27 -373
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +1778 -3673
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +4 -8
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +372 -940
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/review-candidate-view.ts +341 -132
- package/lib/review-host-relay.ts +210 -68
- package/lib/review-integration-v2.ts +839 -307
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/sdd-preflight.ts +177 -66
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -82
- package/runtime/gentle-ai-binary.mjs +1 -1
- package/runtime/native-review-cli.mjs +322 -890
- package/runtime/review-integration-v2.mjs +789 -257
- package/runtime/review-relay-contract.mjs +1 -1
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
- package/scripts/gentle-ai-installer.mjs +75 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
- package/scripts/test-packed-runner.mjs +4 -7
- package/scripts/verify-package-files.mjs +12 -13
- package/skills/_shared/review-ledger-contract.md +8 -14
- package/skills/chained-pr/SKILL.md +3 -0
- package/skills/cognitive-doc-design/SKILL.md +1 -1
- package/skills/comment-writer/SKILL.md +1 -1
- package/skills/gentle-ai/SKILL.md +7 -74
- package/skills/judgment-day/SKILL.md +5 -7
- package/skills/rdd-defect-workflow/SKILL.md +3 -3
- package/skills/release/SKILL.md +3 -3
- package/skills/skill-registry/SKILL.md +1 -1
- package/skills/work-unit-commits/SKILL.md +3 -1
- package/tests/artifact-language.test.ts +24 -11
- package/tests/ask-user-choice.test.ts +264 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +15 -1168
- package/tests/delegated-key-learnings-contract.test.ts +8 -6
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
- package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
- package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary.test.ts +51 -228
- package/tests/gentle-ai-installer.test.ts +137 -47
- package/tests/gentle-ai.test.ts +820 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/maintainer/provider-relay.maintest.ts +340 -4
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +28 -1
- package/tests/native-review-cli.test.ts +586 -962
- package/tests/native-review-consent.test.ts +150 -66
- package/tests/native-review-parity-runtime.test.ts +80 -359
- package/tests/native-review-parity.test.ts +674 -925
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +132 -88
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +37 -35
- package/tests/provider-defect-handoff.test.ts +15 -118
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +409 -3
- package/tests/review-compact-contract.test.ts +4 -30
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +363 -1450
- package/tests/review-controller-native-routing.test.ts +1251 -5642
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +222 -71
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +112 -153
- package/tests/review-dispatch-hydration-gap.test.ts +1 -53
- package/tests/review-gate.test.ts +0 -45
- package/tests/review-host-relay-restart-parity.test.ts +360 -0
- package/tests/review-host-relay-routing.test.ts +117 -82
- package/tests/review-host-relay.test.ts +242 -8
- package/tests/review-integration-v2-forward.test.ts +576 -553
- package/tests/review-integration-v2.test.ts +239 -165
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +97 -35
- package/tests/review-recovered-lineage-routing.test.ts +0 -47
- package/tests/review-relay-transport-agent.test.ts +119 -46
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +361 -196
- package/tests/sdd-agent-tools.test.ts +36 -0
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +5 -8
- package/tests/writer-edit-surface-scope.test.ts +230 -0
- package/themes/Gentleman-Cute.json +94 -0
- package/themes/Gentleman-Sexy.json +92 -0
- package/lib/git-commit-transaction.ts +0 -861
- package/runtime/git-commit-transaction.mjs +0 -862
- package/scripts/run-git-commit-transaction.mjs +0 -35
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
- package/tests/git-commit-transaction.test.ts +0 -530
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
@@ -10,15 +10,10 @@ import type {
|
|
|
10
10
|
ToolCallEventResult,
|
|
11
11
|
} from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import gentleAi, { __testing, createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
13
|
-
import {
|
|
14
|
-
projectExactTagCreatePushAsReleaseV1,
|
|
15
|
-
setReleaseGhCommandRunnerForTestingV1,
|
|
16
|
-
} from "../lib/review-publication-gate.ts";
|
|
17
13
|
import {
|
|
18
14
|
REVIEW_MODE,
|
|
19
15
|
REVIEW_TRANSITION,
|
|
20
16
|
ReviewTransactionStore,
|
|
21
|
-
canonicalHash,
|
|
22
17
|
createReviewState,
|
|
23
18
|
setReviewMutationLockPlatformForTesting,
|
|
24
19
|
type ReviewBudgetV1,
|
|
@@ -31,19 +26,6 @@ import { qualifiedReviewLockPlatform, testSnapshot } from "./review-test-fixture
|
|
|
31
26
|
import type { NativeReviewCli } from "../lib/native-review-cli.ts";
|
|
32
27
|
|
|
33
28
|
setReviewMutationLockPlatformForTesting(qualifiedReviewLockPlatform());
|
|
34
|
-
// The release fast path independently derives required CI success via the
|
|
35
|
-
// gh CLI; these controller-level fixtures are local bare clones with no real
|
|
36
|
-
// GitHub remote, so a deterministic Check Runs response stands in for `gh`.
|
|
37
|
-
// Legacy combined status stays pending to model Checks-only repositories.
|
|
38
|
-
let releaseCheckRuns: { total_count: number; returned: number; checks: ReadonlyArray<readonly [string, string | null]> } = { total_count: 1, returned: 1, checks: [["completed", "success"]] };
|
|
39
|
-
const releaseCheckRunArguments: string[][] = [];
|
|
40
|
-
setReleaseGhCommandRunnerForTestingV1((args) => {
|
|
41
|
-
if (args[1]?.includes("/check-runs?")) {
|
|
42
|
-
releaseCheckRunArguments.push([...args]);
|
|
43
|
-
return { status: 0, stdout: JSON.stringify(releaseCheckRuns) };
|
|
44
|
-
}
|
|
45
|
-
return { status: 0, stdout: "pending" };
|
|
46
|
-
});
|
|
47
29
|
|
|
48
30
|
interface ReviewToolResult {
|
|
49
31
|
content: Array<{ type: string; text: string }>;
|
|
@@ -80,9 +62,6 @@ interface RepositoryFixture {
|
|
|
80
62
|
repository: string;
|
|
81
63
|
baseCommit: string;
|
|
82
64
|
baseTree: string;
|
|
83
|
-
finalCommit?: string;
|
|
84
|
-
finalTree?: string;
|
|
85
|
-
tagObject?: string;
|
|
86
65
|
}
|
|
87
66
|
|
|
88
67
|
function budget(): ReviewBudgetV1 {
|
|
@@ -140,8 +119,8 @@ function git(repository: string, ...args: string[]): string {
|
|
|
140
119
|
}).trim();
|
|
141
120
|
}
|
|
142
121
|
|
|
143
|
-
function createRepository(t: test.TestContext
|
|
144
|
-
const parent = mkdtempSync(join(tmpdir(), "gentle-pi-review-controller-"));
|
|
122
|
+
function createRepository(t: test.TestContext): RepositoryFixture {
|
|
123
|
+
const parent = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-review-controller-")));
|
|
145
124
|
const repository = join(parent, "repo");
|
|
146
125
|
mkdirSync(repository);
|
|
147
126
|
t.after(() => rmSync(parent, { recursive: true, force: true }));
|
|
@@ -162,44 +141,7 @@ function createRepository(t: test.TestContext, commitFinal: boolean): Repository
|
|
|
162
141
|
const baseTree = git(repository, "rev-parse", "HEAD^{tree}");
|
|
163
142
|
git(repository, "branch", "base", baseCommit);
|
|
164
143
|
writeFileSync(join(repository, "app.ts"), "export const value = 2;\n");
|
|
165
|
-
|
|
166
|
-
git(repository, "add", ".");
|
|
167
|
-
git(
|
|
168
|
-
repository,
|
|
169
|
-
"-c",
|
|
170
|
-
"user.name=Review Controller",
|
|
171
|
-
"-c",
|
|
172
|
-
"user.email=review-controller@example.invalid",
|
|
173
|
-
"commit",
|
|
174
|
-
"-m",
|
|
175
|
-
"final",
|
|
176
|
-
);
|
|
177
|
-
const finalCommit = git(repository, "rev-parse", "HEAD");
|
|
178
|
-
const finalTree = git(repository, "rev-parse", "HEAD^{tree}");
|
|
179
|
-
git(repository, "branch", "final", finalCommit);
|
|
180
|
-
git(
|
|
181
|
-
repository,
|
|
182
|
-
"-c",
|
|
183
|
-
"user.name=Review Controller",
|
|
184
|
-
"-c",
|
|
185
|
-
"user.email=review-controller@example.invalid",
|
|
186
|
-
"tag",
|
|
187
|
-
"-a",
|
|
188
|
-
"v1.2.3",
|
|
189
|
-
"-m",
|
|
190
|
-
"release",
|
|
191
|
-
finalCommit,
|
|
192
|
-
);
|
|
193
|
-
const tagObject = git(repository, "rev-parse", "refs/tags/v1.2.3^{object}");
|
|
194
|
-
return {
|
|
195
|
-
parent,
|
|
196
|
-
repository,
|
|
197
|
-
baseCommit,
|
|
198
|
-
baseTree,
|
|
199
|
-
finalCommit,
|
|
200
|
-
finalTree,
|
|
201
|
-
tagObject,
|
|
202
|
-
};
|
|
144
|
+
return { parent, repository, baseCommit, baseTree };
|
|
203
145
|
}
|
|
204
146
|
|
|
205
147
|
function details(result: ReviewToolResult): Record<string, unknown> {
|
|
@@ -215,8 +157,7 @@ async function controllerCall(
|
|
|
215
157
|
return details(await controller.execute("review-tool-call", params, undefined, undefined, ctx));
|
|
216
158
|
}
|
|
217
159
|
|
|
218
|
-
function createTerminalAuthority(fixture: RepositoryFixture, lineageId: string): void {
|
|
219
|
-
assert.ok(fixture.finalTree);
|
|
160
|
+
function createTerminalAuthority(fixture: RepositoryFixture, lineageId: string, completeTree: string): void {
|
|
220
161
|
const store = ReviewTransactionStore.forRepository(fixture.repository, { mutationLockPlatform: qualifiedReviewLockPlatform() });
|
|
221
162
|
store.create(
|
|
222
163
|
createReviewState({
|
|
@@ -224,7 +165,7 @@ function createTerminalAuthority(fixture: RepositoryFixture, lineageId: string):
|
|
|
224
165
|
mode: REVIEW_MODE.ORDINARY,
|
|
225
166
|
snapshot: testSnapshot({
|
|
226
167
|
baseTree: fixture.baseTree,
|
|
227
|
-
completeTree
|
|
168
|
+
completeTree,
|
|
228
169
|
genesisPaths: ["app.ts"],
|
|
229
170
|
route: REVIEW_ROUTE.STANDARD,
|
|
230
171
|
lenses: [REVIEW_LENS.READABILITY],
|
|
@@ -249,14 +190,14 @@ function createTerminalAuthority(fixture: RepositoryFixture, lineageId: string):
|
|
|
249
190
|
}
|
|
250
191
|
|
|
251
192
|
test("controller SDD status treats removed OpenSpec recovery authority and deleted marker as blocking", async (t) => {
|
|
252
|
-
const fixture = createRepository(t
|
|
193
|
+
const fixture = createRepository(t);
|
|
253
194
|
const changeName = "recover-legacy-review-authority";
|
|
254
195
|
const changeRoot = join(fixture.repository, "openspec", "changes", changeName);
|
|
255
196
|
mkdirSync(changeRoot, { recursive: true });
|
|
256
197
|
writeFileSync(join(fixture.repository, "app.ts"), "export const value = 2;\n");
|
|
257
198
|
git(fixture.repository, "add", "app.ts");
|
|
258
|
-
|
|
259
|
-
createTerminalAuthority(fixture, "archived-graph-source");
|
|
199
|
+
const completeTree = git(fixture.repository, "write-tree");
|
|
200
|
+
createTerminalAuthority(fixture, "archived-graph-source", completeTree);
|
|
260
201
|
const supersessionRoot = join(resolveRepositoryAuthorityV1(fixture.repository).store_root, "control", "authority-supersession-v1");
|
|
261
202
|
const marker = join(supersessionRoot, "recovery-required-v1", `${domainHashV1("openspec-change-name", changeName)}.json`);
|
|
262
203
|
mkdirSync(join(supersessionRoot, "recovery-required-v1"), { recursive: true });
|
|
@@ -271,8 +212,8 @@ test("controller SDD status treats removed OpenSpec recovery authority and delet
|
|
|
271
212
|
assert.match(status.blockedReasons.join("\n"), /active change not found/i);
|
|
272
213
|
});
|
|
273
214
|
|
|
274
|
-
test("controller SDD status
|
|
275
|
-
const fixture = createRepository(t
|
|
215
|
+
test("controller SDD status ignores recovery-required review markers after terminal burn", async (t) => {
|
|
216
|
+
const fixture = createRepository(t);
|
|
276
217
|
const changeName = "recover-legacy-review-authority";
|
|
277
218
|
const changeRoot = join(fixture.repository, "openspec", "changes", changeName);
|
|
278
219
|
mkdirSync(join(changeRoot, "specs", "review"), { recursive: true });
|
|
@@ -288,12 +229,12 @@ test("controller SDD status blocks archive for a recovery-required marker withou
|
|
|
288
229
|
|
|
289
230
|
const status = await __testing.resolveControllerSddStatus(fixture.repository, changeName, false, "openspec");
|
|
290
231
|
|
|
291
|
-
assert.equal(status.dependencies.archive, "
|
|
292
|
-
assert.equal(status.nextRecommended, "
|
|
232
|
+
assert.equal(status.dependencies.archive, "ready");
|
|
233
|
+
assert.equal(status.nextRecommended, "sdd-archive");
|
|
293
234
|
});
|
|
294
235
|
|
|
295
236
|
test("controller keeps graph-v1 ordinary mutation read-only while preserving repository-file input confinement", async (t) => {
|
|
296
|
-
const fixture = createRepository(t
|
|
237
|
+
const fixture = createRepository(t);
|
|
297
238
|
const lineageId = "controller-file-validator";
|
|
298
239
|
const store = ReviewTransactionStore.forRepository(fixture.repository, { mutationLockPlatform: qualifiedReviewLockPlatform() });
|
|
299
240
|
store.create(createReviewState({
|
|
@@ -376,7 +317,7 @@ test("controller keeps graph-v1 ordinary mutation read-only while preserving rep
|
|
|
376
317
|
});
|
|
377
318
|
|
|
378
319
|
test("controller rejects graph-style ADVANCE without graph-v1 authority", async (t) => {
|
|
379
|
-
const fixture = createRepository(t
|
|
320
|
+
const fixture = createRepository(t);
|
|
380
321
|
const lineageId = "controller-correction-evidence";
|
|
381
322
|
const { controller } = registerRuntime();
|
|
382
323
|
const ctx = extensionContext(fixture.repository);
|
|
@@ -401,7 +342,7 @@ test("controller rejects graph-style ADVANCE without graph-v1 authority", async
|
|
|
401
342
|
|
|
402
343
|
|
|
403
344
|
test("controller successfully starts the explicitly supported judgment-day mode", async (t) => {
|
|
404
|
-
const fixture = createRepository(t
|
|
345
|
+
const fixture = createRepository(t);
|
|
405
346
|
const { controller } = registerRuntime();
|
|
406
347
|
const started = await controllerCall(controller, extensionContext(fixture.repository), {
|
|
407
348
|
operation: "start",
|
|
@@ -431,7 +372,7 @@ test("general STATUS returns the typed native-status-unsupported boundary withou
|
|
|
431
372
|
});
|
|
432
373
|
|
|
433
374
|
test("failed START gives exact mode and serialization guidance and creates no lineage", async (t) => {
|
|
434
|
-
const fixture = createRepository(t
|
|
375
|
+
const fixture = createRepository(t);
|
|
435
376
|
const { controller } = registerRuntime();
|
|
436
377
|
const ctx = extensionContext(fixture.repository);
|
|
437
378
|
|
|
@@ -466,7 +407,7 @@ test("failed START gives exact mode and serialization guidance and creates no li
|
|
|
466
407
|
});
|
|
467
408
|
|
|
468
409
|
|
|
469
|
-
test("shipped controller
|
|
410
|
+
test("shipped controller fails closed while static prompts defer RDD lifecycle ownership to Gentle AI", () => {
|
|
470
411
|
const { controller } = registerRuntime();
|
|
471
412
|
const toolContract = [
|
|
472
413
|
controller.description,
|
|
@@ -474,750 +415,19 @@ test("shipped controller and orchestrator contracts specify inspect-first compac
|
|
|
474
415
|
...(controller.promptGuidelines ?? []),
|
|
475
416
|
JSON.stringify(controller.parameters),
|
|
476
417
|
].join("\n");
|
|
477
|
-
|
|
478
|
-
assert.match(toolContract, /mode\\?":\\?"ordinary|mode.*ordinary/is);
|
|
479
|
-
assert.match(toolContract, /ordinary.*Judgment Day/is);
|
|
480
|
-
assert.match(toolContract, /JSON(?:-serialized object)? string/is);
|
|
481
|
-
assert.match(toolContract, /blocked-legacy.*explicit.*authorization/is);
|
|
482
|
-
assert.match(toolContract, /RESET.*reclaim.*RECOVER.*recover/s);
|
|
418
|
+
|
|
483
419
|
assert.match(toolContract, /native-input-required.*never.*invent/is);
|
|
484
420
|
assert.match(toolContract, /output.*lost|response.*lost|ambiguous.*START/is);
|
|
485
|
-
assert.match(toolContract, /ambiguous START
|
|
421
|
+
assert.match(toolContract, /ambiguous START output.*target-scoped native status.*declared action.*ambiguous gentle_review_capture.*never replays/is);
|
|
486
422
|
assert.doesNotMatch(toolContract, /START throws.*lineage does not exist/is);
|
|
487
423
|
|
|
424
|
+
const boundary = "Gentle AI dynamically supplies runtime-specific RDD instructions via generated Pi APPEND_SYSTEM composition. Follow only those exact native instructions; if absent or unsupported, this package does not invent or fall back.";
|
|
425
|
+
const core = readFileSync("assets/orchestrator.md", "utf8");
|
|
426
|
+
assert.match(core, new RegExp(boundary.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
427
|
+
|
|
488
428
|
for (const path of ["assets/orchestrator-delegation.md", "skills/gentle-ai/SKILL.md"]) {
|
|
489
429
|
const contract = readFileSync(path, "utf8");
|
|
490
|
-
assert.
|
|
491
|
-
assert.match(contract, /mode `ordinary`|mode.*ordinary|ordinary review/is, path);
|
|
492
|
-
assert.match(contract, /Judgment Day.*explicit/is, path);
|
|
493
|
-
assert.match(contract, /before authority access.*no lineage|pre-authority.*no lineage/is, path);
|
|
494
|
-
assert.match(contract, /unknown.*target-scoped status.*before any retry|unknown.*immediately calls target-scoped status/is, path);
|
|
495
|
-
assert.match(contract, /exact_replay_safe/is, path);
|
|
496
|
-
}
|
|
497
|
-
for (const path of ["assets/orchestrator-delegation.md", "skills/gentle-ai/SKILL.md"]) {
|
|
498
|
-
const recoveryContract = readFileSync(path, "utf8");
|
|
499
|
-
assert.match(recoveryContract, /blocked-legacy.*explicit.*authoriz/is, path);
|
|
500
|
-
assert.match(recoveryContract, /RESET.*RECOVER.*native.*(reclaim|recover)/is, path);
|
|
501
|
-
}
|
|
502
|
-
});
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
test("controller binds push, PR, and release authorization to exact command arguments", async (t) => {
|
|
506
|
-
const fixture = createRepository(t, true);
|
|
507
|
-
assert.ok(fixture.finalCommit && fixture.finalTree && fixture.tagObject);
|
|
508
|
-
const remotePath = join(fixture.parent, "remote.git");
|
|
509
|
-
execFileSync("git", ["clone", "--bare", fixture.repository, remotePath], {
|
|
510
|
-
cwd: fixture.parent,
|
|
511
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
512
|
-
});
|
|
513
|
-
execFileSync("git", ["--git-dir", remotePath, "update-ref", "refs/heads/main", fixture.baseCommit]);
|
|
514
|
-
git(fixture.repository, "remote", "add", "origin", remotePath);
|
|
515
|
-
createTerminalAuthority(fixture, "controller-targets");
|
|
516
|
-
const { controller, toolCall } = registerRuntime();
|
|
517
|
-
const ctx = extensionContext(fixture.repository, true);
|
|
518
|
-
|
|
519
|
-
for (const [command, key] of [
|
|
520
|
-
["git push origin main:main", "push"],
|
|
521
|
-
["gh pr create --base base --head final", "pr"],
|
|
522
|
-
["gh release create v1.2.3 --notes bounded", "release"],
|
|
523
|
-
] as const) {
|
|
524
|
-
const validated = await controllerCall(controller, ctx, {
|
|
525
|
-
operation: "validate",
|
|
526
|
-
lineageId: "controller-targets",
|
|
527
|
-
idempotencyKey: `controller-targets-${key}`,
|
|
528
|
-
command,
|
|
529
|
-
input: JSON.stringify({ scopeBudget: budget() }),
|
|
530
|
-
});
|
|
531
|
-
assert.equal((validated.result as Record<string, unknown>).status, "allow", command);
|
|
532
|
-
assert.equal(await toolCall({ toolName: "bash", input: { command } }, ctx), undefined, command);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
for (const command of [
|
|
536
|
-
"git push --all origin",
|
|
537
|
-
"git push origin main:main final:feature",
|
|
538
|
-
"git push --follow-tags origin main:main",
|
|
539
|
-
"git push origin --follow-tags main:main",
|
|
540
|
-
"git push origin main:main --follow-tags",
|
|
541
|
-
]) {
|
|
542
|
-
await assert.rejects(
|
|
543
|
-
controller.execute(
|
|
544
|
-
"unsupported-push",
|
|
545
|
-
{
|
|
546
|
-
operation: "validate",
|
|
547
|
-
lineageId: "controller-targets",
|
|
548
|
-
idempotencyKey: `unsupported-${command.length}`,
|
|
549
|
-
command,
|
|
550
|
-
input: JSON.stringify({ scopeBudget: budget() }),
|
|
551
|
-
},
|
|
552
|
-
undefined,
|
|
553
|
-
undefined,
|
|
554
|
-
ctx,
|
|
555
|
-
),
|
|
556
|
-
/exactly derive|unsupported.*push|complete ref update|force push refspec/i,
|
|
557
|
-
);
|
|
558
|
-
}
|
|
559
|
-
await t.test("rejects force refspecs and unsupported push --repo parsing", async () => {
|
|
560
|
-
for (const [command, pattern] of [
|
|
561
|
-
["git push origin +main:main", /force push refspec/i],
|
|
562
|
-
["git push --repo attacker origin main:main", /unsupported push option.*--repo/i],
|
|
563
|
-
] as const) {
|
|
564
|
-
await assert.rejects(
|
|
565
|
-
controller.execute(
|
|
566
|
-
"unsafe-push-form",
|
|
567
|
-
{
|
|
568
|
-
operation: "validate",
|
|
569
|
-
lineageId: "controller-targets",
|
|
570
|
-
idempotencyKey: `unsafe-push-form-${command.length}`,
|
|
571
|
-
command,
|
|
572
|
-
input: JSON.stringify({ scopeBudget: budget() }),
|
|
573
|
-
},
|
|
574
|
-
undefined,
|
|
575
|
-
undefined,
|
|
576
|
-
ctx,
|
|
577
|
-
),
|
|
578
|
-
pattern,
|
|
579
|
-
);
|
|
580
|
-
}
|
|
581
|
-
});
|
|
582
|
-
for (const [command, key] of [
|
|
583
|
-
["env SAFE=1 git push --follow-tags origin main:main", "env"],
|
|
584
|
-
["command git push origin --follow-tags main:main", "command"],
|
|
585
|
-
["sh -c 'git push origin main:main --follow-tags'", "shell"],
|
|
586
|
-
] as const) {
|
|
587
|
-
await assert.rejects(
|
|
588
|
-
controller.execute(
|
|
589
|
-
"unsupported-wrapped-follow-tags-push",
|
|
590
|
-
{
|
|
591
|
-
operation: "validate",
|
|
592
|
-
lineageId: "controller-targets",
|
|
593
|
-
idempotencyKey: `unsupported-wrapped-follow-tags-push-${key}`,
|
|
594
|
-
command,
|
|
595
|
-
input: JSON.stringify({ scopeBudget: budget() }),
|
|
596
|
-
},
|
|
597
|
-
undefined,
|
|
598
|
-
undefined,
|
|
599
|
-
ctx,
|
|
600
|
-
),
|
|
601
|
-
/compound or wrapped lifecycle command.*fail closed/i,
|
|
602
|
-
);
|
|
603
|
-
}
|
|
604
|
-
for (const [command, key] of [
|
|
605
|
-
["gh release create v1.2.3 --repo other/project", "long"],
|
|
606
|
-
["gh release create v1.2.3 -Rother/project", "short"],
|
|
607
|
-
] as const) {
|
|
608
|
-
await assert.rejects(
|
|
609
|
-
controller.execute(
|
|
610
|
-
"unsupported-release-repository",
|
|
611
|
-
{
|
|
612
|
-
operation: "validate",
|
|
613
|
-
lineageId: "controller-targets",
|
|
614
|
-
idempotencyKey: `unsupported-release-repository-${key}`,
|
|
615
|
-
command,
|
|
616
|
-
input: JSON.stringify({ scopeBudget: budget() }),
|
|
617
|
-
},
|
|
618
|
-
undefined,
|
|
619
|
-
undefined,
|
|
620
|
-
ctx,
|
|
621
|
-
),
|
|
622
|
-
/exact local review repository|--repo/i,
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
await controllerCall(controller, ctx, {
|
|
627
|
-
operation: "validate",
|
|
628
|
-
lineageId: "controller-targets",
|
|
629
|
-
idempotencyKey: "controller-targets-release-mismatch",
|
|
630
|
-
command: "gh release create v1.2.3",
|
|
631
|
-
input: JSON.stringify({ scopeBudget: budget() }),
|
|
632
|
-
});
|
|
633
|
-
git(
|
|
634
|
-
fixture.repository,
|
|
635
|
-
"-c",
|
|
636
|
-
"user.name=Review Controller",
|
|
637
|
-
"-c",
|
|
638
|
-
"user.email=review-controller@example.invalid",
|
|
639
|
-
"tag",
|
|
640
|
-
"-a",
|
|
641
|
-
"v9.9.9",
|
|
642
|
-
"-m",
|
|
643
|
-
"different release argument",
|
|
644
|
-
fixture.finalCommit,
|
|
645
|
-
);
|
|
646
|
-
const mismatchedRelease = await toolCall(
|
|
647
|
-
{
|
|
648
|
-
toolName: "bash",
|
|
649
|
-
input: {
|
|
650
|
-
command: "gh release create v9.9.9",
|
|
651
|
-
reviewGate: {
|
|
652
|
-
target: {
|
|
653
|
-
kind: "release",
|
|
654
|
-
tag_ref: "refs/tags/v1.2.3",
|
|
655
|
-
tag_object: fixture.tagObject,
|
|
656
|
-
peeled_commit: fixture.finalCommit,
|
|
657
|
-
tree: fixture.finalTree,
|
|
658
|
-
},
|
|
659
|
-
},
|
|
660
|
-
},
|
|
661
|
-
},
|
|
662
|
-
ctx,
|
|
663
|
-
);
|
|
664
|
-
assert.equal(mismatchedRelease?.block, true);
|
|
665
|
-
assert.match(mismatchedRelease?.reason ?? "", /registered review controller authorization/i);
|
|
666
|
-
});
|
|
667
|
-
|
|
668
|
-
test("controller authorizes the exact first push after an approved intended-commit receipt", async (t) => {
|
|
669
|
-
const fixture = createRepository(t, true);
|
|
670
|
-
assert.ok(fixture.finalCommit && fixture.finalTree);
|
|
671
|
-
const fetchRemotePath = join(fixture.parent, "fetch.git");
|
|
672
|
-
const pushRemotePath = join(fixture.parent, "push.git");
|
|
673
|
-
execFileSync("git", ["clone", "--bare", fixture.repository, fetchRemotePath], {
|
|
674
|
-
cwd: fixture.parent,
|
|
675
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
676
|
-
});
|
|
677
|
-
execFileSync("git", ["--git-dir", fetchRemotePath, "update-ref", "refs/heads/feature/first-push", fixture.finalCommit]);
|
|
678
|
-
execFileSync("git", ["clone", "--bare", fixture.repository, pushRemotePath], {
|
|
679
|
-
cwd: fixture.parent,
|
|
680
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
681
|
-
});
|
|
682
|
-
execFileSync("git", ["--git-dir", pushRemotePath, "update-ref", "refs/heads/main", fixture.baseCommit]);
|
|
683
|
-
git(fixture.repository, "remote", "add", "origin", fetchRemotePath);
|
|
684
|
-
git(fixture.repository, "remote", "set-url", "--add", "--push", "origin", pushRemotePath);
|
|
685
|
-
git(fixture.repository, "branch", "feature/first-push", fixture.finalCommit);
|
|
686
|
-
createTerminalAuthority(fixture, "controller-first-push");
|
|
687
|
-
const { controller, toolCall } = registerRuntime();
|
|
688
|
-
const ctx = extensionContext(fixture.repository, true);
|
|
689
|
-
const command = "git push -u origin feature/first-push";
|
|
690
|
-
const validated = await controllerCall(controller, ctx, {
|
|
691
|
-
operation: "validate",
|
|
692
|
-
lineageId: "controller-first-push",
|
|
693
|
-
idempotencyKey: "controller-first-push-gate",
|
|
694
|
-
command,
|
|
695
|
-
input: JSON.stringify({ scopeBudget: budget() }),
|
|
696
|
-
});
|
|
697
|
-
|
|
698
|
-
assert.equal((validated.result as Record<string, unknown>).status, "allow", JSON.stringify(validated));
|
|
699
|
-
const derivedTarget = validated.derived_target as Record<string, unknown>;
|
|
700
|
-
assert.equal((derivedTarget.updates as Array<Record<string, unknown>>)[0]?.kind, "create");
|
|
701
|
-
assert.equal(await toolCall({ toolName: "bash", input: { command } }, ctx), undefined);
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
test("controller resolves explicit abbreviated push destinations against advertised refs", async (t) => {
|
|
705
|
-
const fixture = createRepository(t, true);
|
|
706
|
-
assert.ok(fixture.finalCommit && fixture.finalTree);
|
|
707
|
-
const remotePath = join(fixture.parent, "destination-resolution.git");
|
|
708
|
-
execFileSync("git", ["clone", "--bare", fixture.repository, remotePath], {
|
|
709
|
-
cwd: fixture.parent,
|
|
710
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
711
|
-
});
|
|
712
|
-
execFileSync("git", ["--git-dir", remotePath, "update-ref", "refs/tags/publish-target", fixture.baseCommit]);
|
|
713
|
-
git(fixture.repository, "remote", "add", "origin", remotePath);
|
|
714
|
-
createTerminalAuthority(fixture, "controller-destination-resolution");
|
|
715
|
-
const { controller } = registerRuntime();
|
|
716
|
-
const ctx = extensionContext(fixture.repository, true);
|
|
717
|
-
const validated = await controllerCall(controller, ctx, {
|
|
718
|
-
operation: "validate",
|
|
719
|
-
lineageId: "controller-destination-resolution",
|
|
720
|
-
idempotencyKey: "controller-destination-tag",
|
|
721
|
-
command: "git push origin final:publish-target",
|
|
722
|
-
input: JSON.stringify({ scopeBudget: budget() }),
|
|
723
|
-
});
|
|
724
|
-
const update = (validated.derived_target as { updates: Array<Record<string, unknown>> }).updates[0];
|
|
725
|
-
assert.equal(update?.destination_ref, "refs/tags/publish-target");
|
|
726
|
-
|
|
727
|
-
execFileSync("git", ["--git-dir", remotePath, "update-ref", "refs/heads/ambiguous-target", fixture.baseCommit]);
|
|
728
|
-
execFileSync("git", ["--git-dir", remotePath, "update-ref", "refs/tags/ambiguous-target", fixture.baseCommit]);
|
|
729
|
-
await assert.rejects(
|
|
730
|
-
controller.execute(
|
|
731
|
-
"ambiguous-push-destination",
|
|
732
|
-
{
|
|
733
|
-
operation: "validate",
|
|
734
|
-
lineageId: "controller-destination-resolution",
|
|
735
|
-
idempotencyKey: "controller-destination-ambiguous",
|
|
736
|
-
command: "git push origin final:ambiguous-target",
|
|
737
|
-
input: JSON.stringify({ scopeBudget: budget() }),
|
|
738
|
-
},
|
|
739
|
-
undefined,
|
|
740
|
-
undefined,
|
|
741
|
-
ctx,
|
|
742
|
-
),
|
|
743
|
-
/ambiguous/i,
|
|
744
|
-
);
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
test("controller push probes preserve safe user URL rewriting and ordinary credentials", async (t) => {
|
|
748
|
-
const fixture = createRepository(t, true);
|
|
749
|
-
assert.ok(fixture.finalCommit && fixture.finalTree);
|
|
750
|
-
const fetchRemotePath = join(fixture.parent, "fetch.git");
|
|
751
|
-
const pushRemotePath = join(fixture.parent, "push.git");
|
|
752
|
-
const userHome = join(fixture.parent, "home");
|
|
753
|
-
mkdirSync(userHome);
|
|
754
|
-
execFileSync("git", ["init", "--bare", fetchRemotePath], { cwd: fixture.parent, stdio: ["ignore", "pipe", "pipe"] });
|
|
755
|
-
execFileSync("git", ["clone", "--bare", fixture.repository, pushRemotePath], { cwd: fixture.parent, stdio: ["ignore", "pipe", "pipe"] });
|
|
756
|
-
execFileSync("git", ["--git-dir", pushRemotePath, "update-ref", "refs/heads/main", fixture.baseCommit]);
|
|
757
|
-
const logicalPushUrl = "https://github.com/example/first-push.git";
|
|
758
|
-
execFileSync("git", ["config", "--global", `url.${pushRemotePath}.insteadOf`, logicalPushUrl], {
|
|
759
|
-
env: { ...process.env, HOME: userHome },
|
|
760
|
-
});
|
|
761
|
-
git(fixture.repository, "remote", "add", "origin", fetchRemotePath);
|
|
762
|
-
git(fixture.repository, "remote", "set-url", "--add", "--push", "origin", logicalPushUrl);
|
|
763
|
-
git(fixture.repository, "branch", "feature/safe-config", fixture.finalCommit);
|
|
764
|
-
createTerminalAuthority(fixture, "controller-safe-config");
|
|
765
|
-
const { controller } = registerRuntime();
|
|
766
|
-
const ctx = extensionContext(fixture.repository, true);
|
|
767
|
-
const originalEnvironment = new Map<string, string | undefined>();
|
|
768
|
-
for (const [key, value] of Object.entries({
|
|
769
|
-
HOME: userHome,
|
|
770
|
-
GIT_ASKPASS: join(fixture.parent, "askpass"),
|
|
771
|
-
})) {
|
|
772
|
-
originalEnvironment.set(key, process.env[key]);
|
|
773
|
-
process.env[key] = value;
|
|
430
|
+
assert.doesNotMatch(contract, /INSPECT before START|start -> finalize -> validate|next_transition|review\.capture-result/is, path);
|
|
774
431
|
}
|
|
775
|
-
|
|
776
|
-
for (const [key, value] of originalEnvironment) {
|
|
777
|
-
if (value === undefined) delete process.env[key];
|
|
778
|
-
else process.env[key] = value;
|
|
779
|
-
}
|
|
780
|
-
});
|
|
781
|
-
|
|
782
|
-
const validated = await controllerCall(controller, ctx, {
|
|
783
|
-
operation: "validate",
|
|
784
|
-
lineageId: "controller-safe-config",
|
|
785
|
-
idempotencyKey: "controller-safe-config-gate",
|
|
786
|
-
command: "git push -u origin feature/safe-config",
|
|
787
|
-
input: JSON.stringify({ scopeBudget: budget() }),
|
|
788
|
-
});
|
|
789
|
-
assert.equal((validated.result as Record<string, unknown>).status, "allow", JSON.stringify(validated));
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
test("controller blocks a previously authorized push when inherited Git config injection appears at execution", async (t) => {
|
|
793
|
-
const fixture = createRepository(t, true);
|
|
794
|
-
assert.ok(fixture.finalCommit && fixture.finalTree);
|
|
795
|
-
const pushRemotePath = join(fixture.parent, "execution-boundary-push.git");
|
|
796
|
-
execFileSync("git", ["clone", "--bare", fixture.repository, pushRemotePath], {
|
|
797
|
-
cwd: fixture.parent,
|
|
798
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
799
|
-
});
|
|
800
|
-
execFileSync("git", ["--git-dir", pushRemotePath, "update-ref", "refs/heads/main", fixture.baseCommit]);
|
|
801
|
-
git(fixture.repository, "remote", "add", "origin", pushRemotePath);
|
|
802
|
-
git(fixture.repository, "branch", "feature/execution-boundary", fixture.finalCommit);
|
|
803
|
-
createTerminalAuthority(fixture, "controller-execution-boundary");
|
|
804
|
-
const { controller, toolCall } = registerRuntime();
|
|
805
|
-
const ctx = extensionContext(fixture.repository, true);
|
|
806
|
-
const command = "git push -u origin feature/execution-boundary";
|
|
807
|
-
const validated = await controllerCall(controller, ctx, {
|
|
808
|
-
operation: "validate",
|
|
809
|
-
lineageId: "controller-execution-boundary",
|
|
810
|
-
idempotencyKey: "controller-execution-boundary-gate",
|
|
811
|
-
command,
|
|
812
|
-
input: JSON.stringify({ scopeBudget: budget() }),
|
|
813
|
-
});
|
|
814
|
-
assert.equal((validated.result as Record<string, unknown>).status, "allow");
|
|
815
|
-
|
|
816
|
-
const originalCount = process.env.GIT_CONFIG_COUNT;
|
|
817
|
-
const originalKey = process.env.GIT_CONFIG_KEY_0;
|
|
818
|
-
const originalValue = process.env.GIT_CONFIG_VALUE_0;
|
|
819
|
-
process.env.GIT_CONFIG_COUNT = "1";
|
|
820
|
-
process.env.GIT_CONFIG_KEY_0 = "remote.origin.pushurl";
|
|
821
|
-
process.env.GIT_CONFIG_VALUE_0 = join(fixture.parent, "attacker.git");
|
|
822
|
-
try {
|
|
823
|
-
const blocked = await toolCall({ toolName: "bash", input: { command } }, ctx);
|
|
824
|
-
assert.equal(blocked?.block, true);
|
|
825
|
-
assert.match(blocked?.reason ?? "", /Git.*environment|routing|configuration override/i);
|
|
826
|
-
} finally {
|
|
827
|
-
if (originalCount === undefined) delete process.env.GIT_CONFIG_COUNT;
|
|
828
|
-
else process.env.GIT_CONFIG_COUNT = originalCount;
|
|
829
|
-
if (originalKey === undefined) delete process.env.GIT_CONFIG_KEY_0;
|
|
830
|
-
else process.env.GIT_CONFIG_KEY_0 = originalKey;
|
|
831
|
-
if (originalValue === undefined) delete process.env.GIT_CONFIG_VALUE_0;
|
|
832
|
-
else process.env.GIT_CONFIG_VALUE_0 = originalValue;
|
|
833
|
-
}
|
|
834
|
-
});
|
|
835
|
-
|
|
836
|
-
test("controller fails closed when a configured remote has multiple pushurl destinations", async (t) => {
|
|
837
|
-
const fixture = createRepository(t, true);
|
|
838
|
-
assert.ok(fixture.finalCommit);
|
|
839
|
-
const fetchRemotePath = join(fixture.parent, "fetch.git");
|
|
840
|
-
const firstPushPath = join(fixture.parent, "push-one.git");
|
|
841
|
-
const secondPushPath = join(fixture.parent, "push-two.git");
|
|
842
|
-
for (const path of [fetchRemotePath, firstPushPath, secondPushPath]) {
|
|
843
|
-
execFileSync("git", ["init", "--bare", path], { cwd: fixture.parent, stdio: ["ignore", "pipe", "pipe"] });
|
|
844
|
-
}
|
|
845
|
-
git(fixture.repository, "remote", "add", "origin", fetchRemotePath);
|
|
846
|
-
git(fixture.repository, "remote", "set-url", "--add", "--push", "origin", firstPushPath);
|
|
847
|
-
git(fixture.repository, "remote", "set-url", "--add", "--push", "origin", secondPushPath);
|
|
848
|
-
git(fixture.repository, "branch", "feature/multiple-pushurl", fixture.finalCommit);
|
|
849
|
-
createTerminalAuthority(fixture, "controller-multiple-pushurl");
|
|
850
|
-
const { controller } = registerRuntime();
|
|
851
|
-
|
|
852
|
-
await assert.rejects(
|
|
853
|
-
controller.execute("multiple-pushurl", {
|
|
854
|
-
operation: "validate",
|
|
855
|
-
lineageId: "controller-multiple-pushurl",
|
|
856
|
-
idempotencyKey: "controller-multiple-pushurl-gate",
|
|
857
|
-
command: "git push -u origin feature/multiple-pushurl",
|
|
858
|
-
input: JSON.stringify({ scopeBudget: budget() }),
|
|
859
|
-
}, undefined, undefined, extensionContext(fixture.repository, true)),
|
|
860
|
-
/multiple pushurl|one effective push destination/i,
|
|
861
|
-
);
|
|
862
|
-
});
|
|
863
|
-
|
|
864
|
-
test("controller release fast path bypasses receipt validation only for the proven immutable origin/main SHA", async (t) => {
|
|
865
|
-
const fixture = createRepository(t, true);
|
|
866
|
-
assert.ok(fixture.finalCommit && fixture.finalTree && fixture.tagObject);
|
|
867
|
-
const remotePath = join(fixture.parent, "remote.git");
|
|
868
|
-
git(fixture.repository, "-c", "user.name=Review Controller", "-c", "user.email=review-controller@example.invalid", "tag", "-a", "v1.0.5", "-m", "patch release", fixture.finalCommit);
|
|
869
|
-
execFileSync("git", ["clone", "--bare", fixture.repository, remotePath], {
|
|
870
|
-
cwd: fixture.parent,
|
|
871
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
872
|
-
});
|
|
873
|
-
execFileSync("git", ["--git-dir", remotePath, "update-ref", "refs/heads/main", fixture.finalCommit]);
|
|
874
|
-
git(fixture.repository, "remote", "add", "origin", remotePath);
|
|
875
|
-
const { controller, toolCall } = registerRuntime();
|
|
876
|
-
const ctx = extensionContext(fixture.repository, true);
|
|
877
|
-
// Local publication inputs must be irrelevant: dirty worktree during validation.
|
|
878
|
-
writeFileSync(join(fixture.repository, "app.ts"), "export const value = 3; // dirty worktree\n");
|
|
879
|
-
const releaseEvidence = {
|
|
880
|
-
protected_ref: "refs/heads/main",
|
|
881
|
-
remote: "origin",
|
|
882
|
-
ci: { revision: fixture.finalCommit, status: "success" },
|
|
883
|
-
external_evidence: "none",
|
|
884
|
-
post_incident: false,
|
|
885
|
-
};
|
|
886
|
-
|
|
887
|
-
const command = "gh release create v1.0.5 --notes bounded";
|
|
888
|
-
const validated = await controllerCall(controller, ctx, {
|
|
889
|
-
operation: "validate",
|
|
890
|
-
idempotencyKey: "release-fast-path",
|
|
891
|
-
command,
|
|
892
|
-
input: JSON.stringify({ scopeBudget: budget(), release: releaseEvidence }),
|
|
893
|
-
});
|
|
894
|
-
const result = validated.result as Record<string, unknown>;
|
|
895
|
-
assert.equal(result.status, "allow", JSON.stringify(validated));
|
|
896
|
-
assert.equal(result.actor_count, 0);
|
|
897
|
-
const fastPath = validated.release_fast_path as Record<string, unknown>;
|
|
898
|
-
assert.equal(fastPath.eligible, true);
|
|
899
|
-
assert.equal(fastPath.remote_head, fixture.finalCommit);
|
|
900
|
-
assert.equal(typeof validated.authorization, "object");
|
|
901
|
-
assert.equal(await toolCall({ toolName: "bash", input: { command } }, ctx), undefined);
|
|
902
|
-
|
|
903
|
-
const revalidated = await controllerCall(controller, ctx, {
|
|
904
|
-
operation: "validate",
|
|
905
|
-
idempotencyKey: "release-fast-path-recheck",
|
|
906
|
-
command,
|
|
907
|
-
input: JSON.stringify({ scopeBudget: budget(), release: releaseEvidence }),
|
|
908
|
-
});
|
|
909
|
-
assert.equal((revalidated.result as Record<string, unknown>).status, "allow");
|
|
910
|
-
execFileSync("git", ["--git-dir", remotePath, "update-ref", "refs/heads/main", fixture.baseCommit]);
|
|
911
|
-
const advanced = await toolCall({ toolName: "bash", input: { command } }, ctx);
|
|
912
|
-
assert.equal(advanced?.block, true);
|
|
913
|
-
assert.match(advanced?.reason ?? "", /advanced|re-proven/i);
|
|
914
|
-
});
|
|
915
|
-
|
|
916
|
-
test("controller routes exact tag-create pushes through the release fast path before GitHub release creation", async (t) => {
|
|
917
|
-
t.after(() => { releaseCheckRuns = { total_count: 1, returned: 1, checks: [["completed", "success"]] }; });
|
|
918
|
-
const fixture = createRepository(t, true);
|
|
919
|
-
assert.ok(fixture.finalCommit && fixture.tagObject);
|
|
920
|
-
const remotePath = join(fixture.parent, "remote.git");
|
|
921
|
-
git(fixture.repository, "-c", "user.name=Review Controller", "-c", "user.email=review-controller@example.invalid", "tag", "-a", "v1.0.5", "-m", "patch release", fixture.finalCommit);
|
|
922
|
-
execFileSync("git", ["init", "--bare", remotePath], { cwd: fixture.parent, stdio: ["ignore", "pipe", "pipe"] });
|
|
923
|
-
git(fixture.repository, "remote", "add", "origin", remotePath);
|
|
924
|
-
git(fixture.repository, "push", "origin", `${fixture.finalCommit}:refs/heads/main`);
|
|
925
|
-
const { controller, toolCall } = registerRuntime();
|
|
926
|
-
const ctx = extensionContext(fixture.repository, true);
|
|
927
|
-
const releaseEvidence = {
|
|
928
|
-
protected_ref: "refs/heads/main",
|
|
929
|
-
remote: "origin",
|
|
930
|
-
ci: { revision: fixture.finalCommit, status: "success" },
|
|
931
|
-
external_evidence: "none",
|
|
932
|
-
post_incident: false,
|
|
933
|
-
};
|
|
934
|
-
const pushCommand = "git push origin refs/tags/v1.0.5";
|
|
935
|
-
const pushed = await controllerCall(controller, ctx, {
|
|
936
|
-
operation: "validate",
|
|
937
|
-
idempotencyKey: "tag-push-release-fast-path",
|
|
938
|
-
command: pushCommand,
|
|
939
|
-
input: JSON.stringify({ scopeBudget: budget(), release: releaseEvidence }),
|
|
940
|
-
});
|
|
941
|
-
assert.equal((pushed.result as Record<string, unknown>).status, "allow", JSON.stringify(pushed));
|
|
942
|
-
assert.equal((pushed.result as Record<string, unknown>).actor_count, 0);
|
|
943
|
-
assert.equal((pushed.derived_target as Record<string, unknown>).kind, "push");
|
|
944
|
-
assert.equal((pushed.authorization as Record<string, unknown>).target_hash, canonicalHash(pushed.derived_target));
|
|
945
|
-
const fastPathAuthorization = (pushed.authorization as { release_fast_path: Record<string, unknown> }).release_fast_path;
|
|
946
|
-
assert.equal(fastPathAuthorization.expected_ci_revision, fixture.finalCommit);
|
|
947
|
-
assert.equal(fastPathAuthorization.expected_ci_status, "success");
|
|
948
|
-
assert.equal(await toolCall({ toolName: "bash", input: { command: pushCommand } }, ctx), undefined);
|
|
949
|
-
assert.equal(releaseCheckRunArguments.at(-1)?.[1], `repos/{owner}/{repo}/commits/${fixture.finalCommit}/check-runs?per_page=100`);
|
|
950
|
-
git(fixture.repository, "push", "origin", "refs/tags/v1.0.5");
|
|
951
|
-
for (const [tag, checks] of [
|
|
952
|
-
["v1.0.6", [["completed", "failure"]]],
|
|
953
|
-
["v1.0.7", [["in_progress", null]]],
|
|
954
|
-
] as const) {
|
|
955
|
-
git(fixture.repository, "-c", "user.name=Review Controller", "-c", "user.email=review-controller@example.invalid", "tag", "-a", tag, "-m", "ci recheck", fixture.finalCommit);
|
|
956
|
-
releaseCheckRuns = { total_count: 1, returned: 1, checks: [["completed", "success"]] };
|
|
957
|
-
const command = `git push origin refs/tags/${tag}`;
|
|
958
|
-
await controllerCall(controller, ctx, {
|
|
959
|
-
operation: "validate", idempotencyKey: `tag-push-ci-${tag}`, command,
|
|
960
|
-
input: JSON.stringify({ scopeBudget: budget(), release: releaseEvidence }),
|
|
961
|
-
});
|
|
962
|
-
releaseCheckRuns = { total_count: 1, returned: 1, checks };
|
|
963
|
-
assert.equal((await toolCall({ toolName: "bash", input: { command } }, ctx))?.block, true);
|
|
964
|
-
}
|
|
965
|
-
releaseCheckRuns = { total_count: 1, returned: 1, checks: [["completed", "success"]] };
|
|
966
|
-
assert.equal(
|
|
967
|
-
execFileSync("git", ["--git-dir", remotePath, "rev-parse", "refs/tags/v1.0.5^{object}"], { encoding: "utf8" }).trim(),
|
|
968
|
-
git(fixture.repository, "rev-parse", "refs/tags/v1.0.5^{object}"),
|
|
969
|
-
);
|
|
970
|
-
assert.equal(
|
|
971
|
-
execFileSync("git", ["--git-dir", remotePath, "rev-parse", "refs/tags/v1.0.5^{commit}"], { encoding: "utf8" }).trim(),
|
|
972
|
-
fixture.finalCommit,
|
|
973
|
-
);
|
|
974
|
-
|
|
975
|
-
const releaseCommand = "gh release create v1.0.5 --title patch --notes-file notes.md";
|
|
976
|
-
const released = await controllerCall(controller, ctx, {
|
|
977
|
-
operation: "validate",
|
|
978
|
-
idempotencyKey: "remote-tag-release-fast-path",
|
|
979
|
-
command: releaseCommand,
|
|
980
|
-
input: JSON.stringify({ scopeBudget: budget(), release: releaseEvidence }),
|
|
981
|
-
});
|
|
982
|
-
assert.equal((released.result as Record<string, unknown>).status, "allow", JSON.stringify(released));
|
|
983
|
-
assert.equal((released.result as Record<string, unknown>).actor_count, 0);
|
|
984
|
-
assert.equal((released.derived_target as Record<string, unknown>).peeled_commit, fixture.finalCommit);
|
|
985
|
-
assert.equal(await toolCall({ toolName: "bash", input: { command: releaseCommand } }, ctx), undefined);
|
|
986
|
-
await controllerCall(controller, ctx, {
|
|
987
|
-
operation: "validate",
|
|
988
|
-
idempotencyKey: "remote-tag-release-fast-path-recheck",
|
|
989
|
-
command: releaseCommand,
|
|
990
|
-
input: JSON.stringify({ scopeBudget: budget(), release: releaseEvidence }),
|
|
991
|
-
});
|
|
992
|
-
execFileSync("git", ["--git-dir", remotePath, "update-ref", "refs/heads/main", fixture.baseCommit]);
|
|
993
|
-
const advanced = await toolCall({ toolName: "bash", input: { command: releaseCommand } }, ctx);
|
|
994
|
-
assert.equal(advanced?.block, true);
|
|
995
|
-
assert.match(advanced?.reason ?? "", /advanced|re-proven/i);
|
|
996
|
-
});
|
|
997
|
-
|
|
998
|
-
test("controller rejects ambiguous or ineligible tag pushes from the receipt-free release fast path", async (t) => {
|
|
999
|
-
const fixture = createRepository(t, true);
|
|
1000
|
-
assert.ok(fixture.finalCommit);
|
|
1001
|
-
const remotePath = join(fixture.parent, "remote.git");
|
|
1002
|
-
execFileSync("git", ["init", "--bare", remotePath], { cwd: fixture.parent, stdio: ["ignore", "pipe", "pipe"] });
|
|
1003
|
-
git(fixture.repository, "remote", "add", "origin", remotePath);
|
|
1004
|
-
git(fixture.repository, "push", "origin", `${fixture.finalCommit}:refs/heads/main`);
|
|
1005
|
-
git(fixture.repository, "-c", "user.name=Review Controller", "-c", "user.email=review-controller@example.invalid", "tag", "-a", "v1.0.5", "-m", "patch release", fixture.finalCommit);
|
|
1006
|
-
git(fixture.repository, "-c", "user.name=Review Controller", "-c", "user.email=review-controller@example.invalid", "tag", "-a", "nightly", "-m", "nightly", fixture.finalCommit);
|
|
1007
|
-
git(fixture.repository, "-c", "user.name=Review Controller", "-c", "user.email=review-controller@example.invalid", "tag", "-a", "v1.0.7", "-m", "base release", fixture.baseCommit);
|
|
1008
|
-
git(fixture.repository, "tag", "v1.0.6", fixture.finalCommit);
|
|
1009
|
-
const { controller, toolCall } = registerRuntime();
|
|
1010
|
-
const ctx = extensionContext(fixture.repository, true);
|
|
1011
|
-
const releaseEvidence = {
|
|
1012
|
-
protected_ref: "refs/heads/main",
|
|
1013
|
-
remote: "origin",
|
|
1014
|
-
ci: { revision: fixture.finalCommit, status: "success" },
|
|
1015
|
-
external_evidence: "none",
|
|
1016
|
-
post_incident: false,
|
|
1017
|
-
};
|
|
1018
|
-
const validate = async (command: string, evidence = releaseEvidence): Promise<void> => {
|
|
1019
|
-
await assert.rejects(controller.execute("ineligible-tag-push", {
|
|
1020
|
-
operation: "validate",
|
|
1021
|
-
idempotencyKey: `ineligible-${command}`,
|
|
1022
|
-
command,
|
|
1023
|
-
input: JSON.stringify({ scopeBudget: budget(), release: evidence }),
|
|
1024
|
-
}, undefined, undefined, ctx));
|
|
1025
|
-
};
|
|
1026
|
-
const tagCreateTarget = (tag: string) => ({
|
|
1027
|
-
kind: "push" as const,
|
|
1028
|
-
remote: "origin",
|
|
1029
|
-
destination_id: "a".repeat(64),
|
|
1030
|
-
updates: [{
|
|
1031
|
-
kind: "create" as const,
|
|
1032
|
-
source_ref: `refs/tags/${tag}`,
|
|
1033
|
-
destination_ref: `refs/tags/${tag}`,
|
|
1034
|
-
old_object: null,
|
|
1035
|
-
old_peeled_commit: null,
|
|
1036
|
-
old_tree: null,
|
|
1037
|
-
new_object: fixture.finalCommit!,
|
|
1038
|
-
new_peeled_commit: fixture.finalCommit!,
|
|
1039
|
-
new_tree: fixture.finalTree!,
|
|
1040
|
-
}],
|
|
1041
|
-
});
|
|
1042
|
-
|
|
1043
|
-
await validate("git push origin :refs/tags/v1.0.5");
|
|
1044
|
-
await validate("git push origin refs/tags/v1.0.5:refs/tags/v1.0.6");
|
|
1045
|
-
await validate("git push origin refs/tags/v1.0.5 refs/tags/v1.0.6");
|
|
1046
|
-
await validate("git push origin refs/tags/nightly");
|
|
1047
|
-
await validate("git push origin refs/tags/v1.0.7");
|
|
1048
|
-
await validate("git push origin refs/tags/v1.0.5", { ...releaseEvidence, ci: { revision: fixture.finalCommit, status: "failure" } });
|
|
1049
|
-
await validate("git push origin refs/tags/v1.0.5", { ...releaseEvidence, ci: { revision: fixture.finalCommit, status: "pending" } });
|
|
1050
|
-
await validate("git push origin v1.0.5");
|
|
1051
|
-
await validate("git push --force origin refs/tags/v1.0.5");
|
|
1052
|
-
for (const command of [
|
|
1053
|
-
"git push --exec git-receive-pack origin refs/tags/v1.0.5",
|
|
1054
|
-
"git push --receive-pack git-receive-pack origin refs/tags/v1.0.5",
|
|
1055
|
-
]) {
|
|
1056
|
-
await validate(command);
|
|
1057
|
-
assert.equal((await toolCall({ toolName: "bash", input: { command } }, ctx))?.block, true);
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
for (const tag of [
|
|
1061
|
-
"v1.2.3+foo+bar",
|
|
1062
|
-
"v1.2.3-foo..bar",
|
|
1063
|
-
"v1.2.3-01",
|
|
1064
|
-
"v01.2.3",
|
|
1065
|
-
"v1.2.3-",
|
|
1066
|
-
"v1.2.3+",
|
|
1067
|
-
]) {
|
|
1068
|
-
assert.equal(projectExactTagCreatePushAsReleaseV1(tagCreateTarget(tag)), null, tag);
|
|
1069
|
-
}
|
|
1070
|
-
assert.notEqual(projectExactTagCreatePushAsReleaseV1(tagCreateTarget("v1.2.3-rc.1+build.5")), null);
|
|
1071
|
-
for (const tag of ["v1.2.3+foo+bar", "v1.2.3-01", "v01.2.3", "v1.2.3-", "v1.2.3+"]) {
|
|
1072
|
-
git(fixture.repository, "-c", "user.name=Review Controller", "-c", "user.email=review-controller@example.invalid", "tag", "-a", tag, "-m", "invalid semver", fixture.finalCommit);
|
|
1073
|
-
await validate(`git push origin refs/tags/${tag}`);
|
|
1074
|
-
}
|
|
1075
|
-
await validate("git push origin refs/tags/v1.2.3-foo..bar");
|
|
1076
|
-
|
|
1077
|
-
const lightweight = await controllerCall(controller, ctx, {
|
|
1078
|
-
operation: "validate",
|
|
1079
|
-
idempotencyKey: "lightweight-exact-tag-push",
|
|
1080
|
-
command: "git push origin refs/tags/v1.0.6",
|
|
1081
|
-
input: JSON.stringify({ scopeBudget: budget(), release: releaseEvidence }),
|
|
1082
|
-
});
|
|
1083
|
-
assert.equal((lightweight.result as Record<string, unknown>).status, "allow", JSON.stringify(lightweight));
|
|
1084
|
-
assert.equal((lightweight.result as Record<string, unknown>).actor_count, 0);
|
|
1085
|
-
|
|
1086
|
-
git(fixture.repository, "push", "origin", "refs/tags/v1.0.5");
|
|
1087
|
-
await validate("git push origin refs/tags/v1.0.5");
|
|
1088
|
-
});
|
|
1089
|
-
|
|
1090
|
-
test("controller release tag-push fast path binds the evidence remote and re-proves one fetch/push destination at bash time", async (t) => {
|
|
1091
|
-
const fixture = createRepository(t, true);
|
|
1092
|
-
assert.ok(fixture.finalCommit);
|
|
1093
|
-
const fetchRemotePath = join(fixture.parent, "fetch.git");
|
|
1094
|
-
const pushRemotePath = join(fixture.parent, "push.git");
|
|
1095
|
-
const alternateRemotePath = join(fixture.parent, "alternate.git");
|
|
1096
|
-
for (const path of [fetchRemotePath, pushRemotePath, alternateRemotePath]) {
|
|
1097
|
-
execFileSync("git", ["clone", "--bare", fixture.repository, path], { cwd: fixture.parent, stdio: ["ignore", "pipe", "pipe"] });
|
|
1098
|
-
}
|
|
1099
|
-
git(fixture.repository, "remote", "add", "origin", fetchRemotePath);
|
|
1100
|
-
git(fixture.repository, "remote", "set-url", "--add", "--push", "origin", pushRemotePath);
|
|
1101
|
-
git(fixture.repository, "remote", "add", "evidence", alternateRemotePath);
|
|
1102
|
-
execFileSync("git", ["--git-dir", fetchRemotePath, "update-ref", "refs/heads/main", fixture.finalCommit]);
|
|
1103
|
-
git(fixture.repository, "-c", "user.name=Review Controller", "-c", "user.email=review-controller@example.invalid", "tag", "-a", "v1.0.5", "-m", "patch release", fixture.finalCommit);
|
|
1104
|
-
const { controller, toolCall } = registerRuntime();
|
|
1105
|
-
const ctx = extensionContext(fixture.repository, true);
|
|
1106
|
-
const evidence = {
|
|
1107
|
-
protected_ref: "refs/heads/main",
|
|
1108
|
-
remote: "origin",
|
|
1109
|
-
ci: { revision: fixture.finalCommit, status: "success" },
|
|
1110
|
-
external_evidence: "none",
|
|
1111
|
-
post_incident: false,
|
|
1112
|
-
};
|
|
1113
|
-
const command = "git push origin refs/tags/v1.0.5";
|
|
1114
|
-
|
|
1115
|
-
await assert.rejects(
|
|
1116
|
-
controller.execute("split-fetch-push-release-fast-path", {
|
|
1117
|
-
operation: "validate",
|
|
1118
|
-
idempotencyKey: "split-fetch-push-release-fast-path",
|
|
1119
|
-
command,
|
|
1120
|
-
input: JSON.stringify({ scopeBudget: budget(), release: evidence }),
|
|
1121
|
-
}, undefined, undefined, ctx),
|
|
1122
|
-
/fetch.*push|destination|identity/i,
|
|
1123
|
-
);
|
|
1124
|
-
await assert.rejects(
|
|
1125
|
-
controller.execute("nonmatching-evidence-remote", {
|
|
1126
|
-
operation: "validate",
|
|
1127
|
-
idempotencyKey: "nonmatching-evidence-remote",
|
|
1128
|
-
command,
|
|
1129
|
-
input: JSON.stringify({ scopeBudget: budget(), release: { ...evidence, remote: "evidence" } }),
|
|
1130
|
-
}, undefined, undefined, ctx),
|
|
1131
|
-
/evidence remote.*match/i,
|
|
1132
|
-
);
|
|
1133
|
-
|
|
1134
|
-
git(fixture.repository, "remote", "set-url", "--delete", "--push", "origin", pushRemotePath);
|
|
1135
|
-
git(fixture.repository, "remote", "set-url", "--add", "--push", "origin", fetchRemotePath);
|
|
1136
|
-
const validated = await controllerCall(controller, ctx, {
|
|
1137
|
-
operation: "validate",
|
|
1138
|
-
idempotencyKey: "pushurl-drift-release-fast-path",
|
|
1139
|
-
command,
|
|
1140
|
-
input: JSON.stringify({ scopeBudget: budget(), release: evidence }),
|
|
1141
|
-
});
|
|
1142
|
-
assert.equal((validated.result as Record<string, unknown>).status, "allow", JSON.stringify(validated));
|
|
1143
|
-
git(fixture.repository, "remote", "set-url", "--delete", "--push", "origin", fetchRemotePath);
|
|
1144
|
-
git(fixture.repository, "remote", "set-url", "--add", "--push", "origin", pushRemotePath);
|
|
1145
|
-
const drifted = await toolCall({ toolName: "bash", input: { command } }, ctx);
|
|
1146
|
-
assert.equal(drifted?.block, true);
|
|
1147
|
-
assert.match(drifted?.reason ?? "", /target|destination|fetch.*push|binding/i);
|
|
1148
|
-
});
|
|
1149
|
-
|
|
1150
|
-
test("failed or unprovable release fast-path conditions fall back to native receipt validation and fail closed", async (t) => {
|
|
1151
|
-
const fixture = createRepository(t, true);
|
|
1152
|
-
assert.ok(fixture.finalCommit && fixture.finalTree);
|
|
1153
|
-
const remotePath = join(fixture.parent, "remote.git");
|
|
1154
|
-
execFileSync("git", ["clone", "--bare", fixture.repository, remotePath], {
|
|
1155
|
-
cwd: fixture.parent,
|
|
1156
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1157
|
-
});
|
|
1158
|
-
execFileSync("git", ["--git-dir", remotePath, "update-ref", "refs/heads/main", fixture.finalCommit]);
|
|
1159
|
-
git(fixture.repository, "remote", "add", "origin", remotePath);
|
|
1160
|
-
const { controller } = registerRuntime();
|
|
1161
|
-
const ctx = extensionContext(fixture.repository, true);
|
|
1162
|
-
|
|
1163
|
-
for (const [key, release] of [
|
|
1164
|
-
["failed-ci", { protected_ref: "refs/heads/main", remote: "origin", ci: { revision: fixture.finalCommit, status: "failure" }, external_evidence: "none", post_incident: false }],
|
|
1165
|
-
["post-incident", { protected_ref: "refs/heads/main", remote: "origin", ci: { revision: fixture.finalCommit, status: "success" }, external_evidence: "none", post_incident: true }],
|
|
1166
|
-
["escalating-evidence", { protected_ref: "refs/heads/main", remote: "origin", ci: { revision: fixture.finalCommit, status: "success" }, external_evidence: "escalating", post_incident: false }],
|
|
1167
|
-
] as const) {
|
|
1168
|
-
await assert.rejects(
|
|
1169
|
-
controller.execute(
|
|
1170
|
-
"release-fast-path-fallback",
|
|
1171
|
-
{
|
|
1172
|
-
operation: "validate",
|
|
1173
|
-
idempotencyKey: `release-fast-path-fallback-${key}`,
|
|
1174
|
-
command: "gh release create v1.2.3 --notes bounded",
|
|
1175
|
-
input: JSON.stringify({ scopeBudget: budget(), release }),
|
|
1176
|
-
},
|
|
1177
|
-
undefined,
|
|
1178
|
-
undefined,
|
|
1179
|
-
ctx,
|
|
1180
|
-
),
|
|
1181
|
-
/lineageId/i,
|
|
1182
|
-
key,
|
|
1183
|
-
);
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
await assert.rejects(
|
|
1187
|
-
controller.execute(
|
|
1188
|
-
"release-evidence-wrong-event",
|
|
1189
|
-
{
|
|
1190
|
-
operation: "validate",
|
|
1191
|
-
lineageId: "controller-fast-path-wrong-event",
|
|
1192
|
-
idempotencyKey: "release-evidence-wrong-event",
|
|
1193
|
-
command: "git commit -m bounded",
|
|
1194
|
-
input: JSON.stringify({
|
|
1195
|
-
scopeBudget: budget(),
|
|
1196
|
-
release: { protected_ref: "refs/heads/main", remote: "origin", ci: { revision: fixture.finalCommit, status: "success" }, external_evidence: "none", post_incident: false },
|
|
1197
|
-
}),
|
|
1198
|
-
},
|
|
1199
|
-
undefined,
|
|
1200
|
-
undefined,
|
|
1201
|
-
ctx,
|
|
1202
|
-
),
|
|
1203
|
-
/pre-release/i,
|
|
1204
|
-
);
|
|
1205
|
-
|
|
1206
|
-
const receiptFallback = await (async () => {
|
|
1207
|
-
createTerminalAuthority(fixture, "controller-fast-path-fallback");
|
|
1208
|
-
return controllerCall(controller, ctx, {
|
|
1209
|
-
operation: "validate",
|
|
1210
|
-
lineageId: "controller-fast-path-fallback",
|
|
1211
|
-
idempotencyKey: "release-fast-path-receipt-fallback",
|
|
1212
|
-
command: "gh release create v1.2.3 --notes bounded",
|
|
1213
|
-
input: JSON.stringify({
|
|
1214
|
-
scopeBudget: budget(),
|
|
1215
|
-
release: { protected_ref: "refs/heads/main", remote: "origin", ci: { revision: fixture.finalCommit, status: "failure" }, external_evidence: "none", post_incident: false },
|
|
1216
|
-
}),
|
|
1217
|
-
});
|
|
1218
|
-
})();
|
|
1219
|
-
assert.equal((receiptFallback.result as Record<string, unknown>).status, "allow");
|
|
1220
|
-
const fallbackFastPath = receiptFallback.release_fast_path as Record<string, unknown>;
|
|
1221
|
-
assert.equal(fallbackFastPath.eligible, false);
|
|
1222
|
-
assert.match(String(fallbackFastPath.reason), /required CI/i);
|
|
432
|
+
assert.match(readFileSync("skills/gentle-ai/SKILL.md", "utf8"), /sole lifecycle authority/i);
|
|
1223
433
|
});
|