pi-subagents 0.59.0 → 0.61.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/CHANGELOG.md +69 -0
- package/docs/agents.md +2 -2
- package/docs/configuration.md +9 -5
- package/docs/extension-api.md +14 -7
- package/docs/models.md +1 -1
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +15 -3
- package/docs/workflows.md +15 -14
- package/install.mjs +2 -1
- package/package.json +1 -1
- package/skills/council-mode/SKILL.md +48 -243
- package/skills/council-mode/references/pass-contracts.md +150 -0
- package/skills/pi-subagents/SKILL.md +89 -37
- package/skills/pi-subagents/references/constraints-and-recipes.md +30 -234
- package/skills/pi-subagents/references/execution-controls.md +79 -7
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +35 -28
- package/skills/pi-subagents/references/review-and-validation.md +73 -0
- package/src/agents/agent-management.ts +256 -88
- package/src/agents/agents.ts +527 -221
- package/src/api/background-work.ts +7 -2
- package/src/api/external-runs.ts +67 -4
- package/src/api/preflight.ts +13 -8
- package/src/api/shared-types.ts +3 -0
- package/src/extension/index.ts +7 -4
- package/src/extension/public-execution.ts +48 -4
- package/src/extension/rpc.ts +62 -4
- package/src/extension/schemas.ts +12 -7
- package/src/extension/tool-description.ts +16 -16
- package/src/runs/background/async-execution.ts +53 -37
- package/src/runs/background/async-job-tracker.ts +65 -3
- package/src/runs/background/async-resume.ts +3 -1
- package/src/runs/background/async-status.ts +104 -11
- package/src/runs/background/auto-drain.ts +1 -1
- package/src/runs/background/control-channel.ts +3 -2
- package/src/runs/background/fleet-view.ts +1 -1
- package/src/runs/background/result-watcher.ts +1 -1
- package/src/runs/background/resume-guidance.ts +1 -1
- package/src/runs/background/run-status.ts +15 -4
- package/src/runs/background/subagent-runner.ts +6 -3
- package/src/runs/background/subagent-wait.ts +30 -23
- package/src/runs/background/wait-completions.ts +4 -1
- package/src/runs/background/wait-tool.ts +24 -18
- package/src/runs/foreground/execution.ts +72 -4
- package/src/runs/foreground/subagent-executor.ts +276 -59
- package/src/runs/shared/acceptance.ts +43 -18
- package/src/runs/shared/async-status-projection.ts +138 -4
- package/src/runs/shared/background-process-options.ts +9 -0
- package/src/runs/shared/host-step-status.ts +1 -0
- package/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
- package/src/runs/shared/model-fallback.ts +61 -17
- package/src/runs/shared/mutation-evidence.ts +52 -3
- package/src/runs/shared/permissions.ts +1 -1
- package/src/runs/shared/pi-args.ts +47 -1
- package/src/runs/shared/single-output.ts +45 -18
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -2
- package/src/runs/shared/tool-timeout.ts +1 -1
- package/src/runs/shared/workflow-graph.ts +16 -0
- package/src/shared/types.ts +74 -4
- package/src/shared/workflow-child-permit.ts +91 -0
- package/src/slash/prompt-template-bridge.ts +37 -1
- package/src/slash/slash-commands.ts +18 -26
- package/src/tui/fleet-status.ts +11 -3
- package/src/tui/render-helpers.ts +31 -0
- package/src/tui/render.ts +673 -145
- package/src/watchdog/change-signature.ts +40 -1
- package/src/workflows/host-command.ts +6 -1
- package/src/workflows/scripted-workflow.ts +206 -6
- package/src/workflows/workflow-child-summary.ts +1 -1
- package/src/workflows/workflow-receipt.ts +41 -4
- package/src/workflows/workflow-resources.ts +150 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
|
+
import * as os from "node:os";
|
|
4
5
|
import * as path from "node:path";
|
|
5
6
|
import { PROJECT_SUBAGENTS_RELATIVE_DIR } from "../shared/artifacts.ts";
|
|
6
7
|
|
|
@@ -11,6 +12,7 @@ const IGNORED_CHANGE_SEGMENTS = new Set([".git", "node_modules"]);
|
|
|
11
12
|
const DEFAULT_MAX_HASH_FILE_BYTES = 64 * 1024 * 1024;
|
|
12
13
|
const DEFAULT_MAX_HASH_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
13
14
|
const DEFAULT_MAX_HASH_ENTRIES = 2_000;
|
|
15
|
+
const TRACKED_ENTRY_PROBE_MAX_BYTES = 1_024;
|
|
14
16
|
|
|
15
17
|
function positiveEnvNumber(name: string, fallback: number): number {
|
|
16
18
|
const parsed = Number(process.env[name]);
|
|
@@ -43,6 +45,37 @@ function git(cwd: string, args: string[]): string | undefined {
|
|
|
43
45
|
return result.stdout;
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
function comparablePath(value: string): string {
|
|
49
|
+
let resolved = path.resolve(value);
|
|
50
|
+
try {
|
|
51
|
+
resolved = fs.realpathSync.native(resolved);
|
|
52
|
+
} catch {
|
|
53
|
+
// Keep the lexical path when canonicalization is unavailable.
|
|
54
|
+
}
|
|
55
|
+
return process.platform === "win32" ? resolved.toLowerCase() : resolved;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isHomeRepoRoot(root: string): boolean {
|
|
59
|
+
return comparablePath(root) === comparablePath(os.homedir());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Probe the index without asking Git to inspect the working tree. The output
|
|
64
|
+
* cap keeps this check bounded even for repositories with a very large index;
|
|
65
|
+
* an ENOBUFS result still proves that at least one tracked path exists.
|
|
66
|
+
*/
|
|
67
|
+
function hasTrackedEntries(root: string): boolean | undefined {
|
|
68
|
+
const result = spawnSync("git", ["-C", root, "ls-files", "--cached", "--error-unmatch", "--", "."], {
|
|
69
|
+
encoding: "utf-8",
|
|
70
|
+
maxBuffer: TRACKED_ENTRY_PROBE_MAX_BYTES,
|
|
71
|
+
windowsHide: true,
|
|
72
|
+
});
|
|
73
|
+
const errorCode = (result.error as NodeJS.ErrnoException | undefined)?.code;
|
|
74
|
+
if (result.status === 0 || errorCode === "ENOBUFS") return true;
|
|
75
|
+
if (result.status === 1) return false;
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
46
79
|
function normalizeRelPath(value: string): string {
|
|
47
80
|
return value.replaceAll(path.sep, "/").replace(/^\.\//, "");
|
|
48
81
|
}
|
|
@@ -181,7 +214,13 @@ function buildRepoChangeSignature(root: string, statusOutput: string): WatchdogR
|
|
|
181
214
|
export function computeWatchdogRepoChangeSignature(cwd: string): WatchdogRepoChangeSignature | undefined {
|
|
182
215
|
const root = git(cwd, ["rev-parse", "--show-toplevel"])?.trim();
|
|
183
216
|
if (!root) return undefined;
|
|
184
|
-
const
|
|
217
|
+
const skipUntracked = isHomeRepoRoot(root) || hasTrackedEntries(root) === false;
|
|
218
|
+
const statusOutput = git(root, [
|
|
219
|
+
"status",
|
|
220
|
+
"--porcelain=v1",
|
|
221
|
+
"-z",
|
|
222
|
+
`--untracked-files=${skipUntracked ? "no" : "all"}`,
|
|
223
|
+
]);
|
|
185
224
|
if (statusOutput === undefined) return undefined;
|
|
186
225
|
try {
|
|
187
226
|
return buildRepoChangeSignature(root, statusOutput);
|
|
@@ -53,7 +53,12 @@ export function normalizeWorkflowHostCommandParams(value: unknown, label = "runs
|
|
|
53
53
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`);
|
|
54
54
|
const params = value as Record<string, unknown>;
|
|
55
55
|
const unknownFields = Object.keys(params).filter((field) => !["kind", "command", "timeoutMs", "output", "role", "provider"].includes(field));
|
|
56
|
-
if (unknownFields.length)
|
|
56
|
+
if (unknownFields.length) {
|
|
57
|
+
const cwdHint = unknownFields.includes("cwd")
|
|
58
|
+
? " The host step does not accept per-step cwd; commands and relative output paths use the workflow cwd. Set cwd on the outer subagent request, or put a trusted directory change in command (for example, 'cd /path/to/worktree && npm test')."
|
|
59
|
+
: "";
|
|
60
|
+
throw new Error(`${label} has unsupported fields: ${unknownFields.join(", ")}.${cwdHint}`);
|
|
61
|
+
}
|
|
57
62
|
if (params.kind !== "command") throw new Error(`${label}.kind must be 'command'.`);
|
|
58
63
|
if (typeof params.command !== "string" || !params.command.trim()) throw new Error(`${label}.command must be a non-empty string.`);
|
|
59
64
|
const command = params.command.trim();
|
|
@@ -4,7 +4,8 @@ import { dirname, resolve as resolvePath } from "node:path";
|
|
|
4
4
|
import { Worker } from "node:worker_threads";
|
|
5
5
|
import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../runs/shared/parallel-utils.ts";
|
|
6
6
|
import { HOST_STEP_MAX_COUNT } from "../runs/shared/host-step-status.ts";
|
|
7
|
-
import
|
|
7
|
+
import { classifyTaskMutationIntent } from "../runs/shared/task-intent.ts";
|
|
8
|
+
import type { AcceptanceRecoveryMetadata, HostStepNodeV1, SingleResult } from "../shared/types.ts";
|
|
8
9
|
import { normalizeWorkflowHostCommandParams, type WorkflowHostCommandParams, type WorkflowHostCommandResult } from "./host-command.ts";
|
|
9
10
|
|
|
10
11
|
const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
@@ -500,8 +501,26 @@ function runLane(lane, firstResult, observe) {
|
|
|
500
501
|
return visit(0, firstResult);
|
|
501
502
|
}
|
|
502
503
|
|
|
504
|
+
function workflowPlanStringMetadata(params) {
|
|
505
|
+
return {
|
|
506
|
+
...(typeof params.phase === "string" && params.phase.trim() ? { phase: params.phase.trim() } : {}),
|
|
507
|
+
...(typeof params.label === "string" && params.label.trim() ? { label: params.label.trim() } : {}),
|
|
508
|
+
...(typeof params.agent === "string" && params.agent.trim() ? { agent: params.agent.trim() } : {}),
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
|
|
503
512
|
function runLanes(laneSpecs) {
|
|
504
513
|
const lanes = validateLaneSpecs(laneSpecs);
|
|
514
|
+
parentPort.postMessage({ type: "lanePlan", lanes: lanes.map((lane) => ({
|
|
515
|
+
key: lane.key,
|
|
516
|
+
stages: lane.stages.map((stage) => ({
|
|
517
|
+
key: stage.key,
|
|
518
|
+
generatedKey: stage.generatedKey,
|
|
519
|
+
...workflowPlanStringMetadata(stage.params),
|
|
520
|
+
...(typeof stage.params.as === "string" && stage.params.as.trim() ? { outputName: stage.params.as.trim() } : {}),
|
|
521
|
+
...(stage.params.outputSchema !== undefined ? { structured: true } : {}),
|
|
522
|
+
})),
|
|
523
|
+
})) });
|
|
505
524
|
const firstItems = lanes.map((lane) => {
|
|
506
525
|
const first = lane.stages[0];
|
|
507
526
|
return { key: first.generatedKey, ...first.params };
|
|
@@ -586,7 +605,7 @@ function validateRunCall(key, params, label, fingerprints) {
|
|
|
586
605
|
if (typeof key !== "string" || !runKeyPattern.test(key)) throw new Error(label + " has an invalid key.");
|
|
587
606
|
if (hostKeys.has(key)) throw new Error("Workflow key '" + key + "' is already used by runs.host.");
|
|
588
607
|
if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error(label + " requires a params object.");
|
|
589
|
-
if (Object.prototype.hasOwnProperty.call(params, "action") || Object.prototype.hasOwnProperty.call(params, "workflowScript") || Object.prototype.hasOwnProperty.call(params, "tasks") || Object.prototype.hasOwnProperty.call(params, "chain") || Object.prototype.hasOwnProperty.call(params, "parallel") || Object.prototype.hasOwnProperty.call(params, "concurrency") || Object.prototype.hasOwnProperty.call(params, "chainDir")) {
|
|
608
|
+
if (Object.prototype.hasOwnProperty.call(params, "action") || Object.prototype.hasOwnProperty.call(params, "workflowScript") || Object.prototype.hasOwnProperty.call(params, "globalConcurrencyLimit") || Object.prototype.hasOwnProperty.call(params, "maxSubagentSpawnsPerRun") || Object.prototype.hasOwnProperty.call(params, "tasks") || Object.prototype.hasOwnProperty.call(params, "chain") || Object.prototype.hasOwnProperty.call(params, "parallel") || Object.prototype.hasOwnProperty.call(params, "concurrency") || Object.prototype.hasOwnProperty.call(params, "chainDir")) {
|
|
590
609
|
const hint = label === "runs.run" ? "; use runs.all(...) and JavaScript control flow for orchestration." : ".";
|
|
591
610
|
throw new Error(label + " accepts one child via { agent, task } and execution controls only" + hint);
|
|
592
611
|
}
|
|
@@ -624,7 +643,12 @@ function validateHostCommand(key, params) {
|
|
|
624
643
|
if (!params || typeof params !== "object" || Array.isArray(params)) throw new Error("runs.host('" + key + "') params must be an object.");
|
|
625
644
|
const allowed = new Set(["kind", "command", "timeoutMs", "output", "role", "provider"]);
|
|
626
645
|
const unknown = Object.keys(params).filter((field) => !allowed.has(field));
|
|
627
|
-
if (unknown.length)
|
|
646
|
+
if (unknown.length) {
|
|
647
|
+
const cwdHint = unknown.includes("cwd")
|
|
648
|
+
? " The host step does not accept per-step cwd; commands and relative output paths use the workflow cwd. Set cwd on the outer subagent request, or put a trusted directory change in command (for example, 'cd /path/to/worktree && npm test')."
|
|
649
|
+
: "";
|
|
650
|
+
throw new Error("runs.host('" + key + "') params have unsupported fields: " + unknown.join(", ") + "." + cwdHint);
|
|
651
|
+
}
|
|
628
652
|
if (params.kind !== "command") throw new Error("runs.host('" + key + "') kind must be 'command'.");
|
|
629
653
|
if (typeof params.command !== "string" || !params.command.trim() || params.command.includes("\u0000") || new TextEncoder().encode(params.command.trim()).byteLength > 16384) throw new Error("runs.host('" + key + "') command must be a non-empty string of at most 16384 bytes without NUL.");
|
|
630
654
|
if (!Number.isInteger(params.timeoutMs) || params.timeoutMs < 1 || params.timeoutMs > 86400000) throw new Error("runs.host('" + key + "') timeoutMs must be an integer from 1 to 86400000.");
|
|
@@ -952,6 +976,7 @@ export interface WorkflowScriptChildResult {
|
|
|
952
976
|
requestedContext?: "fresh" | "fork";
|
|
953
977
|
resolvedContext?: "fresh" | "fork" | "mixed";
|
|
954
978
|
outputReference?: string;
|
|
979
|
+
recovery?: AcceptanceRecoveryMetadata;
|
|
955
980
|
outputPathMapping?: { requestedPath: string; savedPath: string };
|
|
956
981
|
externalAdapter?: import("../shared/types.ts").ExternalCliReceiptMetadata;
|
|
957
982
|
resumability?: { state: "resumable" } | { state: "not-resumable"; reason: string };
|
|
@@ -977,6 +1002,22 @@ export interface WorkflowScriptTraceEntry {
|
|
|
977
1002
|
warning?: string;
|
|
978
1003
|
}
|
|
979
1004
|
|
|
1005
|
+
/** Bounded plan metadata emitted when a workflow materializes a runs.lanes graph. */
|
|
1006
|
+
export interface WorkflowLanePlanStage {
|
|
1007
|
+
key: string;
|
|
1008
|
+
generatedKey: string;
|
|
1009
|
+
agent?: string;
|
|
1010
|
+
phase?: string;
|
|
1011
|
+
label?: string;
|
|
1012
|
+
outputName?: string;
|
|
1013
|
+
structured?: boolean;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
export interface WorkflowLanePlan {
|
|
1017
|
+
key: string;
|
|
1018
|
+
stages: WorkflowLanePlanStage[];
|
|
1019
|
+
}
|
|
1020
|
+
|
|
980
1021
|
export interface WorkflowSteerOptions {
|
|
981
1022
|
mode?: "steer" | "follow_up" | "auto";
|
|
982
1023
|
index?: number;
|
|
@@ -1044,6 +1085,7 @@ export interface RunWorkflowScriptOptions {
|
|
|
1044
1085
|
};
|
|
1045
1086
|
registerStopChild?: (stop: ((key: string, message?: string) => boolean) | undefined) => void;
|
|
1046
1087
|
onTrace?: (trace: WorkflowScriptTraceEntry[]) => void;
|
|
1088
|
+
onLanePlan?: (lanes: WorkflowLanePlan[]) => void;
|
|
1047
1089
|
onEmit?: (emits: unknown[]) => void;
|
|
1048
1090
|
}
|
|
1049
1091
|
|
|
@@ -1067,6 +1109,138 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
1067
1109
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1068
1110
|
}
|
|
1069
1111
|
|
|
1112
|
+
function isAcceptanceMetadataRecovery(result: WorkflowScriptChildResult): boolean {
|
|
1113
|
+
return !result.ok
|
|
1114
|
+
&& result.recovery?.status === "available-for-review"
|
|
1115
|
+
&& result.recovery.reason === "acceptance-metadata-rejected";
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
const RECOVERY_REVIEW_MUTATION_VERB_PATTERN = /\b(?:add(?:ing)?|append(?:ing)?|apply(?:ing)?|cherry[ -]pick(?:ing)?|change|changing|clean(?:ing)?|commit(?:ting)?|cop(?:y|ying)|create|creating|delete|deleting|edit(?:ing)?|fix(?:ing)?|implement(?:ing)?|insert(?:ing)?|make|making|merge|merging|mov(?:e|ing)|modify(?:ing)?|mutate|mutating|open(?:ing)?|patch(?:ing)?|prepend(?:ing)?|push(?:ing)?|rebase|rebasing|refactor(?:ing)?|remove|removing|rename|renaming|replace|replacing|revert(?:ing)?|revise|revising|rewrite|rewriting|sav(?:e|ing)|stag(?:e|ing)|stash(?:ing)?|tag(?:ging)?|touch(?:ing)?|update|updating|write|writing)\b/i;
|
|
1119
|
+
const RECOVERY_REVIEW_READ_ONLY_PATTERN = /\b(?:read[- ]only|review only|only return findings|return findings only|suggest fixes only|without\s+(?:editing|modifying|changing|writing|touching)|do not\s+(?:edit|modify|change|write|touch)|don't\s+(?:edit|modify|change|write|touch)|must not\s+(?:edit|modify|change|write|touch))\b/i;
|
|
1120
|
+
const RECOVERY_REVIEW_NO_MUTATION_CLAUSE_PATTERN = /\b(?:do not|don't|must not)\s+(?:edit|modify|change|write|touch)(?:\s+files?)?(?:\s*,\s*(?:commit|push|comment|merge|launch(?:\s+subagents?)?)(?=\s*(?:,|\bor\b|[.;!?\n)]|$)))*(?:\s*,?\s*or\s+(?:commit|push|comment|merge|launch(?:\s+subagents?)?)(?=\s*(?:[.;!?\n)]|$)))?/gi;
|
|
1121
|
+
const RECOVERY_REVIEW_DELIVERABLE_PATTERN = /\b(?:compose|create|draft|prepare|produce|write)\s+(?:(?:a|an|the|your)\s+)?(?:findings?|review|report|summary|analysis|recommendations?)(?:\s+(?:to|at|in)\s+\S+)?/gi;
|
|
1122
|
+
const RECOVERY_REVIEW_CONTEXT_OBJECT_PATTERN = /\breview\s+(?:(?:the|this|that|saved)\s+)?(?:patch|diff|changes?|implementation|report)\b/gi;
|
|
1123
|
+
const RECOVERY_REVIEW_MUTATION_NOUN_CONTEXT_PATTERN = /\b(?:later\s+real\s+)?update\s+imperatives?\b/gi;
|
|
1124
|
+
const RECOVERY_REVIEW_PRIOR_FIX_CONTEXT_PATTERN = /\bthe\s+prior\s+fix\s+keeps\b/gi;
|
|
1125
|
+
const RECOVERY_REVIEW_DETECTION_CONTEXT_PATTERN = /\b(?:mutation\s+detection\s+now\s+includes\s+move\/rename\/copy\s+file\s+mutation\s+imperatives|delegation\s+detection\s+now\s+blocks\s+get\/let\/have\/tell\/ask\s+follow-up\s+forms)(?=[,.;!?\n]|\b(?:and|but|then|however|nevertheless|nonetheless|yet)\b|$)/gi;
|
|
1126
|
+
const RECOVERY_REVIEW_PATTERN_CHANGE_CONTEXT_PATTERN = /\bRECOVERY_REVIEW_MUTATION_VERB_PATTERN\s+now\s+includes\s+append,\s+prepend,\s+and\s+sav(?:e|ing)\b(?=\s*(?:[,.;!?\n)]|$))/gi;
|
|
1127
|
+
const RECOVERY_REVIEW_GIT_PATTERN_CHANGE_CONTEXT_PATTERN = /\bfixed\s+mutating\s+git\s+follow-up\s+bypasses\b|\b(?:added|adding)\s+[a-z][a-z-]*(?:\/[a-z][a-z-]*)*(?:(?:\s*,\s*(?:and\s+)?|\s+and\s+)[a-z][a-z-]*(?:\/[a-z][a-z-]*)*)*\s+to\s+the\s+(?:mutation\s+imperative|mutating\s+git\s+command)\s+pattern\b|\band\s+[a-z][a-z-]*(?:\/[a-z][a-z-]*)*(?:(?:\s*,\s*(?:and\s+)?|\s+and\s+)[a-z][a-z-]*(?:\/[a-z][a-z-]*)*)*\s+to\s+the\s+mutating\s+git\s+command\s+pattern\b/gi;
|
|
1128
|
+
const RECOVERY_REVIEW_VALIDATION_EVIDENCE_PATTERN = /\bvalidation(?:\s+after\s+fix)?\s*:/gi;
|
|
1129
|
+
const RECOVERY_REVIEW_CONTRACT_PROHIBITION_PATTERN = /\b(?:do not|don't|must not)\s+(?:mutate\s+durable\s+state|launch\s+(?:mutating|destructive|mutating\/destructive)\s+work)(?:\s+or\s+(?:mutate\s+durable\s+state|launch\s+(?:mutating|destructive|mutating\/destructive)\s+work))*/gi;
|
|
1130
|
+
const RECOVERY_REVIEW_ONLY_REVIEW_CONTRACT_PATTERN = /\bmay\s+only\s+launch\s+explicit\s+read-only\s+review\s+children\s+with\s+acceptance:false\b/gi;
|
|
1131
|
+
const RECOVERY_REVIEW_BLOCKED_CONTEXT_FRAGMENT_PATTERN = /\bstate\.set, runs\.host, runs\.steer, ordinary\/mutating children, and destructive command wording are blocked(?=[,.;!?\n)\]}]|$)/gi;
|
|
1132
|
+
const RECOVERY_REVIEW_REGRESSION_EVIDENCE_PATTERN = /\b(?:existing regressions cover plain rm and git clean\/reset\/restore|prior regressions covering rm\/git clean as evidence only)(?=[,.;!?\n)\]}]|$)/gi;
|
|
1133
|
+
const RECOVERY_REVIEW_BLOCKED_QUOTED_EXAMPLE_PATTERN = /(?:`[^`\n]+`|'[^'\n]+'|"[^"\n]+")(?:(?:\s*,\s*(?:and\s+)?|\s+and\s+)(?:`[^`\n]+`|'[^'\n]+'|"[^"\n]+"))*\s+(?:is|are|remains?)\s+(?:blocked|(?:an?\s+)?blocked\s+examples?)(?=[,.;!?\n)\]}]|$)/gi;
|
|
1134
|
+
const RECOVERY_REVIEW_CONTEXT_QUOTED_EXAMPLE_PATTERN = /\b(?:examples?|phrasing|forms|variants):\s*(?:`[^`\n]+`|'[^'\n]+'|"[^"\n]+")(?:(?:\s*,\s*(?:and\s+)?|\s+and\s+)(?:`[^`\n]+`|'[^'\n]+'|"[^"\n]+"))*/gi;
|
|
1135
|
+
const RECOVERY_REVIEW_LISTED_BLOCKED_EXAMPLE_PATTERN = /(?:^|[.;!?\n]\s*)blocked\s+examples:\s*(?:\r?\n[ \t]*(?:[-*]|\d+[.)])\s+[^\r\n]+)+/gim;
|
|
1136
|
+
const RECOVERY_REVIEW_BLOCKS_QUOTED_EXAMPLE_PATTERN = /\b(?:this\s+)?blocks?\s+examples?\s+like\s+(?:`[^`\n]+`|'[^'\n]+'|"[^"\n]+")(?:(?:\s*,\s*(?:and\s+)?|\s+and\s+)(?:`[^`\n]+`|'[^'\n]+'|"[^"\n]+"))*/gi;
|
|
1137
|
+
const RECOVERY_REVIEW_EXAMPLES_LIKE_BLOCKED_PATTERN = /\bexamples?\s+like\s+(?:`[^`\n]+`|'[^'\n]+'|"[^"\n]+")(?:(?:\s*,\s*(?:and\s+)?|\s+and\s+)(?:`[^`\n]+`|'[^'\n]+'|"[^"\n]+"))*\s+(?:is|are|remains?)\s+blocked\b/gi;
|
|
1138
|
+
const RECOVERY_REVIEW_QUOTED_VISIBLE_BLOCKED_PATTERN = /(?:\b(?:(?:the\s+)?examples?|commands?\s+hidden\s+in)\s+|(?:^|[\s([{]))(?:`[^`\n]+`|'[^'\n]+'|"[^"\n]+")(?:(?:\s*,\s*(?:and\s+|or\s+)?|\s+(?:and|or)\s+)(?:`[^`\n]+`|'[^'\n]+'|"[^"\n]+"))*\s+(?:(?:is|are|remains?)\s+)?visible\s+and\s+blocked\b/gi;
|
|
1139
|
+
const RECOVERY_REVIEW_PROMPTS_LIKE_BLOCKED_PATTERN = /\bdescribed\s+prompts?\s+like\s+Run\s+git\s+rebase\s+main,\s+git\s+rebase\s+main,\s+Run\s+git\s+cherry-pick\s+abc123,\s+cherry-pick\s+abc123,\s+and\s+Stage\s+the\s+changed\s+files\s+as\s+blocked\b/gi;
|
|
1140
|
+
const RECOVERY_REVIEW_GIT_MUTATIONS_BROADER_CONTEXT_PATTERN = /\bpositive\s+git\s+mutations\s+are\s+broader:\s*git\s+branch\s+-D\s+old,\s+git\s+tag\s+-d\s+v1\.0,\s+git\s+stash,\s+git\s+revert\s+abc123,\s+and\s+natural\s+cherry\s+pick\s+abc123\s+now\s+trips?\s+the\s+recovery\s+barrier\b/gi;
|
|
1141
|
+
const RECOVERY_REVIEW_GIT_COVERAGE_CONTEXT_PATTERN = /\bbroadened\s+positive\s+git\s+mutation\s+coverage\s+for\s+natural\s+`cherry\s+pick`,\s+`revert`,\s+`stash`,\s+`tag`,\s+plus\s+git\s+`branch\|revert\|stash\|tag`/gi;
|
|
1142
|
+
const RECOVERY_REVIEW_REGRESSION_QUOTED_EXAMPLE_PATTERN = /\b(?:added\s+)?exact\s+regressions?\s+for\s+(?:`[^`\n]+`|'[^'\n]+'|"[^"\n]+")(?:(?:\s*,\s*(?:and\s+)?|\s+and\s+)(?:`[^`\n]+`|'[^'\n]+'|"[^"\n]+"))*/gi;
|
|
1143
|
+
const RECOVERY_REVIEW_REGRESSION_ANAPHORIC_EXAMPLE_PATTERN = /\b(?:added\s+)?exact\s+regressions?\s+for\s+(?:do|run|execute|perform|apply)\s+(?:it|that|this|(?:the\s+)?(?:(?:previous(?:ly)?|prior|above|quoted|blocked)\s+){0,4}(?:command|example|operation|action|phrase|instruction|request))(?:[\s,]+(?:now|still|again|really|actually|immediately)){0,3}[\s,]+anyway(?:(?:\s*,\s*(?:and\s+)?|\s+and\s+)(?:do|run|execute|perform|apply)\s+(?:it|that|this|(?:the\s+)?(?:(?:previous(?:ly)?|prior|above|quoted|blocked)\s+){0,4}(?:command|example|operation|action|phrase|instruction|request))(?:[\s,]+(?:now|still|again|really|actually|immediately)){0,3}[\s,]+anyway)*/gi;
|
|
1144
|
+
const RECOVERY_REVIEW_REGRESSION_FOLLOWED_BY_ANAPHORIC_PATTERN = /\badded\s+(?:exact\s+)?regressions?\s+for\s+quoted\s+rm\s+remaining\s+blocked\s+followed\s+by\s+execute\s+the\s+previous\s+command\b(?=\s*(?:[,.;!?\n)]|\bwhile\b|$))/gi;
|
|
1145
|
+
const RECOVERY_REVIEW_REGRESSION_FOLLOWED_BY_NAMED_RM_PATTERN = /\b(?:added\s+)?(?:exact\s+)?regressions?\s+for\s+quoted\s+rm\s+remaining\s+blocked\s+followed\s+by\s+(?:do|run|execute|perform|apply)\s+(?:it|that|this|(?:the\s+)?(?:(?:previous(?:ly)?|prior|above|quoted|blocked)\s+){0,4}(?:command|example|operation|action|phrase|instruction|request))(?:[\s,]+(?:now|still|again|really|actually|immediately)){0,3}(?:[\s,]+anyway)?(?=\s*(?:[,.;!?\n)]|\bwhile\b|$))/gi;
|
|
1146
|
+
const RECOVERY_REVIEW_BLOCKED_LIVE_VARIANT_CONTEXT_PATTERN = /\b(?:while\s+)?keeping\s+live-command\s+variants\s+such\s+as\s+followed\s+by\s+execute\s+the\s+previous\s+command\s+then\s+update\s+tests\s+blocked\b(?=\s*(?:[,.;!?\n)]|$))/gi;
|
|
1147
|
+
const RECOVERY_REVIEW_ANAPHORIC_REFERENCES_CONTEXT_PATTERN = /\bdirect\s+anaphoric\s+references\s+now\s+include\s+numeric\s+and\s+word\s+ordinals\s+through\s+tenth\s+plus\s+one,\s+so\s+(?:do|run|execute|perform|apply)\s+(?:it|that|this|(?:the\s+)?(?:(?:\d+(?:st|nd|rd|th)|first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth|last|next|previous(?:ly)?|prior|above|quoted|blocked)\s+){0,4}(?:command|example|operation|action|phrase|instruction|request|one))(?:[\s,]+(?:right|now|still|again|really|actually|immediately)){0,4}[\s,]+anyway(?:(?:\s*,\s*(?:and\s+)?|\s+and\s+)(?:do|run|execute|perform|apply)\s+(?:it|that|this|(?:the\s+)?(?:(?:\d+(?:st|nd|rd|th)|first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth|last|next|previous(?:ly)?|prior|above|quoted|blocked)\s+){0,4}(?:command|example|operation|action|phrase|instruction|request|one))(?:[\s,]+(?:right|now|still|again|really|actually|immediately)){0,4}[\s,]+anyway)*\s+(?:is|are|remains?)\s+blocked\b(?:\s+after\s+quoted\s+destructive\s+examples\s+are\s+scrubbed)?/gi;
|
|
1148
|
+
const RECOVERY_REVIEW_NO_ANAPHORIC_MUTATION_CLAUSE_PATTERN = /\b(?:do not|don't|must not)\s+(?:do|run|execute|perform|apply)\s+(?:it|that|this|(?:the\s+)?(?:(?:\d+(?:st|nd|rd|th)|first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth|last|next|previous(?:ly)?|prior|above|quoted|blocked)\s+){0,4}(?:command|example|operation|action|phrase|instruction|request|one))(?:(?!\b(?:and|but|then|however|nevertheless|nonetheless|yet)\b)[^,.;!?\n—–-]){0,80}/gi;
|
|
1149
|
+
const RECOVERY_REVIEW_NO_DELEGATION_CLAUSE_PATTERN = /\b(?:do not|don't|must not)\s+(?:(?:launch|start|spawn|run)\b(?:(?!\b(?:and|but|then|however|nevertheless|nonetheless|yet)\b)[^,.;!?\n])*(?:workers?|reviewers?|agents?|subagents?|children|child|runs?)|(?:get|let|request|hand\s+off|assign|use|have|tell)\b(?:(?!\b(?:and|but|then|however|nevertheless|nonetheless|yet)\b)[^,.;!?\n])*(?:workers?|reviewers?|agents?|subagents?|children|child)|(?:get|let|have|tell|request)\b(?:(?!\b(?:and|but|then|however|nevertheless|nonetheless|yet)\b)[^,.;!?\n])*(?:review|implementation|fix(?:es)?|changes?|follow-up)\b(?:(?!\b(?:and|but|then|however|nevertheless|nonetheless|yet)\b)[^,.;!?\n])*\b(?:from|with|via|by)\b(?:(?!\b(?:and|but|then|however|nevertheless|nonetheless|yet)\b)[^,.;!?\n])*(?:workers?|reviewers?|agents?|subagents?|children|child)|ask\b(?:(?!\b(?:and|but|then|however|nevertheless|nonetheless|yet)\b)[^,.;!?\n])*(?:(?:workers?|reviewers?|agents?|subagents?|children|child)\b(?:(?!\b(?:and|but|then|however|nevertheless|nonetheless|yet)\b)[^,.;!?\n])*\b(?:continue|implement|review|fix|edit|write|modify|change|patch|update|delete|remove|create|follow-up)|(?:review|implementation|fix(?:es)?|changes?|follow-up)\b(?:(?!\b(?:and|but|then|however|nevertheless|nonetheless|yet)\b)[^,.;!?\n])*\b(?:from|via)\b(?:(?!\b(?:and|but|then|however|nevertheless|nonetheless|yet)\b)[^,.;!?\n])*(?:workers?|reviewers?|agents?|subagents?|children|child)))\b/gi;
|
|
1150
|
+
const RECOVERY_REVIEW_DELEGATION_PATTERN = /\b(?:launch|start|spawn|run)\b[^,.;!?\n]*(?:workers?|reviewers?|agents?|subagents?|children|child|runs?)\b|\b(?:delegate|hand\s+off|assign)\s+(?:remediation|implementation(?:\s+follow-up)?|changes?|fix(?:es)?|follow-up)\s+to\s+(?:(?:a|an|the)\s+)?(?:workers?|reviewers?|agents?|subagents?|children|child)\b|\b(?:get|let|have|tell|request)\b[^,.;!?\n]*(?:workers?|reviewers?|agents?|subagents?|children|child)\b[^,.;!?\n]*\b(?:continue|implement|review|fix|edit|write|modify|change|patch|update|delete|remove|create|follow-up)\b|\b(?:get|let|have|tell|request)\b[^,.;!?\n]*(?:review|implementation|fix(?:es)?|changes?|follow-up)\b[^,.;!?\n]*\b(?:from|with|via|by)\b[^,.;!?\n]*(?:workers?|reviewers?|agents?|subagents?|children|child)\b|\bask\b[^,.;!?\n]*(?:(?:workers?|reviewers?|agents?|subagents?|children|child)\b[^,.;!?\n]*\b(?:continue|implement|review|fix|edit|write|modify|change|patch|update|delete|remove|create|follow-up)|(?:review|implementation|fix(?:es)?|changes?|follow-up)\b[^,.;!?\n]*\b(?:from|via)\b[^,.;!?\n]*(?:workers?|reviewers?|agents?|subagents?|children|child))\b|\buse\b[^,.;!?\n]*(?:workers?|reviewers?|agents?|subagents?|children|child|runs?)\s+(?:for|to)\s+(?:implementation|follow-up|remediation|fix|edit|write|modify|change|patch|update|delete|remove|create)\b/i;
|
|
1151
|
+
const RECOVERY_REVIEW_ANAPHORIC_MUTATION_PATTERN = /\b(?:do|run|execute|perform|apply)\s+(?:it|that|this|(?:the\s+)?(?:(?:\d+(?:st|nd|rd|th)|first|second|third|fourth|fifth|sixth|seventh|eighth|ninth|tenth|last|next|previous(?:ly)?|prior|above|quoted|blocked)\s+){0,4}(?:command|example|operation|action|phrase|instruction|request|one))(?!(?:\s+(?:(?:now|still|also|already)\s+)*(?:is|are|remains?)\s+blocked\b))(?:(?:[\s,]+\w+){0,4}[\s,]+anyway|(?:(?!\b(?:and|but|then|however|nevertheless|nonetheless|yet)\b)[^,.;!?\n]){0,80})(?=\s*(?:[,.;!?\n)]|\b(?:and|but|then|however|nevertheless|nonetheless|yet)\b|$))/i;
|
|
1152
|
+
const RECOVERY_REVIEW_DESTRUCTIVE_COMMAND_PATTERN = /(?:^|[\s;,.`'"([{])(?:\S*\/)?(?:rm|rmdir|unlink|truncate|mv|cp|chmod|chown)\b|\bgit\b(?:\s+(?:-[A-Za-z](?:\s+(?:"[^"\n]*"|'[^'\n]*'|\S+))?|--(?:git-dir|work-tree|namespace|exec-path|config-env)(?:=(?:"[^"\n]*"|'[^'\n]*'|\S+)|\s+(?:"[^"\n]*"|'[^'\n]*'|\S+))|--[A-Za-z0-9-]+(?:=(?:"[^"\n]*"|'[^'\n]*'|\S+))?))*\s+(?:add|branch|cherry-pick|clean|commit|merge|rebase|reset|restore|revert|stash|tag|checkout|switch)\b/i;
|
|
1153
|
+
const RECOVERY_REVIEW_DASH_LIVE_ACTION_PATTERN = /(?:[—–]|--|\s-\s|:|\s\/\s)\s*(?:then\s+)?(?:(?:add|append|apply|change|cherry[ -]pick|clean|commit|copy|create|delete|edit|fix|implement|insert|make|merge|move|modify|mutate|open|patch|prepend|push|rebase|refactor|remove|rename|replace|revert|revise|rewrite|save|stage|stash|tag|touch|update|write)\b|(?:launch|start|spawn|run)\b[^,.;!?\n]*(?:workers?|reviewers?|agents?|subagents?|children|child|runs?)\b|(?:\S*\/)?(?:rm|rmdir|unlink|truncate|mv|cp|chmod|chown)\b|git\b[^,.;!?\n]*\b(?:add|branch|cherry-pick|clean|commit|merge|rebase|reset|restore|revert|stash|tag|checkout|switch)\b)/i;
|
|
1154
|
+
|
|
1155
|
+
function isExplicitReadOnlyRecoveryReview(params: Record<string, unknown>): boolean {
|
|
1156
|
+
const agent = typeof params.agent === "string" ? params.agent.trim() : "";
|
|
1157
|
+
const task = typeof params.task === "string" ? params.task.trim() : "";
|
|
1158
|
+
const taskDestructiveCommandText = task
|
|
1159
|
+
.replace(RECOVERY_REVIEW_BLOCKED_CONTEXT_FRAGMENT_PATTERN, " ")
|
|
1160
|
+
.replace(RECOVERY_REVIEW_REGRESSION_EVIDENCE_PATTERN, " ")
|
|
1161
|
+
.replace(RECOVERY_REVIEW_BLOCKED_QUOTED_EXAMPLE_PATTERN, " ")
|
|
1162
|
+
.replace(RECOVERY_REVIEW_CONTEXT_QUOTED_EXAMPLE_PATTERN, " ")
|
|
1163
|
+
.replace(RECOVERY_REVIEW_LISTED_BLOCKED_EXAMPLE_PATTERN, " ")
|
|
1164
|
+
.replace(RECOVERY_REVIEW_BLOCKS_QUOTED_EXAMPLE_PATTERN, " ")
|
|
1165
|
+
.replace(RECOVERY_REVIEW_EXAMPLES_LIKE_BLOCKED_PATTERN, " ")
|
|
1166
|
+
.replace(RECOVERY_REVIEW_QUOTED_VISIBLE_BLOCKED_PATTERN, " ")
|
|
1167
|
+
.replace(RECOVERY_REVIEW_PROMPTS_LIKE_BLOCKED_PATTERN, " ")
|
|
1168
|
+
.replace(RECOVERY_REVIEW_GIT_MUTATIONS_BROADER_CONTEXT_PATTERN, " ")
|
|
1169
|
+
.replace(RECOVERY_REVIEW_GIT_COVERAGE_CONTEXT_PATTERN, " ")
|
|
1170
|
+
.replace(RECOVERY_REVIEW_REGRESSION_FOLLOWED_BY_NAMED_RM_PATTERN, " ");
|
|
1171
|
+
const taskDashLiveActionText = task
|
|
1172
|
+
.replace(RECOVERY_REVIEW_DELIVERABLE_PATTERN, " ")
|
|
1173
|
+
.replace(RECOVERY_REVIEW_CONTEXT_OBJECT_PATTERN, " ")
|
|
1174
|
+
.replace(RECOVERY_REVIEW_MUTATION_NOUN_CONTEXT_PATTERN, " ")
|
|
1175
|
+
.replace(RECOVERY_REVIEW_PRIOR_FIX_CONTEXT_PATTERN, " ")
|
|
1176
|
+
.replace(RECOVERY_REVIEW_DETECTION_CONTEXT_PATTERN, " ")
|
|
1177
|
+
.replace(RECOVERY_REVIEW_PATTERN_CHANGE_CONTEXT_PATTERN, " ")
|
|
1178
|
+
.replace(RECOVERY_REVIEW_GIT_PATTERN_CHANGE_CONTEXT_PATTERN, " ")
|
|
1179
|
+
.replace(RECOVERY_REVIEW_VALIDATION_EVIDENCE_PATTERN, " ")
|
|
1180
|
+
.replace(RECOVERY_REVIEW_CONTRACT_PROHIBITION_PATTERN, " ")
|
|
1181
|
+
.replace(RECOVERY_REVIEW_ONLY_REVIEW_CONTRACT_PATTERN, " ")
|
|
1182
|
+
.replace(RECOVERY_REVIEW_BLOCKED_QUOTED_EXAMPLE_PATTERN, " ")
|
|
1183
|
+
.replace(RECOVERY_REVIEW_CONTEXT_QUOTED_EXAMPLE_PATTERN, " ")
|
|
1184
|
+
.replace(RECOVERY_REVIEW_LISTED_BLOCKED_EXAMPLE_PATTERN, " ")
|
|
1185
|
+
.replace(RECOVERY_REVIEW_BLOCKS_QUOTED_EXAMPLE_PATTERN, " ")
|
|
1186
|
+
.replace(RECOVERY_REVIEW_EXAMPLES_LIKE_BLOCKED_PATTERN, " ")
|
|
1187
|
+
.replace(RECOVERY_REVIEW_QUOTED_VISIBLE_BLOCKED_PATTERN, " ")
|
|
1188
|
+
.replace(RECOVERY_REVIEW_PROMPTS_LIKE_BLOCKED_PATTERN, " ")
|
|
1189
|
+
.replace(RECOVERY_REVIEW_GIT_MUTATIONS_BROADER_CONTEXT_PATTERN, " ")
|
|
1190
|
+
.replace(RECOVERY_REVIEW_GIT_COVERAGE_CONTEXT_PATTERN, " ")
|
|
1191
|
+
.replace(RECOVERY_REVIEW_REGRESSION_QUOTED_EXAMPLE_PATTERN, " ")
|
|
1192
|
+
.replace(RECOVERY_REVIEW_REGRESSION_ANAPHORIC_EXAMPLE_PATTERN, " ")
|
|
1193
|
+
.replace(RECOVERY_REVIEW_REGRESSION_FOLLOWED_BY_ANAPHORIC_PATTERN, " ")
|
|
1194
|
+
.replace(RECOVERY_REVIEW_BLOCKED_LIVE_VARIANT_CONTEXT_PATTERN, " ")
|
|
1195
|
+
.replace(RECOVERY_REVIEW_ANAPHORIC_REFERENCES_CONTEXT_PATTERN, " ");
|
|
1196
|
+
const taskMutationText = task
|
|
1197
|
+
.replace(RECOVERY_REVIEW_DELIVERABLE_PATTERN, " ")
|
|
1198
|
+
.replace(RECOVERY_REVIEW_CONTEXT_OBJECT_PATTERN, " ")
|
|
1199
|
+
.replace(RECOVERY_REVIEW_MUTATION_NOUN_CONTEXT_PATTERN, " ")
|
|
1200
|
+
.replace(RECOVERY_REVIEW_PRIOR_FIX_CONTEXT_PATTERN, " ")
|
|
1201
|
+
.replace(RECOVERY_REVIEW_DETECTION_CONTEXT_PATTERN, " ")
|
|
1202
|
+
.replace(RECOVERY_REVIEW_PATTERN_CHANGE_CONTEXT_PATTERN, " ")
|
|
1203
|
+
.replace(RECOVERY_REVIEW_GIT_PATTERN_CHANGE_CONTEXT_PATTERN, " ")
|
|
1204
|
+
.replace(RECOVERY_REVIEW_VALIDATION_EVIDENCE_PATTERN, " ")
|
|
1205
|
+
.replace(RECOVERY_REVIEW_CONTRACT_PROHIBITION_PATTERN, " ")
|
|
1206
|
+
.replace(RECOVERY_REVIEW_ONLY_REVIEW_CONTRACT_PATTERN, " ")
|
|
1207
|
+
.replace(RECOVERY_REVIEW_BLOCKED_CONTEXT_FRAGMENT_PATTERN, " ")
|
|
1208
|
+
.replace(RECOVERY_REVIEW_REGRESSION_EVIDENCE_PATTERN, " ")
|
|
1209
|
+
.replace(RECOVERY_REVIEW_BLOCKED_QUOTED_EXAMPLE_PATTERN, " ")
|
|
1210
|
+
.replace(RECOVERY_REVIEW_CONTEXT_QUOTED_EXAMPLE_PATTERN, " ")
|
|
1211
|
+
.replace(RECOVERY_REVIEW_LISTED_BLOCKED_EXAMPLE_PATTERN, " ")
|
|
1212
|
+
.replace(RECOVERY_REVIEW_BLOCKS_QUOTED_EXAMPLE_PATTERN, " ")
|
|
1213
|
+
.replace(RECOVERY_REVIEW_EXAMPLES_LIKE_BLOCKED_PATTERN, " ")
|
|
1214
|
+
.replace(RECOVERY_REVIEW_QUOTED_VISIBLE_BLOCKED_PATTERN, " ")
|
|
1215
|
+
.replace(RECOVERY_REVIEW_PROMPTS_LIKE_BLOCKED_PATTERN, " ")
|
|
1216
|
+
.replace(RECOVERY_REVIEW_GIT_MUTATIONS_BROADER_CONTEXT_PATTERN, " ")
|
|
1217
|
+
.replace(RECOVERY_REVIEW_GIT_COVERAGE_CONTEXT_PATTERN, " ")
|
|
1218
|
+
.replace(RECOVERY_REVIEW_REGRESSION_QUOTED_EXAMPLE_PATTERN, " ")
|
|
1219
|
+
.replace(RECOVERY_REVIEW_REGRESSION_ANAPHORIC_EXAMPLE_PATTERN, " ")
|
|
1220
|
+
.replace(RECOVERY_REVIEW_REGRESSION_FOLLOWED_BY_ANAPHORIC_PATTERN, " ")
|
|
1221
|
+
.replace(RECOVERY_REVIEW_BLOCKED_LIVE_VARIANT_CONTEXT_PATTERN, " ")
|
|
1222
|
+
.replace(RECOVERY_REVIEW_ANAPHORIC_REFERENCES_CONTEXT_PATTERN, " ")
|
|
1223
|
+
.replace(RECOVERY_REVIEW_NO_ANAPHORIC_MUTATION_CLAUSE_PATTERN, " ")
|
|
1224
|
+
.replace(RECOVERY_REVIEW_NO_DELEGATION_CLAUSE_PATTERN, " ")
|
|
1225
|
+
.replace(RECOVERY_REVIEW_NO_MUTATION_CLAUSE_PATTERN, " ")
|
|
1226
|
+
.replace(new RegExp(RECOVERY_REVIEW_READ_ONLY_PATTERN.source, "gi"), " ");
|
|
1227
|
+
return params.acceptance === false
|
|
1228
|
+
&& agent !== ""
|
|
1229
|
+
&& /\b(?:advisor|oracle|review|reviewer)\b/i.test(agent)
|
|
1230
|
+
&& RECOVERY_REVIEW_READ_ONLY_PATTERN.test(task)
|
|
1231
|
+
&& !RECOVERY_REVIEW_DESTRUCTIVE_COMMAND_PATTERN.test(taskDestructiveCommandText)
|
|
1232
|
+
&& !RECOVERY_REVIEW_MUTATION_VERB_PATTERN.test(taskMutationText)
|
|
1233
|
+
&& !RECOVERY_REVIEW_DELEGATION_PATTERN.test(taskMutationText)
|
|
1234
|
+
&& !RECOVERY_REVIEW_ANAPHORIC_MUTATION_PATTERN.test(taskMutationText)
|
|
1235
|
+
&& !RECOVERY_REVIEW_DESTRUCTIVE_COMMAND_PATTERN.test(taskMutationText)
|
|
1236
|
+
&& !RECOVERY_REVIEW_DASH_LIVE_ACTION_PATTERN.test(taskDashLiveActionText)
|
|
1237
|
+
&& classifyTaskMutationIntent(agent, task).kind === "read-only";
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function recoveryBarrierMessage(sourceKey: string, target: string): string {
|
|
1241
|
+
return `Run '${target}' cannot launch after run '${sourceKey}' returned rejected acceptance recovery; only explicit read-only review children with acceptance:false may follow.`;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1070
1244
|
function isPlainJsonObject(value: unknown): value is Record<string, unknown> {
|
|
1071
1245
|
if (!isRecord(value)) return false;
|
|
1072
1246
|
const prototype = Object.getPrototypeOf(value);
|
|
@@ -1227,7 +1401,7 @@ function validateStaticHostCall(call: AstNode): WorkflowScriptValidationError[]
|
|
|
1227
1401
|
for (const property of params.properties) {
|
|
1228
1402
|
if (!astNode(property)) continue;
|
|
1229
1403
|
const name = staticPropertyKey(property);
|
|
1230
|
-
if (name !== undefined && !allowed.has(name)) errors.push({ message: `runs.host params contain unsupported field '${name}'.`, ...nodeLocation(property) });
|
|
1404
|
+
if (name !== undefined && !allowed.has(name)) errors.push({ message: name === "cwd" ? "runs.host params contain unsupported field 'cwd'. The host step does not accept per-step cwd; commands and relative output paths use the workflow cwd. Set cwd on the outer subagent request, or put a trusted directory change in command (for example, 'cd /path/to/worktree && npm test')." : `runs.host params contain unsupported field '${name}'.`, ...nodeLocation(property) });
|
|
1231
1405
|
}
|
|
1232
1406
|
const kindNode = directObjectPropertyValue(params, "kind");
|
|
1233
1407
|
const commandNode = directObjectPropertyValue(params, "command");
|
|
@@ -1478,7 +1652,7 @@ function setupAbortResumeParams(params: Record<string, unknown>, result: Workflo
|
|
|
1478
1652
|
export async function runWorkflowScript(options: RunWorkflowScriptOptions): Promise<WorkflowScriptResult> {
|
|
1479
1653
|
if (!options.script.trim()) throw new Error("workflowScript must not be empty.");
|
|
1480
1654
|
if (options.timeoutMs !== undefined && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1)) throw new Error("workflow script timeout must be a positive integer.");
|
|
1481
|
-
if (options.globalConcurrencyLimit !== undefined && (!Number.
|
|
1655
|
+
if (options.globalConcurrencyLimit !== undefined && (!Number.isSafeInteger(options.globalConcurrencyLimit) || options.globalConcurrencyLimit < 1)) {
|
|
1482
1656
|
throw new Error("workflow script global concurrency limit must be a positive integer.");
|
|
1483
1657
|
}
|
|
1484
1658
|
const launchSemaphore = new Semaphore(options.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT);
|
|
@@ -1505,6 +1679,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1505
1679
|
const observedSteerCalls = new Set<number>();
|
|
1506
1680
|
const observedHostCalls = new Set<number>();
|
|
1507
1681
|
const childController = new AbortController();
|
|
1682
|
+
let acceptanceRecoveryBarrier: { key: string } | undefined;
|
|
1508
1683
|
let settled = false;
|
|
1509
1684
|
let finishing = false;
|
|
1510
1685
|
|
|
@@ -1524,6 +1699,13 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1524
1699
|
console.error("Workflow onTrace callback failed:", error);
|
|
1525
1700
|
}
|
|
1526
1701
|
};
|
|
1702
|
+
const lanePlanChanged = (lanes: WorkflowLanePlan[]) => {
|
|
1703
|
+
try {
|
|
1704
|
+
options.onLanePlan?.(lanes);
|
|
1705
|
+
} catch (error) {
|
|
1706
|
+
console.error("Workflow onLanePlan callback failed:", error);
|
|
1707
|
+
}
|
|
1708
|
+
};
|
|
1527
1709
|
const hostStepChanged = (hostStep: HostStepNodeV1) => {
|
|
1528
1710
|
try {
|
|
1529
1711
|
options.onHostStep?.(hostStep);
|
|
@@ -1536,6 +1718,13 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1536
1718
|
const text = error instanceof Error ? error.message : String(error);
|
|
1537
1719
|
return { key, ok: false, output: text, error: text, artifactPaths: [] };
|
|
1538
1720
|
};
|
|
1721
|
+
const assertRecoveryBarrierAllowsRun = (key: string, params: Record<string, unknown>): void => {
|
|
1722
|
+
if (!acceptanceRecoveryBarrier || isExplicitReadOnlyRecoveryReview(params)) return;
|
|
1723
|
+
throw new Error(recoveryBarrierMessage(acceptanceRecoveryBarrier.key, key));
|
|
1724
|
+
};
|
|
1725
|
+
const recordAcceptanceRecoveryBarrier = (key: string, result: WorkflowScriptChildResult): void => {
|
|
1726
|
+
if (isAcceptanceMetadataRecovery(result)) acceptanceRecoveryBarrier = { key };
|
|
1727
|
+
};
|
|
1539
1728
|
const stopChild = (key: string, message = `Workflow child '${key}' stopped by user.`): boolean => {
|
|
1540
1729
|
if (!launches.has(key) || children.has(key)) return false;
|
|
1541
1730
|
stoppedLaunches.add(key);
|
|
@@ -1626,6 +1815,10 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1626
1815
|
});
|
|
1627
1816
|
worker.on("message", (message: Record<string, unknown>) => {
|
|
1628
1817
|
if (settled) return;
|
|
1818
|
+
if (message.type === "lanePlan" && Array.isArray(message.lanes)) {
|
|
1819
|
+
lanePlanChanged(message.lanes as WorkflowLanePlan[]);
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1629
1822
|
if (message.type === "emit") {
|
|
1630
1823
|
try {
|
|
1631
1824
|
assertWorkflowJsonValue(message.value, "emit");
|
|
@@ -1712,6 +1905,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1712
1905
|
return respond(Promise.reject(error));
|
|
1713
1906
|
}
|
|
1714
1907
|
if (message.method === "state.get") return respond(Promise.resolve().then(() => options.state!.get(key)));
|
|
1908
|
+
if (acceptanceRecoveryBarrier) return respond(Promise.reject(new Error(recoveryBarrierMessage(acceptanceRecoveryBarrier.key, `state.set('${key}')`))));
|
|
1715
1909
|
const value = message.args.value;
|
|
1716
1910
|
try {
|
|
1717
1911
|
assertWorkflowJsonValue(value, `state.set('${key}') value`);
|
|
@@ -1748,6 +1942,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1748
1942
|
const steerMessage = message.args.message;
|
|
1749
1943
|
if (typeof steerMessage !== "string" || !steerMessage.trim()) return respond(Promise.reject(new Error(`runs.steer('${key}') requires a non-empty message.`)));
|
|
1750
1944
|
const steerOptions = isRecord(message.args.options) ? message.args.options as WorkflowSteerOptions : {};
|
|
1945
|
+
if (acceptanceRecoveryBarrier) return respond(Promise.reject(new Error(recoveryBarrierMessage(acceptanceRecoveryBarrier.key, `runs.steer('${key}')`))));
|
|
1751
1946
|
const startedAt = Date.now();
|
|
1752
1947
|
trace.push({ operation: "steer", key, state: "started" });
|
|
1753
1948
|
traceChanged();
|
|
@@ -1778,6 +1973,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1778
1973
|
} catch (error) {
|
|
1779
1974
|
return respond(Promise.reject(error));
|
|
1780
1975
|
}
|
|
1976
|
+
if (acceptanceRecoveryBarrier) return respond(Promise.reject(new Error(recoveryBarrierMessage(acceptanceRecoveryBarrier.key, `runs.host('${key}')`))));
|
|
1781
1977
|
if (!options.host) return respond(Promise.reject(new Error("runs.host is unavailable in this host context.")));
|
|
1782
1978
|
if (hostCalls.size >= HOST_STEP_MAX_COUNT) return respond(Promise.reject(new Error(`workflowScript supports at most ${HOST_STEP_MAX_COUNT} runs.host calls.`)));
|
|
1783
1979
|
const startedAt = Date.now();
|
|
@@ -1833,7 +2029,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1833
2029
|
const deliver = (promise: Promise<WorkflowScriptChildResult>) => collectFailure
|
|
1834
2030
|
? promise
|
|
1835
2031
|
: promise.then((result) => {
|
|
1836
|
-
|
|
2032
|
+
const recoverableAcceptanceMetadata = result.recovery?.status === "available-for-review"
|
|
2033
|
+
&& result.recovery.reason === "acceptance-metadata-rejected";
|
|
2034
|
+
if (!result.ok && !result.stopped && !recoverableAcceptanceMetadata) {
|
|
1837
2035
|
const childError = new Error(result.detached ? `Run '${key}' detached: ${result.error ?? result.output}` : `Run '${key}' failed: ${result.error ?? result.output}`) as Error & { workflowErrorKind?: "detached-child" };
|
|
1838
2036
|
if (result.detached) childError.workflowErrorKind = "detached-child";
|
|
1839
2037
|
throw childError;
|
|
@@ -1897,6 +2095,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1897
2095
|
});
|
|
1898
2096
|
admission = Promise.resolve().then(() => {
|
|
1899
2097
|
if (settled || finishing) return;
|
|
2098
|
+
for (const call of calls) assertRecoveryBarrierAllowsRun(call.key, call.params);
|
|
1900
2099
|
return options.admit?.(calls);
|
|
1901
2100
|
});
|
|
1902
2101
|
if (batch) batchAdmissions.set(batch.id, admission);
|
|
@@ -1957,6 +2156,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1957
2156
|
childStopControllers.delete(key);
|
|
1958
2157
|
if (stoppedLaunches.has(key)) return children.get(key) ?? normalized;
|
|
1959
2158
|
children.set(key, normalized);
|
|
2159
|
+
recordAcceptanceRecoveryBarrier(key, normalized);
|
|
1960
2160
|
const state = normalized.ok ? "completed" : normalized.stopped ? "stopped" : normalized.detached ? "detached" : "failed";
|
|
1961
2161
|
trace.push({ operation: "run", key, state, durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), ...(generatedLaneKey ? { generatedLaneKey } : {}), ...(normalized.agent ? { agent: normalized.agent } : {}), ...(normalized.runId ? { runId: normalized.runId } : {}), ...(!normalized.ok ? { error: normalized.error ?? normalized.output } : {}) });
|
|
1962
2162
|
traceChanged();
|
|
@@ -117,5 +117,5 @@ export function parseWorkflowChildSummary(value: unknown): WorkflowChildSummaryV
|
|
|
117
117
|
if (new Set(children.map((child) => child.childId)).size !== children.length) throw new Error("workflowChildren has duplicate childId values.");
|
|
118
118
|
if (typeof input.parentToolCallId !== "string") throw new Error("workflowChildren.parentToolCallId is invalid.");
|
|
119
119
|
if (typeof input.workflowRunId !== "string") throw new Error("workflowChildren.workflowRunId is invalid.");
|
|
120
|
-
return { version: 1, parentToolCallId: requiredId(input.parentToolCallId, "parentToolCallId"), workflowRunId: requiredId(input.workflowRunId, "workflowRunId"), inventoryComplete: input.inventoryComplete, workflowState, children };
|
|
120
|
+
return { version: 1, parentToolCallId: requiredId(input.parentToolCallId, "workflowChildren.parentToolCallId"), workflowRunId: requiredId(input.workflowRunId, "workflowChildren.workflowRunId"), inventoryComplete: input.inventoryComplete, workflowState, children };
|
|
121
121
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { writePrivateAtomicJson } from "../shared/atomic-json.ts";
|
|
4
|
-
import type { ExternalCliReceiptMetadata, WorkflowReceipt, WorkflowReceiptEntry, WorkflowReceiptState, WorkflowRecoveryAction, WorkflowTerminalOutcome, WorkflowTerminalResolution } from "../shared/types.ts";
|
|
4
|
+
import type { AcceptanceRecoveryMetadata, ExternalCliReceiptMetadata, WorkflowReceipt, WorkflowReceiptEntry, WorkflowReceiptState, WorkflowRecoveryAction, WorkflowResourceProvenanceV1, WorkflowTerminalOutcome, WorkflowTerminalResolution } from "../shared/types.ts";
|
|
5
5
|
import type { WorkflowReceiptResumeReference, WorkflowScriptChildResult } from "./scripted-workflow.ts";
|
|
6
6
|
import { parseWorkflowChildSummary } from "./workflow-child-summary.ts";
|
|
7
7
|
import { HOST_STEP_MAX_COUNT, assertUniqueHostStepIds, parseHostStepNode } from "../runs/shared/host-step-status.ts";
|
|
@@ -14,6 +14,7 @@ export const WORKFLOW_RECEIPT_FILE = "workflow-receipt.json";
|
|
|
14
14
|
const MAX_WORKFLOW_RECEIPT_BYTES = 2 * 1024 * 1024;
|
|
15
15
|
|
|
16
16
|
const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
17
|
+
const RESOURCE_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
|
17
18
|
|
|
18
19
|
function assertSafeRunId(value: string, label: string): string {
|
|
19
20
|
const normalized = value.trim();
|
|
@@ -38,6 +39,7 @@ export function buildWorkflowReceipt(input: {
|
|
|
38
39
|
children: WorkflowScriptChildResult[];
|
|
39
40
|
hostSteps?: WorkflowReceipt["hostSteps"];
|
|
40
41
|
workflowChildren?: WorkflowReceipt["workflowChildren"];
|
|
42
|
+
resource?: WorkflowResourceProvenanceV1;
|
|
41
43
|
terminalOutcome?: WorkflowTerminalOutcome;
|
|
42
44
|
createdAt?: number;
|
|
43
45
|
}): WorkflowReceipt {
|
|
@@ -61,6 +63,7 @@ export function buildWorkflowReceipt(input: {
|
|
|
61
63
|
...(child.requestedContext ? { requestedContext: child.requestedContext } : {}),
|
|
62
64
|
...(child.resolvedContext ? { resolvedContext: child.resolvedContext } : {}),
|
|
63
65
|
...(child.outputReference ? { outputReference: child.outputReference } : {}),
|
|
66
|
+
...(child.recovery ? { acceptanceRecovery: child.recovery } : {}),
|
|
64
67
|
...(child.externalAdapter ? { externalAdapter: child.externalAdapter } : {}),
|
|
65
68
|
continuation: { runIds },
|
|
66
69
|
};
|
|
@@ -71,7 +74,8 @@ export function buildWorkflowReceipt(input: {
|
|
|
71
74
|
if (input.hostSteps && input.hostSteps.length > HOST_STEP_MAX_COUNT) throw new Error(`Workflow receipt has more than ${HOST_STEP_MAX_COUNT} host steps.`);
|
|
72
75
|
const hostSteps = input.hostSteps?.map((hostStep, index) => parseHostStepNode(hostStep, `workflow receipt hostSteps[${index}]`));
|
|
73
76
|
if (hostSteps) assertUniqueHostStepIds(hostSteps, "workflow receipt");
|
|
74
|
-
|
|
77
|
+
const resource = parseWorkflowResource(input.resource, "workflow receipt");
|
|
78
|
+
return { version: WORKFLOW_RECEIPT_VERSION, workflowRunId, state: input.state, createdAt: input.createdAt ?? Date.now(), entries, ...(resource ? { resource } : {}), ...(hostSteps?.length ? { hostSteps } : {}), ...(input.workflowChildren ? { workflowChildren: input.workflowChildren } : {}), ...(input.terminalOutcome ? { terminalOutcome: input.terminalOutcome } : {}) };
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
export function writeWorkflowReceipt(asyncDir: string, receipt: WorkflowReceipt): string {
|
|
@@ -210,6 +214,20 @@ function parseTerminalOutcome(value: unknown, label: string): WorkflowTerminalOu
|
|
|
210
214
|
return { state: "partial", reason: outcome.reason };
|
|
211
215
|
}
|
|
212
216
|
|
|
217
|
+
function parseAcceptanceRecoveryMetadata(value: unknown, label: string): AcceptanceRecoveryMetadata | undefined {
|
|
218
|
+
if (value === undefined) return undefined;
|
|
219
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`);
|
|
220
|
+
const recovery = value as Record<string, unknown>;
|
|
221
|
+
if (recovery.status !== "available-for-review" || recovery.reason !== "acceptance-metadata-rejected") throw new Error(`${label} is invalid.`);
|
|
222
|
+
if (typeof recovery.reportPath !== "string" || !recovery.reportPath.trim() || typeof recovery.reportHash !== "string" || !/^[a-f0-9]{64}$/u.test(recovery.reportHash)) throw new Error(`${label} is invalid.`);
|
|
223
|
+
return {
|
|
224
|
+
status: "available-for-review",
|
|
225
|
+
reason: "acceptance-metadata-rejected",
|
|
226
|
+
reportPath: recovery.reportPath,
|
|
227
|
+
reportHash: recovery.reportHash,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
213
231
|
function parseEntry(value: unknown, key: string, source: string): WorkflowReceiptEntry {
|
|
214
232
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid workflow receipt '${source}': entry '${key}' must be an object.`);
|
|
215
233
|
const entry = value as Record<string, unknown>;
|
|
@@ -231,10 +249,11 @@ function parseEntry(value: unknown, key: string, source: string): WorkflowReceip
|
|
|
231
249
|
if (state === "resumable" && latestRunId === undefined) throw new Error(`Invalid workflow receipt '${source}': entry '${key}' resumable entry has no retained run id.`);
|
|
232
250
|
if (state === "not-resumable" && (typeof reason !== "string" || !reason.trim())) throw new Error(`Invalid workflow receipt '${source}': entry '${key}' non-resumable reason is missing.`);
|
|
233
251
|
const terminalOutcome = parseTerminalOutcome(entry.terminalOutcome, `Invalid workflow receipt '${source}': entry '${key}' terminalOutcome`);
|
|
252
|
+
const acceptanceRecovery = parseAcceptanceRecoveryMetadata(entry.acceptanceRecovery, `Invalid workflow receipt '${source}': entry '${key}' acceptanceRecovery`);
|
|
234
253
|
parseExternalCliReceiptMetadata(entry.externalAdapter, key, source);
|
|
235
254
|
const lane = normalizeWorkflowLaneMetadata(entry.lane, `Invalid workflow receipt '${source}': entry '${key}'.lane`);
|
|
236
255
|
assertWorkflowLaneKey(lane, key, `Invalid workflow receipt '${source}': entry '${key}'.lane`);
|
|
237
|
-
return { ...(value as WorkflowReceiptEntry), ...(lane ? { lane } : {}), ...(terminalOutcome ? { terminalOutcome } : {}) };
|
|
256
|
+
return { ...(value as WorkflowReceiptEntry), ...(lane ? { lane } : {}), ...(terminalOutcome ? { terminalOutcome } : {}), ...(acceptanceRecovery ? { acceptanceRecovery } : {}) };
|
|
238
257
|
}
|
|
239
258
|
|
|
240
259
|
function parseWorkflowResolution(value: unknown, source: string): WorkflowTerminalResolution | undefined {
|
|
@@ -243,6 +262,23 @@ function parseWorkflowResolution(value: unknown, source: string): WorkflowTermin
|
|
|
243
262
|
return value;
|
|
244
263
|
}
|
|
245
264
|
|
|
265
|
+
function parseWorkflowResource(value: unknown, source: string): WorkflowResourceProvenanceV1 | undefined {
|
|
266
|
+
if (value === undefined) return undefined;
|
|
267
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid workflow receipt '${source}': resource must be an object.`);
|
|
268
|
+
const resource = value as Record<string, unknown>;
|
|
269
|
+
if (resource.kind !== "workflow" || typeof resource.name !== "string" || !KEY_PATTERN.test(resource.name) || !Number.isInteger(resource.version) || (resource.version as number) < 1 || (resource.version as number) > 1_000_000 || resource.invocation !== "named" || resource.expansion !== "resolved" || typeof resource.id !== "string" || !RESOURCE_ID_PATTERN.test(resource.id)) {
|
|
270
|
+
throw new Error(`Invalid workflow receipt '${source}': resource is invalid.`);
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
kind: "workflow",
|
|
274
|
+
name: resource.name,
|
|
275
|
+
version: resource.version as number,
|
|
276
|
+
invocation: "named",
|
|
277
|
+
expansion: "resolved",
|
|
278
|
+
id: resource.id,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
246
282
|
function parseRecovery(value: unknown, workflowRunId: string, entries: Record<string, WorkflowReceiptEntry>, source: string): WorkflowRecoveryAction[] | undefined {
|
|
247
283
|
if (value === undefined) return undefined;
|
|
248
284
|
if (!Array.isArray(value)) throw new Error(`Invalid workflow receipt '${source}': recovery must be an array.`);
|
|
@@ -294,9 +330,10 @@ export function readWorkflowReceipt(asyncDirRoot: string, workflowRunId: string)
|
|
|
294
330
|
assertUniqueHostStepIds(hostSteps, receiptPath);
|
|
295
331
|
}
|
|
296
332
|
const workflowResolution = parseWorkflowResolution(receipt.workflowResolution, receiptPath);
|
|
333
|
+
const resource = parseWorkflowResource(receipt.resource, receiptPath);
|
|
297
334
|
const terminalOutcome = parseTerminalOutcome(receipt.terminalOutcome, `Invalid workflow receipt '${receiptPath}': terminalOutcome`);
|
|
298
335
|
const recovery = parseRecovery(receipt.recovery, workflowRunId, entries, receiptPath);
|
|
299
|
-
return { version: 1, workflowRunId, state: receipt.state, createdAt: receipt.createdAt, entries, ...(hostSteps?.length ? { hostSteps } : {}), ...(workflowChildren ? { workflowChildren } : {}), ...(workflowResolution ? { workflowResolution } : {}), ...(terminalOutcome ? { terminalOutcome } : {}), ...(recovery ? { recovery } : {}) };
|
|
336
|
+
return { version: 1, workflowRunId, state: receipt.state, createdAt: receipt.createdAt, entries, ...(resource ? { resource } : {}), ...(hostSteps?.length ? { hostSteps } : {}), ...(workflowChildren ? { workflowChildren } : {}), ...(workflowResolution ? { workflowResolution } : {}), ...(terminalOutcome ? { terminalOutcome } : {}), ...(recovery ? { recovery } : {}) };
|
|
300
337
|
}
|
|
301
338
|
|
|
302
339
|
export function resolveWorkflowReceiptResumeEntry(input: {
|