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.
- package/README.md +63 -59
- package/assets/orchestrator-delegation.md +1 -1
- package/docs/assets/brand/gentle-shell-banner.gif +0 -0
- package/docs/assets/diagrams/odd-workflow.svg +74 -0
- package/docs/assets/features/agents-view.png +0 -0
- package/docs/assets/features/changes-view.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/assets/features/profiles-routing.png +0 -0
- package/docs/gentle-shell.md +52 -15
- package/docs/readme-reference.md +67 -7
- package/docs/review-integration.md +22 -17
- package/extensions/ask-user-question.ts +210 -0
- package/extensions/gentle-agents.ts +93 -18
- package/extensions/gentle-ai.ts +180 -37
- package/extensions/gentle-shell.ts +476 -39
- package/extensions/gentle-todo.ts +19 -1
- package/extensions/quiet-tools.ts +28 -5
- package/extensions/startup-banner.ts +25 -10
- package/lib/agents-view.ts +41 -14
- package/lib/agents-widget.ts +84 -13
- package/lib/animation-policy.ts +52 -0
- package/lib/background-cache-warming.ts +38 -0
- package/lib/command-palette-catalog.ts +2 -0
- package/lib/double-esc-cancel-policy.ts +138 -0
- package/lib/inprocess-reviewer.ts +297 -0
- package/lib/native-review-cli.ts +50 -10
- package/lib/odd-runtime-delegation-gate.ts +88 -0
- package/lib/questionnaire/questionnaire-view.ts +603 -0
- package/lib/questionnaire/schema.ts +82 -0
- package/lib/questionnaire/validate.ts +141 -0
- package/lib/review-candidate-view-owner.ts +20 -5
- package/lib/review-candidate-view.ts +9 -2
- package/lib/review-host-relay.ts +256 -171
- package/lib/review-integration-v2.ts +114 -27
- package/lib/shell-bar.ts +163 -75
- package/lib/shell-card.ts +19 -9
- package/lib/shell-changes-view.ts +43 -5
- package/lib/shell-changes.ts +92 -5
- package/lib/shell-hover.ts +39 -0
- package/lib/shell-prompt.ts +10 -1
- package/lib/shell-sidebar-layout.ts +118 -16
- package/lib/shell-sidebar.ts +16 -0
- package/lib/shell-todo.ts +7 -1
- package/lib/shell-usage-view.ts +103 -12
- package/lib/shell-usage.ts +120 -6
- package/package.json +1 -1
- package/runtime/native-review-cli.mjs +49 -9
- package/runtime/review-integration-v2.mjs +114 -27
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/maintainer/provider-relay-matrix.mjs +118 -47
- package/scripts/verify-package-files.mjs +3 -4
- package/tests/agents-grouping.test.ts +75 -18
- package/tests/agents-view.test.ts +28 -18
- package/tests/agents-widget.test.ts +100 -12
- package/tests/animation-policy.test.ts +42 -0
- package/tests/ask-user-question.test.ts +435 -0
- package/tests/background-cache-warming.test.ts +60 -0
- package/tests/background-subagents.test.ts +68 -0
- package/tests/command-palette.test.ts +10 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +176 -138
- package/tests/double-esc-cancel-policy.test.ts +194 -0
- package/tests/gentle-agents.test.ts +599 -7
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +125 -9
- package/tests/gentle-shell.test.ts +1149 -24
- package/tests/gentle-todo.test.ts +17 -4
- package/tests/inprocess-reviewer.test.ts +460 -0
- package/tests/maintainer/provider-relay.maintest.ts +101 -143
- package/tests/native-review-capability-contract.test.ts +34 -1
- package/tests/native-review-parity.test.ts +19 -0
- package/tests/odd-runtime-delegation-gate.test.ts +212 -0
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/package-manifest.test.ts +6 -17
- package/tests/questionnaire-schema.test.ts +274 -0
- package/tests/questionnaire-view.test.ts +446 -0
- package/tests/rdd-status-line.test.ts +21 -4
- package/tests/review-candidate-owner-retry.test.ts +63 -0
- package/tests/review-candidate-view.test.ts +15 -0
- package/tests/review-controller-native-routing.test.ts +86 -0
- package/tests/review-host-relay-routing.test.ts +77 -0
- package/tests/review-host-relay.test.ts +297 -299
- package/tests/review-integration-v2-forward.test.ts +61 -0
- package/tests/review-integration-v2.test.ts +146 -1
- package/tests/review-ledger-contract.test.ts +1 -2
- package/tests/review-relay-transport-agent.test.ts +129 -26
- package/tests/review-risk-assessment.test.ts +104 -0
- package/tests/runtime-harness.mjs +11 -0
- package/tests/session-changes-shell.test.ts +27 -0
- package/tests/session-worktree-registry.test.ts +41 -0
- package/tests/shell-bar.test.ts +200 -124
- package/tests/shell-card.test.ts +5 -3
- package/tests/shell-changes-view.test.ts +47 -0
- package/tests/shell-changes.test.ts +177 -0
- package/tests/shell-hover.test.ts +19 -0
- package/tests/shell-prompt.test.ts +20 -0
- package/tests/shell-sidebar-fullscreen.test.ts +59 -0
- package/tests/shell-sidebar-layout.test.ts +301 -8
- package/tests/shell-sidebar.test.ts +25 -1
- package/tests/shell-todo.test.ts +36 -0
- package/tests/shell-usage-view.test.ts +120 -1
- package/tests/shell-usage.test.ts +129 -0
- package/tests/skill-collision-prefixes.test.ts +1 -1
- package/tests/startup-banner.test.ts +93 -2
- package/docs/assets/brand/gentle-pi-banner.png +0 -0
- package/lib/opaque-pi-reviewer-adapter.ts +0 -404
- package/skills/release/SKILL.md +0 -137
- 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
|
|
443
|
-
//
|
|
444
|
-
// self-contained authority-advancing
|
|
445
|
-
// `--agent=pi --execute=true`, with NO submission descriptor
|
|
446
|
-
//
|
|
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
|
|
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
|
-
:
|
|
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
|
|
1661
|
-
// operations render
|
|
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
|
|
1664
|
-
//
|
|
1665
|
-
//
|
|
1666
|
-
//
|
|
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
|
|
1698
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1930
|
-
//
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
51
|
-
export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:
|
|
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.
|
|
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.
|
|
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.
|
|
118
|
-
"darwin/arm64": asset("gentle-ai_3.
|
|
119
|
-
"linux/amd64": asset("gentle-ai_3.
|
|
120
|
-
"linux/arm64": asset("gentle-ai_3.
|
|
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 {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
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", "
|
|
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
|
-
|
|
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,
|
|
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
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
321
|
-
//
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
|
339
|
-
// never submit. The maintainer-declared `kind` is not
|
|
340
|
-
// declared binary is actually incapable, so the arm
|
|
341
|
-
// the declaration alone. Pointed at a mis-declared
|
|
342
|
-
// declaration-only gate materializes, runs a REAL
|
|
343
|
-
// a REAL `capture-result --input=...`
|
|
344
|
-
// `fail` — the mutation already
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
// stage
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
|
|
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
|
|
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": "
|
|
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.
|
|
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.
|
|
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;
|