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
@@ -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"]);
@@ -315,97 +335,11 @@ test("START independently binds base/candidate tree and the target-mode overlay
315
335
  assert.throws(() => decodeReviewStartV3(manifestWithoutTrees), /changed_path_manifest/);
316
336
  });
317
337
 
318
- test("status enforces authority/frozen/receipt conditionals and decodes the required repair field", () => {
319
- const current = fixture<JsonObject>("status.fixture.json");
320
- const decoded = decodeReviewStatusV3(current);
321
- assert.equal(decoded.repair.status, "unsupported");
322
- assert.equal(decoded.repair.counts.eligibleCandidates, 0);
323
-
324
- const missingAuthority = clone(current);
325
- delete missingAuthority.authority;
326
- assert.throws(() => decodeReviewStatusV3(missingAuthority), /requires authority/);
327
-
328
- const compactMissingFrozen = clone(current);
329
- delete compactMissingFrozen.frozen;
330
- assert.throws(() => decodeReviewStatusV3(compactMissingFrozen), /requires frozen/);
331
-
332
- const legacyWithFrozen = clone(current);
333
- (legacyWithFrozen.authority as JsonObject).version = "legacy-v1";
334
- (legacyWithFrozen.receipt as JsonObject).status = "expected_missing";
335
- assert.throws(() => decodeReviewStatusV3(legacyWithFrozen), /legacy status cannot expose frozen/);
336
- });
337
-
338
- test("status validation_request and retry_final_verification require their exact authority preconditions", () => {
339
- const current = fixture<JsonObject>("status.fixture.json");
340
- const withValidationRequest = clone(current);
341
- withValidationRequest.validation_request = {
342
- schema: "gentle-ai.review-targeted-validation-request/v1",
343
- request_hash: digest,
344
- lineage_id: "review-status-fixture",
345
- expected_revision: digest,
346
- target_identity: (current.target_identity as string),
347
- fix_finding_ids: ["finding-1"],
348
- projection: "workspace",
349
- correction_candidate_tree: "a".repeat(40),
350
- correction_target_identity: digest,
351
- correction_paths: ["tracked.txt"],
352
- correction_paths_digest: digest,
353
- };
354
- // authority.state is "reviewing" in the fixture, not correction_required.
355
- assert.throws(() => decodeReviewStatusV3(withValidationRequest), /correction_required/);
356
-
357
- const retryWithoutIncident = clone(current);
358
- retryWithoutIncident.action = "retry_final_verification";
359
- retryWithoutIncident.action_disposition = "final_verification_retry";
360
- (retryWithoutIncident.authority as JsonObject).state = "escalated";
361
- assert.throws(() => decodeReviewStatusV3(retryWithoutIncident), /requires final_verification_retry/);
362
- });
363
-
364
- test("operation envelopes decode validate, bind_sdd, and retry_final_verification result variants", () => {
365
- const validateEnvelope: JsonObject = {
366
- schema: "gentle-ai.review-integration.operation/v2",
367
- contract: REVIEW_INTEGRATION_CONTRACT,
368
- operation: "review.validate",
369
- result: { schema: "gentle-ai.review-gate-result/v1", result: "allow", allowed: true, action: "publish", reason: "receipt matches", context: { gate: "pre-pr" } },
370
- };
371
- assert.equal(decodeReviewOperationV2(validateEnvelope).operation, "review.validate");
372
-
373
- const bindSddEnvelope: JsonObject = {
374
- schema: "gentle-ai.review-integration.operation/v2",
375
- contract: REVIEW_INTEGRATION_CONTRACT,
376
- operation: "review.bind_sdd",
377
- 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" } },
378
- };
379
- assert.equal(decodeReviewOperationV2(bindSddEnvelope).operation, "review.bind_sdd");
380
-
381
- const retryEnvelope: JsonObject = {
382
- schema: "gentle-ai.review-integration.operation/v2",
383
- contract: REVIEW_INTEGRATION_CONTRACT,
384
- operation: "review.retry_final_verification",
385
- result: {
386
- operation: "review.retry_final_verification",
387
- predecessor_lineage_id: "review-predecessor",
388
- predecessor_revision: digest,
389
- lineage_id: "review-successor",
390
- state: "validating",
391
- store_revision: digest,
392
- target_identity: digest,
393
- incident_digest: digest,
394
- recovery_disposition: "final_verification_retry",
395
- },
396
- };
397
- assert.equal(decodeReviewOperationV2(retryEnvelope).operation, "review.retry_final_verification");
398
-
399
- const crossedResult = clone(validateEnvelope);
400
- crossedResult.result = bindSddEnvelope.result;
401
- assert.throws(() => decodeReviewOperationV2(crossedResult), /does not match|not allowed|required/);
402
- });
403
-
404
338
  test("failure context accepts scope_change or binding_revision but rejects both or neither", () => {
405
339
  const base: JsonObject = {
406
340
  schema: "gentle-ai.review-integration.failure/v2",
407
341
  contract: REVIEW_INTEGRATION_CONTRACT,
408
- operation: "review.finalize",
342
+ operation: "review.capture-result",
409
343
  phase: "pre_native",
410
344
  code: "gate_scope_changed",
411
345
  message: "the target scope changed since START",
@@ -471,6 +405,44 @@ test("next_transition decodes an execute variant and rejects a stop that carries
471
405
  assert.throws(() => decodeReviewNextTransitionV3(stopWithExecute), /stop cannot carry/);
472
406
  });
473
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
+
474
446
  test("repair decodes a committed execute result and rejects a non-eligible preflight carrying provider_inputs", () => {
475
447
  const executed: JsonObject = {
476
448
  schema: "gentle-ai.review-integration.repair/v2",
@@ -559,10 +531,10 @@ test("repair eligible preflight with wrong required_inputs order is rejected", (
559
531
  test("next_transition.execute accepts the optional selector_arguments and artifacts the schema declares", () => {
560
532
  const base = {
561
533
  kind: "execute" as const,
562
- reason_code: "captured_results_ready",
534
+ reason_code: "fresh_target_ready",
563
535
  execute: {
564
- operation: "review.finalize",
565
- command: "gentle-ai review finalize --lineage=review-96f29cbd865e77a9 --captured-results=true",
536
+ operation: "review.start",
537
+ command: "gentle-ai review start --lineage=review-96f29cbd865e77a9",
566
538
  arguments: [{ name: "lineage", value: "review-96f29cbd865e77a9", token: "--lineage=review-96f29cbd865e77a9" }],
567
539
  preconditions: [{ name: "target_identity", value: "sha256:" + "9".repeat(64) }],
568
540
  binding: { target_identity: "sha256:" + "9".repeat(64) },
@@ -570,47 +542,16 @@ test("next_transition.execute accepts the optional selector_arguments and artifa
570
542
  };
571
543
 
572
544
  assert.doesNotThrow(() => decodeReviewNextTransitionV3(base));
573
- 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" }] } }));
574
546
  assert.doesNotThrow(() => decodeReviewNextTransitionV3({ ...base, execute: { ...base.execute, selector_arguments: [{ name: "projection", value: "workspace", token: "--projection=workspace" }] } }));
575
547
  });
576
548
 
577
- test("next_transition.execute.binding stays open, as its schema declares no properties", () => {
578
- const withContext = {
579
- kind: "execute" as const,
580
- reason_code: "approved_receipt_ready",
581
- execute: {
582
- operation: "review.validate",
583
- arguments: [{ name: "gate", value: "pre-commit", token: "--gate=pre-commit" }],
584
- preconditions: [{ name: "target_identity", value: "sha256:" + "8".repeat(64) }],
585
- // The provider sends this. The schema constrains nothing here, so Pi
586
- // closing the object was stricter than the contract.
587
- binding: { target_identity: "sha256:" + "8".repeat(64), repository_context: "rctx1_" + "c".repeat(64) },
588
- },
589
- };
590
-
591
- assert.doesNotThrow(() => decodeReviewNextTransitionV3(withContext));
592
- });
593
-
594
549
  // gentle-pi#311 P4-roles: the two Go-owned non-lens provider role capture
595
550
  // 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).
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.
599
554
  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
555
  const roleInput = (name: string, captureOperation: string, schema: string, extra: Record<string, unknown> = {}) => ({
615
556
  kind: "collect",
616
557
  reason_code: "provider_refuter_required",
@@ -630,33 +571,166 @@ test("next_transition decodes the self-contained provider role capture vectors s
630
571
  }] },
631
572
  });
632
573
 
574
+ const decodeRoleTransition = (value: unknown) => decodeReviewNextTransitionV3(value, { v5: true });
633
575
  const refuter = roleInput("provider_refuter", "review.capture-refuter", "https://gentle-ai.dev/schema/review/refuter/v1");
634
- const decodedRefuter = decodeReviewNextTransitionV3(refuter);
576
+ const decodedRefuter = decodeRoleTransition(refuter);
635
577
  assert.equal(decodedRefuter.collect?.inputs[0]?.captureOperation, "review.capture-refuter");
636
578
  assert.equal(decodedRefuter.collect?.inputs[0]?.arguments.at(-1)?.token, "--execute=true");
637
579
 
638
580
  assert.throws(
639
- () => decodeReviewNextTransitionV3(roleInput("provider_refuter", "review.capture-refuter", "https://gentle-ai.dev/schema/review/reviewer/v1")),
581
+ () => decodeRoleTransition(roleInput("provider_refuter", "review.capture-refuter", "https://gentle-ai.dev/schema/review/reviewer/v1")),
640
582
  /schema must be https:\/\/gentle-ai\.dev\/schema\/review\/refuter\/v1/,
641
583
  );
642
584
  assert.throws(
643
- () => decodeReviewNextTransitionV3(roleInput("provider_refuter", "review.capture-refuter", "https://gentle-ai.dev/schema/review/refuter/v1", {
585
+ () => decodeRoleTransition(roleInput("provider_refuter", "review.capture-refuter", "https://gentle-ai.dev/schema/review/refuter/v1", {
644
586
  submission: { operation_token: "capture-refuter", argument_tokens: ["--input={{value}}"], values: [{ slot: "{{value}}", domain: "artifact-path", substitution_location: 0 }] },
645
587
  })),
646
588
  /submission is not allowed on the self-contained/,
647
589
  );
648
590
 
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
-
591
+ const validator = roleInput("provider_targeted_validator", "review.capture-validation", "https://gentle-ai.dev/schema/review/validator/v1");
654
592
  assert.throws(
655
- () => decodeReviewNextTransitionV3(roleInput("provider_targeted_validator", "review.capture-validation", "https://gentle-ai.dev/schema/review/validator/v1")),
593
+ () => decodeRoleTransition(validator),
656
594
  /validation_request is required/,
657
595
  );
596
+
658
597
  assert.throws(
659
- () => decodeReviewNextTransitionV3(roleInput("provider_targeted_validator", "review.capture-validation", "https://gentle-ai.dev/schema/review/refuter/v1", { validation_request: validationRequest })),
598
+ () => decodeRoleTransition(roleInput("provider_targeted_validator", "review.capture-validation", "https://gentle-ai.dev/schema/review/refuter/v1")),
660
599
  /schema must be https:\/\/gentle-ai\.dev\/schema\/review\/validator\/v1/,
661
600
  );
662
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
+
724
+ test("next_transition.execute.binding stays open, as its schema declares no properties", () => {
725
+ const withContext = {
726
+ kind: "execute" as const,
727
+ reason_code: "fresh_target_ready",
728
+ execute: {
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)}` },
733
+ },
734
+ };
735
+ assert.doesNotThrow(() => decodeReviewNextTransitionV3(withContext));
736
+ });