gentle-pi 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -79
- package/assets/agents/review-readability.md +0 -2
- package/assets/agents/review-reliability.md +0 -2
- package/assets/agents/review-resilience.md +0 -2
- package/assets/agents/review-risk.md +0 -2
- package/assets/agents/sdd-apply.md +5 -3
- package/assets/agents/sdd-explore.md +1 -0
- package/assets/agents/sdd-proposal.md +2 -0
- package/assets/agents/sdd-research.md +54 -0
- package/assets/agents/sdd-status.md +4 -4
- package/assets/agents/sdd-tasks.md +2 -3
- package/assets/agents/sdd-verify.md +21 -1
- package/assets/chains/sdd-full.chain.md +1 -1
- package/assets/chains/sdd-verify.chain.md +1 -1
- package/assets/orchestrator-delegation.md +56 -249
- package/assets/orchestrator-memory.md +2 -0
- package/assets/orchestrator.md +20 -40
- package/assets/sdd-orchestrator-workflow.md +30 -37
- package/assets/support/sdd-status-contract.md +7 -7
- package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
- package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
- package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
- package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
- package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
- package/docs/native-authority-architecture.md +9 -11
- package/docs/review-integration.md +27 -373
- package/extensions/ask-user-choice.ts +151 -0
- package/extensions/gentle-ai.ts +2349 -3785
- package/extensions/quiet-tools.ts +515 -32
- package/extensions/sdd-init.ts +4 -8
- package/lib/agent-home.ts +8 -0
- package/lib/gentle-ai-renderer.ts +70 -0
- package/lib/model-routing-authority.ts +133 -0
- package/lib/native-review-cli.ts +387 -940
- package/lib/opaque-pi-reviewer-adapter.ts +284 -0
- package/lib/provider-contract-bundle.ts +88 -6
- package/lib/review-candidate-view.ts +341 -132
- package/lib/review-host-relay.ts +324 -96
- package/lib/review-integration-v2.ts +868 -307
- package/lib/review-last-event-controller.ts +35 -0
- package/lib/sdd-preflight.ts +179 -68
- package/lib/sdd-status.ts +66 -111
- package/lib/terminal-theme.ts +1 -1
- package/package.json +83 -82
- package/runtime/gentle-ai-binary.mjs +1 -1
- package/runtime/native-review-cli.mjs +337 -890
- package/runtime/review-integration-v2.mjs +816 -255
- package/runtime/review-relay-contract.mjs +1 -1
- package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +4 -5
- package/scripts/gentle-ai-installer.mjs +75 -22
- package/scripts/maintainer/provider-relay-matrix.mjs +195 -11
- package/scripts/test-packed-runner.mjs +4 -7
- package/scripts/verify-package-files.mjs +23 -23
- package/skills/_shared/review-ledger-contract.md +14 -14
- package/skills/chained-pr/SKILL.md +3 -0
- package/skills/cognitive-doc-design/SKILL.md +1 -1
- package/skills/comment-writer/SKILL.md +1 -1
- package/skills/gentle-ai/SKILL.md +7 -74
- package/skills/issue-creation/SKILL.md +53 -93
- package/skills/judgment-day/SKILL.md +5 -7
- package/skills/rdd-defect-workflow/SKILL.md +3 -3
- package/skills/release/SKILL.md +3 -3
- package/skills/skill-registry/SKILL.md +1 -1
- package/skills/work-unit-commits/SKILL.md +3 -1
- package/tests/artifact-language.test.ts +24 -11
- package/tests/ask-user-choice.test.ts +264 -0
- package/tests/codegraph-tools.test.ts +3 -3
- package/tests/crosslane/cross-lane.mjs +15 -1168
- package/tests/delegated-key-learnings-contract.test.ts +8 -6
- package/tests/devbinary/native-review-parity.devtest.ts +167 -250
- package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
- package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
- package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
- package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
- package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
- package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
- package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
- package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
- package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
- package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
- package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
- package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -0
- package/tests/gentle-ai-binary.test.ts +83 -13
- package/tests/gentle-ai-dev-binary.test.ts +51 -228
- package/tests/gentle-ai-installer.test.ts +137 -47
- package/tests/gentle-ai.test.ts +821 -131
- package/tests/gentle-theme.test.ts +133 -0
- package/tests/issue-creation-skill.test.ts +103 -0
- package/tests/maintainer/provider-relay.maintest.ts +340 -4
- package/tests/model-routing-authority.test.ts +257 -0
- package/tests/native-review-capability-contract.test.ts +45 -1
- package/tests/native-review-cli.test.ts +586 -962
- package/tests/native-review-consent.test.ts +150 -66
- package/tests/native-review-parity-runtime.test.ts +80 -359
- package/tests/native-review-parity.test.ts +674 -925
- package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
- package/tests/orchestrator-budget.test.ts +132 -88
- package/tests/orchestrator-rdd-ownership.test.ts +103 -0
- package/tests/package-manifest.test.ts +152 -36
- package/tests/provider-contract-bundle.test.ts +76 -0
- package/tests/provider-contract-mirror.test.ts +19 -0
- package/tests/provider-defect-handoff.test.ts +15 -118
- package/tests/quiet-tool-rendering.test.ts +1055 -28
- package/tests/review-agent-end-preflight.test.ts +408 -0
- package/tests/review-authority-recovery-docs.test.ts +1 -2
- package/tests/review-candidate-view.test.ts +409 -3
- package/tests/review-compact-contract.test.ts +4 -30
- package/tests/review-contract-prompt.test.ts +139 -0
- package/tests/review-controller-lock-status.test.ts +2 -2
- package/tests/review-controller-native-recovery.test.ts +363 -1450
- package/tests/review-controller-native-routing.test.ts +1251 -5642
- package/tests/review-controller-retired-ops.test.ts +1 -1
- package/tests/review-controller-workspace-root.test.ts +222 -71
- package/tests/review-controller.test.ts +26 -816
- package/tests/review-corrected-finalize-binding.test.ts +112 -153
- package/tests/review-dispatch-hydration-gap.test.ts +1 -53
- package/tests/review-gate.test.ts +0 -45
- package/tests/review-host-relay-restart-parity.test.ts +360 -0
- package/tests/review-host-relay-routing.test.ts +328 -91
- package/tests/review-host-relay.test.ts +436 -14
- package/tests/review-integration-v2-forward.test.ts +623 -553
- package/tests/review-integration-v2.test.ts +239 -165
- package/tests/review-last-event-closure.test.ts +408 -0
- package/tests/review-ledger-contract.test.ts +97 -35
- package/tests/review-recovered-lineage-routing.test.ts +0 -47
- package/tests/review-relay-transport-agent.test.ts +119 -46
- package/tests/review-snapshot.test.ts +3 -2
- package/tests/runtime-harness.mjs +362 -196
- package/tests/sdd-agent-tools.test.ts +36 -0
- package/tests/sdd-preflight.test.ts +81 -15
- package/tests/sdd-status.test.ts +109 -110
- package/tests/skill-collision-prefixes.test.ts +5 -8
- package/tests/writer-edit-surface-scope.test.ts +366 -0
- package/themes/Gentleman-Cute.json +94 -0
- package/themes/Gentleman-Sexy.json +92 -0
- package/lib/git-commit-transaction.ts +0 -861
- package/runtime/git-commit-transaction.mjs +0 -862
- package/scripts/run-git-commit-transaction.mjs +0 -35
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
- package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
- package/tests/git-commit-transaction.test.ts +0 -530
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
- /package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/schemas/targeted-validator.schema.json +0 -0
package/lib/review-host-relay.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
// The thin Pi host relay (gentle-pi#311 P4; provider contract gentle-ai#3249).
|
|
2
2
|
//
|
|
3
3
|
// gentle-ai owns prompt materialization, role and schema selection, byte
|
|
4
|
-
// budgets, parsing, admission, immutable capture, retry
|
|
5
|
-
// accounting,
|
|
4
|
+
// budgets, parsing, admission, immutable capture, retry, correction
|
|
5
|
+
// accounting, and receipt state. This host boundary is
|
|
6
6
|
// intentionally narrow:
|
|
7
7
|
//
|
|
8
8
|
// 1. Run the exact provider-issued capture binding with `--agent pi
|
|
9
9
|
// --materialize` and take stdout as opaque prompt BYTES, verbatim.
|
|
10
|
-
// 2.
|
|
11
|
-
//
|
|
12
|
-
// stdout as raw final bytes. Model/provider/profile selection
|
|
13
|
-
// user-owned: no --model, no --provider, environment untouched.
|
|
10
|
+
// 2. Pass those prompt bytes to the pure opaque Pi adapter, which owns its
|
|
11
|
+
// locked-down print-mode subprocess and fresh empty scratch directory;
|
|
12
|
+
// take its stdout as raw final bytes. Model/provider/profile selection
|
|
13
|
+
// stays user-owned: no --model, no --provider, environment untouched.
|
|
14
14
|
// 3. Submit those bytes untouched through the provider-owned `submission`
|
|
15
15
|
// form carried by the collect input: execute its exact operation and
|
|
16
16
|
// argument tokens with only the tempfile path substituted into the
|
|
@@ -30,25 +30,18 @@ import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
|
30
30
|
import { tmpdir } from "node:os";
|
|
31
31
|
import { isAbsolute, join } from "node:path";
|
|
32
32
|
import { resolveGentleAiBinary } from "./gentle-ai-binary.ts";
|
|
33
|
+
import {
|
|
34
|
+
OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE,
|
|
35
|
+
OpaquePiReviewerTransportError,
|
|
36
|
+
runOpaquePiReviewer,
|
|
37
|
+
type OpaquePiReviewerResult,
|
|
38
|
+
} from "./opaque-pi-reviewer-adapter.ts";
|
|
33
39
|
import { REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION, REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS, type ReviewCaptureSubmissionV1, type ReviewCollectInputV3 } from "./review-integration-v2.ts";
|
|
34
40
|
import { GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "./review-relay-contract.ts";
|
|
35
41
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
39
|
-
// --model/--provider/--profile, which remain user-owned.
|
|
40
|
-
export const REVIEW_HOST_RELAY_PI_ARGV = Object.freeze([
|
|
41
|
-
"--print",
|
|
42
|
-
"--mode", "text",
|
|
43
|
-
"--no-session",
|
|
44
|
-
"--no-tools",
|
|
45
|
-
"--no-extensions",
|
|
46
|
-
"--no-skills",
|
|
47
|
-
"--no-prompt-templates",
|
|
48
|
-
"--no-themes",
|
|
49
|
-
"--no-context-files",
|
|
50
|
-
"--no-approve",
|
|
51
|
-
] as const);
|
|
42
|
+
// Compatibility export for existing relay consumers. The pure adapter owns the
|
|
43
|
+
// fixed Pi process boundary and its locked-down argv.
|
|
44
|
+
export { OPAQUE_PI_REVIEWER_ARGV as REVIEW_HOST_RELAY_PI_ARGV } from "./opaque-pi-reviewer-adapter.ts";
|
|
52
45
|
|
|
53
46
|
export const REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE =
|
|
54
47
|
"provider relay requires a gentle-ai build with the pi host relay surface";
|
|
@@ -61,6 +54,11 @@ export const REVIEW_HOST_RELAY_FAILURE = {
|
|
|
61
54
|
EMPTY_PROMPT: "empty-prompt",
|
|
62
55
|
PI_LAUNCH_FAILED: "pi-launch-failed",
|
|
63
56
|
PI_FAILED: "pi-failed",
|
|
57
|
+
// gentle-pi#367: a reviewer killed by the relay bound is not a crash. It
|
|
58
|
+
// is the one failure class that a byte-identical relaunch cannot survive,
|
|
59
|
+
// so it carries its own kind, its own elapsed/limit evidence, and its own
|
|
60
|
+
// continuation instead of hiding inside `pi-failed`.
|
|
61
|
+
PI_TIMED_OUT: "pi-timed-out",
|
|
64
62
|
PI_EMPTY_OUTPUT: "pi-empty-output",
|
|
65
63
|
SUBMISSION_REFUSED: "submission-refused",
|
|
66
64
|
} as const;
|
|
@@ -79,11 +77,20 @@ export class ReviewHostRelayError extends Error {
|
|
|
79
77
|
readonly exitCode: number | null;
|
|
80
78
|
readonly stderr: string;
|
|
81
79
|
readonly timedOut: boolean;
|
|
80
|
+
// Wall time the killed or failed child actually consumed, and the bound it
|
|
81
|
+
// was measured against. Both are null only when no child process ran.
|
|
82
|
+
// Without them a transport failure cannot be told apart from a crash, which
|
|
83
|
+
// is what forced the gentle-pi#367 reporter to measure the relay by hand.
|
|
84
|
+
readonly elapsedMs: number | null;
|
|
85
|
+
readonly timeoutMs: number | null;
|
|
82
86
|
// "none" until the submission invocation launches; a launched submission
|
|
83
87
|
// whose outcome could not be read is "unknown" and the caller reconciles
|
|
84
|
-
// through negotiated STATUS, never through a blind retry.
|
|
88
|
+
// through negotiated STATUS, never through a blind retry. A launched
|
|
89
|
+
// submission that gentle-ai refused with its typed admission refusal is
|
|
90
|
+
// "none" again: the provider states that the lens slot was not consumed
|
|
91
|
+
// (gentle-pi#522 / #524).
|
|
85
92
|
readonly mutationOutcome: "none" | "unknown";
|
|
86
|
-
constructor(kind: ReviewHostRelayFailureKind, stage: ReviewHostRelayStage, message: string, details?: { exitCode?: number | null; stderr?: string; timedOut?: boolean }) {
|
|
93
|
+
constructor(kind: ReviewHostRelayFailureKind, stage: ReviewHostRelayStage, message: string, details?: { exitCode?: number | null; stderr?: string; timedOut?: boolean; elapsedMs?: number; timeoutMs?: number; mutationOutcome?: "none" | "unknown" }) {
|
|
87
94
|
super(message);
|
|
88
95
|
this.name = "ReviewHostRelayError";
|
|
89
96
|
this.kind = kind;
|
|
@@ -91,10 +98,23 @@ export class ReviewHostRelayError extends Error {
|
|
|
91
98
|
this.exitCode = details?.exitCode ?? null;
|
|
92
99
|
this.stderr = details?.stderr ?? "";
|
|
93
100
|
this.timedOut = details?.timedOut ?? false;
|
|
94
|
-
this.
|
|
101
|
+
this.elapsedMs = details?.elapsedMs ?? null;
|
|
102
|
+
this.timeoutMs = details?.timeoutMs ?? null;
|
|
103
|
+
this.mutationOutcome = details?.mutationOutcome ?? (stage === "submit" ? "unknown" : "none");
|
|
95
104
|
}
|
|
96
105
|
}
|
|
97
106
|
|
|
107
|
+
// gentle-pi#522 / #524: gentle-ai refuses a reviewer submission before any
|
|
108
|
+
// admission with exit 1 and its typed operator line, `<reason> [invalid_request]`.
|
|
109
|
+
// That code is the provider's preflight class: the request was refused as
|
|
110
|
+
// sent and the lens slot was not consumed. The relay recognises only that
|
|
111
|
+
// typed shape; it never parses the reason, and it never retries.
|
|
112
|
+
const ADMISSION_REFUSAL = /\[invalid_request\]/;
|
|
113
|
+
|
|
114
|
+
export function isReviewHostRelayAdmissionRefusal(capture: { exitCode: number | null; timedOut: boolean }, stderr: string): boolean {
|
|
115
|
+
return capture.exitCode === 1 && !capture.timedOut && ADMISSION_REFUSAL.test(stderr);
|
|
116
|
+
}
|
|
117
|
+
|
|
98
118
|
// Refusal classification for the materialize invocation. The installed
|
|
99
119
|
// gentle-ai is the only authority on whether the materialize form exists; Pi
|
|
100
120
|
// never version-sniffs. Two typed refusal classes are distinguished:
|
|
@@ -237,6 +257,8 @@ export function resolveReviewHostRelaySubmission(submission: ReviewCaptureSubmis
|
|
|
237
257
|
|
|
238
258
|
export interface ReviewHostRelayRequest {
|
|
239
259
|
readonly captureArgumentTokens: readonly string[];
|
|
260
|
+
/** Canonical target worktree for coordinator-only native materialize/submit calls. */
|
|
261
|
+
readonly targetCwd?: string;
|
|
240
262
|
/** The provider-owned completing form; absent means contract mismatch. */
|
|
241
263
|
readonly submission?: ReviewCaptureSubmissionV1;
|
|
242
264
|
/** Absolute path; defaults to the verified package-local binary. */
|
|
@@ -245,6 +267,12 @@ export interface ReviewHostRelayRequest {
|
|
|
245
267
|
readonly piExecutable?: string;
|
|
246
268
|
readonly environment?: NodeJS.ProcessEnv;
|
|
247
269
|
readonly gentleAiTimeoutMs?: number;
|
|
270
|
+
/**
|
|
271
|
+
* Overrides the reviewer bound entirely. Production leaves it unset and the
|
|
272
|
+
* relay derives the bound from the materialized prompt bytes and
|
|
273
|
+
* {@link REVIEW_HOST_RELAY_PI_TIMEOUT_ENV}; this seam exists so tests can
|
|
274
|
+
* exercise the timeout leg without a wall-clock wait.
|
|
275
|
+
*/
|
|
248
276
|
readonly piTimeoutMs?: number;
|
|
249
277
|
readonly signal?: AbortSignal;
|
|
250
278
|
}
|
|
@@ -256,24 +284,88 @@ export interface ReviewHostRelayResult {
|
|
|
256
284
|
readonly submission: string;
|
|
257
285
|
}
|
|
258
286
|
|
|
287
|
+
/** Opaque materialize-and-review result, not yet submitted to the provider. */
|
|
288
|
+
export interface ReviewHostRelayPreparedResult {
|
|
289
|
+
/** Copy-safe request snapshot captured before materialization starts. */
|
|
290
|
+
readonly request: ReviewHostRelayRequest;
|
|
291
|
+
readonly promptByteLength: number;
|
|
292
|
+
readonly resultByteLength: number;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const preparedResultBytes = new WeakMap<ReviewHostRelayPreparedResult, Buffer>();
|
|
296
|
+
|
|
259
297
|
export type ReviewHostRelayRunner = (request: ReviewHostRelayRequest) => Promise<ReviewHostRelayResult>;
|
|
298
|
+
export type ReviewHostRelayPreparationRunner = (request: ReviewHostRelayRequest) => Promise<ReviewHostRelayPreparedResult>;
|
|
299
|
+
export type ReviewHostRelaySubmissionRunner = (prepared: ReviewHostRelayPreparedResult) => Promise<ReviewHostRelayResult>;
|
|
260
300
|
|
|
261
301
|
const DEFAULT_GENTLE_AI_TIMEOUT_MS = 120_000;
|
|
262
|
-
|
|
302
|
+
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
// The reviewer subprocess bound (gentle-pi#367).
|
|
305
|
+
//
|
|
306
|
+
// The previous bound was a single hardcoded 600_000 ms reachable only through
|
|
307
|
+
// the test-injectable runner. A field-measured lens legitimately needed 478s
|
|
308
|
+
// against a ~1.58 MB materialized prompt: it survived by hand and was killed
|
|
309
|
+
// under the relay, and the sanctioned continuation then re-spent every lens to
|
|
310
|
+
// reach the same wall. One fixed number cannot serve a prompt class that
|
|
311
|
+
// varies by orders of magnitude, so the bound is derived instead:
|
|
312
|
+
//
|
|
313
|
+
// floor + ceil(promptBytes / MiB * perMebibyte), clamped to the ceiling
|
|
314
|
+
//
|
|
315
|
+
// The floor covers model latency that does not depend on prompt size; the
|
|
316
|
+
// linear term covers the part that does. At the measured 1.58 MB the derived
|
|
317
|
+
// bound is ~37 minutes, roughly a 4.7x margin over the 478s the reviewer
|
|
318
|
+
// actually needed — deliberately generous, because the reviewer model and
|
|
319
|
+
// provider are user-owned and the relay cannot know their throughput.
|
|
320
|
+
//
|
|
321
|
+
// GENTLE_PI_REVIEW_RELAY_PI_TIMEOUT_MS replaces the derived bound entirely for
|
|
322
|
+
// callers who know their own configuration. It follows the repository's
|
|
323
|
+
// established numeric-override shape (GENTLE_PI_CANDIDATE_GIT_TIMEOUT_MS,
|
|
324
|
+
// GENTLE_PI_REVIEW_MAX_BUFFER_BYTES): a positive decimal, silently ignored
|
|
325
|
+
// when malformed, and clamped to the same hard ceiling so no configuration can
|
|
326
|
+
// turn a foreground FINALIZE into an unbounded child process.
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
export const REVIEW_HOST_RELAY_PI_TIMEOUT_ENV = "GENTLE_PI_REVIEW_RELAY_PI_TIMEOUT_MS";
|
|
330
|
+
export const REVIEW_HOST_RELAY_PI_TIMEOUT_FLOOR_MS = 900_000;
|
|
331
|
+
export const REVIEW_HOST_RELAY_PI_TIMEOUT_PER_MEBIBYTE_MS = 900_000;
|
|
332
|
+
export const REVIEW_HOST_RELAY_PI_TIMEOUT_MAX_MS = 7_200_000;
|
|
333
|
+
const BYTES_PER_MEBIBYTE = 1024 * 1024;
|
|
334
|
+
|
|
335
|
+
export function resolveReviewHostRelayPiTimeoutMs(promptByteLength: number, environment: NodeJS.ProcessEnv = process.env): number {
|
|
336
|
+
const configured = environment[REVIEW_HOST_RELAY_PI_TIMEOUT_ENV];
|
|
337
|
+
if (configured !== undefined && /^[1-9]\d*$/.test(configured)) {
|
|
338
|
+
const parsed = Number(configured);
|
|
339
|
+
if (Number.isSafeInteger(parsed)) return Math.min(parsed, REVIEW_HOST_RELAY_PI_TIMEOUT_MAX_MS);
|
|
340
|
+
}
|
|
341
|
+
const bytes = Number.isSafeInteger(promptByteLength) && promptByteLength > 0 ? promptByteLength : 0;
|
|
342
|
+
const scaled = REVIEW_HOST_RELAY_PI_TIMEOUT_FLOOR_MS + Math.ceil((bytes / BYTES_PER_MEBIBYTE) * REVIEW_HOST_RELAY_PI_TIMEOUT_PER_MEBIBYTE_MS);
|
|
343
|
+
return Math.min(scaled, REVIEW_HOST_RELAY_PI_TIMEOUT_MAX_MS);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// The reviewer ran out of time; it did not crash. The message states both
|
|
347
|
+
// measurements and names the two things that can change the outcome, because
|
|
348
|
+
// the one thing that cannot is relaunching the identical slot.
|
|
349
|
+
export function reviewHostRelayPiTimeoutMessage(elapsedMs: number, timeoutMs: number, promptByteLength: number): string {
|
|
350
|
+
return `pi reviewer subprocess exceeded the relay bound: killed after ${elapsedMs}ms against a ${timeoutMs}ms limit for a ${promptByteLength}-byte materialized prompt. `
|
|
351
|
+
+ `Relaunching the same slot unchanged reaches the same wall. Raise ${REVIEW_HOST_RELAY_PI_TIMEOUT_ENV} above the reviewer's real wall time (ceiling ${REVIEW_HOST_RELAY_PI_TIMEOUT_MAX_MS}ms) or reduce the candidate scope so the materialized prompt is smaller.`;
|
|
352
|
+
}
|
|
263
353
|
|
|
264
354
|
interface ProcessCapture {
|
|
265
355
|
stdout: Buffer;
|
|
266
356
|
stderr: Buffer;
|
|
267
357
|
exitCode: number | null;
|
|
268
358
|
timedOut: boolean;
|
|
359
|
+
elapsedMs: number;
|
|
269
360
|
}
|
|
270
361
|
|
|
271
|
-
function
|
|
362
|
+
function collectGentleAiProcess(
|
|
272
363
|
file: string,
|
|
273
364
|
arguments_: readonly string[],
|
|
274
365
|
options: { cwd: string; env: NodeJS.ProcessEnv; stdin?: Buffer; timeoutMs: number; signal?: AbortSignal },
|
|
275
366
|
): Promise<ProcessCapture> {
|
|
276
367
|
return new Promise((resolve, reject) => {
|
|
368
|
+
const startedAt = Date.now();
|
|
277
369
|
const child = spawn(file, [...arguments_], {
|
|
278
370
|
cwd: options.cwd,
|
|
279
371
|
env: options.env,
|
|
@@ -305,7 +397,7 @@ function collectProcess(
|
|
|
305
397
|
if (settled) return;
|
|
306
398
|
settled = true;
|
|
307
399
|
if (timer !== undefined) clearTimeout(timer);
|
|
308
|
-
resolve({ stdout: Buffer.concat(stdout), stderr: Buffer.concat(stderr), exitCode: code, timedOut });
|
|
400
|
+
resolve({ stdout: Buffer.concat(stdout), stderr: Buffer.concat(stderr), exitCode: code, timedOut, elapsedMs: Date.now() - startedAt });
|
|
309
401
|
});
|
|
310
402
|
if (options.stdin === undefined) {
|
|
311
403
|
child.stdin.end();
|
|
@@ -316,6 +408,45 @@ function collectProcess(
|
|
|
316
408
|
});
|
|
317
409
|
}
|
|
318
410
|
|
|
411
|
+
function relayPiTransportError(error: unknown, promptByteLength: number, piTimeoutMs: number): ReviewHostRelayError {
|
|
412
|
+
if (!(error instanceof OpaquePiReviewerTransportError)) {
|
|
413
|
+
return new ReviewHostRelayError(
|
|
414
|
+
REVIEW_HOST_RELAY_FAILURE.PI_LAUNCH_FAILED,
|
|
415
|
+
"pi",
|
|
416
|
+
`pi subprocess could not start: ${error instanceof Error ? error.message : String(error)}`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
const details = {
|
|
420
|
+
exitCode: error.exitCode,
|
|
421
|
+
stderr: error.stderr.toString("utf8"),
|
|
422
|
+
timedOut: error.timedOut,
|
|
423
|
+
...(error.elapsedMs === null ? {} : { elapsedMs: error.elapsedMs }),
|
|
424
|
+
...(error.timeoutMs === null ? {} : { timeoutMs: error.timeoutMs }),
|
|
425
|
+
};
|
|
426
|
+
if (
|
|
427
|
+
error.kind === OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.TIMED_OUT
|
|
428
|
+
&& error.elapsedMs !== null
|
|
429
|
+
&& error.timeoutMs !== null
|
|
430
|
+
) {
|
|
431
|
+
return new ReviewHostRelayError(
|
|
432
|
+
REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT,
|
|
433
|
+
"pi",
|
|
434
|
+
reviewHostRelayPiTimeoutMessage(error.elapsedMs, error.timeoutMs, promptByteLength),
|
|
435
|
+
{ ...details, timedOut: true, elapsedMs: error.elapsedMs, timeoutMs: error.timeoutMs },
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
if (error.kind === OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.EMPTY_OUTPUT) {
|
|
439
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_EMPTY_OUTPUT, "pi", "pi subprocess produced no output bytes", details);
|
|
440
|
+
}
|
|
441
|
+
if (
|
|
442
|
+
error.kind === OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.LAUNCH_FAILED
|
|
443
|
+
|| error.kind === OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.SCRATCH_FAILED
|
|
444
|
+
) {
|
|
445
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_LAUNCH_FAILED, "pi", `pi subprocess could not start: ${error.message}`, details);
|
|
446
|
+
}
|
|
447
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_FAILED, "pi", "pi subprocess failed", details);
|
|
448
|
+
}
|
|
449
|
+
|
|
319
450
|
function assertTokens(name: string, tokens: readonly string[]): void {
|
|
320
451
|
if (tokens.length === 0) throw new TypeError(`Pi host relay requires the provider-issued ${name} tokens`);
|
|
321
452
|
if (tokens.some((token) => typeof token !== "string" || token.length === 0)) {
|
|
@@ -323,38 +454,54 @@ function assertTokens(name: string, tokens: readonly string[]): void {
|
|
|
323
454
|
}
|
|
324
455
|
}
|
|
325
456
|
|
|
457
|
+
function snapshotReviewHostRelayRequest(request: ReviewHostRelayRequest): ReviewHostRelayRequest {
|
|
458
|
+
assertTokens("capture", request.captureArgumentTokens);
|
|
459
|
+
// The completing form is validated before any process launches: a materialize
|
|
460
|
+
// slot without a provider-owned submission is a typed contract mismatch,
|
|
461
|
+
// never a synthesized invocation.
|
|
462
|
+
resolveReviewHostRelaySubmission(request.submission);
|
|
463
|
+
const gentleAiExecutable = request.gentleAiExecutable ?? resolveGentleAiBinary();
|
|
464
|
+
if (!isAbsolute(gentleAiExecutable)) throw new TypeError("Pi host relay requires an absolute gentle-ai executable path");
|
|
465
|
+
const environment = Object.freeze({ ...(request.environment ?? process.env) }) as NodeJS.ProcessEnv;
|
|
466
|
+
const submission = request.submission === undefined ? undefined : Object.freeze({
|
|
467
|
+
operationToken: request.submission.operationToken,
|
|
468
|
+
argumentTokens: Object.freeze([...request.submission.argumentTokens]),
|
|
469
|
+
values: Object.freeze(request.submission.values.map((value) => Object.freeze({ ...value }))),
|
|
470
|
+
});
|
|
471
|
+
return Object.freeze({
|
|
472
|
+
...request,
|
|
473
|
+
captureArgumentTokens: Object.freeze([...request.captureArgumentTokens]),
|
|
474
|
+
...(submission === undefined ? {} : { submission }),
|
|
475
|
+
gentleAiExecutable,
|
|
476
|
+
environment,
|
|
477
|
+
gentleAiTimeoutMs: request.gentleAiTimeoutMs ?? DEFAULT_GENTLE_AI_TIMEOUT_MS,
|
|
478
|
+
targetCwd: request.targetCwd ?? process.cwd(),
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
326
482
|
/**
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
* a failure; the caller re-queries negotiated STATUS instead of retrying.
|
|
483
|
+
* Materializes one provider-bound reviewer prompt and runs its opaque Pi
|
|
484
|
+
* subprocess. It does not submit anything, so independent reviewer work can
|
|
485
|
+
* finish before the caller performs provider-ordered admission.
|
|
331
486
|
*/
|
|
332
|
-
export async function
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
const gentleAiEnvironment = { ...baseEnvironment, [GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV]: GENTLE_PI_REVIEW_RELAY_CONTRACT };
|
|
344
|
-
const gentleAiTimeoutMs = request.gentleAiTimeoutMs ?? DEFAULT_GENTLE_AI_TIMEOUT_MS;
|
|
345
|
-
const piTimeoutMs = request.piTimeoutMs ?? DEFAULT_PI_TIMEOUT_MS;
|
|
346
|
-
|
|
347
|
-
// (a) Materialize the Go-issued opaque prompt. This invocation is also the
|
|
348
|
-
// capability detection: an old binary's unknown-flag refusal proves the
|
|
349
|
-
// relay surface is absent, and the provider's handshake refusal surfaces
|
|
350
|
-
// verbatim. No version sniffing.
|
|
487
|
+
export async function prepareReviewHostRelaySlot(
|
|
488
|
+
request: ReviewHostRelayRequest,
|
|
489
|
+
reviewer: typeof runOpaquePiReviewer = runOpaquePiReviewer,
|
|
490
|
+
): Promise<ReviewHostRelayPreparedResult> {
|
|
491
|
+
// Copy mutable transport configuration before the first async boundary. The
|
|
492
|
+
// supplied AbortSignal intentionally stays live across materialize, reviewer,
|
|
493
|
+
// and submit, preserving the established cancellation behavior.
|
|
494
|
+
const preparedRequest = snapshotReviewHostRelayRequest(request);
|
|
495
|
+
|
|
496
|
+
// The provider materializes the opaque prompt and detects whether this relay
|
|
497
|
+
// surface is available. No version sniffing or prompt reconstruction occurs.
|
|
351
498
|
let materialized: ProcessCapture;
|
|
352
499
|
try {
|
|
353
|
-
materialized = await
|
|
354
|
-
cwd:
|
|
355
|
-
env:
|
|
356
|
-
timeoutMs: gentleAiTimeoutMs
|
|
357
|
-
...(
|
|
500
|
+
materialized = await collectGentleAiProcess(preparedRequest.gentleAiExecutable!, ["review", "capture-result", ...preparedRequest.captureArgumentTokens], {
|
|
501
|
+
cwd: preparedRequest.targetCwd!,
|
|
502
|
+
env: { ...preparedRequest.environment!, [GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV]: GENTLE_PI_REVIEW_RELAY_CONTRACT },
|
|
503
|
+
timeoutMs: preparedRequest.gentleAiTimeoutMs!,
|
|
504
|
+
...(preparedRequest.signal === undefined ? {} : { signal: preparedRequest.signal }),
|
|
358
505
|
});
|
|
359
506
|
} catch (error) {
|
|
360
507
|
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.MATERIALIZE_FAILED, "materialize", `gentle-ai prompt materialization could not start: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -362,75 +509,156 @@ export async function runReviewHostRelaySlot(request: ReviewHostRelayRequest): P
|
|
|
362
509
|
if (materialized.exitCode !== 0 || materialized.timedOut) {
|
|
363
510
|
const stderr = materialized.stderr.toString("utf8");
|
|
364
511
|
const refusal = classifyReviewHostRelayRefusal(stderr);
|
|
512
|
+
const timing = { elapsedMs: materialized.elapsedMs, timeoutMs: preparedRequest.gentleAiTimeoutMs! };
|
|
365
513
|
if (refusal === "unknown-flag") {
|
|
366
|
-
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.RELAY_UNAVAILABLE, "materialize", REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE, {
|
|
514
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.RELAY_UNAVAILABLE, "materialize", REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE, {
|
|
515
|
+
exitCode: materialized.exitCode,
|
|
516
|
+
stderr,
|
|
517
|
+
timedOut: materialized.timedOut,
|
|
518
|
+
...timing,
|
|
519
|
+
});
|
|
367
520
|
}
|
|
368
521
|
if (refusal === "handshake") {
|
|
369
|
-
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.HANDSHAKE_REFUSED, "materialize", stderr, {
|
|
522
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.HANDSHAKE_REFUSED, "materialize", stderr, {
|
|
523
|
+
exitCode: materialized.exitCode,
|
|
524
|
+
stderr,
|
|
525
|
+
timedOut: materialized.timedOut,
|
|
526
|
+
...timing,
|
|
527
|
+
});
|
|
370
528
|
}
|
|
371
|
-
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.MATERIALIZE_FAILED, "materialize",
|
|
529
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.MATERIALIZE_FAILED, "materialize", materialized.timedOut
|
|
530
|
+
? `gentle-ai prompt materialization exceeded its ${preparedRequest.gentleAiTimeoutMs!}ms bound after ${materialized.elapsedMs}ms`
|
|
531
|
+
: "gentle-ai prompt materialization failed", { exitCode: materialized.exitCode, stderr, timedOut: materialized.timedOut, ...timing });
|
|
372
532
|
}
|
|
373
533
|
const promptBytes = materialized.stdout;
|
|
374
534
|
if (promptBytes.length === 0) {
|
|
375
|
-
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.EMPTY_PROMPT, "materialize", "gentle-ai prompt materialization produced no bytes", {
|
|
535
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.EMPTY_PROMPT, "materialize", "gentle-ai prompt materialization produced no bytes", {
|
|
536
|
+
exitCode: 0,
|
|
537
|
+
stderr: materialized.stderr.toString("utf8"),
|
|
538
|
+
elapsedMs: materialized.elapsedMs,
|
|
539
|
+
timeoutMs: preparedRequest.gentleAiTimeoutMs!,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
// The reviewer bound is derived from the prompt the provider actually
|
|
543
|
+
// materialized. The explicit request timeout is a test seam that wins over
|
|
544
|
+
// both the user-owned environment override and the scale-derived bound.
|
|
545
|
+
const piTimeoutMs = preparedRequest.piTimeoutMs ?? resolveReviewHostRelayPiTimeoutMs(promptBytes.length, preparedRequest.environment);
|
|
546
|
+
|
|
547
|
+
// The pure adapter owns the fresh isolated Pi process. Its input and output
|
|
548
|
+
// are opaque bytes; this coordinator only maps transport failures.
|
|
549
|
+
let piResult: OpaquePiReviewerResult;
|
|
550
|
+
try {
|
|
551
|
+
piResult = await reviewer(promptBytes, {
|
|
552
|
+
...(preparedRequest.piExecutable === undefined ? {} : { piExecutable: preparedRequest.piExecutable }),
|
|
553
|
+
environment: preparedRequest.environment,
|
|
554
|
+
timeoutMs: piTimeoutMs,
|
|
555
|
+
...(preparedRequest.signal === undefined ? {} : { signal: preparedRequest.signal }),
|
|
556
|
+
});
|
|
557
|
+
} catch (error) {
|
|
558
|
+
throw relayPiTransportError(error, promptBytes.length, piTimeoutMs);
|
|
559
|
+
}
|
|
560
|
+
const prepared = Object.freeze({
|
|
561
|
+
request: preparedRequest,
|
|
562
|
+
promptByteLength: promptBytes.length,
|
|
563
|
+
resultByteLength: piResult.stdoutByteLength,
|
|
564
|
+
});
|
|
565
|
+
preparedResultBytes.set(prepared, Buffer.from(piResult.stdout));
|
|
566
|
+
return prepared;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Starts every reviewer before awaiting any result. If one or more reviewers
|
|
571
|
+
* fail, it rejects only after every started transport has settled and reports
|
|
572
|
+
* the earliest failed request in provider order.
|
|
573
|
+
*/
|
|
574
|
+
export async function runReviewHostRelayReviewerGroup(
|
|
575
|
+
requests: readonly ReviewHostRelayRequest[],
|
|
576
|
+
prepare: ReviewHostRelayPreparationRunner = prepareReviewHostRelaySlot,
|
|
577
|
+
): Promise<readonly ReviewHostRelayPreparedResult[]> {
|
|
578
|
+
if (requests.length === 0) {
|
|
579
|
+
throw new TypeError("Pi host relay reviewer group requires at least one provider-bound request");
|
|
376
580
|
}
|
|
581
|
+
const settled = await Promise.allSettled(requests.map(async (request) => await prepare(request)));
|
|
582
|
+
const failed = settled.find((result) => result.status === "rejected");
|
|
583
|
+
if (failed?.status === "rejected") throw failed.reason;
|
|
584
|
+
return settled.map((result) => (result as PromiseFulfilledResult<ReviewHostRelayPreparedResult>).value);
|
|
585
|
+
}
|
|
377
586
|
|
|
378
|
-
|
|
379
|
-
|
|
587
|
+
/**
|
|
588
|
+
* Submits one already-reviewed opaque result through the exact provider-owned
|
|
589
|
+
* completing form. Only the provider-declared artifact slot is substituted.
|
|
590
|
+
*/
|
|
591
|
+
export async function submitReviewHostRelayPreparedResult(prepared: ReviewHostRelayPreparedResult): Promise<ReviewHostRelayResult> {
|
|
592
|
+
const resultBytes = preparedResultBytes.get(prepared);
|
|
593
|
+
if (resultBytes === undefined) throw new TypeError("Pi host relay requires a recognized prepared result");
|
|
594
|
+
const { request } = prepared;
|
|
595
|
+
assertTokens("capture", request.captureArgumentTokens);
|
|
596
|
+
const submissionBinding = resolveReviewHostRelaySubmission(request.submission);
|
|
380
597
|
const stagingDirectory = await mkdtemp(join(tmpdir(), "gentle-pi-host-relay-result-"));
|
|
598
|
+
let primaryFailure = false;
|
|
381
599
|
try {
|
|
382
|
-
await chmod(scratchDirectory, 0o700);
|
|
383
600
|
await chmod(stagingDirectory, 0o700);
|
|
384
|
-
let piRun: ProcessCapture;
|
|
385
|
-
try {
|
|
386
|
-
piRun = await collectProcess(request.piExecutable ?? "pi", REVIEW_HOST_RELAY_PI_ARGV, {
|
|
387
|
-
cwd: scratchDirectory,
|
|
388
|
-
env: baseEnvironment,
|
|
389
|
-
stdin: promptBytes,
|
|
390
|
-
timeoutMs: piTimeoutMs,
|
|
391
|
-
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
392
|
-
});
|
|
393
|
-
} catch (error) {
|
|
394
|
-
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_LAUNCH_FAILED, "pi", `pi subprocess could not start: ${error instanceof Error ? error.message : String(error)}`);
|
|
395
|
-
}
|
|
396
|
-
if (piRun.exitCode !== 0 || piRun.timedOut) {
|
|
397
|
-
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_FAILED, "pi", "pi subprocess failed", { exitCode: piRun.exitCode, stderr: piRun.stderr.toString("utf8"), timedOut: piRun.timedOut });
|
|
398
|
-
}
|
|
399
|
-
const resultBytes = piRun.stdout;
|
|
400
|
-
if (resultBytes.length === 0) {
|
|
401
|
-
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_EMPTY_OUTPUT, "pi", "pi subprocess produced no output bytes", { exitCode: 0, stderr: piRun.stderr.toString("utf8") });
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// (d) Submit the raw final bytes untouched through the provider-owned
|
|
405
|
-
// completing form: its exact operation and argument tokens, with only
|
|
406
|
-
// the artifact path substituted into the declared {{value}} slot.
|
|
407
601
|
const resultFile = join(stagingDirectory, "result.raw");
|
|
408
602
|
await writeFile(resultFile, resultBytes, { mode: 0o600 });
|
|
409
603
|
await chmod(resultFile, 0o600);
|
|
410
604
|
const submitTokens = submissionBinding.argumentTokens.map((token, index) =>
|
|
411
|
-
index === submissionBinding.substitutionLocation
|
|
605
|
+
index === submissionBinding.substitutionLocation
|
|
606
|
+
? token.split(REVIEW_HOST_RELAY_SUBMISSION_VALUE_SLOT).join(resultFile)
|
|
607
|
+
: token,
|
|
412
608
|
);
|
|
413
609
|
let submission: ProcessCapture;
|
|
414
610
|
try {
|
|
415
|
-
submission = await
|
|
416
|
-
cwd:
|
|
417
|
-
env:
|
|
418
|
-
timeoutMs: gentleAiTimeoutMs
|
|
611
|
+
submission = await collectGentleAiProcess(request.gentleAiExecutable!, ["review", submissionBinding.operationToken, ...submitTokens], {
|
|
612
|
+
cwd: request.targetCwd!,
|
|
613
|
+
env: { ...request.environment!, [GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV]: GENTLE_PI_REVIEW_RELAY_CONTRACT },
|
|
614
|
+
timeoutMs: request.gentleAiTimeoutMs!,
|
|
419
615
|
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
420
616
|
});
|
|
421
617
|
} catch (error) {
|
|
422
618
|
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit", `gentle-ai capture submission could not start: ${error instanceof Error ? error.message : String(error)}`);
|
|
423
619
|
}
|
|
424
620
|
if (submission.exitCode !== 0 || submission.timedOut || submission.stdout.length === 0) {
|
|
425
|
-
|
|
621
|
+
const stderr = submission.stderr.toString("utf8");
|
|
622
|
+
const details = { exitCode: submission.exitCode, stderr, timedOut: submission.timedOut, elapsedMs: submission.elapsedMs, timeoutMs: request.gentleAiTimeoutMs! };
|
|
623
|
+
// A typed admission refusal proves the provider consumed no slot.
|
|
624
|
+
// Every other launched submission stays unknown pending fresh STATUS.
|
|
625
|
+
if (isReviewHostRelayAdmissionRefusal(submission, stderr)) {
|
|
626
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit", stderr.trim(), {
|
|
627
|
+
...details,
|
|
628
|
+
mutationOutcome: "none",
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED, "submit", submission.timedOut
|
|
632
|
+
? `gentle-ai capture submission exceeded its ${request.gentleAiTimeoutMs!}ms bound after ${submission.elapsedMs}ms`
|
|
633
|
+
: "gentle-ai refused the relayed capture submission", details);
|
|
426
634
|
}
|
|
427
635
|
return {
|
|
428
|
-
promptByteLength:
|
|
429
|
-
resultByteLength:
|
|
636
|
+
promptByteLength: prepared.promptByteLength,
|
|
637
|
+
resultByteLength: prepared.resultByteLength,
|
|
430
638
|
submission: submission.stdout.toString("utf8"),
|
|
431
639
|
};
|
|
640
|
+
} catch (error) {
|
|
641
|
+
primaryFailure = true;
|
|
642
|
+
throw error;
|
|
432
643
|
} finally {
|
|
433
|
-
|
|
434
|
-
|
|
644
|
+
try {
|
|
645
|
+
await rm(stagingDirectory, { recursive: true, force: true });
|
|
646
|
+
} catch (error) {
|
|
647
|
+
if (!primaryFailure) {
|
|
648
|
+
throw new ReviewHostRelayError(
|
|
649
|
+
REVIEW_HOST_RELAY_FAILURE.SUBMISSION_REFUSED,
|
|
650
|
+
"submit",
|
|
651
|
+
`Pi host relay result staging cleanup failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
435
655
|
}
|
|
436
656
|
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Compatibility one-binding path: materialize → opaque Pi adapter → submit.
|
|
660
|
+
* It preserves the established API and its typed failure behavior exactly.
|
|
661
|
+
*/
|
|
662
|
+
export async function runReviewHostRelaySlot(request: ReviewHostRelayRequest): Promise<ReviewHostRelayResult> {
|
|
663
|
+
return await submitReviewHostRelayPreparedResult(await prepareReviewHostRelaySlot(request));
|
|
664
|
+
}
|