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.
Files changed (130) hide show
  1. package/README.md +51 -80
  2. package/assets/agents/review-readability.md +0 -2
  3. package/assets/agents/review-reliability.md +0 -2
  4. package/assets/agents/review-resilience.md +0 -2
  5. package/assets/agents/review-risk.md +0 -2
  6. package/assets/agents/sdd-apply.md +5 -3
  7. package/assets/agents/sdd-proposal.md +2 -0
  8. package/assets/agents/sdd-research.md +54 -0
  9. package/assets/agents/sdd-status.md +4 -4
  10. package/assets/agents/sdd-tasks.md +2 -3
  11. package/assets/agents/sdd-verify.md +21 -1
  12. package/assets/chains/sdd-full.chain.md +1 -1
  13. package/assets/chains/sdd-verify.chain.md +1 -1
  14. package/assets/orchestrator-delegation.md +54 -248
  15. package/assets/orchestrator-memory.md +2 -0
  16. package/assets/orchestrator.md +22 -38
  17. package/assets/sdd-orchestrator-workflow.md +30 -37
  18. package/assets/support/sdd-status-contract.md +7 -7
  19. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  20. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  21. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  22. package/docs/native-authority-architecture.md +9 -11
  23. package/docs/review-integration.md +27 -373
  24. package/extensions/ask-user-choice.ts +151 -0
  25. package/extensions/gentle-ai.ts +1778 -3673
  26. package/extensions/quiet-tools.ts +515 -32
  27. package/extensions/sdd-init.ts +4 -8
  28. package/lib/gentle-ai-renderer.ts +70 -0
  29. package/lib/model-routing-authority.ts +133 -0
  30. package/lib/native-review-cli.ts +372 -940
  31. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  32. package/lib/review-candidate-view.ts +341 -132
  33. package/lib/review-host-relay.ts +210 -68
  34. package/lib/review-integration-v2.ts +839 -307
  35. package/lib/review-last-event-controller.ts +35 -0
  36. package/lib/sdd-preflight.ts +177 -66
  37. package/lib/sdd-status.ts +66 -111
  38. package/lib/terminal-theme.ts +1 -1
  39. package/package.json +83 -82
  40. package/runtime/gentle-ai-binary.mjs +1 -1
  41. package/runtime/native-review-cli.mjs +322 -890
  42. package/runtime/review-integration-v2.mjs +789 -257
  43. package/runtime/review-relay-contract.mjs +1 -1
  44. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
  45. package/scripts/gentle-ai-installer.mjs +75 -22
  46. package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
  47. package/scripts/test-packed-runner.mjs +4 -7
  48. package/scripts/verify-package-files.mjs +12 -13
  49. package/skills/_shared/review-ledger-contract.md +8 -14
  50. package/skills/chained-pr/SKILL.md +3 -0
  51. package/skills/cognitive-doc-design/SKILL.md +1 -1
  52. package/skills/comment-writer/SKILL.md +1 -1
  53. package/skills/gentle-ai/SKILL.md +7 -74
  54. package/skills/judgment-day/SKILL.md +5 -7
  55. package/skills/rdd-defect-workflow/SKILL.md +3 -3
  56. package/skills/release/SKILL.md +3 -3
  57. package/skills/skill-registry/SKILL.md +1 -1
  58. package/skills/work-unit-commits/SKILL.md +3 -1
  59. package/tests/artifact-language.test.ts +24 -11
  60. package/tests/ask-user-choice.test.ts +264 -0
  61. package/tests/codegraph-tools.test.ts +3 -3
  62. package/tests/crosslane/cross-lane.mjs +15 -1168
  63. package/tests/delegated-key-learnings-contract.test.ts +8 -6
  64. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  65. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  66. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  67. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  68. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  69. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  70. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  71. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  72. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  73. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  74. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  75. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  76. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  77. package/tests/gentle-ai-binary.test.ts +83 -13
  78. package/tests/gentle-ai-dev-binary.test.ts +51 -228
  79. package/tests/gentle-ai-installer.test.ts +137 -47
  80. package/tests/gentle-ai.test.ts +820 -131
  81. package/tests/gentle-theme.test.ts +133 -0
  82. package/tests/maintainer/provider-relay.maintest.ts +340 -4
  83. package/tests/model-routing-authority.test.ts +257 -0
  84. package/tests/native-review-capability-contract.test.ts +28 -1
  85. package/tests/native-review-cli.test.ts +586 -962
  86. package/tests/native-review-consent.test.ts +150 -66
  87. package/tests/native-review-parity-runtime.test.ts +80 -359
  88. package/tests/native-review-parity.test.ts +674 -925
  89. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  90. package/tests/orchestrator-budget.test.ts +132 -88
  91. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  92. package/tests/package-manifest.test.ts +37 -35
  93. package/tests/provider-defect-handoff.test.ts +15 -118
  94. package/tests/quiet-tool-rendering.test.ts +1055 -28
  95. package/tests/review-authority-recovery-docs.test.ts +1 -2
  96. package/tests/review-candidate-view.test.ts +409 -3
  97. package/tests/review-compact-contract.test.ts +4 -30
  98. package/tests/review-controller-lock-status.test.ts +2 -2
  99. package/tests/review-controller-native-recovery.test.ts +363 -1450
  100. package/tests/review-controller-native-routing.test.ts +1251 -5642
  101. package/tests/review-controller-retired-ops.test.ts +1 -1
  102. package/tests/review-controller-workspace-root.test.ts +222 -71
  103. package/tests/review-controller.test.ts +26 -816
  104. package/tests/review-corrected-finalize-binding.test.ts +112 -153
  105. package/tests/review-dispatch-hydration-gap.test.ts +1 -53
  106. package/tests/review-gate.test.ts +0 -45
  107. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  108. package/tests/review-host-relay-routing.test.ts +117 -82
  109. package/tests/review-host-relay.test.ts +242 -8
  110. package/tests/review-integration-v2-forward.test.ts +576 -553
  111. package/tests/review-integration-v2.test.ts +239 -165
  112. package/tests/review-last-event-closure.test.ts +408 -0
  113. package/tests/review-ledger-contract.test.ts +97 -35
  114. package/tests/review-recovered-lineage-routing.test.ts +0 -47
  115. package/tests/review-relay-transport-agent.test.ts +119 -46
  116. package/tests/review-snapshot.test.ts +3 -2
  117. package/tests/runtime-harness.mjs +361 -196
  118. package/tests/sdd-agent-tools.test.ts +36 -0
  119. package/tests/sdd-preflight.test.ts +81 -15
  120. package/tests/sdd-status.test.ts +109 -110
  121. package/tests/skill-collision-prefixes.test.ts +5 -8
  122. package/tests/writer-edit-surface-scope.test.ts +230 -0
  123. package/themes/Gentleman-Cute.json +94 -0
  124. package/themes/Gentleman-Sexy.json +92 -0
  125. package/lib/git-commit-transaction.ts +0 -861
  126. package/runtime/git-commit-transaction.mjs +0 -862
  127. package/scripts/run-git-commit-transaction.mjs +0 -35
  128. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  129. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  130. 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, commitFinal: boolean): RepositoryFixture {
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
- if (!commitFinal) return { parent, repository, baseCommit, baseTree };
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: fixture.finalTree,
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, false);
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
- fixture.finalTree = git(fixture.repository, "write-tree");
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 blocks archive for a recovery-required marker without a supersession record", async (t) => {
275
- const fixture = createRepository(t, false);
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, "blocked");
292
- assert.equal(status.nextRecommended, "parent-lifecycle");
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, false);
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, false);
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, false);
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, false);
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 and orchestrator contracts specify inspect-first compact facade without cascade", () => {
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
- assert.match(toolContract, /operation.*start.*finalize.*validate.*input/is);
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, answer-consent, or FINALIZE.*target-scoped native status.*declared action/is);
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.match(contract, /INSPECT before START|inspect.*before.*start/is, path);
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
- t.after(() => {
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
  });