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
@@ -0,0 +1,408 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+ import { __testing } from "../extensions/gentle-ai.ts";
6
+ import { reconcileUnknownReviewLastEventCapture } from "../lib/review-last-event-controller.ts";
7
+ import * as nativeReviewCliModule from "../lib/native-review-cli.ts";
8
+ import type { NativeReviewCli } from "../lib/native-review-cli.ts";
9
+ import {
10
+ decodeReviewLastEventClosureV1,
11
+ decodeReviewStartV3,
12
+ type ReviewCollectInputV3,
13
+ type ReviewStatusV3,
14
+ } from "../lib/review-integration-v2.ts";
15
+
16
+ const SHA = `sha256:${"a".repeat(64)}`;
17
+ const TREE = "b".repeat(40);
18
+ const CAPTURED_FIXTURES = join(process.cwd(), "tests", "fixtures", "devbinary");
19
+
20
+ function captured(name: string): unknown {
21
+ return JSON.parse(readFileSync(join(CAPTURED_FIXTURES, name), "utf8"));
22
+ }
23
+
24
+ function closure(operation: string, lineageId: string): Record<string, unknown> {
25
+ return {
26
+ schema: "gentle-ai.review-last-event-closure/v1",
27
+ operation,
28
+ lineage_id: lineageId,
29
+ state: operation === "review.capture-correction-plan" ? "correction_required" : "approved",
30
+ ...(operation === "review.capture-correction-plan"
31
+ ? { target_identity: SHA, request_hash: SHA, correction_lines: 2 }
32
+ : { action: "native last event closed the review" }),
33
+ store_revision: SHA,
34
+ };
35
+ }
36
+
37
+ function bindingArguments(lineageId: string, suffix = "0"): ReviewCollectInputV3["arguments"] {
38
+ return [
39
+ { name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
40
+ { name: "expected-revision", value: SHA, token: `--expected-revision=${SHA}` },
41
+ { name: "target", value: SHA, token: `--target=${SHA}` },
42
+ { name: "repository-context", value: `rctx1_${"c".repeat(64)}`, token: `--repository-context=rctx1_${"c".repeat(64)}` },
43
+ { name: "lens", value: `review-risk-${suffix}`, token: `--lens=review-risk-${suffix}` },
44
+ { name: "order", value: suffix, token: `--order=${suffix}` },
45
+ { name: "subject-hash", value: SHA, token: `--subject-hash=${SHA}` },
46
+ ];
47
+ }
48
+
49
+ function materializeInput(lineageId: string, suffix = "0"): ReviewCollectInputV3 {
50
+ const arguments_ = [
51
+ ...bindingArguments(lineageId, suffix),
52
+ { name: "agent", value: "pi", token: "--agent=pi" },
53
+ { name: "materialize", value: "true", token: "--materialize=true" },
54
+ ];
55
+ return {
56
+ name: "reviewer_result",
57
+ schema: "https://gentle-ai.dev/schema/review/reviewer/v1",
58
+ captureOperation: "review.capture-result",
59
+ arguments: arguments_,
60
+ artifactSubject: {
61
+ schema: "gentle-ai.review-artifact-subject/v2",
62
+ subjectHash: SHA,
63
+ lineageId,
64
+ authorityRevision: SHA,
65
+ targetIdentity: SHA,
66
+ baseTree: TREE,
67
+ candidateTree: TREE,
68
+ changedPathManifestSha256: SHA,
69
+ lens: `review-risk-${suffix}`,
70
+ selectedOrder: Number(suffix),
71
+ },
72
+ submission: {
73
+ operationToken: "capture-result",
74
+ argumentTokens: [...bindingArguments(lineageId, suffix).map((argument) => argument.token!), "--input={{value}}"],
75
+ values: [{ slot: "reviewer_result", domain: "artifact_path_or_stdin", substitutionLocation: 7 }],
76
+ },
77
+ };
78
+ }
79
+
80
+ function correctionPlanInput(lineageId: string): ReviewCollectInputV3 {
81
+ return {
82
+ name: "correction_plan",
83
+ schema: "gentle-ai.review-correction-plan/v1",
84
+ captureOperation: "review.capture-correction-plan",
85
+ arguments: bindingArguments(lineageId),
86
+ submission: {
87
+ operationToken: "capture-correction-plan",
88
+ argumentTokens: [...bindingArguments(lineageId).map((argument) => argument.token!), "--correction-lines={{value}}"],
89
+ values: [{ slot: "correction_lines", domain: "positive_integer", substitutionLocation: 7, minimum: 2, maximum: 3 }],
90
+ },
91
+ };
92
+ }
93
+
94
+ function roleInput(lineageId: string, operation: "review.capture-refuter" | "review.capture-validation"): ReviewCollectInputV3 {
95
+ return {
96
+ name: operation === "review.capture-refuter" ? "provider_refuter" : "targeted_validator",
97
+ schema: "gentle-ai.review-provider-role/v1",
98
+ captureOperation: operation,
99
+ arguments: [
100
+ ...bindingArguments(lineageId),
101
+ { name: "agent", value: "pi", token: "--agent=pi" },
102
+ { name: "execute", value: "true", token: "--execute=true" },
103
+ ],
104
+ };
105
+ }
106
+
107
+ function status(lineageId: string, inputs: readonly ReviewCollectInputV3[]): ReviewStatusV3 {
108
+ return {
109
+ contract: "gentle-ai.review-integration/v2",
110
+ applicability: "current_target",
111
+ authority: { version: "compact-v2", lineageId, state: "reviewing", generation: 1, revision: SHA },
112
+ action: "stop",
113
+ replayability: "not_replayable",
114
+ targetIdentity: SHA,
115
+ projection: {
116
+ schema: "gentle-ai.review-candidate-projection/v1",
117
+ kind: "current-changes",
118
+ projection: "workspace",
119
+ baseTree: TREE,
120
+ initialReviewTree: TREE,
121
+ currentCandidateTree: TREE,
122
+ pathsDigest: SHA,
123
+ paths: ["app.ts"],
124
+ intendedUntracked: [],
125
+ intendedUntrackedProof: SHA,
126
+ initialSnapshotIdentity: SHA,
127
+ currentSnapshotIdentity: SHA,
128
+ },
129
+ candidates: [],
130
+ nextTransition: { kind: "collect", reasonCode: "capture_required", collect: { inputs: [...inputs] } },
131
+ raw: { schema: "gentle-ai.review-integration.status/v5" },
132
+ } as unknown as ReviewStatusV3;
133
+ }
134
+
135
+ function capture(lineageId: string, input: ReviewCollectInputV3, native: NativeReviewCli, extras: Record<string, unknown> = {}): Promise<Record<string, unknown>> {
136
+ return __testing.executeReviewCaptureOperation({ lineageId, collectBinding: JSON.stringify(input), ...extras }, process.cwd(), native);
137
+ }
138
+
139
+ test("strictly decodes the real zero-lens closed START and every last-event closure", () => {
140
+ const start = decodeReviewStartV3(captured("start-v3-zero-lens-closed.captured.json"));
141
+ assert.equal(start.action, "closed");
142
+ assert.equal(start.state, "approved");
143
+ assert.equal(start.lensesRequired, false);
144
+ assert.deepEqual(start.selectedLenses, []);
145
+ for (const [name, operation, state] of [
146
+ ["last-event-capture-result-approved.captured.json", "review/capture-result", "approved"],
147
+ ["last-event-capture-result-correction-required.captured.json", "review/capture-result", "correction_required"],
148
+ ["last-event-capture-correction-plan.captured.json", "review.capture-correction-plan", "correction_required"],
149
+ ["last-event-capture-refuter-correction-required.captured.json", "review.capture-refuter", "correction_required"],
150
+ ["last-event-capture-refuter-approved.captured.json", "review.capture-refuter", "approved"],
151
+ ["last-event-capture-validation-approved.captured.json", "review/capture-validation", "approved"],
152
+ ] as const) {
153
+ const decoded = decodeReviewLastEventClosureV1(captured(name));
154
+ assert.equal(decoded.operation, operation, name);
155
+ assert.equal(decoded.state, state, name);
156
+ }
157
+ });
158
+
159
+ test("retired legacy client no longer exposes a FINALIZE route", () => {
160
+ assert.equal("NativeReviewCliV214" in nativeReviewCliModule, false);
161
+ assert.equal("NativeReviewCliV213" in nativeReviewCliModule, false);
162
+ });
163
+
164
+ test("one exact offered materialize binding executes only its selected slot and never follows STATUS", async (t) => {
165
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
166
+ const lineageId = "one-slot-lineage";
167
+ const selected = materializeInput(lineageId, "0");
168
+ const ignored = materializeInput(lineageId, "1");
169
+ let statusCalls = 0;
170
+ const native = {
171
+ targetStatus: async () => {
172
+ statusCalls += 1;
173
+ return status(lineageId, [selected, ignored]);
174
+ },
175
+ } as unknown as NativeReviewCli;
176
+ const relayed: unknown[] = [];
177
+ __testing.setReviewHostRelayRunnerForTesting(async (request) => {
178
+ relayed.push(request);
179
+ return { promptByteLength: 12, resultByteLength: 8, submission: "{\"admission_decision\":\"completed\"}" };
180
+ });
181
+
182
+ const result = await capture(lineageId, selected, native, { reviewerRunAcknowledged: true });
183
+ assert.equal(statusCalls, 1);
184
+ assert.equal(relayed.length, 1);
185
+ assert.equal(result.status, "captured");
186
+ assert.equal(result.outcome, "native-reviewer-result-captured");
187
+ assert.deepEqual((relayed[0] as { captureArgumentTokens: readonly string[] }).captureArgumentTokens, selected.arguments.map((argument) => argument.token));
188
+ });
189
+
190
+ test("malformed, stale, duplicate, and incomplete collect bindings reject before capture mutation", async (t) => {
191
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
192
+ const lineageId = "rejected-binding";
193
+ const input = materializeInput(lineageId);
194
+ let statusCalls = 0;
195
+ let launches = 0;
196
+ const native = { targetStatus: async () => { statusCalls += 1; return status(lineageId, [input]); } } as unknown as NativeReviewCli;
197
+ __testing.setReviewHostRelayRunnerForTesting(async () => { launches += 1; return { promptByteLength: 1, resultByteLength: 1, submission: "{}" }; });
198
+
199
+ await assert.rejects(() => __testing.executeReviewCaptureOperation({ lineageId, collectBinding: "{" }, process.cwd(), native), /collectBinding is not valid JSON/);
200
+ const stale = await __testing.executeReviewCaptureOperation({ lineageId, collectBinding: JSON.stringify(materializeInput("other")) }, process.cwd(), native);
201
+ const duplicate = await __testing.executeReviewCaptureOperation({ lineageId, collectBinding: JSON.stringify(input) }, process.cwd(), {
202
+ targetStatus: async () => { statusCalls += 1; return status(lineageId, [input, input]); },
203
+ } as unknown as NativeReviewCli);
204
+ assert.equal(stale.outcome, "capture-binding-rejected");
205
+ assert.equal(duplicate.outcome, "capture-binding-rejected");
206
+
207
+ const missingLineageToken: ReviewCollectInputV3 = { ...input, arguments: input.arguments.filter((argument) => argument.name !== "lineage") };
208
+ const missingTargetToken: ReviewCollectInputV3 = { ...input, arguments: input.arguments.filter((argument) => argument.name !== "target") };
209
+ const mismatchedLineageToken: ReviewCollectInputV3 = {
210
+ ...input,
211
+ arguments: input.arguments.map((argument) => argument.name === "lineage" ? { ...argument, value: "other-lineage" } : argument),
212
+ };
213
+ const mismatchedTargetToken: ReviewCollectInputV3 = {
214
+ ...input,
215
+ arguments: input.arguments.map((argument) => argument.name === "target" ? { ...argument, value: `sha256:${"d".repeat(64)}` } : argument),
216
+ };
217
+ const missingAuthorityLineage = { ...status(lineageId, [input]), authority: undefined } as ReviewStatusV3;
218
+ const emptyAuthorityLineage = {
219
+ ...status(lineageId, [input]),
220
+ authority: { ...status(lineageId, [input]).authority!, lineageId: "" },
221
+ } as unknown as ReviewStatusV3;
222
+ const missingStatusTarget = { ...status(lineageId, [missingTargetToken]), targetIdentity: undefined } as unknown as ReviewStatusV3;
223
+ const emptyStatusTarget = { ...status(lineageId, [input]), targetIdentity: "" } as unknown as ReviewStatusV3;
224
+ for (const [offeredInput, currentStatus] of [
225
+ [missingLineageToken, status(lineageId, [missingLineageToken])],
226
+ [missingTargetToken, status(lineageId, [missingTargetToken])],
227
+ [mismatchedLineageToken, status(lineageId, [mismatchedLineageToken])],
228
+ [mismatchedTargetToken, status(lineageId, [mismatchedTargetToken])],
229
+ [input, missingAuthorityLineage],
230
+ [input, emptyAuthorityLineage],
231
+ [missingTargetToken, missingStatusTarget],
232
+ [input, emptyStatusTarget],
233
+ ] as const) {
234
+ const rejected = await __testing.executeReviewCaptureOperation(
235
+ { lineageId, collectBinding: JSON.stringify(offeredInput), reviewerRunAcknowledged: true },
236
+ process.cwd(),
237
+ { targetStatus: async () => currentStatus } as unknown as NativeReviewCli,
238
+ );
239
+ assert.equal(rejected.outcome, "capture-binding-rejected");
240
+ }
241
+ assert.equal(launches, 0);
242
+ assert.equal(statusCalls, 2);
243
+ });
244
+
245
+ test("forecast spends zero and acknowledgement runs exactly one materialize reviewer", async (t) => {
246
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
247
+ const lineageId = "forecast-lineage";
248
+ const input = materializeInput(lineageId);
249
+ let statusCalls = 0;
250
+ let launches = 0;
251
+ const native = { targetStatus: async () => { statusCalls += 1; return status(lineageId, [input]); } } as unknown as NativeReviewCli;
252
+ __testing.setReviewHostRelayRunnerForTesting(async () => { launches += 1; return { promptByteLength: 1, resultByteLength: 1, submission: "{}" }; });
253
+
254
+ const forecast = await capture(lineageId, input, native);
255
+ const acknowledged = await capture(lineageId, input, native, { reviewerRunAcknowledged: true });
256
+ assert.equal(forecast.outcome, "reviewer-model-run-forecast");
257
+ assert.equal((forecast.cost_forecast as { model_runs: number }).model_runs, 1);
258
+ assert.equal(acknowledged.status, "captured");
259
+ assert.equal(launches, 1);
260
+ assert.equal(statusCalls, 2);
261
+ });
262
+
263
+ test("correction-plan preserves exact provider tokens and bounds without a follow-up lifecycle call", async () => {
264
+ const lineageId = "correction-plan";
265
+ const input = correctionPlanInput(lineageId);
266
+ let statusCalls = 0;
267
+ const requests: Array<{ argumentTokens: readonly string[]; correctionLines: number }> = [];
268
+ const native = {
269
+ targetStatus: async () => { statusCalls += 1; return status(lineageId, [input]); },
270
+ captureCorrectionPlan: async (request: { argumentTokens: readonly string[]; correctionLines: number }) => {
271
+ requests.push(request);
272
+ return decodeReviewLastEventClosureV1(closure("review.capture-correction-plan", lineageId));
273
+ },
274
+ } as unknown as NativeReviewCli;
275
+ const missing = await capture(lineageId, input, native);
276
+ const outOfBounds = await capture(lineageId, input, native, { correctionLines: 4 });
277
+ const completed = await capture(lineageId, input, native, { correctionLines: 2 });
278
+ assert.equal(missing.outcome, "correction-lines-required");
279
+ assert.equal(outOfBounds.outcome, "capture-binding-rejected");
280
+ assert.equal(completed.status, "closed");
281
+ assert.equal(requests.length, 1);
282
+ assert.deepEqual(requests[0]!.argumentTokens, input.submission!.argumentTokens);
283
+ assert.equal(requests[0]!.correctionLines, 2);
284
+ assert.equal(statusCalls, 3);
285
+ });
286
+
287
+ test("refuter and validation each execute one self-contained provider vector", async () => {
288
+ for (const operation of ["review.capture-refuter", "review.capture-validation"] as const) {
289
+ const lineageId = `role-${operation}`;
290
+ const input = roleInput(lineageId, operation);
291
+ const requests: Array<{ captureOperation: string; argumentTokens: readonly string[] }> = [];
292
+ const native = {
293
+ targetStatus: async () => status(lineageId, [input]),
294
+ captureProviderRole: async (request: { captureOperation: string; argumentTokens: readonly string[] }) => {
295
+ requests.push(request);
296
+ return { schema: "gentle-ai.review-provider-role-capture/v1", lineageId, targetIdentity: SHA, role: operation, captured: true };
297
+ },
298
+ } as unknown as NativeReviewCli;
299
+ const result = await capture(lineageId, input, native);
300
+ assert.equal(result.status, "captured");
301
+ assert.equal(requests.length, 1);
302
+ assert.equal(requests[0]!.captureOperation, operation);
303
+ assert.deepEqual(requests[0]!.argumentTokens, input.arguments.map((argument) => argument.token!));
304
+ }
305
+ });
306
+
307
+ test("terminal capture closes directly, nonterminal capture does not auto-follow, and unknown mutation reconciles once", async (t) => {
308
+ t.after(() => __testing.setReviewHostRelayRunnerForTesting());
309
+ const lineageId = "closure-behavior";
310
+ const input = materializeInput(lineageId);
311
+ let statusCalls = 0;
312
+ const native = { targetStatus: async () => { statusCalls += 1; return status(lineageId, [input]); } } as unknown as NativeReviewCli;
313
+ __testing.setReviewHostRelayRunnerForTesting(async () => ({ promptByteLength: 1, resultByteLength: 1, submission: JSON.stringify(closure("review/capture-result", lineageId)) }));
314
+ const terminal = await capture(lineageId, input, native, { reviewerRunAcknowledged: true });
315
+ assert.equal(terminal.status, "closed");
316
+ assert.equal(statusCalls, 1, "terminal closure performs no post-success STATUS");
317
+
318
+ statusCalls = 0;
319
+ let captureCalls = 0;
320
+ const correction = correctionPlanInput(lineageId);
321
+ const ambiguousNative = {
322
+ targetStatus: async () => { statusCalls += 1; return status(lineageId, [correction]); },
323
+ captureCorrectionPlan: async () => {
324
+ captureCalls += 1;
325
+ throw Object.assign(new Error("response lost"), { mutationOutcome: "unknown", nextAction: "review.status" });
326
+ },
327
+ } as unknown as NativeReviewCli;
328
+ const reconciled = await capture(lineageId, correction, ambiguousNative, { correctionLines: 2 });
329
+ assert.equal(reconciled.status, "reconciled");
330
+ assert.equal(captureCalls, 1);
331
+ assert.equal(statusCalls, 2, "only initial STATUS plus one ambiguity reconciliation");
332
+ });
333
+
334
+ test("unknown-capture reconciliation validates the target-scoped lineage without replay", async () => {
335
+ const calls: Array<Record<string, unknown>> = [];
336
+ const native = {
337
+ targetStatus: async (request: Record<string, unknown>) => {
338
+ calls.push(request);
339
+ assert.equal(request.lineageId, "reconcile-lineage");
340
+ return { targetIdentity: SHA, authority: { lineageId: "reconcile-lineage" } } as ReviewStatusV3;
341
+ },
342
+ } as unknown as NativeReviewCli;
343
+ const result = await reconcileUnknownReviewLastEventCapture(native, "/repo", { lineageId: "reconcile-lineage", targetIdentity: SHA });
344
+ await reconcileUnknownReviewLastEventCapture(native, "/repo", { lineageId: "reconcile-lineage", targetIdentity: SHA }, { baseRef: "refs/heads/main", committedOnly: true });
345
+ assert.deepEqual(calls, [{ cwd: "/repo", lineageId: "reconcile-lineage" }, { cwd: "/repo", lineageId: "reconcile-lineage", baseRef: "refs/heads/main", committedOnly: true }]);
346
+ assert.equal(result.targetIdentity, SHA);
347
+ let launches = 0;
348
+ const strictNative = new nativeReviewCliModule.NativeReviewCliV216(async () => { launches += 1; throw new Error("must not launch"); }, "/package/.gentle-ai/gentle-ai");
349
+ for (const selector of [{ committedOnly: true }, { baseRef: "refs/heads/main" }, { baseRef: "refs/heads/main", committedOnly: false }] as unknown as readonly Parameters<typeof reconcileUnknownReviewLastEventCapture>[3][]) await assert.rejects(() => reconcileUnknownReviewLastEventCapture(strictNative, "/repo", { lineageId: "reconcile-lineage", targetIdentity: SHA }, selector));
350
+ assert.equal(launches, 0);
351
+ });
352
+
353
+ // One approved terminal closure carrying the exact continuation the provider
354
+ // renders: closed positional arguments, the single approved precondition, and a
355
+ // binding that names the candidate being burned.
356
+ function approvedClosureWithAcknowledgement(): Record<string, unknown> {
357
+ const lineageId = "review-acknowledgement-fixture";
358
+ const body = closure("review/capture-result", lineageId);
359
+ body.acknowledgement = {
360
+ operation: "review.acknowledge-approved",
361
+ command: "gentle-ai review acknowledge-approved",
362
+ arguments: [
363
+ { name: "cwd", value: "/repo", token: "--cwd=/repo" },
364
+ { name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
365
+ { name: "target", value: SHA, token: `--target=${SHA}` },
366
+ { name: "expected-revision", value: SHA, token: `--expected-revision=${SHA}` },
367
+ { name: "token", value: "a".repeat(64), token: `--token=${"a".repeat(64)}` },
368
+ ],
369
+ preconditions: [{ name: "state", value: "approved" }],
370
+ binding: { lineage_id: lineageId, revision: SHA, target_identity: SHA },
371
+ };
372
+ return body;
373
+ }
374
+
375
+ test("the approved acknowledgement fixture decodes as a runnable continuation", () => {
376
+ const decoded = decodeReviewLastEventClosureV1(approvedClosureWithAcknowledgement());
377
+ assert.equal(decoded.acknowledgement?.operation, "review.acknowledge-approved");
378
+ assert.equal(decoded.acknowledgementUndecodable, undefined);
379
+ });
380
+
381
+ test("an approved acknowledgement must prove the target its relayed token burns", () => {
382
+ const closure = approvedClosureWithAcknowledgement();
383
+ const acknowledgement = closure.acknowledgement as Record<string, unknown>;
384
+ const args = (acknowledgement.arguments as Record<string, unknown>[]).map((argument) => ({ ...argument }));
385
+ // The relayed token now names a different candidate than the binding does.
386
+ const decoyTarget = `sha256:${"b".repeat(64)}`;
387
+ args[2] = { name: "target", value: decoyTarget, token: `--target=${decoyTarget}` };
388
+ acknowledgement.arguments = args;
389
+ assert.throws(
390
+ () => decodeReviewLastEventClosureV1(closure),
391
+ /acknowledgement target argument does not match its binding/,
392
+ "a relayed target the binding does not commit to must be refused",
393
+ );
394
+ });
395
+
396
+ test("an unreadable acknowledgement degrades the continuation, never the approval", () => {
397
+ const closure = approvedClosureWithAcknowledgement();
398
+ // One extra provider-side argument is enough to miss the closed shape.
399
+ const acknowledgement = closure.acknowledgement as Record<string, unknown>;
400
+ acknowledgement.arguments = [
401
+ ...(acknowledgement.arguments as unknown[]),
402
+ { name: "future", value: "1", token: "--future=1" },
403
+ ];
404
+ const decoded = decodeReviewLastEventClosureV1(closure);
405
+ assert.equal(decoded.state, "approved", "the approval outcome must survive an unreadable continuation");
406
+ assert.equal(decoded.acknowledgement, undefined, "an unreadable continuation must not be offered as runnable");
407
+ assert.equal(decoded.acknowledgementUndecodable, true, "the host must be told the continuation exists and could not be read");
408
+ });
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
- import { readFileSync } from "node:fs";
3
- import { join } from "node:path";
2
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { dirname, join, relative, resolve, sep } from "node:path";
4
4
  import test from "node:test";
5
5
  import { REVIEW_LENS_PARITY_PATTERNS } from "./support/review-lens-parity.ts";
6
6
 
@@ -85,7 +85,7 @@ const FIX_PATTERNS = [
85
85
  /Do not add findings, alter frozen claims, authorize transitions, deliver, publish, or start another actor\./,
86
86
  ] as const;
87
87
 
88
- test("canonical contract defines compact risk, causal admission, correction, CAS, compatibility, and gates", () => {
88
+ test("canonical contract defines compact risk, causal admission, correction, CAS, compatibility, and the delivery boundary", () => {
89
89
  const content = read(CANONICAL);
90
90
  assertMatches(CANONICAL, content, [
91
91
  /start -> finalize -> validate/,
@@ -101,29 +101,25 @@ test("canonical contract defines compact risk, causal admission, correction, CAS
101
101
  /original budget/i,
102
102
  /frozen findings and genesis scope/i,
103
103
  /content-derived revisions, compare-and-swap replacement, exact retry idempotency/i,
104
- /graph-v1 ordinary lineages remain readable and gate-validatable but reject new mutation/i,
104
+ /graph-v1 ordinary lineages remain readable for compatibility but reject new mutation/i,
105
105
  /Legacy graph bundle export\/import is retired/i,
106
106
  /Judgment Day remains mutable on graph-v1/i,
107
- /reloads authority and re-derives target\/publication evidence before allow/i,
108
- /one one-shot authorization for the exact subsequent command/i,
109
- /Native validation uses `gentle-ai\.review-integration\/v2`/i,
110
- /durable hook\/native-validation transaction/i,
111
- /Pi-owned `review-publication-gate` module isolates command projection and publication revalidation/i,
107
+ /--agent=pi --materialize=true/,
108
+ /provider-owned submission form/i,
109
+ /self-contained authority-advancing vectors/i,
110
+ /Commit, push, pull-request creation, and release creation are not RDD gates/i,
111
+ /Review outcomes and receipt state are informational and never authorize, consume, rewrite, or block a Bash delivery command/i,
112
+ /Pi does not inspect RDD mode or native authority for those commands/i,
113
+ /Review transactions, validation, and SDD never perform delivery commands themselves/i,
112
114
  /local orchestrator and same-user process are trusted/i,
113
115
  /reviewer and validator outputs remain semantically untrusted/i,
114
116
  /do not report.*trusted local orchestrator.*security finding/i,
115
117
  /untrusted repository content.*malformed inputs.*stale authority.*path drift.*external callers/i,
116
118
  ...JUDGMENT_DAY_PATTERNS,
117
119
  ]);
118
- assert.match(read(README), /Trust boundary:[\s\S]*separately privileged signer\/service/);
119
- assert.doesNotMatch(read(README), /Known limitation:[\s\S]*runtime-owned child-agent identity\/attestation/);
120
- assert.match(read(README), /split fetch\/push[\s\S]*unsupported[\s\S]*upstream[\s\S]*base-ref/i);
121
- assert.match(read(README), /Residual gap \(separate follow-up\): native first-push authorization remains unsupported until Pi has a persisted explicit advertised-base source\./);
122
- const lifecycleSpec = read("openspec/specs/review-transaction/spec.md");
123
- assert.match(lifecycleSpec, /split fetch\/push[\s\S]*upstream contract limitation/i);
124
- assert.match(lifecycleSpec, /allow response MUST return the exact requested gate/i);
125
- assert.match(lifecycleSpec, /non-authorizing denial MAY return an empty gate[\s\S]*pre_pr_boundary/i);
126
- assert.match(lifecycleSpec, /one aggregate bash-time deadline/i);
120
+ assert.match(read(README), /Review outcomes and receipt state are informational; commit, push, pull-request, and release delivery follow ordinary repository policy\./);
121
+ assert.doesNotMatch(read(README), /one one-shot authorization for the exact command/i);
122
+ assert.doesNotMatch(read(README), /review-publication-gate/i);
127
123
  });
128
124
 
129
125
  for (const path of REVIEW_LENSES) {
@@ -133,6 +129,69 @@ for (const path of REVIEW_LENSES) {
133
129
  });
134
130
  }
135
131
 
132
+ function packagePaths(): string[] {
133
+ const manifest = JSON.parse(read("package.json")) as { files?: unknown };
134
+ if (!Array.isArray(manifest.files)) throw new Error("package.json must declare package files");
135
+ return manifest.files.map((entry) => {
136
+ if (typeof entry !== "string") throw new Error("package.json files entries must be strings");
137
+ return entry.replace(/\/+$/, "");
138
+ });
139
+ }
140
+
141
+ function projectPath(absolutePath: string): string | undefined {
142
+ const path = relative(ROOT, absolutePath);
143
+ if (path === "" || path === ".." || path.startsWith(`..${sep}`)) return undefined;
144
+ return path.split(sep).join("/");
145
+ }
146
+
147
+ function isPackagedFile(path: string, packageRoots: readonly string[]): boolean {
148
+ const absolutePath = resolve(ROOT, path);
149
+ const projectRelativePath = projectPath(absolutePath);
150
+ if (!projectRelativePath || !existsSync(absolutePath) || !statSync(absolutePath).isFile()) return false;
151
+ return packageRoots.some((root) => projectRelativePath === root || projectRelativePath.startsWith(`${root}/`));
152
+ }
153
+
154
+ function packagedOrdinaryReviewLenses(): string[] {
155
+ const packageRoots = packagePaths();
156
+ return readdirSync(join(ROOT, "assets", "agents"), { withFileTypes: true })
157
+ .filter((entry) => entry.isFile() && entry.name.startsWith("review-") && entry.name.endsWith(".md"))
158
+ .map((entry) => `assets/agents/${entry.name}`)
159
+ .filter((path) => isPackagedFile(path, packageRoots))
160
+ .sort();
161
+ }
162
+
163
+ const MARKDOWN_FILE_REFERENCE = /(?:https?:\/\/[^\s)`\]]+|(?:\.{1,2}\/)?[A-Za-z0-9][A-Za-z0-9_./-]*)\.md\b/g;
164
+
165
+ function assertMarkdownFileDependenciesResolve(path: string, content: string, packageRoots: readonly string[]): void {
166
+ const sourceDirectory = dirname(join(ROOT, path));
167
+ const unresolved = [...content.matchAll(MARKDOWN_FILE_REFERENCE)]
168
+ .map((match) => match[0])
169
+ .filter((reference) => {
170
+ if (reference.startsWith("http://") || reference.startsWith("https://")) return true;
171
+ return ![resolve(ROOT, reference), resolve(sourceDirectory, reference)].some((candidate) => {
172
+ const candidatePath = projectPath(candidate);
173
+ return candidatePath !== undefined && isPackagedFile(candidatePath, packageRoots);
174
+ });
175
+ });
176
+ assert.deepEqual(unresolved, [], `${path} names unavailable Markdown dependencies: ${unresolved.join(", ")}`);
177
+ }
178
+
179
+ test("packaged ordinary review lenses resolve their Markdown file dependencies", () => {
180
+ const packageRoots = packagePaths();
181
+ const lenses = packagedOrdinaryReviewLenses();
182
+ assert.ok(lenses.length > 0, "package must ship at least one ordinary review lens");
183
+ for (const path of lenses) assertMarkdownFileDependenciesResolve(path, read(path), packageRoots);
184
+ });
185
+
186
+ test("ordinary review dependencies allow packaged files and reject unresolved paths", () => {
187
+ const packageRoots = packagePaths();
188
+ assertMarkdownFileDependenciesResolve("assets/agents/review-risk.md", "Sources: docs/native-authority-architecture.md", packageRoots);
189
+ assert.throws(
190
+ () => assertMarkdownFileDependenciesResolve("assets/agents/review-risk.md", "Sources: docs/nonexistent/security.md", packageRoots),
191
+ /unavailable Markdown dependencies: docs\/nonexistent\/security\.md/,
192
+ );
193
+ });
194
+
136
195
  test("ordinary lens prompts contain the literal compact-v2 native result envelope", () => {
137
196
  const expectedLenses = ["review-risk", "review-resilience", "review-readability", "review-reliability"];
138
197
  for (const [index, path] of REVIEW_LENSES.entries()) {
@@ -251,33 +310,36 @@ test("Judgment Day skill and prompts preserve bounded fix and re-judgment author
251
310
  assertMatches(FIX_AGENT, read(FIX_AGENT), FIX_PATTERNS);
252
311
  });
253
312
 
254
- test("orchestrator, skill, and README agree on compact facade and compatibility", () => {
313
+ test("orchestrator, injected skill, and README defer RDD lifecycle ownership to Gentle AI", () => {
314
+ const boundary = "Gentle AI dynamically supplies runtime-specific RDD instructions via generated Pi APPEND_SYSTEM composition. Follow only those exact native instructions; if absent or unsupported, this package does not invent or fall back.";
315
+ const orchestrator = union(ORCHESTRATOR);
316
+ assert.ok(orchestrator.includes(boundary), "orchestrator must carry the sole static ownership boundary");
317
+
255
318
  for (const [label, content] of [
256
- ["orchestrator", union(ORCHESTRATOR)],
257
319
  [GENTLE_SKILL, read(GENTLE_SKILL)],
258
320
  [README, read(README)],
259
321
  ] as const) {
260
322
  assertMatches(label, content, [
261
- /start -> finalize -> validate/,
262
- /`evidence_class`[\s\S]*`causal_disposition`/,
263
- /one correction transaction/i,
264
- /(?:graph-v1|legacy)[\s\S]*(?:read-only|reject mutation)/i,
265
- /Judgment Day[\s\S]*(?:explicit|separate)/i,
266
- /(?:one-shot|one exact one-shot)[\s\S]*(?:bash time|bash-time)/i,
323
+ /Gentle AI dynamically supplies runtime-specific RDD instructions/i,
324
+ /(?:sole lifecycle authority|does not define an RDD lifecycle)/i,
267
325
  ]);
268
326
  }
327
+
328
+ for (const [label, content] of [
329
+ ["orchestrator", orchestrator],
330
+ [GENTLE_SKILL, read(GENTLE_SKILL)],
331
+ ] as const) {
332
+ assert.doesNotMatch(content, /start -> finalize -> validate|INSPECT before START|next_transition|review\.capture-result/i, label);
333
+ }
269
334
  });
270
335
 
271
- test("README documents the exact native pairing and authority-preserving rollback boundary", () => {
336
+ test("README documents the dynamic runtime authority boundary without an old package route", () => {
272
337
  const content = read(README);
273
- assert.match(content, /package-local Gentle AI v2\.4\.0 executable/i);
274
- assert.match(content, /independently hashes it[\s\S]*negotiates `gentle-ai\.review-integration\/v2`/i);
275
- assert.match(content, /Capabilities are cached by that executable digest/i);
276
- assert.match(content, /Every START, target status, FINALIZE, validate, and BIND-SDD request passes the same contract identifier/i);
277
- assert.match(content, /rollback MUST preserve every native store and receipt/);
278
- assert.match(content, /MUST NOT run a downgraded binary/i);
279
- assert.match(content, /existing branch.*advertised commit equals.*old object/is);
280
- assert.match(content, /never guesses? a base.*upstream.*default branch.*nearest ancestor/is);
338
+ assert.match(content, /Gentle AI dynamically supplies runtime-specific RDD instructions/i);
339
+ assert.match(content, /does not define an RDD lifecycle/i);
340
+ assert.doesNotMatch(content, /New ordinary review uses compact `gentle_review` `start -> finalize -> validate`\./);
341
+ assert.match(content, /Dangerous-command safety remains independent and authoritative/);
342
+ assert.match(content, /Project and user overrides may shadow a package asset/);
281
343
  });
282
344
 
283
345
  test("managed contracts retain no fresh lifecycle review directive", () => {
@@ -138,11 +138,8 @@ async function runController(
138
138
  return await __testing.executeReviewControllerOperation(
139
139
  parameters,
140
140
  cwd,
141
- new Map(),
142
141
  native,
143
142
  undefined,
144
- undefined,
145
- undefined,
146
143
  candidateViews,
147
144
  ) as Record<string, unknown>;
148
145
  }
@@ -200,47 +197,3 @@ test("STATUS discovery never hydrates without pending reviewer-result collection
200
197
  await runController({ operation: "status", lineageId: successor }, cwd, native, registry);
201
198
  assert.equal(registry.hasCurrentBinding(), false, "an execute transition offers no reviewer dispatch and must not hydrate");
202
199
  });
203
-
204
- // --- DEFECT B: finalize routes strictly from the provider transition ---
205
-
206
- test("finalize at reviewer_results_required surfaces capture-result and never enters evidence ordering", async (t) => {
207
- const cwd = repository(t);
208
- const lineageId = "recovered-lineage";
209
- const status = () => recoveredStatus({
210
- lineageId,
211
- baseTree: "3".repeat(40),
212
- currentCandidateTree: "4".repeat(40),
213
- paths: ["tracked.txt"],
214
- inputs: [reviewerResultCollectInput(lineageId, "review-reliability", 0)],
215
- });
216
-
217
- // Plain finalize with no negotiated collection answers.
218
- const plainHarness = statusOnlyNative([status()]);
219
- const plain = await runController({ operation: "finalize", lineageId, input: JSON.stringify({}) }, cwd, plainHarness.native, new CandidateViewRegistry());
220
- assert.equal(plain.status, "blocked");
221
- assert.equal(plain.outcome, "reviewer-results-required");
222
- assert.match(String(plain.reason), /capture the reviewer result first/i);
223
- assert.match(String(plain.reason), /review\.capture-result/);
224
- assert.equal(plain.mutation_performed, false);
225
- assert.equal(plain.mutation_outcome, "none");
226
- assert.deepEqual(plain.pending_lenses, ["review-reliability"]);
227
- assert.equal(plainHarness.finalizeCalls(), 0, "raw native finalize must never run while reviewer results are outstanding");
228
- assert.doesNotMatch(JSON.stringify(plain), /evidence-first-ordering/);
229
-
230
- // The live misroute shape: finalize carrying final evidence must not fall
231
- // into the correction evidence-first-ordering lane either.
232
- const evidenceHarness = statusOnlyNative([status()]);
233
- const misrouted = await runController(
234
- { operation: "finalize", lineageId, input: JSON.stringify({ final_evidence: "all checks green", final_verification_passed: true }) },
235
- cwd,
236
- evidenceHarness.native,
237
- new CandidateViewRegistry(),
238
- );
239
- assert.equal(misrouted.status, "blocked");
240
- assert.equal(misrouted.outcome, "reviewer-results-required");
241
- assert.match(String(misrouted.reason), /capture the reviewer result first/i);
242
- assert.equal(misrouted.mutation_performed, false);
243
- assert.equal(misrouted.mutation_outcome, "none");
244
- assert.equal(evidenceHarness.finalizeCalls(), 0);
245
- assert.doesNotMatch(JSON.stringify(misrouted), /evidence-first-ordering/);
246
- });