pi-subagents 0.67.0 → 0.69.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 +90 -0
- package/README.md +1 -1
- package/docs/agents.md +41 -12
- package/docs/configuration.md +61 -19
- package/docs/extension-api.md +5 -1
- package/docs/missions.md +2 -2
- package/docs/models.md +11 -79
- package/docs/observability.md +18 -8
- package/docs/standalone-background.md +13 -3
- package/docs/tool-reference.md +38 -14
- package/docs/watchdog.md +10 -12
- package/docs/workflows.md +59 -1
- package/index.ts +5 -2
- package/package.json +4 -2
- package/runner-peer-loader.mjs +24 -0
- package/runner-peer-preload.mjs +25 -11
- package/skills/pi-subagents/SKILL.md +18 -21
- package/skills/pi-subagents/references/constraints-and-recipes.md +3 -2
- package/skills/pi-subagents/references/execution-controls.md +6 -4
- 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 +16 -12
- package/skills/pi-subagents/references/review-and-validation.md +3 -3
- package/src/agents/agent-management.ts +57 -58
- package/src/agents/agent-serializer.ts +4 -3
- package/src/agents/agents.ts +185 -72
- package/src/agents/chain-serializer.ts +5 -0
- package/src/agents/runtime-agent-registry.ts +7 -6
- package/src/agents/skills.ts +1 -1
- package/src/api/preflight.ts +20 -16
- package/src/api/required-child-extensions.ts +6 -0
- package/src/extension/config.ts +10 -37
- package/src/extension/fanout-child.ts +3 -0
- package/src/extension/herdr-pi-bridge.ts +160 -0
- package/src/extension/index.ts +42 -31
- package/src/extension/public-execution.ts +3 -3
- package/src/extension/schemas.ts +23 -6
- package/src/extension/tool-description.ts +8 -7
- package/src/inspectors/ghostty/plugin.ts +13 -1
- package/src/intercom/native-supervisor-channel.ts +22 -18
- package/src/policy/authority.ts +4 -0
- package/src/profiles/profiles.ts +12 -6
- package/src/runs/background/active-run-index.ts +17 -1
- package/src/runs/background/async-execution.ts +309 -126
- package/src/runs/background/async-job-tracker.ts +8 -6
- package/src/runs/background/async-resume.ts +13 -4
- package/src/runs/background/async-status.ts +15 -4
- package/src/runs/background/auto-drain.ts +20 -10
- package/src/runs/background/binary-bootstrap.ts +5 -0
- package/src/runs/background/chain-append.ts +1 -1
- package/src/runs/background/chain-root-attachment.ts +14 -33
- package/src/runs/background/notify.ts +74 -6
- 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 +20 -29
- package/src/runs/background/runner-aliases.ts +4 -33
- package/src/runs/background/runner-child-launch.ts +4 -1
- package/src/runs/background/runner-child-sessions.ts +2 -2
- package/src/runs/background/runner-http-dispatcher.ts +119 -0
- package/src/runs/background/scheduled-runs.ts +11 -5
- package/src/runs/background/stale-run-reconciler.ts +35 -11
- package/src/runs/background/subagent-runner.ts +413 -276
- package/src/runs/background/subagent-wait.ts +128 -23
- 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 +4 -2
- package/src/runs/foreground/async-stop-action.ts +93 -3
- package/src/runs/foreground/execution.ts +115 -219
- package/src/runs/foreground/foreground-history.ts +2 -1
- package/src/runs/foreground/subagent-executor.ts +281 -80
- package/src/runs/shared/acceptance.ts +194 -37
- package/src/runs/shared/async-status-projection.ts +123 -33
- package/src/runs/shared/child-launch-plan.ts +15 -3
- package/src/runs/shared/child-launch.ts +19 -6
- package/src/runs/shared/child-runtime-config.ts +5 -0
- package/src/runs/shared/child-session.ts +94 -50
- package/src/runs/shared/child-tool-plan.ts +28 -16
- package/src/runs/shared/dynamic-fanout.ts +2 -2
- package/src/runs/shared/external-cli-contract.ts +11 -1
- package/src/runs/shared/external-cli-preflight.ts +6 -2
- 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/model-resolution-diagnostic.ts +76 -0
- package/src/runs/shared/{model-fallback.ts → model-resolution.ts} +22 -237
- package/src/runs/shared/model-scope.ts +1 -1
- package/src/runs/shared/nested-events.ts +11 -2
- package/src/runs/shared/parallel-utils.ts +7 -2
- package/src/runs/shared/pi-spawn.ts +1 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +4 -2
- package/src/runs/shared/worktree-setup-command.ts +27 -4
- package/src/runs/shared/worktree.ts +30 -8
- package/src/shared/child-cache-retention.ts +43 -0
- package/src/shared/launch-contract.ts +6 -9
- 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 +81 -29
- package/src/slash/slash-commands.ts +0 -6
- package/src/slash/subagents-admin.ts +13 -9
- package/src/tui/render.ts +20 -10
- package/src/watchdog/child-status.ts +28 -36
- package/src/watchdog/lsp-diagnostics.ts +1 -1
- package/src/watchdog/model-selection.ts +1 -1
- package/src/watchdog/register-child.ts +10 -3
- package/src/watchdog/register-main.ts +20 -20
- package/src/watchdog/render.ts +1 -1
- package/src/watchdog/review.ts +14 -30
- package/src/watchdog/rules.ts +1 -1
- package/src/watchdog/runtime.ts +23 -12
- package/src/watchdog/settings.ts +3 -6
- package/src/watchdog/types.ts +3 -5
- package/src/watchdog/warning-format.ts +1 -1
- package/src/workflows/scripted-workflow.ts +68 -7
- package/src/workflows/workflow-receipt.ts +21 -3
- package/src/workflows/workflow-resources.ts +13 -2
- 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/watchdog/runtime.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { WatchdogScopeArtifact } from "./scope.ts";
|
|
|
14
14
|
import { resolveWatchdogConfig } from "./settings.ts";
|
|
15
15
|
import { formatWatchdogOrchestrationActivity, formatWatchdogTurnDelta } from "./turn-delta.ts";
|
|
16
16
|
import {
|
|
17
|
+
WATCHDOG_WARNING_IMPORTANCES,
|
|
17
18
|
type ResolvedWatchdogConfig,
|
|
18
19
|
type WatchdogEndpointConfig,
|
|
19
20
|
type WatchdogLspRuntimeSnapshot,
|
|
@@ -31,6 +32,8 @@ type ReviewStopReason = "stop" | "error" | "aborted" | "length";
|
|
|
31
32
|
export interface WatchdogReviewResult {
|
|
32
33
|
warnings?: WatchdogWarning[];
|
|
33
34
|
stopReason?: ReviewStopReason;
|
|
35
|
+
/** Provider error text for a failed review, surfaced in status `Last error`. */
|
|
36
|
+
errorMessage?: string;
|
|
34
37
|
clarification?: { question: string; evidence: string };
|
|
35
38
|
}
|
|
36
39
|
|
|
@@ -83,6 +86,7 @@ interface MainWatchdogRuntimeOptions {
|
|
|
83
86
|
review?: WatchdogReviewFunction;
|
|
84
87
|
reviewDescription?: string;
|
|
85
88
|
displayWarning?: (warning: WatchdogWarningDetails, options?: WatchdogWarningSendOptions) => void;
|
|
89
|
+
displayUserWarning?: (warning: WatchdogWarningDetails) => void;
|
|
86
90
|
/** Supplying this main-session delivery capability gates clarification (children omit it). */
|
|
87
91
|
displayClarification?: (content: string) => void;
|
|
88
92
|
reviewChangesOnly?: boolean;
|
|
@@ -130,6 +134,7 @@ export class MainWatchdogRuntime {
|
|
|
130
134
|
private readonly reviewConnected: boolean;
|
|
131
135
|
private readonly reviewDescription: string;
|
|
132
136
|
private readonly displayWarning: ((warning: WatchdogWarningDetails, options?: WatchdogWarningSendOptions) => void) | undefined;
|
|
137
|
+
private readonly displayUserWarning: ((warning: WatchdogWarningDetails) => void) | undefined;
|
|
133
138
|
private readonly reviewChangesOnly: boolean;
|
|
134
139
|
private readonly lspDiagnostics: WatchdogLspDiagnosticsFunction;
|
|
135
140
|
private readonly repoChangeSignature: typeof computeWatchdogRepoChangeSignature;
|
|
@@ -187,6 +192,7 @@ export class MainWatchdogRuntime {
|
|
|
187
192
|
this.reviewConnected = Boolean(options.review);
|
|
188
193
|
this.reviewDescription = options.reviewDescription ?? (options.review ? "injected seam" : "not wired");
|
|
189
194
|
this.displayWarning = options.displayWarning;
|
|
195
|
+
this.displayUserWarning = options.displayUserWarning;
|
|
190
196
|
this.displayClarification = options.displayClarification;
|
|
191
197
|
this.reviewChangesOnly = options.reviewChangesOnly === true;
|
|
192
198
|
this.lspDiagnostics = options.lspDiagnostics ?? collectWatchdogLspDiagnostics;
|
|
@@ -268,7 +274,7 @@ export class MainWatchdogRuntime {
|
|
|
268
274
|
return this.getSnapshot();
|
|
269
275
|
}
|
|
270
276
|
|
|
271
|
-
reset(_reason = "reset", options: { clearReviewInputSignature?: boolean; resetChangeSignature?: boolean; clearLspLedger?: boolean; clearScope?: boolean } = {}): void {
|
|
277
|
+
reset(_reason = "reset", options: { clearReviewInputSignature?: boolean; resetChangeSignature?: boolean; clearLspLedger?: boolean; clearScope?: boolean; clearActivity?: boolean } = {}): void {
|
|
272
278
|
this.activeReviewAbortController?.abort();
|
|
273
279
|
this.abortActiveAgentEnd();
|
|
274
280
|
this.epoch++;
|
|
@@ -290,10 +296,8 @@ export class MainWatchdogRuntime {
|
|
|
290
296
|
this.lspLedger.reset();
|
|
291
297
|
this.lastLspSnapshot = undefined;
|
|
292
298
|
}
|
|
293
|
-
if (options.clearScope)
|
|
294
|
-
|
|
295
|
-
this.clearActivity();
|
|
296
|
-
}
|
|
299
|
+
if (options.clearScope) this.scope.reset();
|
|
300
|
+
if (options.clearScope || options.clearActivity) this.clearActivity();
|
|
297
301
|
if (options.clearReviewInputSignature) this.lastReviewInputSignature = undefined;
|
|
298
302
|
if (options.resetChangeSignature) this.resetRepoChangeBaseline({ reviewed: true });
|
|
299
303
|
this.guard.reset();
|
|
@@ -467,13 +471,13 @@ export class MainWatchdogRuntime {
|
|
|
467
471
|
this.ruleWarningsThisRun.add(violation.summary);
|
|
468
472
|
const details = normalizeWatchdogWarningDetails(ruleViolationWarning(violation), { state: "displayed", displayedAt: new Date().toISOString() });
|
|
469
473
|
this.lastWarning = details;
|
|
470
|
-
this.
|
|
474
|
+
this.routeWarning(details, { deliverAs: "steer" });
|
|
471
475
|
}
|
|
472
476
|
|
|
473
|
-
|
|
477
|
+
displayRecordedWarning(warning: WatchdogWarning): void {
|
|
474
478
|
const details = normalizeWatchdogWarningDetails(warning, { state: "displayed", source: warning.source ?? "main" });
|
|
475
479
|
this.lastWarning = details;
|
|
476
|
-
|
|
480
|
+
this.routeWarning(details);
|
|
477
481
|
}
|
|
478
482
|
|
|
479
483
|
getSnapshot(cwd?: string): WatchdogRuntimeSnapshot {
|
|
@@ -536,7 +540,13 @@ export class MainWatchdogRuntime {
|
|
|
536
540
|
}
|
|
537
541
|
|
|
538
542
|
private warningMeetsThreshold(warning: WatchdogWarning): boolean {
|
|
539
|
-
return
|
|
543
|
+
return (WATCHDOG_WARNING_IMPORTANCES as readonly string[]).includes(warning.importance)
|
|
544
|
+
&& (this.configResult.config.severityThreshold === "concern" || warning.severity === "blocker");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private routeWarning(details: WatchdogWarningDetails, options?: WatchdogWarningSendOptions): void {
|
|
548
|
+
if (details.importance === "high") this.displayWarning?.(details, options);
|
|
549
|
+
else this.displayUserWarning?.(details);
|
|
540
550
|
}
|
|
541
551
|
|
|
542
552
|
private acceptWarning(epoch: number, reviewId: number, warning: WatchdogWarning): boolean {
|
|
@@ -658,7 +668,8 @@ export class MainWatchdogRuntime {
|
|
|
658
668
|
}
|
|
659
669
|
for (const warning of result.warnings ?? []) this.acceptWarning(reviewEpoch, reviewId, warning);
|
|
660
670
|
if (result.stopReason && result.stopReason !== "stop") {
|
|
661
|
-
|
|
671
|
+
const detail = result.errorMessage?.trim() ? ` ${boundWatchdogReviewText(result.errorMessage.trim(), 600)}` : "";
|
|
672
|
+
this.fail(`Watchdog review ended with stop reason '${result.stopReason}'.${detail}`);
|
|
662
673
|
return "completed";
|
|
663
674
|
}
|
|
664
675
|
this.displayAcceptedReviewWarning(options.correction);
|
|
@@ -690,7 +701,7 @@ export class MainWatchdogRuntime {
|
|
|
690
701
|
};
|
|
691
702
|
if (correction) {
|
|
692
703
|
this.lastWarning = details;
|
|
693
|
-
this.
|
|
704
|
+
this.routeWarning(details, { deliverAs: "steer" });
|
|
694
705
|
return;
|
|
695
706
|
}
|
|
696
707
|
this.deliverBoundaryWarning(details);
|
|
@@ -711,7 +722,7 @@ export class MainWatchdogRuntime {
|
|
|
711
722
|
: details;
|
|
712
723
|
this.stalemate = stalemate;
|
|
713
724
|
this.lastWarning = delivered;
|
|
714
|
-
this.
|
|
725
|
+
this.routeWarning(delivered, stalemate ? { triggerTurn: false } : undefined);
|
|
715
726
|
}
|
|
716
727
|
|
|
717
728
|
// The previous boundary finding is a repeat to count, not a duplicate, until stalemate.
|
package/src/watchdog/settings.ts
CHANGED
|
@@ -111,9 +111,9 @@ const ROLE_MODEL_RULE_FIELDS = new Set(["allow", "deny", "note"]);
|
|
|
111
111
|
const GUIDANCE_FIELDS = new Set(["watchdogMd"]);
|
|
112
112
|
const SCOPE_FIELDS = new Set(["enabled"]);
|
|
113
113
|
const CADENCE_FIELDS = new Set(["everyNTools"]);
|
|
114
|
-
const ENDPOINT_FIELDS = new Set(["enabled", "model", "
|
|
115
|
-
const CHILDREN_FIELDS = new Set(["enabled", "model", "
|
|
116
|
-
const CHILD_OVERRIDE_FIELDS = new Set(["enabled", "model", "
|
|
114
|
+
const ENDPOINT_FIELDS = new Set(["enabled", "model", "thinking"]);
|
|
115
|
+
const CHILDREN_FIELDS = new Set(["enabled", "model", "thinking", "watchdogTailTimeoutMs", "cadence", "overrides"]);
|
|
116
|
+
const CHILD_OVERRIDE_FIELDS = new Set(["enabled", "model", "thinking", "cadence"]);
|
|
117
117
|
const LSP_FIELDS = new Set(["enabled", "timeoutMs", "maxFiles", "maxDiagnostics"]);
|
|
118
118
|
|
|
119
119
|
function cloneDefaultConfig(): ResolvedWatchdogConfig {
|
|
@@ -221,7 +221,6 @@ function parseEndpointPatch(value: unknown, field: string, meta: ParseMeta): Wat
|
|
|
221
221
|
const input = parseObject(value, field, meta);
|
|
222
222
|
assertKnownFields(input, ENDPOINT_FIELDS, field, meta);
|
|
223
223
|
const patch: WatchdogEndpointPatch = {};
|
|
224
|
-
if ("fallbackModels" in input) patch.fallbackModels = parseStringList(input.fallbackModels, `${field}.fallbackModels`, meta);
|
|
225
224
|
if ("enabled" in input) patch.enabled = parseBoolean(input.enabled, `${field}.enabled`, meta);
|
|
226
225
|
if ("model" in input) patch.model = parseNonEmptyString(input.model, `${field}.model`, meta);
|
|
227
226
|
if ("thinking" in input) patch.thinking = parseThinking(input.thinking, `${field}.thinking`, meta);
|
|
@@ -232,7 +231,6 @@ function parseChildOverridePatch(value: unknown, field: string, meta: ParseMeta)
|
|
|
232
231
|
const input = parseObject(value, field, meta);
|
|
233
232
|
assertKnownFields(input, CHILD_OVERRIDE_FIELDS, field, meta);
|
|
234
233
|
const patch: WatchdogChildOverridePatch = {};
|
|
235
|
-
if ("fallbackModels" in input) patch.fallbackModels = parseStringList(input.fallbackModels, `${field}.fallbackModels`, meta);
|
|
236
234
|
if ("enabled" in input) patch.enabled = parseBoolean(input.enabled, `${field}.enabled`, meta);
|
|
237
235
|
if ("model" in input) patch.model = parseNonEmptyString(input.model, `${field}.model`, meta);
|
|
238
236
|
if ("thinking" in input) patch.thinking = parseThinking(input.thinking, `${field}.thinking`, meta);
|
|
@@ -244,7 +242,6 @@ function parseChildrenPatch(value: unknown, field: string, meta: ParseMeta): Wat
|
|
|
244
242
|
const input = parseObject(value, field, meta);
|
|
245
243
|
assertKnownFields(input, CHILDREN_FIELDS, field, meta);
|
|
246
244
|
const patch: WatchdogChildrenPatch = {};
|
|
247
|
-
if ("fallbackModels" in input) patch.fallbackModels = parseStringList(input.fallbackModels, `${field}.fallbackModels`, meta);
|
|
248
245
|
if ("enabled" in input) patch.enabled = parseBoolean(input.enabled, `${field}.enabled`, meta);
|
|
249
246
|
if ("model" in input) patch.model = parseNonEmptyString(input.model, `${field}.model`, meta);
|
|
250
247
|
if ("thinking" in input) patch.thinking = parseThinking(input.thinking, `${field}.thinking`, meta);
|
package/src/watchdog/types.ts
CHANGED
|
@@ -15,8 +15,8 @@ export const WATCHDOG_WARNING_CATEGORIES = [
|
|
|
15
15
|
] as const;
|
|
16
16
|
export type WatchdogCategory = typeof WATCHDOG_WARNING_CATEGORIES[number];
|
|
17
17
|
|
|
18
|
-
export const
|
|
19
|
-
export type
|
|
18
|
+
export const WATCHDOG_WARNING_IMPORTANCES = ["low", "medium", "high"] as const;
|
|
19
|
+
export type WatchdogImportance = typeof WATCHDOG_WARNING_IMPORTANCES[number];
|
|
20
20
|
|
|
21
21
|
export const WATCHDOG_WARNING_SOURCES = ["main", "child", "lsp"] as const;
|
|
22
22
|
export type WatchdogWarningSource = typeof WATCHDOG_WARNING_SOURCES[number];
|
|
@@ -44,11 +44,11 @@ export type WatchdogWarningState = typeof WATCHDOG_WARNING_STATES[number];
|
|
|
44
44
|
|
|
45
45
|
export interface WatchdogWarning {
|
|
46
46
|
severity: WatchdogSeverity;
|
|
47
|
+
importance: WatchdogImportance;
|
|
47
48
|
summary: string;
|
|
48
49
|
evidence: string;
|
|
49
50
|
recommendedAction: string;
|
|
50
51
|
category?: WatchdogCategory;
|
|
51
|
-
confidence?: WatchdogConfidence;
|
|
52
52
|
source?: WatchdogWarningSource;
|
|
53
53
|
agent?: string;
|
|
54
54
|
runId?: string;
|
|
@@ -87,14 +87,12 @@ export interface WatchdogCadenceConfig {
|
|
|
87
87
|
export interface WatchdogEndpointConfig {
|
|
88
88
|
enabled: boolean;
|
|
89
89
|
model?: string;
|
|
90
|
-
fallbackModels?: string[];
|
|
91
90
|
thinking?: string | false;
|
|
92
91
|
}
|
|
93
92
|
|
|
94
93
|
export interface WatchdogChildOverrideConfig {
|
|
95
94
|
enabled?: boolean;
|
|
96
95
|
model?: string;
|
|
97
|
-
fallbackModels?: string[];
|
|
98
96
|
thinking?: string | false;
|
|
99
97
|
cadence?: Partial<WatchdogCadenceConfig>;
|
|
100
98
|
}
|
|
@@ -34,12 +34,12 @@ export function formatWatchdogWarningContent(warning: WatchdogWarning): string {
|
|
|
34
34
|
const details = normalizeWatchdogWarningDetails(warning);
|
|
35
35
|
const attrs = [
|
|
36
36
|
`severity="${escapeXmlAttribute(details.severity)}"`,
|
|
37
|
+
`importance="${escapeXmlAttribute(details.importance)}"`,
|
|
37
38
|
`category="${escapeXmlAttribute(details.category)}"`,
|
|
38
39
|
`source="${escapeXmlAttribute(details.source)}"`,
|
|
39
40
|
`guidance="weigh, don't blindly obey"`,
|
|
40
41
|
];
|
|
41
42
|
const optionalTags = [
|
|
42
|
-
tag("confidence", details.confidence),
|
|
43
43
|
tag("agent", details.agent),
|
|
44
44
|
tag("run_id", details.runId),
|
|
45
45
|
tag("state", details.state),
|
|
@@ -1,11 +1,11 @@
|
|
|
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";
|
|
5
5
|
import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../runs/shared/parallel-utils.ts";
|
|
6
6
|
import { HOST_STEP_MAX_COUNT } from "../runs/shared/host-step-status.ts";
|
|
7
7
|
import { classifyTaskMutationIntent } from "../runs/shared/task-intent.ts";
|
|
8
|
-
import { describeGateAcceptanceConflict } from "../runs/shared/acceptance.ts";
|
|
8
|
+
import { describeGateAcceptanceConflict, parseGateInput } from "../runs/shared/acceptance.ts";
|
|
9
9
|
import type { AcceptanceRecoveryMetadata, HostStepNode, SingleResult } from "../shared/types.ts";
|
|
10
10
|
import { normalizeWorkflowHostCommandParams, type WorkflowHostCommandParams, type WorkflowHostCommandResult } from "./host-command.ts";
|
|
11
11
|
|
|
@@ -550,7 +550,7 @@ function runLanes(laneSpecs) {
|
|
|
550
550
|
generatedKey: stage.generatedKey,
|
|
551
551
|
...workflowPlanStringMetadata(stage.params),
|
|
552
552
|
...(typeof stage.params.as === "string" && stage.params.as.trim() ? { outputName: stage.params.as.trim() } : {}),
|
|
553
|
-
...(stage.params.outputSchema
|
|
553
|
+
...(stage.params.outputSchema ? { structured: true } : {}),
|
|
554
554
|
})),
|
|
555
555
|
})) });
|
|
556
556
|
const firstItems = lanes.map((lane) => {
|
|
@@ -633,6 +633,24 @@ function validateLaneMetadata(value, label, workflowKey) {
|
|
|
633
633
|
}
|
|
634
634
|
}
|
|
635
635
|
|
|
636
|
+
// Mirrors parseGateInput in src/runs/shared/acceptance.ts; the sandbox cannot import it.
|
|
637
|
+
function describeGateShapeError(gate) {
|
|
638
|
+
const shape = "gate must be a non-empty command string or { command, output?: \"json\", schema?, timeoutMs? }.";
|
|
639
|
+
if (typeof gate === "string") return gate.trim() ? undefined : shape;
|
|
640
|
+
if (!gate || typeof gate !== "object" || Array.isArray(gate)) return shape;
|
|
641
|
+
for (const key of Object.keys(gate)) {
|
|
642
|
+
if (!["command", "output", "schema", "timeoutMs"].includes(key)) return "gate." + key + " is not supported.";
|
|
643
|
+
}
|
|
644
|
+
if (typeof gate.command !== "string" || !gate.command.trim()) return shape;
|
|
645
|
+
if (gate.output !== undefined && gate.output !== "json") return "gate.output must be \"json\" when present.";
|
|
646
|
+
if (gate.schema !== undefined) {
|
|
647
|
+
if (gate.output !== "json") return "gate.schema requires gate.output: \"json\".";
|
|
648
|
+
if (!gate.schema || typeof gate.schema !== "object" || Array.isArray(gate.schema)) return "gate.schema must be a JSON Schema object.";
|
|
649
|
+
}
|
|
650
|
+
if (gate.timeoutMs !== undefined && (!Number.isInteger(gate.timeoutMs) || gate.timeoutMs < 1)) return "gate.timeoutMs must be an integer >= 1.";
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
653
|
+
|
|
636
654
|
function describeGateAcceptanceConflict(gate, acceptance) {
|
|
637
655
|
const render = (value) => {
|
|
638
656
|
let encoded;
|
|
@@ -658,7 +676,10 @@ function validateRunCall(key, params, label, fingerprints) {
|
|
|
658
676
|
if (params.worktree !== undefined && typeof params.worktree !== "boolean") throw new Error(label + " worktree must be true or false.");
|
|
659
677
|
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.");
|
|
660
678
|
validateLaneMetadata(params.lane, label + " lane", key);
|
|
661
|
-
if (params.gate !== undefined
|
|
679
|
+
if (params.gate !== undefined) {
|
|
680
|
+
const gateError = describeGateShapeError(params.gate);
|
|
681
|
+
if (gateError) throw new Error(label + " " + gateError);
|
|
682
|
+
}
|
|
662
683
|
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));
|
|
663
684
|
if (params.gate !== undefined && params.resume !== undefined) throw new Error(label + " gate is not supported with retained resume.");
|
|
664
685
|
if (params.extensionBindings !== undefined && params.resume !== undefined) throw new Error(label + " extensionBindings is not supported with retained resume; resume uses the original retained child binding.");
|
|
@@ -952,6 +973,12 @@ function omitUndefinedWorkflowValues(value, seen = new Set()) {
|
|
|
952
973
|
return normalized;
|
|
953
974
|
}
|
|
954
975
|
|
|
976
|
+
function deepFreezeWorkflowArgs(value) {
|
|
977
|
+
if (value === null || typeof value !== "object" || Object.isFrozen(value)) return value;
|
|
978
|
+
for (const entry of Object.values(value)) deepFreezeWorkflowArgs(entry);
|
|
979
|
+
return Object.freeze(value);
|
|
980
|
+
}
|
|
981
|
+
|
|
955
982
|
parentPort.on("message", async (message) => {
|
|
956
983
|
if (message.type === "response") {
|
|
957
984
|
const entry = pending.get(message.callId);
|
|
@@ -971,6 +998,9 @@ parentPort.on("message", async (message) => {
|
|
|
971
998
|
if (message.stateEnabled) sandbox.state = state;
|
|
972
999
|
const context = vm.createContext(sandbox, { codeGeneration: { strings: false, wasm: false } });
|
|
973
1000
|
contextObjectPrototype = vm.runInContext("Object.prototype", context);
|
|
1001
|
+
// Rebuild args inside the VM realm: a worker-realm object would expose the worker's unrestricted
|
|
1002
|
+
// Function through args.constructor.constructor, bypassing codeGeneration.strings: false.
|
|
1003
|
+
sandbox.args = deepFreezeWorkflowArgs(vm.runInContext("JSON.parse", context)(JSON.stringify(message.args ?? {})));
|
|
974
1004
|
let compiled;
|
|
975
1005
|
try {
|
|
976
1006
|
assertPortableWorkflowScript(message.script);
|
|
@@ -1169,6 +1199,10 @@ export interface WorkflowChildSettledNotification {
|
|
|
1169
1199
|
|
|
1170
1200
|
export interface RunWorkflowScriptOptions {
|
|
1171
1201
|
script: string;
|
|
1202
|
+
/** Normalized raw-script input exposed as the deeply frozen sandbox global `args`. */
|
|
1203
|
+
args?: Readonly<Record<string, unknown>>;
|
|
1204
|
+
/** Parent-session cwd used to recover a stale process cwd. */
|
|
1205
|
+
processCwd?: string;
|
|
1172
1206
|
/** Workflow run ID for notifications. Required when onChildSettled is provided. */
|
|
1173
1207
|
workflowRunId?: string;
|
|
1174
1208
|
/** Host-only first-slice admission context. It is never sent to the workflow worker. */
|
|
@@ -1922,6 +1956,32 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1922
1956
|
}
|
|
1923
1957
|
const launchSemaphore = new Semaphore(options.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT);
|
|
1924
1958
|
|
|
1959
|
+
if (options.processCwd !== undefined) {
|
|
1960
|
+
let staleCwd = false;
|
|
1961
|
+
try {
|
|
1962
|
+
realpathSync(process.cwd());
|
|
1963
|
+
} catch (error) {
|
|
1964
|
+
const code = typeof error === "object" && error !== null && "code" in error ? (error as NodeJS.ErrnoException).code : undefined;
|
|
1965
|
+
if (code !== "ENOENT") throw new Error("Workflow current cwd could not be validated.", { cause: error });
|
|
1966
|
+
staleCwd = true;
|
|
1967
|
+
}
|
|
1968
|
+
try {
|
|
1969
|
+
if (staleCwd) process.chdir(options.processCwd);
|
|
1970
|
+
else {
|
|
1971
|
+
const target = realpathSync(options.processCwd);
|
|
1972
|
+
if (!statSync(target).isDirectory()) {
|
|
1973
|
+
const error = new Error(`ENOTDIR: not a directory, access '${target}'`) as NodeJS.ErrnoException;
|
|
1974
|
+
error.code = "ENOTDIR";
|
|
1975
|
+
throw error;
|
|
1976
|
+
}
|
|
1977
|
+
accessSync(target, constants.X_OK);
|
|
1978
|
+
}
|
|
1979
|
+
} catch (error) {
|
|
1980
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1981
|
+
throw new Error(`Workflow process cwd is unavailable: ${options.processCwd}: ${detail}`, { cause: error });
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1925
1985
|
let acornPath: string;
|
|
1926
1986
|
try {
|
|
1927
1987
|
acornPath = resolveWorkflowParserEntry();
|
|
@@ -2379,8 +2439,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2379
2439
|
if (params.baseRef !== undefined && (typeof params.baseRef !== "string" || !validGitRef(params.baseRef))) {
|
|
2380
2440
|
return respond(Promise.reject(new Error(`runs.run('${key}') ${BASE_REF_VALIDATION_ERROR}`)));
|
|
2381
2441
|
}
|
|
2382
|
-
if (params.gate !== undefined
|
|
2383
|
-
|
|
2442
|
+
if (params.gate !== undefined) {
|
|
2443
|
+
const parsedGate = parseGateInput(params.gate);
|
|
2444
|
+
if (!parsedGate.ok) return respond(Promise.reject(new Error(`runs.run('${key}') ${parsedGate.error}`)));
|
|
2384
2445
|
}
|
|
2385
2446
|
if (params.gate !== undefined && params.acceptance !== undefined && params.acceptance !== false) {
|
|
2386
2447
|
return respond(Promise.reject(new Error(`runs.run('${key}') gate cannot be combined with acceptance; use one gate command or acceptance.verify.` + describeGateAcceptanceConflict(params.gate, params.acceptance))));
|
|
@@ -2509,6 +2570,6 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2509
2570
|
respond(deliver(promise), `runs.run('${key}') result`, (error) => children.set(key, responseBoundaryFailure(key, error)));
|
|
2510
2571
|
});
|
|
2511
2572
|
|
|
2512
|
-
worker.postMessage({ type: "start", script: options.script, stateEnabled: options.state !== undefined });
|
|
2573
|
+
worker.postMessage({ type: "start", script: options.script, ...(options.args ? { args: options.args } : {}), stateEnabled: options.state !== undefined });
|
|
2513
2574
|
});
|
|
2514
2575
|
}
|
|
@@ -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") {
|