gentle-pi 2.4.0 → 2.6.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 +292 -25
- package/assets/agents/gentle-ai-worker.md +13 -0
- package/assets/agents/jd-fix-agent.md +18 -0
- package/assets/agents/jd-judge-a.md +1 -1
- package/assets/agents/jd-judge-b.md +1 -1
- package/assets/agents/sdd-apply.md +7 -5
- package/assets/agents/sdd-archive.md +5 -3
- package/assets/agents/sdd-design.md +4 -0
- package/assets/agents/sdd-explore.md +4 -0
- package/assets/agents/sdd-init.md +4 -0
- package/assets/agents/sdd-onboard.md +4 -0
- package/assets/agents/sdd-proposal.md +4 -0
- package/assets/agents/sdd-remediate.md +37 -0
- package/assets/agents/sdd-research.md +26 -3
- package/assets/agents/sdd-spec.md +4 -0
- package/assets/agents/sdd-status.md +9 -75
- package/assets/agents/sdd-sync.md +4 -0
- package/assets/agents/sdd-tasks.md +4 -0
- package/assets/agents/sdd-verify.md +5 -3
- package/assets/chains/sdd-full.chain.md +4 -0
- package/assets/chains/sdd-plan.chain.md +4 -0
- package/assets/chains/sdd-verify.chain.md +4 -0
- package/assets/migrations/managed-assets-v2.5.0.json +7 -0
- package/assets/orchestrator-delegation.md +39 -11
- package/assets/orchestrator.md +5 -5
- package/assets/sdd-orchestrator-workflow.md +54 -21
- package/assets/support/sdd-status-contract.md +34 -90
- package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
- package/docs/delegated-verification.md +25 -0
- package/docs/telemetry.md +94 -0
- package/docs/windows-startup-console-visibility.md +18 -0
- package/extensions/ask-user-choice.ts +159 -25
- package/extensions/codegraph-tools.ts +95 -5
- package/extensions/gentle-agents.ts +1337 -0
- package/extensions/gentle-ai.ts +2916 -386
- package/extensions/gentle-shell.ts +650 -0
- package/extensions/gentle-todo.ts +234 -0
- package/extensions/quiet-tools.ts +2 -1
- package/extensions/runtime-metrics.ts +130 -0
- package/extensions/sdd-init.ts +2 -2
- package/extensions/startup-banner.ts +52 -75
- package/lib/agent-profiles.ts +550 -0
- package/lib/agents-completion-delivery.ts +72 -0
- package/lib/agents-config.ts +315 -0
- package/lib/agents-history.ts +88 -0
- package/lib/agents-messaging.ts +187 -0
- package/lib/agents-protocol.ts +501 -0
- package/lib/agents-runner.ts +1012 -0
- package/lib/agents-thread-view.ts +57 -0
- package/lib/agents-transcript.ts +87 -0
- package/lib/agents-view-layout.ts +40 -0
- package/lib/agents-view.ts +914 -0
- package/lib/agents-widget.ts +241 -0
- package/lib/gentle-ai-binary.ts +3 -1
- package/lib/gentle-ai-renderer.ts +143 -25
- package/lib/native-choice-list.ts +194 -0
- package/lib/native-fullscreen-interaction.ts +47 -0
- package/lib/native-pointer-region.ts +164 -0
- package/lib/native-review-cli.ts +371 -13
- package/lib/orchestrator-presence.ts +337 -0
- package/lib/profiles-orchestrator.ts +203 -0
- package/lib/review-candidate-view-owner.ts +427 -0
- package/lib/review-candidate-view.ts +150 -48
- package/lib/review-consent-component.ts +247 -0
- package/lib/review-consent-ui.ts +110 -0
- package/lib/review-host-relay.ts +28 -0
- package/lib/review-integration-v2.ts +243 -11
- package/lib/review-last-event-controller.ts +8 -4
- package/lib/review-relay-contract.ts +11 -0
- package/lib/review-reminder-receipt.ts +74 -0
- package/lib/review-repository.ts +2 -2
- package/lib/review-risk-assessment.ts +339 -0
- package/lib/review-session-standing-permission-ipc.ts +309 -0
- package/lib/review-session-standing-permission.ts +240 -0
- package/lib/runtime-metrics-children.ts +199 -0
- package/lib/runtime-metrics-delivery.ts +68 -0
- package/lib/runtime-metrics-native.ts +166 -0
- package/lib/runtime-metrics-pi-identity.ts +113 -0
- package/lib/runtime-metrics-policy.ts +51 -0
- package/lib/runtime-metrics.ts +255 -0
- package/lib/sdd-preflight.ts +362 -81
- package/lib/sdd-research-capabilities.ts +228 -0
- package/lib/sdd-status.ts +29 -7
- package/lib/session-worktree-registry.ts +118 -0
- package/lib/shell-bar.ts +184 -0
- package/lib/shell-card.ts +133 -0
- package/lib/shell-changes-view.ts +530 -0
- package/lib/shell-changes.ts +290 -0
- package/lib/shell-gauge.ts +40 -0
- package/lib/shell-prompt.ts +115 -0
- package/lib/shell-sidebar-banner.ts +11 -0
- package/lib/shell-sidebar-layout.ts +213 -0
- package/lib/shell-sidebar.ts +41 -0
- package/lib/shell-todo.ts +297 -0
- package/lib/shell-usage-view.ts +76 -0
- package/lib/shell-usage.ts +246 -0
- package/lib/telemetry-trigger.ts +153 -0
- package/package.json +8 -5
- package/runtime/gentle-ai-binary.mjs +3 -1
- package/runtime/native-review-cli.mjs +370 -12
- package/runtime/review-integration-v2.mjs +243 -11
- package/runtime/review-relay-contract.mjs +11 -0
- package/runtime/review-risk-assessment.mjs +340 -0
- package/runtime/telemetry-trigger.mjs +154 -0
- package/scripts/build-runtime-modules.mjs +11 -1
- package/scripts/check-types.mjs +125 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/install-gentle-ai.mjs +12 -0
- package/scripts/install-tui-mode-setting.mjs +114 -0
- package/scripts/test-packed-runner.mjs +38 -2
- package/scripts/types-baseline.json +99 -0
- package/scripts/verify-package-files.mjs +8 -2
- package/skills/_shared/review-ledger-contract.md +20 -2
- package/skills/issue-creation/SKILL.md +3 -3
- package/skills/judgment-day/SKILL.md +17 -3
- package/skills/judgment-day/references/prompts-and-formats.md +14 -3
- package/tests/agent-profiles.test.ts +722 -0
- package/tests/agents-completion-delivery.test.ts +94 -0
- package/tests/agents-config.test.ts +205 -0
- package/tests/agents-fake-child.ts +66 -0
- package/tests/agents-grouping.test.ts +179 -0
- package/tests/agents-history.test.ts +54 -0
- package/tests/agents-integration.test.ts +100 -0
- package/tests/agents-messaging.test.ts +94 -0
- package/tests/agents-protocol.test.ts +198 -0
- package/tests/agents-queries.test.ts +190 -0
- package/tests/agents-responsive.test.ts +43 -0
- package/tests/agents-runner-process.test.ts +111 -0
- package/tests/agents-runner.test.ts +959 -0
- package/tests/agents-thread-view.test.ts +45 -0
- package/tests/agents-transcript.test.ts +30 -0
- package/tests/agents-view.test.ts +685 -0
- package/tests/agents-widget.test.ts +141 -0
- package/tests/artifact-language.test.ts +25 -2
- package/tests/ask-user-choice.test.ts +325 -5
- package/tests/asset-installation-runtime.test.ts +108 -0
- package/tests/autonomous-guard.test.ts +116 -1
- package/tests/codegraph-tools.test.ts +112 -2
- package/tests/delegated-key-learnings-contract.test.ts +1 -1
- package/tests/devbinary/native-review-parity.devtest.ts +110 -0
- package/tests/feature-request-form.test.ts +67 -0
- package/tests/fixtures/agents-messaging-child.mjs +5 -0
- package/tests/fixtures/agents-process-child.mjs +23 -0
- package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
- package/tests/gentle-agents.test.ts +2168 -0
- package/tests/gentle-ai-binary.test.ts +7 -2
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai-renderer.test.ts +103 -0
- package/tests/gentle-ai.test.ts +971 -15
- package/tests/gentle-card-text.ts +35 -0
- package/tests/gentle-shell.test.ts +818 -0
- package/tests/gentle-todo.test.ts +226 -0
- package/tests/install-tui-mode-setting.test.ts +324 -0
- package/tests/issue-creation-skill.test.ts +22 -0
- package/tests/model-routing-authority.test.ts +12 -0
- package/tests/native-choice-list.test.ts +202 -0
- package/tests/native-fullscreen-interaction.test.ts +125 -0
- package/tests/native-pointer-region.test.ts +245 -0
- package/tests/native-review-capability-contract.test.ts +27 -1
- package/tests/native-review-cli.test.ts +317 -3
- package/tests/native-review-consent.test.ts +91 -0
- package/tests/native-review-parity-runtime.test.ts +8 -2
- package/tests/native-review-parity.test.ts +43 -29
- package/tests/native-sdd-attempt-authority.test.ts +7 -2
- package/tests/orchestrator-budget.test.ts +69 -0
- package/tests/orchestrator-presence.test.ts +389 -0
- package/tests/orchestrator-rdd-ownership.test.ts +9 -0
- package/tests/package-manifest.test.ts +243 -7
- package/tests/profiles-orchestrator.test.ts +208 -0
- package/tests/quiet-tool-rendering.test.ts +97 -37
- package/tests/rdd-aware-verification-contract.test.ts +226 -0
- package/tests/rdd-status-line.test.ts +286 -0
- package/tests/review-agent-end-preflight.test.ts +332 -24
- package/tests/review-candidate-view.test.ts +751 -7
- package/tests/review-consent-ui.test.ts +352 -0
- package/tests/review-contract-prompt.test.ts +17 -0
- package/tests/review-controller-native-recovery.test.ts +29 -4
- package/tests/review-controller-native-routing.test.ts +884 -7
- package/tests/review-controller-workspace-root.test.ts +45 -2
- package/tests/review-controller.test.ts +26 -1
- package/tests/review-host-relay-restart-parity.test.ts +142 -1
- package/tests/review-host-relay-routing.test.ts +384 -8
- package/tests/review-host-relay.test.ts +29 -0
- package/tests/review-integration-v2-forward.test.ts +44 -0
- package/tests/review-integration-v2.test.ts +276 -0
- package/tests/review-last-event-closure.test.ts +112 -3
- package/tests/review-ledger-contract.test.ts +61 -6
- package/tests/review-relay-contract.test.ts +26 -0
- package/tests/review-reminder-receipt.test.ts +62 -0
- package/tests/review-repository.test.ts +28 -1
- package/tests/review-risk-assessment.test.ts +626 -0
- package/tests/review-session-standing-permission-controller.test.ts +656 -0
- package/tests/review-session-standing-permission-ipc.test.ts +233 -0
- package/tests/review-session-standing-permission-runtime.test.ts +212 -0
- package/tests/review-session-standing-permission.test.ts +156 -0
- package/tests/runtime-harness.mjs +447 -39
- package/tests/runtime-metrics-children.test.ts +206 -0
- package/tests/runtime-metrics-delivery.test.ts +85 -0
- package/tests/runtime-metrics-extension.test.ts +187 -0
- package/tests/runtime-metrics-native.test.ts +209 -0
- package/tests/runtime-metrics-pi-identity.test.ts +113 -0
- package/tests/runtime-metrics-policy.test.ts +62 -0
- package/tests/runtime-metrics.test.ts +184 -0
- package/tests/sdd-agent-tools.test.ts +10 -1
- package/tests/sdd-execution-routing-contract.test.ts +28 -0
- package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
- package/tests/sdd-native-managed-uptake.test.ts +253 -0
- package/tests/sdd-planning-routing-contract.test.ts +45 -0
- package/tests/sdd-preflight.test.ts +252 -8
- package/tests/sdd-research-capabilities.test.ts +256 -0
- package/tests/sdd-research-live.test.ts +241 -0
- package/tests/sdd-selection-transport.test.ts +504 -0
- package/tests/sdd-status.test.ts +51 -0
- package/tests/session-worktree-registry.test.ts +135 -0
- package/tests/shell-bar.test.ts +176 -0
- package/tests/shell-card.test.ts +139 -0
- package/tests/shell-changes-view.test.ts +609 -0
- package/tests/shell-changes.test.ts +350 -0
- package/tests/shell-prompt.test.ts +140 -0
- package/tests/shell-sidebar-banner.test.ts +23 -0
- package/tests/shell-sidebar-layout.test.ts +387 -0
- package/tests/shell-sidebar.test.ts +50 -0
- package/tests/shell-todo.test.ts +259 -0
- package/tests/shell-usage-view.test.ts +62 -0
- package/tests/shell-usage.test.ts +197 -0
- package/tests/startup-banner.test.ts +126 -0
- package/tests/telemetry-trigger.test.ts +351 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
|
-
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
7
7
|
import { __testing } from "../extensions/gentle-ai.ts";
|
|
8
|
-
import type { NativeReviewCli } from "../lib/native-review-cli.ts";
|
|
8
|
+
import type { NativeReviewCli, NativeReviewUnachievableLensCaptureArtifact, NativeReviewUnachievableLensCaptureRequest } from "../lib/native-review-cli.ts";
|
|
9
9
|
import { REVIEW_HOST_RELAY_FAILURE, REVIEW_HOST_RELAY_PI_TIMEOUT_ENV, REVIEW_HOST_RELAY_PI_TIMEOUT_MAX_MS, REVIEW_HOST_RELAY_SUBMISSION_MISSING_MESSAGE, REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE, ReviewHostRelayError, type ReviewHostRelayRequest } from "../lib/review-host-relay.ts";
|
|
10
|
-
import { decodeReviewStatusV3, type ReviewArtifactSubjectV2, type ReviewCaptureSubmissionV1, type ReviewCollectInputV3, type ReviewStatusV3 } from "../lib/review-integration-v2.ts";
|
|
10
|
+
import { decodeReviewNextTransitionV3, decodeReviewStatusV3, type ReviewArtifactSubjectV2, type ReviewCaptureSubmissionV1, type ReviewCollectInputV3, type ReviewStatusV3 } from "../lib/review-integration-v2.ts";
|
|
11
11
|
|
|
12
12
|
// One-slot capture routing: the host relay runs only when the selected
|
|
13
13
|
// provider-returned collect input carries the --materialize token. Every
|
|
@@ -142,25 +142,44 @@ function finalizeStatus(lineageId: string, inputs?: readonly ReviewCollectInputV
|
|
|
142
142
|
} as unknown as ReviewStatusV3;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
function providerRefuterRequiredStatus(lineageId: string): ReviewStatusV3 {
|
|
146
|
+
const refuter: ReviewCollectInputV3 = {
|
|
147
|
+
name: "provider_refuter",
|
|
148
|
+
schema: "https://gentle-ai.dev/schema/review/refuter/v1",
|
|
149
|
+
captureOperation: "review.capture-refuter",
|
|
150
|
+
arguments: bindingArguments(lineageId, "review-risk", 0),
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
...finalizeStatus(lineageId),
|
|
154
|
+
nextTransition: { kind: "collect", reasonCode: "provider_refuter_required", collect: { inputs: [refuter] } },
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
145
158
|
interface RoutingHarness {
|
|
146
159
|
statusQueue: ReviewStatusV3[];
|
|
147
|
-
statusCalls: Array<{ cwd: string; lineageId?: string }>;
|
|
160
|
+
statusCalls: Array<{ cwd: string; lineageId?: string; agent?: "pi" }>;
|
|
161
|
+
unachievableCalls: NativeReviewUnachievableLensCaptureRequest[];
|
|
148
162
|
native: NativeReviewCli;
|
|
149
163
|
}
|
|
150
164
|
|
|
151
|
-
|
|
165
|
+
type UnachievableResponder = (request: NativeReviewUnachievableLensCaptureRequest) => Promise<NativeReviewUnachievableLensCaptureArtifact>;
|
|
166
|
+
|
|
167
|
+
// The declaration verb stays OPT-IN: an absent captureUnachievableLens is the invocation-adjacent capability gate, so every pre-existing failure test keeps its exact envelope through the unchanged fall-back.
|
|
168
|
+
function nativeHarness(statuses: readonly ReviewStatusV3[], unachievableResponder?: UnachievableResponder): RoutingHarness {
|
|
152
169
|
const harness: RoutingHarness = {
|
|
153
170
|
statusQueue: [...statuses],
|
|
154
171
|
statusCalls: [],
|
|
172
|
+
unachievableCalls: [],
|
|
155
173
|
native: undefined as unknown as NativeReviewCli,
|
|
156
174
|
};
|
|
157
175
|
harness.native = {
|
|
158
176
|
targetStatus: async (request) => {
|
|
159
|
-
harness.statusCalls.push({ cwd: request.cwd, ...(request.lineageId === undefined ? {} : { lineageId: request.lineageId }) });
|
|
177
|
+
harness.statusCalls.push({ cwd: request.cwd, ...(request.lineageId === undefined ? {} : { lineageId: request.lineageId }), ...(request.agent === undefined ? {} : { agent: request.agent }) });
|
|
160
178
|
const next = harness.statusQueue.shift();
|
|
161
179
|
if (next === undefined) throw new Error("status queue exhausted");
|
|
162
180
|
return next;
|
|
163
181
|
},
|
|
182
|
+
...(unachievableResponder === undefined ? {} : { captureUnachievableLens: async (request: NativeReviewUnachievableLensCaptureRequest) => { harness.unachievableCalls.push(request); return await unachievableResponder(request); } }),
|
|
164
183
|
};
|
|
165
184
|
return harness;
|
|
166
185
|
}
|
|
@@ -226,7 +245,7 @@ function prepared(request: ReviewHostRelayRequest) { return { request, promptByt
|
|
|
226
245
|
test("grouped capture forecasts once, reaches a four-reviewer barrier, and reconciles unclosed submissions", async (t) => {
|
|
227
246
|
t.after(() => __testing.setReviewHostRelayGroupRunnersForTesting());
|
|
228
247
|
const cwd = repository(t), lineageId = "relay-group", inputs = groupInputs(lineageId), lenses = inputs.map((input) => input.arguments.find((argument) => argument.name === "lens")!.value);
|
|
229
|
-
const harness = nativeHarness([finalizeStatus(lineageId, inputs), finalizeStatus(lineageId, inputs), ...inputs.map((_input, index) => finalizeStatus(lineageId, index === 1 ? [...inputs.slice(index), { name: "unrelated", schema: "example", captureOperation: "external.example", arguments: [] }] : inputs.slice(index))),
|
|
248
|
+
const harness = nativeHarness([finalizeStatus(lineageId, inputs), finalizeStatus(lineageId, inputs), ...inputs.map((_input, index) => finalizeStatus(lineageId, index === 1 ? [...inputs.slice(index), { name: "unrelated", schema: "example", captureOperation: "external.example", arguments: [] }] : inputs.slice(index))), providerRefuterRequiredStatus(lineageId)]);
|
|
230
249
|
let ready!: () => void;
|
|
231
250
|
const allStarted = new Promise<void>((resolve) => { ready = resolve; });
|
|
232
251
|
const releases = new Map<string, () => void>(), completed: string[] = [], submitted: string[] = [];
|
|
@@ -249,7 +268,9 @@ test("grouped capture forecasts once, reaches a four-reviewer barrier, and recon
|
|
|
249
268
|
assert.deepEqual(completed, [...lenses].reverse(), "reviewer completion order is not submission order");
|
|
250
269
|
assert.deepEqual(submitted, lenses);
|
|
251
270
|
assert.deepEqual({ outcome: result.outcome, statusCalls: harness.statusCalls.length, providerAction: result.provider_action }, { outcome: "native-reviewer-group-status-reconciled", statusCalls: 7, providerAction: "stop" });
|
|
252
|
-
assert.equal(
|
|
271
|
+
assert.equal(harness.statusCalls.at(-1)?.agent, "pi", "post-last-capture reconciliation preserves the Pi host runtime");
|
|
272
|
+
assert.equal((result.next_transition as { kind?: string; reasonCode?: string } | undefined)?.kind, "collect");
|
|
273
|
+
assert.equal((result.next_transition as { kind?: string; reasonCode?: string } | undefined)?.reasonCode, "provider_refuter_required");
|
|
253
274
|
});
|
|
254
275
|
|
|
255
276
|
test("captured v5 STATUS accepts capture-phase Pn when authority has advanced to Rn at the forecast boundary", async (t) => {
|
|
@@ -465,6 +486,7 @@ test("a submission whose outcome is genuinely indeterminate still reconciles thr
|
|
|
465
486
|
assert.deepEqual(result.failure, { kind: "submission-refused", stage: "submit", exit_code: null, timed_out: true, elapsed_ms: 120_004, timeout_ms: 120_000 });
|
|
466
487
|
assert.match(String(result.reason), /exceeded its 120000ms bound/);
|
|
467
488
|
assert.equal(harness.statusCalls.length, 2, "an indeterminate submission reconciles exactly once through STATUS");
|
|
489
|
+
assert.equal(harness.statusCalls.at(-1)?.agent, "pi", "ordinary host-relay reconciliation preserves the Pi host runtime");
|
|
468
490
|
});
|
|
469
491
|
|
|
470
492
|
test("a relay timeout reports its one-slot measurements and no auto-follow", async (t) => {
|
|
@@ -490,6 +512,360 @@ test("a relay timeout reports its one-slot measurements and no auto-follow", asy
|
|
|
490
512
|
assert.equal(harness.statusCalls.length, 1);
|
|
491
513
|
});
|
|
492
514
|
|
|
515
|
+
// gentle-pi#638: the typed stop a provider renders after a host declared a
|
|
516
|
+
// slot unachievable, built through the production decoder so the routing
|
|
517
|
+
// tests exercise the same typed shape the extension consumes.
|
|
518
|
+
function unachievableStopStatus(lineageId: string, lens: ReviewArtifactSubjectV2["lens"], order: number, reason = "relay_transport_bound_exceeded"): ReviewStatusV3 {
|
|
519
|
+
const subjectHash = `sha256:${String(order).repeat(64)}`;
|
|
520
|
+
const repositoryContext = `rctx1_${"e".repeat(64)}`;
|
|
521
|
+
const rawTransition = {
|
|
522
|
+
kind: "stop",
|
|
523
|
+
reason_code: "unachievable_lens_slot",
|
|
524
|
+
unachievable_lens_slots: [
|
|
525
|
+
{
|
|
526
|
+
lens,
|
|
527
|
+
selected_order: order,
|
|
528
|
+
subject_hash: subjectHash,
|
|
529
|
+
reason,
|
|
530
|
+
withdraw: {
|
|
531
|
+
operation: "review.capture-unachievable",
|
|
532
|
+
command: `gentle-ai review capture-unachievable --lineage=${lineageId} --expected-revision=${SHA} --target=${SHA} --repository-context=${repositoryContext} --request-hash=${subjectHash} --withdraw=true`,
|
|
533
|
+
arguments: [
|
|
534
|
+
{ name: "lineage", value: lineageId, token: `--lineage=${lineageId}` },
|
|
535
|
+
{ name: "expected-revision", value: SHA, token: `--expected-revision=${SHA}` },
|
|
536
|
+
{ name: "target", value: SHA, token: `--target=${SHA}` },
|
|
537
|
+
{ name: "repository-context", value: repositoryContext, token: `--repository-context=${repositoryContext}` },
|
|
538
|
+
{ name: "request-hash", value: subjectHash, token: `--request-hash=${subjectHash}` },
|
|
539
|
+
{ name: "withdraw", value: "true", token: "--withdraw=true" },
|
|
540
|
+
],
|
|
541
|
+
binding: { lineage_id: lineageId, revision: SHA, target_identity: SHA, repository_context: repositoryContext },
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
],
|
|
545
|
+
};
|
|
546
|
+
return {
|
|
547
|
+
...finalizeStatus(lineageId),
|
|
548
|
+
nextTransition: decodeReviewNextTransitionV3(rawTransition),
|
|
549
|
+
raw: { schema: "gentle-ai.review-integration.status/v5", action: "stop", lineage_id: lineageId, target_identity: SHA, next_transition: rawTransition },
|
|
550
|
+
} as unknown as ReviewStatusV3;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function unachievableArtifact(lineageId: string, lens: ReviewArtifactSubjectV2["lens"], order: number, reason = "relay_transport_bound_exceeded"): NativeReviewUnachievableLensCaptureArtifact {
|
|
554
|
+
return { schema: "gentle-ai.review-capture-unachievable/v1", lineageId, targetIdentity: SHA, lens, selectedOrder: order, reason, recorded: true };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// gentle-pi#638: a deterministic pi timeout is declared unachievable through
|
|
558
|
+
// the native verb, and one bound STATUS re-query renders the typed stop with
|
|
559
|
+
// its withdraw binding instead of reoffering the same slot.
|
|
560
|
+
test("a deterministic pi timeout declares the slot unachievable and renders the typed stop", async (t) => {
|
|
561
|
+
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
562
|
+
const cwd = repository(t);
|
|
563
|
+
const lineageId = "relay-lineage";
|
|
564
|
+
const input = relayCollectInput(lineageId, "review-risk", 0);
|
|
565
|
+
const stop = unachievableStopStatus(lineageId, "review-risk", 0);
|
|
566
|
+
const harness = nativeHarness([finalizeStatus(lineageId, [input]), stop], () => Promise.resolve(unachievableArtifact(lineageId, "review-risk", 0)));
|
|
567
|
+
__testing.setReviewHostRelayRunnerForTesting(async () => {
|
|
568
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
const result = await runCapture(cwd, harness, lineageId);
|
|
572
|
+
|
|
573
|
+
assert.equal(result.status, "blocked");
|
|
574
|
+
assert.equal(result.outcome, "unachievable-lens-slot-declared");
|
|
575
|
+
assert.deepEqual(result.failure, { kind: "pi-timed-out", stage: "pi", exit_code: null, timed_out: true, elapsed_ms: 2_256_004, timeout_ms: 2_256_000 });
|
|
576
|
+
assert.deepEqual(harness.unachievableCalls, [{ cwd: realpathSync(cwd), lineageId, targetIdentity: SHA, expectedRevision: SHA, requestHash: `sha256:${"0".repeat(64)}`, reason: "relay_transport_bound_exceeded", detail: "killed after 2256004ms against a 2256000ms relay bound", repositoryContext: `rctx1_${"e".repeat(64)}` }]);
|
|
577
|
+
assert.deepEqual(result.declaration, { lens: "review-risk", selected_order: 0, subject_hash: `sha256:${"0".repeat(64)}`, reason: "relay_transport_bound_exceeded" });
|
|
578
|
+
assert.deepEqual(result.result, stop.raw);
|
|
579
|
+
const slots = result.unachievable_lens_slots as Array<{ lens: string; subject_hash: string; withdraw: string }>;
|
|
580
|
+
assert.equal(slots.length, 1);
|
|
581
|
+
assert.equal(slots[0]!.lens, "review-risk");
|
|
582
|
+
assert.equal(slots[0]!.subject_hash, `sha256:${"0".repeat(64)}`);
|
|
583
|
+
assert.match(slots[0]!.withdraw, /capture-unachievable.*--withdraw=true/);
|
|
584
|
+
assert.match(String(result.next_action), /withdraw/);
|
|
585
|
+
assert.equal(result.mutation_performed, true);
|
|
586
|
+
// gentle-pi#822: the native declaration was recorded, so the flow reports the mutation as committed, not none.
|
|
587
|
+
assert.equal(result.mutation_outcome, "committed");
|
|
588
|
+
assert.equal(harness.statusCalls.length, 2, "one selection STATUS plus exactly one bound re-query");
|
|
589
|
+
assert.equal(harness.statusCalls.at(-1)?.agent, "pi", "the bound re-query preserves the Pi host runtime");
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// gentle-pi#638: generic admission rejections describe the submitted reviewer
|
|
593
|
+
// bytes, not a deterministic failure of the provider-bound slot. A fresh
|
|
594
|
+
// reviewer can repair malformed JSON or a binding mismatch, so both retain the
|
|
595
|
+
// ordinary exact-reoffer path and must never declare the slot unachievable.
|
|
596
|
+
test("repairable submission refusals retain the exact-slot retry path", async (t) => {
|
|
597
|
+
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
598
|
+
const cwd = repository(t);
|
|
599
|
+
const lineageId = "relay-lineage";
|
|
600
|
+
for (const refusal of [
|
|
601
|
+
"Error: reviewer payload contains no complete JSON object [invalid_request]\n",
|
|
602
|
+
"Error: reviewer artifact admission binding_mismatch [invalid_request]\n",
|
|
603
|
+
]) {
|
|
604
|
+
const input = relayCollectInput(lineageId, "review-reliability", 0);
|
|
605
|
+
const harness = nativeHarness([finalizeStatus(lineageId, [input])], async () => {
|
|
606
|
+
throw new Error("a repairable refusal must not call capture-unachievable");
|
|
607
|
+
});
|
|
608
|
+
__testing.setReviewHostRelayRunnerForTesting(async () => {
|
|
609
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit", refusal.trim(), { exitCode: 1, stderr: refusal, mutationOutcome: "none" });
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
const result = await runCapture(cwd, harness, lineageId);
|
|
613
|
+
|
|
614
|
+
assert.equal(result.outcome, "pi-host-relay-transport-failure");
|
|
615
|
+
assert.equal(result.mutation_performed, false);
|
|
616
|
+
assert.equal(result.mutation_outcome, "none");
|
|
617
|
+
assert.match(String(result.next_action), /fresh STATUS/);
|
|
618
|
+
assert.deepEqual(harness.unachievableCalls, []);
|
|
619
|
+
assert.equal(harness.statusCalls.length, 1, "a repairable non-mutation needs no reconciliation before the provider reoffers it");
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// gentle-pi#822: a stop carrying slots that do not match the identity this
|
|
624
|
+
// session declared is a reconciliation failure — never a success rendering
|
|
625
|
+
// someone else's withdraw command.
|
|
626
|
+
test("a stop whose slots do not match the declared identity reports a reconciliation failure", async (t) => {
|
|
627
|
+
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
628
|
+
const cwd = repository(t);
|
|
629
|
+
const lineageId = "relay-lineage";
|
|
630
|
+
const input = relayCollectInput(lineageId, "review-risk", 0);
|
|
631
|
+
const foreignStop = unachievableStopStatus(lineageId, "review-risk", 1);
|
|
632
|
+
const harness = nativeHarness([finalizeStatus(lineageId, [input]), foreignStop], () => Promise.resolve(unachievableArtifact(lineageId, "review-risk", 0)));
|
|
633
|
+
__testing.setReviewHostRelayRunnerForTesting(async () => {
|
|
634
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
const result = await runCapture(cwd, harness, lineageId);
|
|
638
|
+
|
|
639
|
+
assert.equal(result.status, "blocked");
|
|
640
|
+
assert.equal(result.outcome, "unachievable-lens-declaration-reconciliation-failed");
|
|
641
|
+
assert.deepEqual(result.declaration, { lens: "review-risk", selected_order: 0, subject_hash: `sha256:${"0".repeat(64)}`, reason: "relay_transport_bound_exceeded" });
|
|
642
|
+
assert.equal(result.unachievable_lens_slots, undefined, "a foreign slot is never exposed as this session's withdraw command");
|
|
643
|
+
const reconciliationFailure = result.reconciliation_failure as { outcome?: string; reason?: string } | undefined;
|
|
644
|
+
assert.equal(reconciliationFailure?.outcome, "unachievable-lens-slot-declaration-unmatched");
|
|
645
|
+
assert.match(String(reconciliationFailure?.reason), /no unachievable_lens_slots entry matches the declared slot identity/);
|
|
646
|
+
assert.equal(result.mutation_performed, true);
|
|
647
|
+
assert.equal(result.mutation_outcome, "committed");
|
|
648
|
+
assert.match(String(result.next_action), /refused the unachievable declaration/);
|
|
649
|
+
assert.equal(harness.statusCalls.length, 2, "the bound re-query still runs exactly once");
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// gentle-pi#822: with several declared entries on the stop, only the matching
|
|
653
|
+
// identity is exposed — never the withdraw commands of other runs.
|
|
654
|
+
test("a stop carrying several declared slots exposes only the one this session declared", async (t) => {
|
|
655
|
+
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
656
|
+
const cwd = repository(t);
|
|
657
|
+
const lineageId = "relay-lineage";
|
|
658
|
+
const input = relayCollectInput(lineageId, "review-risk", 0);
|
|
659
|
+
const stop = unachievableStopStatus(lineageId, "review-risk", 0);
|
|
660
|
+
const foreign = unachievableStopStatus(lineageId, "review-reliability", 2).nextTransition?.unachievableLensSlots?.[0];
|
|
661
|
+
assert.ok(foreign !== undefined);
|
|
662
|
+
stop.nextTransition = { ...stop.nextTransition!, unachievableLensSlots: [...stop.nextTransition!.unachievableLensSlots!, foreign] };
|
|
663
|
+
const harness = nativeHarness([finalizeStatus(lineageId, [input]), stop], () => Promise.resolve(unachievableArtifact(lineageId, "review-risk", 0)));
|
|
664
|
+
__testing.setReviewHostRelayRunnerForTesting(async () => {
|
|
665
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
const result = await runCapture(cwd, harness, lineageId);
|
|
669
|
+
|
|
670
|
+
assert.equal(result.outcome, "unachievable-lens-slot-declared");
|
|
671
|
+
const slots = result.unachievable_lens_slots as Array<{ lens: string; selected_order: number; subject_hash: string }>;
|
|
672
|
+
assert.equal(slots.length, 1, "only the declared entry is exposed");
|
|
673
|
+
assert.equal(slots[0]!.lens, "review-risk");
|
|
674
|
+
assert.equal(slots[0]!.selected_order, 0);
|
|
675
|
+
assert.equal(slots[0]!.subject_hash, `sha256:${"0".repeat(64)}`);
|
|
676
|
+
assert.equal(result.mutation_outcome, "committed");
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
// gentle-pi#822 (CodeRabbit finding): success must be proven by the unachievable_lens_slot stop itself — a bound STATUS that comes back with a collect reoffer instead of the stop is a reconciliation failure, never a silent success with no withdraw command.
|
|
680
|
+
test("a bound STATUS that reoffers the collect transition instead of the stop reports a reconciliation failure", async (t) => {
|
|
681
|
+
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
682
|
+
const cwd = repository(t);
|
|
683
|
+
const lineageId = "relay-lineage";
|
|
684
|
+
const input = relayCollectInput(lineageId, "review-risk", 0);
|
|
685
|
+
const harness = nativeHarness([finalizeStatus(lineageId, [input]), finalizeStatus(lineageId, [input])], () => Promise.resolve(unachievableArtifact(lineageId, "review-risk", 0)));
|
|
686
|
+
__testing.setReviewHostRelayRunnerForTesting(async () => {
|
|
687
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
const result = await runCapture(cwd, harness, lineageId);
|
|
691
|
+
|
|
692
|
+
assert.equal(result.status, "blocked");
|
|
693
|
+
assert.equal(result.outcome, "unachievable-lens-declaration-reconciliation-failed");
|
|
694
|
+
assert.equal(result.unachievable_lens_slots, undefined, "no stop means no withdraw command to expose");
|
|
695
|
+
const reconciliationFailure = result.reconciliation_failure as { outcome?: string; reason?: string };
|
|
696
|
+
assert.equal(reconciliationFailure?.outcome, "unachievable-lens-slot-declaration-unmatched");
|
|
697
|
+
assert.match(String(reconciliationFailure?.reason), /did not return the unachievable_lens_slot stop/);
|
|
698
|
+
assert.equal(result.mutation_performed, true, "the declaration was recorded");
|
|
699
|
+
assert.equal(result.mutation_outcome, "committed");
|
|
700
|
+
assert.equal(harness.statusCalls.length, 2, "the bound re-query still runs exactly once");
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
// gentle-pi#822 (CodeRabbit finding): the same proof requirement covers every STATUS shape that is not the stop — a foreign stop reason code or no transition at all.
|
|
704
|
+
test("a bound STATUS stop with a foreign reason code or no transition reports a reconciliation failure", async (t) => {
|
|
705
|
+
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
706
|
+
const cwd = repository(t);
|
|
707
|
+
const lineageId = "relay-lineage";
|
|
708
|
+
const input = relayCollectInput(lineageId, "review-risk", 0);
|
|
709
|
+
const responder = () => Promise.resolve(unachievableArtifact(lineageId, "review-risk", 0));
|
|
710
|
+
const relayFailure = async () => {
|
|
711
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
const foreignStop = { ...finalizeStatus(lineageId), nextTransition: { kind: "stop", reasonCode: "rdd_disabled" } } as unknown as ReviewStatusV3;
|
|
715
|
+
const foreignHarness = nativeHarness([finalizeStatus(lineageId, [input]), foreignStop], responder);
|
|
716
|
+
__testing.setReviewHostRelayRunnerForTesting(relayFailure);
|
|
717
|
+
const foreign = await runCapture(cwd, foreignHarness, lineageId);
|
|
718
|
+
|
|
719
|
+
assert.equal(foreign.outcome, "unachievable-lens-declaration-reconciliation-failed");
|
|
720
|
+
assert.equal(foreign.unachievable_lens_slots, undefined);
|
|
721
|
+
const foreignFailure = foreign.reconciliation_failure as { outcome?: string; reason?: string };
|
|
722
|
+
assert.match(String(foreignFailure?.reason), /did not return the unachievable_lens_slot stop/);
|
|
723
|
+
assert.equal(foreign.mutation_performed, true);
|
|
724
|
+
assert.equal(foreign.mutation_outcome, "committed");
|
|
725
|
+
|
|
726
|
+
const bareHarness = nativeHarness([finalizeStatus(lineageId, [input]), finalizeStatus(lineageId)], responder);
|
|
727
|
+
__testing.setReviewHostRelayRunnerForTesting(relayFailure);
|
|
728
|
+
const bare = await runCapture(cwd, bareHarness, lineageId);
|
|
729
|
+
|
|
730
|
+
assert.equal(bare.outcome, "unachievable-lens-declaration-reconciliation-failed");
|
|
731
|
+
assert.equal(bare.unachievable_lens_slots, undefined);
|
|
732
|
+
const bareFailure = bare.reconciliation_failure as { outcome?: string; reason?: string };
|
|
733
|
+
assert.match(String(bareFailure?.reason), /did not return the unachievable_lens_slot stop/);
|
|
734
|
+
assert.equal(bare.mutation_outcome, "committed");
|
|
735
|
+
assert.equal(bareHarness.statusCalls.length, 2);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
// gentle-pi#822: STATUS for one lineage renders only that lineage's withdraw
|
|
739
|
+
// command as the hint; a request for an unlisted lineage omits the hint rather
|
|
740
|
+
// than surfacing a potentially unrelated withdraw command.
|
|
741
|
+
test("an unachievable stop renders the withdraw hint only for the requested lineage", async (t) => {
|
|
742
|
+
const cwd = repository(t);
|
|
743
|
+
const matchedHarness = nativeHarness([unachievableStopStatus("other-lineage", "review-risk", 0)]);
|
|
744
|
+
const matched = (await __testing.executeReviewControllerOperation({ operation: "status", lineageId: "other-lineage" }, cwd, matchedHarness.native)) as Record<string, unknown>;
|
|
745
|
+
assert.match(String(matched.hint), /capture-unachievable.*--withdraw=true/);
|
|
746
|
+
assert.equal(matched.requested_lineage_id, "other-lineage");
|
|
747
|
+
|
|
748
|
+
const unmatchedHarness = nativeHarness([unachievableStopStatus("other-lineage", "review-risk", 0)]);
|
|
749
|
+
const unmatched = (await __testing.executeReviewControllerOperation({ operation: "status", lineageId: "relay-lineage" }, cwd, unmatchedHarness.native)) as Record<string, unknown>;
|
|
750
|
+
assert.equal(unmatched.hint, undefined, "no hint when no slot matches the requested lineage");
|
|
751
|
+
assert.equal(unmatched.requested_lineage_id, "relay-lineage");
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
// The fail-open capability gate: an older binary that refuses the whole verb
|
|
755
|
+
// keeps today's transport-failure envelope untouched.
|
|
756
|
+
test("an unknown-verb refusal falls back to today's transport-failure behavior", async (t) => {
|
|
757
|
+
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
758
|
+
const cwd = repository(t);
|
|
759
|
+
const lineageId = "relay-lineage";
|
|
760
|
+
const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-risk", 0)])], async () => {
|
|
761
|
+
throw Object.assign(new Error("native command returned empty output"), { diagnostics: { stderr: 'Error: unknown review command "capture-unachievable"\n' } });
|
|
762
|
+
});
|
|
763
|
+
__testing.setReviewHostRelayRunnerForTesting(async () => {
|
|
764
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
const result = await runCapture(cwd, harness, lineageId);
|
|
768
|
+
|
|
769
|
+
assert.equal(result.status, "blocked");
|
|
770
|
+
assert.equal(result.outcome, "pi-host-relay-timeout");
|
|
771
|
+
assert.equal(harness.unachievableCalls.length, 1, "the verb was attempted exactly once");
|
|
772
|
+
assert.equal(harness.statusCalls.length, 1, "the fall-back performs no bound STATUS re-query");
|
|
773
|
+
assert.match(String(result.next_action), new RegExp(`${REVIEW_HOST_RELAY_PI_TIMEOUT_ENV}=<milliseconds>`));
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
// Every non-capability declaration failure — here a typed binding-mismatch
|
|
777
|
+
// refusal — is surfaced with both failures, never hidden behind the relay
|
|
778
|
+
// failure it followed.
|
|
779
|
+
test("a refused declaration surfaces both failures instead of the transport fall-back", async (t) => {
|
|
780
|
+
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
781
|
+
const cwd = repository(t);
|
|
782
|
+
const lineageId = "relay-lineage";
|
|
783
|
+
const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-risk", 0)])], async () => {
|
|
784
|
+
throw Object.assign(new Error("review capture-unachievable binding does not match the current reviewing authority"), { diagnostics: { stderr: "Error: review capture-unachievable binding does not match [invalid_request]" } });
|
|
785
|
+
});
|
|
786
|
+
__testing.setReviewHostRelayRunnerForTesting(async () => {
|
|
787
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
const result = await runCapture(cwd, harness, lineageId);
|
|
791
|
+
|
|
792
|
+
assert.equal(result.status, "blocked");
|
|
793
|
+
assert.equal(result.outcome, "unachievable-lens-declaration-failed");
|
|
794
|
+
assert.equal(result.mutation_performed, false);
|
|
795
|
+
assert.equal(result.mutation_outcome, "none");
|
|
796
|
+
assert.deepEqual(result.failure, { kind: "pi-timed-out", stage: "pi", exit_code: null, timed_out: true, elapsed_ms: 2_256_004, timeout_ms: 2_256_000 });
|
|
797
|
+
assert.ok(result.declaration_failure, "the declaration refusal rides the envelope");
|
|
798
|
+
assert.equal(harness.statusCalls.length, 1, "a refused declaration performs no bound STATUS re-query");
|
|
799
|
+
assert.match(String(result.next_action), /fresh STATUS/);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
// gentle-pi#822 (outside-diff finding): the declaration failure's own envelope carries the mutation truth — a failure AFTER the provider recorded the declaration reports committed, never a hardcoded none.
|
|
803
|
+
test("a declaration failure whose envelope already committed propagates its mutation state", async (t) => {
|
|
804
|
+
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
805
|
+
const cwd = repository(t);
|
|
806
|
+
const lineageId = "relay-lineage";
|
|
807
|
+
const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-risk", 0)])], async () => {
|
|
808
|
+
throw Object.assign(new Error("review capture-unachievable failed after recording"), { failureEnvelope: { raw: { code: "invalid_request", message: "review capture-unachievable failed after recording" }, mutationOutcome: "committed" } });
|
|
809
|
+
});
|
|
810
|
+
__testing.setReviewHostRelayRunnerForTesting(async () => {
|
|
811
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
const result = await runCapture(cwd, harness, lineageId);
|
|
815
|
+
|
|
816
|
+
assert.equal(result.status, "blocked");
|
|
817
|
+
assert.equal(result.outcome, "unachievable-lens-declaration-failed");
|
|
818
|
+
assert.equal(result.mutation_performed, true);
|
|
819
|
+
assert.equal(result.mutation_outcome, "committed");
|
|
820
|
+
assert.ok(result.declaration_failure, "the declaration failure still rides the envelope");
|
|
821
|
+
assert.equal(harness.statusCalls.length, 1, "a committed outcome needs no reconciliation re-query");
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
// gentle-pi#822 (outside-diff finding): an unknown declaration outcome is proven or disproven by one bound STATUS re-query without ever changing the failure outcome.
|
|
825
|
+
test("an unknown declaration outcome is proven committed by one bound STATUS re-query", async (t) => {
|
|
826
|
+
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
827
|
+
const cwd = repository(t);
|
|
828
|
+
const lineageId = "relay-lineage";
|
|
829
|
+
const input = relayCollectInput(lineageId, "review-risk", 0);
|
|
830
|
+
const stop = unachievableStopStatus(lineageId, "review-risk", 0);
|
|
831
|
+
const harness = nativeHarness([finalizeStatus(lineageId, [input]), stop], async () => {
|
|
832
|
+
throw Object.assign(new Error("native command timed out while recording the declaration"), { failureEnvelope: { raw: { code: "deadline_exceeded", message: "native command timed out" }, mutationOutcome: "unknown" } });
|
|
833
|
+
});
|
|
834
|
+
__testing.setReviewHostRelayRunnerForTesting(async () => {
|
|
835
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
const result = await runCapture(cwd, harness, lineageId);
|
|
839
|
+
|
|
840
|
+
assert.equal(result.status, "blocked");
|
|
841
|
+
assert.equal(result.outcome, "unachievable-lens-declaration-failed", "the failure outcome never changes");
|
|
842
|
+
assert.equal(result.mutation_performed, true);
|
|
843
|
+
assert.equal(result.mutation_outcome, "committed");
|
|
844
|
+
assert.equal(harness.statusCalls.length, 2, "one selection STATUS plus exactly one bound reconciliation re-query");
|
|
845
|
+
assert.equal(harness.statusCalls.at(-1)?.agent, "pi", "the reconciliation re-query preserves the Pi host runtime");
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
// gentle-pi#822 (outside-diff finding): a malformed declaration error with no envelope at all keeps the conservative none outcome and performs no re-query.
|
|
849
|
+
test("a malformed declaration error without an envelope keeps mutation none", async (t) => {
|
|
850
|
+
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
851
|
+
const cwd = repository(t);
|
|
852
|
+
const lineageId = "relay-lineage";
|
|
853
|
+
const harness = nativeHarness([finalizeStatus(lineageId, [relayCollectInput(lineageId, "review-risk", 0)])], async () => {
|
|
854
|
+
throw new TypeError("cannot read properties of undefined (reading 'detail')");
|
|
855
|
+
});
|
|
856
|
+
__testing.setReviewHostRelayRunnerForTesting(async () => {
|
|
857
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { exitCode: null, timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
const result = await runCapture(cwd, harness, lineageId);
|
|
861
|
+
|
|
862
|
+
assert.equal(result.status, "blocked");
|
|
863
|
+
assert.equal(result.outcome, "unachievable-lens-declaration-failed");
|
|
864
|
+
assert.equal(result.mutation_performed, false);
|
|
865
|
+
assert.equal(result.mutation_outcome, "none");
|
|
866
|
+
assert.equal(harness.statusCalls.length, 1, "no envelope means no unknown outcome and no re-query");
|
|
867
|
+
});
|
|
868
|
+
|
|
493
869
|
test("a non-timeout transport failure keeps its generic continuation and now carries its measurements", async (t) => {
|
|
494
870
|
t.after(() => __testing.setReviewHostRelayRunnerForTesting());
|
|
495
871
|
const cwd = repository(t);
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
resolveReviewHostRelayPiTimeoutMs,
|
|
19
19
|
resolveReviewHostRelaySubmission,
|
|
20
20
|
reviewHostRelaySlots,
|
|
21
|
+
reviewHostRelayUnachievableDetail,
|
|
22
|
+
reviewHostRelayUnachievableReason,
|
|
21
23
|
prepareReviewHostRelaySlot,
|
|
22
24
|
runReviewHostRelayReviewerGroup,
|
|
23
25
|
runReviewHostRelaySlot,
|
|
@@ -779,6 +781,33 @@ test("refusal classification distinguishes unknown-flag, handshake, and other",
|
|
|
779
781
|
assert.equal(classifyReviewHostRelayRefusal("some unrelated explosion"), "other");
|
|
780
782
|
});
|
|
781
783
|
|
|
784
|
+
// gentle-pi#638: only a reviewer killed by the scaled relay bound is proven
|
|
785
|
+
// deterministic for this exact slot. Generic admission refusals are repairable
|
|
786
|
+
// by a fresh reviewer and retain the ordinary exact-reoffer behavior.
|
|
787
|
+
test("the unachievable predicate names only deterministic slot failures", () => {
|
|
788
|
+
const killed = new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "pi reviewer subprocess exceeded the relay bound", { timedOut: true, elapsedMs: 2_256_004, timeoutMs: 2_256_000 });
|
|
789
|
+
assert.equal(reviewHostRelayUnachievableReason(killed), "relay_transport_bound_exceeded");
|
|
790
|
+
assert.equal(reviewHostRelayUnachievableDetail(killed), "killed after 2256004ms against a 2256000ms relay bound");
|
|
791
|
+
|
|
792
|
+
for (const refusal of ["reviewer payload contains no complete JSON object", "reviewer artifact admission binding_mismatch"]) {
|
|
793
|
+
const admitted = new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit", `${refusal} [invalid_request]`, { exitCode: 1, mutationOutcome: "none" });
|
|
794
|
+
assert.equal(reviewHostRelayUnachievableReason(admitted), undefined, `${refusal} is repairable by a fresh reviewer`);
|
|
795
|
+
assert.equal(reviewHostRelayUnachievableDetail(admitted), undefined);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const unknownOutcome = new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit", "gentle-ai capture submission exceeded its bound", { timedOut: true });
|
|
799
|
+
assert.equal(reviewHostRelayUnachievableReason(unknownOutcome), undefined, "an unknown mutation outcome is never deterministic");
|
|
800
|
+
assert.equal(reviewHostRelayUnachievableDetail(unknownOutcome), undefined);
|
|
801
|
+
|
|
802
|
+
for (const kind of [REVIEW_HOST_RELAY_FAILURE.PI_FAILED, REVIEW_HOST_RELAY_FAILURE.PI_LAUNCH_FAILED, REVIEW_HOST_RELAY_FAILURE.PI_EMPTY_OUTPUT, REVIEW_HOST_RELAY_FAILURE.MATERIALIZE_FAILED, REVIEW_HOST_RELAY_FAILURE.EMPTY_PROMPT, REVIEW_HOST_RELAY_FAILURE.RELAY_UNAVAILABLE, REVIEW_HOST_RELAY_FAILURE.HANDSHAKE_REFUSED, REVIEW_HOST_RELAY_FAILURE.SUBMISSION_CONTRACT_MISMATCH]) {
|
|
803
|
+
assert.equal(reviewHostRelayUnachievableReason(new ReviewHostRelayError(kind, "pi", "transient")), undefined, `${kind} stays transient`);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const unmeasured = new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT, "pi", "killed at the bound");
|
|
807
|
+
assert.equal(reviewHostRelayUnachievableReason(unmeasured), "relay_transport_bound_exceeded");
|
|
808
|
+
assert.equal(reviewHostRelayUnachievableDetail(unmeasured), undefined, "no measurements means no detail, never a fabricated one");
|
|
809
|
+
});
|
|
810
|
+
|
|
782
811
|
// ---------------------------------------------------------------------------
|
|
783
812
|
// Slot detection — the provider decides; nothing is inferred.
|
|
784
813
|
// ---------------------------------------------------------------------------
|
|
@@ -699,3 +699,47 @@ test("review-acknowledged/v1 is disjoint from every prior captured identity in b
|
|
|
699
699
|
assert.throws(() => decoder(clone(acknowledged)), `${name} must reject the acknowledged envelope`);
|
|
700
700
|
}
|
|
701
701
|
});
|
|
702
|
+
|
|
703
|
+
// Producer-shaped projection: gentle-ai d521b5e, target_status.go and
|
|
704
|
+
// last-event-closure.schema.json#/properties/escalation (not a live capture).
|
|
705
|
+
function escalatedStatusV7(): JsonObject {
|
|
706
|
+
const body = currentStatusFixture("status-v5-capture-result-submission.captured.json");
|
|
707
|
+
body.schema = "gentle-ai.review-integration.status/v7";
|
|
708
|
+
(body.authority as JsonObject).state = "escalated";
|
|
709
|
+
body.action = "stop";
|
|
710
|
+
body.replayability = "manual_action_required";
|
|
711
|
+
body.next_transition = { kind: "stop", reason_code: "escalated" };
|
|
712
|
+
delete body.forecast;
|
|
713
|
+
body.escalation = { cause: "unknown_causality", finding_ids: ["R1"] };
|
|
714
|
+
return body;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
test("v7 escalation is typed metadata and preserves native stop authority", () => {
|
|
718
|
+
const body = escalatedStatusV7();
|
|
719
|
+
const decoded = decodeReviewStatusV3(body);
|
|
720
|
+
assert.deepEqual(decoded.escalation, { cause: "unknown_causality", findingIds: ["R1"] });
|
|
721
|
+
delete body.escalation;
|
|
722
|
+
const older = decodeReviewStatusV3(body);
|
|
723
|
+
assert.deepEqual({ ...decoded, escalation: undefined, raw: undefined }, { ...older, escalation: undefined, raw: undefined });
|
|
724
|
+
assert.equal(decoded.action, "stop");
|
|
725
|
+
assert.equal(decoded.authority?.state, "escalated");
|
|
726
|
+
assert.deepEqual(decoded.nextTransition, { kind: "stop", reasonCode: "escalated" });
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
test("v7 escalation strictly decodes all causes and optional refuter evidence", () => {
|
|
730
|
+
for (const cause of ["unknown_causality", "insufficient_evidence", "missing_refuter_outcome", "targeted_validator_rejected", "correction_budget_exceeded", "unresolved_severe_findings"]) {
|
|
731
|
+
const body = escalatedStatusV7();
|
|
732
|
+
const rows = ["corroborated", "refuted", "inconclusive"].map((outcome) => ({ finding_id: "R2", outcome, proof: "observed evidence" }));
|
|
733
|
+
body.escalation = { cause, finding_ids: [], refuter_outcomes: rows };
|
|
734
|
+
assert.deepEqual(decodeReviewStatusV3(body).escalation, { cause, findingIds: [], refuterOutcomes: rows.map(({ finding_id, ...row }) => ({ findingId: finding_id, ...row })) });
|
|
735
|
+
}
|
|
736
|
+
for (const escalation of [null, undefined, [], {}, { cause: "future", finding_ids: [] }, { cause: "unknown_causality" }, { cause: "unknown_causality", finding_ids: [""] }, { cause: "unknown_causality", finding_ids: "R1" }, { cause: "unknown_causality", finding_ids: [], extra: true }]) {
|
|
737
|
+
assert.throws(() => decodeReviewStatusV3({ ...escalatedStatusV7(), escalation }), /status\.escalation/);
|
|
738
|
+
}
|
|
739
|
+
for (const refuter_outcomes of [null, undefined, {}, [null], [{}], [{ finding_id: "R1", outcome: "future", proof: "proof" }], [{ finding_id: "", outcome: "refuted", proof: "proof" }], [{ finding_id: "R1", outcome: "refuted", proof: "" }], [{ finding_id: "R1", outcome: "refuted", proof: "proof", extra: true }]]) {
|
|
740
|
+
assert.throws(() => decodeReviewStatusV3({ ...escalatedStatusV7(), escalation: { cause: "missing_refuter_outcome", finding_ids: ["R1"], refuter_outcomes } }), /refuter_outcomes/);
|
|
741
|
+
}
|
|
742
|
+
for (const version of [3, 5, 6]) {
|
|
743
|
+
assert.throws(() => decodeReviewStatusV3({ ...escalatedStatusV7(), schema: `gentle-ai.review-integration.status/v${version}` }), /not allowed/);
|
|
744
|
+
}
|
|
745
|
+
});
|