gentle-pi 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/README.md +76 -79
  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-explore.md +1 -0
  8. package/assets/agents/sdd-proposal.md +2 -0
  9. package/assets/agents/sdd-research.md +54 -0
  10. package/assets/agents/sdd-status.md +4 -4
  11. package/assets/agents/sdd-tasks.md +2 -3
  12. package/assets/agents/sdd-verify.md +21 -1
  13. package/assets/chains/sdd-full.chain.md +1 -1
  14. package/assets/chains/sdd-verify.chain.md +1 -1
  15. package/assets/orchestrator-delegation.md +56 -249
  16. package/assets/orchestrator-memory.md +2 -0
  17. package/assets/orchestrator.md +20 -40
  18. package/assets/sdd-orchestrator-workflow.md +30 -37
  19. package/assets/support/sdd-status-contract.md +7 -7
  20. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  21. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  22. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  23. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  24. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
  25. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
  26. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
  27. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
  28. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
  29. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
  30. package/docs/native-authority-architecture.md +9 -11
  31. package/docs/review-integration.md +27 -373
  32. package/extensions/ask-user-choice.ts +151 -0
  33. package/extensions/gentle-ai.ts +2349 -3785
  34. package/extensions/quiet-tools.ts +515 -32
  35. package/extensions/sdd-init.ts +4 -8
  36. package/lib/agent-home.ts +8 -0
  37. package/lib/gentle-ai-renderer.ts +70 -0
  38. package/lib/model-routing-authority.ts +133 -0
  39. package/lib/native-review-cli.ts +387 -940
  40. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  41. package/lib/provider-contract-bundle.ts +88 -6
  42. package/lib/review-candidate-view.ts +341 -132
  43. package/lib/review-host-relay.ts +324 -96
  44. package/lib/review-integration-v2.ts +868 -307
  45. package/lib/review-last-event-controller.ts +35 -0
  46. package/lib/sdd-preflight.ts +179 -68
  47. package/lib/sdd-status.ts +66 -111
  48. package/lib/terminal-theme.ts +1 -1
  49. package/package.json +83 -82
  50. package/runtime/gentle-ai-binary.mjs +1 -1
  51. package/runtime/native-review-cli.mjs +337 -890
  52. package/runtime/review-integration-v2.mjs +816 -255
  53. package/runtime/review-relay-contract.mjs +1 -1
  54. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
  55. package/scripts/gentle-ai-installer.mjs +75 -22
  56. package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
  57. package/scripts/test-packed-runner.mjs +4 -7
  58. package/scripts/verify-package-files.mjs +23 -23
  59. package/skills/_shared/review-ledger-contract.md +14 -14
  60. package/skills/chained-pr/SKILL.md +3 -0
  61. package/skills/cognitive-doc-design/SKILL.md +1 -1
  62. package/skills/comment-writer/SKILL.md +1 -1
  63. package/skills/gentle-ai/SKILL.md +7 -74
  64. package/skills/issue-creation/SKILL.md +53 -93
  65. package/skills/judgment-day/SKILL.md +5 -7
  66. package/skills/rdd-defect-workflow/SKILL.md +3 -3
  67. package/skills/release/SKILL.md +3 -3
  68. package/skills/skill-registry/SKILL.md +1 -1
  69. package/skills/work-unit-commits/SKILL.md +3 -1
  70. package/tests/artifact-language.test.ts +24 -11
  71. package/tests/ask-user-choice.test.ts +264 -0
  72. package/tests/codegraph-tools.test.ts +3 -3
  73. package/tests/crosslane/cross-lane.mjs +15 -1168
  74. package/tests/delegated-key-learnings-contract.test.ts +8 -6
  75. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  76. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  77. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  78. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  79. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  80. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  81. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  82. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  83. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  84. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  85. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  86. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  87. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  88. package/tests/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
  89. package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
  90. package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
  91. package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
  92. package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
  93. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
  94. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
  95. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -0
  96. package/tests/gentle-ai-binary.test.ts +83 -13
  97. package/tests/gentle-ai-dev-binary.test.ts +51 -228
  98. package/tests/gentle-ai-installer.test.ts +137 -47
  99. package/tests/gentle-ai.test.ts +821 -131
  100. package/tests/gentle-theme.test.ts +133 -0
  101. package/tests/issue-creation-skill.test.ts +103 -0
  102. package/tests/maintainer/provider-relay.maintest.ts +340 -4
  103. package/tests/model-routing-authority.test.ts +257 -0
  104. package/tests/native-review-capability-contract.test.ts +45 -1
  105. package/tests/native-review-cli.test.ts +586 -962
  106. package/tests/native-review-consent.test.ts +150 -66
  107. package/tests/native-review-parity-runtime.test.ts +80 -359
  108. package/tests/native-review-parity.test.ts +674 -925
  109. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  110. package/tests/orchestrator-budget.test.ts +132 -88
  111. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  112. package/tests/package-manifest.test.ts +152 -36
  113. package/tests/provider-contract-bundle.test.ts +76 -0
  114. package/tests/provider-contract-mirror.test.ts +19 -0
  115. package/tests/provider-defect-handoff.test.ts +15 -118
  116. package/tests/quiet-tool-rendering.test.ts +1055 -28
  117. package/tests/review-agent-end-preflight.test.ts +408 -0
  118. package/tests/review-authority-recovery-docs.test.ts +1 -2
  119. package/tests/review-candidate-view.test.ts +409 -3
  120. package/tests/review-compact-contract.test.ts +4 -30
  121. package/tests/review-contract-prompt.test.ts +139 -0
  122. package/tests/review-controller-lock-status.test.ts +2 -2
  123. package/tests/review-controller-native-recovery.test.ts +363 -1450
  124. package/tests/review-controller-native-routing.test.ts +1251 -5642
  125. package/tests/review-controller-retired-ops.test.ts +1 -1
  126. package/tests/review-controller-workspace-root.test.ts +222 -71
  127. package/tests/review-controller.test.ts +26 -816
  128. package/tests/review-corrected-finalize-binding.test.ts +112 -153
  129. package/tests/review-dispatch-hydration-gap.test.ts +1 -53
  130. package/tests/review-gate.test.ts +0 -45
  131. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  132. package/tests/review-host-relay-routing.test.ts +328 -91
  133. package/tests/review-host-relay.test.ts +436 -14
  134. package/tests/review-integration-v2-forward.test.ts +623 -553
  135. package/tests/review-integration-v2.test.ts +239 -165
  136. package/tests/review-last-event-closure.test.ts +408 -0
  137. package/tests/review-ledger-contract.test.ts +97 -35
  138. package/tests/review-recovered-lineage-routing.test.ts +0 -47
  139. package/tests/review-relay-transport-agent.test.ts +119 -46
  140. package/tests/review-snapshot.test.ts +3 -2
  141. package/tests/runtime-harness.mjs +362 -196
  142. package/tests/sdd-agent-tools.test.ts +36 -0
  143. package/tests/sdd-preflight.test.ts +81 -15
  144. package/tests/sdd-status.test.ts +109 -110
  145. package/tests/skill-collision-prefixes.test.ts +5 -8
  146. package/tests/writer-edit-surface-scope.test.ts +366 -0
  147. package/themes/Gentleman-Cute.json +94 -0
  148. package/themes/Gentleman-Sexy.json +92 -0
  149. package/lib/git-commit-transaction.ts +0 -861
  150. package/runtime/git-commit-transaction.mjs +0 -862
  151. package/scripts/run-git-commit-transaction.mjs +0 -35
  152. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  153. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  154. package/tests/git-commit-transaction.test.ts +0 -530
  155. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
  156. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
  157. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
  158. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
  159. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
  160. /package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/schemas/targeted-validator.schema.json +0 -0
@@ -7,13 +7,9 @@ import {
7
7
  NativeReviewCliV216,
8
8
  NativeReviewConsentBindingError,
9
9
  NativeReviewConsentRequiredError,
10
- clearNativeReviewCapabilitiesCacheForTesting,
11
10
  type ExecFileAdapter,
12
11
  } from "../lib/native-review-cli.ts";
13
- import {
14
- NativeReviewCliV216 as RuntimeNativeReviewCliV216,
15
- clearNativeReviewCapabilitiesCacheForTesting as clearRuntimeNativeReviewCapabilitiesCacheForTesting,
16
- } from "../runtime/native-review-cli.mjs";
12
+ import { NativeReviewCliV216 as RuntimeNativeReviewCliV216 } from "../runtime/native-review-cli.mjs";
17
13
  import type { ReviewConsentV2 } from "../lib/review-integration-v2.ts";
18
14
 
19
15
  const fixtureRoot = join(process.cwd(), "contracts", "review-integration", "v2", "fixtures");
@@ -29,7 +25,7 @@ function capabilities(): Record<string, unknown> {
29
25
  function unrelatedStatus(targetIdentity: string): Record<string, unknown> {
30
26
  const status = fixture<Record<string, unknown>>("status.fixture.json");
31
27
  status.applicability = "unrelated";
32
- status.receipt = { status: "not_applicable" };
28
+ delete status.receipt;
33
29
  status.action = "start";
34
30
  status.replayability = "not_replayable";
35
31
  status.target_identity = targetIdentity;
@@ -43,8 +39,41 @@ function unrelatedStatus(targetIdentity: string): Record<string, unknown> {
43
39
  return status;
44
40
  }
45
41
 
42
+ function startTransitionTokens(targetIdentity: string): readonly string[] {
43
+ return [
44
+ "--contract=gentle-ai.review-integration/v2",
45
+ "--cwd=/repo",
46
+ `--target=${targetIdentity}`,
47
+ "--projection=workspace",
48
+ "--agent=pi",
49
+ "--consent=relay",
50
+ ] as const;
51
+ }
52
+
53
+ function executableStartStatus(targetIdentity: string): Record<string, unknown> {
54
+ const status = unrelatedStatus(targetIdentity);
55
+ const tokens = startTransitionTokens(targetIdentity);
56
+ status.next_transition = {
57
+ kind: "execute",
58
+ reason_code: "review_start_required",
59
+ execute: {
60
+ operation: "review.start",
61
+ arguments: tokens.map((token) => {
62
+ const separator = token.indexOf("=");
63
+ return { name: token.slice(2, separator), value: token.slice(separator + 1), token };
64
+ }),
65
+ preconditions: [],
66
+ binding: { target_identity: targetIdentity },
67
+ },
68
+ };
69
+ return status;
70
+ }
71
+
46
72
  function queuedAdapter(outputs: readonly Record<string, unknown>[]): { adapter: ExecFileAdapter; calls: Array<readonly string[]> } {
47
- const queue = [...outputs];
73
+ // V216 no longer probes capabilities before negotiated STATUS. Keep the
74
+ // historical queue construction below, but discard only its obsolete probe
75
+ // response so every remaining entry binds the current invocation.
76
+ const queue = outputs.filter((body) => body.schema !== "gentle-ai.review-integration.capabilities/v2");
48
77
  const calls: Array<readonly string[]> = [];
49
78
  return {
50
79
  calls,
@@ -58,52 +87,49 @@ function queuedAdapter(outputs: readonly Record<string, unknown>[]): { adapter:
58
87
  }
59
88
 
60
89
  function client(adapter: ExecFileAdapter): NativeReviewCliV216 {
61
- clearNativeReviewCapabilitiesCacheForTesting();
62
90
  return new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => executableDigest);
63
91
  }
64
92
 
65
93
  function runtimeClient(adapter: ExecFileAdapter): RuntimeNativeReviewCliV216 {
66
- clearRuntimeNativeReviewCapabilitiesCacheForTesting();
67
94
  return new RuntimeNativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined, () => executableDigest);
68
95
  }
69
96
 
70
- test("negotiated ordinary START declares relay and preserves the complete target-bound consent envelope", async () => {
97
+ test("negotiated ordinary START executes the complete STATUS-rendered relay vector and preserves the consent envelope", async () => {
71
98
  const consent = fixture<Record<string, unknown>>("consent.fixture.json");
72
99
  const target = String(consent.target_identity);
73
- const queue = queuedAdapter([
74
- capabilities(),
75
- unrelatedStatus(target),
76
- consent,
77
- ]);
78
- await assert.rejects(
79
- () => client(queue.adapter).start({ cwd: "/repo" }),
80
- (error: unknown) => {
81
- assert.ok(error instanceof NativeReviewConsentRequiredError);
82
- assert.deepEqual(error.consent.raw, consent);
83
- assert.equal(error.consent.targetIdentity, target);
84
- return true;
85
- },
86
- );
87
- assert.deepEqual(queue.calls.at(-1), [
88
- "review", "start", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo",
89
- "--target", target, "--projection", "workspace", "--consent", "relay",
90
- ]);
91
- assert.equal(queue.calls.some((arguments_) => arguments_[1] === "status"), true);
100
+ const tokens = startTransitionTokens(target);
101
+ for (const createClient of [client, runtimeClient]) {
102
+ const queue = queuedAdapter([capabilities(), executableStartStatus(target), structuredClone(consent)]);
103
+ await assert.rejects(
104
+ () => createClient(queue.adapter).start({ cwd: "/repo" }),
105
+ (error: unknown) => {
106
+ assert.equal((error as { name?: string }).name, "NativeReviewConsentRequiredError");
107
+ const required = error as { consent: { raw: Record<string, unknown>; targetIdentity: string } };
108
+ assert.deepEqual(required.consent.raw, consent);
109
+ assert.equal(required.consent.targetIdentity, target);
110
+ return true;
111
+ },
112
+ );
113
+ assert.deepEqual(queue.calls, [
114
+ ["review", "status", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo", "--projection", "workspace", "--agent", "pi", "--next-transition"],
115
+ ["review", "start", ...tokens],
116
+ ]);
117
+ }
92
118
  });
93
119
 
94
- test("controller-prebound START target is used without projecting a second workspace candidate", async () => {
120
+ test("controller-prebound START target checks STATUS once and executes its exact matching transition", async () => {
95
121
  const consent = fixture<Record<string, unknown>>("consent.fixture.json");
96
122
  const target = String(consent.target_identity);
97
- const queue = queuedAdapter([capabilities(), consent]);
98
- await assert.rejects(
99
- () => client(queue.adapter).start({ cwd: "/repo", targetIdentity: target, projection: "workspace" }),
100
- (error: unknown) => error instanceof NativeReviewConsentRequiredError,
101
- );
102
- assert.deepEqual(queue.calls.at(-1), [
103
- "review", "start", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo",
104
- "--target", target, "--projection", "workspace", "--consent", "relay",
105
- ]);
106
- assert.equal(queue.calls.some((arguments_) => arguments_[1] === "status"), false, "a prebound START target must not be projected again");
123
+ const tokens = startTransitionTokens(target);
124
+ for (const createClient of [client, runtimeClient]) {
125
+ const queue = queuedAdapter([capabilities(), executableStartStatus(target), structuredClone(consent)]);
126
+ await assert.rejects(
127
+ () => createClient(queue.adapter).start({ cwd: "/repo", targetIdentity: target, projection: "workspace" }),
128
+ (error: unknown) => (error as { name?: string }).name === "NativeReviewConsentRequiredError",
129
+ );
130
+ assert.equal(queue.calls.filter((arguments_) => arguments_[1] === "status").length, 1, "a prebound target is a drift check, never a STATUS bypass");
131
+ assert.deepEqual(queue.calls.at(-1), ["review", "start", ...tokens]);
132
+ }
107
133
  });
108
134
 
109
135
  test("consent follow-up executes the provider-named invocation exactly once and refuses changed lineage or target bindings", async () => {
@@ -308,34 +334,89 @@ test("declined consent decodes the provider's explicit empty authority fields wi
308
334
  const devbinaryFixtureRoot = join(process.cwd(), "tests", "fixtures", "devbinary");
309
335
  const devbinaryFixture = <T = Record<string, unknown>>(name: string): T => JSON.parse(readFileSync(join(devbinaryFixtureRoot, name), "utf8")) as T;
310
336
 
311
- test("negotiated START surfaces the captured consent/v3 envelope instead of failing schema-incompatible", async () => {
337
+ function piConsent(rewriteInvocation: (invocation: string) => string = (invocation) => invocation): Record<string, unknown> {
312
338
  const consent = devbinaryFixture<Record<string, unknown>>("consent-v3.captured.json");
313
- const target = String(consent.target_identity);
314
- const queue = queuedAdapter([capabilities(), unrelatedStatus(target), consent]);
315
- await assert.rejects(
316
- () => client(queue.adapter).start({ cwd: "/repo" }),
317
- (error: unknown) => {
318
- assert.ok(error instanceof NativeReviewConsentRequiredError, `expected NativeReviewConsentRequiredError, got ${String(error)}`);
319
- assert.deepEqual(error.consent.raw, consent);
320
- assert.equal(error.consent.schema, "gentle-ai.review-integration.consent/v3");
321
- assert.equal(error.consent.targetIdentity, target);
322
- return true;
323
- },
324
- );
325
- assert.deepEqual(queue.calls.at(-1), [
326
- "review", "start", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo",
327
- "--target", target, "--projection", "workspace", "--consent", "relay",
328
- ]);
339
+ consent.agent = "pi";
340
+ for (const choice of consent.choices as Array<Record<string, unknown>>) {
341
+ choice.invocation = rewriteInvocation(String(choice.invocation));
342
+ }
343
+ return consent;
344
+ }
329
345
 
330
- const runtimeQueue = queuedAdapter([capabilities(), unrelatedStatus(target), structuredClone(consent)]);
331
- await assert.rejects(
332
- () => runtimeClient(runtimeQueue.adapter).start({ cwd: "/repo" }),
333
- (error: unknown) => (error as Error).name === "NativeReviewConsentRequiredError",
334
- );
346
+ function piBoundConsent(): Record<string, unknown> {
347
+ return piConsent((invocation) => invocation.replace(" --consent ", " --agent pi --consent "));
348
+ }
349
+
350
+ test("Pi START rejects a Pi-bound consent/v3 envelope when any provider choice omits or misbinds the Pi agent", async () => {
351
+ const target = String(devbinaryFixture<Record<string, unknown>>("consent-v3.captured.json").target_identity);
352
+ const tokens = startTransitionTokens(target);
353
+ const invalidUnselectedChoice = (replace: (invocation: string) => string): Record<string, unknown> => {
354
+ const consent = piBoundConsent();
355
+ const choice = (consent.choices as Array<Record<string, unknown>>).find((candidate) => candidate.answer === "declined")!;
356
+ choice.invocation = replace(String(choice.invocation));
357
+ return consent;
358
+ };
359
+ const cases = [
360
+ ["missing", piConsent()],
361
+ ["embedded", piConsent((invocation) => invocation.replace(" --consent ", " --note=--agent\\ pi --consent "))],
362
+ ["duplicate", invalidUnselectedChoice((invocation) => invocation.replace("--agent pi", "--agent pi --agent pi"))],
363
+ ["conflicting", invalidUnselectedChoice((invocation) => invocation.replace("--agent pi", "--agent pi --agent claude-code"))],
364
+ ] as const;
365
+ for (const [label, consent] of cases) {
366
+ for (const createClient of [client, runtimeClient]) {
367
+ const queue = queuedAdapter([capabilities(), executableStartStatus(target), structuredClone(consent)]);
368
+ await assert.rejects(
369
+ () => createClient(queue.adapter).start({ cwd: "/repo" }),
370
+ (error: unknown) => {
371
+ assert.equal((error as { name?: string }).name, "NativeReviewCliError", `${label} binding must fail closed before consent presentation`);
372
+ assert.equal((error as { code?: string }).code, "schema-incompatible");
373
+ return true;
374
+ },
375
+ );
376
+ assert.deepEqual(queue.calls.at(-1), ["review", "start", ...tokens]);
377
+ }
378
+ }
379
+ });
380
+
381
+ test("Pi START rejects a foreign consent/v3 envelope and only relays exact Pi-bound choice invocations", async () => {
382
+ const consent = devbinaryFixture<Record<string, unknown>>("consent-v3.captured.json");
383
+ const target = String(consent.target_identity);
384
+ const tokens = startTransitionTokens(target);
385
+ for (const createClient of [client, runtimeClient]) {
386
+ const foreignQueue = queuedAdapter([capabilities(), executableStartStatus(target), structuredClone(consent)]);
387
+ await assert.rejects(
388
+ () => createClient(foreignQueue.adapter).start({ cwd: "/repo" }),
389
+ (error: unknown) => {
390
+ assert.equal((error as { name?: string }).name, "NativeReviewCliError");
391
+ assert.equal((error as { code?: string }).code, "schema-incompatible");
392
+ return true;
393
+ },
394
+ );
395
+ assert.deepEqual(foreignQueue.calls.at(-1), ["review", "start", ...tokens]);
396
+ for (const piBound of [
397
+ piBoundConsent(),
398
+ piConsent((invocation) => invocation.replace(" --consent ", " --agent=pi --consent ")),
399
+ ]) {
400
+ const piQueue = queuedAdapter([capabilities(), executableStartStatus(target), piBound]);
401
+ await assert.rejects(
402
+ () => createClient(piQueue.adapter).start({ cwd: "/repo" }),
403
+ (error: unknown) => {
404
+ assert.equal((error as { name?: string }).name, "NativeReviewConsentRequiredError");
405
+ const required = error as { consent: { raw: Record<string, unknown>; schema: string; targetIdentity: string; agent?: string } };
406
+ assert.deepEqual(required.consent.raw, piBound);
407
+ assert.equal(required.consent.schema, "gentle-ai.review-integration.consent/v3");
408
+ assert.equal(required.consent.targetIdentity, target);
409
+ assert.equal(required.consent.agent, "pi");
410
+ return true;
411
+ },
412
+ );
413
+ assert.deepEqual(piQueue.calls.at(-1), ["review", "start", ...tokens]);
414
+ }
415
+ }
335
416
  });
336
417
 
337
- test("a granted consent/v3 answer decodes the captured start/v3 result with its event binding", async () => {
338
- const decoded = (await import("../lib/review-integration-v2.ts")).decodeReviewConsentV3(devbinaryFixture<Record<string, unknown>>("consent-v3.captured.json"));
418
+ test("a granted Pi consent/v3 answer replays the captured exact agent-bound invocation", async () => {
419
+ const decoded = (await import("../lib/review-integration-v2.ts")).decodeReviewConsentV3(piBoundConsent(), "pi");
339
420
  const cwd = decoded.choices[0].invocation.split(" --cwd ")[1]!.split(" ")[0]!;
340
421
  const started = devbinaryFixture<Record<string, unknown>>("start-v3-consent-granted.captured.json");
341
422
  const queue = queuedAdapter([capabilities(), started]);
@@ -346,6 +427,7 @@ test("a granted consent/v3 answer decodes the captured start/v3 result with its
346
427
  assert.equal(result.start.selectedLenses.length, 4);
347
428
  // The follow-up runs the provider-owned invocation verbatim, exactly once.
348
429
  assert.deepEqual(queue.calls.at(-1), decoded.choices[0].invocation.split(" ").slice(1));
430
+ assert.equal(queue.calls.filter((arguments_) => arguments_.includes("--agent") && arguments_.includes("pi")).length, 1);
349
431
  assert.equal(queue.calls.filter((arguments_) => arguments_.includes("granted")).length, 1);
350
432
 
351
433
  const runtimeQueue = queuedAdapter([capabilities(), structuredClone(started)]);
@@ -353,8 +435,8 @@ test("a granted consent/v3 answer decodes the captured start/v3 result with its
353
435
  assert.equal(runtimeResult.kind, "started");
354
436
  });
355
437
 
356
- test("a declined consent/v3 answer accepts the captured declined result and creates no authority", async () => {
357
- const decoded = (await import("../lib/review-integration-v2.ts")).decodeReviewConsentV3(devbinaryFixture<Record<string, unknown>>("consent-v3.captured.json"));
438
+ test("a declined Pi consent/v3 answer replays the captured exact agent-bound invocation and creates no authority", async () => {
439
+ const decoded = (await import("../lib/review-integration-v2.ts")).decodeReviewConsentV3(piBoundConsent(), "pi");
358
440
  const cwd = decoded.choices[1].invocation.split(" --cwd ")[1]!.split(" ")[0]!;
359
441
  const declined = devbinaryFixture<Record<string, unknown>>("start-v3-consent-declined.captured.json");
360
442
  const queue = queuedAdapter([capabilities(), declined]);
@@ -363,4 +445,6 @@ test("a declined consent/v3 answer accepts the captured declined result and crea
363
445
  assert.ok(result.kind === "declined");
364
446
  assert.equal(result.consent, "declined_this_candidate");
365
447
  assert.equal(result.targetIdentity, decoded.targetIdentity);
448
+ assert.deepEqual(queue.calls.at(-1), decoded.choices[1].invocation.split(" ").slice(1));
449
+ assert.equal(queue.calls.filter((arguments_) => arguments_.includes("--agent") && arguments_.includes("pi")).length, 1);
366
450
  });