gentle-pi 2.1.2 → 2.2.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 (171) hide show
  1. package/README.md +70 -12
  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 -1
  7. package/assets/agents/review-reliability.md +4 -1
  8. package/assets/agents/review-resilience.md +4 -1
  9. package/assets/agents/review-risk.md +4 -1
  10. package/assets/agents/sdd-apply.md +6 -1
  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 +6 -1
  17. package/assets/agents/sdd-spec.md +6 -1
  18. package/assets/agents/sdd-status.md +6 -1
  19. package/assets/agents/sdd-sync.md +6 -1
  20. package/assets/agents/sdd-tasks.md +6 -1
  21. package/assets/agents/sdd-verify.md +6 -1
  22. package/assets/chains/4r-review.chain.md +2 -0
  23. package/assets/chains/sdd-full.chain.md +1 -1
  24. package/assets/chains/sdd-plan.chain.md +1 -1
  25. package/assets/chains/sdd-verify.chain.md +1 -1
  26. package/assets/orchestrator-delegation.md +246 -67
  27. package/assets/orchestrator.md +7 -14
  28. package/assets/sdd-orchestrator-workflow.md +154 -9
  29. package/assets/support/sdd-status-contract.md +19 -1
  30. package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
  31. package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
  32. package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
  33. package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
  34. package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
  35. package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
  36. package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
  37. package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
  38. package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
  39. package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
  40. package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
  41. package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
  42. package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
  43. package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
  44. package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
  45. package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
  46. package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
  47. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
  48. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
  49. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
  50. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
  51. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
  52. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
  53. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
  54. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
  55. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
  56. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
  57. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
  58. package/docs/native-authority-architecture.md +5 -5
  59. package/docs/review-integration.md +22 -2
  60. package/extensions/gentle-ai.ts +1595 -201
  61. package/extensions/sdd-init.ts +19 -6
  62. package/extensions/skill-registry.ts +10 -2
  63. package/extensions/startup-banner.ts +10 -4
  64. package/lib/gentle-ai-binary.ts +173 -2
  65. package/lib/git-commit-transaction.ts +77 -17
  66. package/lib/native-review-cli.ts +528 -65
  67. package/lib/provider-contract-bundle.ts +704 -0
  68. package/lib/review-candidate-view.ts +527 -18
  69. package/lib/review-compact-contract.ts +59 -248
  70. package/lib/review-host-relay.ts +436 -0
  71. package/lib/review-integration-v2.ts +537 -36
  72. package/lib/review-relay-contract.ts +16 -0
  73. package/lib/sdd-preflight.ts +53 -1
  74. package/package.json +5 -2
  75. package/runtime/gentle-ai-binary.mjs +173 -2
  76. package/runtime/git-commit-transaction.mjs +75 -15
  77. package/runtime/native-review-cli.mjs +524 -61
  78. package/runtime/review-integration-v2.mjs +536 -35
  79. package/runtime/review-relay-contract.mjs +17 -0
  80. package/scripts/build-git-commit-transaction-runner.mjs +1 -0
  81. package/scripts/check-provider-contract.mjs +138 -0
  82. package/scripts/gentle-ai-installer.mjs +23 -13
  83. package/scripts/maintainer/provider-relay-matrix.mjs +219 -0
  84. package/scripts/mirror-provider-contract.mjs +143 -0
  85. package/scripts/test-packed-runner.mjs +16 -2
  86. package/scripts/verify-package-files.mjs +110 -33
  87. package/skills/_shared/review-ledger-contract.md +4 -6
  88. package/skills/gentle-ai/SKILL.md +4 -4
  89. package/skills/issue-creation/SKILL.md +94 -168
  90. package/skills/judgment-day/SKILL.md +7 -1
  91. package/skills/judgment-day/references/prompts-and-formats.md +2 -0
  92. package/skills/rdd-defect-workflow/SKILL.md +54 -0
  93. package/tests/background-subagents.test.ts +771 -0
  94. package/tests/crosslane/cross-lane.mjs +1169 -0
  95. package/tests/delegated-key-learnings-contract.test.ts +238 -0
  96. package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
  97. package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
  98. package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
  99. package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
  100. package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
  101. package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
  102. package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
  103. package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
  104. package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
  105. package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
  106. package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
  107. package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
  108. package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
  109. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
  110. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
  111. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
  112. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
  113. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
  114. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
  115. package/tests/gentle-ai-binary.test.ts +1 -1
  116. package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
  117. package/tests/gentle-ai-dev-binary.test.ts +336 -0
  118. package/tests/gentle-ai-installer.test.ts +46 -46
  119. package/tests/git-commit-transaction.test.ts +229 -1
  120. package/tests/maintainer/provider-relay.maintest.ts +265 -0
  121. package/tests/native-review-capability-contract.test.ts +48 -2
  122. package/tests/native-review-cli.test.ts +56 -0
  123. package/tests/native-review-consent.test.ts +164 -3
  124. package/tests/native-review-parity-runtime.test.ts +37 -0
  125. package/tests/native-review-parity.test.ts +218 -15
  126. package/tests/native-sdd-attempt-authority.test.ts +235 -0
  127. package/tests/orchestrator-budget.test.ts +30 -5
  128. package/tests/package-manifest.test.ts +98 -72
  129. package/tests/provider-contract-bundle.test.ts +385 -0
  130. package/tests/provider-contract-mirror.test.ts +206 -0
  131. package/tests/provider-defect-handoff.test.ts +355 -0
  132. package/tests/review-actor-tool-deny.test.ts +12 -13
  133. package/tests/review-candidate-view.test.ts +489 -9
  134. package/tests/review-compact-contract.test.ts +52 -119
  135. package/tests/review-controller-native-recovery.test.ts +643 -47
  136. package/tests/review-controller-native-routing.test.ts +1667 -222
  137. package/tests/review-controller-workspace-root.test.ts +17 -2
  138. package/tests/review-corrected-finalize-binding.test.ts +175 -0
  139. package/tests/review-dispatch-hydration-gap.test.ts +197 -0
  140. package/tests/review-host-relay-routing.test.ts +317 -0
  141. package/tests/review-host-relay.test.ts +520 -0
  142. package/tests/review-integration-v2-forward.test.ts +631 -0
  143. package/tests/review-integration-v2.test.ts +114 -0
  144. package/tests/review-ledger-contract.test.ts +12 -28
  145. package/tests/review-recovered-lineage-routing.test.ts +246 -0
  146. package/tests/review-relay-transport-agent.test.ts +249 -0
  147. package/tests/runtime-harness.mjs +242 -14
  148. package/tests/sdd-agent-tools.test.ts +18 -33
  149. package/tests/skill-collision-prefixes.test.ts +1 -0
  150. package/tests/skill-registry.test.ts +50 -1
  151. package/tests/verify-package-files.test.ts +62 -0
  152. package/assets/agents/review-refuter.md +0 -40
  153. package/assets/agents/review-validator.md +0 -23
  154. package/lib/native-review-remediation.ts +0 -49
  155. package/lib/review-compact.ts +0 -947
  156. package/lib/review-refuter-adapter.ts +0 -129
  157. package/lib/review-runtime-contract.ts +0 -68
  158. package/prompts/gcl.md +0 -54
  159. package/prompts/gis.md +0 -25
  160. package/prompts/gpr.md +0 -41
  161. package/prompts/gwr.md +0 -31
  162. package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
  163. package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
  164. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
  165. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
  166. package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
  167. package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
  168. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
  169. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
  170. package/tests/review-compact.test.ts +0 -243
  171. package/tests/review-refuter-adapter.test.ts +0 -89
@@ -245,6 +245,50 @@ test("capabilities enforce the exact mandatory feature set while accepting a sup
245
245
  const extraOperation = clone(source);
246
246
  (extraOperation.operations as string[]).push("review.future_operation");
247
247
  assert.doesNotThrow(() => decode(extraOperation));
248
+
249
+ // mandatory features must stay an exact match: a known-but-optional feature
250
+ // name added to the mandatory list must still be rejected. This is the
251
+ // contract boundary (unknown_mandatory: "reject") and must not regress when
252
+ // gates/projections become additive-tolerant below.
253
+ const extraMandatoryUnknownAddition = clone(source);
254
+ ((extraMandatoryUnknownAddition.features as JsonObject).mandatory as JsonObject[]).push({ name: "bounded_process_waits", supported: true, requires: [] });
255
+ assert.throws(() => decode(extraMandatoryUnknownAddition), /mandatory/);
256
+ });
257
+
258
+ test("capabilities gates and projections accept an additive superset beyond the required floor", () => {
259
+ const source = fixture<JsonObject>("capabilities.fixture.json");
260
+ const decode = (value: unknown) => decodeReviewCapabilitiesV2(value, executableDigest);
261
+
262
+ const extraGate = clone(source);
263
+ (extraGate.gates as string[]).push("future-gate");
264
+ const decodedExtraGate = decode(extraGate) as { gates: ReadonlySet<string> };
265
+ assert.equal(decodedExtraGate.gates.has("future-gate"), false, "an unknown advertised gate must not leak into internal use");
266
+ for (const gate of ["post-apply", "pre-commit", "pre-push", "pre-pr", "release"]) {
267
+ assert.equal(decodedExtraGate.gates.has(gate), true, gate);
268
+ }
269
+ assert.equal(decodedExtraGate.gates.size, 5);
270
+
271
+ const extraProjection = clone(source);
272
+ (extraProjection.projections as string[]).push("future-projection");
273
+ const decodedExtraProjection = decode(extraProjection) as { projections: ReadonlySet<string> };
274
+ assert.equal(decodedExtraProjection.projections.has("future-projection"), false, "an unknown advertised projection must not leak into internal use");
275
+ for (const projection of ["staged", "workspace"]) {
276
+ assert.equal(decodedExtraProjection.projections.has(projection), true, projection);
277
+ }
278
+ assert.equal(decodedExtraProjection.projections.size, 2);
279
+ });
280
+
281
+ test("capabilities gates and projections still enforce the required floor", () => {
282
+ const source = fixture<JsonObject>("capabilities.fixture.json");
283
+ const decode = (value: unknown) => decodeReviewCapabilitiesV2(value, executableDigest);
284
+
285
+ const missingGate = clone(source);
286
+ missingGate.gates = (missingGate.gates as string[]).filter((gate) => gate !== "release");
287
+ assert.throws(() => decode(missingGate), /gates/);
288
+
289
+ const missingProjection = clone(source);
290
+ missingProjection.projections = (missingProjection.projections as string[]).filter((projection) => projection !== "workspace");
291
+ assert.throws(() => decode(missingProjection), /projections/);
248
292
  });
249
293
 
250
294
  test("START independently binds base/candidate tree and the target-mode overlay pair", () => {
@@ -546,3 +590,73 @@ test("next_transition.execute.binding stays open, as its schema declares no prop
546
590
 
547
591
  assert.doesNotThrow(() => decodeReviewNextTransitionV3(withContext));
548
592
  });
593
+
594
+ // gentle-pi#311 P4-roles: the two Go-owned non-lens provider role capture
595
+ // operations render SELF-CONTAINED vectors (binding tokens + --agent=pi
596
+ // --execute=true). Their schemas are pinned, capture-validation carries the
597
+ // frozen validation request, and a submission descriptor on either one is a
598
+ // contract violation (it would hand the caller a way to author the verdict).
599
+ test("next_transition decodes the self-contained provider role capture vectors strictly", () => {
600
+ const tree = "b".repeat(40);
601
+ const validationRequest = {
602
+ schema: "gentle-ai.review-targeted-validation-request/v1",
603
+ request_hash: digest,
604
+ lineage_id: "review-fixture",
605
+ expected_revision: digest,
606
+ target_identity: digest,
607
+ fix_finding_ids: ["RISK-001"],
608
+ projection: "workspace",
609
+ correction_candidate_tree: tree,
610
+ correction_target_identity: digest,
611
+ correction_paths: ["lib/a.ts"],
612
+ correction_paths_digest: digest,
613
+ };
614
+ const roleInput = (name: string, captureOperation: string, schema: string, extra: Record<string, unknown> = {}) => ({
615
+ kind: "collect",
616
+ reason_code: "provider_refuter_required",
617
+ collect: { inputs: [{
618
+ name,
619
+ schema,
620
+ capture_operation: captureOperation,
621
+ arguments: [
622
+ { name: "lineage", value: "review-fixture", token: "--lineage=review-fixture" },
623
+ { name: "expected-revision", value: digest, token: `--expected-revision=${digest}` },
624
+ { name: "target", value: digest, token: `--target=${digest}` },
625
+ { name: "repository-context", value: `rctx1_${"c".repeat(64)}`, token: `--repository-context=rctx1_${"c".repeat(64)}` },
626
+ { name: "agent", value: "pi", token: "--agent=pi" },
627
+ { name: "execute", value: "true", token: "--execute=true" },
628
+ ],
629
+ ...extra,
630
+ }] },
631
+ });
632
+
633
+ const refuter = roleInput("provider_refuter", "review.capture-refuter", "https://gentle-ai.dev/schema/review/refuter/v1");
634
+ const decodedRefuter = decodeReviewNextTransitionV3(refuter);
635
+ assert.equal(decodedRefuter.collect?.inputs[0]?.captureOperation, "review.capture-refuter");
636
+ assert.equal(decodedRefuter.collect?.inputs[0]?.arguments.at(-1)?.token, "--execute=true");
637
+
638
+ assert.throws(
639
+ () => decodeReviewNextTransitionV3(roleInput("provider_refuter", "review.capture-refuter", "https://gentle-ai.dev/schema/review/reviewer/v1")),
640
+ /schema must be https:\/\/gentle-ai\.dev\/schema\/review\/refuter\/v1/,
641
+ );
642
+ assert.throws(
643
+ () => decodeReviewNextTransitionV3(roleInput("provider_refuter", "review.capture-refuter", "https://gentle-ai.dev/schema/review/refuter/v1", {
644
+ submission: { operation_token: "capture-refuter", argument_tokens: ["--input={{value}}"], values: [{ slot: "{{value}}", domain: "artifact-path", substitution_location: 0 }] },
645
+ })),
646
+ /submission is not allowed on the self-contained/,
647
+ );
648
+
649
+ const validator = roleInput("provider_targeted_validator", "review.capture-validation", "https://gentle-ai.dev/schema/review/validator/v1", { validation_request: validationRequest });
650
+ const decodedValidator = decodeReviewNextTransitionV3(validator);
651
+ assert.equal(decodedValidator.collect?.inputs[0]?.captureOperation, "review.capture-validation");
652
+ assert.equal(decodedValidator.collect?.inputs[0]?.validationRequest?.requestHash, digest);
653
+
654
+ assert.throws(
655
+ () => decodeReviewNextTransitionV3(roleInput("provider_targeted_validator", "review.capture-validation", "https://gentle-ai.dev/schema/review/validator/v1")),
656
+ /validation_request is required/,
657
+ );
658
+ assert.throws(
659
+ () => decodeReviewNextTransitionV3(roleInput("provider_targeted_validator", "review.capture-validation", "https://gentle-ai.dev/schema/review/refuter/v1", { validation_request: validationRequest })),
660
+ /schema must be https:\/\/gentle-ai\.dev\/schema\/review\/validator\/v1/,
661
+ );
662
+ });
@@ -14,8 +14,6 @@ const REVIEW_LENSES = [
14
14
  "assets/agents/review-reliability.md",
15
15
  ] as const;
16
16
  const JUDGES = ["assets/agents/jd-judge-a.md", "assets/agents/jd-judge-b.md"] as const;
17
- const REFUTER = "assets/agents/review-refuter.md";
18
- const VALIDATOR = "assets/agents/review-validator.md";
19
17
  const FIX_AGENT = "assets/agents/jd-fix-agent.md";
20
18
  const JD_SKILL = "skills/judgment-day/SKILL.md";
21
19
  const JD_PROMPTS = "skills/judgment-day/references/prompts-and-formats.md";
@@ -108,7 +106,7 @@ test("canonical contract defines compact risk, causal admission, correction, CAS
108
106
  /Judgment Day remains mutable on graph-v1/i,
109
107
  /reloads authority and re-derives target\/publication evidence before allow/i,
110
108
  /one one-shot authorization for the exact subsequent command/i,
111
- /gentle-ai\.review-integration\/v1/i,
109
+ /Native validation uses `gentle-ai\.review-integration\/v2`/i,
112
110
  /durable hook\/native-validation transaction/i,
113
111
  /Pi-owned `review-publication-gate` module isolates command projection and publication revalidation/i,
114
112
  /local orchestrator and same-user process are trusted/i,
@@ -196,29 +194,15 @@ test("risk lens distinguishes trusted orchestration from concrete boundary bypas
196
194
  assert.match(content, /untrusted repository content.*malformed inputs.*stale authority.*path drift.*external callers/i);
197
195
  });
198
196
 
199
- test("ordinary refuter is one complete read-only inferential batch with concrete proof", () => {
200
- const content = read(REFUTER);
201
- assertMatches(REFUTER, content, [
202
- /Receive the complete inferential-severe frozen-row list once/,
203
- /`gentle-ai\.refuter-result-batch\/v1`/,
204
- /`request_hash`[\s\S]*`finding_id`/,
205
- /`proof_refs`[\s\S]*`changed-hunk:`[\s\S]*`candidate-created-path:`[\s\S]*`differential-test:`[\s\S]*`before-after:`/,
206
- /independent concrete refuter proof is valid and need not repeat reviewer `proof_refs`/,
207
- /Use `inconclusive` when the supplied evidence supports neither `refuted` nor `corroborated`/,
208
- /Do not create findings, alter frozen claims, request fixes, launch actors, persist authority, or repeat/,
209
- ]);
210
- });
211
-
212
- test("targeted validator checks only original criteria and correction regression without expanding scope", () => {
213
- const content = read(VALIDATOR);
214
- assertMatches(VALIDATOR, content, [
215
- /original-criteria proof/,
216
- /correction-regression proof/,
217
- /Validate the original criteria and correction regression only/,
218
- /Never expand paths, IDs, untracked scope, acceptance criteria, or correction purpose/,
219
- /empty `fix_caused_findings` array/,
220
- /Do not request another fix or attempt, launch actors, persist authority, or repeat yourself/,
221
- ]);
197
+ test("the Pi-owned adversarial role agents are retired: roles execute through Go-owned pi processes", () => {
198
+ // gentle-pi#311 P5: the refuter and targeted validator are no longer
199
+ // Pi-authored actors. The provider renders self-contained
200
+ // review.capture-refuter / review.capture-validation vectors; executing
201
+ // them makes Go materialize the role prompt, spawn its own locked-down pi
202
+ // subprocess, and admit the raw verdict.
203
+ for (const retired of ["assets/agents/review-refuter.md", "assets/agents/review-validator.md"]) {
204
+ assert.throws(() => read(retired), `${retired} must be deleted`);
205
+ }
222
206
  });
223
207
 
224
208
  for (const path of JUDGES) {
@@ -286,8 +270,8 @@ test("orchestrator, skill, and README agree on compact facade and compatibility"
286
270
 
287
271
  test("README documents the exact native pairing and authority-preserving rollback boundary", () => {
288
272
  const content = read(README);
289
- assert.match(content, /package-local Gentle AI v2\.1\.11 executable/i);
290
- assert.match(content, /independently hashes it[\s\S]*negotiates `gentle-ai\.review-integration\/v1`/i);
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);
291
275
  assert.match(content, /Capabilities are cached by that executable digest/i);
292
276
  assert.match(content, /Every START, target status, FINALIZE, validate, and BIND-SDD request passes the same contract identifier/i);
293
277
  assert.match(content, /rollback MUST preserve every native store and receipt/);
@@ -0,0 +1,246 @@
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
+ new Map(),
142
+ native,
143
+ undefined,
144
+ undefined,
145
+ undefined,
146
+ candidateViews,
147
+ ) as Record<string, unknown>;
148
+ }
149
+
150
+ // --- DEFECT A: STATUS discovery hydrates the dispatch binding ---
151
+
152
+ test("STATUS discovery hydrates the dispatch binding for an externally recovered lineage", async (t) => {
153
+ const cwd = repository(t);
154
+ writeFileSync(join(cwd, "tracked.txt"), "successor candidate change\n");
155
+ // Derive the real frozen identity of the live workspace candidate the way
156
+ // native recover froze it: the descriptor in status/v5 names these trees.
157
+ const probe = new CandidateViewRegistry();
158
+ const probeView = probe.create({ contributorRoot: cwd });
159
+ const frozen = { baseTree: probeView.baseTree, currentCandidateTree: probeView.candidateTree, paths: probeView.paths };
160
+ probe.cleanup(probeView.token);
161
+
162
+ const successor = "review-recovered-successor";
163
+ const status = recoveredStatus({ lineageId: successor, ...frozen, inputs: [reviewerResultCollectInput(successor, "review-reliability", 0)] });
164
+ const { native } = statusOnlyNative([status]);
165
+ const registry = new CandidateViewRegistry();
166
+
167
+ // The controller does not know this lineage: dispatch refuses.
168
+ assert.throws(
169
+ () => injectReviewCandidateView({ agent: "review-reliability", task: "review", mode: "task" }, registry),
170
+ /no current controller-owned candidate view lineage binding/,
171
+ );
172
+
173
+ const envelope = await runController({ operation: "status", lineageId: successor }, cwd, native, registry);
174
+ assert.equal(envelope.operation, "status");
175
+
176
+ // After the controller itself decoded the successor's authoritative
177
+ // STATUS, the dispatch binding must be hydrated from that status.
178
+ assert.equal(registry.hasCurrentBinding(), true, "STATUS discovery must hydrate the controller-owned dispatch binding");
179
+ try {
180
+ const dispatch: Record<string, unknown> = { agent: "review-reliability", task: "review the recovered successor", mode: "task" };
181
+ assert.doesNotThrow(() => injectReviewCandidateView(dispatch, registry));
182
+ assert.match(String(dispatch.task), new RegExp(successor));
183
+ assert.equal(registry.resolveCurrentForLens("review-reliability").candidateTree, frozen.currentCandidateTree);
184
+ } finally {
185
+ registry.cleanup(registry.resolveCurrentForLens("review-reliability").token);
186
+ }
187
+ });
188
+
189
+ test("STATUS discovery never hydrates without pending reviewer-result collection", async (t) => {
190
+ const cwd = repository(t);
191
+ writeFileSync(join(cwd, "tracked.txt"), "successor candidate change\n");
192
+ const probe = new CandidateViewRegistry();
193
+ const probeView = probe.create({ contributorRoot: cwd });
194
+ const frozen = { baseTree: probeView.baseTree, currentCandidateTree: probeView.candidateTree, paths: probeView.paths };
195
+ probe.cleanup(probeView.token);
196
+
197
+ const successor = "review-recovered-successor";
198
+ const { native } = statusOnlyNative([recoveredStatus({ lineageId: successor, ...frozen, executeFinalize: true })]);
199
+ const registry = new CandidateViewRegistry();
200
+ await runController({ operation: "status", lineageId: successor }, cwd, native, registry);
201
+ assert.equal(registry.hasCurrentBinding(), false, "an execute transition offers no reviewer dispatch and must not hydrate");
202
+ });
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
+ });