gentle-pi 3.2.1 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -59
- package/assets/orchestrator-delegation.md +1 -1
- package/docs/assets/brand/gentle-shell-banner.gif +0 -0
- package/docs/assets/diagrams/odd-workflow.svg +74 -0
- package/docs/assets/features/agents-view.png +0 -0
- package/docs/assets/features/changes-view.png +0 -0
- package/docs/assets/features/command-palette.png +0 -0
- package/docs/assets/features/profiles-routing.png +0 -0
- package/docs/gentle-shell.md +52 -15
- package/docs/readme-reference.md +67 -7
- package/docs/review-integration.md +22 -17
- package/extensions/ask-user-question.ts +210 -0
- package/extensions/gentle-agents.ts +93 -18
- package/extensions/gentle-ai.ts +180 -37
- package/extensions/gentle-shell.ts +476 -39
- package/extensions/gentle-todo.ts +19 -1
- package/extensions/quiet-tools.ts +28 -5
- package/extensions/startup-banner.ts +25 -10
- package/lib/agents-view.ts +41 -14
- package/lib/agents-widget.ts +84 -13
- package/lib/animation-policy.ts +52 -0
- package/lib/background-cache-warming.ts +38 -0
- package/lib/command-palette-catalog.ts +2 -0
- package/lib/double-esc-cancel-policy.ts +138 -0
- package/lib/inprocess-reviewer.ts +297 -0
- package/lib/native-review-cli.ts +50 -10
- package/lib/odd-runtime-delegation-gate.ts +88 -0
- package/lib/questionnaire/questionnaire-view.ts +603 -0
- package/lib/questionnaire/schema.ts +82 -0
- package/lib/questionnaire/validate.ts +141 -0
- package/lib/review-candidate-view-owner.ts +20 -5
- package/lib/review-candidate-view.ts +9 -2
- package/lib/review-host-relay.ts +256 -171
- package/lib/review-integration-v2.ts +114 -27
- package/lib/shell-bar.ts +163 -75
- package/lib/shell-card.ts +19 -9
- package/lib/shell-changes-view.ts +43 -5
- package/lib/shell-changes.ts +92 -5
- package/lib/shell-hover.ts +39 -0
- package/lib/shell-prompt.ts +10 -1
- package/lib/shell-sidebar-layout.ts +118 -16
- package/lib/shell-sidebar.ts +16 -0
- package/lib/shell-todo.ts +7 -1
- package/lib/shell-usage-view.ts +103 -12
- package/lib/shell-usage.ts +120 -6
- package/package.json +1 -1
- package/runtime/native-review-cli.mjs +49 -9
- package/runtime/review-integration-v2.mjs +114 -27
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/maintainer/provider-relay-matrix.mjs +118 -47
- package/scripts/verify-package-files.mjs +3 -4
- package/tests/agents-grouping.test.ts +75 -18
- package/tests/agents-view.test.ts +28 -18
- package/tests/agents-widget.test.ts +100 -12
- package/tests/animation-policy.test.ts +42 -0
- package/tests/ask-user-question.test.ts +435 -0
- package/tests/background-cache-warming.test.ts +60 -0
- package/tests/background-subagents.test.ts +68 -0
- package/tests/command-palette.test.ts +10 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +176 -138
- package/tests/double-esc-cancel-policy.test.ts +194 -0
- package/tests/gentle-agents.test.ts +599 -7
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-ai.test.ts +125 -9
- package/tests/gentle-shell.test.ts +1149 -24
- package/tests/gentle-todo.test.ts +17 -4
- package/tests/inprocess-reviewer.test.ts +460 -0
- package/tests/maintainer/provider-relay.maintest.ts +101 -143
- package/tests/native-review-capability-contract.test.ts +34 -1
- package/tests/native-review-parity.test.ts +19 -0
- package/tests/odd-runtime-delegation-gate.test.ts +212 -0
- package/tests/orchestrator-rdd-ownership.test.ts +3 -3
- package/tests/package-manifest.test.ts +6 -17
- package/tests/questionnaire-schema.test.ts +274 -0
- package/tests/questionnaire-view.test.ts +446 -0
- package/tests/rdd-status-line.test.ts +21 -4
- package/tests/review-candidate-owner-retry.test.ts +63 -0
- package/tests/review-candidate-view.test.ts +15 -0
- package/tests/review-controller-native-routing.test.ts +86 -0
- package/tests/review-host-relay-routing.test.ts +77 -0
- package/tests/review-host-relay.test.ts +297 -299
- package/tests/review-integration-v2-forward.test.ts +61 -0
- package/tests/review-integration-v2.test.ts +146 -1
- package/tests/review-ledger-contract.test.ts +1 -2
- package/tests/review-relay-transport-agent.test.ts +129 -26
- package/tests/review-risk-assessment.test.ts +104 -0
- package/tests/runtime-harness.mjs +11 -0
- package/tests/session-changes-shell.test.ts +27 -0
- package/tests/session-worktree-registry.test.ts +41 -0
- package/tests/shell-bar.test.ts +200 -124
- package/tests/shell-card.test.ts +5 -3
- package/tests/shell-changes-view.test.ts +47 -0
- package/tests/shell-changes.test.ts +177 -0
- package/tests/shell-hover.test.ts +19 -0
- package/tests/shell-prompt.test.ts +20 -0
- package/tests/shell-sidebar-fullscreen.test.ts +59 -0
- package/tests/shell-sidebar-layout.test.ts +301 -8
- package/tests/shell-sidebar.test.ts +25 -1
- package/tests/shell-todo.test.ts +36 -0
- package/tests/shell-usage-view.test.ts +120 -1
- package/tests/shell-usage.test.ts +129 -0
- package/tests/skill-collision-prefixes.test.ts +1 -1
- package/tests/startup-banner.test.ts +93 -2
- package/docs/assets/brand/gentle-pi-banner.png +0 -0
- package/lib/opaque-pi-reviewer-adapter.ts +0 -404
- package/skills/release/SKILL.md +0 -137
- package/tests/opaque-pi-reviewer-adapter.test.ts +0 -410
package/lib/review-host-relay.ts
CHANGED
|
@@ -7,14 +7,18 @@
|
|
|
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
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
10
|
+
// 2. Run that prompt through one in-process reviewer completion
|
|
11
|
+
// (lib/inprocess-reviewer.ts#runInProcessReviewer): resolve the lens's
|
|
12
|
+
// "provider/id" selection through the live model registry, authenticate
|
|
13
|
+
// through the registry's own resolver, and complete the frozen prompt as
|
|
14
|
+
// a single user message. There is no child process, no extension
|
|
15
|
+
// allowlist, and no ambient default model — a missing registry or a
|
|
16
|
+
// routing entry with no model is a typed refusal before materialize ever
|
|
17
|
+
// runs (gentle-ai#4611; gentle-pi#311 P2).
|
|
18
|
+
// 3. Submit the completion's text untouched through the provider-owned
|
|
19
|
+
// `submission` form carried by the collect input: execute its exact
|
|
20
|
+
// operation and argument tokens with only the tempfile path substituted
|
|
21
|
+
// into the declared {{value}} slot (BOM-less: the buffer is written
|
|
18
22
|
// byte-for-byte). The host never synthesizes or filters the completing
|
|
19
23
|
// form; a materialize slot without a provider submission is a typed
|
|
20
24
|
// contract mismatch, never a rebuilt invocation.
|
|
@@ -25,28 +29,22 @@
|
|
|
25
29
|
// never from transcript inference. The relay never parses or rebuilds
|
|
26
30
|
// binding, evidence, prompt, schema, budgets, or admission.
|
|
27
31
|
|
|
28
|
-
import { spawn } from "node:child_process";
|
|
29
32
|
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
33
|
+
import { spawn } from "node:child_process";
|
|
30
34
|
import { tmpdir } from "node:os";
|
|
31
35
|
import { isAbsolute, join } from "node:path";
|
|
36
|
+
import { completeSimple } from "@earendil-works/pi-ai/compat";
|
|
32
37
|
import { resolveGentleAiBinary } from "./gentle-ai-binary.ts";
|
|
33
|
-
import { SAFE_MODEL_ID_PATTERN } from "./model-routing-authority.ts";
|
|
34
|
-
import { existsSync } from "node:fs";
|
|
35
|
-
import { delimiter as pathDelimiter } from "node:path";
|
|
36
38
|
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
type
|
|
41
|
-
type
|
|
42
|
-
} from "./
|
|
39
|
+
INPROCESS_REVIEWER_FAILURE,
|
|
40
|
+
runInProcessReviewer,
|
|
41
|
+
type InProcessReviewerFailureCode,
|
|
42
|
+
type InProcessReviewerOutcome,
|
|
43
|
+
type InProcessReviewerRegistry,
|
|
44
|
+
} from "./inprocess-reviewer.ts";
|
|
43
45
|
import { REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION, REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS, type ReviewCaptureSubmissionV1, type ReviewCollectInputV3 } from "./review-integration-v2.ts";
|
|
44
46
|
import { GENTLE_PI_REVIEW_RELAY_CONTRACT, GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV } from "./review-relay-contract.ts";
|
|
45
47
|
|
|
46
|
-
// Compatibility export for existing relay consumers. The pure adapter owns the
|
|
47
|
-
// fixed Pi process boundary and its locked-down argv.
|
|
48
|
-
export { OPAQUE_PI_REVIEWER_ARGV as REVIEW_HOST_RELAY_PI_ARGV } from "./opaque-pi-reviewer-adapter.ts";
|
|
49
|
-
|
|
50
48
|
export const REVIEW_HOST_RELAY_UNAVAILABLE_MESSAGE =
|
|
51
49
|
"provider relay requires a gentle-ai build with the pi host relay surface";
|
|
52
50
|
|
|
@@ -56,19 +54,40 @@ export const REVIEW_HOST_RELAY_FAILURE = {
|
|
|
56
54
|
SUBMISSION_CONTRACT_MISMATCH: "submission-contract-mismatch",
|
|
57
55
|
MATERIALIZE_FAILED: "materialize-failed",
|
|
58
56
|
EMPTY_PROMPT: "empty-prompt",
|
|
57
|
+
// gentle-pi#311 P2: these four names predate the in-process completion,
|
|
58
|
+
// when a killed or crashed child process was the only way a reviewer
|
|
59
|
+
// failed. They stay exactly as they are — including their "pi" wording —
|
|
60
|
+
// because other test lanes (the maintainer provider-relay matrix, the
|
|
61
|
+
// restart-parity harness) construct `ReviewHostRelayError` literals with
|
|
62
|
+
// them directly and are out of this change's scope (gentle-pi#311 P4).
|
|
63
|
+
// PI_TIMED_OUT and PI_FAILED are still reachable from production: a
|
|
64
|
+
// reviewer completion that exceeds its bound or fails for any reason not
|
|
65
|
+
// covered by a more specific REVIEWER_* code below reuses them, since the
|
|
66
|
+
// semantics (a deterministic bound; a generic failure) carried over
|
|
67
|
+
// unchanged. PI_LAUNCH_FAILED and PI_EMPTY_OUTPUT are no longer produced
|
|
68
|
+
// by this module — there is no child to fail to launch, and empty output
|
|
69
|
+
// is now REVIEWER_EMPTY_OUTPUT with different evidence — but the codes
|
|
70
|
+
// stay defined for the lanes above.
|
|
59
71
|
PI_LAUNCH_FAILED: "pi-launch-failed",
|
|
60
72
|
PI_FAILED: "pi-failed",
|
|
61
|
-
// gentle-pi#367: a reviewer killed by the relay bound is not a crash. It
|
|
62
|
-
// is the one failure class that a byte-identical relaunch cannot survive,
|
|
63
|
-
// so it carries its own kind, its own elapsed/limit evidence, and its own
|
|
64
|
-
// continuation instead of hiding inside `pi-failed`.
|
|
65
73
|
PI_TIMED_OUT: "pi-timed-out",
|
|
66
74
|
PI_EMPTY_OUTPUT: "pi-empty-output",
|
|
67
|
-
// gentle-shell#1158 / #1136: a caller-owned
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
// never a mid-review transport
|
|
75
|
+
// gentle-shell#1158 / #1136 (superseded by gentle-pi#311 P2): a caller-owned
|
|
76
|
+
// reviewer selection that cannot possibly complete — no model registry, or
|
|
77
|
+
// a routing entry with no configured model — is a configuration failure,
|
|
78
|
+
// refused typed before anything runs, never a mid-review transport
|
|
79
|
+
// mystery. The in-process path has no ambient default model to fall back
|
|
80
|
+
// to, so a missing selection is refused here rather than launched anyway.
|
|
71
81
|
REVIEWER_CONFIG_INVALID: "reviewer-config-invalid",
|
|
82
|
+
// gentle-pi#311 P2 — in-process completion outcomes with no equivalent
|
|
83
|
+
// above (lib/inprocess-reviewer.ts#INPROCESS_REVIEWER_FAILURE).
|
|
84
|
+
REVIEWER_MODEL_NOT_FOUND: "reviewer-model-not-found",
|
|
85
|
+
REVIEWER_AUTH_UNAVAILABLE: "reviewer-auth-unavailable",
|
|
86
|
+
REVIEWER_THINKING_INVALID: "reviewer-thinking-invalid",
|
|
87
|
+
REVIEWER_TOOL_CALL: "reviewer-tool-call-attempted",
|
|
88
|
+
REVIEWER_EMPTY_OUTPUT: "reviewer-empty-output",
|
|
89
|
+
REVIEWER_OUTPUT_TOO_LARGE: "reviewer-output-too-large",
|
|
90
|
+
REVIEWER_ABORTED: "reviewer-aborted",
|
|
72
91
|
SUBMISSION_REFUSED: "submission-refused",
|
|
73
92
|
} as const;
|
|
74
93
|
export type ReviewHostRelayFailureKind = (typeof REVIEW_HOST_RELAY_FAILURE)[keyof typeof REVIEW_HOST_RELAY_FAILURE];
|
|
@@ -86,10 +105,11 @@ export class ReviewHostRelayError extends Error {
|
|
|
86
105
|
readonly exitCode: number | null;
|
|
87
106
|
readonly stderr: string;
|
|
88
107
|
readonly timedOut: boolean;
|
|
89
|
-
// Wall time the
|
|
90
|
-
// was measured against. Both are null only when
|
|
91
|
-
// Without them a transport failure cannot be told apart
|
|
92
|
-
// is what forced the gentle-pi#367 reporter to measure
|
|
108
|
+
// Wall time the aborted, timed-out, or failed reviewer completion actually
|
|
109
|
+
// consumed, and the bound it was measured against. Both are null only when
|
|
110
|
+
// no completion ran. Without them a transport failure cannot be told apart
|
|
111
|
+
// from a crash, which is what forced the gentle-pi#367 reporter to measure
|
|
112
|
+
// the relay by hand.
|
|
93
113
|
readonly elapsedMs: number | null;
|
|
94
114
|
readonly timeoutMs: number | null;
|
|
95
115
|
// "none" until the submission invocation launches; a launched submission
|
|
@@ -99,9 +119,9 @@ export class ReviewHostRelayError extends Error {
|
|
|
99
119
|
// "none" again: the provider states that the lens slot was not consumed
|
|
100
120
|
// (gentle-pi#522 / #524).
|
|
101
121
|
readonly mutationOutcome: "none" | "unknown";
|
|
102
|
-
/** What the reviewer
|
|
103
|
-
readonly reviewerEvidence:
|
|
104
|
-
constructor(kind: ReviewHostRelayFailureKind, stage: ReviewHostRelayStage, message: string, details?: { exitCode?: number | null; stderr?: string; timedOut?: boolean; elapsedMs?: number; timeoutMs?: number; mutationOutcome?: "none" | "unknown"; reviewerEvidence?:
|
|
122
|
+
/** What the in-process reviewer outcome carried as structured evidence (e.g. the completion's stopReason on an empty-output refusal). */
|
|
123
|
+
readonly reviewerEvidence: Record<string, unknown> | undefined;
|
|
124
|
+
constructor(kind: ReviewHostRelayFailureKind, stage: ReviewHostRelayStage, message: string, details?: { exitCode?: number | null; stderr?: string; timedOut?: boolean; elapsedMs?: number; timeoutMs?: number; mutationOutcome?: "none" | "unknown"; reviewerEvidence?: Record<string, unknown> }) {
|
|
105
125
|
super(message);
|
|
106
126
|
this.name = "ReviewHostRelayError";
|
|
107
127
|
this.kind = kind;
|
|
@@ -199,6 +219,16 @@ export interface ReviewHostRelaySlot {
|
|
|
199
219
|
readonly lens?: string;
|
|
200
220
|
readonly order?: string;
|
|
201
221
|
readonly subjectHash?: string;
|
|
222
|
+
/**
|
|
223
|
+
* Overrides the routing config key the reviewer selection resolves
|
|
224
|
+
* through (gentle-pi#311 P3). A lens slot leaves this unset and resolves
|
|
225
|
+
* through `lens` instead; a v9 host-mediated refuter/targeted-validator
|
|
226
|
+
* slot sets it to its fixed `review-refuter` / `review-validator` key,
|
|
227
|
+
* since those roles carry no per-slot lens identity.
|
|
228
|
+
*/
|
|
229
|
+
readonly routingKey?: string;
|
|
230
|
+
/** The provider-declared collect input name (e.g. `provider_refuter`), carried for diagnostics only. */
|
|
231
|
+
readonly name?: string;
|
|
202
232
|
}
|
|
203
233
|
|
|
204
234
|
function argumentValue(input: ReviewCollectInputV3, name: string): string | undefined {
|
|
@@ -260,6 +290,40 @@ export function reviewProviderRoleVectorSlots(inputs: readonly ReviewCollectInpu
|
|
|
260
290
|
}));
|
|
261
291
|
}
|
|
262
292
|
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
// Host-mediated provider role slots (gentle-pi#311 P3; provider contract
|
|
295
|
+
// v9) — the same two role capture operations above, but rendered exactly
|
|
296
|
+
// like a lens materialize slot: binding tokens plus `--agent=pi
|
|
297
|
+
// --materialize=true` (never `--execute`) and a provider-owned submission
|
|
298
|
+
// descriptor. These slots run through the SAME relay machinery a lens slot
|
|
299
|
+
// does (`prepareReviewHostRelaySlot` / `submitReviewHostRelayPreparedResult`)
|
|
300
|
+
// — there is no second relay. The only role-specific parts are the fixed
|
|
301
|
+
// `routingKey` (there is no per-slot lens identity to read one from) and the
|
|
302
|
+
// input's own schema, which already names refuter vs targeted-validator in
|
|
303
|
+
// every refusal that carries the request.
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
const REVIEW_HOST_MEDIATED_ROLE_ROUTING_KEY: Record<ReviewProviderRoleVectorSlot["captureOperation"], "review-refuter" | "review-validator"> = {
|
|
307
|
+
[REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_REFUTER]: "review-refuter",
|
|
308
|
+
[REVIEW_PROVIDER_ROLE_CAPTURE_OPERATION.CAPTURE_VALIDATION]: "review-validator",
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
export function isReviewHostMediatedRoleCollectInput(input: ReviewCollectInputV3): boolean {
|
|
312
|
+
return (REVIEW_PROVIDER_ROLE_CAPTURE_OPERATIONS as readonly string[]).includes(input.captureOperation)
|
|
313
|
+
&& argumentValue(input, "materialize") === "true"
|
|
314
|
+
&& argumentValue(input, "agent") === "pi"
|
|
315
|
+
&& input.submission !== undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function reviewHostMediatedRoleSlots(inputs: readonly ReviewCollectInputV3[]): readonly ReviewHostRelaySlot[] {
|
|
319
|
+
return inputs.filter((input) => isReviewHostMediatedRoleCollectInput(input)).map((input) => ({
|
|
320
|
+
captureArgumentTokens: input.arguments.map((argument) => renderToken(argument)),
|
|
321
|
+
submission: input.submission!,
|
|
322
|
+
routingKey: REVIEW_HOST_MEDIATED_ROLE_ROUTING_KEY[input.captureOperation as ReviewProviderRoleVectorSlot["captureOperation"]],
|
|
323
|
+
name: input.name,
|
|
324
|
+
}));
|
|
325
|
+
}
|
|
326
|
+
|
|
263
327
|
// Resolves the provider-owned submission form into an executable binding.
|
|
264
328
|
// Fails closed with a typed contract-mismatch error whenever the completing
|
|
265
329
|
// form is absent or cannot bind exactly one artifact value; the relay never
|
|
@@ -303,25 +367,35 @@ export interface ReviewHostRelayRequest {
|
|
|
303
367
|
readonly submission?: ReviewCaptureSubmissionV1;
|
|
304
368
|
/** Absolute path; defaults to the verified package-local binary. */
|
|
305
369
|
readonly gentleAiExecutable?: string;
|
|
306
|
-
/** User-owned pi launcher; defaults to `pi` on PATH. */
|
|
307
|
-
readonly piExecutable?: string;
|
|
308
370
|
readonly environment?: NodeJS.ProcessEnv;
|
|
309
371
|
readonly gentleAiTimeoutMs?: number;
|
|
310
372
|
/**
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
373
|
+
* The live model registry the reviewer completion resolves its selection
|
|
374
|
+
* and credentials through — structurally, pi's own `ModelRegistry`
|
|
375
|
+
* (`ctx.modelRegistry`). Absent is a typed refusal before materialize ever
|
|
376
|
+
* runs; the relay never falls back to a child process or an ambient
|
|
377
|
+
* default model (gentle-ai#4611; gentle-pi#311 P2).
|
|
315
378
|
*/
|
|
316
|
-
readonly
|
|
379
|
+
readonly reviewerRegistry?: InProcessReviewerRegistry;
|
|
317
380
|
/**
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
* absolute and exist; anything else is refused typed before launch.
|
|
381
|
+
* The lens's user-owned "provider/id" selection, read from the agent model
|
|
382
|
+
* routing config's `review-<lens>` entry. The relay never invents one: a
|
|
383
|
+
* routing entry with no configured model is refused typed before
|
|
384
|
+
* materialize, naming {@link ReviewHostRelayRequest.routingKey}.
|
|
323
385
|
*/
|
|
324
|
-
readonly
|
|
386
|
+
readonly selection?: string;
|
|
387
|
+
/** The routing entry's thinking label, forwarded verbatim to the completion. */
|
|
388
|
+
readonly thinking?: string;
|
|
389
|
+
/**
|
|
390
|
+
* The caller's live pi session id, forwarded into the in-process completion
|
|
391
|
+
* request so an OpenCode-routed reviewer model carries its
|
|
392
|
+
* `x-opencode-session` attribution header (a side-call bypasses pi's main
|
|
393
|
+
* agent loop, where pi adds those headers itself). Absent forwards nothing:
|
|
394
|
+
* no invented header, no error.
|
|
395
|
+
*/
|
|
396
|
+
readonly reviewerSessionId?: string;
|
|
397
|
+
/** Names the routing config key (e.g. "review-risk") in refusal messages; defaults to a generic label when absent. */
|
|
398
|
+
readonly routingKey?: string;
|
|
325
399
|
/**
|
|
326
400
|
* Overrides the reviewer bound entirely. Production leaves it unset and the
|
|
327
401
|
* relay derives the bound from the materialized prompt bytes and
|
|
@@ -356,7 +430,7 @@ export type ReviewHostRelaySubmissionRunner = (prepared: ReviewHostRelayPrepared
|
|
|
356
430
|
const DEFAULT_GENTLE_AI_TIMEOUT_MS = 120_000;
|
|
357
431
|
|
|
358
432
|
// ---------------------------------------------------------------------------
|
|
359
|
-
// The reviewer
|
|
433
|
+
// The reviewer completion bound (gentle-pi#367).
|
|
360
434
|
//
|
|
361
435
|
// The previous bound was a single hardcoded 600_000 ms reachable only through
|
|
362
436
|
// the test-injectable runner. A field-measured lens legitimately needed 478s
|
|
@@ -378,7 +452,7 @@ const DEFAULT_GENTLE_AI_TIMEOUT_MS = 120_000;
|
|
|
378
452
|
// established numeric-override shape (GENTLE_PI_CANDIDATE_GIT_TIMEOUT_MS,
|
|
379
453
|
// GENTLE_PI_REVIEW_MAX_BUFFER_BYTES): a positive decimal, silently ignored
|
|
380
454
|
// when malformed, and clamped to the same hard ceiling so no configuration can
|
|
381
|
-
// turn a foreground FINALIZE into an unbounded
|
|
455
|
+
// turn a foreground FINALIZE into an unbounded completion.
|
|
382
456
|
// ---------------------------------------------------------------------------
|
|
383
457
|
|
|
384
458
|
export const REVIEW_HOST_RELAY_PI_TIMEOUT_ENV = "GENTLE_PI_REVIEW_RELAY_PI_TIMEOUT_MS";
|
|
@@ -398,11 +472,12 @@ export function resolveReviewHostRelayPiTimeoutMs(promptByteLength: number, envi
|
|
|
398
472
|
return Math.min(scaled, REVIEW_HOST_RELAY_PI_TIMEOUT_MAX_MS);
|
|
399
473
|
}
|
|
400
474
|
|
|
401
|
-
// The reviewer ran out of time; it did not crash. The message
|
|
402
|
-
// measurements and names the two things that can change the
|
|
403
|
-
// the one thing that cannot is relaunching the identical
|
|
475
|
+
// The reviewer completion ran out of time; it did not crash. The message
|
|
476
|
+
// states both measurements and names the two things that can change the
|
|
477
|
+
// outcome, because the one thing that cannot is relaunching the identical
|
|
478
|
+
// slot.
|
|
404
479
|
export function reviewHostRelayPiTimeoutMessage(elapsedMs: number, timeoutMs: number, promptByteLength: number): string {
|
|
405
|
-
return `
|
|
480
|
+
return `the reviewer completion exceeded the relay bound: aborted after ${elapsedMs}ms against a ${timeoutMs}ms limit for a ${promptByteLength}-byte materialized prompt. `
|
|
406
481
|
+ `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.`;
|
|
407
482
|
}
|
|
408
483
|
|
|
@@ -463,54 +538,48 @@ function collectGentleAiProcess(
|
|
|
463
538
|
});
|
|
464
539
|
}
|
|
465
540
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
REVIEW_HOST_RELAY_FAILURE.
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
if (
|
|
508
|
-
error.kind === OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.LAUNCH_FAILED
|
|
509
|
-
|| error.kind === OPAQUE_PI_REVIEWER_TRANSPORT_FAILURE.SCRATCH_FAILED
|
|
510
|
-
) {
|
|
511
|
-
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_LAUNCH_FAILED, "pi", `pi subprocess could not start: ${error.message}`, details);
|
|
541
|
+
// Maps one in-process reviewer refusal onto a typed relay error. TIMED_OUT
|
|
542
|
+
// reuses PI_TIMED_OUT and PROVIDER_FAILED reuses PI_FAILED (identical
|
|
543
|
+
// semantics: a deterministic bound; a generic failure bucket); every other
|
|
544
|
+
// code gets its own REVIEWER_* kind with no prior equivalent.
|
|
545
|
+
function relayReviewerRefusalError(
|
|
546
|
+
outcome: Extract<InProcessReviewerOutcome, { kind: "refused" }>,
|
|
547
|
+
timing: { elapsedMs: number; timeoutMs: number },
|
|
548
|
+
promptByteLength: number,
|
|
549
|
+
): ReviewHostRelayError {
|
|
550
|
+
const details = { ...timing, ...(outcome.evidence === undefined ? {} : { reviewerEvidence: outcome.evidence }) };
|
|
551
|
+
const code: InProcessReviewerFailureCode = outcome.code;
|
|
552
|
+
switch (code) {
|
|
553
|
+
case INPROCESS_REVIEWER_FAILURE.MODEL_NOT_FOUND:
|
|
554
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.REVIEWER_MODEL_NOT_FOUND, "pi", outcome.message, details);
|
|
555
|
+
case INPROCESS_REVIEWER_FAILURE.AUTH_UNAVAILABLE:
|
|
556
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.REVIEWER_AUTH_UNAVAILABLE, "pi", outcome.message, details);
|
|
557
|
+
case INPROCESS_REVIEWER_FAILURE.THINKING_INVALID:
|
|
558
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.REVIEWER_THINKING_INVALID, "pi", outcome.message, details);
|
|
559
|
+
case INPROCESS_REVIEWER_FAILURE.TOOL_CALL_ATTEMPTED:
|
|
560
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.REVIEWER_TOOL_CALL, "pi", outcome.message, details);
|
|
561
|
+
case INPROCESS_REVIEWER_FAILURE.EMPTY_OUTPUT:
|
|
562
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.REVIEWER_EMPTY_OUTPUT, "pi", outcome.message, details);
|
|
563
|
+
case INPROCESS_REVIEWER_FAILURE.OUTPUT_TOO_LARGE:
|
|
564
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.REVIEWER_OUTPUT_TOO_LARGE, "pi", outcome.message, details);
|
|
565
|
+
case INPROCESS_REVIEWER_FAILURE.ABORTED:
|
|
566
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.REVIEWER_ABORTED, "pi", outcome.message, details);
|
|
567
|
+
case INPROCESS_REVIEWER_FAILURE.SELECTION_INVALID:
|
|
568
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.REVIEWER_CONFIG_INVALID, "pi", outcome.message, details);
|
|
569
|
+
case INPROCESS_REVIEWER_FAILURE.TIMED_OUT:
|
|
570
|
+
return new ReviewHostRelayError(
|
|
571
|
+
REVIEW_HOST_RELAY_FAILURE.PI_TIMED_OUT,
|
|
572
|
+
"pi",
|
|
573
|
+
reviewHostRelayPiTimeoutMessage(timing.elapsedMs, timing.timeoutMs, promptByteLength),
|
|
574
|
+
{ ...details, timedOut: true },
|
|
575
|
+
);
|
|
576
|
+
case INPROCESS_REVIEWER_FAILURE.PROVIDER_FAILED:
|
|
577
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_FAILED, "pi", outcome.message, details);
|
|
578
|
+
default: {
|
|
579
|
+
const unreachable: never = code;
|
|
580
|
+
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_FAILED, "pi", `unrecognized reviewer refusal code ${String(unreachable)}`, details);
|
|
581
|
+
}
|
|
512
582
|
}
|
|
513
|
-
return new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.PI_FAILED, "pi", "pi subprocess failed", details);
|
|
514
583
|
}
|
|
515
584
|
|
|
516
585
|
function assertTokens(name: string, tokens: readonly string[]): void {
|
|
@@ -520,66 +589,53 @@ function assertTokens(name: string, tokens: readonly string[]): void {
|
|
|
520
589
|
}
|
|
521
590
|
}
|
|
522
591
|
|
|
523
|
-
export const REVIEW_HOST_RELAY_EXTENSIONS_ENV = "GENTLE_PI_REVIEW_RELAY_EXTENSIONS";
|
|
524
|
-
|
|
525
592
|
/**
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
593
|
+
* Validates the caller-owned reviewer selection before any process launches: a
|
|
594
|
+
* missing model registry or a routing entry with no configured model is a
|
|
595
|
+
* typed refusal, never a mid-review transport failure and never a fallback to
|
|
596
|
+
* an ambient default model (gentle-ai#4611; gentle-pi#311 P2, superseding
|
|
597
|
+
* gentle-shell#1158 / #1136's child-process launch configuration).
|
|
530
598
|
*/
|
|
531
|
-
|
|
532
|
-
const
|
|
533
|
-
if (
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
function reviewerLaunchArguments(reviewerModel: string | undefined, reviewerExtensionPaths: readonly string[] | undefined): readonly string[] {
|
|
540
|
-
return [
|
|
541
|
-
...(reviewerModel === undefined ? [] : ["--model", reviewerModel]),
|
|
542
|
-
...(reviewerExtensionPaths ?? []).flatMap((path) => ["-e", path]),
|
|
543
|
-
];
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
function validateReviewerLaunchConfiguration(request: ReviewHostRelayRequest): { reviewerModel?: string; reviewerExtensionPaths?: readonly string[] } {
|
|
547
|
-
if (request.reviewerModel !== undefined) {
|
|
548
|
-
if (typeof request.reviewerModel !== "string" || request.reviewerModel.length === 0 || !SAFE_MODEL_ID_PATTERN.test(request.reviewerModel)) {
|
|
549
|
-
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.REVIEWER_CONFIG_INVALID, "pi", `Pi host relay reviewer launch configuration is invalid: the caller-owned reviewer selection ${JSON.stringify(request.reviewerModel)} is not a safe model id`);
|
|
550
|
-
}
|
|
599
|
+
function validateReviewerSelectionConfiguration(request: ReviewHostRelayRequest): { reviewerRegistry: InProcessReviewerRegistry; selection: string; thinking?: string; routingKey: string } {
|
|
600
|
+
const routingKey = typeof request.routingKey === "string" && request.routingKey.length > 0 ? request.routingKey : "review capture";
|
|
601
|
+
if (request.reviewerRegistry === undefined) {
|
|
602
|
+
throw new ReviewHostRelayError(
|
|
603
|
+
REVIEW_HOST_RELAY_FAILURE.REVIEWER_CONFIG_INVALID,
|
|
604
|
+
"pi",
|
|
605
|
+
`Pi host relay reviewer launch configuration is invalid: no model registry is available to complete ${routingKey}`,
|
|
606
|
+
);
|
|
551
607
|
}
|
|
552
|
-
if (request.
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.REVIEWER_CONFIG_INVALID, "pi", `Pi host relay reviewer launch configuration is invalid: the extension path ${JSON.stringify(path)} is not absolute`);
|
|
559
|
-
}
|
|
560
|
-
if (!existsSync(path)) {
|
|
561
|
-
throw new ReviewHostRelayError(REVIEW_HOST_RELAY_FAILURE.REVIEWER_CONFIG_INVALID, "pi", `Pi host relay reviewer launch configuration is invalid: the extension path ${JSON.stringify(path)} does not exist`);
|
|
562
|
-
}
|
|
563
|
-
}
|
|
608
|
+
if (typeof request.selection !== "string" || request.selection.length === 0) {
|
|
609
|
+
throw new ReviewHostRelayError(
|
|
610
|
+
REVIEW_HOST_RELAY_FAILURE.REVIEWER_CONFIG_INVALID,
|
|
611
|
+
"pi",
|
|
612
|
+
`Pi host relay reviewer launch configuration is invalid: no model is configured for ${routingKey}; assign it a model in the agent model routing config`,
|
|
613
|
+
);
|
|
564
614
|
}
|
|
565
615
|
return {
|
|
566
|
-
|
|
567
|
-
|
|
616
|
+
reviewerRegistry: request.reviewerRegistry,
|
|
617
|
+
selection: request.selection,
|
|
618
|
+
...(request.thinking === undefined ? {} : { thinking: request.thinking }),
|
|
619
|
+
...(request.reviewerSessionId === undefined ? {} : { reviewerSessionId: request.reviewerSessionId }),
|
|
620
|
+
routingKey,
|
|
568
621
|
};
|
|
569
622
|
}
|
|
570
623
|
|
|
571
624
|
function snapshotReviewHostRelayRequest(request: ReviewHostRelayRequest): ReviewHostRelayRequest {
|
|
625
|
+
// Structural malformations (empty/blank tokens, a non-absolute executable)
|
|
626
|
+
// are TypeErrors — a programmer mistake, never a typed relay refusal — and
|
|
627
|
+
// are checked before the business-level reviewer selection below.
|
|
572
628
|
assertTokens("capture", request.captureArgumentTokens);
|
|
629
|
+
const gentleAiExecutable = request.gentleAiExecutable ?? resolveGentleAiBinary();
|
|
630
|
+
if (!isAbsolute(gentleAiExecutable)) throw new TypeError("Pi host relay requires an absolute gentle-ai executable path");
|
|
573
631
|
// Caller-owned reviewer selection is validated before any process launches:
|
|
574
632
|
// a broken configuration is a typed refusal, never a mid-review transport
|
|
575
|
-
// failure (gentle-
|
|
576
|
-
const
|
|
633
|
+
// failure (gentle-pi#311 P2).
|
|
634
|
+
const reviewerSelection = validateReviewerSelectionConfiguration(request);
|
|
577
635
|
// The completing form is validated before any process launches: a materialize
|
|
578
636
|
// slot without a provider-owned submission is a typed contract mismatch,
|
|
579
637
|
// never a synthesized invocation.
|
|
580
638
|
resolveReviewHostRelaySubmission(request.submission);
|
|
581
|
-
const gentleAiExecutable = request.gentleAiExecutable ?? resolveGentleAiBinary();
|
|
582
|
-
if (!isAbsolute(gentleAiExecutable)) throw new TypeError("Pi host relay requires an absolute gentle-ai executable path");
|
|
583
639
|
const environment = Object.freeze({ ...(request.environment ?? process.env) }) as NodeJS.ProcessEnv;
|
|
584
640
|
const submission = request.submission === undefined ? undefined : Object.freeze({
|
|
585
641
|
operationToken: request.submission.operationToken,
|
|
@@ -590,7 +646,7 @@ function snapshotReviewHostRelayRequest(request: ReviewHostRelayRequest): Review
|
|
|
590
646
|
...request,
|
|
591
647
|
captureArgumentTokens: Object.freeze([...request.captureArgumentTokens]),
|
|
592
648
|
...(submission === undefined ? {} : { submission }),
|
|
593
|
-
...
|
|
649
|
+
...reviewerSelection,
|
|
594
650
|
gentleAiExecutable,
|
|
595
651
|
environment,
|
|
596
652
|
gentleAiTimeoutMs: request.gentleAiTimeoutMs ?? DEFAULT_GENTLE_AI_TIMEOUT_MS,
|
|
@@ -599,13 +655,14 @@ function snapshotReviewHostRelayRequest(request: ReviewHostRelayRequest): Review
|
|
|
599
655
|
}
|
|
600
656
|
|
|
601
657
|
/**
|
|
602
|
-
* Materializes one provider-bound reviewer prompt and runs
|
|
603
|
-
*
|
|
604
|
-
* finish before the caller performs provider-ordered
|
|
658
|
+
* Materializes one provider-bound reviewer prompt and runs it through one
|
|
659
|
+
* in-process reviewer completion. It does not submit anything, so independent
|
|
660
|
+
* reviewer work can finish before the caller performs provider-ordered
|
|
661
|
+
* admission.
|
|
605
662
|
*/
|
|
606
663
|
export async function prepareReviewHostRelaySlot(
|
|
607
664
|
request: ReviewHostRelayRequest,
|
|
608
|
-
|
|
665
|
+
runReviewer: typeof runInProcessReviewer = runInProcessReviewer,
|
|
609
666
|
): Promise<ReviewHostRelayPreparedResult> {
|
|
610
667
|
// Copy mutable transport configuration before the first async boundary. The
|
|
611
668
|
// supplied AbortSignal intentionally stays live across materialize, reviewer,
|
|
@@ -614,9 +671,14 @@ export async function prepareReviewHostRelaySlot(
|
|
|
614
671
|
|
|
615
672
|
// The provider materializes the opaque prompt and detects whether this relay
|
|
616
673
|
// surface is available. No version sniffing or prompt reconstruction occurs.
|
|
674
|
+
// The materialize subcommand is the provider's own submission operation
|
|
675
|
+
// token (validated present above): "capture-result" for a lens slot,
|
|
676
|
+
// "capture-refuter" or "capture-validation" for a v9 host-mediated role
|
|
677
|
+
// slot — the provider always names the same operation for both the
|
|
678
|
+
// materialize and the submit leg of one slot.
|
|
617
679
|
let materialized: ProcessCapture;
|
|
618
680
|
try {
|
|
619
|
-
materialized = await collectGentleAiProcess(preparedRequest.gentleAiExecutable!, ["review",
|
|
681
|
+
materialized = await collectGentleAiProcess(preparedRequest.gentleAiExecutable!, ["review", preparedRequest.submission!.operationToken, ...preparedRequest.captureArgumentTokens], {
|
|
620
682
|
cwd: preparedRequest.targetCwd!,
|
|
621
683
|
env: { ...preparedRequest.environment!, [GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV]: GENTLE_PI_REVIEW_RELAY_CONTRACT },
|
|
622
684
|
timeoutMs: preparedRequest.gentleAiTimeoutMs!,
|
|
@@ -663,27 +725,44 @@ export async function prepareReviewHostRelaySlot(
|
|
|
663
725
|
// both the user-owned environment override and the scale-derived bound.
|
|
664
726
|
const piTimeoutMs = preparedRequest.piTimeoutMs ?? resolveReviewHostRelayPiTimeoutMs(promptBytes.length, preparedRequest.environment);
|
|
665
727
|
|
|
666
|
-
// The
|
|
667
|
-
//
|
|
668
|
-
|
|
669
|
-
|
|
728
|
+
// The completion runs in-process through the live model registry: no
|
|
729
|
+
// child, no extension allowlist, no ambient default model. Elapsed time is
|
|
730
|
+
// measured here (there is no killed process to read it from) so a timed-out
|
|
731
|
+
// or aborted refusal still carries the same elapsed/limit evidence a killed
|
|
732
|
+
// child used to.
|
|
733
|
+
const startedAt = Date.now();
|
|
734
|
+
let outcome: InProcessReviewerOutcome;
|
|
670
735
|
try {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
736
|
+
outcome = await runReviewer(
|
|
737
|
+
{
|
|
738
|
+
selection: preparedRequest.selection!,
|
|
739
|
+
...(preparedRequest.thinking === undefined ? {} : { thinking: preparedRequest.thinking }),
|
|
740
|
+
...(preparedRequest.reviewerSessionId === undefined ? {} : { sessionId: preparedRequest.reviewerSessionId }),
|
|
741
|
+
prompt: promptBytes,
|
|
742
|
+
timeoutMs: piTimeoutMs,
|
|
743
|
+
...(preparedRequest.signal === undefined ? {} : { signal: preparedRequest.signal }),
|
|
744
|
+
routingKey: preparedRequest.routingKey!,
|
|
745
|
+
},
|
|
746
|
+
{ registry: preparedRequest.reviewerRegistry!, complete: completeSimple },
|
|
747
|
+
);
|
|
678
748
|
} catch (error) {
|
|
679
|
-
throw
|
|
749
|
+
throw new ReviewHostRelayError(
|
|
750
|
+
REVIEW_HOST_RELAY_FAILURE.PI_FAILED,
|
|
751
|
+
"pi",
|
|
752
|
+
`the reviewer completion could not run: ${error instanceof Error ? error.message : String(error)}`,
|
|
753
|
+
{ elapsedMs: Date.now() - startedAt, timeoutMs: piTimeoutMs },
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
if (outcome.kind === "refused") {
|
|
757
|
+
throw relayReviewerRefusalError(outcome, { elapsedMs: Date.now() - startedAt, timeoutMs: piTimeoutMs }, promptBytes.length);
|
|
680
758
|
}
|
|
759
|
+
const resultBytes = Buffer.from(outcome.text, "utf8");
|
|
681
760
|
const prepared = Object.freeze({
|
|
682
761
|
request: preparedRequest,
|
|
683
762
|
promptByteLength: promptBytes.length,
|
|
684
|
-
resultByteLength:
|
|
763
|
+
resultByteLength: resultBytes.length,
|
|
685
764
|
});
|
|
686
|
-
preparedResultBytes.set(prepared,
|
|
765
|
+
preparedResultBytes.set(prepared, resultBytes);
|
|
687
766
|
return prepared;
|
|
688
767
|
}
|
|
689
768
|
|
|
@@ -777,9 +856,15 @@ export async function submitReviewHostRelayPreparedResult(prepared: ReviewHostRe
|
|
|
777
856
|
}
|
|
778
857
|
|
|
779
858
|
/**
|
|
780
|
-
* Compatibility one-binding path: materialize →
|
|
781
|
-
* It preserves the established API and its typed
|
|
859
|
+
* Compatibility one-binding path: materialize → in-process reviewer
|
|
860
|
+
* completion → submit. It preserves the established API and its typed
|
|
861
|
+
* failure behavior exactly. `runReviewer` is the same test seam
|
|
862
|
+
* {@link prepareReviewHostRelaySlot} takes, threaded through so a caller never
|
|
863
|
+
* needs to call the two-step path just to inject a fake completion.
|
|
782
864
|
*/
|
|
783
|
-
export async function runReviewHostRelaySlot(
|
|
784
|
-
|
|
865
|
+
export async function runReviewHostRelaySlot(
|
|
866
|
+
request: ReviewHostRelayRequest,
|
|
867
|
+
runReviewer: typeof runInProcessReviewer = runInProcessReviewer,
|
|
868
|
+
): Promise<ReviewHostRelayResult> {
|
|
869
|
+
return await submitReviewHostRelayPreparedResult(await prepareReviewHostRelaySlot(request, runReviewer));
|
|
785
870
|
}
|