pi-subagents 0.66.0 → 0.68.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 +138 -0
- package/README.md +5 -4
- package/agents/evidence-auditor.md +34 -0
- package/agents/reviewer.md +3 -2
- package/docs/agents.md +43 -15
- package/docs/configuration.md +67 -23
- package/docs/extension-api.md +38 -19
- package/docs/missions.md +10 -2
- package/docs/models.md +11 -79
- package/docs/observability.md +22 -12
- package/docs/standalone-background.md +59 -0
- package/docs/tool-reference.md +33 -20
- package/docs/watchdog.md +39 -10
- package/docs/workflows.md +37 -13
- package/index.ts +5 -2
- package/inspector-runner.mjs +2 -2
- package/package.json +4 -2
- package/prompts/parallel-review.md +1 -1
- package/runner-peer-loader.mjs +24 -0
- package/runner-peer-preload.mjs +32 -0
- package/skills/pi-subagents/SKILL.md +32 -21
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +11 -9
- package/skills/pi-subagents/references/management-authoring-rpc.md +0 -1
- package/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +17 -13
- package/skills/pi-subagents/references/review-and-validation.md +3 -3
- package/src/agents/advertised-agent-prompt.ts +34 -3
- package/src/agents/agent-management.ts +57 -58
- package/src/agents/agent-serializer.ts +4 -3
- package/src/agents/agents.ts +190 -71
- package/src/agents/builtin-names.ts +1 -0
- package/src/agents/chain-serializer.ts +5 -0
- package/src/agents/runtime-agent-registry.ts +7 -6
- package/src/api/delegation.ts +4 -0
- package/src/api/preflight.ts +94 -59
- package/src/api/required-child-extensions.ts +6 -0
- package/src/api/shared-types.ts +2 -0
- package/src/extension/config.ts +10 -37
- package/src/extension/fanout-child.ts +66 -4
- package/src/extension/herdr-pi-bridge.ts +160 -0
- package/src/extension/index.ts +62 -39
- package/src/extension/public-execution.ts +7 -5
- package/src/extension/rpc.ts +4 -0
- package/src/extension/schemas.ts +81 -81
- package/src/extension/tool-description.ts +30 -82
- 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 +44 -31
- package/src/policy/authority.ts +4 -0
- package/src/profiles/profiles.ts +12 -6
- package/src/runs/background/active-async-capacity.ts +4 -0
- package/src/runs/background/active-run-index.ts +17 -1
- package/src/runs/background/async-execution.ts +348 -176
- package/src/runs/background/async-job-tracker.ts +8 -6
- package/src/runs/background/async-resume.ts +17 -12
- package/src/runs/background/async-status.ts +15 -4
- package/src/runs/background/auto-drain.ts +23 -10
- package/src/runs/background/binary-bootstrap.ts +38 -0
- package/src/runs/background/chain-append.ts +1 -1
- package/src/runs/background/chain-root-attachment.ts +14 -33
- package/src/runs/background/fleet-view.ts +30 -2
- package/src/runs/background/notify.ts +105 -7
- package/src/runs/background/owned-process-tree.ts +29 -2
- package/src/runs/background/result-files.ts +8 -4
- package/src/runs/background/result-watcher.ts +19 -2
- package/src/runs/background/run-child-session.ts +81 -33
- package/src/runs/background/run-status.ts +3 -0
- package/src/runs/background/runner-aliases.ts +12 -33
- package/src/runs/background/runner-child-launch.ts +6 -1
- package/src/runs/background/runner-child-sessions.ts +5 -4
- package/src/runs/background/runner-http-dispatcher.ts +119 -0
- package/src/runs/background/scheduled-runs.ts +51 -18
- package/src/runs/background/stale-run-reconciler.ts +35 -11
- package/src/runs/background/steering.ts +20 -2
- package/src/runs/background/subagent-runner.ts +441 -304
- package/src/runs/background/subagent-wait.ts +176 -28
- package/src/runs/background/wait-completions.ts +75 -27
- package/src/runs/background/wait-subscriptions.ts +9 -3
- package/src/runs/background/wait-tool.ts +5 -3
- package/src/runs/foreground/async-steering-action.ts +18 -7
- package/src/runs/foreground/async-stop-action.ts +93 -3
- package/src/runs/foreground/execution.ts +134 -248
- package/src/runs/foreground/foreground-history.ts +2 -1
- package/src/runs/foreground/prompt-audit.ts +3 -1
- package/src/runs/foreground/subagent-executor.ts +374 -178
- package/src/runs/foreground/workflow-detach-reconcile.ts +2 -0
- package/src/runs/foreground/workflow-foreground-steering.ts +2 -1
- package/src/runs/shared/acceptance.ts +38 -11
- package/src/runs/shared/async-status-projection.ts +127 -33
- package/src/runs/shared/capability-ceiling.ts +2 -0
- package/src/runs/shared/child-hooks.ts +25 -10
- package/src/runs/shared/child-launch-plan.ts +15 -3
- package/src/runs/shared/child-launch.ts +28 -5
- package/src/runs/shared/child-lifecycle.ts +6 -3
- package/src/runs/shared/child-runtime-config.ts +8 -1
- package/src/runs/shared/child-session.ts +127 -52
- package/src/runs/shared/child-tool-plan.ts +142 -11
- package/src/runs/shared/completion-guard.ts +5 -3
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/effective-system-prompt.ts +33 -0
- package/src/runs/shared/external-cli-contract.ts +11 -1
- package/src/runs/shared/external-cli-preflight.ts +6 -2
- package/src/runs/shared/external-cli-runner.ts +9 -7
- package/src/runs/shared/herdr-connection.ts +134 -0
- package/src/runs/shared/herdr-external-adapters.ts +169 -0
- package/src/runs/shared/herdr-machine.ts +279 -0
- package/src/runs/shared/herdr-pi-protocol.ts +59 -0
- package/src/runs/shared/herdr-placed-run.ts +263 -0
- package/src/runs/shared/llm-intent-arbiter.ts +12 -3
- package/src/runs/shared/model-resolution-diagnostic.ts +76 -0
- package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -235
- package/src/runs/shared/model-scope.ts +1 -1
- package/src/runs/shared/nested-events.ts +11 -2
- package/src/runs/shared/orca-progress-tabs.ts +1 -1
- package/src/runs/shared/parallel-utils.ts +7 -2
- package/src/runs/shared/pi-spawn.ts +10 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +12 -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 +27 -4
- package/src/runs/shared/worktree.ts +45 -15
- package/src/shared/child-cache-retention.ts +43 -0
- package/src/shared/fork-context.ts +15 -72
- package/src/shared/launch-contract.ts +68 -8
- package/src/shared/opencode-session-headers.ts +30 -0
- package/src/shared/pruned-fork.ts +1 -1
- package/src/shared/required-child-extensions.ts +81 -0
- package/src/shared/settings.ts +5 -2
- package/src/shared/shortcuts.ts +0 -4
- package/src/shared/types.ts +74 -30
- package/src/slash/delegation-adapters.ts +3 -1
- package/src/slash/delegation-request.ts +14 -0
- package/src/slash/slash-commands.ts +2 -7
- package/src/slash/subagents-admin.ts +24 -13
- package/src/tui/fleet-status.ts +164 -19
- package/src/tui/fleet.ts +16 -14
- package/src/tui/render.ts +168 -37
- package/src/watchdog/child-status.ts +28 -28
- package/src/watchdog/lsp-diagnostics.ts +1 -1
- package/src/watchdog/model-selection.ts +21 -1
- package/src/watchdog/permission-arbiter.ts +3 -1
- package/src/watchdog/register-child.ts +10 -2
- package/src/watchdog/register-main.ts +39 -35
- package/src/watchdog/render.ts +1 -1
- package/src/watchdog/review.ts +123 -74
- package/src/watchdog/rules.ts +1 -1
- package/src/watchdog/runtime.ts +100 -27
- package/src/watchdog/scope.ts +1 -1
- package/src/watchdog/settings.ts +3 -0
- package/src/watchdog/tool-actions.ts +13 -12
- package/src/watchdog/turn-delta.ts +23 -0
- package/src/watchdog/types.ts +5 -3
- package/src/watchdog/warning-format.ts +1 -1
- package/src/workflows/scripted-workflow.ts +279 -10
- package/src/workflows/workflow-checklist.ts +2 -2
- package/src/workflows/workflow-receipt.ts +21 -3
- package/src/workflows/workflow-resources.ts +13 -2
- package/runner-server-preload.mjs +0 -13
- package/src/runs/shared/model-exclusions.ts +0 -374
- package/src/runs/shared/readonly-model-continuation.ts +0 -69
- package/src/runs/shared/readonly-session-evidence.ts +0 -307
- /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { accessSync, constants, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { dirname, resolve as resolvePath } from "node:path";
|
|
4
4
|
import { Worker } from "node:worker_threads";
|
|
@@ -24,11 +24,22 @@ export interface WorkflowScriptValidationError {
|
|
|
24
24
|
message: string;
|
|
25
25
|
line?: number;
|
|
26
26
|
column?: number;
|
|
27
|
+
kind?: "spawn-budget";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface WorkflowScriptValidationWarning {
|
|
31
|
+
message: string;
|
|
32
|
+
kind: "dynamic-spawn-count";
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
export interface WorkflowScriptValidationResult {
|
|
30
36
|
ok: boolean;
|
|
31
37
|
errors: WorkflowScriptValidationError[];
|
|
38
|
+
warnings?: WorkflowScriptValidationWarning[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface WorkflowScriptValidationOptions {
|
|
42
|
+
maxSubagentSpawnsPerRun?: number;
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
const WORKER_SOURCE = String.raw`
|
|
@@ -539,7 +550,7 @@ function runLanes(laneSpecs) {
|
|
|
539
550
|
generatedKey: stage.generatedKey,
|
|
540
551
|
...workflowPlanStringMetadata(stage.params),
|
|
541
552
|
...(typeof stage.params.as === "string" && stage.params.as.trim() ? { outputName: stage.params.as.trim() } : {}),
|
|
542
|
-
...(stage.params.outputSchema
|
|
553
|
+
...(stage.params.outputSchema ? { structured: true } : {}),
|
|
543
554
|
})),
|
|
544
555
|
})) });
|
|
545
556
|
const firstItems = lanes.map((lane) => {
|
|
@@ -696,8 +707,26 @@ function validateHostCommand(key, params) {
|
|
|
696
707
|
hostKeys.add(key);
|
|
697
708
|
}
|
|
698
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
|
+
|
|
699
726
|
function launchRunsAll(items, generatedLaneKeys) {
|
|
700
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);
|
|
701
730
|
const fingerprints = new Map(runFingerprints);
|
|
702
731
|
const calls = [];
|
|
703
732
|
for (let index = 0; index < items.length; index++) {
|
|
@@ -714,6 +743,33 @@ function launchRunsAll(items, generatedLaneKeys) {
|
|
|
714
743
|
return { calls, launched };
|
|
715
744
|
}
|
|
716
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
|
+
|
|
717
773
|
const runs = Object.freeze({
|
|
718
774
|
run(key, params) {
|
|
719
775
|
validateRunCall(key, params, "runs.run", runFingerprints);
|
|
@@ -722,7 +778,7 @@ const runs = Object.freeze({
|
|
|
722
778
|
},
|
|
723
779
|
all(items) {
|
|
724
780
|
const { calls, launched } = launchRunsAll(items);
|
|
725
|
-
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)))));
|
|
726
782
|
},
|
|
727
783
|
lanes(laneSpecs) {
|
|
728
784
|
return runLanes(laneSpecs);
|
|
@@ -896,6 +952,12 @@ function omitUndefinedWorkflowValues(value, seen = new Set()) {
|
|
|
896
952
|
return normalized;
|
|
897
953
|
}
|
|
898
954
|
|
|
955
|
+
function deepFreezeWorkflowArgs(value) {
|
|
956
|
+
if (value === null || typeof value !== "object" || Object.isFrozen(value)) return value;
|
|
957
|
+
for (const entry of Object.values(value)) deepFreezeWorkflowArgs(entry);
|
|
958
|
+
return Object.freeze(value);
|
|
959
|
+
}
|
|
960
|
+
|
|
899
961
|
parentPort.on("message", async (message) => {
|
|
900
962
|
if (message.type === "response") {
|
|
901
963
|
const entry = pending.get(message.callId);
|
|
@@ -915,6 +977,9 @@ parentPort.on("message", async (message) => {
|
|
|
915
977
|
if (message.stateEnabled) sandbox.state = state;
|
|
916
978
|
const context = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
|
|
917
979
|
contextObjectPrototype = vm.runInContext("Object.prototype", context);
|
|
980
|
+
// Rebuild args inside the VM realm: a worker-realm object would expose the worker's unrestricted
|
|
981
|
+
// Function through args.constructor.constructor, bypassing codeGeneration.strings: false.
|
|
982
|
+
sandbox.args = deepFreezeWorkflowArgs(vm.runInContext("JSON.parse", context)(JSON.stringify(message.args ?? {})));
|
|
918
983
|
let compiled;
|
|
919
984
|
try {
|
|
920
985
|
assertPortableWorkflowScript(message.script);
|
|
@@ -1099,8 +1164,26 @@ export class WorkflowScriptError extends Error {
|
|
|
1099
1164
|
}
|
|
1100
1165
|
}
|
|
1101
1166
|
|
|
1167
|
+
export type WorkflowChildSettledOutcome = "completed" | "failed" | "paused" | "stopped";
|
|
1168
|
+
|
|
1169
|
+
export interface WorkflowChildSettledNotification {
|
|
1170
|
+
workflowRunId: string;
|
|
1171
|
+
childKey: string;
|
|
1172
|
+
childRunId?: string;
|
|
1173
|
+
outcome: WorkflowChildSettledOutcome;
|
|
1174
|
+
outputReference?: string;
|
|
1175
|
+
error?: string;
|
|
1176
|
+
workflowRunning: boolean;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1102
1179
|
export interface RunWorkflowScriptOptions {
|
|
1103
1180
|
script: string;
|
|
1181
|
+
/** Normalized raw-script input exposed as the deeply frozen sandbox global `args`. */
|
|
1182
|
+
args?: Readonly<Record<string, unknown>>;
|
|
1183
|
+
/** Parent-session cwd used to recover a stale process cwd. */
|
|
1184
|
+
processCwd?: string;
|
|
1185
|
+
/** Workflow run ID for notifications. Required when onChildSettled is provided. */
|
|
1186
|
+
workflowRunId?: string;
|
|
1104
1187
|
/** Host-only first-slice admission context. It is never sent to the workflow worker. */
|
|
1105
1188
|
oneUsePermit?: { claim: (key: string) => string | undefined };
|
|
1106
1189
|
timeoutMs?: number;
|
|
@@ -1109,7 +1192,7 @@ export interface RunWorkflowScriptOptions {
|
|
|
1109
1192
|
continueAfterAbortWhenChildrenSettled?: (abortError: Error) => boolean;
|
|
1110
1193
|
/** Maximum children executing concurrently within this workflow. Defaults to 20. */
|
|
1111
1194
|
globalConcurrencyLimit?: number;
|
|
1112
|
-
admit?: (calls: Array<{ key: string; params: Record<string, unknown> }
|
|
1195
|
+
admit?: (calls: Array<{ key: string; params: Record<string, unknown> }>, signal: AbortSignal) => void | Promise<void>;
|
|
1113
1196
|
launch: (key: string, params: Record<string, unknown>, signal: AbortSignal, admission: { admitted: boolean; batch: boolean }) => Promise<WorkflowScriptChildResult>;
|
|
1114
1197
|
resolveResume?: (reference: WorkflowReceiptResumeReference | string, signal: AbortSignal, index?: number) => string | WorkflowResolvedResumeReference | Promise<string | WorkflowResolvedResumeReference>;
|
|
1115
1198
|
status: (keyOrRunId: string, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
|
|
@@ -1124,6 +1207,7 @@ export interface RunWorkflowScriptOptions {
|
|
|
1124
1207
|
onTrace?: (trace: WorkflowScriptTraceEntry[]) => void;
|
|
1125
1208
|
onLanePlan?: (lanes: WorkflowLanePlan[]) => void;
|
|
1126
1209
|
onEmit?: (emits: unknown[]) => void;
|
|
1210
|
+
onChildSettled?: (notification: WorkflowChildSettledNotification) => void;
|
|
1127
1211
|
}
|
|
1128
1212
|
|
|
1129
1213
|
function combinedAbortSignal(signals: AbortSignal[]): AbortSignal {
|
|
@@ -1426,7 +1510,7 @@ function literalString(node: unknown): string | undefined {
|
|
|
1426
1510
|
return undefined;
|
|
1427
1511
|
}
|
|
1428
1512
|
|
|
1429
|
-
function directRunsCall(node: unknown, method: "run" | "all" | "host"):
|
|
1513
|
+
function directRunsCall(node: unknown, method: "run" | "all" | "host" | "lanes"): boolean {
|
|
1430
1514
|
if (!astNode(node) || node.type !== "CallExpression" || !astNode(node.callee) || node.callee.type !== "MemberExpression") return false;
|
|
1431
1515
|
const property = node.callee.computed === true ? literalString(node.callee.property) : astNode(node.callee.property) && node.callee.property.type === "Identifier" ? node.callee.property.name : undefined;
|
|
1432
1516
|
return property === method && astNode(node.callee.object) && node.callee.object.type === "Identifier" && node.callee.object.name === "runs";
|
|
@@ -1563,8 +1647,121 @@ function directRunsAllKeys(call: AstNode): Array<{ key: string; node: AstNode }>
|
|
|
1563
1647
|
});
|
|
1564
1648
|
}
|
|
1565
1649
|
|
|
1650
|
+
function containsWorkflowLaunch(node: unknown): boolean {
|
|
1651
|
+
let found = false;
|
|
1652
|
+
walkAst(node, (candidate) => {
|
|
1653
|
+
if (directRunsCall(candidate, "run") || directRunsCall(candidate, "all") || directRunsCall(candidate, "lanes")) found = true;
|
|
1654
|
+
});
|
|
1655
|
+
return found;
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
function containsRunsIdentifier(node: unknown): boolean {
|
|
1659
|
+
let found = false;
|
|
1660
|
+
walkAst(node, (candidate) => {
|
|
1661
|
+
if (candidate.type === "Identifier" && candidate.name === "runs") found = true;
|
|
1662
|
+
});
|
|
1663
|
+
return found;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
function invalidatesRunsBinding(workflowBody: AstNode): boolean {
|
|
1667
|
+
let invalidated = false;
|
|
1668
|
+
walkAst(workflowBody, (node) => {
|
|
1669
|
+
if ((node.type === "VariableDeclarator" || node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ClassDeclaration" || node.type === "ClassExpression")
|
|
1670
|
+
&& containsRunsIdentifier(node.id)) invalidated = true;
|
|
1671
|
+
if ((node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") && Array.isArray(node.params)
|
|
1672
|
+
&& node.params.some(containsRunsIdentifier)) invalidated = true;
|
|
1673
|
+
if (node.type === "CatchClause" && containsRunsIdentifier(node.param)) invalidated = true;
|
|
1674
|
+
if ((node.type === "AssignmentExpression" || node.type === "UpdateExpression") && containsRunsIdentifier(node.left ?? node.argument)) invalidated = true;
|
|
1675
|
+
});
|
|
1676
|
+
return invalidated;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
function containsAbruptControl(node: AstNode): boolean {
|
|
1680
|
+
let found = false;
|
|
1681
|
+
walkAst(node, (candidate) => {
|
|
1682
|
+
if (candidate !== node && (candidate.type === "ReturnStatement" || candidate.type === "ThrowStatement" || candidate.type === "BreakStatement" || candidate.type === "ContinueStatement")) found = true;
|
|
1683
|
+
}, false);
|
|
1684
|
+
return found;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
function directLaunchCall(expression: unknown): AstNode | undefined {
|
|
1688
|
+
if (!astNode(expression)) return undefined;
|
|
1689
|
+
const candidate = expression.type === "AwaitExpression" && astNode(expression.argument) ? expression.argument : expression;
|
|
1690
|
+
return directRunsCall(candidate, "run") || directRunsCall(candidate, "all") ? candidate : undefined;
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
function staticWorkflowLaunchPlan(workflowBody: AstNode): { keys: string[]; dynamic: boolean } {
|
|
1694
|
+
if (workflowBody.type !== "BlockStatement" || !Array.isArray(workflowBody.body) || invalidatesRunsBinding(workflowBody)) {
|
|
1695
|
+
return { keys: [], dynamic: containsWorkflowLaunch(workflowBody) };
|
|
1696
|
+
}
|
|
1697
|
+
const keys = new Set<string>();
|
|
1698
|
+
let dynamic = false;
|
|
1699
|
+
let remainderUncertain = false;
|
|
1700
|
+
const addCall = (call: AstNode): void => {
|
|
1701
|
+
const args = Array.isArray(call.arguments) ? call.arguments : [];
|
|
1702
|
+
let nestedLaunch = false;
|
|
1703
|
+
for (const argument of args) walkAst(argument, (node) => {
|
|
1704
|
+
if (node !== call && (directRunsCall(node, "run") || directRunsCall(node, "all") || directRunsCall(node, "lanes"))) nestedLaunch = true;
|
|
1705
|
+
});
|
|
1706
|
+
if (nestedLaunch) {
|
|
1707
|
+
dynamic = true;
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
if (directRunsCall(call, "run")) {
|
|
1711
|
+
const key = literalString(args[0]);
|
|
1712
|
+
if (key === undefined) dynamic = true;
|
|
1713
|
+
else keys.add(key);
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
const array = astNode(args[0]) && args[0].type === "ArrayExpression" && Array.isArray(args[0].elements) ? args[0] : undefined;
|
|
1717
|
+
if (!array) {
|
|
1718
|
+
dynamic = true;
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
const batchKeys: string[] = [];
|
|
1722
|
+
for (const item of array.elements as unknown[]) {
|
|
1723
|
+
if (!astNode(item) || item.type !== "ObjectExpression" || !Array.isArray(item.properties)
|
|
1724
|
+
|| item.properties.some((property) => !astNode(property) || property.type !== "Property" || staticPropertyKey(property) === undefined)) {
|
|
1725
|
+
dynamic = true;
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
const key = literalString(directObjectPropertyValue(item, "key"));
|
|
1729
|
+
if (key === undefined) {
|
|
1730
|
+
dynamic = true;
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
batchKeys.push(key);
|
|
1734
|
+
}
|
|
1735
|
+
for (const key of batchKeys) keys.add(key);
|
|
1736
|
+
};
|
|
1737
|
+
for (const statement of workflowBody.body) {
|
|
1738
|
+
if (!astNode(statement)) continue;
|
|
1739
|
+
if (remainderUncertain) {
|
|
1740
|
+
if (containsWorkflowLaunch(statement)) dynamic = true;
|
|
1741
|
+
continue;
|
|
1742
|
+
}
|
|
1743
|
+
const expressions: unknown[] = [];
|
|
1744
|
+
if (statement.type === "VariableDeclaration" && Array.isArray(statement.declarations)) {
|
|
1745
|
+
for (const declaration of statement.declarations) if (astNode(declaration)) expressions.push(declaration.init);
|
|
1746
|
+
} else if (statement.type === "ExpressionStatement") expressions.push(statement.expression);
|
|
1747
|
+
else if (statement.type === "ReturnStatement" || statement.type === "ThrowStatement") expressions.push(statement.argument);
|
|
1748
|
+
let handledLaunch = false;
|
|
1749
|
+
for (const expression of expressions) {
|
|
1750
|
+
const call = directLaunchCall(expression);
|
|
1751
|
+
if (call) {
|
|
1752
|
+
addCall(call);
|
|
1753
|
+
handledLaunch = true;
|
|
1754
|
+
} else if (containsWorkflowLaunch(expression)) dynamic = true;
|
|
1755
|
+
}
|
|
1756
|
+
if (!handledLaunch && expressions.length === 0 && containsWorkflowLaunch(statement)) dynamic = true;
|
|
1757
|
+
if (statement.type === "ReturnStatement" || statement.type === "ThrowStatement") remainderUncertain = true;
|
|
1758
|
+
else if (containsAbruptControl(statement)) remainderUncertain = true;
|
|
1759
|
+
}
|
|
1760
|
+
return { keys: [...keys], dynamic };
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1566
1763
|
/** Parse a workflowScript and apply only rules that are decidable from its local syntax. */
|
|
1567
|
-
export function validateWorkflowScript(script: string): WorkflowScriptValidationResult {
|
|
1764
|
+
export function validateWorkflowScript(script: string, options: WorkflowScriptValidationOptions = {}): WorkflowScriptValidationResult {
|
|
1568
1765
|
const errors: WorkflowScriptValidationError[] = [];
|
|
1569
1766
|
if (!script.trim()) return { ok: false, errors: [{ message: "workflowScript must not be empty." }] };
|
|
1570
1767
|
let root: AstNode;
|
|
@@ -1631,7 +1828,7 @@ export function validateWorkflowScript(script: string): WorkflowScriptValidation
|
|
|
1631
1828
|
const statement = workflowBody.body[statementIndex];
|
|
1632
1829
|
if (!astNode(statement) || statement.type !== "VariableDeclaration" || !Array.isArray(statement.declarations)) continue;
|
|
1633
1830
|
for (const declaration of statement.declarations) {
|
|
1634
|
-
if (!astNode(declaration) || !astNode(declaration.id) || declaration.id.type !== "Identifier" || !astNode(declaration.init) || declaration.init.type !== "AwaitExpression" || !directRunsCall(declaration.init.argument, "all")) continue;
|
|
1831
|
+
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;
|
|
1635
1832
|
const name = declaration.id.name as string;
|
|
1636
1833
|
const keys = new Set(directRunsAllKeys(declaration.init.argument).map((entry) => entry.key));
|
|
1637
1834
|
if (keys.size === 0) continue;
|
|
@@ -1647,8 +1844,24 @@ export function validateWorkflowScript(script: string): WorkflowScriptValidation
|
|
|
1647
1844
|
}
|
|
1648
1845
|
}
|
|
1649
1846
|
|
|
1847
|
+
const warnings: WorkflowScriptValidationWarning[] = [];
|
|
1848
|
+
if (options.maxSubagentSpawnsPerRun !== undefined) {
|
|
1849
|
+
const plan = staticWorkflowLaunchPlan(workflowBody);
|
|
1850
|
+
if (plan.keys.length > options.maxSubagentSpawnsPerRun) {
|
|
1851
|
+
errors.push({
|
|
1852
|
+
kind: "spawn-budget",
|
|
1853
|
+
message: `workflowScript statically requires child launches ${plan.keys.map((key) => `'${key}'`).join(", ")}; minimum required: ${plan.keys.length}; configured: ${options.maxSubagentSpawnsPerRun}.`,
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
if (plan.dynamic) {
|
|
1857
|
+
warnings.push({
|
|
1858
|
+
kind: "dynamic-spawn-count",
|
|
1859
|
+
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}.`,
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1650
1863
|
const unique = errors.filter((error, index) => errors.findIndex((candidate) => candidate.message === error.message && candidate.line === error.line && candidate.column === error.column) === index);
|
|
1651
|
-
return { ok: unique.length === 0, errors: unique };
|
|
1864
|
+
return { ok: unique.length === 0, errors: unique, ...(warnings.length > 0 ? { warnings } : {}) };
|
|
1652
1865
|
}
|
|
1653
1866
|
function workflowStringMetadata(params: Record<string, unknown>): Pick<WorkflowScriptTraceEntry, "phase" | "label" | "agent"> {
|
|
1654
1867
|
return {
|
|
@@ -1722,6 +1935,32 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1722
1935
|
}
|
|
1723
1936
|
const launchSemaphore = new Semaphore(options.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT);
|
|
1724
1937
|
|
|
1938
|
+
if (options.processCwd !== undefined) {
|
|
1939
|
+
let staleCwd = false;
|
|
1940
|
+
try {
|
|
1941
|
+
realpathSync(process.cwd());
|
|
1942
|
+
} catch (error) {
|
|
1943
|
+
const code = typeof error === "object" && error !== null && "code" in error ? (error as NodeJS.ErrnoException).code : undefined;
|
|
1944
|
+
if (code !== "ENOENT") throw new Error("Workflow current cwd could not be validated.", { cause: error });
|
|
1945
|
+
staleCwd = true;
|
|
1946
|
+
}
|
|
1947
|
+
try {
|
|
1948
|
+
if (staleCwd) process.chdir(options.processCwd);
|
|
1949
|
+
else {
|
|
1950
|
+
const target = realpathSync(options.processCwd);
|
|
1951
|
+
if (!statSync(target).isDirectory()) {
|
|
1952
|
+
const error = new Error(`ENOTDIR: not a directory, access '${target}'`) as NodeJS.ErrnoException;
|
|
1953
|
+
error.code = "ENOTDIR";
|
|
1954
|
+
throw error;
|
|
1955
|
+
}
|
|
1956
|
+
accessSync(target, constants.X_OK);
|
|
1957
|
+
}
|
|
1958
|
+
} catch (error) {
|
|
1959
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1960
|
+
throw new Error(`Workflow process cwd is unavailable: ${options.processCwd}: ${detail}`, { cause: error });
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1725
1964
|
let acornPath: string;
|
|
1726
1965
|
try {
|
|
1727
1966
|
acornPath = resolveWorkflowParserEntry();
|
|
@@ -1812,6 +2051,30 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1812
2051
|
traceChanged();
|
|
1813
2052
|
return true;
|
|
1814
2053
|
};
|
|
2054
|
+
const notifyChildSettled = (key: string, result: WorkflowScriptChildResult): void => {
|
|
2055
|
+
if (!options.onChildSettled || !options.workflowRunId) return;
|
|
2056
|
+
const outcome: WorkflowChildSettledOutcome = result.ok
|
|
2057
|
+
? "completed"
|
|
2058
|
+
: result.stopped
|
|
2059
|
+
? "stopped"
|
|
2060
|
+
: result.detached
|
|
2061
|
+
? "paused"
|
|
2062
|
+
: "failed";
|
|
2063
|
+
const outputReference = result.outputReference ?? result.artifactPaths[0];
|
|
2064
|
+
try {
|
|
2065
|
+
options.onChildSettled({
|
|
2066
|
+
workflowRunId: options.workflowRunId,
|
|
2067
|
+
childKey: key,
|
|
2068
|
+
...(result.runId ? { childRunId: result.runId } : {}),
|
|
2069
|
+
outcome,
|
|
2070
|
+
...(outputReference ? { outputReference } : {}),
|
|
2071
|
+
...(!result.ok && result.error ? { error: result.error } : {}),
|
|
2072
|
+
workflowRunning: !settled && !finishing,
|
|
2073
|
+
});
|
|
2074
|
+
} catch (error) {
|
|
2075
|
+
console.error("Workflow onChildSettled callback failed:", error);
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
1815
2078
|
options.registerStopChild?.(stopChild);
|
|
1816
2079
|
|
|
1817
2080
|
return await new Promise<WorkflowScriptResult>((resolve, reject) => {
|
|
@@ -2192,7 +2455,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2192
2455
|
admission = Promise.resolve().then(() => {
|
|
2193
2456
|
if (settled || finishing) return;
|
|
2194
2457
|
for (const call of calls) assertRecoveryBarrierAllowsRun(call.key, call.params);
|
|
2195
|
-
return options.admit?.(calls);
|
|
2458
|
+
return options.admit?.(calls, childController.signal);
|
|
2196
2459
|
});
|
|
2197
2460
|
if (batch) batchAdmissions.set(batch.id, admission);
|
|
2198
2461
|
}
|
|
@@ -2249,6 +2512,10 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2249
2512
|
} finally {
|
|
2250
2513
|
launchSemaphore.release();
|
|
2251
2514
|
}
|
|
2515
|
+
}, (error: unknown) => {
|
|
2516
|
+
if (!childController.signal.aborted) throw error;
|
|
2517
|
+
const reason = childController.signal.reason;
|
|
2518
|
+
return stoppedChildResult(key, reason instanceof Error ? reason.message : typeof reason === "string" ? reason : "Workflow script aborted.");
|
|
2252
2519
|
}).then((result) => {
|
|
2253
2520
|
let normalized = !result.ok && !result.error ? { ...result, error: result.output } : result;
|
|
2254
2521
|
if (resolvedResumeLineage?.length && normalized.runId) {
|
|
@@ -2261,6 +2528,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2261
2528
|
const state = normalized.ok ? "completed" : normalized.stopped ? "stopped" : normalized.detached ? "detached" : "failed";
|
|
2262
2529
|
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 } : {}) });
|
|
2263
2530
|
traceChanged();
|
|
2531
|
+
notifyChildSettled(key, normalized);
|
|
2264
2532
|
return normalized;
|
|
2265
2533
|
}, (error: unknown) => {
|
|
2266
2534
|
const text = error instanceof Error ? error.message : String(error);
|
|
@@ -2270,6 +2538,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2270
2538
|
children.set(key, failure);
|
|
2271
2539
|
trace.push({ operation: "run", key, state: "failed", durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), ...(generatedLaneKey ? { generatedLaneKey } : {}), error: text });
|
|
2272
2540
|
traceChanged();
|
|
2541
|
+
notifyChildSettled(key, failure);
|
|
2273
2542
|
return failure;
|
|
2274
2543
|
});
|
|
2275
2544
|
launches.set(key, { fingerprint, promise, observed: callObserved, ...(generatedLaneKey ? { generatedLaneKey } : {}) });
|
|
@@ -2279,6 +2548,6 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2279
2548
|
respond(deliver(promise), `runs.run('${key}') result`, (error) => children.set(key, responseBoundaryFailure(key, error)));
|
|
2280
2549
|
});
|
|
2281
2550
|
|
|
2282
|
-
worker.postMessage({ type: "start", script: options.script, stateEnabled: options.state !== undefined });
|
|
2551
|
+
worker.postMessage({ type: "start", script: options.script, ...(options.args ? { args: options.args } : {}), stateEnabled: options.state !== undefined });
|
|
2283
2552
|
});
|
|
2284
2553
|
}
|
|
@@ -186,7 +186,7 @@ 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
|
|
|
@@ -296,7 +296,7 @@ function priority(item: WorkflowChecklistItem): number {
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
function applyNow(item: WorkflowChecklistItem, now: number | undefined): WorkflowChecklistItem {
|
|
299
|
-
return item.
|
|
299
|
+
return item.state === "running" && item.startedAt !== undefined && now !== undefined ? { ...item, durationMs: Math.max(0, now - item.startedAt) } : item;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
302
|
function finalize(phase: WorkflowChecklistPhase): void {
|
|
@@ -37,6 +37,7 @@ export function buildWorkflowReceipt(input: {
|
|
|
37
37
|
workflowRunId: string;
|
|
38
38
|
state: WorkflowReceiptState;
|
|
39
39
|
children: WorkflowScriptChildResult[];
|
|
40
|
+
argsDigest?: string;
|
|
40
41
|
hostSteps?: WorkflowReceipt["hostSteps"];
|
|
41
42
|
workflowChildren?: WorkflowReceipt["workflowChildren"];
|
|
42
43
|
resource?: WorkflowResourceProvenance;
|
|
@@ -44,6 +45,7 @@ export function buildWorkflowReceipt(input: {
|
|
|
44
45
|
createdAt?: number;
|
|
45
46
|
}): WorkflowReceipt {
|
|
46
47
|
const workflowRunId = assertSafeRunId(input.workflowRunId, "workflowRunId");
|
|
48
|
+
if (input.argsDigest !== undefined && !/^[a-f0-9]{64}$/u.test(input.argsDigest)) throw new Error("workflow receipt argsDigest must be a lowercase SHA-256 digest.");
|
|
47
49
|
if (input.workflowChildren?.workflowRunId !== undefined && input.workflowChildren.workflowRunId !== workflowRunId) throw new Error("workflowChildren workflowRunId does not match its receipt.");
|
|
48
50
|
const entries: Record<string, WorkflowReceiptEntry> = Object.create(null) as Record<string, WorkflowReceiptEntry>;
|
|
49
51
|
for (const child of input.children) {
|
|
@@ -75,7 +77,7 @@ export function buildWorkflowReceipt(input: {
|
|
|
75
77
|
const hostSteps = input.hostSteps?.map((hostStep, index) => parseHostStepNode(hostStep, `workflow receipt hostSteps[${index}]`));
|
|
76
78
|
if (hostSteps) assertUniqueHostStepIds(hostSteps, "workflow receipt");
|
|
77
79
|
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 } : {}) };
|
|
80
|
+
return { version: WORKFLOW_RECEIPT_VERSION, workflowRunId, state: input.state, createdAt: input.createdAt ?? Date.now(), entries, ...(input.argsDigest ? { argsDigest: input.argsDigest } : {}), ...(resource ? { resource } : {}), ...(hostSteps?.length ? { hostSteps } : {}), ...(input.workflowChildren ? { workflowChildren: input.workflowChildren } : {}), ...(input.terminalOutcome ? { terminalOutcome: input.terminalOutcome } : {}) };
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
export function writeWorkflowReceipt(asyncDir: string, receipt: WorkflowReceipt): string {
|
|
@@ -115,12 +117,26 @@ const EXTERNAL_CLI_CAPABILITIES = {
|
|
|
115
117
|
extensionBindings: false,
|
|
116
118
|
} as const;
|
|
117
119
|
|
|
120
|
+
function parseExternalCliMachine(value: unknown, label: string): void {
|
|
121
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label}.machine must be an object.`);
|
|
122
|
+
const machine = value as Record<string, unknown>;
|
|
123
|
+
const unknownMachine = Object.keys(machine).filter((field) => !["provider", "id", "label", "target", "session", "cwd", "remoteGit"].includes(field));
|
|
124
|
+
if (unknownMachine.length > 0) throw new Error(`${label}.machine has unsupported fields: ${unknownMachine.join(", ")}.`);
|
|
125
|
+
if (machine.provider !== "herdr" || typeof machine.id !== "string" || !machine.id.trim() || typeof machine.target !== "string" || !machine.target.trim() || typeof machine.cwd !== "string" || !machine.cwd.trim()) throw new Error(`${label}.machine is invalid.`);
|
|
126
|
+
for (const field of ["label", "session"] as const) if (machine[field] !== undefined && (typeof machine[field] !== "string" || !machine[field].trim())) throw new Error(`${label}.machine.${field} is invalid.`);
|
|
127
|
+
if (machine.remoteGit === undefined) return;
|
|
128
|
+
if (!machine.remoteGit || typeof machine.remoteGit !== "object" || Array.isArray(machine.remoteGit)) throw new Error(`${label}.machine.remoteGit must be an object.`);
|
|
129
|
+
const remoteGit = machine.remoteGit as Record<string, unknown>;
|
|
130
|
+
const unknownRemoteGit = Object.keys(remoteGit).filter((field) => !["head", "branch", "dirty"].includes(field));
|
|
131
|
+
if (unknownRemoteGit.length > 0 || (remoteGit.head !== undefined && (typeof remoteGit.head !== "string" || !remoteGit.head.trim())) || (remoteGit.branch !== undefined && (typeof remoteGit.branch !== "string" || !remoteGit.branch.trim())) || (remoteGit.dirty !== undefined && typeof remoteGit.dirty !== "boolean")) throw new Error(`${label}.machine.remoteGit is invalid.`);
|
|
132
|
+
}
|
|
133
|
+
|
|
118
134
|
function parseExternalCliReceiptMetadata(value: unknown, key: string, source: string): ExternalCliReceiptMetadata | undefined {
|
|
119
135
|
if (value === undefined) return undefined;
|
|
120
136
|
const label = `Invalid workflow receipt '${source}': entry '${key}' externalAdapter`;
|
|
121
137
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object.`);
|
|
122
138
|
const metadata = value as Record<string, unknown>;
|
|
123
|
-
const unknownMetadata = Object.keys(metadata).filter((field) => !["adapter", "capabilities", "safety", "outputArtifacts", "handoff", "supervisor", "nonResumableReason"].includes(field));
|
|
139
|
+
const unknownMetadata = Object.keys(metadata).filter((field) => !["adapter", "capabilities", "machine", "safety", "outputArtifacts", "handoff", "supervisor", "nonResumableReason"].includes(field));
|
|
124
140
|
if (unknownMetadata.length > 0) throw new Error(`${label} has unsupported fields: ${unknownMetadata.join(", ")}.`);
|
|
125
141
|
const adapter = metadata.adapter;
|
|
126
142
|
if (!adapter || typeof adapter !== "object" || Array.isArray(adapter)) throw new Error(`${label}.adapter must be an object.`);
|
|
@@ -136,6 +152,7 @@ function parseExternalCliReceiptMetadata(value: unknown, key: string, source: st
|
|
|
136
152
|
for (const [capability, expected] of Object.entries(EXTERNAL_CLI_CAPABILITIES)) {
|
|
137
153
|
if (capabilityRecord[capability] !== expected) throw new Error(`${label}.capabilities.${capability} is invalid.`);
|
|
138
154
|
}
|
|
155
|
+
if (metadata.machine !== undefined) parseExternalCliMachine(metadata.machine, label);
|
|
139
156
|
const safety = metadata.safety;
|
|
140
157
|
if (adapterRecord.id === "codex-exec") {
|
|
141
158
|
if (!safety || typeof safety !== "object" || Array.isArray(safety)) throw new Error(`${label}.safety is missing.`);
|
|
@@ -330,10 +347,11 @@ export function readWorkflowReceipt(asyncDirRoot: string, workflowRunId: string)
|
|
|
330
347
|
assertUniqueHostStepIds(hostSteps, receiptPath);
|
|
331
348
|
}
|
|
332
349
|
const workflowResolution = parseWorkflowResolution(receipt.workflowResolution, receiptPath);
|
|
350
|
+
if (receipt.argsDigest !== undefined && (typeof receipt.argsDigest !== "string" || !/^[a-f0-9]{64}$/u.test(receipt.argsDigest))) throw new Error(`Invalid workflow receipt '${receiptPath}': argsDigest is invalid.`);
|
|
333
351
|
const resource = parseWorkflowResource(receipt.resource, receiptPath);
|
|
334
352
|
const terminalOutcome = parseTerminalOutcome(receipt.terminalOutcome, `Invalid workflow receipt '${receiptPath}': terminalOutcome`);
|
|
335
353
|
const recovery = parseRecovery(receipt.recovery, workflowRunId, entries, receiptPath);
|
|
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 } : {}) };
|
|
354
|
+
return { version: 1, workflowRunId, state: receipt.state, createdAt: receipt.createdAt, entries, ...(receipt.argsDigest ? { argsDigest: receipt.argsDigest } : {}), ...(resource ? { resource } : {}), ...(hostSteps?.length ? { hostSteps } : {}), ...(workflowChildren ? { workflowChildren } : {}), ...(workflowResolution ? { workflowResolution } : {}), ...(terminalOutcome ? { terminalOutcome } : {}), ...(recovery ? { recovery } : {}) };
|
|
337
355
|
}
|
|
338
356
|
|
|
339
357
|
export function resolveWorkflowReceiptResumeEntry(input: {
|
|
@@ -125,7 +125,7 @@ function validatePlainJson(value: unknown, path: string, depth = 0): void {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
function
|
|
128
|
+
export function normalizeWorkflowArgs(value: unknown): { args: Record<string, unknown> } | { error: string } {
|
|
129
129
|
if (value === undefined) return { args: {} };
|
|
130
130
|
if (!isPlainRecord(value)) return { error: "workflow args must be a plain JSON object." };
|
|
131
131
|
try {
|
|
@@ -137,6 +137,17 @@ function normalizeArgs(value: unknown): { args: Record<string, unknown> } | { er
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
export function deepFreezeWorkflowArgs<T extends Record<string, unknown>>(args: T): Readonly<T> {
|
|
141
|
+
deepFreezeWorkflowValue(args);
|
|
142
|
+
return args;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function deepFreezeWorkflowValue(value: unknown): void {
|
|
146
|
+
if (!Array.isArray(value) && !isPlainRecord(value)) return;
|
|
147
|
+
for (const entry of Object.values(value)) deepFreezeWorkflowValue(entry);
|
|
148
|
+
Object.freeze(value);
|
|
149
|
+
}
|
|
150
|
+
|
|
140
151
|
function resolveRunCi(args: Readonly<Record<string, unknown>>): ReturnType<WorkflowResourceDefinition["resolve"]> {
|
|
141
152
|
const allowed = new Set(["command", "timeoutMs"]);
|
|
142
153
|
const unsupported = Object.keys(args).filter((key) => !allowed.has(key));
|
|
@@ -191,7 +202,7 @@ function resolveResource(nameValue: unknown, argsValue?: unknown, sessionId?: st
|
|
|
191
202
|
if (!RESOURCE_NAME_PATTERN.test(name)) return { ok: false, error: "workflow must use a safe resource name." };
|
|
192
203
|
const resource = findWorkflowResource(name) ?? (sessionId ? registry().bySession.get(sessionId)?.get(name) : undefined);
|
|
193
204
|
if (!resource) return { ok: false, error: `Unknown workflow resource '${name}'. Available resources: ${listWorkflowResourceNames().join(", ")}.` };
|
|
194
|
-
const normalizedArgs =
|
|
205
|
+
const normalizedArgs = normalizeWorkflowArgs(argsValue);
|
|
195
206
|
if ("error" in normalizedArgs) return { ok: false, error: normalizedArgs.error };
|
|
196
207
|
const resolved = resource.resolve(normalizedArgs.args);
|
|
197
208
|
if (resolved && typeof (resolved as unknown as { then?: unknown }).then === "function") {
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
// Only loaded when the parent supplies Pi 0.85.0's missing server exports.
|
|
2
|
-
import { registerHooks } from "node:module";
|
|
3
|
-
import { pathToFileURL } from "node:url";
|
|
4
|
-
|
|
5
|
-
const aliases = JSON.parse(process.env.JITI_ALIAS);
|
|
6
|
-
registerHooks({
|
|
7
|
-
resolve(specifier, context, nextResolve) {
|
|
8
|
-
if (specifier === "@earendil-works/pi-server" || specifier === "@earendil-works/pi-server/unix") {
|
|
9
|
-
return nextResolve(pathToFileURL(aliases[specifier]).href, context);
|
|
10
|
-
}
|
|
11
|
-
return nextResolve(specifier, context);
|
|
12
|
-
},
|
|
13
|
-
});
|