gentle-pi 2.1.2 → 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 (228) hide show
  1. package/README.md +108 -79
  2. package/assets/agents/gentle-ai-worker.md +7 -3
  3. package/assets/agents/jd-fix-agent.md +1 -1
  4. package/assets/agents/jd-judge-a.md +3 -1
  5. package/assets/agents/jd-judge-b.md +3 -1
  6. package/assets/agents/review-readability.md +4 -3
  7. package/assets/agents/review-reliability.md +4 -3
  8. package/assets/agents/review-resilience.md +4 -3
  9. package/assets/agents/review-risk.md +4 -3
  10. package/assets/agents/sdd-apply.md +11 -4
  11. package/assets/agents/sdd-archive.md +6 -1
  12. package/assets/agents/sdd-design.md +6 -1
  13. package/assets/agents/sdd-explore.md +6 -2
  14. package/assets/agents/sdd-init.md +10 -2
  15. package/assets/agents/sdd-onboard.md +6 -1
  16. package/assets/agents/sdd-proposal.md +8 -1
  17. package/assets/agents/sdd-research.md +54 -0
  18. package/assets/agents/sdd-spec.md +6 -1
  19. package/assets/agents/sdd-status.md +10 -5
  20. package/assets/agents/sdd-sync.md +6 -1
  21. package/assets/agents/sdd-tasks.md +8 -4
  22. package/assets/agents/sdd-verify.md +27 -2
  23. package/assets/chains/4r-review.chain.md +2 -0
  24. package/assets/chains/sdd-full.chain.md +2 -2
  25. package/assets/chains/sdd-plan.chain.md +1 -1
  26. package/assets/chains/sdd-verify.chain.md +2 -2
  27. package/assets/orchestrator-delegation.md +145 -160
  28. package/assets/orchestrator-memory.md +2 -0
  29. package/assets/orchestrator.md +25 -48
  30. package/assets/sdd-orchestrator-workflow.md +163 -25
  31. package/assets/support/sdd-status-contract.md +24 -6
  32. package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
  33. package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
  34. package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
  35. package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
  36. package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
  37. package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
  38. package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
  39. package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
  40. package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
  41. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  42. package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
  43. package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
  44. package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
  45. package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
  46. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  47. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  48. package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
  49. package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
  50. package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
  51. package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
  52. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
  53. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
  54. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
  55. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
  56. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
  57. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
  58. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
  59. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
  60. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
  61. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
  62. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
  63. package/docs/native-authority-architecture.md +14 -16
  64. package/docs/review-integration.md +27 -353
  65. package/extensions/ask-user-choice.ts +151 -0
  66. package/extensions/gentle-ai.ts +2710 -3211
  67. package/extensions/quiet-tools.ts +515 -32
  68. package/extensions/sdd-init.ts +21 -12
  69. package/extensions/skill-registry.ts +10 -2
  70. package/extensions/startup-banner.ts +10 -4
  71. package/lib/gentle-ai-binary.ts +173 -2
  72. package/lib/gentle-ai-renderer.ts +70 -0
  73. package/lib/model-routing-authority.ts +133 -0
  74. package/lib/native-review-cli.ts +676 -781
  75. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  76. package/lib/provider-contract-bundle.ts +704 -0
  77. package/lib/review-candidate-view.ts +836 -118
  78. package/lib/review-compact-contract.ts +59 -248
  79. package/lib/review-host-relay.ts +578 -0
  80. package/lib/review-integration-v2.ts +1254 -221
  81. package/lib/review-last-event-controller.ts +35 -0
  82. package/lib/review-relay-contract.ts +16 -0
  83. package/lib/sdd-preflight.ts +230 -67
  84. package/lib/sdd-status.ts +66 -111
  85. package/lib/terminal-theme.ts +1 -1
  86. package/package.json +83 -79
  87. package/runtime/gentle-ai-binary.mjs +174 -3
  88. package/runtime/native-review-cli.mjs +610 -715
  89. package/runtime/review-integration-v2.mjs +1223 -190
  90. package/runtime/review-relay-contract.mjs +17 -0
  91. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +5 -5
  92. package/scripts/check-provider-contract.mjs +138 -0
  93. package/scripts/gentle-ai-installer.mjs +85 -22
  94. package/scripts/maintainer/provider-relay-matrix.mjs +403 -0
  95. package/scripts/mirror-provider-contract.mjs +143 -0
  96. package/scripts/test-packed-runner.mjs +20 -9
  97. package/scripts/verify-package-files.mjs +119 -43
  98. package/skills/_shared/review-ledger-contract.md +11 -19
  99. package/skills/chained-pr/SKILL.md +3 -0
  100. package/skills/cognitive-doc-design/SKILL.md +1 -1
  101. package/skills/comment-writer/SKILL.md +1 -1
  102. package/skills/gentle-ai/SKILL.md +7 -74
  103. package/skills/issue-creation/SKILL.md +94 -168
  104. package/skills/judgment-day/SKILL.md +9 -5
  105. package/skills/judgment-day/references/prompts-and-formats.md +2 -0
  106. package/skills/rdd-defect-workflow/SKILL.md +54 -0
  107. package/skills/release/SKILL.md +3 -3
  108. package/skills/skill-registry/SKILL.md +1 -1
  109. package/skills/work-unit-commits/SKILL.md +3 -1
  110. package/tests/artifact-language.test.ts +24 -11
  111. package/tests/ask-user-choice.test.ts +264 -0
  112. package/tests/background-subagents.test.ts +771 -0
  113. package/tests/codegraph-tools.test.ts +3 -3
  114. package/tests/crosslane/cross-lane.mjs +16 -0
  115. package/tests/delegated-key-learnings-contract.test.ts +240 -0
  116. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  117. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  118. package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
  119. package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
  120. package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
  121. package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
  122. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  123. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  124. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  125. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  126. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  127. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  128. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  129. package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
  130. package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
  131. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  132. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  133. package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
  134. package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
  135. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  136. package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
  137. package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
  138. package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
  139. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  140. package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
  141. package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
  142. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
  143. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
  144. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
  145. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
  146. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
  147. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
  148. package/tests/gentle-ai-binary.test.ts +83 -13
  149. package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
  150. package/tests/gentle-ai-dev-binary.test.ts +159 -0
  151. package/tests/gentle-ai-installer.test.ts +138 -48
  152. package/tests/gentle-ai.test.ts +820 -131
  153. package/tests/gentle-theme.test.ts +133 -0
  154. package/tests/maintainer/provider-relay.maintest.ts +601 -0
  155. package/tests/model-routing-authority.test.ts +257 -0
  156. package/tests/native-review-capability-contract.test.ts +75 -2
  157. package/tests/native-review-cli.test.ts +588 -908
  158. package/tests/native-review-consent.test.ts +283 -38
  159. package/tests/native-review-parity-runtime.test.ts +108 -350
  160. package/tests/native-review-parity.test.ts +684 -732
  161. package/tests/native-sdd-attempt-authority.test.ts +235 -0
  162. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  163. package/tests/orchestrator-budget.test.ts +158 -89
  164. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  165. package/tests/package-manifest.test.ts +128 -100
  166. package/tests/provider-contract-bundle.test.ts +385 -0
  167. package/tests/provider-contract-mirror.test.ts +206 -0
  168. package/tests/provider-defect-handoff.test.ts +252 -0
  169. package/tests/quiet-tool-rendering.test.ts +1055 -28
  170. package/tests/review-actor-tool-deny.test.ts +12 -13
  171. package/tests/review-authority-recovery-docs.test.ts +1 -2
  172. package/tests/review-candidate-view.test.ts +898 -12
  173. package/tests/review-compact-contract.test.ts +29 -122
  174. package/tests/review-controller-lock-status.test.ts +2 -2
  175. package/tests/review-controller-native-recovery.test.ts +366 -857
  176. package/tests/review-controller-native-routing.test.ts +1253 -4199
  177. package/tests/review-controller-retired-ops.test.ts +1 -1
  178. package/tests/review-controller-workspace-root.test.ts +236 -70
  179. package/tests/review-controller.test.ts +26 -816
  180. package/tests/review-corrected-finalize-binding.test.ts +134 -0
  181. package/tests/review-dispatch-hydration-gap.test.ts +145 -0
  182. package/tests/review-gate.test.ts +0 -45
  183. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  184. package/tests/review-host-relay-routing.test.ts +352 -0
  185. package/tests/review-host-relay.test.ts +754 -0
  186. package/tests/review-integration-v2-forward.test.ts +654 -0
  187. package/tests/review-integration-v2.test.ts +317 -129
  188. package/tests/review-last-event-closure.test.ts +408 -0
  189. package/tests/review-ledger-contract.test.ts +106 -60
  190. package/tests/review-recovered-lineage-routing.test.ts +199 -0
  191. package/tests/review-relay-transport-agent.test.ts +322 -0
  192. package/tests/review-snapshot.test.ts +3 -2
  193. package/tests/runtime-harness.mjs +573 -180
  194. package/tests/sdd-agent-tools.test.ts +53 -32
  195. package/tests/sdd-preflight.test.ts +81 -15
  196. package/tests/sdd-status.test.ts +109 -110
  197. package/tests/skill-collision-prefixes.test.ts +6 -8
  198. package/tests/skill-registry.test.ts +50 -1
  199. package/tests/verify-package-files.test.ts +62 -0
  200. package/tests/writer-edit-surface-scope.test.ts +230 -0
  201. package/themes/Gentleman-Cute.json +94 -0
  202. package/themes/Gentleman-Sexy.json +92 -0
  203. package/assets/agents/review-refuter.md +0 -40
  204. package/assets/agents/review-validator.md +0 -23
  205. package/lib/git-commit-transaction.ts +0 -801
  206. package/lib/native-review-remediation.ts +0 -49
  207. package/lib/review-compact.ts +0 -947
  208. package/lib/review-refuter-adapter.ts +0 -129
  209. package/lib/review-runtime-contract.ts +0 -68
  210. package/prompts/gcl.md +0 -54
  211. package/prompts/gis.md +0 -25
  212. package/prompts/gpr.md +0 -41
  213. package/prompts/gwr.md +0 -31
  214. package/runtime/git-commit-transaction.mjs +0 -802
  215. package/scripts/run-git-commit-transaction.mjs +0 -35
  216. package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
  217. package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
  218. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
  219. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
  220. package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
  221. package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
  222. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
  223. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
  224. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  225. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  226. package/tests/git-commit-transaction.test.ts +0 -302
  227. package/tests/review-compact.test.ts +0 -243
  228. package/tests/review-refuter-adapter.test.ts +0 -89
@@ -0,0 +1,199 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import test from "node:test";
7
+ import { __testing } from "../extensions/gentle-ai.ts";
8
+ import type { NativeReviewCli } from "../lib/native-review-cli.ts";
9
+ import { CandidateViewRegistry, injectReviewCandidateView } from "../lib/review-candidate-view.ts";
10
+ import type { ReviewCollectInputV3, ReviewStatusV3 } from "../lib/review-integration-v2.ts";
11
+
12
+ // Live-confirmed adapter defects (2026-08-16, gentle-ai 2.4.0-main, Engram
13
+ // #12461/#12466), both around a lineage recovered EXTERNALLY through native
14
+ // `gentle-ai review recover` (disposition scope_changed):
15
+ //
16
+ // A. The controller never saw the successor's START, so direct reviewer
17
+ // dispatch refused with `no current controller-owned candidate view
18
+ // lineage binding` even though the controller itself had just decoded
19
+ // the successor's authoritative STATUS.
20
+ // B. `gentle_review finalize` on a lineage still at reviewer_results_required
21
+ // misrouted into the correction evidence-first-ordering lane and failed
22
+ // instead of surfacing the provider-offered review.capture-result step.
23
+
24
+ const SHA = `sha256:${"1".repeat(64)}`;
25
+
26
+ function git(cwd: string, ...arguments_: string[]): string {
27
+ return execFileSync("git", arguments_, { cwd, encoding: "utf8" }).trim();
28
+ }
29
+
30
+ function repository(t: test.TestContext): string {
31
+ const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-recovered-routing-"));
32
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
33
+ git(cwd, "init", "-b", "main");
34
+ writeFileSync(join(cwd, "tracked.txt"), "base\n");
35
+ git(cwd, "add", "tracked.txt");
36
+ git(cwd, "-c", "user.name=Recovered Test", "-c", "user.email=recovered@example.invalid", "commit", "-m", "base");
37
+ return cwd;
38
+ }
39
+
40
+ function reviewerResultCollectInput(lineageId: string, lens: string, order: number): ReviewCollectInputV3 {
41
+ return {
42
+ name: "reviewer_result",
43
+ schema: "https://gentle-ai.dev/schema/review/reviewer/v1",
44
+ captureOperation: "review.capture-result",
45
+ arguments: [
46
+ { name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
47
+ { name: "expected-revision", value: SHA, token: `--expected-revision=${SHA}` },
48
+ { name: "target", value: SHA, token: `--target=${SHA}` },
49
+ { name: "lens", value: lens, token: `--lens=${lens}` },
50
+ { name: "order", value: String(order), token: `--order=${order}` },
51
+ { name: "subject-hash", value: `sha256:${String(order).repeat(64)}`, token: `--subject-hash=sha256:${String(order).repeat(64)}` },
52
+ ],
53
+ artifactSubject: {
54
+ schema: "gentle-ai.review-artifact-subject/v2",
55
+ subjectHash: `sha256:${String(order).repeat(64)}`,
56
+ lineageId,
57
+ authorityRevision: SHA,
58
+ targetIdentity: SHA,
59
+ baseTree: "3".repeat(40),
60
+ candidateTree: "4".repeat(40),
61
+ changedPathManifestSha256: SHA,
62
+ lens,
63
+ selectedOrder: order,
64
+ },
65
+ } as unknown as ReviewCollectInputV3;
66
+ }
67
+
68
+ interface StatusFixtureShape {
69
+ lineageId: string;
70
+ baseTree: string;
71
+ currentCandidateTree: string;
72
+ paths: readonly string[];
73
+ inputs?: readonly ReviewCollectInputV3[];
74
+ executeFinalize?: boolean;
75
+ }
76
+
77
+ function recoveredStatus(shape: StatusFixtureShape): ReviewStatusV3 {
78
+ return {
79
+ contract: "gentle-ai.review-integration/v2",
80
+ applicability: "current_target",
81
+ authority: { version: "compact-v2", lineageId: shape.lineageId, state: "reviewer_results_required", generation: 2, revision: SHA },
82
+ receipt: { status: "expected_missing" },
83
+ action: "finalize",
84
+ replayability: "not_replayable",
85
+ targetIdentity: SHA,
86
+ projection: {
87
+ schema: "gentle-ai.review-integration.projection/v1",
88
+ kind: "current-changes",
89
+ projection: "workspace",
90
+ baseTree: shape.baseTree,
91
+ initialReviewTree: shape.currentCandidateTree,
92
+ currentCandidateTree: shape.currentCandidateTree,
93
+ pathsDigest: SHA,
94
+ paths: [...shape.paths],
95
+ intendedUntracked: [],
96
+ intendedUntrackedProof: SHA,
97
+ initialSnapshotIdentity: SHA,
98
+ currentSnapshotIdentity: SHA,
99
+ },
100
+ candidates: [],
101
+ ...(shape.inputs !== undefined
102
+ ? { nextTransition: { kind: "collect", reasonCode: "reviewer_results_required", collect: { inputs: [...shape.inputs] } } }
103
+ : {}),
104
+ ...(shape.executeFinalize === true
105
+ ? { nextTransition: { kind: "execute", reasonCode: "captured_results_ready", execute: { operation: "review.finalize", arguments: [], binding: { lineageId: shape.lineageId } } } }
106
+ : {}),
107
+ raw: { schema: "gentle-ai.review-integration.status/v5", action: "finalize", lineage_id: shape.lineageId },
108
+ } as unknown as ReviewStatusV3;
109
+ }
110
+
111
+ function statusOnlyNative(statuses: readonly ReviewStatusV3[]): { native: NativeReviewCli; finalizeCalls: () => number; statusCalls: () => number } {
112
+ const queue = [...statuses];
113
+ let finalizeCalls = 0;
114
+ let statusCalls = 0;
115
+ const native = {
116
+ targetStatus: async () => {
117
+ statusCalls += 1;
118
+ // The v5 lane legitimately re-queries STATUS (workspace-root
119
+ // rebind); keep serving the terminal fixture once drained.
120
+ const next = queue.length > 1 ? queue.shift() : queue[0];
121
+ if (next === undefined) throw new Error("status queue exhausted");
122
+ return next;
123
+ },
124
+ finalize: async () => {
125
+ finalizeCalls += 1;
126
+ throw new Error("native finalize must not be invoked while reviewer results are outstanding");
127
+ },
128
+ } as unknown as NativeReviewCli;
129
+ return { native, finalizeCalls: () => finalizeCalls, statusCalls: () => statusCalls };
130
+ }
131
+
132
+ async function runController(
133
+ parameters: Record<string, unknown>,
134
+ cwd: string,
135
+ native: NativeReviewCli,
136
+ candidateViews: CandidateViewRegistry,
137
+ ): Promise<Record<string, unknown>> {
138
+ return await __testing.executeReviewControllerOperation(
139
+ parameters,
140
+ cwd,
141
+ native,
142
+ undefined,
143
+ candidateViews,
144
+ ) as Record<string, unknown>;
145
+ }
146
+
147
+ // --- DEFECT A: STATUS discovery hydrates the dispatch binding ---
148
+
149
+ test("STATUS discovery hydrates the dispatch binding for an externally recovered lineage", async (t) => {
150
+ const cwd = repository(t);
151
+ writeFileSync(join(cwd, "tracked.txt"), "successor candidate change\n");
152
+ // Derive the real frozen identity of the live workspace candidate the way
153
+ // native recover froze it: the descriptor in status/v5 names these trees.
154
+ const probe = new CandidateViewRegistry();
155
+ const probeView = probe.create({ contributorRoot: cwd });
156
+ const frozen = { baseTree: probeView.baseTree, currentCandidateTree: probeView.candidateTree, paths: probeView.paths };
157
+ probe.cleanup(probeView.token);
158
+
159
+ const successor = "review-recovered-successor";
160
+ const status = recoveredStatus({ lineageId: successor, ...frozen, inputs: [reviewerResultCollectInput(successor, "review-reliability", 0)] });
161
+ const { native } = statusOnlyNative([status]);
162
+ const registry = new CandidateViewRegistry();
163
+
164
+ // The controller does not know this lineage: dispatch refuses.
165
+ assert.throws(
166
+ () => injectReviewCandidateView({ agent: "review-reliability", task: "review", mode: "task" }, registry),
167
+ /no current controller-owned candidate view lineage binding/,
168
+ );
169
+
170
+ const envelope = await runController({ operation: "status", lineageId: successor }, cwd, native, registry);
171
+ assert.equal(envelope.operation, "status");
172
+
173
+ // After the controller itself decoded the successor's authoritative
174
+ // STATUS, the dispatch binding must be hydrated from that status.
175
+ assert.equal(registry.hasCurrentBinding(), true, "STATUS discovery must hydrate the controller-owned dispatch binding");
176
+ try {
177
+ const dispatch: Record<string, unknown> = { agent: "review-reliability", task: "review the recovered successor", mode: "task" };
178
+ assert.doesNotThrow(() => injectReviewCandidateView(dispatch, registry));
179
+ assert.match(String(dispatch.task), new RegExp(successor));
180
+ assert.equal(registry.resolveCurrentForLens("review-reliability").candidateTree, frozen.currentCandidateTree);
181
+ } finally {
182
+ registry.cleanup(registry.resolveCurrentForLens("review-reliability").token);
183
+ }
184
+ });
185
+
186
+ test("STATUS discovery never hydrates without pending reviewer-result collection", async (t) => {
187
+ const cwd = repository(t);
188
+ writeFileSync(join(cwd, "tracked.txt"), "successor candidate change\n");
189
+ const probe = new CandidateViewRegistry();
190
+ const probeView = probe.create({ contributorRoot: cwd });
191
+ const frozen = { baseTree: probeView.baseTree, currentCandidateTree: probeView.candidateTree, paths: probeView.paths };
192
+ probe.cleanup(probeView.token);
193
+
194
+ const successor = "review-recovered-successor";
195
+ const { native } = statusOnlyNative([recoveredStatus({ lineageId: successor, ...frozen, executeFinalize: true })]);
196
+ const registry = new CandidateViewRegistry();
197
+ await runController({ operation: "status", lineageId: successor }, cwd, native, registry);
198
+ assert.equal(registry.hasCurrentBinding(), false, "an execute transition offers no reviewer dispatch and must not hydrate");
199
+ });
@@ -0,0 +1,322 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import test from "node:test";
7
+ import { __testing } from "../extensions/gentle-ai.ts";
8
+ import { NativeReviewIntegrationError, type NativeReviewCli } from "../lib/native-review-cli.ts";
9
+ import { CandidateViewRegistry } from "../lib/review-candidate-view.ts";
10
+ import type { ReviewCollectInputV3, ReviewStatusV3 } from "../lib/review-integration-v2.ts";
11
+ import type { ReviewHostRelayRequest } from "../lib/review-host-relay.ts";
12
+
13
+ // Third field failure on the recovered-lineage defect (2026-08-16, gentle-pi
14
+ // main 402f9f77 + gentle-ai 2.4.0-main.20278905): STATUS recognises the
15
+ // lineage, the Pi RELAY never runs, no lens is launched, zero mutations.
16
+ //
17
+ // Measured against the live binary on a faithful reproduction (linked
18
+ // worktree, uncommitted tracked files, externally recovered successor):
19
+ //
20
+ // agent-less STATUS -> collect input arguments:
21
+ // lineage, expected-revision, target, repository-context, lens, order,
22
+ // subject-hash (no agent, no materialize, no submission)
23
+ // `--agent pi` STATUS -> the same input PLUS agent=pi, materialize=true and
24
+ // the provider submission (operation_token=capture-result)
25
+ //
26
+ // The adapter's negotiated targetStatus never sent `--agent pi`, so the
27
+ // provider never offered the materialize-marked relay slot,
28
+ // reviewHostRelaySlots() returned 0 for every real lineage, and the host relay
29
+ // was unreachable. The prior fixes all exercised dispatch-shaped slots.
30
+ //
31
+ // Binaries older than v2.4.0 do not define `--agent` on `review status` and
32
+ // refuse it outright, so the agent is probed rather than version-sniffed and
33
+ // the typed refusal must reach the user instead of being swallowed.
34
+
35
+ const SHA = `sha256:${"1".repeat(64)}`;
36
+ const TRANSPORT_REFUSAL_CODE = "immutable_review_transport_unsupported";
37
+
38
+ function repository(t: test.TestContext): string {
39
+ const cwd = mkdtempSync(join(tmpdir(), "gentle-pi-relay-transport-"));
40
+ t.after(() => rmSync(cwd, { recursive: true, force: true }));
41
+ execFileSync("git", ["init", "-b", "main"], { cwd });
42
+ writeFileSync(join(cwd, "app.ts"), "export const value = 1;\n");
43
+ execFileSync("git", ["add", "."], { cwd });
44
+ execFileSync("git", ["-c", "user.name=Relay", "-c", "user.email=relay@example.invalid", "commit", "-m", "base"], { cwd });
45
+ return cwd;
46
+ }
47
+
48
+ function collectInput(lineageId: string, materialize: boolean): ReviewCollectInputV3 {
49
+ const binding = [
50
+ { name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
51
+ { name: "expected-revision", value: SHA, token: `--expected-revision=${SHA}` },
52
+ { name: "target", value: SHA, token: `--target=${SHA}` },
53
+ { name: "repository-context", value: `rctx1_${"e".repeat(64)}`, token: `--repository-context=rctx1_${"e".repeat(64)}` },
54
+ { name: "lens", value: "review-reliability", token: "--lens=review-reliability" },
55
+ { name: "order", value: "0", token: "--order=0" },
56
+ { name: "subject-hash", value: SHA, token: `--subject-hash=${SHA}` },
57
+ ];
58
+ return {
59
+ name: "reviewer_result",
60
+ schema: "https://gentle-ai.dev/schema/review/reviewer/v1",
61
+ captureOperation: "review.capture-result",
62
+ arguments: materialize
63
+ ? [...binding, { name: "agent", value: "pi", token: "--agent=pi" }, { name: "materialize", value: "true", token: "--materialize=true" }]
64
+ : binding,
65
+ artifactSubject: {
66
+ schema: "gentle-ai.review-artifact-subject/v2",
67
+ subjectHash: SHA,
68
+ lineageId,
69
+ authorityRevision: SHA,
70
+ targetIdentity: SHA,
71
+ baseTree: "3".repeat(40),
72
+ candidateTree: "4".repeat(40),
73
+ changedPathManifestSha256: SHA,
74
+ lens: "review-reliability",
75
+ selectedOrder: 0,
76
+ },
77
+ ...(materialize
78
+ ? {
79
+ submission: {
80
+ operationToken: "capture-result",
81
+ argumentTokens: [...binding.map((argument) => argument.token), "--input={{value}}"],
82
+ values: [{ slot: "reviewer_result", domain: "artifact_path_or_stdin", substitutionLocation: binding.length }],
83
+ },
84
+ }
85
+ : {}),
86
+ } as unknown as ReviewCollectInputV3;
87
+ }
88
+
89
+ function recoveredStatus(lineageId: string, materialize: boolean): ReviewStatusV3 {
90
+ return {
91
+ contract: "gentle-ai.review-integration/v2",
92
+ applicability: "current_target",
93
+ authority: { version: "compact-v2", lineageId, state: "reviewer_results_required", generation: 2, revision: SHA },
94
+ action: "stop",
95
+ replayability: "not_replayable",
96
+ targetIdentity: SHA,
97
+ projection: {
98
+ schema: "gentle-ai.review-integration.projection/v1",
99
+ kind: "current-changes",
100
+ projection: "workspace",
101
+ baseTree: "3".repeat(40),
102
+ initialReviewTree: "4".repeat(40),
103
+ currentCandidateTree: "4".repeat(40),
104
+ pathsDigest: SHA,
105
+ paths: ["app.ts"],
106
+ intendedUntracked: [],
107
+ intendedUntrackedProof: SHA,
108
+ initialSnapshotIdentity: SHA,
109
+ currentSnapshotIdentity: SHA,
110
+ },
111
+ candidates: [],
112
+ nextTransition: { kind: "collect", reasonCode: "reviewer_results_required", collect: { inputs: [collectInput(lineageId, materialize)] } },
113
+ raw: { schema: "gentle-ai.review-integration.status/v5", action: "stop", lineage_id: lineageId },
114
+ } as unknown as ReviewStatusV3;
115
+ }
116
+
117
+ /**
118
+ * Mirrors the MEASURED live provider: the materialize-marked relay slot is
119
+ * offered only when the caller asks for the pi agent.
120
+ */
121
+ function transportAwareNative(options: { refusalCode?: string } = {}): {
122
+ native: NativeReviewCli;
123
+ agents: Array<string | undefined>;
124
+ } {
125
+ const agents: Array<string | undefined> = [];
126
+ const native = {
127
+ targetStatus: async (request: { lineageId?: string; agent?: string }) => {
128
+ agents.push(request.agent);
129
+ if (request.agent === "pi" && options.refusalCode !== undefined) {
130
+ throw new NativeReviewIntegrationError({
131
+ schema: "gentle-ai.review-integration.failure/v2",
132
+ contract: "gentle-ai.review-integration/v2",
133
+ operation: "review.status",
134
+ phase: "pre_native",
135
+ code: options.refusalCode,
136
+ message: "supported immutable review runtimes: claude-code, opencode, codex",
137
+ mutationOutcome: "none",
138
+ authorityApplicability: "current_target",
139
+ retrySafe: true,
140
+ replayability: "not_replayable",
141
+ nextAction: "stop",
142
+ raw: {},
143
+ } as never);
144
+ }
145
+ return recoveredStatus(request.lineageId ?? "relay-lineage", request.agent === "pi");
146
+ },
147
+ } as unknown as NativeReviewCli;
148
+ return { native, agents };
149
+ }
150
+
151
+ async function runCapture(cwd: string, native: NativeReviewCli, lineageId: string, input: Record<string, unknown> = { reviewerRunAcknowledged: true }): Promise<Record<string, unknown>> {
152
+ return await __testing.executeReviewCaptureOperation(
153
+ { lineageId, collectBinding: JSON.stringify(collectInput(lineageId, true)), ...input },
154
+ cwd, native, undefined, new CandidateViewRegistry(),
155
+ ) as Record<string, unknown>;
156
+ }
157
+
158
+
159
+ async function runStatus(cwd: string, native: NativeReviewCli, lineageId: string): Promise<Record<string, unknown>> {
160
+ return await __testing.executeReviewControllerOperation(
161
+ { operation: "status", lineageId },
162
+ cwd, native, undefined, new CandidateViewRegistry(),
163
+ ) as Record<string, unknown>;
164
+ }
165
+
166
+ test("the negotiated status asks for the pi agent so the provider offers its materialize relay slot", async (t) => {
167
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
168
+ const cwd = repository(t);
169
+ const lineageId = "relay-recovered-successor";
170
+ const { native, agents } = transportAwareNative();
171
+ const relayed: ReviewHostRelayRequest[] = [];
172
+ __testing.setReviewHostRelayRunnerForTesting(async (request: ReviewHostRelayRequest) => {
173
+ relayed.push(request);
174
+ return { promptByteLength: 128, resultByteLength: 64, submission: '{"admission_decision":"completed"}' };
175
+ });
176
+
177
+ const result = await runCapture(cwd, native, lineageId);
178
+
179
+ assert.equal(agents.at(0), "pi", "the successful Pi route starts with pi-bound STATUS negotiation");
180
+ assert.ok(agents.every((agent) => agent === "pi"), "the selected capture performs no post-success agent-less STATUS re-query");
181
+ assert.equal(relayed.length, 1, "the materialize-marked slot must reach the host relay");
182
+ assert.ok(relayed[0]!.captureArgumentTokens.includes("--materialize=true"));
183
+ assert.ok(relayed[0]!.submission !== undefined, "the provider submission drives the completing form");
184
+ const hostRelay = result.host_relay as { transport: string } | undefined;
185
+ assert.ok(hostRelay !== undefined, "the envelope reports the one relay capture");
186
+ assert.equal(hostRelay.transport, "pi_host_relay");
187
+ });
188
+
189
+ test("capture forecasts the reviewer model run once and spends nothing until it is acknowledged", async (t) => {
190
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
191
+ const cwd = repository(t);
192
+ const lineageId = "forecast-lineage";
193
+ const { native } = transportAwareNative();
194
+ let launches = 0;
195
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
196
+ launches += 1;
197
+ return { promptByteLength: 128, resultByteLength: 64, submission: '{"admission_decision":"completed"}' };
198
+ });
199
+
200
+ // An unacknowledged capture must forecast, never spend.
201
+ const forecast = await runCapture(cwd, native, lineageId, {});
202
+ assert.equal(launches, 0, "no reviewer model run may start before the forecast is acknowledged");
203
+ assert.equal(forecast.status, "blocked");
204
+ assert.equal(forecast.outcome, "reviewer-model-run-forecast");
205
+ assert.equal(forecast.mutation_performed, false);
206
+ assert.equal(forecast.mutation_outcome, "none");
207
+ const cost = forecast.cost_forecast as { transport: string; model_runs: number; lenses: readonly string[] };
208
+ assert.equal(cost.transport, "pi_host_relay");
209
+ assert.equal(cost.model_runs, 1, "one model run per outstanding lens, forecast once before launch");
210
+ assert.deepEqual(cost.lenses, ["review-reliability"]);
211
+
212
+ // The acknowledged capture runs exactly the forecast work.
213
+ const acknowledged = await runCapture(cwd, native, lineageId, { reviewerRunAcknowledged: true });
214
+ assert.equal(launches, 1, "the acknowledged capture runs the forecast model run");
215
+ assert.equal(acknowledged.status, "captured");
216
+ });
217
+
218
+ test("a provider that refuses the pi transport blocks immediately without an agent-less lifecycle fallback", async (t) => {
219
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
220
+ const cwd = repository(t);
221
+ const lineageId = "legacy-transport-lineage";
222
+ const { native, agents } = transportAwareNative({ refusalCode: TRANSPORT_REFUSAL_CODE });
223
+ let relayCalls = 0;
224
+ __testing.setReviewHostRelayRunnerForTesting(async () => {
225
+ relayCalls += 1;
226
+ return { promptByteLength: 1, resultByteLength: 1, submission: "{}" };
227
+ });
228
+
229
+ const result = await runCapture(cwd, native, lineageId);
230
+
231
+ assert.deepEqual(agents, ["pi"], "the refusal path issues one pi-bound STATUS and never falls back to an agent-less STATUS");
232
+ assert.equal(relayCalls, 0, "a refused transport must not launch a relay capture");
233
+ const transport = result.relay_transport as { supported: boolean; code: string; message: string } | undefined;
234
+ assert.ok(transport !== undefined, "the blocked envelope must report the typed transport refusal");
235
+ assert.equal(transport.supported, false);
236
+ assert.equal(transport.code, TRANSPORT_REFUSAL_CODE);
237
+ assert.match(transport.message, /immutable review runtimes/i);
238
+ assert.equal(result.status, "blocked");
239
+ assert.equal(result.outcome, "pi-host-relay-transport-unavailable");
240
+ assert.equal(result.mutation_performed, false);
241
+ assert.equal(result.mutation_outcome, "none");
242
+ assert.match(String(result.next_action), /support.*--agent pi/i);
243
+ assert.doesNotMatch(JSON.stringify(result), /no current controller-owned candidate view/);
244
+ assert.doesNotMatch(JSON.stringify(result), /"status":"(?:approved|allowed)"/);
245
+ });
246
+
247
+ test("a transport refusal names the runnable wrapper continuation, never a raw CLI command as the only exit", async (t) => {
248
+ // #535: a provider-printed raw `gentle-ai review ...` continuation is a dead
249
+ // end in this runtime — Pi is not in the provider's immutable review runtime
250
+ // list — so the refusal envelope itself must name the exit that runs in this
251
+ // surface: the gentle_review / gentle_review_capture wrapper tools. The
252
+ // provider's diagnostic stays intact as evidence alongside the runnable exit.
253
+ const captureCwd = repository(t);
254
+ const { native: captureNative } = transportAwareNative({ refusalCode: TRANSPORT_REFUSAL_CODE });
255
+ const capture = await runCapture(captureCwd, captureNative, "continuation-capture-lineage");
256
+ __testing.clearReviewTransportProbeForTesting(captureNative);
257
+ assert.equal(capture.outcome, "pi-host-relay-transport-unavailable");
258
+ const captureNextAction = String(capture.next_action);
259
+ assert.match(captureNextAction, /gentle_review \{"operation":"inspect"\}/, "the capture refusal re-enters negotiated STATUS through the wrapper controller");
260
+ assert.match(captureNextAction, /gentle_review_capture/, "the capture refusal names the wrapper resubmission, not a CLI command");
261
+ assert.doesNotMatch(captureNextAction, /gentle-ai review/, "a raw `gentle-ai review ...` invocation is not a runnable exit in this runtime");
262
+ assert.deepEqual(capture.wrapper_continuation, { tool: "gentle_review", operation: "inspect", then: "gentle_review_capture" });
263
+ assert.match(String((capture.relay_transport as { message: string }).message), /immutable review runtimes/i, "the provider diagnostic remains intact in the envelope");
264
+
265
+ const statusCwd = repository(t);
266
+ const { native: statusNative } = transportAwareNative({ refusalCode: TRANSPORT_REFUSAL_CODE });
267
+ const status = await runStatus(statusCwd, statusNative, "continuation-status-lineage");
268
+ __testing.clearReviewTransportProbeForTesting(statusNative);
269
+ assert.equal(status.outcome, "pi-host-relay-transport-unavailable");
270
+ const statusNextAction = String(status.next_action);
271
+ assert.match(statusNextAction, /gentle_review \{"operation":"inspect"\}/, "the controller refusal re-enters negotiated STATUS through the wrapper controller");
272
+ assert.doesNotMatch(statusNextAction, /gentle-ai review/, "a raw `gentle-ai review ...` invocation is not a runnable exit in this runtime");
273
+ assert.deepEqual(status.wrapper_continuation, { tool: "gentle_review", operation: "inspect" });
274
+ });
275
+
276
+ test("a remembered pi transport refusal remains blocked without re-probing or lifecycle continuation", async (t) => {
277
+ const cwd = repository(t);
278
+ const { native, agents } = transportAwareNative({ refusalCode: TRANSPORT_REFUSAL_CODE });
279
+ const first = await runCapture(cwd, native, "probe-lineage");
280
+ const second = await runCapture(cwd, native, "probe-lineage");
281
+ assert.equal(first.outcome, "pi-host-relay-transport-unavailable");
282
+ assert.equal(second.outcome, "pi-host-relay-transport-unavailable");
283
+ assert.deepEqual(agents, ["pi"], "a cached refusal must not re-probe or issue an agent-less STATUS");
284
+ __testing.clearReviewTransportProbeForTesting(native);
285
+ });
286
+
287
+ test("every typed Pi transport refusal code fails closed", async (t) => {
288
+ const refusalCodes = [
289
+ "immutable_review_transport_unsupported",
290
+ "unsupported_agent",
291
+ "unknown_flag",
292
+ ];
293
+ for (const refusalCode of refusalCodes) {
294
+ const cwd = repository(t);
295
+ const { native, agents } = transportAwareNative({ refusalCode });
296
+ const result = await runCapture(cwd, native, `refusal-${refusalCode}`);
297
+ assert.equal(result.status, "blocked", `${refusalCode} must be blocked`);
298
+ assert.equal(result.outcome, "pi-host-relay-transport-unavailable", `${refusalCode} must use the unavailable envelope`);
299
+ assert.equal((result.relay_transport as { code: string }).code, refusalCode);
300
+ assert.deepEqual(agents, ["pi"], `${refusalCode} must not fall back to agent-less STATUS`);
301
+ __testing.clearReviewTransportProbeForTesting(native);
302
+ }
303
+ });
304
+
305
+ test("typed status errors outside the Pi transport refusal set remain native errors", async (t) => {
306
+ const cwd = repository(t);
307
+ const { native, agents } = transportAwareNative({ refusalCode: "provider_unavailable" });
308
+ const result = await runCapture(cwd, native, "non-transport-status-error");
309
+ assert.deepEqual(agents, ["pi"], "a non-transport error must not be retried as an agent-less lifecycle STATUS");
310
+ assert.equal(result.status, "blocked");
311
+ assert.equal(result.outcome, undefined, "a non-transport error must not be coerced into a transport refusal envelope");
312
+ assert.ok("native_failure" in result, "the native error envelope must remain available to the caller");
313
+ assert.equal(result.relay_transport, undefined);
314
+ });
315
+
316
+ test("ordinary STATUS inspection uses the Pi public collect-binding route", async (t) => {
317
+ const cwd = repository(t);
318
+ const { native, agents } = transportAwareNative();
319
+ const result = await runStatus(cwd, native, "ordinary-status-lineage");
320
+ assert.deepEqual(agents, ["pi"], "ordinary STATUS must request the Pi public collect-binding route");
321
+ assert.equal(result.operation, "status");
322
+ });
@@ -2,9 +2,10 @@ import assert from "node:assert/strict";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import {
4
4
  existsSync,
5
- mkdtempSync,
6
5
  mkdirSync,
6
+ mkdtempSync,
7
7
  readFileSync,
8
+ realpathSync,
8
9
  rmSync,
9
10
  writeFileSync,
10
11
  } from "node:fs";
@@ -60,7 +61,7 @@ function createRepository(t: test.TestContext): {
60
61
  repository: string;
61
62
  git: (...args: string[]) => string;
62
63
  } {
63
- const parent = mkdtempSync(join(tmpdir(), "gentle-pi-snapshot-"));
64
+ const parent = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-snapshot-")));
64
65
  const repository = join(parent, "repo");
65
66
  mkdirSync(repository);
66
67
  t.after(() => rmSync(parent, { recursive: true, force: true }));