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
@@ -87,13 +87,14 @@ test("no SDD phase executor infers Key Learnings through `standard phase envelop
87
87
 
88
88
  test("SDD executor coverage is exhaustive against actual agent files", () => {
89
89
  const actual = sddAgents();
90
- // Allowlist: the 12 known phase executors. A new sdd-*.md without a
90
+ // Allowlist: the 13 known phase executors. A new sdd-*.md without a
91
91
  // Key Learnings Closing section fails the first test; this test proves
92
92
  // the allowlist matches reality so coverage cannot silently drift.
93
93
  const expected = [
94
94
  "sdd-apply.md", "sdd-archive.md", "sdd-design.md", "sdd-explore.md",
95
- "sdd-init.md", "sdd-onboard.md", "sdd-proposal.md", "sdd-spec.md",
96
- "sdd-status.md", "sdd-sync.md", "sdd-tasks.md", "sdd-verify.md",
95
+ "sdd-init.md", "sdd-onboard.md", "sdd-proposal.md", "sdd-research.md",
96
+ "sdd-spec.md", "sdd-status.md", "sdd-sync.md", "sdd-tasks.md",
97
+ "sdd-verify.md",
97
98
  ];
98
99
  assert.deepEqual(actual, expected, "SDD agent set must match the known allowlist");
99
100
  });
@@ -228,9 +229,10 @@ test("modified SDD agents are packaged and installed by the existing installer",
228
229
  "assets/agents/sdd-apply.md", "assets/agents/sdd-archive.md",
229
230
  "assets/agents/sdd-design.md", "assets/agents/sdd-explore.md",
230
231
  "assets/agents/sdd-init.md", "assets/agents/sdd-onboard.md",
231
- "assets/agents/sdd-proposal.md", "assets/agents/sdd-spec.md",
232
- "assets/agents/sdd-status.md", "assets/agents/sdd-sync.md",
233
- "assets/agents/sdd-tasks.md", "assets/agents/sdd-verify.md",
232
+ "assets/agents/sdd-proposal.md", "assets/agents/sdd-research.md",
233
+ "assets/agents/sdd-spec.md", "assets/agents/sdd-status.md",
234
+ "assets/agents/sdd-sync.md", "assets/agents/sdd-tasks.md",
235
+ "assets/agents/sdd-verify.md",
234
236
  ];
235
237
  for (const path of expected) {
236
238
  assert.match(verifier, new RegExp(path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")), `${path} must be in the package verifier`);
@@ -1,39 +1,26 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { execFileSync } from "node:child_process";
3
- import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, 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";
7
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
8
- import { createGentleAiExtension } from "../../extensions/gentle-ai.ts";
7
+ import { __testing } from "../../extensions/gentle-ai.ts";
9
8
  import {
10
- NativeReviewCliV214,
9
+ NATIVE_REVIEW_ERROR_CODE,
10
+ NativeReviewCliError,
11
+ NativeReviewCliV216,
12
+ NativeReviewConsentRequiredError,
11
13
  createNodeExecFileAdapter,
12
- setNativeCliContractForTesting,
13
14
  type ExecFileAdapter,
14
- type NativeReviewCli,
15
15
  } from "../../lib/native-review-cli.ts";
16
- import { CandidateViewRegistry } from "../../lib/review-candidate-view.ts";
17
- import { readReviewConsentLatch } from "../../lib/review-consent-latch.ts";
18
- import type { AuthorityRepairAssessmentV1, ReviewStatusV3 } from "../../lib/review-integration-v2.ts";
19
16
  import { requireDevBinary } from "../support/native-binary-gate.ts";
20
17
 
21
- // Organic RDD Parity, Phase 6: non-gating dev-binary journey.
18
+ // Organic RDD Parity: candidate-bound dev-binary journeys.
22
19
  //
23
- // `pnpm test` never picks this file up — it globs `tests/*.test.ts` only, and
24
- // this file both lives one directory deeper and ends in `.devtest.ts`. It
25
- // only runs via `pnpm run test:dev-binary`, and only when GENTLE_AI_DEV_BINARY
26
- // names an existing absolute path. It never reads or writes the shipped
27
- // 2.1.11 pins (lib/gentle-ai-binary.ts, scripts/gentle-ai-installer.mjs) —
28
- // every capability stays negotiated per-process through the executable
29
- // override seam below, exactly like every other native-review-cli test.
30
- //
31
- // Gated through the same loud-skip machinery as the two `tests/*.test.ts`
32
- // binary-verified suites (Phase 4), extended here to cover this THIRD
33
- // self-skipping suite (Phase 13.12/13.13 follow-up): under
34
- // GENTLE_PI_REQUIRE_DEV_BINARY=1 a missing/invalid dev binary throws instead
35
- // of silently reporting 5 tests / 0 pass / 0 fail, which is exactly how the
36
- // v2.2.2 "receipt-driven development" terminology rot went unnoticed.
20
+ // This suite runs only via `pnpm run test:dev-binary` and only when
21
+ // GENTLE_AI_DEV_BINARY names a current, absolute candidate binary. Every START
22
+ // first obtains candidate-bound STATUS and executes the returned transition;
23
+ // consent answers replay the envelope's own invocation exactly once.
37
24
  const DEV_BINARY = process.env.GENTLE_AI_DEV_BINARY;
38
25
  const devBinaryGate = requireDevBinary({
39
26
  devBinaryPath: DEV_BINARY,
@@ -42,45 +29,23 @@ const devBinaryGate = requireDevBinary({
42
29
  });
43
30
  if (!devBinaryGate.run) console.log(`tests/devbinary/native-review-parity.devtest.ts: ${devBinaryGate.reason}`);
44
31
  const RUNNABLE = devBinaryGate.run;
32
+ const DEV_HOME = mkdtempSync(join(tmpdir(), "gentle-pi-dev-binary-home-"));
33
+ const ORIGINAL_HOME = process.env.HOME;
34
+ const ORIGINAL_USERPROFILE = process.env.USERPROFILE;
45
35
 
46
- // A synthetic capable version, exactly like native-review-parity.test.ts's
47
- // CAPABLE_VERSION overlay. Real dev binaries never carry a pinned three-part
48
- // semver (the live binary under test reports "gentle-ai dev-organic-...",
49
- // confirmed live during Phase 6 ground-truthing), so `verifyVersion`'s frozen
50
- // `/^gentle-ai ([0-9]+\.[0-9]+\.[0-9]+)\n$/` regex can never match it — by
51
- // design, a dev build is never a pinned release. The bridge below substitutes
52
- // exactly one in-memory version response and forwards every other call to the
53
- // real process untouched, so argv fidelity is guaranteed: NativeReviewCliV214
54
- // itself builds every argv array here, never this file.
55
- const BRIDGE_VERSION = "9.8.7";
36
+ type NativeCall = readonly string[];
56
37
 
57
- function bridgeAdapter(binary: string): ExecFileAdapter {
38
+ function bridgeAdapter(binary: string, calls: NativeCall[]): ExecFileAdapter {
58
39
  const real = createNodeExecFileAdapter();
59
40
  return async (request) => {
60
- if (request.arguments[0] === "version") {
61
- return { stdout: `gentle-ai ${BRIDGE_VERSION}\n`, stderr: "", exitCode: 0, signal: null, timedOut: false, outputLimitExceeded: false };
62
- }
41
+ calls.push([...request.arguments]);
63
42
  return real({ ...request, file: binary });
64
43
  };
65
44
  }
66
45
 
67
- // A NativeReviewCli backed by the real dev binary for reviewMode/start/
68
- // validate (the organic-parity surface under test), with a synthetic
69
- // targetStatus fixture standing in for the negotiated review-integration/v2
70
- // contract; the plain-CLI decode path in lib/native-review-cli.ts (reviewMode,
71
- // start, validate here) stays independent of the negotiated status shape.
72
- function journeyNative(binary: string): NativeReviewCli {
73
- const bridge = new NativeReviewCliV214(bridgeAdapter(binary), binary);
74
- return {
75
- start: (request) => bridge.start(request),
76
- validate: (request) => bridge.validate(request),
77
- reviewMode: (request) => bridge.reviewMode(request),
78
- async targetStatus(request) {
79
- return request.lineageId === undefined
80
- ? unrelatedStartTargetStatus()
81
- : currentTargetStatusFixture(request.lineageId, request.cwd);
82
- },
83
- } as unknown as NativeReviewCli;
46
+ function journeyNative(binary: string): { native: NativeReviewCliV216; calls: NativeCall[] } {
47
+ const calls: NativeCall[] = [];
48
+ return { native: new NativeReviewCliV216(bridgeAdapter(binary, calls), binary), calls };
84
49
  }
85
50
 
86
51
  function git(cwd: string, ...args: string[]): string {
@@ -99,156 +64,69 @@ function repository(t: test.TestContext): string {
99
64
  return cwd;
100
65
  }
101
66
 
102
- const UNSUPPORTED_REPAIR_ASSESSMENT: AuthorityRepairAssessmentV1 = {
103
- schema: "gentle-ai.review-authority-repair-assessment/v1",
104
- status: "unsupported",
105
- counts: { lineages: 0, compactLineages: 0, legacyLineages: 0, events: 0, bytes: 0, eligibleCandidates: 0, unsupportedLineages: 0, conflicts: 0 },
106
- supportedOperations: ["review/complete-fix", "review/validate-fix"],
107
- authorizationSchema: "gentle-ai.review-repair-authorization/v1",
108
- };
109
- const RAW_UNSUPPORTED_REPAIR_ASSESSMENT = {
110
- schema: UNSUPPORTED_REPAIR_ASSESSMENT.schema,
111
- status: UNSUPPORTED_REPAIR_ASSESSMENT.status,
112
- counts: {
113
- lineages: 0, compact_lineages: 0, legacy_lineages: 0, events: 0, bytes: 0,
114
- eligible_candidates: 0, unsupported_lineages: 0, conflicts: 0,
115
- },
116
- supported_operations: UNSUPPORTED_REPAIR_ASSESSMENT.supportedOperations,
117
- authorization_schema: UNSUPPORTED_REPAIR_ASSESSMENT.authorizationSchema,
118
- };
119
-
120
- function unrelatedStartTargetStatus(): ReviewStatusV3 {
121
- const sha = `sha256:${"a".repeat(64)}`;
122
- const tree = "b".repeat(40);
123
- const projection = {
124
- schema: "gentle-ai.review-integration.projection/v1" as const, kind: "current-changes" as const, projection: "workspace" as const,
125
- baseTree: tree, initialReviewTree: tree, currentCandidateTree: tree, pathsDigest: sha, paths: [], intendedUntracked: [],
126
- intendedUntrackedProof: sha, initialSnapshotIdentity: sha, currentSnapshotIdentity: sha,
127
- };
128
- return {
129
- contract: "gentle-ai.review-integration/v2", applicability: "unrelated", receipt: { status: "not_applicable" }, action: "start",
130
- replayability: "not_replayable", targetIdentity: sha, projection, repair: UNSUPPORTED_REPAIR_ASSESSMENT, candidates: [],
131
- raw: {
132
- schema: "gentle-ai.review-integration.status/v3", contract: "gentle-ai.review-integration/v2", operation: "review.status",
133
- applicability: "unrelated", receipt: { status: "not_applicable" }, action: "start", replayability: "not_replayable", target_identity: sha,
134
- repair: RAW_UNSUPPORTED_REPAIR_ASSESSMENT,
135
- projection: { schema: projection.schema, kind: projection.kind, projection: projection.projection, base_tree: tree, initial_review_tree: tree, current_candidate_tree: tree, paths_digest: sha, paths: [], intended_untracked: [], intended_untracked_proof: sha, initial_snapshot_identity: sha, current_snapshot_identity: sha },
136
- candidates: [],
137
- },
138
- };
139
- }
140
-
141
- function currentTargetStatusFixture(lineageId: string, cwd: string): ReviewStatusV3 {
142
- const sha = `sha256:${"a".repeat(64)}`;
143
- const baseTree = git(cwd, "rev-parse", "HEAD^{tree}");
144
- const candidateTree = git(cwd, "write-tree");
145
- const projection = {
146
- schema: "gentle-ai.review-integration.projection/v1" as const, kind: "current-changes" as const, projection: "workspace" as const,
147
- baseTree, initialReviewTree: candidateTree, currentCandidateTree: candidateTree, pathsDigest: sha, paths: ["app.ts"], intendedUntracked: [],
148
- intendedUntrackedProof: sha, initialSnapshotIdentity: sha, currentSnapshotIdentity: sha,
149
- };
150
- return {
151
- contract: "gentle-ai.review-integration/v2", applicability: "current_target",
152
- authority: { version: "compact-v2", lineageId, state: "reviewing", generation: 1, revision: sha },
153
- receipt: { status: "expected_missing" }, action: "finalize", replayability: "not_replayable",
154
- frozen: { tier: "medium", originalChangedLines: 1, correctionBudget: 1 },
155
- targetIdentity: sha, projection, repair: UNSUPPORTED_REPAIR_ASSESSMENT, candidates: [],
156
- raw: {
157
- schema: "gentle-ai.review-integration.status/v3", contract: "gentle-ai.review-integration/v2", operation: "review.status",
158
- applicability: "current_target", receipt: { status: "expected_missing" }, action: "finalize", replayability: "not_replayable", target_identity: sha,
159
- authority: { version: "compact-v2", lineage_id: lineageId, state: "reviewing", generation: 1, revision: sha },
160
- frozen: { tier: "medium", original_changed_lines: 1, correction_budget: 1 },
161
- repair: RAW_UNSUPPORTED_REPAIR_ASSESSMENT,
162
- projection: { schema: projection.schema, kind: projection.kind, projection: projection.projection, base_tree: baseTree, initial_review_tree: candidateTree, current_candidate_tree: candidateTree, paths_digest: sha, paths: ["app.ts"], intended_untracked: [], intended_untracked_proof: sha, initial_snapshot_identity: sha, current_snapshot_identity: sha },
163
- candidates: [],
164
- },
165
- };
67
+ function enableGlobalReview(cwd: string): void {
68
+ assert.ok(DEV_BINARY, "GENTLE_AI_DEV_BINARY is required for this devtest");
69
+ const enabled = JSON.parse(execFileSync(DEV_BINARY, [
70
+ "review", "mode", "enable", "--scope", "global", "--cwd", cwd, "--json",
71
+ ], { encoding: "utf8" })) as { status: { effective: string } };
72
+ assert.equal(enabled.status.effective, "on");
73
+ const status = JSON.parse(execFileSync(DEV_BINARY, [
74
+ "review", "mode", "status", "--cwd", cwd, "--json",
75
+ ], { encoding: "utf8" })) as { status: { effective: string } };
76
+ assert.equal(status.status.effective, "on");
166
77
  }
167
78
 
168
- interface RegisteredTool {
169
- execute: (toolCallId: string, params: unknown, signal: AbortSignal | undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<{ details?: unknown }>;
170
- }
171
- interface RegisteredCommandFixture {
172
- handler: (args: string, ctx: ExtensionContext) => Promise<void>;
79
+ async function enableReview(native: NativeReviewCliV216, cwd: string): Promise<void> {
80
+ enableGlobalReview(cwd);
81
+ const disabled = await native.reviewMode({ cwd, operation: "disable" });
82
+ assert.equal(disabled.status.effective, "off");
83
+ assert.equal(disabled.status.source, "clone_local");
84
+ const enabled = await native.reviewMode({ cwd, operation: "enable" });
85
+ assert.equal(enabled.status.effective, "on");
86
+ assert.equal(enabled.status.source, "global");
173
87
  }
174
88
 
175
- function runtime(nativeReviewCli: NativeReviewCli): { controller: RegisteredTool; commands: Map<string, RegisteredCommandFixture> } {
176
- const tools = new Map<string, RegisteredTool>();
177
- const commands = new Map<string, RegisteredCommandFixture>();
178
- const dependencies = { nativeReviewCli, candidateViews: new CandidateViewRegistry() } as unknown as Parameters<typeof createGentleAiExtension>[0];
179
- createGentleAiExtension(dependencies)({
180
- on() {},
181
- registerTool(definition: RegisteredTool & { name: string }) { tools.set(definition.name, definition); },
182
- registerCommand(name: string, definition: RegisteredCommandFixture) { commands.set(name, definition); },
183
- } as unknown as ExtensionAPI);
184
- const controller = tools.get("gentle_review");
185
- assert.ok(controller);
186
- return { controller: controller!, commands };
187
- }
188
-
189
- function headlessContext(cwd: string, notices: Array<{ message: string; type?: string }> = []): ExtensionContext {
190
- return { cwd, hasUI: false, ui: { notify: (message: string, type?: string) => { notices.push({ message, type }); } } } as unknown as ExtensionContext;
191
- }
192
- function confirmContext(cwd: string, answer: boolean): ExtensionContext {
193
- return { cwd, hasUI: true, ui: { confirm: async () => answer, notify: () => {} } } as unknown as ExtensionContext;
89
+ async function candidateConsent(native: NativeReviewCliV216, cwd: string) {
90
+ const status = await native.targetStatus({ cwd, agent: "pi" });
91
+ assert.equal(status.nextTransition?.kind, "execute");
92
+ assert.equal(status.nextTransition?.execute?.operation, "review.start");
93
+ try {
94
+ await native.start({ cwd });
95
+ assert.fail("candidate START must return consent/v3 before review authority exists");
96
+ } catch (error) {
97
+ assert.ok(error instanceof NativeReviewConsentRequiredError, error instanceof Error ? error.message : String(error));
98
+ assert.equal(error.consent.schema, "gentle-ai.review-integration.consent/v3");
99
+ assert.equal(error.consent.agent, "pi");
100
+ return error.consent;
101
+ }
194
102
  }
195
103
 
196
- const START_ORDINARY = { operation: "start", input: JSON.stringify({ mode: "ordinary" }) };
197
- async function execStart(controller: RegisteredTool, id: string, ctx: ExtensionContext): Promise<Record<string, unknown>> {
198
- const { details } = await controller.execute(id, START_ORDINARY, undefined, undefined, ctx);
199
- return details as Record<string, unknown>;
104
+ function consentAnswerCall(calls: NativeCall[], answer: "granted" | "declined"): NativeCall {
105
+ const matching = calls.filter((arguments_) => arguments_.at(0) === "review" && arguments_.at(1) === "start" && arguments_.includes("--consent") && arguments_.at(arguments_.indexOf("--consent") + 1) === answer);
106
+ assert.equal(matching.length, 1, `${answer} must execute exactly one provider invocation`);
107
+ return matching[0]!;
200
108
  }
201
109
 
202
110
  // ---------------------------------------------------------------------------
203
- // Kill switch round trip (mirrors /gentle:review-mode status|disable|enable
204
- // from the community guide flows).
111
+ // Kill switch round trip.
205
112
  // ---------------------------------------------------------------------------
206
113
 
207
- test("dev-binary: gentle:review-mode round-trips status, disable, and enable against the real binary", { skip: !RUNNABLE }, async (t) => {
114
+ test("dev-binary: global opt-in then clone disable and enable clears only the local override", { skip: !RUNNABLE }, async (t) => {
208
115
  const cwd = repository(t);
209
- const { commands } = runtime(journeyNative(DEV_BINARY!));
210
- const command = commands.get("gentle:review-mode")!;
211
- const notices: Array<{ message: string; type?: string }> = [];
212
-
213
- await command.handler("status", headlessContext(cwd, notices));
214
- assert.match(notices.at(-1)!.message, /receipt-driven development: on \(decided by \w+\)/);
215
-
216
- await command.handler("disable", headlessContext(cwd, notices));
217
- assert.match(notices.at(-1)!.message, /receipt-driven development: off \(decided by \w+\)/);
218
-
219
- await command.handler("status", headlessContext(cwd, notices));
220
- assert.match(notices.at(-1)!.message, /receipt-driven development: off \(decided by \w+\)/);
221
-
222
- await command.handler("enable", headlessContext(cwd, notices));
223
- assert.match(notices.at(-1)!.message, /receipt-driven development: on \(decided by \w+\)/);
116
+ const { native } = journeyNative(DEV_BINARY!);
117
+ enableGlobalReview(cwd);
118
+ assert.equal((await native.reviewMode({ cwd, operation: "status" })).status.effective, "on");
119
+ assert.equal((await native.reviewMode({ cwd, operation: "disable" })).status.effective, "off");
120
+ assert.equal((await native.reviewMode({ cwd, operation: "status" })).status.source, "clone_local");
121
+ assert.equal((await native.reviewMode({ cwd, operation: "enable" })).status.effective, "on");
122
+ assert.equal((await native.reviewMode({ cwd, operation: "status" })).status.source, "global");
224
123
  });
225
124
 
226
125
  // ---------------------------------------------------------------------------
227
- // Tier 0 silence: an empty candidate surfaces its hint verbatim and never
228
- // triggers a consent prompt or notice.
126
+ // Candidate-bound consent/v3 answers.
229
127
  // ---------------------------------------------------------------------------
230
128
 
231
- test("dev-binary: an empty candidate stays silent (no consent notice) and surfaces the real hint verbatim", { skip: !RUNNABLE }, async (t) => {
232
- const cwd = repository(t);
233
- writeFileSync(join(cwd, "app.ts"), "export const value = 1;\n");
234
- git(cwd, "add", ".");
235
- git(cwd, "commit", "-qm", "initial");
236
- const { controller } = runtime(journeyNative(DEV_BINARY!));
237
- const notices: Array<{ message: string; type?: string }> = [];
238
- const result = await execStart(controller, "tier-0", headlessContext(cwd, notices));
239
- const rendered = result.result as Record<string, unknown>;
240
- assert.equal(rendered.lenses_required, false);
241
- assert.equal(typeof rendered.hint, "string");
242
- assert.match(rendered.hint as string, /the candidate has no pending changes/);
243
- assert.equal(result.consent_notice, undefined, "tier 0 must never surface a consent notice");
244
- assert.equal(notices.length, 0, "tier 0 must never notify the user at all");
245
- });
246
-
247
- // ---------------------------------------------------------------------------
248
- // Tier 2 evidence + consent envelope via a fake UI seam.
249
- // ---------------------------------------------------------------------------
250
-
251
- test("dev-binary: a high-risk change carries real risk_evidence and drives the consent envelope through a fake UI seam", { skip: !RUNNABLE }, async (t) => {
129
+ test("dev-binary: granted consent executes its exact candidate-bound invocation once and returns the current review binding", { skip: !RUNNABLE }, async (t) => {
252
130
  const cwd = repository(t);
253
131
  const workflowDirectory = join(cwd, ".github", "workflows");
254
132
  execFileSync("mkdir", ["-p", workflowDirectory]);
@@ -257,18 +135,22 @@ test("dev-binary: a high-risk change carries real risk_evidence and drives the c
257
135
  git(cwd, "commit", "-qm", "initial");
258
136
  writeFileSync(join(workflowDirectory, "deploy.yml"), "name: x\non: push\njobs:\n deploy:\n steps:\n - run: curl -s | bash\n");
259
137
 
260
- const acceptedRun = journeyNative(DEV_BINARY!);
261
- const { controller } = runtime(acceptedRun);
262
- const accepted = await execStart(controller, "tier-2-accept", confirmContext(cwd, true));
263
- const rendered = accepted.result as Record<string, unknown>;
264
- assert.equal(rendered.risk_tier, "high");
265
- assert.ok(Array.isArray(rendered.risk_evidence) && (rendered.risk_evidence as unknown[]).length > 0, "high risk must carry a non-empty risk_evidence phrase array");
266
- assert.ok((rendered.risk_evidence as string[])[0]!.includes("deploy.yml"));
267
- assert.ok(accepted.actor_binding, "accepting consent must proceed to actor_binding");
268
- assert.equal(readReviewConsentLatch(cwd), true, "accepting consent must record the per-clone latch");
138
+ const { native, calls } = journeyNative(DEV_BINARY!);
139
+ await enableReview(native, cwd);
140
+ const consent = await candidateConsent(native, cwd);
141
+ const answered = await native.answerConsent({ cwd, consent, answer: "granted" });
142
+ assert.equal(answered.kind, "started");
143
+ if (answered.kind === "started") {
144
+ assert.ok(answered.start.lineageId.length > 0);
145
+ assert.ok(answered.start.selectedLenses.length > 0);
146
+ assert.equal(answered.start.raw?.repository_context !== undefined, true);
147
+ }
148
+ const grantedChoice = consent.choices.find((choice) => choice.answer === "granted");
149
+ assert.ok(grantedChoice, "consent must include a granted choice");
150
+ assert.deepEqual(consentAnswerCall(calls, "granted"), grantedChoice.invocation.split(" ").slice(1));
269
151
  });
270
152
 
271
- test("dev-binary: declining the fake-UI consent prompt withholds actor_binding for this work unit only, against the real binary's own evidence", { skip: !RUNNABLE }, async (t) => {
153
+ test("dev-binary: declined consent executes its exact candidate-bound invocation once and creates no lineage, result, or actor", { skip: !RUNNABLE }, async (t) => {
272
154
  const cwd = repository(t);
273
155
  const workflowDirectory = join(cwd, ".github", "workflows");
274
156
  execFileSync("mkdir", ["-p", workflowDirectory]);
@@ -277,72 +159,107 @@ test("dev-binary: declining the fake-UI consent prompt withholds actor_binding f
277
159
  git(cwd, "commit", "-qm", "initial");
278
160
  writeFileSync(join(workflowDirectory, "deploy.yml"), "name: x\non: push\njobs:\n deploy:\n steps:\n - run: curl -s | bash\n");
279
161
 
280
- const { controller } = runtime(journeyNative(DEV_BINARY!));
281
- const declined = await execStart(controller, "tier-2-decline", confirmContext(cwd, false));
282
- assert.equal(declined.actor_binding, undefined, "declining must withhold actor dispatch for this work unit");
283
- assert.equal(readReviewConsentLatch(cwd), false, "declining must never persist the latch");
284
- assert.ok(declined.result, "the native start result itself is still reported even on decline");
162
+ const { native, calls } = journeyNative(DEV_BINARY!);
163
+ await enableReview(native, cwd);
164
+ const consent = await candidateConsent(native, cwd);
165
+ const answered = await native.answerConsent({ cwd, consent, answer: "declined" });
166
+ assert.equal(answered.kind, "declined");
167
+ if (answered.kind === "declined") {
168
+ assert.equal(answered.consent, "declined_this_candidate");
169
+ assert.equal("lineageId" in answered, false);
170
+ assert.equal("start" in answered, false);
171
+ assert.equal("actor" in answered.raw, false);
172
+ }
173
+ const declinedChoice = consent.choices.find((choice) => choice.answer === "declined");
174
+ assert.ok(declinedChoice, "consent must include a declined choice");
175
+ assert.deepEqual(consentAnswerCall(calls, "declined"), declinedChoice.invocation.split(" ").slice(1));
176
+ const after = await native.targetStatus({ cwd, agent: "pi" });
177
+ assert.equal(after.authority, undefined);
178
+ assert.equal("receipt" in after, false, "last-event STATUS no longer exposes receipt state");
285
179
  });
286
180
 
287
181
  // ---------------------------------------------------------------------------
288
- // Disabled/unmanaged delivery renders as a successful skip, never a failure.
182
+ // Truthful non-authority responses.
289
183
  // ---------------------------------------------------------------------------
290
184
 
291
- // IMPORTANT reachability finding (Phase 6 ground-truthing): gentle-ai only
292
- // emits the disabled/unmanaged delivery envelope through lineage
293
- // AUTO-DISCOVERY (`review validate --gate <gate>` with no `--lineage`) — its
294
- // Go source (internal/cli/review_facade.go:2412-2433,1904-1916) routes an
295
- // explicit `--lineage <id>` through discoverCompactFacadeReview's
296
- // lineage-specific branch, whose failures are plain `fmt.Errorf` values, never
297
- // the `*ReviewReceiptDiscoveryError` the disabled/unmanaged check requires —
298
- // confirmed live against both a nonexistent lineage id ("load compact facade
299
- // review lineage: ... no such file or directory", exit 1) and a real
300
- // previously-started lineage under a disabled switch ("facade review receipt
301
- // is not available", exit 1). Neither is the structured JSON envelope.
302
- // Pi's `gentle_review` VALIDATE operation always binds an explicit
303
- // lineageId (extensions/gentle-ai.ts throws "Review controller validate
304
- // requires a lineageId" otherwise), so this envelope is structurally
305
- // unreachable through today's controller wiring — this is a pre-existing
306
- // property of the VALIDATE contract, not something Phase 5.2 introduced.
307
- // This is flagged as a risk for a future design revision, not silently
308
- // papered over here. What Phase 6 CAN and does prove against the real binary
309
- // is that Pi's decoder correctly decodes the envelope gentle-ai actually
310
- // sends via auto-discovery; the controller-level mapping/early-return
311
- // (mapNativeValidateResult + the skip response) is proven separately with a
312
- // synthetic fixture in tests/review-controller-native-routing.test.ts,
313
- // because gentle-ai's own explicit-lineage code path can never supply it.
314
- test("dev-binary: VALIDATE via lineage auto-discovery decodes the real disabled/unmanaged delivery envelope", { skip: !RUNNABLE }, async (t) => {
185
+ test("dev-binary: an empty candidate exposes the current STATUS refusal and never reconstructs START", { skip: !RUNNABLE }, async (t) => {
315
186
  const cwd = repository(t);
316
187
  writeFileSync(join(cwd, "app.ts"), "export const value = 1;\n");
317
188
  git(cwd, "add", ".");
318
189
  git(cwd, "commit", "-qm", "initial");
319
- writeFileSync(join(cwd, "app.ts"), "export const value = 2;\n");
320
- git(cwd, "add", "--", "app.ts");
321
190
 
322
- // Each journey uses its own throwaway temp repository (deleted in
323
- // repository(t)'s cleanup), so there is no shared clone-local state to
324
- // restore afterward.
325
- const native = journeyNative(DEV_BINARY!);
326
- await native.reviewMode!({ cwd, operation: "disable" });
191
+ const { native, calls } = journeyNative(DEV_BINARY!);
192
+ await enableReview(native, cwd);
193
+ const status = await native.targetStatus({ cwd, agent: "pi" });
194
+ assert.equal(status.nextTransition?.kind, "collect");
195
+ assert.equal(status.nextTransition?.reasonCode, "empty_candidate_base_ref_required");
196
+ await assert.rejects(
197
+ () => native.start({ cwd }),
198
+ (error: unknown) => error instanceof NativeReviewCliError
199
+ && error.code === NATIVE_REVIEW_ERROR_CODE.SCHEMA_INCOMPATIBLE
200
+ && error.mutationOutcome === "none",
201
+ );
202
+ assert.equal(calls.filter((arguments_) => arguments_.at(0) === "review" && arguments_.at(1) === "start").length, 0);
203
+ });
204
+
205
+ test("dev-binary: a low-risk START closes the review with no receipt or follow-up capture", { skip: !RUNNABLE }, async (t) => {
206
+ const cwd = repository(t);
207
+ writeFileSync(join(cwd, "guide.md"), "# Guide\n\nBase\n");
208
+ git(cwd, "add", "guide.md");
209
+ git(cwd, "commit", "-qm", "docs: base guide");
210
+ writeFileSync(join(cwd, "guide.md"), "# Guide\n\nBase\n\nPassive update\n");
327
211
 
328
- // No lineageId: this is the one shape gentle-ai actually routes through
329
- // its disabled/unmanaged discovery branch.
330
- const result = await native.validate({ cwd, gate: "pre-commit" });
331
- assert.equal(result.delivery, "disabled/unmanaged");
332
- assert.equal(result.allowed, false);
333
- assert.equal(result.result, "invalidated");
334
- assert.equal(result.action, "repository-policy");
212
+ const { native } = journeyNative(DEV_BINARY!);
213
+ await enableReview(native, cwd);
214
+ const status = await native.targetStatus({ cwd, agent: "pi" });
215
+ const started = await native.start({ cwd, targetIdentity: status.targetIdentity });
216
+ assert.equal(started.action, "closed");
217
+ assert.equal(started.state, "approved");
218
+ assert.deepEqual(started.selectedLenses, []);
219
+ assert.equal(started.lensesRequired, false);
220
+ });
221
+
222
+ // gentle-pi#518: native STATUS projects a staged exact rename as its source
223
+ // and its destination. The Pi controller freezes the same identity, so
224
+ // ordinary START reaches native admission instead of being rejected locally
225
+ // with candidate-target-projection-drift.
226
+ test("dev-binary: ordinary START through the Pi controller admits a staged exact rename plus an addition", { skip: !RUNNABLE }, async (t) => {
227
+ const cwd = repository(t);
228
+ mkdirSync(join(cwd, "active"));
229
+ writeFileSync(join(cwd, "active", "document.md"), "# Document\n\nline one\nline two\n");
230
+ git(cwd, "add", "active/document.md");
231
+ git(cwd, "commit", "-qm", "docs: base document");
232
+ mkdirSync(join(cwd, "archive"));
233
+ git(cwd, "mv", "active/document.md", "archive/document.md");
234
+ writeFileSync(join(cwd, "report.md"), "# Report\n\nPassive report\n");
235
+ git(cwd, "add", "-A");
236
+ assert.match(git(cwd, "diff", "--cached", "--name-status", "--find-renames=100%"), /^R100\tactive\/document\.md\tarchive\/document\.md$/m, "the candidate must stage an exact rename");
237
+
238
+ const { native, calls } = journeyNative(DEV_BINARY!);
239
+ await enableReview(native, cwd);
240
+ const status = await native.targetStatus({ cwd, agent: "pi" });
241
+ assert.deepEqual([...status.projection.paths].sort(), ["active/document.md", "archive/document.md", "report.md"], "native STATUS projects the rename as both of its paths");
242
+
243
+ const started = await __testing.executeReviewControllerOperation({ operation: "start", input: JSON.stringify({ mode: "ordinary" }) }, cwd, native);
244
+ assert.notEqual(started.outcome, "native-operation-failed", `START must reach native admission: ${JSON.stringify(started)}`);
245
+ assert.equal(started.operation, "start");
246
+ const result = started.result as Record<string, unknown>;
247
+ assert.equal(typeof result.lineage_id, "string", "native START must create a lineage");
248
+ // Documentation-only candidate: native admission closes it approved with
249
+ // no lenses, exactly as the low-risk journey above.
250
+ assert.equal(result.action, "closed");
251
+ assert.equal(result.state, "approved");
252
+ assert.equal(calls.filter((arguments_) => arguments_.at(0) === "review" && arguments_.at(1) === "start").length, 1, "exactly one native START runs");
335
253
  });
336
254
 
337
255
  test.before(() => {
338
- if (RUNNABLE) {
339
- setNativeCliContractForTesting(BRIDGE_VERSION, {
340
- start: true, finalize: true, validate: true, bindSdd: true, sddStatus: true, status: true, inventory: true,
341
- reclaim: true, recover: true, abandon: true, quarantineLegacy: true, reconcileAuthority: true, repairLegacyAlias: true,
342
- mode: true, riskEvidence: true, hint: true, delivery: true,
343
- });
344
- }
256
+ process.env.HOME = DEV_HOME;
257
+ process.env.USERPROFILE = DEV_HOME;
345
258
  });
346
259
  test.after(() => {
347
- if (RUNNABLE) setNativeCliContractForTesting(BRIDGE_VERSION, undefined);
260
+ if (ORIGINAL_HOME === undefined) delete process.env.HOME;
261
+ else process.env.HOME = ORIGINAL_HOME;
262
+ if (ORIGINAL_USERPROFILE === undefined) delete process.env.USERPROFILE;
263
+ else process.env.USERPROFILE = ORIGINAL_USERPROFILE;
264
+ rmSync(DEV_HOME, { recursive: true, force: true });
348
265
  });