pi-subagents 0.65.1 → 0.67.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 +123 -0
- package/README.md +5 -4
- package/agents/evidence-auditor.md +34 -0
- package/agents/researcher.md +23 -13
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +20 -3
- package/docs/configuration.md +25 -5
- package/docs/extension-api.md +124 -18
- package/docs/missions.md +8 -0
- package/docs/models.md +59 -2
- package/docs/observability.md +46 -6
- package/docs/standalone-background.md +49 -0
- package/docs/tool-reference.md +20 -10
- package/docs/watchdog.md +35 -4
- package/docs/workflows.md +40 -19
- package/inspector-runner.mjs +2 -2
- package/package.json +2 -1
- package/prompts/parallel-review.md +1 -1
- package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
- package/skills/pi-subagents/SKILL.md +14 -0
- package/skills/pi-subagents/references/execution-controls.md +20 -5
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
- package/src/agents/advertised-agent-prompt.ts +94 -0
- package/src/agents/agent-management.ts +14 -1
- package/src/agents/agent-serializer.ts +2 -0
- package/src/agents/agents.ts +14 -0
- package/src/agents/builtin-names.ts +1 -0
- package/src/api/delegation.ts +4 -0
- package/src/api/preflight.ts +76 -45
- package/src/api/shared-types.ts +3 -1
- package/src/api/workflow-resources.ts +6 -0
- package/src/extension/fanout-child.ts +63 -4
- package/src/extension/index.ts +58 -8
- package/src/extension/public-execution.ts +4 -3
- package/src/extension/rpc.ts +8 -21
- package/src/extension/schemas.ts +71 -80
- package/src/extension/tool-description.ts +29 -81
- package/src/inspectors/actions.ts +148 -0
- package/src/inspectors/ghostty/actions.ts +74 -0
- package/src/inspectors/ghostty/plugin.ts +17 -0
- package/src/inspectors/herdr/actions.ts +99 -179
- package/src/inspectors/herdr/plugin.ts +20 -0
- package/src/inspectors/herdr/project-panes.ts +1 -1
- package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
- package/src/inspectors/plugins.ts +8 -0
- package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
- package/src/inspectors/types.ts +51 -0
- package/src/intercom/intercom-bridge.ts +50 -8
- package/src/intercom/native-supervisor-channel.ts +104 -67
- package/src/runs/background/active-async-capacity.ts +22 -18
- package/src/runs/background/async-execution.ts +45 -56
- package/src/runs/background/async-job-tracker.ts +35 -3
- package/src/runs/background/async-resume.ts +5 -9
- package/src/runs/background/async-status-snapshot.ts +10 -12
- package/src/runs/background/async-status.ts +17 -9
- package/src/runs/background/auto-drain.ts +44 -30
- package/src/runs/background/binary-bootstrap.ts +33 -0
- package/src/runs/background/chain-root-attachment.ts +8 -0
- package/src/runs/background/control-channel.ts +78 -44
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +117 -13
- package/src/runs/background/owned-process-tree.ts +35 -8
- package/src/runs/background/process-terminal.ts +23 -23
- package/src/runs/background/run-child-session.ts +121 -36
- package/src/runs/background/run-status.ts +78 -5
- package/src/runs/background/runner-aliases.ts +28 -9
- package/src/runs/background/runner-child-launch.ts +88 -0
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/scheduled-runs.ts +40 -13
- package/src/runs/background/stale-run-reconciler.ts +3 -1
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +458 -239
- package/src/runs/background/subagent-wait.ts +54 -8
- package/src/runs/background/wait-completions.ts +4 -0
- package/src/runs/background/wait-tool.ts +1 -1
- package/src/runs/foreground/async-steering-action.ts +37 -7
- package/src/runs/foreground/execution.ts +145 -56
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +584 -297
- package/src/runs/foreground/workflow-detach-reconcile.ts +10 -5
- package/src/runs/foreground/workflow-foreground-steering.ts +57 -2
- package/src/runs/shared/acceptance.ts +7 -4
- package/src/runs/shared/agent-contract.ts +1 -1
- package/src/runs/shared/async-status-projection.ts +51 -47
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +167 -3
- package/src/runs/shared/child-launch.ts +28 -13
- package/src/runs/shared/child-lifecycle.ts +6 -3
- package/src/runs/shared/child-runtime-config.ts +3 -1
- package/src/runs/shared/child-session.ts +75 -8
- package/src/runs/shared/child-tool-plan.ts +124 -5
- package/src/runs/shared/completion-evidence.ts +2 -2
- package/src/runs/shared/completion-guard.ts +6 -3
- package/src/runs/shared/effective-system-prompt.ts +33 -0
- package/src/runs/shared/external-cli-runner.ts +9 -7
- package/src/runs/shared/host-step-status.ts +11 -11
- package/src/runs/shared/llm-intent-arbiter.ts +21 -11
- package/src/runs/shared/model-fallback.ts +12 -6
- package/src/runs/shared/nested-events.ts +5 -5
- package/src/runs/shared/orca-progress-tabs.ts +7 -1
- package/src/runs/shared/parallel-handoff.ts +57 -12
- package/src/runs/shared/parallel-utils.ts +2 -2
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/readonly-drain-observation.ts +42 -0
- package/src/runs/shared/readonly-model-continuation.ts +69 -0
- package/src/runs/shared/readonly-session-evidence.ts +307 -0
- package/src/runs/shared/run-fanout-budget.ts +8 -8
- package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
- package/src/runs/shared/subagent-prompt-runtime.ts +20 -4
- package/src/runs/shared/task-intent.ts +46 -13
- package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
- package/src/runs/shared/worktree-setup-command.ts +190 -0
- package/src/runs/shared/worktree.ts +366 -208
- package/src/shared/fork-context.ts +15 -72
- package/src/shared/launch-contract.ts +65 -2
- package/src/shared/opencode-session-headers.ts +30 -0
- package/src/shared/types.ts +85 -61
- package/src/shared/utils.ts +7 -2
- package/src/shared/workflow-child-permit.ts +18 -13
- package/src/slash/delegation-adapters.ts +3 -1
- package/src/slash/delegation-request.ts +14 -0
- package/src/slash/slash-commands.ts +2 -1
- package/src/slash/subagents-admin.ts +11 -4
- package/src/tui/fleet-status.ts +164 -19
- package/src/tui/fleet.ts +27 -19
- package/src/tui/render.ts +172 -33
- package/src/watchdog/child-status.ts +8 -0
- package/src/watchdog/model-selection.ts +20 -0
- package/src/watchdog/permission-arbiter.ts +3 -1
- package/src/watchdog/register-child.ts +1 -0
- package/src/watchdog/register-main.ts +31 -27
- package/src/watchdog/review.ts +132 -67
- package/src/watchdog/runtime.ts +82 -20
- package/src/watchdog/scope.ts +1 -1
- package/src/watchdog/settings.ts +9 -3
- package/src/watchdog/tool-actions.ts +13 -12
- package/src/watchdog/turn-delta.ts +23 -0
- package/src/watchdog/types.ts +4 -0
- package/src/workflows/chat-progress.ts +3 -3
- package/src/workflows/scripted-workflow.ts +275 -17
- package/src/workflows/workflow-checklist.ts +13 -17
- package/src/workflows/workflow-child-summary.ts +57 -8
- package/src/workflows/workflow-preflight.ts +19 -19
- package/src/workflows/workflow-receipt.ts +3 -3
- package/src/workflows/workflow-resources.ts +96 -21
- package/src/workflows/workflow-settlement.ts +3 -0
- /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
|
@@ -6,10 +6,11 @@ import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../runs/shared/para
|
|
|
6
6
|
import { HOST_STEP_MAX_COUNT } from "../runs/shared/host-step-status.ts";
|
|
7
7
|
import { classifyTaskMutationIntent } from "../runs/shared/task-intent.ts";
|
|
8
8
|
import { describeGateAcceptanceConflict } from "../runs/shared/acceptance.ts";
|
|
9
|
-
import type { AcceptanceRecoveryMetadata,
|
|
9
|
+
import type { AcceptanceRecoveryMetadata, HostStepNode, SingleResult } from "../shared/types.ts";
|
|
10
10
|
import { normalizeWorkflowHostCommandParams, type WorkflowHostCommandParams, type WorkflowHostCommandResult } from "./host-command.ts";
|
|
11
11
|
|
|
12
12
|
const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
13
|
+
const BASE_REF_VALIDATION_ERROR = "baseRef must be a valid Git ref: use HEAD or a supported named ref (for example, refs/heads/main). Full 40/64-character commit IDs and revision expressions are unsupported.";
|
|
13
14
|
function validGitRef(ref: unknown): ref is string {
|
|
14
15
|
if (typeof ref !== "string" || !ref || ref === "@" || Buffer.byteLength(ref, "utf-8") > 1024 || ref.startsWith("/") || ref.endsWith("/") || ref.includes("//") || ref.includes("..") || ref.includes("@{")) return false;
|
|
15
16
|
if (/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/iu.test(ref)) return false;
|
|
@@ -23,11 +24,22 @@ export interface WorkflowScriptValidationError {
|
|
|
23
24
|
message: string;
|
|
24
25
|
line?: number;
|
|
25
26
|
column?: number;
|
|
27
|
+
kind?: "spawn-budget";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface WorkflowScriptValidationWarning {
|
|
31
|
+
message: string;
|
|
32
|
+
kind: "dynamic-spawn-count";
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
export interface WorkflowScriptValidationResult {
|
|
29
36
|
ok: boolean;
|
|
30
37
|
errors: WorkflowScriptValidationError[];
|
|
38
|
+
warnings?: WorkflowScriptValidationWarning[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface WorkflowScriptValidationOptions {
|
|
42
|
+
maxSubagentSpawnsPerRun?: number;
|
|
31
43
|
}
|
|
32
44
|
|
|
33
45
|
const WORKER_SOURCE = String.raw`
|
|
@@ -644,7 +656,7 @@ function validateRunCall(key, params, label, fingerprints) {
|
|
|
644
656
|
}
|
|
645
657
|
if (Object.prototype.hasOwnProperty.call(params, "clarify")) throw new Error(label + " does not support clarify UI.");
|
|
646
658
|
if (params.worktree !== undefined && typeof params.worktree !== "boolean") throw new Error(label + " worktree must be true or false.");
|
|
647
|
-
if (params.baseRef !== undefined && (typeof params.baseRef !== "string" || !validGitRef(params.baseRef))) throw new Error(label + " baseRef must be a valid Git ref.");
|
|
659
|
+
if (params.baseRef !== undefined && (typeof params.baseRef !== "string" || !validGitRef(params.baseRef))) throw new Error(label + " baseRef must be a valid Git ref: use HEAD or a supported named ref (for example, refs/heads/main). Full 40/64-character commit IDs and revision expressions are unsupported.");
|
|
648
660
|
validateLaneMetadata(params.lane, label + " lane", key);
|
|
649
661
|
if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) throw new Error(label + " gate must be a non-empty command string.");
|
|
650
662
|
if (params.gate !== undefined && params.acceptance !== undefined && params.acceptance !== false) throw new Error(label + " gate cannot be combined with acceptance; use one gate command or acceptance.verify." + describeGateAcceptanceConflict(params.gate, params.acceptance));
|
|
@@ -695,8 +707,26 @@ function validateHostCommand(key, params) {
|
|
|
695
707
|
hostKeys.add(key);
|
|
696
708
|
}
|
|
697
709
|
|
|
710
|
+
const RUNS_ALL_PERMISSIVE_WARNING = "runs.all received runs.run(...) launch promise(s) or thenable(s). Those children were already launched individually, so runs.all cannot apply batch fingerprint validation, batch grouping in the fleet view, or collectFailure semantics; a failed child will throw at the runs.all boundary instead of returning as { ok: false }. To get full runs.all semantics, pass config objects: runs.all(items.map((item) => ({ key: item.key, agent: item.agent, task: item.task }))).";
|
|
711
|
+
|
|
712
|
+
function runsAllItemThenableInfo(item, index) {
|
|
713
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) return null;
|
|
714
|
+
if (typeof item.then !== "function") return null;
|
|
715
|
+
const tracker = promiseObservationTracker(item);
|
|
716
|
+
const observation = tracker && Array.isArray(tracker.observations) ? tracker.observations.find((entry) => entry && entry.operation === "run") : undefined;
|
|
717
|
+
return {
|
|
718
|
+
key: observation && typeof observation.key === "string" ? observation.key : ("runs.all[" + index + "]"),
|
|
719
|
+
callId: observation && typeof observation.callId === "number" ? observation.callId : null,
|
|
720
|
+
promise: item,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
let warnedPermissiveRunsAll = false;
|
|
725
|
+
|
|
698
726
|
function launchRunsAll(items, generatedLaneKeys) {
|
|
699
727
|
if (!Array.isArray(items)) throw new Error("runs.all(items) requires an array.");
|
|
728
|
+
const anyThenable = items.some((item, index) => runsAllItemThenableInfo(item, index) !== null);
|
|
729
|
+
if (anyThenable) return launchRunsAllPermissive(items);
|
|
700
730
|
const fingerprints = new Map(runFingerprints);
|
|
701
731
|
const calls = [];
|
|
702
732
|
for (let index = 0; index < items.length; index++) {
|
|
@@ -713,6 +743,33 @@ function launchRunsAll(items, generatedLaneKeys) {
|
|
|
713
743
|
return { calls, launched };
|
|
714
744
|
}
|
|
715
745
|
|
|
746
|
+
function launchRunsAllPermissive(items) {
|
|
747
|
+
if (!warnedPermissiveRunsAll) {
|
|
748
|
+
warnedPermissiveRunsAll = true;
|
|
749
|
+
try { capturedConsole.warn(RUNS_ALL_PERMISSIVE_WARNING); } catch { /* console emission is best-effort */ }
|
|
750
|
+
}
|
|
751
|
+
const fingerprints = new Map(runFingerprints);
|
|
752
|
+
const calls = [];
|
|
753
|
+
const launched = [];
|
|
754
|
+
for (let index = 0; index < items.length; index++) {
|
|
755
|
+
if (!Object.prototype.hasOwnProperty.call(items, index)) throw new Error("runs.all items must not contain sparse entries.");
|
|
756
|
+
const item = items[index];
|
|
757
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) throw new Error("runs.all item " + index + " must be an object or a runs.run(...) launch promise.");
|
|
758
|
+
const thenable = runsAllItemThenableInfo(item, index);
|
|
759
|
+
if (thenable) {
|
|
760
|
+
calls.push({ key: thenable.key, params: {} });
|
|
761
|
+
launched.push(thenable);
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
const { key, ...params } = item;
|
|
765
|
+
validateRunCall(key, params, "runs.all item " + index, fingerprints);
|
|
766
|
+
calls.push({ key, params });
|
|
767
|
+
launched.push(null);
|
|
768
|
+
}
|
|
769
|
+
runFingerprints = fingerprints;
|
|
770
|
+
return { calls, launched: launched.map((entry, index) => entry ?? runHostCall(calls[index].key, calls[index].params, false, undefined)) };
|
|
771
|
+
}
|
|
772
|
+
|
|
716
773
|
const runs = Object.freeze({
|
|
717
774
|
run(key, params) {
|
|
718
775
|
validateRunCall(key, params, "runs.run", runFingerprints);
|
|
@@ -721,7 +778,7 @@ const runs = Object.freeze({
|
|
|
721
778
|
},
|
|
722
779
|
all(items) {
|
|
723
780
|
const { calls, launched } = launchRunsAll(items);
|
|
724
|
-
return trackRunObservation(launched.map(({ key, callId }) => ({ key, operation: "run", callId })),
|
|
781
|
+
return trackRunObservation(launched.map(({ key, callId }) => ({ key, operation: "run", callId })), trackPromiseCombinator(launched.map(({ promise }) => promise), (values) => Promise.all(values).then((results) => wrapRunsAllResults(results.map(decorateWorkflowChildResult), calls.map(({ key }) => key)))));
|
|
725
782
|
},
|
|
726
783
|
lanes(laneSpecs) {
|
|
727
784
|
return runLanes(laneSpecs);
|
|
@@ -1098,8 +1155,22 @@ export class WorkflowScriptError extends Error {
|
|
|
1098
1155
|
}
|
|
1099
1156
|
}
|
|
1100
1157
|
|
|
1158
|
+
export type WorkflowChildSettledOutcome = "completed" | "failed" | "paused" | "stopped";
|
|
1159
|
+
|
|
1160
|
+
export interface WorkflowChildSettledNotification {
|
|
1161
|
+
workflowRunId: string;
|
|
1162
|
+
childKey: string;
|
|
1163
|
+
childRunId?: string;
|
|
1164
|
+
outcome: WorkflowChildSettledOutcome;
|
|
1165
|
+
outputReference?: string;
|
|
1166
|
+
error?: string;
|
|
1167
|
+
workflowRunning: boolean;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1101
1170
|
export interface RunWorkflowScriptOptions {
|
|
1102
1171
|
script: string;
|
|
1172
|
+
/** Workflow run ID for notifications. Required when onChildSettled is provided. */
|
|
1173
|
+
workflowRunId?: string;
|
|
1103
1174
|
/** Host-only first-slice admission context. It is never sent to the workflow worker. */
|
|
1104
1175
|
oneUsePermit?: { claim: (key: string) => string | undefined };
|
|
1105
1176
|
timeoutMs?: number;
|
|
@@ -1108,13 +1179,13 @@ export interface RunWorkflowScriptOptions {
|
|
|
1108
1179
|
continueAfterAbortWhenChildrenSettled?: (abortError: Error) => boolean;
|
|
1109
1180
|
/** Maximum children executing concurrently within this workflow. Defaults to 20. */
|
|
1110
1181
|
globalConcurrencyLimit?: number;
|
|
1111
|
-
admit?: (calls: Array<{ key: string; params: Record<string, unknown> }
|
|
1182
|
+
admit?: (calls: Array<{ key: string; params: Record<string, unknown> }>, signal: AbortSignal) => void | Promise<void>;
|
|
1112
1183
|
launch: (key: string, params: Record<string, unknown>, signal: AbortSignal, admission: { admitted: boolean; batch: boolean }) => Promise<WorkflowScriptChildResult>;
|
|
1113
|
-
resolveResume?: (reference: WorkflowReceiptResumeReference, signal: AbortSignal) => string | WorkflowResolvedResumeReference | Promise<string | WorkflowResolvedResumeReference>;
|
|
1184
|
+
resolveResume?: (reference: WorkflowReceiptResumeReference | string, signal: AbortSignal, index?: number) => string | WorkflowResolvedResumeReference | Promise<string | WorkflowResolvedResumeReference>;
|
|
1114
1185
|
status: (keyOrRunId: string, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
|
|
1115
1186
|
steer?: (key: string, message: string, options: WorkflowSteerOptions, signal: AbortSignal) => Promise<WorkflowSteerResult>;
|
|
1116
1187
|
host?: (key: string, params: WorkflowHostCommandParams, signal: AbortSignal) => Promise<WorkflowHostCommandResult>;
|
|
1117
|
-
onHostStep?: (hostStep:
|
|
1188
|
+
onHostStep?: (hostStep: HostStepNode) => void;
|
|
1118
1189
|
state?: {
|
|
1119
1190
|
get: (key: string) => unknown | Promise<unknown>;
|
|
1120
1191
|
set: (key: string, value: unknown) => void | Promise<void>;
|
|
@@ -1123,6 +1194,7 @@ export interface RunWorkflowScriptOptions {
|
|
|
1123
1194
|
onTrace?: (trace: WorkflowScriptTraceEntry[]) => void;
|
|
1124
1195
|
onLanePlan?: (lanes: WorkflowLanePlan[]) => void;
|
|
1125
1196
|
onEmit?: (emits: unknown[]) => void;
|
|
1197
|
+
onChildSettled?: (notification: WorkflowChildSettledNotification) => void;
|
|
1126
1198
|
}
|
|
1127
1199
|
|
|
1128
1200
|
function combinedAbortSignal(signals: AbortSignal[]): AbortSignal {
|
|
@@ -1425,7 +1497,7 @@ function literalString(node: unknown): string | undefined {
|
|
|
1425
1497
|
return undefined;
|
|
1426
1498
|
}
|
|
1427
1499
|
|
|
1428
|
-
function directRunsCall(node: unknown, method: "run" | "all" | "host"):
|
|
1500
|
+
function directRunsCall(node: unknown, method: "run" | "all" | "host" | "lanes"): boolean {
|
|
1429
1501
|
if (!astNode(node) || node.type !== "CallExpression" || !astNode(node.callee) || node.callee.type !== "MemberExpression") return false;
|
|
1430
1502
|
const property = node.callee.computed === true ? literalString(node.callee.property) : astNode(node.callee.property) && node.callee.property.type === "Identifier" ? node.callee.property.name : undefined;
|
|
1431
1503
|
return property === method && astNode(node.callee.object) && node.callee.object.type === "Identifier" && node.callee.object.name === "runs";
|
|
@@ -1502,7 +1574,7 @@ function definitelyNonJson(node: AstNode, normalizeUndefined = false): string |
|
|
|
1502
1574
|
if (node.type === "ObjectExpression" && Array.isArray(node.properties)) {
|
|
1503
1575
|
const values = new Map<string, AstNode>();
|
|
1504
1576
|
for (const property of node.properties) {
|
|
1505
|
-
if (!astNode(property) || property.type !== "Property" || !astNode(property.value)) return undefined;
|
|
1577
|
+
if (!astNode(property) || property.type !== "Property" || !astNode(property.value) || property.kind !== "init") return undefined;
|
|
1506
1578
|
const key = staticPropertyKey(property);
|
|
1507
1579
|
if (key === undefined) return undefined;
|
|
1508
1580
|
values.set(key, property.value);
|
|
@@ -1531,6 +1603,26 @@ function directObjectPropertyValue(node: AstNode, name: string): AstNode | undef
|
|
|
1531
1603
|
return value;
|
|
1532
1604
|
}
|
|
1533
1605
|
|
|
1606
|
+
function validateStaticBaseRef(params: AstNode, owner: string): WorkflowScriptValidationError[] {
|
|
1607
|
+
if (params.type !== "ObjectExpression" || !Array.isArray(params.properties)) return [];
|
|
1608
|
+
// Inspect the final definition only. A later spread or unknown key may overwrite it.
|
|
1609
|
+
for (let index = params.properties.length - 1; index >= 0; index--) {
|
|
1610
|
+
const property = params.properties[index];
|
|
1611
|
+
if (!astNode(property) || property.type !== "Property") return [];
|
|
1612
|
+
const key = staticPropertyKey(property);
|
|
1613
|
+
if (key === undefined) return [];
|
|
1614
|
+
if (key !== "baseRef") continue;
|
|
1615
|
+
if (property.kind !== "init" || !astNode(property.value)) return [];
|
|
1616
|
+
const valueNode = property.value;
|
|
1617
|
+
const value = literalString(valueNode);
|
|
1618
|
+
if ((value !== undefined || valueNode.type === "Literal") && !validGitRef(value)) {
|
|
1619
|
+
return [{ message: `${owner} ${BASE_REF_VALIDATION_ERROR}`, ...nodeLocation(valueNode) }];
|
|
1620
|
+
}
|
|
1621
|
+
return [];
|
|
1622
|
+
}
|
|
1623
|
+
return [];
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1534
1626
|
function directRunsAllKeys(call: AstNode): Array<{ key: string; node: AstNode }> {
|
|
1535
1627
|
const args = Array.isArray(call.arguments) ? call.arguments : [];
|
|
1536
1628
|
const items = astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements) ? args[0].elements : [];
|
|
@@ -1542,8 +1634,121 @@ function directRunsAllKeys(call: AstNode): Array<{ key: string; node: AstNode }>
|
|
|
1542
1634
|
});
|
|
1543
1635
|
}
|
|
1544
1636
|
|
|
1637
|
+
function containsWorkflowLaunch(node: unknown): boolean {
|
|
1638
|
+
let found = false;
|
|
1639
|
+
walkAst(node, (candidate) => {
|
|
1640
|
+
if (directRunsCall(candidate, "run") || directRunsCall(candidate, "all") || directRunsCall(candidate, "lanes")) found = true;
|
|
1641
|
+
});
|
|
1642
|
+
return found;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function containsRunsIdentifier(node: unknown): boolean {
|
|
1646
|
+
let found = false;
|
|
1647
|
+
walkAst(node, (candidate) => {
|
|
1648
|
+
if (candidate.type === "Identifier" && candidate.name === "runs") found = true;
|
|
1649
|
+
});
|
|
1650
|
+
return found;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
function invalidatesRunsBinding(workflowBody: AstNode): boolean {
|
|
1654
|
+
let invalidated = false;
|
|
1655
|
+
walkAst(workflowBody, (node) => {
|
|
1656
|
+
if ((node.type === "VariableDeclarator" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ClassDeclaration" || node.type === "ClassExpression")
|
|
1657
|
+
&& containsRunsIdentifier(node.id)) invalidated = true;
|
|
1658
|
+
if ((node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") && Array.isArray(node.params)
|
|
1659
|
+
&& node.params.some(containsRunsIdentifier)) invalidated = true;
|
|
1660
|
+
if (node.type === "CatchClause" && containsRunsIdentifier(node.param)) invalidated = true;
|
|
1661
|
+
if ((node.type === "AssignmentExpression" || node.type === "UpdateExpression") && containsRunsIdentifier(node.left ?? node.argument)) invalidated = true;
|
|
1662
|
+
});
|
|
1663
|
+
return invalidated;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
function containsAbruptControl(node: AstNode): boolean {
|
|
1667
|
+
let found = false;
|
|
1668
|
+
walkAst(node, (candidate) => {
|
|
1669
|
+
if (candidate !== node && (candidate.type === "ReturnStatement" || candidate.type === "ThrowStatement" || candidate.type === "BreakStatement" || candidate.type === "ContinueStatement")) found = true;
|
|
1670
|
+
}, false);
|
|
1671
|
+
return found;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
function directLaunchCall(expression: unknown): AstNode | undefined {
|
|
1675
|
+
if (!astNode(expression)) return undefined;
|
|
1676
|
+
const candidate = expression.type === "AwaitExpression" && astNode(expression.argument) ? expression.argument : expression;
|
|
1677
|
+
return directRunsCall(candidate, "run") || directRunsCall(candidate, "all") ? candidate : undefined;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
function staticWorkflowLaunchPlan(workflowBody: AstNode): { keys: string[]; dynamic: boolean } {
|
|
1681
|
+
if (workflowBody.type !== "BlockStatement" || !Array.isArray(workflowBody.body) || invalidatesRunsBinding(workflowBody)) {
|
|
1682
|
+
return { keys: [], dynamic: containsWorkflowLaunch(workflowBody) };
|
|
1683
|
+
}
|
|
1684
|
+
const keys = new Set<string>();
|
|
1685
|
+
let dynamic = false;
|
|
1686
|
+
let remainderUncertain = false;
|
|
1687
|
+
const addCall = (call: AstNode): void => {
|
|
1688
|
+
const args = Array.isArray(call.arguments) ? call.arguments : [];
|
|
1689
|
+
let nestedLaunch = false;
|
|
1690
|
+
for (const argument of args) walkAst(argument, (node) => {
|
|
1691
|
+
if (node !== call && (directRunsCall(node, "run") || directRunsCall(node, "all") || directRunsCall(node, "lanes"))) nestedLaunch = true;
|
|
1692
|
+
});
|
|
1693
|
+
if (nestedLaunch) {
|
|
1694
|
+
dynamic = true;
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
if (directRunsCall(call, "run")) {
|
|
1698
|
+
const key = literalString(args[0]);
|
|
1699
|
+
if (key === undefined) dynamic = true;
|
|
1700
|
+
else keys.add(key);
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
const array = astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements) ? args[0] : undefined;
|
|
1704
|
+
if (!array) {
|
|
1705
|
+
dynamic = true;
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
const batchKeys: string[] = [];
|
|
1709
|
+
for (const item of array.elements as unknown[]) {
|
|
1710
|
+
if (!astNode(item) || item.type !== "ObjectExpression" || !Array.isArray(item.properties)
|
|
1711
|
+
|| item.properties.some((property) => !astNode(property) || property.type !== "Property" || staticPropertyKey(property) === undefined)) {
|
|
1712
|
+
dynamic = true;
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
const key = literalString(directObjectPropertyValue(item, "key"));
|
|
1716
|
+
if (key === undefined) {
|
|
1717
|
+
dynamic = true;
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
batchKeys.push(key);
|
|
1721
|
+
}
|
|
1722
|
+
for (const key of batchKeys) keys.add(key);
|
|
1723
|
+
};
|
|
1724
|
+
for (const statement of workflowBody.body) {
|
|
1725
|
+
if (!astNode(statement)) continue;
|
|
1726
|
+
if (remainderUncertain) {
|
|
1727
|
+
if (containsWorkflowLaunch(statement)) dynamic = true;
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
const expressions: unknown[] = [];
|
|
1731
|
+
if (statement.type === "VariableDeclaration" && Array.isArray(statement.declarations)) {
|
|
1732
|
+
for (const declaration of statement.declarations) if (astNode(declaration)) expressions.push(declaration.init);
|
|
1733
|
+
} else if (statement.type === "ExpressionStatement") expressions.push(statement.expression);
|
|
1734
|
+
else if (statement.type === "ReturnStatement" || statement.type === "ThrowStatement") expressions.push(statement.argument);
|
|
1735
|
+
let handledLaunch = false;
|
|
1736
|
+
for (const expression of expressions) {
|
|
1737
|
+
const call = directLaunchCall(expression);
|
|
1738
|
+
if (call) {
|
|
1739
|
+
addCall(call);
|
|
1740
|
+
handledLaunch = true;
|
|
1741
|
+
} else if (containsWorkflowLaunch(expression)) dynamic = true;
|
|
1742
|
+
}
|
|
1743
|
+
if (!handledLaunch && expressions.length === 0 && containsWorkflowLaunch(statement)) dynamic = true;
|
|
1744
|
+
if (statement.type === "ReturnStatement" || statement.type === "ThrowStatement") remainderUncertain = true;
|
|
1745
|
+
else if (containsAbruptControl(statement)) remainderUncertain = true;
|
|
1746
|
+
}
|
|
1747
|
+
return { keys: [...keys], dynamic };
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1545
1750
|
/** Parse a workflowScript and apply only rules that are decidable from its local syntax. */
|
|
1546
|
-
export function validateWorkflowScript(script: string): WorkflowScriptValidationResult {
|
|
1751
|
+
export function validateWorkflowScript(script: string, options: WorkflowScriptValidationOptions = {}): WorkflowScriptValidationResult {
|
|
1547
1752
|
const errors: WorkflowScriptValidationError[] = [];
|
|
1548
1753
|
if (!script.trim()) return { ok: false, errors: [{ message: "workflowScript must not be empty." }] };
|
|
1549
1754
|
let root: AstNode;
|
|
@@ -1572,6 +1777,7 @@ export function validateWorkflowScript(script: string): WorkflowScriptValidation
|
|
|
1572
1777
|
const key = literalString(keyNode);
|
|
1573
1778
|
if (keyNode && key !== undefined && !KEY_PATTERN.test(key)) errors.push({ message: "runs.run key must be 1-128 characters using letters, numbers, '.', '_' or '-', and start with a letter or number.", ...nodeLocation(keyNode) });
|
|
1574
1779
|
if (astNode(args[1])) {
|
|
1780
|
+
errors.push(...validateStaticBaseRef(args[1], "runs.run"));
|
|
1575
1781
|
const message = definitelyNonJson(args[1]);
|
|
1576
1782
|
if (message) errors.push({ message: `runs.run params are invalid: ${message}.`, ...nodeLocation(args[1]) });
|
|
1577
1783
|
}
|
|
@@ -1581,6 +1787,7 @@ export function validateWorkflowScript(script: string): WorkflowScriptValidation
|
|
|
1581
1787
|
const args = Array.isArray(node.arguments) ? node.arguments : [];
|
|
1582
1788
|
if (astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements)) {
|
|
1583
1789
|
for (const item of args[0].elements) if (astNode(item)) {
|
|
1790
|
+
errors.push(...validateStaticBaseRef(item, "runs.all item"));
|
|
1584
1791
|
const message = definitelyNonJson(item);
|
|
1585
1792
|
if (message) errors.push({ message: `runs.all item params are invalid: ${message}.`, ...nodeLocation(item) });
|
|
1586
1793
|
}
|
|
@@ -1608,7 +1815,7 @@ export function validateWorkflowScript(script: string): WorkflowScriptValidation
|
|
|
1608
1815
|
const statement = workflowBody.body[statementIndex];
|
|
1609
1816
|
if (!astNode(statement) || statement.type !== "VariableDeclaration" || !Array.isArray(statement.declarations)) continue;
|
|
1610
1817
|
for (const declaration of statement.declarations) {
|
|
1611
|
-
if (!astNode(declaration) || !astNode(declaration.id) || declaration.id.type !== "Identifier" || !astNode(declaration.init) || declaration.init.type !== "AwaitExpression" || !directRunsCall(declaration.init.argument, "all")) continue;
|
|
1818
|
+
if (!astNode(declaration) || !astNode(declaration.id) || declaration.id.type !== "Identifier" || !astNode(declaration.init) || declaration.init.type !== "AwaitExpression" || !astNode(declaration.init.argument) || !directRunsCall(declaration.init.argument, "all")) continue;
|
|
1612
1819
|
const name = declaration.id.name as string;
|
|
1613
1820
|
const keys = new Set(directRunsAllKeys(declaration.init.argument).map((entry) => entry.key));
|
|
1614
1821
|
if (keys.size === 0) continue;
|
|
@@ -1624,8 +1831,24 @@ export function validateWorkflowScript(script: string): WorkflowScriptValidation
|
|
|
1624
1831
|
}
|
|
1625
1832
|
}
|
|
1626
1833
|
|
|
1834
|
+
const warnings: WorkflowScriptValidationWarning[] = [];
|
|
1835
|
+
if (options.maxSubagentSpawnsPerRun !== undefined) {
|
|
1836
|
+
const plan = staticWorkflowLaunchPlan(workflowBody);
|
|
1837
|
+
if (plan.keys.length > options.maxSubagentSpawnsPerRun) {
|
|
1838
|
+
errors.push({
|
|
1839
|
+
kind: "spawn-budget",
|
|
1840
|
+
message: `workflowScript statically requires child launches ${plan.keys.map((key) => `'${key}'`).join(", ")}; minimum required: ${plan.keys.length}; configured: ${options.maxSubagentSpawnsPerRun}.`,
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
if (plan.dynamic) {
|
|
1844
|
+
warnings.push({
|
|
1845
|
+
kind: "dynamic-spawn-count",
|
|
1846
|
+
message: `workflowScript contains dynamic child launches; static validation proved ${plan.keys.length} launch(es), so runtime fan-out enforcement remains authoritative for the configured budget of ${options.maxSubagentSpawnsPerRun}.`,
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1627
1850
|
const unique = errors.filter((error, index) => errors.findIndex((candidate) => candidate.message === error.message && candidate.line === error.line && candidate.column === error.column) === index);
|
|
1628
|
-
return { ok: unique.length === 0, errors: unique };
|
|
1851
|
+
return { ok: unique.length === 0, errors: unique, ...(warnings.length > 0 ? { warnings } : {}) };
|
|
1629
1852
|
}
|
|
1630
1853
|
function workflowStringMetadata(params: Record<string, unknown>): Pick<WorkflowScriptTraceEntry, "phase" | "label" | "agent"> {
|
|
1631
1854
|
return {
|
|
@@ -1751,7 +1974,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1751
1974
|
console.error("Workflow onLanePlan callback failed:", error);
|
|
1752
1975
|
}
|
|
1753
1976
|
};
|
|
1754
|
-
const hostStepChanged = (hostStep:
|
|
1977
|
+
const hostStepChanged = (hostStep: HostStepNode) => {
|
|
1755
1978
|
try {
|
|
1756
1979
|
options.onHostStep?.(hostStep);
|
|
1757
1980
|
} catch (error) {
|
|
@@ -1789,6 +2012,30 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1789
2012
|
traceChanged();
|
|
1790
2013
|
return true;
|
|
1791
2014
|
};
|
|
2015
|
+
const notifyChildSettled = (key: string, result: WorkflowScriptChildResult): void => {
|
|
2016
|
+
if (!options.onChildSettled || !options.workflowRunId) return;
|
|
2017
|
+
const outcome: WorkflowChildSettledOutcome = result.ok
|
|
2018
|
+
? "completed"
|
|
2019
|
+
: result.stopped
|
|
2020
|
+
? "stopped"
|
|
2021
|
+
: result.detached
|
|
2022
|
+
? "paused"
|
|
2023
|
+
: "failed";
|
|
2024
|
+
const outputReference = result.outputReference ?? result.artifactPaths[0];
|
|
2025
|
+
try {
|
|
2026
|
+
options.onChildSettled({
|
|
2027
|
+
workflowRunId: options.workflowRunId,
|
|
2028
|
+
childKey: key,
|
|
2029
|
+
...(result.runId ? { childRunId: result.runId } : {}),
|
|
2030
|
+
outcome,
|
|
2031
|
+
...(outputReference ? { outputReference } : {}),
|
|
2032
|
+
...(!result.ok && result.error ? { error: result.error } : {}),
|
|
2033
|
+
workflowRunning: !settled && !finishing,
|
|
2034
|
+
});
|
|
2035
|
+
} catch (error) {
|
|
2036
|
+
console.error("Workflow onChildSettled callback failed:", error);
|
|
2037
|
+
}
|
|
2038
|
+
};
|
|
1792
2039
|
options.registerStopChild?.(stopChild);
|
|
1793
2040
|
|
|
1794
2041
|
return await new Promise<WorkflowScriptResult>((resolve, reject) => {
|
|
@@ -2047,7 +2294,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2047
2294
|
if (!options.host) return respond(Promise.reject(new Error("runs.host is unavailable in this host context.")));
|
|
2048
2295
|
if (hostCalls.size >= HOST_STEP_MAX_COUNT) return respond(Promise.reject(new Error(`workflowScript supports at most ${HOST_STEP_MAX_COUNT} runs.host calls.`)));
|
|
2049
2296
|
const startedAt = Date.now();
|
|
2050
|
-
const startedStep:
|
|
2297
|
+
const startedStep: HostStepNode = {
|
|
2051
2298
|
version: 1,
|
|
2052
2299
|
kind: "host-step",
|
|
2053
2300
|
monitorKind: "command",
|
|
@@ -2130,7 +2377,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2130
2377
|
return respond(Promise.reject(new Error(`runs.run('${key}') worktree must be true or false.`)));
|
|
2131
2378
|
}
|
|
2132
2379
|
if (params.baseRef !== undefined && (typeof params.baseRef !== "string" || !validGitRef(params.baseRef))) {
|
|
2133
|
-
return respond(Promise.reject(new Error(`runs.run('${key}')
|
|
2380
|
+
return respond(Promise.reject(new Error(`runs.run('${key}') ${BASE_REF_VALIDATION_ERROR}`)));
|
|
2134
2381
|
}
|
|
2135
2382
|
if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) {
|
|
2136
2383
|
return respond(Promise.reject(new Error(`runs.run('${key}') gate must be a non-empty command string.`)));
|
|
@@ -2169,7 +2416,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2169
2416
|
admission = Promise.resolve().then(() => {
|
|
2170
2417
|
if (settled || finishing) return;
|
|
2171
2418
|
for (const call of calls) assertRecoveryBarrierAllowsRun(call.key, call.params);
|
|
2172
|
-
return options.admit?.(calls);
|
|
2419
|
+
return options.admit?.(calls, childController.signal);
|
|
2173
2420
|
});
|
|
2174
2421
|
if (batch) batchAdmissions.set(batch.id, admission);
|
|
2175
2422
|
}
|
|
@@ -2183,10 +2430,11 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2183
2430
|
const childStopController = new AbortController();
|
|
2184
2431
|
childStopControllers.set(key, childStopController);
|
|
2185
2432
|
const childSignal = combinedAbortSignal([childController.signal, childStopController.signal]);
|
|
2186
|
-
const
|
|
2433
|
+
const resumeInput = resumeReference ?? (typeof params.resume === "string" && options.resolveResume ? params.resume : undefined);
|
|
2434
|
+
const resolvedResumeValue = resumeInput
|
|
2187
2435
|
? await Promise.resolve().then(() => {
|
|
2188
2436
|
if (!options.resolveResume) throw new Error("Keyed workflow receipt resume is unavailable in this host.");
|
|
2189
|
-
return options.resolveResume(
|
|
2437
|
+
return options.resolveResume(resumeInput, childSignal, typeof params.index === "number" ? params.index : undefined);
|
|
2190
2438
|
})
|
|
2191
2439
|
: undefined;
|
|
2192
2440
|
const resolvedResume = typeof resolvedResumeValue === "string"
|
|
@@ -2202,6 +2450,10 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2202
2450
|
: [];
|
|
2203
2451
|
resolvedResumeLineage = [...new Set(lineage.length ? lineage : [resolvedResumeId!])];
|
|
2204
2452
|
if (resolvedResumeLineage.at(-1) !== resolvedResumeId) resolvedResumeLineage.push(resolvedResumeId!);
|
|
2453
|
+
if (typeof resumeInput === "string") {
|
|
2454
|
+
const predecessor = [...children.values()].find((child) => child.runId === resolvedResumeId);
|
|
2455
|
+
if (predecessor?.continuation && predecessor.continuation.runIds.at(-1) === resolvedResumeId) resolvedResumeLineage = predecessor.continuation.runIds;
|
|
2456
|
+
}
|
|
2205
2457
|
}
|
|
2206
2458
|
const launchParams = resolvedResumeId ? { ...params, resume: resolvedResumeId } : params;
|
|
2207
2459
|
await launchSemaphore.acquire();
|
|
@@ -2221,6 +2473,10 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2221
2473
|
} finally {
|
|
2222
2474
|
launchSemaphore.release();
|
|
2223
2475
|
}
|
|
2476
|
+
}, (error: unknown) => {
|
|
2477
|
+
if (!childController.signal.aborted) throw error;
|
|
2478
|
+
const reason = childController.signal.reason;
|
|
2479
|
+
return stoppedChildResult(key, reason instanceof Error ? reason.message : typeof reason === "string" ? reason : "Workflow script aborted.");
|
|
2224
2480
|
}).then((result) => {
|
|
2225
2481
|
let normalized = !result.ok && !result.error ? { ...result, error: result.output } : result;
|
|
2226
2482
|
if (resolvedResumeLineage?.length && normalized.runId) {
|
|
@@ -2233,6 +2489,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2233
2489
|
const state = normalized.ok ? "completed" : normalized.stopped ? "stopped" : normalized.detached ? "detached" : "failed";
|
|
2234
2490
|
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 } : {}) });
|
|
2235
2491
|
traceChanged();
|
|
2492
|
+
notifyChildSettled(key, normalized);
|
|
2236
2493
|
return normalized;
|
|
2237
2494
|
}, (error: unknown) => {
|
|
2238
2495
|
const text = error instanceof Error ? error.message : String(error);
|
|
@@ -2242,6 +2499,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2242
2499
|
children.set(key, failure);
|
|
2243
2500
|
trace.push({ operation: "run", key, state: "failed", durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), ...(generatedLaneKey ? { generatedLaneKey } : {}), error: text });
|
|
2244
2501
|
traceChanged();
|
|
2502
|
+
notifyChildSettled(key, failure);
|
|
2245
2503
|
return failure;
|
|
2246
2504
|
});
|
|
2247
2505
|
launches.set(key, { fingerprint, promise, observed: callObserved, ...(generatedLaneKey ? { generatedLaneKey } : {}) });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { AsyncJobStep,
|
|
1
|
+
import type { AsyncJobStep, HostStepNode, WorkflowGraphNode, WorkflowGraphSnapshot, WorkflowPreflightLane, WorkflowPreflight } from "../shared/types.ts";
|
|
2
2
|
import { sanitizeDisplayText } from "../shared/display-text.ts";
|
|
3
|
-
import { workflowPreflightLaneForRuntimeKey } from "./workflow-preflight.ts";
|
|
3
|
+
import { workflowPreflightLaneForRuntimeKey as laneFor } from "./workflow-preflight.ts";
|
|
4
4
|
|
|
5
5
|
export type WorkflowChecklistState = "complete" | "running" | "queued" | "blocked" | "failed" | "paused" | "stopped";
|
|
6
6
|
|
|
@@ -62,12 +62,12 @@ export interface WorkflowChecklistItem {
|
|
|
62
62
|
toolCount?: number;
|
|
63
63
|
outputName?: string;
|
|
64
64
|
error?: string;
|
|
65
|
-
preflight?:
|
|
65
|
+
preflight?: WorkflowPreflightLane;
|
|
66
66
|
kind?: "child" | "host";
|
|
67
|
-
monitorKind?:
|
|
67
|
+
monitorKind?: HostStepNode["monitorKind"];
|
|
68
68
|
provider?: string;
|
|
69
69
|
role?: string;
|
|
70
|
-
verdict?:
|
|
70
|
+
verdict?: HostStepNode["verdict"];
|
|
71
71
|
target?: string;
|
|
72
72
|
reasonCode?: string;
|
|
73
73
|
stale?: boolean;
|
|
@@ -106,8 +106,8 @@ export interface WorkflowChecklistProjection {
|
|
|
106
106
|
export interface WorkflowChecklistInput {
|
|
107
107
|
graph?: WorkflowGraphSnapshot;
|
|
108
108
|
steps?: readonly WorkflowChecklistStep[] | readonly AsyncJobStep[];
|
|
109
|
-
hostSteps?: readonly
|
|
110
|
-
preflight?:
|
|
109
|
+
hostSteps?: readonly HostStepNode[];
|
|
110
|
+
preflight?: WorkflowPreflight;
|
|
111
111
|
trace?: readonly WorkflowChecklistTraceEntry[];
|
|
112
112
|
now?: number;
|
|
113
113
|
}
|
|
@@ -186,19 +186,15 @@ function duration(step: Pick<WorkflowChecklistStep, "durationMs" | "startedAt" |
|
|
|
186
186
|
const startedAt = finite(step.startedAt);
|
|
187
187
|
if (explicit !== undefined) return Math.max(0, explicit);
|
|
188
188
|
if (startedAt === undefined) return undefined;
|
|
189
|
-
const end = state === "running" ? now : finite(step.endedAt)
|
|
189
|
+
const end = state === "running" ? now : finite(step.endedAt);
|
|
190
190
|
return end === undefined ? undefined : Math.max(0, end - startedAt);
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
-
function laneFor(preflight: WorkflowPreflightV1 | undefined, key: string, preferredKeys: readonly (string | undefined)[] = []): WorkflowPreflightLaneV1 | undefined {
|
|
194
|
-
return workflowPreflightLaneForRuntimeKey(preflight, key, preferredKeys);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
193
|
function stepKey(step: WorkflowChecklistStep): string | undefined {
|
|
198
194
|
return step.key ?? step.workflowKey ?? step.runId;
|
|
199
195
|
}
|
|
200
196
|
|
|
201
|
-
function stepItem(step: WorkflowChecklistStep, index: number, phase: string, key = stepKey(step) ?? `step-${index + 1}`, label = step.label ?? step.description ?? stepKey(step) ?? step.agent ?? key, preflight?:
|
|
197
|
+
function stepItem(step: WorkflowChecklistStep, index: number, phase: string, key = stepKey(step) ?? `step-${index + 1}`, label = step.label ?? step.description ?? stepKey(step) ?? step.agent ?? key, preflight?: WorkflowPreflightLane): WorkflowChecklistItem {
|
|
202
198
|
const state = checklistState(step);
|
|
203
199
|
return {
|
|
204
200
|
key: keyText(key, `step-${index + 1}`),
|
|
@@ -219,7 +215,7 @@ function stepItem(step: WorkflowChecklistStep, index: number, phase: string, key
|
|
|
219
215
|
};
|
|
220
216
|
}
|
|
221
217
|
|
|
222
|
-
function hostItem(host:
|
|
218
|
+
function hostItem(host: HostStepNode, phase: string, key = host.id): WorkflowChecklistItem {
|
|
223
219
|
const state = checklistState({ status: host.state, verdict: host.verdict, stale: host.freshness?.stale });
|
|
224
220
|
return {
|
|
225
221
|
key: keyText(key, "host-step"),
|
|
@@ -264,7 +260,7 @@ function traceSources(trace: readonly WorkflowChecklistTraceEntry[] | undefined)
|
|
|
264
260
|
return [...latest.values()];
|
|
265
261
|
}
|
|
266
262
|
|
|
267
|
-
function traceItem(entry: WorkflowChecklistTraceEntry, index: number, preflight:
|
|
263
|
+
function traceItem(entry: WorkflowChecklistTraceEntry, index: number, preflight: WorkflowPreflightLane | undefined): WorkflowChecklistItem {
|
|
268
264
|
const phase = keyText(preflight?.key ?? entry.generatedLaneKey ?? entry.phase, "Workflow");
|
|
269
265
|
const item = stepItem({ key: entry.key, label: entry.label, phase, agent: entry.agent, status: entry.state === "started" ? "running" : entry.state, durationMs: entry.durationMs, error: entry.error }, index, phase, entry.key, entry.label ?? entry.key, preflight);
|
|
270
266
|
if (entry.operation === "host") item.kind = "host";
|
|
@@ -284,7 +280,7 @@ function add(phases: Map<string, WorkflowChecklistPhase>, phase: string, item: W
|
|
|
284
280
|
phaseFor(phases, phase).items.push(item);
|
|
285
281
|
}
|
|
286
282
|
|
|
287
|
-
function mergeNodeStep(node: WorkflowGraphNode, step: WorkflowChecklistStep, phase: string, trace: WorkflowChecklistTraceEntry | undefined, preflight:
|
|
283
|
+
function mergeNodeStep(node: WorkflowGraphNode, step: WorkflowChecklistStep, phase: string, trace: WorkflowChecklistTraceEntry | undefined, preflight: WorkflowPreflightLane | undefined): WorkflowChecklistItem {
|
|
288
284
|
const state = checklistState(step);
|
|
289
285
|
const nodeState = checklistState({ status: node.status, acceptance: node.acceptanceStatus ? { status: node.acceptanceStatus } : undefined });
|
|
290
286
|
const status = TERMINAL_STATES.has(nodeState) && !TERMINAL_STATES.has(state)
|
|
@@ -300,7 +296,7 @@ function priority(item: WorkflowChecklistItem): number {
|
|
|
300
296
|
}
|
|
301
297
|
|
|
302
298
|
function applyNow(item: WorkflowChecklistItem, now: number | undefined): WorkflowChecklistItem {
|
|
303
|
-
return item.
|
|
299
|
+
return item.state === "running" && item.startedAt !== undefined && now !== undefined ? { ...item, durationMs: Math.max(0, now - item.startedAt) } : item;
|
|
304
300
|
}
|
|
305
301
|
|
|
306
302
|
function finalize(phase: WorkflowChecklistPhase): void {
|