gentle-pi 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/orchestrator-delegation.md +13 -8
- package/assets/orchestrator.md +2 -2
- package/docs/gentle-shell.md +40 -17
- package/docs/readme-reference.md +41 -7
- package/docs/review-integration.md +25 -11
- package/extensions/gentle-agents.ts +85 -17
- package/extensions/gentle-ai.ts +179 -12
- package/extensions/gentle-shell.ts +408 -38
- package/extensions/gentle-todo.ts +19 -1
- package/lib/agents-view.ts +41 -14
- package/lib/agents-widget.ts +84 -13
- package/lib/command-palette-catalog.ts +1 -0
- package/lib/double-esc-cancel-policy.ts +138 -0
- package/lib/inprocess-reviewer.ts +260 -0
- package/lib/model-routing-authority.ts +1 -1
- package/lib/native-review-cli.ts +23 -0
- package/lib/odd-runtime-delegation-gate.ts +88 -0
- package/lib/review-host-relay.ts +262 -94
- package/lib/review-integration-v2.ts +110 -26
- package/lib/shell-bar.ts +158 -29
- 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 +111 -15
- package/lib/shell-sidebar.ts +16 -0
- package/lib/shell-todo.ts +7 -1
- package/lib/shell-usage-view.ts +98 -10
- package/lib/shell-usage.ts +226 -10
- package/package.json +2 -1
- package/runtime/native-review-cli.mjs +23 -0
- package/runtime/review-integration-v2.mjs +110 -26
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/maintainer/provider-relay-matrix.mjs +118 -47
- package/scripts/mirror-odd-routing.mjs +242 -0
- package/scripts/verify-package-files.mjs +3 -3
- 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/command-palette.test.ts +1 -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 +528 -5
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +69 -5
- package/tests/gentle-shell.test.ts +903 -25
- package/tests/gentle-todo.test.ts +17 -4
- package/tests/inprocess-reviewer.test.ts +368 -0
- package/tests/maintainer/provider-relay.maintest.ts +101 -143
- package/tests/native-review-capability-contract.test.ts +32 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +293 -0
- package/tests/odd-routing-contract.test.ts +57 -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 -6
- package/tests/review-controller-native-routing.test.ts +60 -1
- package/tests/review-host-relay-routing.test.ts +77 -0
- package/tests/review-host-relay.test.ts +285 -239
- package/tests/review-integration-v2-forward.test.ts +61 -0
- package/tests/review-integration-v2.test.ts +116 -1
- package/tests/review-relay-transport-agent.test.ts +83 -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 +224 -6
- 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 +243 -5
- package/tests/shell-sidebar.test.ts +25 -1
- package/tests/shell-todo.test.ts +36 -0
- package/tests/shell-usage-view.test.ts +123 -3
- package/tests/shell-usage.test.ts +254 -6
- package/lib/opaque-pi-reviewer-adapter.ts +0 -284
- package/tests/opaque-pi-reviewer-adapter.test.ts +0 -266
|
@@ -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",
|
|
@@ -439,11 +455,21 @@ const REPOSITORY_CONTEXT_OUTCOMES = ["applied", "pending", "blocked_conflict", "
|
|
|
439
455
|
|
|
440
456
|
|
|
441
457
|
|
|
442
|
-
// The two
|
|
443
|
-
//
|
|
444
|
-
// self-contained authority-advancing
|
|
445
|
-
// `--agent=pi --execute=true`, with NO submission descriptor
|
|
446
|
-
//
|
|
458
|
+
// The two non-lens provider role capture operations (gentle-pi#311 P4-roles;
|
|
459
|
+
// provider side gentle-ai#3264). An older gentle-ai still renders each one as
|
|
460
|
+
// a self-contained authority-advancing vector: binding tokens plus
|
|
461
|
+
// `--agent=pi --execute=true`, with NO submission descriptor — executing the
|
|
462
|
+
// exact rendered tokens makes Go materialize the role prompt, run its own
|
|
463
|
+
// locked-down pi subprocess, and admit the verdict itself.
|
|
464
|
+
//
|
|
465
|
+
// gentle-ai's v9 contract makes both roles host-mediated exactly like a lens
|
|
466
|
+
// materialize slot instead (gentle-pi#311 P3): the collect input carries
|
|
467
|
+
// `--materialize=true` (never alongside `--execute`) plus a provider-owned
|
|
468
|
+
// `submission` descriptor, and the host completes the frozen prompt
|
|
469
|
+
// in-process and submits the result through that exact descriptor. The two
|
|
470
|
+
// wire forms are mutually exclusive on one input; see the gating in
|
|
471
|
+
// decodeCollectInput below. The known operation set is closed — an unknown
|
|
472
|
+
// role capture operation is never executed either way.
|
|
447
473
|
export const REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION = {
|
|
448
474
|
CAPTURE_REFUTER: "review.capture-refuter",
|
|
449
475
|
CAPTURE_VALIDATION: "review.capture-validation",
|
|
@@ -1452,11 +1478,18 @@ function decodeCaptureSubmission(value , label , v5 , v6
|
|
|
1452
1478
|
// one-entry values array the host relay already consumes. A payload
|
|
1453
1479
|
// carrying both wire forms at once matches no captured shape and falls
|
|
1454
1480
|
// through to the legacy decoder, which rejects the unknown `value` key.
|
|
1481
|
+
//
|
|
1482
|
+
// gentle-ai's v9 contract renders the same singular-value shape for the
|
|
1483
|
+
// two host-mediated provider role operations (capture-refuter,
|
|
1484
|
+
// capture-validation): they carry a `schema` key exactly like
|
|
1485
|
+
// capture-result, since both bind one artifact-path-or-stdin value, never
|
|
1486
|
+
// the correction-plan's numeric bounds (gentle-pi#311 P3).
|
|
1455
1487
|
if (v5 && typeof value === "object" && value !== null && "value" in value && !("values" in value)) {
|
|
1456
1488
|
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`);
|
|
1489
|
+
const operationToken = enumeration(submission.operation_token, ["capture-result", "capture-correction-plan", "capture-refuter", "capture-validation"] , `${label}.operation_token`);
|
|
1458
1490
|
const argumentTokens = stringArray(submission.argument_tokens, `${label}.argument_tokens`, { minimum: 1 });
|
|
1459
|
-
const
|
|
1491
|
+
const bindsArtifactSchema = operationToken === "capture-result" || operationToken === "capture-refuter" || operationToken === "capture-validation";
|
|
1492
|
+
const row = bindsArtifactSchema
|
|
1460
1493
|
? exactRecord(submission.value, `${label}.value`, ["slot", "domain", "schema", "substitution_location"])
|
|
1461
1494
|
: exactRecord(submission.value, `${label}.value`, ["slot", "domain", "minimum", "maximum", "substitution_location"]);
|
|
1462
1495
|
return {
|
|
@@ -1465,7 +1498,11 @@ function decodeCaptureSubmission(value , label , v5 , v6
|
|
|
1465
1498
|
values: [{
|
|
1466
1499
|
slot: operationToken === "capture-result"
|
|
1467
1500
|
? enumeration(row.slot, ["reviewer_result"] , `${label}.value.slot`)
|
|
1468
|
-
:
|
|
1501
|
+
: operationToken === "capture-correction-plan"
|
|
1502
|
+
? enumeration(row.slot, ["correction_lines"] , `${label}.value.slot`)
|
|
1503
|
+
: operationToken === "capture-refuter"
|
|
1504
|
+
? enumeration(row.slot, ["provider_refuter"] , `${label}.value.slot`)
|
|
1505
|
+
: enumeration(row.slot, ["provider_targeted_validator"] , `${label}.value.slot`),
|
|
1469
1506
|
domain: nonempty(row.domain, `${label}.value.domain`),
|
|
1470
1507
|
...(row.schema === undefined ? {} : { schema: nonempty(row.schema, `${label}.value.schema`) }),
|
|
1471
1508
|
...(row.minimum === undefined ? {} : { minimum: integer(row.minimum, `${label}.value.minimum`, 1, 200) }),
|
|
@@ -1606,7 +1643,7 @@ const CORRECTION_REQUEST_REASON_CODES = Object.freeze(["correction_plan_required
|
|
|
1606
1643
|
// v5 capture operations that must carry a submission descriptor.
|
|
1607
1644
|
const V5_SUBMISSION_CAPTURE_OPERATIONS = Object.freeze(["review.capture-correction-plan"] );
|
|
1608
1645
|
|
|
1609
|
-
function decodeCollectInput(value , label , v5 , v6 ) {
|
|
1646
|
+
function decodeCollectInput(value , label , v5 , v6 , v9 ) {
|
|
1610
1647
|
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
1648
|
const name = text(input.name, `${label}.name`, { minimum: 1, pattern: /^[a-z0-9_]+$/ });
|
|
1612
1649
|
const schema = nonempty(input.schema, `${label}.schema`);
|
|
@@ -1657,13 +1694,18 @@ function decodeCollectInput(value , label , v5 , v6
|
|
|
1657
1694
|
sha256(argumentsList[5] .value, `${label}.arguments[5].value`);
|
|
1658
1695
|
}
|
|
1659
1696
|
|
|
1660
|
-
// gentle-pi#311 P4-roles: the two
|
|
1661
|
-
// operations render
|
|
1697
|
+
// gentle-pi#311 P4-roles / P3: the two non-lens provider role capture
|
|
1698
|
+
// operations render either wire form on one input, never both at once.
|
|
1699
|
+
// An older gentle-ai renders a SELF-CONTAINED authority-advancing vector
|
|
1700
|
+
// (binding tokens + --agent=pi --execute=true, no submission): executing
|
|
1662
1701
|
// 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
|
-
//
|
|
1702
|
+
// own locked-down pi subprocess, and admit the raw verdict. gentle-ai's
|
|
1703
|
+
// v9 contract instead renders a HOST-MEDIATED form: the same binding
|
|
1704
|
+
// tokens plus --materialize=true (never --execute) and a provider-owned
|
|
1705
|
+
// submission descriptor, exactly like a lens capture-result materialize
|
|
1706
|
+
// slot — the host completes the frozen prompt in-process and submits
|
|
1707
|
+
// through that descriptor.
|
|
1708
|
+
const isRoleCaptureOperation = (REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS ).includes(captureOperation);
|
|
1667
1709
|
if (captureOperation === REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_REFUTER && schema !== "https://gentle-ai.dev/schema/review/refuter/v1") {
|
|
1668
1710
|
throw new TypeError(`${label}.schema must be https://gentle-ai.dev/schema/review/refuter/v1`);
|
|
1669
1711
|
}
|
|
@@ -1694,8 +1736,30 @@ function decodeCollectInput(value , label , v5 , v6
|
|
|
1694
1736
|
if (providerArgument("target") !== validationRequest.correctionTargetIdentity) throw new TypeError(`${label}.arguments target must bind validation_request.correction_target_identity`);
|
|
1695
1737
|
if (providerArgument("request-hash") !== validationRequest.requestHash) throw new TypeError(`${label}.arguments request-hash must bind validation_request.request_hash`);
|
|
1696
1738
|
}
|
|
1697
|
-
if (input.submission !== undefined
|
|
1698
|
-
|
|
1739
|
+
if (isRoleCaptureOperation && input.submission !== undefined) {
|
|
1740
|
+
// A submission descriptor only ever rides the v9 host-mediated form
|
|
1741
|
+
// (gentle-ai's v9 provider contract; the sibling branch ahead of the
|
|
1742
|
+
// currently-released v8). A v8-or-earlier provider (status/v3..v8) never
|
|
1743
|
+
// renders one, even if it happened to carry a submission-shaped payload,
|
|
1744
|
+
// so this is gated on v9 specifically — not on v5, which v8 already
|
|
1745
|
+
// satisfies. Within a v9 payload the discriminator is exactly the
|
|
1746
|
+
// materialize slot's --materialize=true with no --execute, the same one
|
|
1747
|
+
// a lens capture-result materialize slot already uses. Mixing a
|
|
1748
|
+
// submission with the old self-contained --execute=true vector, or with
|
|
1749
|
+
// neither flag, is a malformed vector.
|
|
1750
|
+
if (!v9) {
|
|
1751
|
+
throw new TypeError(`${label}.submission requires the v9 provider contract for the host-mediated ${captureOperation} form`);
|
|
1752
|
+
}
|
|
1753
|
+
const argumentNamed = (argumentName ) => {
|
|
1754
|
+
const matches = argumentsList.filter((argument) => argument.name === argumentName);
|
|
1755
|
+
return matches.length === 1 ? matches[0] .value : undefined;
|
|
1756
|
+
};
|
|
1757
|
+
if (argumentNamed("materialize") !== "true") {
|
|
1758
|
+
throw new TypeError(`${label}.submission requires --materialize=true on the host-mediated ${captureOperation} form`);
|
|
1759
|
+
}
|
|
1760
|
+
if (argumentNamed("execute") !== undefined) {
|
|
1761
|
+
throw new TypeError(`${label} must not carry --execute alongside a submission descriptor on ${captureOperation}`);
|
|
1762
|
+
}
|
|
1699
1763
|
}
|
|
1700
1764
|
|
|
1701
1765
|
if (captureOperation === "review.capture-result") {
|
|
@@ -1712,7 +1776,12 @@ function decodeCollectInput(value , label , v5 , v6
|
|
|
1712
1776
|
const submissionOperations = v5
|
|
1713
1777
|
? ["review.capture-result", "review.capture-correction-plan", ...(v6 ? ["external.select_intended_untracked"] : [])]
|
|
1714
1778
|
: ["review.capture-result"];
|
|
1715
|
-
|
|
1779
|
+
// Role operations are gated above by the materialize/execute discriminator
|
|
1780
|
+
// instead of this allowlist: an older gentle-ai's self-contained vector
|
|
1781
|
+
// legitimately carries no submission regardless of v5/v6, so they cannot
|
|
1782
|
+
// simply join the list the way every other submission-carrying operation
|
|
1783
|
+
// does.
|
|
1784
|
+
if (input.submission !== undefined && !isRoleCaptureOperation && !submissionOperations.includes(captureOperation)) {
|
|
1716
1785
|
throw new TypeError(v5 ? `${label}.submission is only valid for ${submissionOperations.join(", ")}` : `${label}.submission is only valid for review.capture-result`);
|
|
1717
1786
|
}
|
|
1718
1787
|
if (intendedUntracked && input.submission === undefined) throw new TypeError(`${label}.submission is required for external.select_intended_untracked`);
|
|
@@ -1750,8 +1819,14 @@ export function decodeReviewManagedAssetsContinuationV1(value , label
|
|
|
1750
1819
|
return { operation, command, ...(agent === undefined ? {} : { agent }), ...(staleAssets === undefined ? {} : { staleAssets }) };
|
|
1751
1820
|
}
|
|
1752
1821
|
|
|
1753
|
-
export function decodeReviewNextTransitionV3(value , options
|
|
1754
|
-
|
|
1822
|
+
export function decodeReviewNextTransitionV3(value , options = {}) {
|
|
1823
|
+
// v9 (gentle-ai's next contract, ahead of the currently-released v8) is
|
|
1824
|
+
// the only rung this sub-decoder distinguishes beyond v6: v7 and v8 add
|
|
1825
|
+
// nothing to next_transition/collect-input decoding (v8 only extended the
|
|
1826
|
+
// reviewer-result transition for OpenCode provider tasks at the top
|
|
1827
|
+
// status level), so they decode on the exact v6 surface here.
|
|
1828
|
+
const v9 = options.v9 === true;
|
|
1829
|
+
const v6 = options.v6 === true || v9;
|
|
1755
1830
|
const v5 = options.v5 === true || v6;
|
|
1756
1831
|
const transition = exactRecord(value, "next_transition", ["kind", "reason_code"], ["execute", "collect", ...(v5 ? ["correction_request"] : []), "continuation", "unachievable_lens_slots"]);
|
|
1757
1832
|
const kind = enumeration(transition.kind, ["execute", "collect", "stop"] , "next_transition.kind");
|
|
@@ -1813,7 +1888,7 @@ export function decodeReviewNextTransitionV3(value , options
|
|
|
1813
1888
|
}
|
|
1814
1889
|
if (kind === "collect") {
|
|
1815
1890
|
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 });
|
|
1891
|
+
const inputs = array(collect.inputs, "next_transition.collect.inputs", (entry, label) => decodeCollectInput(entry, label, v5, v6, v9), { minimum: 1 });
|
|
1817
1892
|
if (transition.execute !== undefined) throw new TypeError("next_transition.execute is incompatible with collect");
|
|
1818
1893
|
return { kind, reasonCode, collect: { inputs }, ...(correctionRequest === undefined ? {} : { correctionRequest }) };
|
|
1819
1894
|
}
|
|
@@ -1926,16 +2001,25 @@ export function decodeReviewStatusV3(value ) {
|
|
|
1926
2001
|
// surfaces. status/v6 adds the intended-untracked selection; status/v7
|
|
1927
2002
|
// (gentle-ai v2.6.0, advertised through capabilities/v2.5 alongside v6)
|
|
1928
2003
|
// adds optional `eligible_untracked_inventory` and escalation metadata,
|
|
1929
|
-
// so it is decoded on the v6 surface.
|
|
1930
|
-
//
|
|
2004
|
+
// so it is decoded on the v6 surface. status/v8 (gentle-ai main, PR #4765;
|
|
2005
|
+
// the current released contract) only extended the reviewer-result
|
|
2006
|
+
// transition for OpenCode provider tasks -- no new top-level key -- so it
|
|
2007
|
+
// decodes on the exact v7 surface too. status/v9 (the sibling gentle-ai
|
|
2008
|
+
// branch's contract, ahead of the released v8) adds nothing at this top
|
|
2009
|
+
// level either: its one addition is the host-mediated role `submission`
|
|
2010
|
+
// on a next_transition.collect input (gentle-pi#311 P3), gated on v9 in
|
|
2011
|
+
// decodeCollectInput, not here. v3 keeps rejecting every
|
|
2012
|
+
// v5/v6/v7/v8/v9-only field.
|
|
1931
2013
|
const schema = typeof value === "object" && value !== null ? (value ).schema : undefined;
|
|
1932
|
-
const
|
|
2014
|
+
const v9 = schema === "gentle-ai.review-integration.status/v9";
|
|
2015
|
+
const v8 = v9 || schema === "gentle-ai.review-integration.status/v8";
|
|
2016
|
+
const v7 = v8 || schema === "gentle-ai.review-integration.status/v7";
|
|
1933
2017
|
const v6 = v7 || schema === "gentle-ai.review-integration.status/v6";
|
|
1934
2018
|
const v5 = v6 || schema === "gentle-ai.review-integration.status/v5";
|
|
1935
2019
|
const body = exactRecord(value, "status", [
|
|
1936
2020
|
"schema", "contract", "operation", "applicability", "action", "replayability", "target_identity", "projection", "repair", "candidates",
|
|
1937
2021
|
], ["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);
|
|
2022
|
+
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
2023
|
|
|
1940
2024
|
const applicability = enumeration(body.applicability, ["current_target", "unrelated", "ambiguous", "corrupted"] , "status.applicability");
|
|
1941
2025
|
let receipt ;
|
|
@@ -1983,7 +2067,7 @@ export function decodeReviewStatusV3(value ) {
|
|
|
1983
2067
|
if (action === "recover" && actionDisposition === undefined) throw new TypeError("recover status requires action_disposition");
|
|
1984
2068
|
if (action !== "recover" && actionDisposition !== undefined) throw new TypeError("status.action_disposition is only valid for the recover action");
|
|
1985
2069
|
if (body.eligibility !== undefined) decodeEligibility(body.eligibility, "status.eligibility");
|
|
1986
|
-
const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition, { v5, v6 });
|
|
2070
|
+
const nextTransition = body.next_transition === undefined ? undefined : decodeReviewNextTransitionV3(body.next_transition, { v5, v6, v9 });
|
|
1987
2071
|
const validationRequest = v5 && body.validation_request !== undefined
|
|
1988
2072
|
? decodeReviewTargetedValidationRequestV1(body.validation_request, "status.validation_request")
|
|
1989
2073
|
: 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.4.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.4.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 82a6de96, the published v3.4.0 release head.
|
|
51
|
+
export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:bQOM+Xa3WN5+idTagEk15rgEEQQWVhZvq8BPF/FHtSs=";
|
|
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.4.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.4.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.4.0_darwin_amd64.tar.gz", "7d13fa45489098ff9e82dee2232204f72bf1c04ef998ceadfe1ea972697f2e56", "d9096b4bb13e56aae1251dfacd2487821a15c40b7c9397555ad615ede47e658a", "gentle-ai"),
|
|
118
|
+
"darwin/arm64": asset("gentle-ai_3.4.0_darwin_arm64.tar.gz", "b9052d8dc02923663d4451a4b031c4ef7328cd866c0cb6729e8ff2b9853603f0", "0aca1239cafd87ce63b194ab69da90e762801d82a96418fd6b7b12ea3806dc07", "gentle-ai"),
|
|
119
|
+
"linux/amd64": asset("gentle-ai_3.4.0_linux_amd64.tar.gz", "c287289a514420381e890991bb3fbea4a2c36d7b4b1774fcf6bc4deb83378915", "309d9aafb48de5ef90ba0a212e8a98e8fdbb82d0852e24015e36a5ce0fe04c06", "gentle-ai"),
|
|
120
|
+
"linux/arm64": asset("gentle-ai_3.4.0_linux_arm64.tar.gz", "dfd1fe70acfe577d2a36e3a658895f481467518a2923b7200403dbcca0181399", "83ba7d27d250019115690be51664bd356e09085da8858761e362e6053c6290d3", "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;
|