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
@@ -11,7 +11,6 @@ import {
11
11
  decodeReviewConsentV2,
12
12
  decodeReviewFailureV2,
13
13
  decodeReviewNextTransitionV3,
14
- decodeReviewOperationV2,
15
14
  decodeReviewProjectionV1,
16
15
  decodeReviewRepairV2,
17
16
  decodeReviewStartV3,
@@ -19,7 +18,9 @@ import {
19
18
  } from "../lib/review-integration-v2.ts";
20
19
 
21
20
  const fixtureRoot = join(process.cwd(), "contracts", "review-integration", "v2", "fixtures");
21
+ const devFixtureRoot = join(process.cwd(), "tests", "fixtures", "devbinary");
22
22
  const fixture = <T = unknown>(name: string): T => JSON.parse(readFileSync(join(fixtureRoot, name), "utf8")) as T;
23
+ const devFixture = <T = unknown>(name: string): T => JSON.parse(readFileSync(join(devFixtureRoot, name), "utf8")) as T;
23
24
  const executableDigest = "dcc846103b16d365eaeeb9d7f289c23fc4f2897f23def1cb3fe7f05557b64705";
24
25
  const digest = `sha256:${"a".repeat(64)}`;
25
26
 
@@ -56,10 +57,9 @@ function assertAdditionalProperty(decoder: Decoder, source: JsonObject, path: re
56
57
  assert.throws(() => decoder(candidate), /not allowed/, path.length === 0 ? "top-level" : path.join("."));
57
58
  }
58
59
 
59
- test("every published review integration v2 fixture decodes", () => {
60
+ test("current review integration fixtures decode", () => {
60
61
  assert.equal(decodeReviewCapabilitiesV2(fixture("capabilities.fixture.json"), executableDigest).contract, REVIEW_INTEGRATION_CONTRACT);
61
62
  assert.equal(decodeReviewStartV3(fixture("start.fixture.json")).riskLevel, "high");
62
- assert.equal(decodeReviewStatusV3(fixture("status.fixture.json")).contract, REVIEW_INTEGRATION_CONTRACT);
63
63
  assert.equal(decodeReviewConsentV2(fixture("consent.fixture.json")).action, "consent_required");
64
64
  });
65
65
 
@@ -107,10 +107,52 @@ test("START enforces required, exact, and enum-bounded payloads", () => {
107
107
  }
108
108
  });
109
109
 
110
- test("status enforces required properties and rejects additional keys", () => {
111
- const source = fixture<JsonObject>("status.fixture.json");
112
- assertRequired(decodeReviewStatusV3, source, ["schema", "contract", "operation", "applicability", "receipt", "action", "replayability", "target_identity", "projection", "repair", "candidates"]);
113
- assertAdditionalProperty(decodeReviewStatusV3, source);
110
+ test("start/v3 transports the published selected_lenses wire contract without Pi-owned risk policy", () => {
111
+ const captured = devFixture<JsonObject>("start-v3-zero-lens-closed.captured.json");
112
+ const decoded = decodeReviewStartV3(captured);
113
+ assert.equal(decoded.action, "closed");
114
+ assert.equal(decoded.lensesRequired, false);
115
+ assert.deepEqual(decoded.selectedLenses, []);
116
+
117
+ for (const riskLevel of ["medium", "high"] as const) {
118
+ const noLens = clone(captured);
119
+ noLens.risk_level = riskLevel;
120
+ assert.doesNotThrow(() => decodeReviewStartV3(noLens));
121
+ }
122
+
123
+ const nullLenses = clone(captured);
124
+ nullLenses.selected_lenses = null;
125
+ assert.throws(() => decodeReviewStartV3(nullLenses), /selected_lenses.*invalid length/);
126
+
127
+ const unknownLens = clone(captured);
128
+ unknownLens.selected_lenses = ["review-future"];
129
+ assert.throws(() => decodeReviewStartV3(unknownLens), /selected_lenses.*unsupported/);
130
+
131
+ const tooManyLenses = clone(captured);
132
+ tooManyLenses.selected_lenses = ["review-risk", "review-resilience", "review-readability", "review-reliability", "review-future"];
133
+ assert.throws(() => decodeReviewStartV3(tooManyLenses), /selected_lenses.*invalid length/);
134
+
135
+ const duplicateLenses = clone(captured);
136
+ duplicateLenses.selected_lenses = ["review-risk", "review-risk"];
137
+ assert.throws(() => decodeReviewStartV3(duplicateLenses), /selected_lenses.*duplicates/);
138
+
139
+ const wrongSchema = clone(captured);
140
+ wrongSchema.schema = "gentle-ai.review-integration.start/v2";
141
+ assert.throws(() => decodeReviewStartV3(wrongSchema), /schema/);
142
+
143
+ const invalidAction = clone(captured);
144
+ invalidAction.action = "dispatch-locally";
145
+ assert.throws(() => decodeReviewStartV3(invalidAction), /action.*unsupported/);
146
+
147
+ const invalidLensesRequired = clone(captured);
148
+ invalidLensesRequired.lenses_required = "false";
149
+ assert.throws(() => decodeReviewStartV3(invalidLensesRequired), /lenses_required/);
150
+ });
151
+
152
+ test("historical status fixture with a receipt is rejected after receipt retirement", () => {
153
+ // This archived fixture is pinned for schema-rejection coverage only. Its
154
+ // bytes retain the retired receipt surface and must never regain authority.
155
+ assert.throws(() => decodeReviewStatusV3(fixture("status.fixture.json")), /receipt.*not allowed/);
114
156
  });
115
157
 
116
158
  test("projection enforces every required property and rejects additional keys", () => {
@@ -123,7 +165,7 @@ test("failure enforces exact keys, enums, and identifiers", () => {
123
165
  const source: JsonObject = {
124
166
  schema: "gentle-ai.review-integration.failure/v2",
125
167
  contract: REVIEW_INTEGRATION_CONTRACT,
126
- operation: "review.finalize",
168
+ operation: "review.capture-result",
127
169
  phase: "pre_native",
128
170
  code: "gate_scope_changed",
129
171
  message: "the target scope changed since START",
@@ -142,28 +184,6 @@ test("failure enforces exact keys, enums, and identifiers", () => {
142
184
  assert.throws(() => decodeReviewFailureV2(badCode), /code/);
143
185
  });
144
186
 
145
- function finalizeEnvelope(): JsonObject {
146
- return {
147
- schema: "gentle-ai.review-integration.operation/v2",
148
- contract: REVIEW_INTEGRATION_CONTRACT,
149
- operation: "review.finalize",
150
- result: {
151
- operation: "review/finalize",
152
- lineage_id: "review-fixture",
153
- state: "approved",
154
- action: "publish",
155
- store_revision: digest,
156
- },
157
- };
158
- }
159
-
160
- test("operation envelopes strictly bind the outer operation to one exact result variant", () => {
161
- const envelope = finalizeEnvelope();
162
- assert.equal(decodeReviewOperationV2(envelope).operation, "review.finalize");
163
- assertAdditionalProperty(decodeReviewOperationV2, envelope);
164
- assertNestedRequired(decodeReviewOperationV2, envelope, ["result"], ["operation", "lineage_id", "state", "action", "store_revision"]);
165
- });
166
-
167
187
  test("net-new decoders: consent, next-transition, and artifact-subject reject malformed payloads", () => {
168
188
  const consentSource = fixture<JsonObject>("consent.fixture.json");
169
189
  assertRequired(decodeReviewConsentV2, consentSource, ["schema", "contract", "operation", "action", "blocking", "target_identity", "projection", "risk_level", "changed_files", "changed_lines", "headline", "reason", "value", "risk_evidence", "choices", "off_path"]);
@@ -245,6 +265,50 @@ test("capabilities enforce the exact mandatory feature set while accepting a sup
245
265
  const extraOperation = clone(source);
246
266
  (extraOperation.operations as string[]).push("review.future_operation");
247
267
  assert.doesNotThrow(() => decode(extraOperation));
268
+
269
+ // mandatory features must stay an exact match: a known-but-optional feature
270
+ // name added to the mandatory list must still be rejected. This is the
271
+ // contract boundary (unknown_mandatory: "reject") and must not regress when
272
+ // gates/projections become additive-tolerant below.
273
+ const extraMandatoryUnknownAddition = clone(source);
274
+ ((extraMandatoryUnknownAddition.features as JsonObject).mandatory as JsonObject[]).push({ name: "bounded_process_waits", supported: true, requires: [] });
275
+ assert.throws(() => decode(extraMandatoryUnknownAddition), /mandatory/);
276
+ });
277
+
278
+ test("capabilities gates and projections accept an additive superset beyond the required floor", () => {
279
+ const source = fixture<JsonObject>("capabilities.fixture.json");
280
+ const decode = (value: unknown) => decodeReviewCapabilitiesV2(value, executableDigest);
281
+
282
+ const extraGate = clone(source);
283
+ (extraGate.gates as string[]).push("future-gate");
284
+ const decodedExtraGate = decode(extraGate) as { gates: ReadonlySet<string> };
285
+ assert.equal(decodedExtraGate.gates.has("future-gate"), false, "an unknown advertised gate must not leak into internal use");
286
+ for (const gate of ["post-apply", "pre-commit", "pre-push", "pre-pr", "release"]) {
287
+ assert.equal(decodedExtraGate.gates.has(gate), true, gate);
288
+ }
289
+ assert.equal(decodedExtraGate.gates.size, 5);
290
+
291
+ const extraProjection = clone(source);
292
+ (extraProjection.projections as string[]).push("future-projection");
293
+ const decodedExtraProjection = decode(extraProjection) as { projections: ReadonlySet<string> };
294
+ assert.equal(decodedExtraProjection.projections.has("future-projection"), false, "an unknown advertised projection must not leak into internal use");
295
+ for (const projection of ["staged", "workspace"]) {
296
+ assert.equal(decodedExtraProjection.projections.has(projection), true, projection);
297
+ }
298
+ assert.equal(decodedExtraProjection.projections.size, 2);
299
+ });
300
+
301
+ test("capabilities gates and projections still enforce the required floor", () => {
302
+ const source = fixture<JsonObject>("capabilities.fixture.json");
303
+ const decode = (value: unknown) => decodeReviewCapabilitiesV2(value, executableDigest);
304
+
305
+ const missingGate = clone(source);
306
+ missingGate.gates = (missingGate.gates as string[]).filter((gate) => gate !== "release");
307
+ assert.throws(() => decode(missingGate), /gates/);
308
+
309
+ const missingProjection = clone(source);
310
+ missingProjection.projections = (missingProjection.projections as string[]).filter((projection) => projection !== "workspace");
311
+ assert.throws(() => decode(missingProjection), /projections/);
248
312
  });
249
313
 
250
314
  test("START independently binds base/candidate tree and the target-mode overlay pair", () => {
@@ -271,97 +335,11 @@ test("START independently binds base/candidate tree and the target-mode overlay
271
335
  assert.throws(() => decodeReviewStartV3(manifestWithoutTrees), /changed_path_manifest/);
272
336
  });
273
337
 
274
- test("status enforces authority/frozen/receipt conditionals and decodes the required repair field", () => {
275
- const current = fixture<JsonObject>("status.fixture.json");
276
- const decoded = decodeReviewStatusV3(current);
277
- assert.equal(decoded.repair.status, "unsupported");
278
- assert.equal(decoded.repair.counts.eligibleCandidates, 0);
279
-
280
- const missingAuthority = clone(current);
281
- delete missingAuthority.authority;
282
- assert.throws(() => decodeReviewStatusV3(missingAuthority), /requires authority/);
283
-
284
- const compactMissingFrozen = clone(current);
285
- delete compactMissingFrozen.frozen;
286
- assert.throws(() => decodeReviewStatusV3(compactMissingFrozen), /requires frozen/);
287
-
288
- const legacyWithFrozen = clone(current);
289
- (legacyWithFrozen.authority as JsonObject).version = "legacy-v1";
290
- (legacyWithFrozen.receipt as JsonObject).status = "expected_missing";
291
- assert.throws(() => decodeReviewStatusV3(legacyWithFrozen), /legacy status cannot expose frozen/);
292
- });
293
-
294
- test("status validation_request and retry_final_verification require their exact authority preconditions", () => {
295
- const current = fixture<JsonObject>("status.fixture.json");
296
- const withValidationRequest = clone(current);
297
- withValidationRequest.validation_request = {
298
- schema: "gentle-ai.review-targeted-validation-request/v1",
299
- request_hash: digest,
300
- lineage_id: "review-status-fixture",
301
- expected_revision: digest,
302
- target_identity: (current.target_identity as string),
303
- fix_finding_ids: ["finding-1"],
304
- projection: "workspace",
305
- correction_candidate_tree: "a".repeat(40),
306
- correction_target_identity: digest,
307
- correction_paths: ["tracked.txt"],
308
- correction_paths_digest: digest,
309
- };
310
- // authority.state is "reviewing" in the fixture, not correction_required.
311
- assert.throws(() => decodeReviewStatusV3(withValidationRequest), /correction_required/);
312
-
313
- const retryWithoutIncident = clone(current);
314
- retryWithoutIncident.action = "retry_final_verification";
315
- retryWithoutIncident.action_disposition = "final_verification_retry";
316
- (retryWithoutIncident.authority as JsonObject).state = "escalated";
317
- assert.throws(() => decodeReviewStatusV3(retryWithoutIncident), /requires final_verification_retry/);
318
- });
319
-
320
- test("operation envelopes decode validate, bind_sdd, and retry_final_verification result variants", () => {
321
- const validateEnvelope: JsonObject = {
322
- schema: "gentle-ai.review-integration.operation/v2",
323
- contract: REVIEW_INTEGRATION_CONTRACT,
324
- operation: "review.validate",
325
- result: { schema: "gentle-ai.review-gate-result/v1", result: "allow", allowed: true, action: "publish", reason: "receipt matches", context: { gate: "pre-pr" } },
326
- };
327
- assert.equal(decodeReviewOperationV2(validateEnvelope).operation, "review.validate");
328
-
329
- const bindSddEnvelope: JsonObject = {
330
- schema: "gentle-ai.review-integration.operation/v2",
331
- contract: REVIEW_INTEGRATION_CONTRACT,
332
- operation: "review.bind_sdd",
333
- result: { schema: "gentle-ai.sdd-review-binding/v1", revision: digest, change: "migrate-review-integration-v2", lineage: "review-fixture", authority_revision: digest, receipt_hash: digest, gate_context: { gate: "post-apply" } },
334
- };
335
- assert.equal(decodeReviewOperationV2(bindSddEnvelope).operation, "review.bind_sdd");
336
-
337
- const retryEnvelope: JsonObject = {
338
- schema: "gentle-ai.review-integration.operation/v2",
339
- contract: REVIEW_INTEGRATION_CONTRACT,
340
- operation: "review.retry_final_verification",
341
- result: {
342
- operation: "review.retry_final_verification",
343
- predecessor_lineage_id: "review-predecessor",
344
- predecessor_revision: digest,
345
- lineage_id: "review-successor",
346
- state: "validating",
347
- store_revision: digest,
348
- target_identity: digest,
349
- incident_digest: digest,
350
- recovery_disposition: "final_verification_retry",
351
- },
352
- };
353
- assert.equal(decodeReviewOperationV2(retryEnvelope).operation, "review.retry_final_verification");
354
-
355
- const crossedResult = clone(validateEnvelope);
356
- crossedResult.result = bindSddEnvelope.result;
357
- assert.throws(() => decodeReviewOperationV2(crossedResult), /does not match|not allowed|required/);
358
- });
359
-
360
338
  test("failure context accepts scope_change or binding_revision but rejects both or neither", () => {
361
339
  const base: JsonObject = {
362
340
  schema: "gentle-ai.review-integration.failure/v2",
363
341
  contract: REVIEW_INTEGRATION_CONTRACT,
364
- operation: "review.finalize",
342
+ operation: "review.capture-result",
365
343
  phase: "pre_native",
366
344
  code: "gate_scope_changed",
367
345
  message: "the target scope changed since START",
@@ -427,6 +405,44 @@ test("next_transition decodes an execute variant and rejects a stop that carries
427
405
  assert.throws(() => decodeReviewNextTransitionV3(stopWithExecute), /stop cannot carry/);
428
406
  });
429
407
 
408
+ function approvedAcknowledgementTransition(): JsonObject {
409
+ return {
410
+ kind: "execute",
411
+ reason_code: "approved_acknowledgement_required",
412
+ execute: {
413
+ operation: "review.acknowledge-approved",
414
+ command: "gentle-ai review acknowledge-approved --provider-vector",
415
+ arguments: [
416
+ { name: "cwd", value: "/provider/repository", token: "--cwd=/provider/repository" },
417
+ { name: "lineage", value: "review-fixture", token: "--lineage=review-fixture" },
418
+ { name: "target", value: digest, token: `--target=${digest}` },
419
+ { name: "expected-revision", value: digest, token: `--expected-revision=${digest}` },
420
+ { name: "token", value: "provider-issued-once", token: "--token=provider-issued-once" },
421
+ ],
422
+ preconditions: [{ name: "state", value: "approved", token: "--state=approved" }],
423
+ binding: { lineage_id: "review-fixture", target_identity: digest, revision: digest },
424
+ },
425
+ };
426
+ }
427
+
428
+ test("acknowledgement execute rejects decoy bindings, closed-vector drift, and malformed approval", () => {
429
+ const valid = approvedAcknowledgementTransition();
430
+ assert.equal(decodeReviewNextTransitionV3(valid).execute?.operation, "review.acknowledge-approved");
431
+ const cases: Array<[string, (candidate: JsonObject) => void]> = [
432
+ ["decoy target", (candidate) => { const argument = ((candidate.execute as JsonObject).arguments as JsonObject[])[2]!; argument.value = `sha256:${"b".repeat(64)}`; argument.token = `--target=${argument.value}`; }],
433
+ ["mismatched expected revision", (candidate) => { const argument = ((candidate.execute as JsonObject).arguments as JsonObject[])[3]!; argument.value = `sha256:${"c".repeat(64)}`; argument.token = `--expected-revision=${argument.value}`; }],
434
+ ["wrong argument order", (candidate) => { ((candidate.execute as JsonObject).arguments as JsonObject[]).reverse(); }],
435
+ ["wrong argument name", (candidate) => { const argument = ((candidate.execute as JsonObject).arguments as JsonObject[])[0]!; argument.name = "repository"; argument.token = `--repository=${argument.value}`; }],
436
+ ["malformed approved precondition", (candidate) => { ((candidate.execute as JsonObject).preconditions as JsonObject[])[0]!.value = "burned"; }],
437
+ ["token value mismatch", (candidate) => { ((candidate.execute as JsonObject).arguments as JsonObject[])[4]!.token = "--token=other"; }],
438
+ ];
439
+ for (const [name, mutate] of cases) {
440
+ const candidate = clone(valid);
441
+ mutate(candidate);
442
+ assert.throws(() => decodeReviewNextTransitionV3(candidate), /acknowledgement|arguments|preconditions|binding/, name);
443
+ }
444
+ });
445
+
430
446
  test("repair decodes a committed execute result and rejects a non-eligible preflight carrying provider_inputs", () => {
431
447
  const executed: JsonObject = {
432
448
  schema: "gentle-ai.review-integration.repair/v2",
@@ -515,10 +531,10 @@ test("repair eligible preflight with wrong required_inputs order is rejected", (
515
531
  test("next_transition.execute accepts the optional selector_arguments and artifacts the schema declares", () => {
516
532
  const base = {
517
533
  kind: "execute" as const,
518
- reason_code: "captured_results_ready",
534
+ reason_code: "fresh_target_ready",
519
535
  execute: {
520
- operation: "review.finalize",
521
- command: "gentle-ai review finalize --lineage=review-96f29cbd865e77a9 --captured-results=true",
536
+ operation: "review.start",
537
+ command: "gentle-ai review start --lineage=review-96f29cbd865e77a9",
522
538
  arguments: [{ name: "lineage", value: "review-96f29cbd865e77a9", token: "--lineage=review-96f29cbd865e77a9" }],
523
539
  preconditions: [{ name: "target_identity", value: "sha256:" + "9".repeat(64) }],
524
540
  binding: { target_identity: "sha256:" + "9".repeat(64) },
@@ -526,23 +542,195 @@ test("next_transition.execute accepts the optional selector_arguments and artifa
526
542
  };
527
543
 
528
544
  assert.doesNotThrow(() => decodeReviewNextTransitionV3(base));
529
- assert.doesNotThrow(() => decodeReviewNextTransitionV3({ ...base, execute: { ...base.execute, artifacts: [{ name: "receipt", path: "review-receipt.json" }] } }));
545
+ assert.doesNotThrow(() => decodeReviewNextTransitionV3({ ...base, execute: { ...base.execute, artifacts: [{ name: "start-record", path: "review-start.json" }] } }));
530
546
  assert.doesNotThrow(() => decodeReviewNextTransitionV3({ ...base, execute: { ...base.execute, selector_arguments: [{ name: "projection", value: "workspace", token: "--projection=workspace" }] } }));
531
547
  });
532
548
 
549
+ // gentle-pi#311 P4-roles: the two Go-owned non-lens provider role capture
550
+ // operations render SELF-CONTAINED vectors (binding tokens + --agent=pi
551
+ // --execute=true). Their schemas are pinned, and a submission descriptor on
552
+ // either one is a contract violation because it would hand the caller a way
553
+ // to author the verdict.
554
+ test("next_transition decodes the self-contained provider role capture vectors strictly", () => {
555
+ const roleInput = (name: string, captureOperation: string, schema: string, extra: Record<string, unknown> = {}) => ({
556
+ kind: "collect",
557
+ reason_code: "provider_refuter_required",
558
+ collect: { inputs: [{
559
+ name,
560
+ schema,
561
+ capture_operation: captureOperation,
562
+ arguments: [
563
+ { name: "lineage", value: "review-fixture", token: "--lineage=review-fixture" },
564
+ { name: "expected-revision", value: digest, token: `--expected-revision=${digest}` },
565
+ { name: "target", value: digest, token: `--target=${digest}` },
566
+ { name: "repository-context", value: `rctx1_${"c".repeat(64)}`, token: `--repository-context=rctx1_${"c".repeat(64)}` },
567
+ { name: "agent", value: "pi", token: "--agent=pi" },
568
+ { name: "execute", value: "true", token: "--execute=true" },
569
+ ],
570
+ ...extra,
571
+ }] },
572
+ });
573
+
574
+ const decodeRoleTransition = (value: unknown) => decodeReviewNextTransitionV3(value, { v5: true });
575
+ const refuter = roleInput("provider_refuter", "review.capture-refuter", "https://gentle-ai.dev/schema/review/refuter/v1");
576
+ const decodedRefuter = decodeRoleTransition(refuter);
577
+ assert.equal(decodedRefuter.collect?.inputs[0]?.captureOperation, "review.capture-refuter");
578
+ assert.equal(decodedRefuter.collect?.inputs[0]?.arguments.at(-1)?.token, "--execute=true");
579
+
580
+ assert.throws(
581
+ () => decodeRoleTransition(roleInput("provider_refuter", "review.capture-refuter", "https://gentle-ai.dev/schema/review/reviewer/v1")),
582
+ /schema must be https:\/\/gentle-ai\.dev\/schema\/review\/refuter\/v1/,
583
+ );
584
+ assert.throws(
585
+ () => decodeRoleTransition(roleInput("provider_refuter", "review.capture-refuter", "https://gentle-ai.dev/schema/review/refuter/v1", {
586
+ submission: { operation_token: "capture-refuter", argument_tokens: ["--input={{value}}"], values: [{ slot: "{{value}}", domain: "artifact-path", substitution_location: 0 }] },
587
+ })),
588
+ /submission is not allowed on the self-contained/,
589
+ );
590
+
591
+ const validator = roleInput("provider_targeted_validator", "review.capture-validation", "https://gentle-ai.dev/schema/review/validator/v1");
592
+ assert.throws(
593
+ () => decodeRoleTransition(validator),
594
+ /validation_request is required/,
595
+ );
596
+
597
+ assert.throws(
598
+ () => decodeRoleTransition(roleInput("provider_targeted_validator", "review.capture-validation", "https://gentle-ai.dev/schema/review/refuter/v1")),
599
+ /schema must be https:\/\/gentle-ai\.dev\/schema\/review\/validator\/v1/,
600
+ );
601
+ });
602
+
603
+ test("v5 targeted-validator collect inputs carry the exact provider-owned validation request", () => {
604
+ const requestHash = `sha256:${"a".repeat(64)}`;
605
+ const expectedRevision = `sha256:${"b".repeat(64)}`;
606
+ const targetIdentity = `sha256:${"c".repeat(64)}`;
607
+ const correctionTargetIdentity = `sha256:${"d".repeat(64)}`;
608
+ const correctionPathsDigest = `sha256:${"e".repeat(64)}`;
609
+ const validationRequest = {
610
+ schema: "gentle-ai.review-targeted-validation-request/v1",
611
+ request_hash: requestHash,
612
+ lineage_id: "review-fixture",
613
+ expected_revision: expectedRevision,
614
+ target_identity: targetIdentity,
615
+ fix_finding_ids: ["R1-001"],
616
+ policy_content: "Gentle AI native bounded review policy.",
617
+ fix_findings: [{
618
+ id: "R1-001",
619
+ lens: "risk",
620
+ location: "selected.txt:1",
621
+ severity: "BLOCKER",
622
+ claim: "the selected relay input must be corrected before delivery",
623
+ proof_refs: ["selected.txt:1"],
624
+ evidence_class: "deterministic",
625
+ causal_disposition: "introduced",
626
+ }],
627
+ fix_classifications: [{
628
+ finding_id: "R1-001",
629
+ class: "deterministic",
630
+ causal_disposition: "introduced",
631
+ proof: "selected.txt:1",
632
+ }],
633
+ projection: "workspace",
634
+ correction_candidate_tree: "f".repeat(40),
635
+ correction_target_identity: correctionTargetIdentity,
636
+ correction_paths: ["selected.txt"],
637
+ correction_paths_digest: correctionPathsDigest,
638
+ };
639
+ const input = {
640
+ name: "provider_targeted_validator",
641
+ schema: "https://gentle-ai.dev/schema/review/validator/v1",
642
+ capture_operation: "review.capture-validation",
643
+ arguments: [
644
+ { name: "lineage", value: "review-fixture", token: "--lineage=review-fixture" },
645
+ { name: "expected-revision", value: expectedRevision, token: `--expected-revision=${expectedRevision}` },
646
+ { name: "target", value: correctionTargetIdentity, token: `--target=${correctionTargetIdentity}` },
647
+ { name: "repository-context", value: `rctx1_${"f".repeat(64)}`, token: `--repository-context=rctx1_${"f".repeat(64)}` },
648
+ { name: "request-hash", value: requestHash, token: `--request-hash=${requestHash}` },
649
+ { name: "agent", value: "pi", token: "--agent=pi" },
650
+ { name: "execute", value: "true", token: "--execute=true" },
651
+ ],
652
+ validation_request: validationRequest,
653
+ };
654
+ const transition = { kind: "collect" as const, reason_code: "targeted_validation_required", collect: { inputs: [input] } };
655
+ const decoded = decodeReviewNextTransitionV3(transition, { v5: true });
656
+ const decodedRequest = decoded.collect?.inputs[0]?.validationRequest;
657
+ assert.equal(decodedRequest?.requestHash, requestHash);
658
+ assert.equal(decodedRequest?.correctionTargetIdentity, correctionTargetIdentity);
659
+ assert.deepEqual(decodedRequest?.correctionPaths, ["selected.txt"]);
660
+ assert.equal(decodedRequest?.policyContent, validationRequest.policy_content);
661
+ assert.deepEqual(decodedRequest?.fixFindings, [{
662
+ id: "R1-001",
663
+ lens: "risk",
664
+ location: "selected.txt:1",
665
+ severity: "BLOCKER",
666
+ claim: "the selected relay input must be corrected before delivery",
667
+ proofRefs: ["selected.txt:1"],
668
+ evidenceClass: "deterministic",
669
+ causalDisposition: "introduced",
670
+ }]);
671
+ assert.deepEqual(decodedRequest?.fixClassifications, [{
672
+ findingId: "R1-001",
673
+ class: "deterministic",
674
+ causalDisposition: "introduced",
675
+ proof: "selected.txt:1",
676
+ }]);
677
+ assert.deepEqual(decoded.collect?.inputs[0]?.arguments, input.arguments, "provider-rendered arguments must remain unchanged");
678
+
679
+ const missingRequest = clone(transition);
680
+ delete (missingRequest.collect.inputs[0] as JsonObject).validation_request;
681
+ assert.throws(() => decodeReviewNextTransitionV3(missingRequest, { v5: true }), /validation_request is required/);
682
+
683
+ const mismatchedRequestHash = clone(transition);
684
+ ((mismatchedRequestHash.collect.inputs[0] as JsonObject).validation_request as JsonObject).request_hash = `sha256:${"0".repeat(64)}`;
685
+ assert.throws(() => decodeReviewNextTransitionV3(mismatchedRequestHash, { v5: true }), /request-hash/);
686
+
687
+ const refuter = clone(transition);
688
+ const refuterInput = refuter.collect.inputs[0] as JsonObject;
689
+ refuterInput.name = "provider_refuter";
690
+ refuterInput.schema = "https://gentle-ai.dev/schema/review/refuter/v1";
691
+ refuterInput.capture_operation = "review.capture-refuter";
692
+ assert.throws(() => decodeReviewNextTransitionV3(refuter, { v5: true }), /validation_request.*targeted-validator/);
693
+
694
+ const malformedRequest = clone(transition);
695
+ ((malformedRequest.collect.inputs[0] as JsonObject).validation_request as JsonObject).unadvertised = true;
696
+ assert.throws(() => decodeReviewNextTransitionV3(malformedRequest, { v5: true }), /not allowed/);
697
+
698
+ const weakenedRequest = clone(transition);
699
+ ((weakenedRequest.collect.inputs[0] as JsonObject).validation_request as JsonObject).policy_content = "";
700
+ assert.throws(() => decodeReviewNextTransitionV3(weakenedRequest, { v5: true }), /policy_content/);
701
+ });
702
+
703
+ test("status enforces authority/frozen/receipt conditionals and decodes the required repair field", () => {
704
+ const current = devFixture<JsonObject>("status-v5-repository-context.captured.json");
705
+ delete current.receipt;
706
+ if (current.action === "finalize") {
707
+ current.action = "stop";
708
+ delete current.next_transition;
709
+ delete current.forecast;
710
+ }
711
+ const decoded = decodeReviewStatusV3(current);
712
+ assert.equal(decoded.repair.status, "unsupported");
713
+ assert.equal(decoded.authority?.state, "reviewing");
714
+
715
+ const missingAuthority = clone(current);
716
+ delete missingAuthority.authority;
717
+ assert.throws(() => decodeReviewStatusV3(missingAuthority), /requires authority/);
718
+
719
+ const compactMissingFrozen = clone(current);
720
+ delete compactMissingFrozen.frozen;
721
+ assert.throws(() => decodeReviewStatusV3(compactMissingFrozen), /requires frozen/);
722
+ });
723
+
533
724
  test("next_transition.execute.binding stays open, as its schema declares no properties", () => {
534
725
  const withContext = {
535
726
  kind: "execute" as const,
536
- reason_code: "approved_receipt_ready",
727
+ reason_code: "fresh_target_ready",
537
728
  execute: {
538
- operation: "review.validate",
539
- arguments: [{ name: "gate", value: "pre-commit", token: "--gate=pre-commit" }],
540
- preconditions: [{ name: "target_identity", value: "sha256:" + "8".repeat(64) }],
541
- // The provider sends this. The schema constrains nothing here, so Pi
542
- // closing the object was stricter than the contract.
543
- binding: { target_identity: "sha256:" + "8".repeat(64), repository_context: "rctx1_" + "c".repeat(64) },
729
+ operation: "review.start",
730
+ arguments: [{ name: "lineage", value: "review-fixture", token: "--lineage=review-fixture" }],
731
+ preconditions: [{ name: "target_identity", value: `sha256:${"8".repeat(64)}` }],
732
+ binding: { target_identity: `sha256:${"8".repeat(64)}`, repository_context: `rctx1_${"c".repeat(64)}` },
544
733
  },
545
734
  };
546
-
547
735
  assert.doesNotThrow(() => decodeReviewNextTransitionV3(withContext));
548
736
  });