gentle-pi 3.2.1 → 3.4.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 (108) hide show
  1. package/README.md +63 -59
  2. package/assets/orchestrator-delegation.md +1 -1
  3. package/docs/assets/brand/gentle-shell-banner.gif +0 -0
  4. package/docs/assets/diagrams/odd-workflow.svg +74 -0
  5. package/docs/assets/features/agents-view.png +0 -0
  6. package/docs/assets/features/changes-view.png +0 -0
  7. package/docs/assets/features/command-palette.png +0 -0
  8. package/docs/assets/features/profiles-routing.png +0 -0
  9. package/docs/gentle-shell.md +52 -15
  10. package/docs/readme-reference.md +67 -7
  11. package/docs/review-integration.md +22 -17
  12. package/extensions/ask-user-question.ts +210 -0
  13. package/extensions/gentle-agents.ts +93 -18
  14. package/extensions/gentle-ai.ts +180 -37
  15. package/extensions/gentle-shell.ts +476 -39
  16. package/extensions/gentle-todo.ts +19 -1
  17. package/extensions/quiet-tools.ts +28 -5
  18. package/extensions/startup-banner.ts +25 -10
  19. package/lib/agents-view.ts +41 -14
  20. package/lib/agents-widget.ts +84 -13
  21. package/lib/animation-policy.ts +52 -0
  22. package/lib/background-cache-warming.ts +38 -0
  23. package/lib/command-palette-catalog.ts +2 -0
  24. package/lib/double-esc-cancel-policy.ts +138 -0
  25. package/lib/inprocess-reviewer.ts +297 -0
  26. package/lib/native-review-cli.ts +50 -10
  27. package/lib/odd-runtime-delegation-gate.ts +88 -0
  28. package/lib/questionnaire/questionnaire-view.ts +603 -0
  29. package/lib/questionnaire/schema.ts +82 -0
  30. package/lib/questionnaire/validate.ts +141 -0
  31. package/lib/review-candidate-view-owner.ts +20 -5
  32. package/lib/review-candidate-view.ts +9 -2
  33. package/lib/review-host-relay.ts +256 -171
  34. package/lib/review-integration-v2.ts +114 -27
  35. package/lib/shell-bar.ts +163 -75
  36. package/lib/shell-card.ts +19 -9
  37. package/lib/shell-changes-view.ts +43 -5
  38. package/lib/shell-changes.ts +92 -5
  39. package/lib/shell-hover.ts +39 -0
  40. package/lib/shell-prompt.ts +10 -1
  41. package/lib/shell-sidebar-layout.ts +118 -16
  42. package/lib/shell-sidebar.ts +16 -0
  43. package/lib/shell-todo.ts +7 -1
  44. package/lib/shell-usage-view.ts +103 -12
  45. package/lib/shell-usage.ts +120 -6
  46. package/package.json +1 -1
  47. package/runtime/native-review-cli.mjs +49 -9
  48. package/runtime/review-integration-v2.mjs +114 -27
  49. package/scripts/gentle-ai-installer.mjs +10 -10
  50. package/scripts/maintainer/provider-relay-matrix.mjs +118 -47
  51. package/scripts/verify-package-files.mjs +3 -4
  52. package/tests/agents-grouping.test.ts +75 -18
  53. package/tests/agents-view.test.ts +28 -18
  54. package/tests/agents-widget.test.ts +100 -12
  55. package/tests/animation-policy.test.ts +42 -0
  56. package/tests/ask-user-question.test.ts +435 -0
  57. package/tests/background-cache-warming.test.ts +60 -0
  58. package/tests/background-subagents.test.ts +68 -0
  59. package/tests/command-palette.test.ts +10 -0
  60. package/tests/devbinary/pi-host-relay.devtest.ts +176 -138
  61. package/tests/double-esc-cancel-policy.test.ts +194 -0
  62. package/tests/gentle-agents.test.ts +599 -7
  63. package/tests/gentle-ai-binary.test.ts +1 -1
  64. package/tests/gentle-ai-installer.test.ts +47 -47
  65. package/tests/gentle-ai.test.ts +125 -9
  66. package/tests/gentle-shell.test.ts +1149 -24
  67. package/tests/gentle-todo.test.ts +17 -4
  68. package/tests/inprocess-reviewer.test.ts +460 -0
  69. package/tests/maintainer/provider-relay.maintest.ts +101 -143
  70. package/tests/native-review-capability-contract.test.ts +34 -1
  71. package/tests/native-review-parity.test.ts +19 -0
  72. package/tests/odd-runtime-delegation-gate.test.ts +212 -0
  73. package/tests/orchestrator-rdd-ownership.test.ts +3 -3
  74. package/tests/package-manifest.test.ts +6 -17
  75. package/tests/questionnaire-schema.test.ts +274 -0
  76. package/tests/questionnaire-view.test.ts +446 -0
  77. package/tests/rdd-status-line.test.ts +21 -4
  78. package/tests/review-candidate-owner-retry.test.ts +63 -0
  79. package/tests/review-candidate-view.test.ts +15 -0
  80. package/tests/review-controller-native-routing.test.ts +86 -0
  81. package/tests/review-host-relay-routing.test.ts +77 -0
  82. package/tests/review-host-relay.test.ts +297 -299
  83. package/tests/review-integration-v2-forward.test.ts +61 -0
  84. package/tests/review-integration-v2.test.ts +146 -1
  85. package/tests/review-ledger-contract.test.ts +1 -2
  86. package/tests/review-relay-transport-agent.test.ts +129 -26
  87. package/tests/review-risk-assessment.test.ts +104 -0
  88. package/tests/runtime-harness.mjs +11 -0
  89. package/tests/session-changes-shell.test.ts +27 -0
  90. package/tests/session-worktree-registry.test.ts +41 -0
  91. package/tests/shell-bar.test.ts +200 -124
  92. package/tests/shell-card.test.ts +5 -3
  93. package/tests/shell-changes-view.test.ts +47 -0
  94. package/tests/shell-changes.test.ts +177 -0
  95. package/tests/shell-hover.test.ts +19 -0
  96. package/tests/shell-prompt.test.ts +20 -0
  97. package/tests/shell-sidebar-fullscreen.test.ts +59 -0
  98. package/tests/shell-sidebar-layout.test.ts +301 -8
  99. package/tests/shell-sidebar.test.ts +25 -1
  100. package/tests/shell-todo.test.ts +36 -0
  101. package/tests/shell-usage-view.test.ts +120 -1
  102. package/tests/shell-usage.test.ts +129 -0
  103. package/tests/skill-collision-prefixes.test.ts +1 -1
  104. package/tests/startup-banner.test.ts +93 -2
  105. package/docs/assets/brand/gentle-pi-banner.png +0 -0
  106. package/lib/opaque-pi-reviewer-adapter.ts +0 -404
  107. package/skills/release/SKILL.md +0 -137
  108. package/tests/opaque-pi-reviewer-adapter.test.ts +0 -410
@@ -200,6 +200,22 @@ const CAPABILITIES_SCHEMA_IDENTITIES
200
200
  requiredMandatoryFeatures: REQUIRED_MANDATORY_FEATURES_V23,
201
201
  optionalFeatureFloor: 14,
202
202
  }),
203
+ // Ground-truthed against the published v3.4.0 binary: capabilities/v2.6
204
+ // advertises the same required surface as v2.5 (status/v6 is still
205
+ // advertised for compatibility) plus the status/v7 and status/v8 schemas,
206
+ // which are superset-checked additions, not requirements --
207
+ // decodeReviewStatusV3 already accepts v7/v8/v9 as additive extensions of
208
+ // v6, so the required-schema floor stays unchanged. `review assess` also
209
+ // gained review_due/review_due_reason/next_transition, which is unrelated
210
+ // to this negotiated capabilities surface. The v3.4.0 binary advertised
211
+ // 15 optional features, same as v2.5's binary (floor stays at 14, its
212
+ // established minimum).
213
+ "gentle-ai.review-integration.capabilities/v2.6": Object.freeze({
214
+ protocolMinor: 6,
215
+ requiredSchemas: Object.freeze([...REQUIRED_SCHEMAS_COMMON_V23, "gentle-ai.review-integration.capabilities/v2.6", "gentle-ai.review-integration.consent/v3", "gentle-ai.review-integration.start/v4", "gentle-ai.review-integration.status/v6", "gentle-ai.review-intended-untracked-selection/v1"]),
216
+ requiredMandatoryFeatures: REQUIRED_MANDATORY_FEATURES_V23,
217
+ optionalFeatureFloor: 14,
218
+ }),
203
219
  });
204
220
  const OPTIONAL_FEATURE_NAMES = Object.freeze([
205
221
  "base_ref_workspace_overlay",
@@ -324,6 +340,7 @@ const REQUIRED_MANDATORY_FEATURES = Object.freeze(FEATURE_NAMES.filter((name) =>
324
340
 
325
341
 
326
342
 
343
+
327
344
 
328
345
 
329
346
  const REPOSITORY_CONTEXT_OUTCOMES = ["applied", "pending", "blocked_conflict", "durability_limited"] ;
@@ -439,11 +456,21 @@ const REPOSITORY_CONTEXT_OUTCOMES = ["applied", "pending", "blocked_conflict", "
439
456
 
440
457
 
441
458
 
442
- // The two Go-owned non-lens provider role capture operations (gentle-pi#311
443
- // P4-roles; provider side gentle-ai#3264). Their collect inputs are
444
- // self-contained authority-advancing vectors: binding tokens plus
445
- // `--agent=pi --execute=true`, with NO submission descriptor. The known set
446
- // is closed — an unknown role capture operation is never executed.
459
+ // The two non-lens provider role capture operations (gentle-pi#311 P4-roles;
460
+ // provider side gentle-ai#3264). An older gentle-ai still renders each one as
461
+ // a self-contained authority-advancing vector: binding tokens plus
462
+ // `--agent=pi --execute=true`, with NO submission descriptor — executing the
463
+ // exact rendered tokens makes Go materialize the role prompt, run its own
464
+ // locked-down pi subprocess, and admit the verdict itself.
465
+ //
466
+ // gentle-ai's v9 contract makes both roles host-mediated exactly like a lens
467
+ // materialize slot instead (gentle-pi#311 P3): the collect input carries
468
+ // `--materialize=true` (never alongside `--execute`) plus a provider-owned
469
+ // `submission` descriptor, and the host completes the frozen prompt
470
+ // in-process and submits the result through that exact descriptor. The two
471
+ // wire forms are mutually exclusive on one input; see the gating in
472
+ // decodeCollectInput below. The known operation set is closed — an unknown
473
+ // role capture operation is never executed either way.
447
474
  export const REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION = {
448
475
  CAPTURE_REFUTER: "review.capture-refuter",
449
476
  CAPTURE_VALIDATION: "review.capture-validation",
@@ -1125,7 +1152,8 @@ export function decodeReviewArtifactSubjectV2(value )
1125
1152
  }
1126
1153
 
1127
1154
  function decodeChangedPathEntry(value , label ) {
1128
- const body = exactRecord(value, label, ["path", "status", "old_mode", "new_mode", "deleted", "type_changed", "mode_only", "intended_untracked"]);
1155
+ const body = exactRecord(value, label, ["path", "status", "old_mode", "new_mode", "deleted", "type_changed", "mode_only", "intended_untracked"], ["generated"]);
1156
+ if (body.generated !== undefined && body.generated !== true) throw new TypeError(`${label}.generated must be true when present`);
1129
1157
  return {
1130
1158
  path: nonempty(body.path, `${label}.path`),
1131
1159
  status: enumeration(body.status, ["A", "D", "M", "T"] , `${label}.status`),
@@ -1135,6 +1163,7 @@ function decodeChangedPathEntry(value , label )
1135
1163
  typeChanged: boolean(body.type_changed, `${label}.type_changed`),
1136
1164
  modeOnly: boolean(body.mode_only, `${label}.mode_only`),
1137
1165
  intendedUntracked: boolean(body.intended_untracked, `${label}.intended_untracked`),
1166
+ ...(body.generated === undefined ? {} : { generated: true }),
1138
1167
  };
1139
1168
  }
1140
1169
 
@@ -1452,11 +1481,18 @@ function decodeCaptureSubmission(value , label , v5 , v6
1452
1481
  // one-entry values array the host relay already consumes. A payload
1453
1482
  // carrying both wire forms at once matches no captured shape and falls
1454
1483
  // through to the legacy decoder, which rejects the unknown `value` key.
1484
+ //
1485
+ // gentle-ai's v9 contract renders the same singular-value shape for the
1486
+ // two host-mediated provider role operations (capture-refuter,
1487
+ // capture-validation): they carry a `schema` key exactly like
1488
+ // capture-result, since both bind one artifact-path-or-stdin value, never
1489
+ // the correction-plan's numeric bounds (gentle-pi#311 P3).
1455
1490
  if (v5 && typeof value === "object" && value !== null && "value" in value && !("values" in value)) {
1456
1491
  const submission = exactRecord(value, label, ["operation_token", "argument_tokens", "value"]);
1457
- const operationToken = enumeration(submission.operation_token, ["capture-result", "capture-correction-plan"] , `${label}.operation_token`);
1492
+ const operationToken = enumeration(submission.operation_token, ["capture-result", "capture-correction-plan", "capture-refuter", "capture-validation"] , `${label}.operation_token`);
1458
1493
  const argumentTokens = stringArray(submission.argument_tokens, `${label}.argument_tokens`, { minimum: 1 });
1459
- const row = operationToken === "capture-result"
1494
+ const bindsArtifactSchema = operationToken === "capture-result" || operationToken === "capture-refuter" || operationToken === "capture-validation";
1495
+ const row = bindsArtifactSchema
1460
1496
  ? exactRecord(submission.value, `${label}.value`, ["slot", "domain", "schema", "substitution_location"])
1461
1497
  : exactRecord(submission.value, `${label}.value`, ["slot", "domain", "minimum", "maximum", "substitution_location"]);
1462
1498
  return {
@@ -1465,7 +1501,11 @@ function decodeCaptureSubmission(value , label , v5 , v6
1465
1501
  values: [{
1466
1502
  slot: operationToken === "capture-result"
1467
1503
  ? enumeration(row.slot, ["reviewer_result"] , `${label}.value.slot`)
1468
- : enumeration(row.slot, ["correction_lines"] , `${label}.value.slot`),
1504
+ : operationToken === "capture-correction-plan"
1505
+ ? enumeration(row.slot, ["correction_lines"] , `${label}.value.slot`)
1506
+ : operationToken === "capture-refuter"
1507
+ ? enumeration(row.slot, ["provider_refuter"] , `${label}.value.slot`)
1508
+ : enumeration(row.slot, ["provider_targeted_validator"] , `${label}.value.slot`),
1469
1509
  domain: nonempty(row.domain, `${label}.value.domain`),
1470
1510
  ...(row.schema === undefined ? {} : { schema: nonempty(row.schema, `${label}.value.schema`) }),
1471
1511
  ...(row.minimum === undefined ? {} : { minimum: integer(row.minimum, `${label}.value.minimum`, 1, 200) }),
@@ -1606,7 +1646,7 @@ const CORRECTION_REQUEST_REASON_CODES = Object.freeze(["correction_plan_required
1606
1646
  // v5 capture operations that must carry a submission descriptor.
1607
1647
  const V5_SUBMISSION_CAPTURE_OPERATIONS = Object.freeze(["review.capture-correction-plan"] );
1608
1648
 
1609
- function decodeCollectInput(value , label , v5 , v6 ) {
1649
+ function decodeCollectInput(value , label , v5 , v6 , v9 ) {
1610
1650
  const input = exactRecord(value, label, ["name", "schema", "capture_operation", "arguments"], ["artifact_subject", "base_tree", "candidate_tree", "changed_path_manifest", "submission", ...(v5 ? ["provider_task", "validation_request"] : [])]);
1611
1651
  const name = text(input.name, `${label}.name`, { minimum: 1, pattern: /^[a-z0-9_]+$/ });
1612
1652
  const schema = nonempty(input.schema, `${label}.schema`);
@@ -1657,13 +1697,18 @@ function decodeCollectInput(value , label , v5 , v6
1657
1697
  sha256(argumentsList[5] .value, `${label}.arguments[5].value`);
1658
1698
  }
1659
1699
 
1660
- // gentle-pi#311 P4-roles: the two Go-owned non-lens provider role capture
1661
- // operations render SELF-CONTAINED authority-advancing vectors. Executing
1700
+ // gentle-pi#311 P4-roles / P3: the two non-lens provider role capture
1701
+ // operations render either wire form on one input, never both at once.
1702
+ // An older gentle-ai renders a SELF-CONTAINED authority-advancing vector
1703
+ // (binding tokens + --agent=pi --execute=true, no submission): executing
1662
1704
  // the exact rendered tokens makes Go materialize the role prompt, run its
1663
- // own locked-down pi subprocess, and admit the raw verdict — so a
1664
- // submission descriptor (the host-mediated completing form) on one of
1665
- // these inputs would hand the caller a way to author the verdict and is
1666
- // rejected as a provider contract violation.
1705
+ // own locked-down pi subprocess, and admit the raw verdict. gentle-ai's
1706
+ // v9 contract instead renders a HOST-MEDIATED form: the same binding
1707
+ // tokens plus --materialize=true (never --execute) and a provider-owned
1708
+ // submission descriptor, exactly like a lens capture-result materialize
1709
+ // slot — the host completes the frozen prompt in-process and submits
1710
+ // through that descriptor.
1711
+ const isRoleCaptureOperation = (REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS ).includes(captureOperation);
1667
1712
  if (captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_REFUTER && schema !== "https://gentle-ai.dev/schema/review/refuter/v1") {
1668
1713
  throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/refuter/v1`);
1669
1714
  }
@@ -1694,8 +1739,30 @@ function decodeCollectInput(value , label , v5 , v6
1694
1739
  if (providerArgument("target") !== validationRequest.correctionTargetIdentity) throw new TypeError(`${label}.arguments target must bind validation_request.correction_target_identity`);
1695
1740
  if (providerArgument("request-hash") !== validationRequest.requestHash) throw new TypeError(`${label}.arguments request-hash must bind validation_request.request_hash`);
1696
1741
  }
1697
- if (input.submission !== undefined && (REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS ).includes(captureOperation)) {
1698
- throw new TypeError(`${label}.submission is not allowed on the self-contained ${captureOperation} vector`);
1742
+ if (isRoleCaptureOperation && input.submission !== undefined) {
1743
+ // A submission descriptor only ever rides the v9 host-mediated form
1744
+ // (gentle-ai's v9 provider contract; the sibling branch ahead of the
1745
+ // currently-released v8). A v8-or-earlier provider (status/v3..v8) never
1746
+ // renders one, even if it happened to carry a submission-shaped payload,
1747
+ // so this is gated on v9 specifically — not on v5, which v8 already
1748
+ // satisfies. Within a v9 payload the discriminator is exactly the
1749
+ // materialize slot's --materialize=true with no --execute, the same one
1750
+ // a lens capture-result materialize slot already uses. Mixing a
1751
+ // submission with the old self-contained --execute=true vector, or with
1752
+ // neither flag, is a malformed vector.
1753
+ if (!v9) {
1754
+ throw new TypeError(`${label}.submission requires the v9 provider contract for the host-mediated ${captureOperation} form`);
1755
+ }
1756
+ const argumentNamed = (argumentName ) => {
1757
+ const matches = argumentsList.filter((argument) => argument.name === argumentName);
1758
+ return matches.length === 1 ? matches[0] .value : undefined;
1759
+ };
1760
+ if (argumentNamed("materialize") !== "true") {
1761
+ throw new TypeError(`${label}.submission requires --materialize=true on the host-mediated ${captureOperation} form`);
1762
+ }
1763
+ if (argumentNamed("execute") !== undefined) {
1764
+ throw new TypeError(`${label} must not carry --execute alongside a submission descriptor on ${captureOperation}`);
1765
+ }
1699
1766
  }
1700
1767
 
1701
1768
  if (captureOperation === "review.capture-result") {
@@ -1712,7 +1779,12 @@ function decodeCollectInput(value , label , v5 , v6
1712
1779
  const submissionOperations = v5
1713
1780
  ? ["review.capture-result", "review.capture-correction-plan", ...(v6 ? ["external.select_intended_untracked"] : [])]
1714
1781
  : ["review.capture-result"];
1715
- if (input.submission !== undefined && !submissionOperations.includes(captureOperation)) {
1782
+ // Role operations are gated above by the materialize/execute discriminator
1783
+ // instead of this allowlist: an older gentle-ai's self-contained vector
1784
+ // legitimately carries no submission regardless of v5/v6, so they cannot
1785
+ // simply join the list the way every other submission-carrying operation
1786
+ // does.
1787
+ if (input.submission !== undefined && !isRoleCaptureOperation && !submissionOperations.includes(captureOperation)) {
1716
1788
  throw new TypeError(v5 ? `${label}.submission is only valid for ${submissionOperations.join(", ")}` : `${label}.submission is only valid for review.capture-result`);
1717
1789
  }
1718
1790
  if (intendedUntracked && input.submission === undefined) throw new TypeError(`${label}.submission is required for external.select_intended_untracked`);
@@ -1750,8 +1822,14 @@ export function decodeReviewManagedAssetsContinuationV1(value , label
1750
1822
  return { operation, command, ...(agent === undefined ? {} : { agent }), ...(staleAssets === undefined ? {} : { staleAssets }) };
1751
1823
  }
1752
1824
 
1753
- export function decodeReviewNextTransitionV3(value , options = {}) {
1754
- const v6 = options.v6 === true;
1825
+ export function decodeReviewNextTransitionV3(value , options = {}) {
1826
+ // v9 (gentle-ai's next contract, ahead of the currently-released v8) is
1827
+ // the only rung this sub-decoder distinguishes beyond v6: v7 and v8 add
1828
+ // nothing to next_transition/collect-input decoding (v8 only extended the
1829
+ // reviewer-result transition for OpenCode provider tasks at the top
1830
+ // status level), so they decode on the exact v6 surface here.
1831
+ const v9 = options.v9 === true;
1832
+ const v6 = options.v6 === true || v9;
1755
1833
  const v5 = options.v5 === true || v6;
1756
1834
  const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : []), "continuation", "unachievable_lens_slots"]);
1757
1835
  const kind = enumeration(transition.kind, ["execute", "collect", "stop"] , "next_transition.kind");
@@ -1813,7 +1891,7 @@ export function decodeReviewNextTransitionV3(value , options
1813
1891
  }
1814
1892
  if (kind === "collect") {
1815
1893
  const collect = exactRecord(transition.collect, "next_transition.collect", ["inputs"]);
1816
- const inputs = array(collect.inputs, "next_transition.collect.inputs", (entry, label) => decodeCollectInput(entry, label, v5, v6), { minimum: 1 });
1894
+ const inputs = array(collect.inputs, "next_transition.collect.inputs", (entry, label) => decodeCollectInput(entry, label, v5, v6, v9), { minimum: 1 });
1817
1895
  if (transition.execute !== undefined) throw new TypeError("next_transition.execute is incompatible with collect");
1818
1896
  return { kind, reasonCode, collect: { inputs }, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
1819
1897
  }
@@ -1926,16 +2004,25 @@ export function decodeReviewStatusV3(value ) {
1926
2004
  // surfaces. status/v6 adds the intended-untracked selection; status/v7
1927
2005
  // (gentle-ai v2.6.0, advertised through capabilities/v2.5 alongside v6)
1928
2006
  // adds optional `eligible_untracked_inventory` and escalation metadata,
1929
- // so it is decoded on the v6 surface. v3 keeps rejecting every v5/v6/v7-only
1930
- // field.
2007
+ // so it is decoded on the v6 surface. status/v8 (gentle-ai main, PR #4765;
2008
+ // the current released contract) only extended the reviewer-result
2009
+ // transition for OpenCode provider tasks -- no new top-level key -- so it
2010
+ // decodes on the exact v7 surface too. status/v9 (the sibling gentle-ai
2011
+ // branch's contract, ahead of the released v8) adds nothing at this top
2012
+ // level either: its one addition is the host-mediated role `submission`
2013
+ // on a next_transition.collect input (gentle-pi#311 P3), gated on v9 in
2014
+ // decodeCollectInput, not here. v3 keeps rejecting every
2015
+ // v5/v6/v7/v8/v9-only field.
1931
2016
  const schema = typeof value === "object" && value !== null ? (value ).schema : undefined;
1932
- const v7 = schema === "gentle-ai.review-integration.status/v7";
2017
+ const v9 = schema === "gentle-ai.review-integration.status/v9";
2018
+ const v8 = v9 || schema === "gentle-ai.review-integration.status/v8";
2019
+ const v7 = v8 || schema === "gentle-ai.review-integration.status/v7";
1933
2020
  const v6 = v7 || schema === "gentle-ai.review-integration.status/v6";
1934
2021
  const v5 = v6 || schema === "gentle-ai.review-integration.status/v5";
1935
2022
  const body = exactRecord(value, "status", [
1936
2023
  "schema", "contract", "operation", "applicability", "action", "replayability", "target_identity", "projection", "repair", "candidates",
1937
2024
  ], ["authority", "frozen", "action_disposition", "eligibility", "next_transition", "authority_target_identity", ...(v5 ? ["receipt", "forecast", "repository_context", "validation_request"] : []), ...(v7 ? ["eligible_untracked_inventory", "escalation"] : [])]);
1938
- requireIdentity(body, v7 ? "gentle-ai.review-integration.status/v7" : v6 ? "gentle-ai.review-integration.status/v6" : v5 ? "gentle-ai.review-integration.status/v5" : "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
2025
+ requireIdentity(body, v9 ? "gentle-ai.review-integration.status/v9" : v8 ? "gentle-ai.review-integration.status/v8" : v7 ? "gentle-ai.review-integration.status/v7" : v6 ? "gentle-ai.review-integration.status/v6" : v5 ? "gentle-ai.review-integration.status/v5" : "gentle-ai.review-integration.status/v3", REVIEW_INTEGRATION_OPERATION.STATUS);
1939
2026
 
1940
2027
  const applicability = enumeration(body.applicability, ["current_target", "unrelated", "ambiguous", "corrupted"] , "status.applicability");
1941
2028
  let receipt ;
@@ -1983,7 +2070,7 @@ export function decodeReviewStatusV3(value ) {
1983
2070
  if (action === "recover" && actionDisposition === undefined) throw new TypeError("recover status requires action_disposition");
1984
2071
  if (action !== "recover" && actionDisposition !== undefined) throw new TypeError("status.action_disposition is only valid for the recover action");
1985
2072
  if (body.eligibility !== undefined) decodeEligibility(body.eligibility, "status.eligibility");
1986
- const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition, { v5, v6 });
2073
+ const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition, { v5, v6, v9 });
1987
2074
  const validationRequest = v5 && body.validation_request !== undefined
1988
2075
  ? decodeReviewTargetedValidationRequestV1(body.validation_request, "status.validation_request")
1989
2076
  : undefined;
@@ -36,7 +36,7 @@ const WINDOWS_SYSTEM_ROOT = "C:\\Windows";
36
36
  // version check below) derives from this constant instead of repeating the
37
37
  // literal, so a pin bump cannot leave a stale copy behind. See
38
38
  // scripts/install-gentle-ai.mjs for the incident that motivated this.
39
- export const INSTALLER_VERSION = "3.2.1";
39
+ export const INSTALLER_VERSION = "3.5.0";
40
40
  export const RELEASE_BASE_URL = `https://github.com/Gentleman-Programming/gentle-ai/releases/download/v${INSTALLER_VERSION}/`;
41
41
  export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
42
42
  SIGNED_RELEASE_ASSET: "signed-release-asset",
@@ -45,10 +45,10 @@ export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
45
45
  export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH = "github.com/gentleman-programming/gentle-ai/v3/cmd/gentle-ai";
46
46
  export const GENTLE_AI_WINDOWS_SOURCE_MODULE = "github.com/gentleman-programming/gentle-ai/v3";
47
47
  export const GENTLE_AI_WINDOWS_SOURCE_TAG = `v${INSTALLER_VERSION}`;
48
- // `go mod download -json github.com/gentleman-programming/gentle-ai/v3@v3.2.1`
48
+ // `go mod download -json github.com/gentleman-programming/gentle-ai/v3@v3.5.0`
49
49
  // with GOSUMDB=sum.golang.org reports this exact module SumDB checksum, and the
50
- // tag resolves to commit e7729359, the published v3.2.1 release head.
51
- export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:0QFo0ERv8/3lgTepEG0E/P7yD9qXXCx2M7ppQviVHMo=";
50
+ // tag resolves to commit 0b335e43, the published v3.5.0 release head.
51
+ export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:3y+Nb7CtgGB3ne3bSNxkzV6j19etkOnGDXIiuv7tMp8=";
52
52
  export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE = `${GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH}@${GENTLE_AI_WINDOWS_SOURCE_TAG}`;
53
53
  export const GENTLE_AI_WINDOWS_MINIMUM_GO_VERSION = "1.25.10";
54
54
  export const GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE_CODE = "GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE";
@@ -67,7 +67,7 @@ export class GentleAiInstallerError extends Error {
67
67
  // Sentinel used while a re-pinned gentle-ai release is not yet published. A
68
68
  // sentinel digest can never match a real SHA-256, so installation fails closed,
69
69
  // and verify-package-files.mjs refuses to pack/publish while any digest below
70
- // still holds it. The v3.2.1 digests are pinned from the published release:
70
+ // still holds it. The v3.5.0 digests are pinned from the published release:
71
71
  // archive sha256 values verified against the minisign-signed checksums.txt and
72
72
  // freshly computed hashes; binary sha256 values computed from the extracted
73
73
  // executables.
@@ -109,15 +109,15 @@ async function downloadPinnedGentleAiAsset(asset, destination, options) {
109
109
  }
110
110
 
111
111
  // Windows is absent from signed release archives on purpose. gentle-ai stopped
112
- // distributing unsigned Windows builds in c4b764d0, so v3.2.1 publishes signed
112
+ // distributing unsigned Windows builds in c4b764d0, so v3.5.0 publishes signed
113
113
  // Darwin/Linux archives only. Windows x64/arm64 uses the separately verified
114
114
  // exact-tag Go SumDB source-build path below; restore archive rows only when
115
115
  // upstream ships signed Windows assets.
116
116
  export const GENTLE_AI_RELEASE_ASSETS = Object.freeze({
117
- "darwin/amd64": asset("gentle-ai_3.2.1_darwin_amd64.tar.gz", "1696a4852435920ebae351a5173adead6da17db6762359dcb12dc3971d983c3f", "ed7d18b7446c63f265a827a736ed4465ec562cc3c07c470743d3740fb4d2f4a9", "gentle-ai"),
118
- "darwin/arm64": asset("gentle-ai_3.2.1_darwin_arm64.tar.gz", "e3a991a03c6fb6373f6a254b808c3fabf36330634d93704c9d9ba365467fc6f8", "2ae66cfa11f8ab93ea46c20e79bd191dedde40a4898f0b312330a0790f802ca3", "gentle-ai"),
119
- "linux/amd64": asset("gentle-ai_3.2.1_linux_amd64.tar.gz", "e2e3171377c040c27a93ae66b4adf824626f23cacd398b3b652af0d87ff65807", "3464525775b60cad6358a589c298a2d4098466bc0a5ae5f0d1e578ab10134659", "gentle-ai"),
120
- "linux/arm64": asset("gentle-ai_3.2.1_linux_arm64.tar.gz", "342c7fc60cf062730c8f593f09269f75485b3b1fa2907b2b188eafded1a0e222", "d2c4d8ebfdc681ceece254d0d12cd04dd7af3b435c945e42623e3475981c2f8f", "gentle-ai"),
117
+ "darwin/amd64": asset("gentle-ai_3.5.0_darwin_amd64.tar.gz", "483b8363cc2b717d224ee7a15c834ed518854c31a1d79d84b6ea0551fd7905cf", "06e5ce2936f0b0579fa382b5cd59cf42c23b07404b162d9ff9a036dd107093a4", "gentle-ai"),
118
+ "darwin/arm64": asset("gentle-ai_3.5.0_darwin_arm64.tar.gz", "d7a5c233ce23e8fc2ce917ed3e6a2e643655e6e8322fddb3c522324d4cf8ae52", "ec7757fe13f6cd5c9dffa6466d75c5cd68992eac44e684d330c838282c5e2e7e", "gentle-ai"),
119
+ "linux/amd64": asset("gentle-ai_3.5.0_linux_amd64.tar.gz", "3ea016622981f5e4f4a80632fb9c4673149c98f4e5cecc4b979b41bda758ba4a", "7a4202f75c8b90056f0ecd8fceaaef9efe407abc6f98bb42a3ee1e5ad69eb197", "gentle-ai"),
120
+ "linux/arm64": asset("gentle-ai_3.5.0_linux_arm64.tar.gz", "aca1997b3ae8439c17883cde39c3ba577ae38352ad6be1d1c4b2c6bc428c4505", "4d81f356f6e221bc5730a280bae049df8315232a2470089993200577e36e97cc", "gentle-ai"),
121
121
  });
122
122
 
123
123
  // A pinned asset is either a signed archive or, for a prerelease pin only,
@@ -9,10 +9,11 @@
9
9
  // `pnpm test` never imports it; `pnpm run test:maintainer` runs the tests;
10
10
  // the CLI is the entry point the separate verifier uses for the real organic
11
11
  // positive journey after a user-visible forecast.
12
+ import { randomUUID } from "node:crypto";
12
13
  import { spawn, spawnSync } from "node:child_process";
13
- import { chmodSync, existsSync, mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
14
- import { tmpdir } from "node:os";
15
- import { delimiter, isAbsolute, join, resolve } from "node:path";
14
+ import { existsSync, readFileSync, statSync } from "node:fs";
15
+ import { delimiter, isAbsolute, resolve } from "node:path";
16
+ import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai/compat";
16
17
  import { REVIEW_HOST_RELAY_FAILURE, ReviewHostRelayError, classifyReviewHostRelayRefusal, resolveReviewHostRelaySubmission, runReviewHostRelaySlot } from "../../lib/review-host-relay.ts";
17
18
  import { GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "../../lib/review-relay-contract.ts";
18
19
  export const DESCRIPTOR_SCHEMA = "gentle-pi.maintainer.provider-relay-descriptor/v1";
@@ -88,13 +89,17 @@ const exactKeys = (obj, allowed, label) => {
88
89
  // the completing form is provably bindable before any process launches.
89
90
  export function validateDescriptor(value) {
90
91
  if (!isObj(value)) fail("descriptor must be a JSON object");
91
- exactKeys(value, new Set(["schema", "gentleAiExecutable", "piExecutable", "cases"]), "descriptor");
92
+ exactKeys(value, new Set(["schema", "gentleAiExecutable", "reviewerSelection", "cases"]), "descriptor");
92
93
  if (value.schema !== DESCRIPTOR_SCHEMA) fail(`descriptor.schema must be exactly "${DESCRIPTOR_SCHEMA}"`);
93
94
  if (!isStr(value.gentleAiExecutable) || !isAbsolute(value.gentleAiExecutable)) fail("descriptor.gentleAiExecutable must be a non-empty absolute path; the runner never re-resolves the production binary");
94
- if (!isStr(value.piExecutable)) fail("descriptor.piExecutable must be a non-empty string");
95
+ // gentle-pi#311 P4: the positive-lens journey used to need a declared real
96
+ // `pi` binary; lens captures now run in-process, so it instead needs a
97
+ // "provider/id" reviewer selection the armed run resolves through a
98
+ // scripted-but-real completion (never a fake pi child).
99
+ if (!isStr(value.reviewerSelection)) fail("descriptor.reviewerSelection must be a non-empty string");
95
100
  if (!Array.isArray(value.cases) || value.cases.length === 0) fail("descriptor.cases must be a non-empty array");
96
101
  const seen = new Set();
97
- return { schema: value.schema, gentleAiExecutable: value.gentleAiExecutable, piExecutable: value.piExecutable, cases: value.cases.map((entry, index) => {
102
+ return { schema: value.schema, gentleAiExecutable: value.gentleAiExecutable, reviewerSelection: value.reviewerSelection, cases: value.cases.map((entry, index) => {
98
103
  if (!isObj(entry)) fail(`descriptor.cases[${index}] must be an object`);
99
104
  if (!isStr(entry.name)) fail(`descriptor.cases[${index}].name must be a non-empty string`);
100
105
  if (seen.has(entry.name)) fail(`descriptor.cases[${index}].name "${entry.name}" is duplicated`);
@@ -205,13 +210,77 @@ export function resolveDeclaredExecutable(executable) {
205
210
  return null;
206
211
  }
207
212
  const missingReason = (executable, label) => `${label} "${executable}" is not an existing executable; arm the descriptor with a real binary, or set GENTLE_PI_REQUIRE_MAINTAINER=1 to fail instead of block.`;
208
- // A guaranteed-nonexistent pi path inside a fresh private directory. mkdtemp
209
- // picks an unpredictable name and 0700 keeps it ours, so nothing can race a
210
- // file into the slot between the mkdtemp and the spawn.
211
- function unlaunchablePi() {
212
- const directory = mkdtempSync(join(tmpdir(), "gentle-pi-maintainer-no-pi-"));
213
- chmodSync(directory, 0o700);
214
- return { directory, executable: join(directory, "pi-must-never-launch") };
213
+ // A reviewer registry whose `find` always misses. Mirrors the pre-in-process
214
+ // "unlaunchable pi" trick this replaced: the maintainer-declared `kind` is
215
+ // not evidence that the declared gentle-ai binary is actually incapable, so
216
+ // a `relay-unavailable` negative control against a mis-declared CAPABLE
217
+ // binary must still never complete a reviewer or submit anything. Resolving
218
+ // nothing guarantees a typed MODEL_NOT_FOUND refusal before any completion
219
+ // or submission, so the control cannot become a hidden real mutation even
220
+ // against a binary that turns out to be capable (gentle-pi#311 P4).
221
+ function unresolvableReviewerRegistry() {
222
+ return {
223
+ find: () => undefined,
224
+ getApiKeyAndHeaders: async () => ({ ok: false, error: "the relay-unavailable negative control never resolves a reviewer" }),
225
+ };
226
+ }
227
+ // The armed, organic positive-lens journey needs the real gentle-ai binary's
228
+ // materialize/submit protocol exercised end-to-end, but this harness cannot
229
+ // assume a maintainer's real provider credentials. Instead of a fake pi
230
+ // child, this registers pi-ai's own faux provider -- the SAME api-registry
231
+ // the real `completeSimple` dispatches through in production -- so the real
232
+ // `runInProcessReviewer` -> real `completeSimple` path runs with no network.
233
+ // The scripted response reads the frozen prompt's `GENTLE_AI_REVIEW_BINDING`
234
+ // line for the subject_hash and its `GENTLE_AI_REVIEW_CONTEXT` line for the
235
+ // changed-path manifest, and answers with every field the reviewer result
236
+ // contract requires -- subject_hash, a completed inspection over exactly the
237
+ // frozen paths, findings, evidence -- so the real Go admission accepts it
238
+ // (gentle-pi#311 P4). Not unregistered: each
239
+ // call uses a fresh random api id, and this runs at most once per short-lived
240
+ // process (the CLI's single `main()` or one test), so a leaked entry in the
241
+ // process-global api-registry is harmless.
242
+ // Reads the one-line JSON that follows a frozen prompt marker (the binding
243
+ // and context lines Go writes at the top of every reviewer task). Returns
244
+ // undefined when the marker is absent or its JSON is malformed, so the faux
245
+ // reviewer answers with an incomplete result that real admission refuses
246
+ // instead of throwing inside the provider and masking the contract drift.
247
+ function frozenPromptMarkerJson(promptText, marker) {
248
+ const prefix = `${marker} `;
249
+ for (const line of promptText.split("\n")) {
250
+ if (!line.startsWith(prefix)) continue;
251
+ try { return JSON.parse(line.slice(prefix.length)); } catch { return undefined; }
252
+ }
253
+ return undefined;
254
+ }
255
+ function armedFauxReviewerRegistry(reviewerSelection) {
256
+ const separatorIndex = reviewerSelection.indexOf("/");
257
+ if (separatorIndex <= 0 || separatorIndex === reviewerSelection.length - 1) {
258
+ throw new DescriptorValidationError(`descriptor.reviewerSelection "${reviewerSelection}" must have the "provider/id" shape`);
259
+ }
260
+ const provider = reviewerSelection.slice(0, separatorIndex);
261
+ const modelId = reviewerSelection.slice(separatorIndex + 1);
262
+ const api = `gentle-pi-maintainer-faux-reviewer-${randomUUID()}`;
263
+ const faux = registerFauxProvider({ api, provider, models: [{ id: modelId }] });
264
+ faux.setResponses([(context) => {
265
+ const content = context.messages[0]?.content;
266
+ const textPart = Array.isArray(content) ? content.find((part) => part.type === "text") : undefined;
267
+ const promptText = textPart?.text ?? "";
268
+ const binding = frozenPromptMarkerJson(promptText, "GENTLE_AI_REVIEW_BINDING");
269
+ const frozenContext = frozenPromptMarkerJson(promptText, "GENTLE_AI_REVIEW_CONTEXT");
270
+ const manifest = Array.isArray(frozenContext?.changed_path_manifest) ? frozenContext.changed_path_manifest : [];
271
+ const paths = manifest.map((entry) => entry?.path).filter((path) => typeof path === "string" && path.length > 0);
272
+ return fauxAssistantMessage(JSON.stringify({
273
+ subject_hash: binding?.subject_hash,
274
+ inspection: { status: "completed", paths },
275
+ findings: [],
276
+ evidence: ["armed maintainer faux reviewer inspected every frozen candidate path"],
277
+ }));
278
+ }]);
279
+ const model = faux.getModel();
280
+ return {
281
+ find: () => model,
282
+ getApiKeyAndHeaders: async () => ({ ok: true, apiKey: "faux-key" }),
283
+ };
215
284
  }
216
285
  function terminateRoleProcessTree(child) {
217
286
  if (child.pid === undefined) return false;
@@ -317,39 +386,41 @@ export async function runMatrix(descriptor, options = {}) {
317
386
  }
318
387
  continue;
319
388
  }
320
- // The positive lens needs a real pi AND an explicit arm; the negative
321
- // control never launches pi (fails closed at materialize). The precheck
322
- // resolves the declared Pi executable ONCE and reuses that exact
323
- // concrete path for launch; a bare declaration is never re-resolved
324
- // between precheck and spawn, so the relay launches exactly the
325
- // executable the harness checked (issue #324).
326
- let piPath = null;
327
- if (caseEntry.kind === "positive-lens") {
328
- piPath = resolveDeclaredExecutable(descriptor.piExecutable);
329
- if (piPath === null) {
330
- verdicts.push({ name: caseEntry.name, kind: caseEntry.kind, verdict: "blocked", reason: missingReason(descriptor.piExecutable, "piExecutable"), command: POSITIVE_JOURNEY_COMMAND });
331
- continue;
332
- }
333
- if (!positiveArmed) {
334
- verdicts.push({ name: caseEntry.name, kind: caseEntry.kind, verdict: "blocked", reason: `positive-lens case is not explicitly armed; set ${ARM_POSITIVE_ENV}=1 with a real capable gentle-ai binary, a real pi, and a real review session (real binding tokens from \`gentle-ai review status --next-transition\`) to run the organic journey. The runner never synthesizes a provider result.`, command: POSITIVE_JOURNEY_COMMAND });
335
- continue;
336
- }
389
+ // The positive lens needs an explicit arm; the negative control never
390
+ // resolves a reviewer (fails closed at the reviewer stage or earlier).
391
+ if (caseEntry.kind === "positive-lens" && !positiveArmed) {
392
+ verdicts.push({ name: caseEntry.name, kind: caseEntry.kind, verdict: "blocked", reason: `positive-lens case is not explicitly armed; set ${ARM_POSITIVE_ENV}=1 with a real capable gentle-ai binary and a real review session (real binding tokens from \`gentle-ai review status --next-transition\`) to run the organic journey. The runner never synthesizes a provider result.`, command: POSITIVE_JOURNEY_COMMAND });
393
+ continue;
337
394
  }
338
- // `relay-unavailable` is a NEGATIVE CONTROL: it must never launch pi and
339
- // never submit. The maintainer-declared `kind` is not evidence that the
340
- // declared binary is actually incapable, so the arm gate cannot key off
341
- // the declaration alone. Pointed at a mis-declared CAPABLE binary, a
342
- // declaration-only gate materializes, runs a REAL pi model, and executes
343
- // a REAL `capture-result --input=...` submission, and only then reports
344
- // `fail` — the mutation already happened. So give the negative control a
345
- // pi that cannot exist: an incapable binary still classifies
346
- // relay-unavailable at materialize (the control stays genuine, since it
347
- // never reached pi anyway), while a capable one fails closed at the pi
348
- // stage as kind=pi-launch-failed stage=pi mutationOutcome=none, with
349
- // zero pi launch and zero submission.
350
- const negativeControl = caseEntry.kind === "relay-unavailable" ? unlaunchablePi() : null;
351
- const request = { captureArgumentTokens: caseEntry.captureArgumentTokens, submission: caseEntry.submission, gentleAiExecutable: gentleAiPath, piExecutable: negativeControl === null ? piPath : negativeControl.executable, ...(options.signal === undefined ? {} : { signal: options.signal }) };
395
+ // `relay-unavailable` is a NEGATIVE CONTROL: it must never complete a
396
+ // reviewer and never submit. The maintainer-declared `kind` is not
397
+ // evidence that the declared binary is actually incapable, so the arm
398
+ // gate cannot key off the declaration alone. Pointed at a mis-declared
399
+ // CAPABLE binary, a declaration-only gate materializes, runs a REAL
400
+ // reviewer completion, and executes a REAL `capture-result --input=...`
401
+ // submission, and only then reports `fail` — the mutation already
402
+ // happened. So give the negative control a reviewer registry that can
403
+ // never resolve: an incapable binary still classifies relay-unavailable
404
+ // at materialize (the control stays genuine, since it never reached the
405
+ // reviewer stage anyway), while a capable one fails closed at the
406
+ // reviewer stage as kind=reviewer-model-not-found stage=pi
407
+ // mutationOutcome=none, with zero completion and zero submission
408
+ // (gentle-pi#311 P4, superseding the pre-in-process "unlaunchable pi").
352
409
  try {
410
+ // A malformed `reviewerSelection` (caught here rather than at
411
+ // descriptor-validation time, since it is only ever exercised by an
412
+ // armed positive-lens case) must fail this case the same way a real
413
+ // relay error would, never crash the whole matrix.
414
+ const reviewerRegistry = caseEntry.kind === "relay-unavailable" ? unresolvableReviewerRegistry() : armedFauxReviewerRegistry(descriptor.reviewerSelection);
415
+ const request = {
416
+ captureArgumentTokens: caseEntry.captureArgumentTokens,
417
+ submission: caseEntry.submission,
418
+ gentleAiExecutable: gentleAiPath,
419
+ reviewerRegistry,
420
+ selection: descriptor.reviewerSelection,
421
+ routingKey: "review-relay-matrix",
422
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
423
+ };
353
424
  const result = await relay(request);
354
425
  if (caseEntry.kind === "relay-unavailable") {
355
426
  verdicts.push({ name: caseEntry.name, kind: caseEntry.kind, verdict: "fail", reason: "expected relay-unavailable (runtime without --materialize) but the relay succeeded; the descriptor's binary is unexpectedly capable" });
@@ -357,7 +428,9 @@ export async function runMatrix(descriptor, options = {}) {
357
428
  verdicts.push({ name: caseEntry.name, kind: caseEntry.kind, verdict: "pass", promptByteLength: result.promptByteLength, resultByteLength: result.resultByteLength, submissionByteLength: result.submission.length });
358
429
  }
359
430
  } catch (error) {
360
- if (error instanceof ReviewHostRelayError) {
431
+ if (error instanceof DescriptorValidationError) {
432
+ verdicts.push({ name: caseEntry.name, kind: caseEntry.kind, verdict: "fail", reason: error.message });
433
+ } else if (error instanceof ReviewHostRelayError) {
361
434
  if (caseEntry.kind === "relay-unavailable") {
362
435
  if (error.kind === REVIEW_HOST_RELAY_FAILURE.RELAY_UNAVAILABLE && error.stage === "materialize" && error.mutationOutcome === "none") {
363
436
  verdicts.push({ name: caseEntry.name, kind: caseEntry.kind, verdict: "pass", stage: error.stage, mutationOutcome: error.mutationOutcome, reason: REVIEW_HOST_RELAY_FAILURE.RELAY_UNAVAILABLE });
@@ -372,8 +445,6 @@ export async function runMatrix(descriptor, options = {}) {
372
445
  } else {
373
446
  verdicts.push({ name: caseEntry.name, kind: caseEntry.kind, verdict: "fail", reason: `unexpected non-relay error: ${error instanceof Error ? error.message : String(error)}` });
374
447
  }
375
- } finally {
376
- if (negativeControl !== null) rmSync(negativeControl.directory, { recursive: true, force: true });
377
448
  }
378
449
  }
379
450
  return verdicts;
@@ -105,7 +105,6 @@ const requiredPaths = [
105
105
  "skills/issue-creation/SKILL.md",
106
106
  "skills/judgment-day/SKILL.md",
107
107
  "skills/rdd-defect-workflow/SKILL.md",
108
- "skills/release/SKILL.md",
109
108
  "skills/skill-creator/SKILL.md",
110
109
  "skills/skill-improver/SKILL.md",
111
110
  "skills/skill-registry/SKILL.md",
@@ -181,7 +180,7 @@ const contractHashes = {
181
180
  "contracts/review-integration/v2/schemas/start.schema.json": "2991e3fcca672d9257d61b6a336fb34e58b15a8e03f8a09a7adf892cae6a8085",
182
181
  "contracts/review-integration/v2/schemas/status.schema.json": "c4dcc736cfc6300560a3c4262d2d982368529d5c49d58d499552a3b0beef9212",
183
182
  "contracts/telemetry/runtime-aggregate-v1.schema.json": "eb0f2993d9271f55cb42eca343e6fbb601a733fb90bd40daeebc92ee60ae1ba9",
184
- "docs/review-integration.md": "9868cd52ae8f15cc2e6a6757014d5b12ff0904e06725d1a4cf29e9bcc3a7bae2",
183
+ "docs/review-integration.md": "9b60fa2775129c468f7eb3510905014553cee5f3cbcb78a608f08baf8bbd7336",
185
184
  };
186
185
 
187
186
  requiredPaths.push(...Object.keys(contractHashes));
@@ -339,7 +338,7 @@ async function main() {
339
338
  });
340
339
 
341
340
  if (driftedContracts.length > 0) {
342
- console.error("gentle-pi packaged review-integration/v1 and review-integration/v2 contract bytes drifted from the pinned v3.2.1 runtime's vendored Gentle AI contract artifacts:");
341
+ console.error("gentle-pi packaged review-integration/v1 and review-integration/v2 contract bytes drifted from the pinned v3.5.0 runtime's vendored Gentle AI contract artifacts:");
343
342
  for (const drift of driftedContracts) console.error(`- ${drift.relativePath}: expected ${drift.expected}, got ${drift.actual}`);
344
343
  process.exit(1);
345
344
  }
@@ -384,7 +383,7 @@ async function main() {
384
383
  process.exit(1);
385
384
  }
386
385
 
387
- console.log(`gentle-pi package resource check passed (${requiredPaths.length} files; ${Object.keys(contractHashes).length} exact byte-pinned contract artifacts for the v3.2.1 runtime).`);
386
+ console.log(`gentle-pi package resource check passed (${requiredPaths.length} files; ${Object.keys(contractHashes).length} exact byte-pinned contract artifacts for the v3.5.0 runtime).`);
388
387
  }
389
388
 
390
389
  const isMainModule = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;