pi-crew 0.11.11 → 0.11.13
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/CHANGELOG.md +102 -0
- package/dist/index.mjs +551 -519
- package/package.json +1 -1
- package/src/config/config-validation.ts +9 -0
- package/src/config/defaults.ts +7 -0
- package/src/config/env-vars.ts +24 -0
- package/src/config/types.ts +11 -0
- package/src/extension/registration/tool-registration.ts +7 -0
- package/src/extension/registration/verify-gate-tool.ts +263 -0
- package/src/extension/team-tool/run-deadline.ts +22 -1
- package/src/extension/team-tool/run.ts +38 -2
- package/src/extension/team-tool-types.ts +4 -0
- package/src/extension/team-tool.ts +31 -3
- package/src/prompt/inbox-poll.ts +141 -3
- package/src/runtime/broker/crew-broker.ts +48 -2
- package/src/runtime/broker/protocol/lock-busy.ts +47 -1
- package/src/runtime/broker/protocol/request-parsers.ts +8 -1
- package/src/runtime/budget-enforcement.ts +36 -20
- package/src/runtime/child-pi/child-pi-constants.ts +24 -0
- package/src/runtime/child-pi/child-pi-kill.ts +59 -2
- package/src/runtime/child-pi/child-pi-rpc.ts +1021 -0
- package/src/runtime/child-pi/child-pi-steering.ts +10 -8
- package/src/runtime/child-pi/child-pi-streams.ts +69 -19
- package/src/runtime/child-pi/child-pi.ts +29 -30
- package/src/runtime/child-pi/owned-process.ts +434 -0
- package/src/runtime/child-pi/postmortem-registry.ts +134 -0
- package/src/runtime/classifier/ambient-noise-gate.ts +126 -0
- package/src/runtime/classifier/classifier-service.ts +277 -61
- package/src/runtime/dispatch-batch.ts +32 -0
- package/src/runtime/live-session/live-agent-control.ts +2 -0
- package/src/runtime/live-session/live-agent-manager.ts +72 -2
- package/src/runtime/live-session/live-session-runtime.ts +435 -40
- package/src/runtime/merge-loop.ts +63 -35
- package/src/runtime/recovery/crash-recovery.ts +141 -3
- package/src/runtime/recovery/retry-executor.ts +29 -0
- package/src/runtime/run-tracker.ts +28 -0
- package/src/runtime/stale-reconciler.ts +36 -1
- package/src/runtime/task-runner.ts +103 -0
- package/src/runtime/verification/classifier-pre-gate.ts +262 -0
- package/src/runtime/verification/verification-gates.ts +5 -1
- package/src/runtime/verification/verify-gate.ts +394 -0
- package/src/schema/config-schema.ts +13 -0
- package/src/state/contracts.ts +18 -0
- package/src/state/coordination/mailbox.ts +203 -55
- package/src/state/stores/sqlite-run-state.ts +378 -0
- package/src/state/stores/state-store.ts +134 -6
- package/src/ui/agents-jobs-browser.ts +154 -8
- package/src/ui/events-state-source.ts +358 -0
- package/src/ui/run-snapshot-cache.ts +87 -3
- package/src/ui/widget/widget-model.ts +13 -2
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* U6B (upgrade spec 2026-10-09, §U6 Phase B): classifier VERIFIER PRE-GATE —
|
|
3
|
+
* the third consumer of the P2-1 host classifier seam, alongside retry-triage
|
|
4
|
+
* (child-executor.ts) and the ambient-noise gate (ambient-noise-gate.ts).
|
|
5
|
+
*
|
|
6
|
+
* Semantics: BEFORE an LLM verifier is spawned, the already-produced
|
|
7
|
+
* STRUCTURED verdict state is scored by the cheap host classifier — ONE
|
|
8
|
+
* `score` question (classifyScore, the graded sibling of classifyBool added
|
|
9
|
+
* by U6B) over JSON state ONLY:
|
|
10
|
+
* { verdict, evidenceLines, changedFiles }
|
|
11
|
+
* The classifier NEVER sees a diff, file contents, or prose transcripts — it
|
|
12
|
+
* judges whether the structured state alone is decisive enough to skip the
|
|
13
|
+
* LLM verifier spawn ("verdict được pre-gate chặn"). Every other outcome
|
|
14
|
+
* escalates to the LLM verifier, exactly the pre-U6B spawn decision.
|
|
15
|
+
*
|
|
16
|
+
* ESCALATION POLICY (spec §U6 Phase B, mandatory — "escalate LLM khi
|
|
17
|
+
* confidence thấp hoặc verdict FAILED"):
|
|
18
|
+
* - verdict failure-shaped (FAILED/error/blocked/...) or not clearly
|
|
19
|
+
* pass-shaped (INCONCLUSIVE/blank) → escalate (a cheap classifier may
|
|
20
|
+
* never rubber-stamp a failure or an ambiguous verdict);
|
|
21
|
+
* - provider confidence below BLOCK_CONFIDENCE_THRESHOLD (or unreported)
|
|
22
|
+
* → escalate;
|
|
23
|
+
* - score below BLOCK_SCORE_THRESHOLD (or outside 0..1) → escalate;
|
|
24
|
+
* - EVERY classifier failure mode (registry missing, `opencode` provider
|
|
25
|
+
* unconfigured → getAvailableOfType("classifier") = [], classify threw,
|
|
26
|
+
* stopReason error, answer missing) → escalate, soft.
|
|
27
|
+
*
|
|
28
|
+
* SOFT-BRANCH CONTRACT (spec caveat, mandatory):
|
|
29
|
+
* - `opencode` provider is sometimes unconfigured on this host — the
|
|
30
|
+
* pre-gate then degrades to "always escalate" (identical to pre-U6B
|
|
31
|
+
* behavior);
|
|
32
|
+
* - a WRONG classifier verdict can only cost ONE extra escalate (the LLM
|
|
33
|
+
* verifier runs anyway) or skip one spawn whose structured state showed a
|
|
34
|
+
* high-confidence unambiguous pass — it can NEVER fail a task. This
|
|
35
|
+
* module never throws up to the caller.
|
|
36
|
+
*
|
|
37
|
+
* Metrics (spec acceptance: % verdicts blocked by the pre-gate, % escalate,
|
|
38
|
+
* zero task-fail due to classifier — flag OFF by default until data exists):
|
|
39
|
+
* - crew.classifier.calls_total{consumer="verifier_pre_gate",outcome} +
|
|
40
|
+
* crew.classifier.confidence via classifyScore (service level);
|
|
41
|
+
* - crew.classifier.decisions_total{consumer="verifier_pre_gate",
|
|
42
|
+
* decision=<action>} + a `task.verifier_pre_gate` diagnostic event ONLY
|
|
43
|
+
* when a real classifier answer informed the decision (fallback paths are
|
|
44
|
+
* already counted by the service's calls_total and must not double-report
|
|
45
|
+
* a decision they did not make). Percentages derive from decisions_total.
|
|
46
|
+
* NOTE: `task.verifier_pre_gate` is NOT yet in TEAM_EVENT_TYPES
|
|
47
|
+
* (src/state/contracts.ts) — src/state/** is outside U6B's file
|
|
48
|
+
* ownership, so registration is deferred to the U13 mount commit (see
|
|
49
|
+
* MOUNT POINT below); scripts/check-event-types-registry.mjs runs
|
|
50
|
+
* report-only today.
|
|
51
|
+
*
|
|
52
|
+
* Concurrency: the seam caps host classify at 4 parallel calls; each consumer
|
|
53
|
+
* (retry-triage, ambient-noise gate, this pre-gate) issues at most ONE
|
|
54
|
+
* concurrent call, so the aggregate stays within the budget.
|
|
55
|
+
*
|
|
56
|
+
* ── MOUNT POINT (U13 lane — this module is intentionally NOT wired yet) ──
|
|
57
|
+
* The verifier spawn path stays UNTOUCHED by U6B per lane ownership:
|
|
58
|
+
* - verification-gates.ts command-gate spawn logic: NOT modified here
|
|
59
|
+
* (U13 owns the mount, spec §U13);
|
|
60
|
+
* - goal-loop judge spawn (goal-evaluator.ts evaluateGoal → runWorker):
|
|
61
|
+
* NOT modified here.
|
|
62
|
+
* When U13 mounts the deterministic ctx.executeTool gates, insert this
|
|
63
|
+
* pre-gate in the decision flow:
|
|
64
|
+
* 1. run the deterministic gates (U13: typecheck / test:critical / grep
|
|
65
|
+
* evidence via ctx.executeTool) and build the structured verdict state:
|
|
66
|
+
* { verdict: "PASS"|"FAILED"|..., evidenceLines: [...], changedFiles: [...] };
|
|
67
|
+
* 2. BEFORE spawning the LLM verifier:
|
|
68
|
+
* const preGate = await classifyVerifierPreGate({
|
|
69
|
+
* enabled: resolveClassifierEnabled(runtime.classifierEnabled), // default false
|
|
70
|
+
* modelRegistry: ctx.modelRegistry,
|
|
71
|
+
* classifierModel: resolveClassifierModel(runtime.classifierModel),
|
|
72
|
+
* verdict, eventsPath, runId, metricRegistry,
|
|
73
|
+
* });
|
|
74
|
+
* — preGate.blockVerifier === true → SKIP the LLM verifier spawn (the
|
|
75
|
+
* structured pass stands; mark the verdict's evaluator/model provenance
|
|
76
|
+
* e.g. "classifier-pre-gate/<model>" so consumers can tell);
|
|
77
|
+
* — otherwise → spawn the LLM verifier as today (optionally attach the
|
|
78
|
+
* pre-gate score/confidence to the verifier's prompt context).
|
|
79
|
+
* The args/result interfaces below are the stable contract for that mount;
|
|
80
|
+
* changes need both lanes' review.
|
|
81
|
+
*/
|
|
82
|
+
import type { MetricRegistry } from "../../observability/metric-registry.ts";
|
|
83
|
+
import { appendEventAsync } from "../../state/event-log/event-log.ts";
|
|
84
|
+
import { logInternalError } from "../../utils/internal-error.ts";
|
|
85
|
+
import { classifyScore } from "../classifier/classifier-service.ts";
|
|
86
|
+
|
|
87
|
+
/** Structured verdict state the classifier reasons over — JSON state ONLY
|
|
88
|
+
* (spec: "chỉ JSON state, KHÔNG phán diff"). Built by the deterministic
|
|
89
|
+
* gates (U13) / the verdict-producing step; never diffs, never file
|
|
90
|
+
* contents — only labels, evidence lines and file NAMES. */
|
|
91
|
+
export interface VerifierPreGateVerdictState {
|
|
92
|
+
/** Short structured verdict label, e.g. "PASS", "FAILED", "INCONCLUSIVE". */
|
|
93
|
+
verdict: string;
|
|
94
|
+
/** Structured evidence lines (command exit codes, artifact refs). Capped before classify. */
|
|
95
|
+
evidenceLines: string[];
|
|
96
|
+
/** Changed-file paths (names only). Capped before classify. */
|
|
97
|
+
changedFiles: string[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type VerifierPreGateAction =
|
|
101
|
+
| "disabled"
|
|
102
|
+
| "blocked"
|
|
103
|
+
| "escalate_verdict_failed"
|
|
104
|
+
| "escalate_low_confidence"
|
|
105
|
+
| "escalate_low_score"
|
|
106
|
+
| "escalate_fallback";
|
|
107
|
+
|
|
108
|
+
/** Default score at/above which a pass-shaped verdict blocks the LLM verifier spawn. */
|
|
109
|
+
export const VERIFIER_PRE_GATE_BLOCK_SCORE_THRESHOLD = 0.9;
|
|
110
|
+
/** Default provider confidence at/above which blocking is allowed. An
|
|
111
|
+
* unreported confidence NEVER blocks (defensive: block is the risky branch). */
|
|
112
|
+
export const VERIFIER_PRE_GATE_BLOCK_CONFIDENCE_THRESHOLD = 0.8;
|
|
113
|
+
|
|
114
|
+
const VERDICT_MAX_CHARS = 200;
|
|
115
|
+
const EVIDENCE_MAX_LINES = 8;
|
|
116
|
+
const EVIDENCE_LINE_MAX_CHARS = 200;
|
|
117
|
+
const CHANGED_FILES_MAX = 16;
|
|
118
|
+
|
|
119
|
+
const VERDICT_FAIL_RE = /\b(fail(?:ed|ure)?|error(?:ed)?|blocked|fatal|timeout|timed[- ]out|red)\b/i;
|
|
120
|
+
const VERDICT_PASS_RE = /\b(pass(?:ed)?|green|succeed(?:ed)?|success(?:ful)?|ok|achieved|complete(?:d)?)\b/i;
|
|
121
|
+
|
|
122
|
+
/** A verdict reaches the block path ONLY when unambiguously pass-shaped:
|
|
123
|
+
* matches PASS vocabulary AND carries no failure vocabulary. Explicit
|
|
124
|
+
* failures and ambiguous labels ("INCONCLUSIVE", blank) both escalate. */
|
|
125
|
+
function isPassShapedVerdict(verdict: string): boolean {
|
|
126
|
+
return VERDICT_PASS_RE.test(verdict) && !VERDICT_FAIL_RE.test(verdict);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Cap the classifier state — classifiers reason over small low-cardinality
|
|
130
|
+
* JSON; the verdict fields are worker-influenced and can be arbitrarily
|
|
131
|
+
* long, so every string is bounded before it reaches the classify call. */
|
|
132
|
+
function capVerdictState(verdict: VerifierPreGateVerdictState): Record<string, unknown> {
|
|
133
|
+
return {
|
|
134
|
+
verdict: verdict.verdict.slice(0, VERDICT_MAX_CHARS),
|
|
135
|
+
evidenceLines: verdict.evidenceLines.slice(0, EVIDENCE_MAX_LINES).map((line) => line.slice(0, EVIDENCE_LINE_MAX_CHARS)),
|
|
136
|
+
changedFiles: verdict.changedFiles.slice(0, CHANGED_FILES_MAX).map((file) => file.slice(0, EVIDENCE_LINE_MAX_CHARS)),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface VerifierPreGateArgs {
|
|
141
|
+
/** Pre-resolved gate (resolveClassifierEnabled) — false short-circuits before ANY registry touch (dormant default). */
|
|
142
|
+
enabled: boolean;
|
|
143
|
+
/** Host-process model registry (ctx.modelRegistry, threaded unknown). */
|
|
144
|
+
modelRegistry: unknown;
|
|
145
|
+
/** Pre-resolved classifier model id (resolveClassifierModel). */
|
|
146
|
+
classifierModel: string;
|
|
147
|
+
/** Structured verdict state (JSON state only — never diffs). */
|
|
148
|
+
verdict: VerifierPreGateVerdictState;
|
|
149
|
+
/** Score at/above which a pass-shaped verdict blocks the spawn (default VERIFIER_PRE_GATE_BLOCK_SCORE_THRESHOLD). */
|
|
150
|
+
blockScoreThreshold?: number;
|
|
151
|
+
/** Confidence at/above which blocking is allowed (default VERIFIER_PRE_GATE_BLOCK_CONFIDENCE_THRESHOLD). */
|
|
152
|
+
blockConfidenceThreshold?: number;
|
|
153
|
+
/** Event-log identity for the task.verifier_pre_gate diagnostic event. */
|
|
154
|
+
eventsPath: string;
|
|
155
|
+
runId: string;
|
|
156
|
+
/** Optional task id (the goal-loop judge is not a task — omit there). */
|
|
157
|
+
taskId?: string;
|
|
158
|
+
/** Optional host metric registry (crew.classifier.* counters/histograms). */
|
|
159
|
+
metricRegistry?: MetricRegistry;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface VerifierPreGateResult {
|
|
163
|
+
/** true → skip the LLM verifier spawn: the structured verdict stands as a pass. */
|
|
164
|
+
blockVerifier: boolean;
|
|
165
|
+
/** true ONLY when a real classifier answer informed the decision (event + decision metric emitted). */
|
|
166
|
+
consulted: boolean;
|
|
167
|
+
/** Which branch produced the outcome (metric label + event data). */
|
|
168
|
+
action: VerifierPreGateAction;
|
|
169
|
+
/** Classifier score reading, when consulted. */
|
|
170
|
+
score: number | undefined;
|
|
171
|
+
/** Provider-reported confidence, when consulted and reported. */
|
|
172
|
+
confidence: number | undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Ask the verifier pre-gate. NEVER REJECTS and never throws up to the
|
|
177
|
+
* caller — the disabled path returns without touching the registry (zero
|
|
178
|
+
* behavior change: flag off by default), every classifier failure mode falls
|
|
179
|
+
* back to "spawn the LLM verifier" (blockVerifier false).
|
|
180
|
+
*/
|
|
181
|
+
export async function classifyVerifierPreGate(args: VerifierPreGateArgs): Promise<VerifierPreGateResult> {
|
|
182
|
+
if (!args.enabled) {
|
|
183
|
+
return { blockVerifier: false, consulted: false, action: "disabled", score: undefined, confidence: undefined };
|
|
184
|
+
}
|
|
185
|
+
const scoreThreshold = args.blockScoreThreshold ?? VERIFIER_PRE_GATE_BLOCK_SCORE_THRESHOLD;
|
|
186
|
+
const confidenceThreshold = args.blockConfidenceThreshold ?? VERIFIER_PRE_GATE_BLOCK_CONFIDENCE_THRESHOLD;
|
|
187
|
+
const result = await classifyScore({
|
|
188
|
+
modelRegistry: args.modelRegistry,
|
|
189
|
+
classifierModel: args.classifierModel,
|
|
190
|
+
questionKey: "decisive",
|
|
191
|
+
question: {
|
|
192
|
+
type: "score",
|
|
193
|
+
instructions:
|
|
194
|
+
"A unit of work reports a STRUCTURED verification verdict (JSON state only — you see no diffs and no file contents). Spawning an expensive LLM verifier is being considered. Score how DECISIVELY the structured state ALONE establishes that the work genuinely PASSED verification.",
|
|
195
|
+
criteria: [
|
|
196
|
+
"1.0 — the evidence lines conclusively prove the pass (explicit passing command exits, concrete artifact refs) and the changed-files list matches the work's expected scope",
|
|
197
|
+
"0.75 — evidence is strong but at least one line is a claim rather than an artifact",
|
|
198
|
+
"0.5 — evidence is mixed: some lines pass, others are inconclusive",
|
|
199
|
+
"0.25 — evidence is thin, generic, or mostly absent",
|
|
200
|
+
"0.0 — the state proves nothing usable, or any line already indicates a failure",
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
state: capVerdictState(args.verdict),
|
|
204
|
+
fallback: 0,
|
|
205
|
+
metricRegistry: args.metricRegistry,
|
|
206
|
+
metricLabels: { consumer: "verifier_pre_gate" },
|
|
207
|
+
});
|
|
208
|
+
if (!result.fromClassifier) {
|
|
209
|
+
// Soft: every classifier failure mode (provider unconfigured, thrown
|
|
210
|
+
// call, malformed answer) escalates — identical to the pre-U6B spawn
|
|
211
|
+
// decision. The service's calls_total already counted the outcome; no
|
|
212
|
+
// decision was made, so no decision metric/event (noise-gate pattern).
|
|
213
|
+
return { blockVerifier: false, consulted: false, action: "escalate_fallback", score: undefined, confidence: undefined };
|
|
214
|
+
}
|
|
215
|
+
let action: VerifierPreGateAction;
|
|
216
|
+
if (!isPassShapedVerdict(args.verdict.verdict)) {
|
|
217
|
+
// Spec: escalate when verdict FAILED — and defensively for anything not
|
|
218
|
+
// unambiguously pass-shaped. Ordered FIRST: a cheap classifier may
|
|
219
|
+
// never rubber-stamp a failure, whatever its score/confidence.
|
|
220
|
+
action = "escalate_verdict_failed";
|
|
221
|
+
} else if (result.confidence === undefined || result.confidence < confidenceThreshold) {
|
|
222
|
+
// Spec: escalate when confidence thấp. An unreported confidence counts
|
|
223
|
+
// as low — blocking is the risky branch and needs positive certainty.
|
|
224
|
+
action = "escalate_low_confidence";
|
|
225
|
+
} else if (!Number.isFinite(result.score) || result.score < 0 || result.score > 1 || result.score < scoreThreshold) {
|
|
226
|
+
// Spec: block only on a decisively high score. Out-of-range scores are
|
|
227
|
+
// treated as non-decisive (defensive) — malformed can only escalate.
|
|
228
|
+
action = "escalate_low_score";
|
|
229
|
+
} else {
|
|
230
|
+
action = "blocked";
|
|
231
|
+
}
|
|
232
|
+
const blockVerifier = action === "blocked";
|
|
233
|
+
try {
|
|
234
|
+
args.metricRegistry
|
|
235
|
+
?.counter("crew.classifier.decisions_total", "Classifier-informed decisions by consumer and verdict")
|
|
236
|
+
.inc({ consumer: "verifier_pre_gate", decision: action });
|
|
237
|
+
} catch (metricError) {
|
|
238
|
+
logInternalError("classifier-pre-gate.metric", metricError, `decision=${action}`, "warn");
|
|
239
|
+
}
|
|
240
|
+
void appendEventAsync(args.eventsPath, {
|
|
241
|
+
type: "task.verifier_pre_gate",
|
|
242
|
+
runId: args.runId,
|
|
243
|
+
...(args.taskId !== undefined ? { taskId: args.taskId } : {}),
|
|
244
|
+
message: blockVerifier
|
|
245
|
+
? "Verifier pre-gate BLOCKED the LLM verifier spawn (structured pass stands)"
|
|
246
|
+
: `Verifier pre-gate escalated to the LLM verifier (${action})`,
|
|
247
|
+
data: {
|
|
248
|
+
action,
|
|
249
|
+
verdict: args.verdict.verdict.slice(0, VERDICT_MAX_CHARS),
|
|
250
|
+
score: result.score,
|
|
251
|
+
confidence: result.confidence,
|
|
252
|
+
classifierModel: result.usedModel,
|
|
253
|
+
configuredModel: args.classifierModel,
|
|
254
|
+
stopReason: result.stopReason,
|
|
255
|
+
evidenceLineCount: args.verdict.evidenceLines.length,
|
|
256
|
+
changedFileCount: args.verdict.changedFiles.length,
|
|
257
|
+
},
|
|
258
|
+
}).catch(() => {
|
|
259
|
+
/* no-op: best-effort diagnostic append, ignore delivery errors */
|
|
260
|
+
});
|
|
261
|
+
return { blockVerifier, consulted: true, action, score: result.score, confidence: result.confidence };
|
|
262
|
+
}
|
|
@@ -239,7 +239,11 @@ function validateGateCommand(command: string): void {
|
|
|
239
239
|
validateAllowedGateCommands(normalized, command);
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
|
|
242
|
+
/** U13 (upgrade spec 2026-10-09): exported so the deterministic verify_gate
|
|
243
|
+
* engine (verify-gate.ts) reuses the SAME hardened command runner the
|
|
244
|
+
* verifier flow already uses — command validation, sanitized env, process-
|
|
245
|
+
* group kill, output cap, timeout. Additive export, no behavior change. */
|
|
246
|
+
export async function executeCommand(
|
|
243
247
|
command: string,
|
|
244
248
|
cwd: string,
|
|
245
249
|
timeoutMs: number = 120000,
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* U13 (upgrade spec 2026-10-09 §U13): DETERMINISTIC VERIFIER PRE-GATE ENGINE.
|
|
3
|
+
*
|
|
4
|
+
* The spec's probe (round 10, 13/13 checks) proved extension tools can run
|
|
5
|
+
* other tools deterministically in the host process via
|
|
6
|
+
* `ctx.executeTool("bash", ...)` — the 5th parameter of ToolDefinition.execute
|
|
7
|
+
* (nested id `<parent>/1`, errors arrive as isError VALUES not throws,
|
|
8
|
+
* `exit_code` readable in structuredContent; limits 256 nested calls / 8KB
|
|
9
|
+
* per call / 32KB total args). That unlocks running the verifier's
|
|
10
|
+
* deterministic checks (typecheck / test:critical / grep evidence lines) for
|
|
11
|
+
* FREE before deciding whether the expensive LLM verifier worker must spawn.
|
|
12
|
+
*
|
|
13
|
+
* TWO ENTRY POINTS share this pure engine:
|
|
14
|
+
* 1. the `verify_gate` TOOL (src/extension/registration/verify-gate-tool.ts)
|
|
15
|
+
* — runs checks via ctx.executeTool("bash", ...) in the host process,
|
|
16
|
+
* exposure "codemode" (hidden from the model, callable by other tools);
|
|
17
|
+
* 2. the verifier SPAWN SITE (task-runner.ts runTeamTask) — runs the same
|
|
18
|
+
* engine through the hardened subprocess runner already exported by
|
|
19
|
+
* verification-gates.ts (executeCommand: command validation, sanitized
|
|
20
|
+
* env, process-group kill, output cap, timeout).
|
|
21
|
+
*
|
|
22
|
+
* TWO-LAYER DECISION (spec: "kết hợp classifier pre-gate U6-B — deterministic
|
|
23
|
+
* trước, classifier sau"):
|
|
24
|
+
* - Layer 1 (deterministic, this module): gate PASS → SKIP the LLM verifier
|
|
25
|
+
* (U13 acceptance: "một run verification với gate xanh không spawn LLM
|
|
26
|
+
* verifier"); gate FAILED → spawn the LLM verifier WITH the gate results
|
|
27
|
+
* in its prompt (it must diagnose/report, not re-derive exit codes).
|
|
28
|
+
* - Layer 2 (classifier, U6B classifier-pre-gate.ts): consulted ONLY when
|
|
29
|
+
* layer 1 is INCONCLUSIVE (no checks resolved — e.g. a repo without
|
|
30
|
+
* typecheck/test:critical scripts). A decisively-high classifier score
|
|
31
|
+
* over the STRUCTURED verdict state may block the spawn; every other
|
|
32
|
+
* outcome escalates. A FAILED gate never reaches the classifier — a cheap
|
|
33
|
+
* classifier may never rubber-stamp a failure (U6B escalation policy),
|
|
34
|
+
* so consulting it there would only burn a call.
|
|
35
|
+
*
|
|
36
|
+
* SAFETY: a gate that cannot run (disabled by env, no checks resolved,
|
|
37
|
+
* executor failure) degrades to "spawn the LLM verifier as today" — the
|
|
38
|
+
* pre-gate can only SAVE a spawn on positive deterministic evidence, never
|
|
39
|
+
* fail a task on its own.
|
|
40
|
+
*
|
|
41
|
+
* Metrics (spec acceptance: "metrics số verifier job tiết kiệm"):
|
|
42
|
+
* - crew.verification.verify_gate_runs_total{verdict} — gate executions;
|
|
43
|
+
* - crew.verification.verifier_spawns_skipped_total{reason} — LLM verifier
|
|
44
|
+
* spawns saved (the "tiết kiệm" counter);
|
|
45
|
+
* - crew.verification.verifier_spawns_total{reason} — spawns that still
|
|
46
|
+
* happened, labeled by which layer let them through.
|
|
47
|
+
*/
|
|
48
|
+
import * as fs from "node:fs";
|
|
49
|
+
import { getCrewEnvBool, getCrewEnvInt } from "../../config/env-vars.ts";
|
|
50
|
+
import type { MetricRegistry } from "../../observability/metric-registry.ts";
|
|
51
|
+
import type { VerifierPreGateResult } from "./classifier-pre-gate.ts";
|
|
52
|
+
import { classifyVerifierPreGate } from "./classifier-pre-gate.ts";
|
|
53
|
+
import { executeCommand } from "./verification-gates.ts";
|
|
54
|
+
|
|
55
|
+
/** One deterministic check to run. Mirrors the phase-gate shape
|
|
56
|
+
* (verification-gates.ts) but kept local so the tool schema can reuse it. */
|
|
57
|
+
export interface VerifyGateCheck {
|
|
58
|
+
/** Short check name (metrics/events/evidence lines). */
|
|
59
|
+
name: string;
|
|
60
|
+
/** Shell command; exit code 0 = pass. */
|
|
61
|
+
command: string;
|
|
62
|
+
/** Stop the gate on the first failure of this check (default true). */
|
|
63
|
+
critical?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Result of one executed check. */
|
|
67
|
+
export interface VerifyGateCheckResult {
|
|
68
|
+
name: string;
|
|
69
|
+
command: string;
|
|
70
|
+
exitCode: number | null;
|
|
71
|
+
passed: boolean;
|
|
72
|
+
durationMs: number;
|
|
73
|
+
/** Tail of the command output (bounded) — evidence for humans/LLM. */
|
|
74
|
+
outputTail: string;
|
|
75
|
+
skipped?: boolean;
|
|
76
|
+
error?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Overall gate verdict. */
|
|
80
|
+
export type VerifyGateVerdict = "PASS" | "FAILED" | "INCONCLUSIVE";
|
|
81
|
+
|
|
82
|
+
export interface VerifyGateOutcome {
|
|
83
|
+
verdict: VerifyGateVerdict;
|
|
84
|
+
checks: VerifyGateCheckResult[];
|
|
85
|
+
/** Compact structured evidence lines (exit codes + commands). */
|
|
86
|
+
evidenceLines: string[];
|
|
87
|
+
allPassed: boolean;
|
|
88
|
+
totalDurationMs: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Injected command executor. The TOOL path wraps ctx.executeTool("bash");
|
|
92
|
+
* the spawn-site path wraps verification-gates executeCommand (subprocess).
|
|
93
|
+
* Must resolve (never reject) — failures come back as exitCode null/error. */
|
|
94
|
+
export type VerifyGateExecutor = (command: string, opts: VerifyGateExecutorOpts) => Promise<VerifyGateExecutorResult>;
|
|
95
|
+
|
|
96
|
+
export interface VerifyGateExecutorOpts {
|
|
97
|
+
cwd: string;
|
|
98
|
+
timeoutMs: number;
|
|
99
|
+
signal?: AbortSignal;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface VerifyGateExecutorResult {
|
|
103
|
+
exitCode: number | null;
|
|
104
|
+
output: string;
|
|
105
|
+
error?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Default per-check timeout — mirrors runPhaseGates (verification-gates.ts). */
|
|
109
|
+
export const DEFAULT_VERIFY_GATE_TIMEOUT_MS = 120_000;
|
|
110
|
+
/** Evidence tail budget per check (chars). */
|
|
111
|
+
const OUTPUT_TAIL_MAX_CHARS = 2_000;
|
|
112
|
+
|
|
113
|
+
/** U13 gate enable resolution: PI_CREW_VERIFY_GATE beats default ON. */
|
|
114
|
+
export function resolveVerifyGateEnabled(explicit?: boolean): boolean {
|
|
115
|
+
const env = getCrewEnvBool("PI_CREW_VERIFY_GATE");
|
|
116
|
+
if (env !== undefined) return env;
|
|
117
|
+
return explicit ?? true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** U13 per-check timeout resolution: PI_CREW_VERIFY_GATE_TIMEOUT_MS beats
|
|
121
|
+
* default 120000 (mirrors runPhaseGates). Invalid/≤0 → default. */
|
|
122
|
+
export function resolveVerifyGateTimeoutMs(explicit?: number): number {
|
|
123
|
+
const env = getCrewEnvInt("PI_CREW_VERIFY_GATE_TIMEOUT_MS");
|
|
124
|
+
const raw = env ?? explicit ?? DEFAULT_VERIFY_GATE_TIMEOUT_MS;
|
|
125
|
+
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_VERIFY_GATE_TIMEOUT_MS;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Default check preset — the spec's "check cấu hình" set, resolved from the
|
|
130
|
+
* target repo's package.json scripts so the gate adapts instead of failing:
|
|
131
|
+
* - `typecheck` script present → `npm run --silent typecheck` (critical);
|
|
132
|
+
* - `test:critical` script present → `npm run --silent test:critical`
|
|
133
|
+
* (critical);
|
|
134
|
+
* - grep evidence lines: no universal marker exists, so they are provided
|
|
135
|
+
* explicitly via the tool's `checks` param (e.g. the bundle-marker rule
|
|
136
|
+
* `grep -c "<marker>" dist/index.mjs`) rather than defaulted.
|
|
137
|
+
* Repos without either script resolve ZERO checks → INCONCLUSIVE → the
|
|
138
|
+
* classifier layer (when enabled) or the LLM verifier spawn runs as today.
|
|
139
|
+
*/
|
|
140
|
+
export function defaultVerifyGateChecks(dir: string): VerifyGateCheck[] {
|
|
141
|
+
let scripts: Record<string, unknown> = {};
|
|
142
|
+
try {
|
|
143
|
+
const raw = fs.readFileSync(`${dir}/package.json`, "utf8");
|
|
144
|
+
const parsed = raw ? (JSON.parse(raw) as { scripts?: Record<string, unknown> }) : undefined;
|
|
145
|
+
scripts = parsed?.scripts ?? {};
|
|
146
|
+
} catch {
|
|
147
|
+
scripts = {};
|
|
148
|
+
}
|
|
149
|
+
const checks: VerifyGateCheck[] = [];
|
|
150
|
+
if (typeof scripts.typecheck === "string") {
|
|
151
|
+
checks.push({ name: "typecheck", command: "npm run --silent typecheck", critical: true });
|
|
152
|
+
}
|
|
153
|
+
if (typeof scripts["test:critical"] === "string") {
|
|
154
|
+
checks.push({ name: "test-critical", command: "npm run --silent test:critical", critical: true });
|
|
155
|
+
}
|
|
156
|
+
return checks;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function tailOutput(output: string): string {
|
|
160
|
+
if (output.length <= OUTPUT_TAIL_MAX_CHARS) return output;
|
|
161
|
+
return `…${output.slice(-OUTPUT_TAIL_MAX_CHARS)}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Cap the number of evidence lines (prompt/metric hygiene). */
|
|
165
|
+
const EVIDENCE_MAX_LINES = 12;
|
|
166
|
+
|
|
167
|
+
export function evaluateVerifyGateResults(checks: VerifyGateCheckResult[]): VerifyGateOutcome {
|
|
168
|
+
const ran = checks.filter((c) => !c.skipped);
|
|
169
|
+
const allPassed = ran.length > 0 && ran.every((c) => c.passed);
|
|
170
|
+
const verdict: VerifyGateVerdict = ran.length === 0 ? "INCONCLUSIVE" : allPassed ? "PASS" : "FAILED";
|
|
171
|
+
const evidenceLines = checks
|
|
172
|
+
.slice(0, EVIDENCE_MAX_LINES)
|
|
173
|
+
.map((c) =>
|
|
174
|
+
c.skipped
|
|
175
|
+
? `${c.name}: SKIPPED — ${c.command}`
|
|
176
|
+
: `${c.name}: exit=${c.exitCode ?? "err"} ${c.passed ? "PASS" : "FAIL"} — ${c.command}${c.error ? ` (error: ${c.error})` : ""}`,
|
|
177
|
+
);
|
|
178
|
+
return {
|
|
179
|
+
verdict,
|
|
180
|
+
checks,
|
|
181
|
+
evidenceLines,
|
|
182
|
+
allPassed,
|
|
183
|
+
totalDurationMs: checks.reduce((sum, c) => sum + c.durationMs, 0),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Run the gate checks sequentially through the injected executor.
|
|
188
|
+
* Mirrors runPhaseGates semantics: stop on first critical failure; abort
|
|
189
|
+
* signal between checks marks the rest skipped. NEVER rejects. */
|
|
190
|
+
export async function runVerifyGate(
|
|
191
|
+
checks: VerifyGateCheck[],
|
|
192
|
+
exec: VerifyGateExecutor,
|
|
193
|
+
opts: { cwd: string; signal?: AbortSignal; timeoutMs?: number },
|
|
194
|
+
): Promise<VerifyGateOutcome> {
|
|
195
|
+
const timeoutMs = resolveVerifyGateTimeoutMs(opts.timeoutMs);
|
|
196
|
+
const results: VerifyGateCheckResult[] = [];
|
|
197
|
+
for (const check of checks) {
|
|
198
|
+
if (opts.signal?.aborted) {
|
|
199
|
+
results.push({
|
|
200
|
+
name: check.name,
|
|
201
|
+
command: check.command,
|
|
202
|
+
exitCode: null,
|
|
203
|
+
passed: false,
|
|
204
|
+
durationMs: 0,
|
|
205
|
+
outputTail: "",
|
|
206
|
+
skipped: true,
|
|
207
|
+
error: "Aborted",
|
|
208
|
+
});
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const start = Date.now();
|
|
212
|
+
let execResult: VerifyGateExecutorResult;
|
|
213
|
+
try {
|
|
214
|
+
execResult = await exec(check.command, { cwd: opts.cwd, timeoutMs, signal: opts.signal });
|
|
215
|
+
} catch (error) {
|
|
216
|
+
// Executor contract says never-reject; defend anyway — an executor
|
|
217
|
+
// blowup must degrade the CHECK, not the host.
|
|
218
|
+
execResult = { exitCode: null, output: "", error: error instanceof Error ? error.message : String(error) };
|
|
219
|
+
}
|
|
220
|
+
results.push({
|
|
221
|
+
name: check.name,
|
|
222
|
+
command: check.command,
|
|
223
|
+
exitCode: execResult.exitCode,
|
|
224
|
+
passed: execResult.exitCode === 0,
|
|
225
|
+
durationMs: Date.now() - start,
|
|
226
|
+
outputTail: tailOutput(execResult.output ?? ""),
|
|
227
|
+
...(execResult.error ? { error: execResult.error } : {}),
|
|
228
|
+
});
|
|
229
|
+
// Stop on critical failure (default critical) — mirrors runPhaseGates.
|
|
230
|
+
if (execResult.exitCode !== 0 && check.critical !== false) break;
|
|
231
|
+
}
|
|
232
|
+
return evaluateVerifyGateResults(results);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Spawn-site executor: the hardened subprocess runner the verifier flow
|
|
236
|
+
* already uses (validateGateCommand + sanitized env + group kill + caps). */
|
|
237
|
+
export function createSubprocessVerifyGateExecutor(): VerifyGateExecutor {
|
|
238
|
+
return async (command, opts) => {
|
|
239
|
+
if (opts.signal?.aborted) return { exitCode: null, output: "", error: "Aborted" };
|
|
240
|
+
const { exitCode, output } = await executeCommand(command, opts.cwd, opts.timeoutMs);
|
|
241
|
+
return { exitCode, output };
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export interface VerifierSpawnDecision {
|
|
246
|
+
/** true → do NOT spawn the LLM verifier; the gate verdict stands. */
|
|
247
|
+
skipVerifier: boolean;
|
|
248
|
+
/** Which layer + branch decided (metric label). */
|
|
249
|
+
reason: string;
|
|
250
|
+
/** true → append the gate outcome block to the verifier's prompt. */
|
|
251
|
+
attachGateContext: boolean;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** TWO-LAYER decision (deterministic first, classifier second). */
|
|
255
|
+
export function resolveVerifierSpawnDecision(outcome: VerifyGateOutcome, preGate?: VerifierPreGateResult): VerifierSpawnDecision {
|
|
256
|
+
if (outcome.verdict === "PASS") {
|
|
257
|
+
return { skipVerifier: true, reason: "deterministic_pass", attachGateContext: false };
|
|
258
|
+
}
|
|
259
|
+
if (outcome.verdict === "FAILED") {
|
|
260
|
+
// The LLM verifier gets the failing evidence in its prompt — it must
|
|
261
|
+
// diagnose/report the failure, not re-derive exit codes.
|
|
262
|
+
return { skipVerifier: false, reason: "deterministic_failed", attachGateContext: true };
|
|
263
|
+
}
|
|
264
|
+
// INCONCLUSIVE: classifier layer (U6B) may block on a decisively good
|
|
265
|
+
// structured state; disabled/soft-fail/low-confidence all escalate.
|
|
266
|
+
if (preGate?.blockVerifier) {
|
|
267
|
+
return { skipVerifier: true, reason: "classifier_blocked", attachGateContext: false };
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
skipVerifier: false,
|
|
271
|
+
reason: preGate ? `classifier_${preGate.action}` : "inconclusive_no_classifier",
|
|
272
|
+
attachGateContext: false,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Render the gate outcome as a compact prompt block for the LLM verifier. */
|
|
277
|
+
export function renderVerifyGateContext(outcome: VerifyGateOutcome): string {
|
|
278
|
+
const lines = [
|
|
279
|
+
"## Deterministic verify_gate results (U13 pre-gate)",
|
|
280
|
+
`Gate verdict: ${outcome.verdict}`,
|
|
281
|
+
...outcome.evidenceLines.map((line) => `- ${line}`),
|
|
282
|
+
];
|
|
283
|
+
for (const check of outcome.checks) {
|
|
284
|
+
if (!check.skipped && !check.passed && check.outputTail.trim().length > 0) {
|
|
285
|
+
const tail = check.outputTail.trim().split("\n").slice(-5).join("\n");
|
|
286
|
+
lines.push(`### ${check.name} output tail`, "```", tail, "```");
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
lines.push(
|
|
290
|
+
"",
|
|
291
|
+
"The deterministic gate already ran these commands — do NOT re-run them; verify the WORK (diff, files, acceptance), using these results as ground truth for build/test state.",
|
|
292
|
+
);
|
|
293
|
+
return lines.join("\n");
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Human/one-line summary used for the skip path's synthetic result artifact. */
|
|
297
|
+
export function renderVerifyGateSummary(outcome: VerifyGateOutcome, reason: string): string {
|
|
298
|
+
return [`verify_gate: ${outcome.verdict} (skip reason: ${reason})`, ...outcome.evidenceLines].join("\n");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export interface VerifierGatePreGateInput {
|
|
302
|
+
cwd: string;
|
|
303
|
+
/** Explicit checks override the default preset (tool path / tests). */
|
|
304
|
+
checks?: VerifyGateCheck[];
|
|
305
|
+
signal?: AbortSignal;
|
|
306
|
+
timeoutMs?: number;
|
|
307
|
+
/** U6B classifier layer args — consulted ONLY on INCONCLUSIVE. */
|
|
308
|
+
classifier?: {
|
|
309
|
+
enabled: boolean;
|
|
310
|
+
modelRegistry: unknown;
|
|
311
|
+
classifierModel: string;
|
|
312
|
+
eventsPath: string;
|
|
313
|
+
runId: string;
|
|
314
|
+
taskId?: string;
|
|
315
|
+
};
|
|
316
|
+
metricRegistry?: MetricRegistry;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export interface VerifierGatePreGateResult {
|
|
320
|
+
outcome: VerifyGateOutcome;
|
|
321
|
+
decision: VerifierSpawnDecision;
|
|
322
|
+
preGate?: VerifierPreGateResult;
|
|
323
|
+
/** Prompt block to append when the verifier still spawns ("" otherwise). */
|
|
324
|
+
promptContext: string;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function incMetric(registry: MetricRegistry | undefined, name: string, help: string, labels: Record<string, string>): void {
|
|
328
|
+
try {
|
|
329
|
+
registry?.counter(name, help).inc(labels);
|
|
330
|
+
} catch {
|
|
331
|
+
/* metrics are best-effort; never break the gate */
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Spawn-site orchestration: run the deterministic gate, consult the U6B
|
|
337
|
+
* classifier layer when inconclusive, resolve the two-layer spawn decision,
|
|
338
|
+
* and emit the acceptance metrics. NEVER rejects — executor failures degrade
|
|
339
|
+
* to INCONCLUSIVE checks, which escalate to the LLM verifier.
|
|
340
|
+
*/
|
|
341
|
+
export async function runVerifierGatePreGate(input: VerifierGatePreGateInput): Promise<VerifierGatePreGateResult> {
|
|
342
|
+
const checks = input.checks ?? defaultVerifyGateChecks(input.cwd);
|
|
343
|
+
const outcome = await runVerifyGate(checks, createSubprocessVerifyGateExecutor(), {
|
|
344
|
+
cwd: input.cwd,
|
|
345
|
+
signal: input.signal,
|
|
346
|
+
timeoutMs: input.timeoutMs,
|
|
347
|
+
});
|
|
348
|
+
let preGate: VerifierPreGateResult | undefined;
|
|
349
|
+
if (outcome.verdict === "INCONCLUSIVE" && input.classifier?.enabled) {
|
|
350
|
+
try {
|
|
351
|
+
preGate = await classifyVerifierPreGate({
|
|
352
|
+
enabled: true,
|
|
353
|
+
modelRegistry: input.classifier.modelRegistry,
|
|
354
|
+
classifierModel: input.classifier.classifierModel,
|
|
355
|
+
verdict: {
|
|
356
|
+
verdict: outcome.verdict,
|
|
357
|
+
evidenceLines: outcome.evidenceLines,
|
|
358
|
+
changedFiles: [],
|
|
359
|
+
},
|
|
360
|
+
eventsPath: input.classifier.eventsPath,
|
|
361
|
+
runId: input.classifier.runId,
|
|
362
|
+
...(input.classifier.taskId !== undefined ? { taskId: input.classifier.taskId } : {}),
|
|
363
|
+
metricRegistry: input.metricRegistry,
|
|
364
|
+
});
|
|
365
|
+
} catch {
|
|
366
|
+
preGate = undefined; // soft: escalate to the LLM verifier
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const decision = resolveVerifierSpawnDecision(outcome, preGate);
|
|
370
|
+
incMetric(input.metricRegistry, "crew.verification.verify_gate_runs_total", "Deterministic verify_gate executions by verdict", {
|
|
371
|
+
verdict: outcome.verdict,
|
|
372
|
+
});
|
|
373
|
+
if (decision.skipVerifier) {
|
|
374
|
+
incMetric(
|
|
375
|
+
input.metricRegistry,
|
|
376
|
+
"crew.verification.verifier_spawns_skipped_total",
|
|
377
|
+
"LLM verifier spawns saved by the U13 deterministic pre-gate",
|
|
378
|
+
{ reason: decision.reason },
|
|
379
|
+
);
|
|
380
|
+
} else {
|
|
381
|
+
incMetric(
|
|
382
|
+
input.metricRegistry,
|
|
383
|
+
"crew.verification.verifier_spawns_total",
|
|
384
|
+
"LLM verifier spawns that still ran, labeled by the deciding layer",
|
|
385
|
+
{ reason: decision.reason },
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
outcome,
|
|
390
|
+
decision,
|
|
391
|
+
...(preGate ? { preGate } : {}),
|
|
392
|
+
promptContext: decision.attachGateContext ? renderVerifyGateContext(outcome) : "",
|
|
393
|
+
};
|
|
394
|
+
}
|