pi-subagents 0.63.0 → 0.65.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 +57 -1
- package/README.md +2 -2
- package/agents/reviewer.md +1 -1
- package/agents/scout.md +1 -1
- package/docs/agents.md +18 -16
- package/docs/configuration.md +7 -15
- package/docs/extension-api.md +15 -6
- package/docs/missions.md +2 -0
- package/docs/observability.md +10 -12
- package/docs/tool-reference.md +7 -6
- package/docs/watchdog.md +92 -114
- package/docs/workflows.md +5 -3
- package/package.json +3 -4
- package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
- package/skills/pi-subagents/references/execution-controls.md +6 -5
- package/src/agents/agent-management.ts +47 -15
- package/src/api/capability-ceiling.ts +0 -1
- package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
- package/src/api/preflight.ts +2 -3
- package/src/extension/doctor.ts +2 -10
- package/src/extension/fanout-child.ts +9 -11
- package/src/extension/index.ts +27 -5
- package/src/extension/public-execution.ts +14 -0
- package/src/extension/rpc.ts +3 -2
- package/src/extension/schemas.ts +2 -1
- package/src/extension/tool-description.ts +9 -8
- package/src/intercom/native-supervisor-channel.ts +138 -60
- package/src/intercom/supervisor-ui.ts +243 -0
- package/src/runs/background/async-execution.ts +46 -24
- package/src/runs/background/async-job-tracker.ts +11 -0
- package/src/runs/background/async-resume.ts +11 -2
- package/src/runs/background/control-channel.ts +2 -204
- package/src/runs/background/notify.ts +41 -2
- package/src/runs/background/process-terminal.ts +1 -1
- package/src/runs/background/run-child-session.ts +613 -0
- package/src/runs/background/run-status.ts +0 -1
- package/src/runs/background/runner-aliases.ts +125 -0
- package/src/runs/background/runner-child-sessions.ts +31 -0
- package/src/runs/background/scheduled-runs.ts +18 -4
- package/src/runs/background/subagent-runner.ts +203 -898
- package/src/runs/foreground/async-steering-action.ts +1 -17
- package/src/runs/foreground/execution.ts +196 -378
- package/src/runs/foreground/foreground-control.ts +4 -0
- package/src/runs/foreground/subagent-executor.ts +121 -58
- package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
- package/src/runs/shared/abort-recovery.ts +3 -3
- package/src/runs/shared/acceptance.ts +10 -0
- package/src/runs/shared/async-status-projection.ts +11 -43
- package/src/runs/shared/capability-ceiling.ts +1 -2
- package/src/runs/shared/child-hooks.ts +25 -0
- package/src/runs/shared/child-identity.ts +13 -2
- package/src/runs/shared/child-launch.ts +314 -0
- package/src/runs/shared/child-lifecycle.ts +25 -0
- package/src/runs/shared/child-runtime-config.ts +126 -0
- package/src/runs/shared/child-session.ts +342 -0
- package/src/runs/shared/child-tool-plan.ts +530 -0
- package/src/runs/shared/claude-code-adapter.ts +5 -1
- package/src/runs/shared/completion-guard.ts +1 -1
- package/src/runs/shared/external-cli-preflight.ts +16 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
- package/src/runs/shared/model-exclusions.ts +82 -14
- package/src/runs/shared/model-fallback.ts +47 -4
- package/src/runs/shared/nested-events.ts +29 -45
- package/src/runs/shared/nested-path.ts +0 -14
- package/src/runs/shared/orca-progress-tabs.ts +12 -7
- package/src/runs/shared/parallel-utils.ts +0 -2
- package/src/runs/shared/permissions.ts +0 -13
- package/src/runs/shared/process-signal.ts +4 -1
- package/src/runs/shared/run-fanout-budget.ts +0 -13
- package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
- package/src/runs/shared/structured-output.ts +17 -4
- package/src/runs/shared/subagent-control.ts +6 -2
- package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
- package/src/runs/shared/tool-availability.ts +18 -62
- package/src/runs/shared/tool-budget.ts +0 -14
- package/src/runs/shared/worktree-cleanup-plan.ts +25 -6
- package/src/runs/shared/worktree.ts +117 -30
- package/src/shared/child-session-name.ts +1 -1
- package/src/shared/jsonl-writer.ts +11 -0
- package/src/shared/thinking-ceiling.ts +0 -6
- package/src/shared/types.ts +64 -30
- package/src/shared/utils.ts +3 -4
- package/src/slash/slash-commands.ts +0 -6
- package/src/tui/fleet.ts +0 -1
- package/src/tui/render.ts +238 -31
- package/src/watchdog/child-status.ts +54 -34
- package/src/watchdog/diff-tool.ts +77 -0
- package/src/watchdog/emission-guard.ts +5 -3
- package/src/watchdog/guidance.ts +20 -0
- package/src/watchdog/register-child.ts +28 -25
- package/src/watchdog/register-main.ts +10 -9
- package/src/watchdog/render.ts +4 -5
- package/src/watchdog/review.ts +15 -4
- package/src/watchdog/rules.ts +70 -0
- package/src/watchdog/runtime.ts +75 -92
- package/src/watchdog/scope.ts +0 -11
- package/src/watchdog/settings.ts +48 -104
- package/src/watchdog/types.ts +18 -32
- package/src/watchdog/warning-format.ts +0 -1
- package/src/workflows/chat-progress.ts +3 -2
- package/src/workflows/scripted-workflow.ts +48 -1
- package/src/workflows/workflow-checklist.ts +10 -12
- package/src/workflows/workflow-preflight.ts +28 -1
- package/src/runs/shared/child-protocol.ts +0 -415
- package/src/runs/shared/pi-args.ts +0 -1062
- package/src/runs/shared/subagent-startup-retry.ts +0 -116
- package/src/shared/post-exit-stdio-guard.ts +0 -85
package/src/watchdog/settings.ts
CHANGED
|
@@ -3,39 +3,31 @@ import * as path from "node:path";
|
|
|
3
3
|
import { THINKING_LEVELS, type ThinkingLevel } from "../shared/model-info.ts";
|
|
4
4
|
import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
|
|
5
5
|
import {
|
|
6
|
-
WATCHDOG_DELIVERY_MODES,
|
|
7
|
-
WATCHDOG_LATE_WARNING_POLICIES,
|
|
8
6
|
WATCHDOG_WARNING_SEVERITIES,
|
|
9
7
|
type ResolvedWatchdogConfig,
|
|
10
|
-
type WatchdogAsyncCompletionConfig,
|
|
11
|
-
type WatchdogAutoFollowConfig,
|
|
12
8
|
type WatchdogChildOverrideConfig,
|
|
13
9
|
type WatchdogChildrenConfig,
|
|
14
|
-
type WatchdogDeliveryMode,
|
|
15
10
|
type WatchdogEndpointConfig,
|
|
16
11
|
type WatchdogCadenceConfig,
|
|
17
12
|
type WatchdogGuidanceConfig,
|
|
18
|
-
type WatchdogLateWarningPolicy,
|
|
19
13
|
type WatchdogScopeConfig,
|
|
20
14
|
type WatchdogLspConfig,
|
|
15
|
+
type WatchdogRoleModelRule,
|
|
16
|
+
type WatchdogRulesConfig,
|
|
21
17
|
type WatchdogSettingsError,
|
|
22
18
|
type WatchdogSettingsResult,
|
|
23
19
|
type WatchdogSettingsSource,
|
|
24
20
|
type WatchdogSeverity,
|
|
25
|
-
type WatchdogSyncBacklog,
|
|
26
21
|
} from "./types.ts";
|
|
27
22
|
|
|
28
|
-
type WatchdogAutoFollowPatch = Partial<WatchdogAutoFollowConfig>;
|
|
29
23
|
type WatchdogGuidancePatch = Partial<WatchdogGuidanceConfig>;
|
|
30
24
|
type WatchdogScopePatch = Partial<WatchdogScopeConfig>;
|
|
31
25
|
type WatchdogCadencePatch = Partial<WatchdogCadenceConfig>;
|
|
32
26
|
type WatchdogEndpointPatch = Partial<WatchdogEndpointConfig>;
|
|
33
27
|
type WatchdogChildOverridePatch = Partial<WatchdogChildOverrideConfig>;
|
|
34
|
-
type WatchdogChildrenPatch = Partial<Omit<WatchdogChildrenConfig, "
|
|
35
|
-
autoFollow?: WatchdogAutoFollowPatch;
|
|
28
|
+
type WatchdogChildrenPatch = Partial<Omit<WatchdogChildrenConfig, "overrides">> & {
|
|
36
29
|
overrides?: Record<string, WatchdogChildOverridePatch>;
|
|
37
30
|
};
|
|
38
|
-
type WatchdogAsyncCompletionPatch = Partial<WatchdogAsyncCompletionConfig>;
|
|
39
31
|
type WatchdogLspPatch = Partial<WatchdogLspConfig>;
|
|
40
32
|
|
|
41
33
|
export type WatchdogSettingsWriteScope = "user" | "project";
|
|
@@ -52,14 +44,13 @@ export interface WatchdogModelSettingsWrite {
|
|
|
52
44
|
thinking?: ThinkingLevel | false | null;
|
|
53
45
|
}
|
|
54
46
|
|
|
55
|
-
type WatchdogConfigPatch = Partial<Omit<ResolvedWatchdogConfig, "guidance" | "
|
|
47
|
+
type WatchdogConfigPatch = Partial<Omit<ResolvedWatchdogConfig, "guidance" | "scope" | "cadence" | "main" | "children" | "lsp" | "rules">> & {
|
|
48
|
+
rules?: WatchdogRulesConfig;
|
|
56
49
|
guidance?: WatchdogGuidancePatch;
|
|
57
|
-
autoFollow?: WatchdogAutoFollowPatch;
|
|
58
50
|
scope?: WatchdogScopePatch;
|
|
59
51
|
cadence?: WatchdogCadencePatch;
|
|
60
52
|
main?: WatchdogEndpointPatch;
|
|
61
53
|
children?: WatchdogChildrenPatch;
|
|
62
|
-
asyncCompletion?: WatchdogAsyncCompletionPatch;
|
|
63
54
|
lsp?: WatchdogLspPatch;
|
|
64
55
|
};
|
|
65
56
|
|
|
@@ -70,22 +61,13 @@ interface ParseMeta {
|
|
|
70
61
|
|
|
71
62
|
export const DEFAULT_WATCHDOG_CONFIG: ResolvedWatchdogConfig = {
|
|
72
63
|
enabled: false,
|
|
73
|
-
delivery: "held",
|
|
74
|
-
showDuringRun: false,
|
|
75
|
-
syncBacklog: "off",
|
|
76
64
|
agentEndTimeoutMs: 30_000,
|
|
77
|
-
lateWarningPolicy: "show-stale-no-autofollow",
|
|
78
65
|
severityThreshold: "concern",
|
|
79
66
|
maxWarnings: null,
|
|
80
67
|
guidance: {
|
|
81
68
|
watchdogMd: true,
|
|
82
|
-
systemPromptPath: null,
|
|
83
|
-
},
|
|
84
|
-
autoFollow: {
|
|
85
|
-
blockers: true,
|
|
86
|
-
maxAttempts: 3,
|
|
87
|
-
stalemateRepeats: 3,
|
|
88
69
|
},
|
|
70
|
+
stalemateRepeats: 3,
|
|
89
71
|
scope: {
|
|
90
72
|
enabled: true,
|
|
91
73
|
},
|
|
@@ -98,73 +80,51 @@ export const DEFAULT_WATCHDOG_CONFIG: ResolvedWatchdogConfig = {
|
|
|
98
80
|
children: {
|
|
99
81
|
enabled: false,
|
|
100
82
|
watchdogTailTimeoutMs: 120_000,
|
|
101
|
-
autoFollow: {
|
|
102
|
-
blockers: true,
|
|
103
|
-
maxAttempts: 3,
|
|
104
|
-
stalemateRepeats: 3,
|
|
105
|
-
},
|
|
106
83
|
overrides: {},
|
|
107
84
|
},
|
|
108
|
-
asyncCompletion: {
|
|
109
|
-
enabled: false,
|
|
110
|
-
autoFollowBlockers: false,
|
|
111
|
-
},
|
|
112
85
|
lsp: {
|
|
113
86
|
enabled: true,
|
|
114
87
|
timeoutMs: 3_000,
|
|
115
88
|
maxFiles: 20,
|
|
116
89
|
maxDiagnostics: 50,
|
|
117
90
|
},
|
|
118
|
-
compactAtPercent: 80,
|
|
119
|
-
reviewRetryDelayMs: 1_000,
|
|
120
|
-
maxReviewFailures: 3,
|
|
121
91
|
};
|
|
122
92
|
|
|
123
93
|
const WATCHDOG_FIELDS = new Set([
|
|
124
94
|
"enabled",
|
|
125
|
-
"delivery",
|
|
126
|
-
"showDuringRun",
|
|
127
|
-
"syncBacklog",
|
|
128
95
|
"agentEndTimeoutMs",
|
|
129
|
-
"lateWarningPolicy",
|
|
130
96
|
"severityThreshold",
|
|
131
97
|
"maxWarnings",
|
|
132
98
|
"guidance",
|
|
133
|
-
"
|
|
99
|
+
"stalemateRepeats",
|
|
134
100
|
"scope",
|
|
135
101
|
"cadence",
|
|
136
102
|
"main",
|
|
137
103
|
"children",
|
|
138
|
-
"asyncCompletion",
|
|
139
104
|
"lsp",
|
|
140
|
-
"
|
|
141
|
-
"reviewRetryDelayMs",
|
|
142
|
-
"maxReviewFailures",
|
|
105
|
+
"rules",
|
|
143
106
|
]);
|
|
144
|
-
const
|
|
145
|
-
const
|
|
107
|
+
const RULES_FIELDS = new Set(["action", "roleModels"]);
|
|
108
|
+
const ROLE_MODEL_RULE_FIELDS = new Set(["allow", "deny", "note"]);
|
|
109
|
+
const GUIDANCE_FIELDS = new Set(["watchdogMd"]);
|
|
146
110
|
const SCOPE_FIELDS = new Set(["enabled"]);
|
|
147
111
|
const CADENCE_FIELDS = new Set(["everyNTools"]);
|
|
148
112
|
const ENDPOINT_FIELDS = new Set(["enabled", "model", "thinking"]);
|
|
149
|
-
const CHILDREN_FIELDS = new Set(["enabled", "model", "thinking", "watchdogTailTimeoutMs", "
|
|
150
|
-
const CHILD_OVERRIDE_FIELDS = new Set(["enabled", "model", "thinking"]);
|
|
151
|
-
const ASYNC_COMPLETION_FIELDS = new Set(["enabled", "autoFollowBlockers"]);
|
|
113
|
+
const CHILDREN_FIELDS = new Set(["enabled", "model", "thinking", "watchdogTailTimeoutMs", "cadence", "overrides"]);
|
|
114
|
+
const CHILD_OVERRIDE_FIELDS = new Set(["enabled", "model", "thinking", "cadence"]);
|
|
152
115
|
const LSP_FIELDS = new Set(["enabled", "timeoutMs", "maxFiles", "maxDiagnostics"]);
|
|
153
116
|
|
|
154
117
|
function cloneDefaultConfig(): ResolvedWatchdogConfig {
|
|
155
118
|
return {
|
|
156
119
|
...DEFAULT_WATCHDOG_CONFIG,
|
|
157
120
|
guidance: { ...DEFAULT_WATCHDOG_CONFIG.guidance },
|
|
158
|
-
autoFollow: { ...DEFAULT_WATCHDOG_CONFIG.autoFollow },
|
|
159
121
|
scope: { ...DEFAULT_WATCHDOG_CONFIG.scope },
|
|
160
122
|
cadence: { ...DEFAULT_WATCHDOG_CONFIG.cadence },
|
|
161
123
|
main: { ...DEFAULT_WATCHDOG_CONFIG.main },
|
|
162
124
|
children: {
|
|
163
125
|
...DEFAULT_WATCHDOG_CONFIG.children,
|
|
164
|
-
autoFollow: { ...DEFAULT_WATCHDOG_CONFIG.children.autoFollow },
|
|
165
126
|
overrides: {},
|
|
166
127
|
},
|
|
167
|
-
asyncCompletion: { ...DEFAULT_WATCHDOG_CONFIG.asyncCompletion },
|
|
168
128
|
lsp: { ...DEFAULT_WATCHDOG_CONFIG.lsp },
|
|
169
129
|
};
|
|
170
130
|
}
|
|
@@ -227,35 +187,11 @@ function parseEnum<T extends string>(value: unknown, field: string, meta: ParseM
|
|
|
227
187
|
throw invalid(meta, field, values.map((item) => `'${item}'`).join(" or "));
|
|
228
188
|
}
|
|
229
189
|
|
|
230
|
-
function parseSyncBacklog(value: unknown, field: string, meta: ParseMeta): WatchdogSyncBacklog {
|
|
231
|
-
if (value === "off") return "off";
|
|
232
|
-
return parseInteger(value, field, meta, "'off' or a positive integer", (candidate) => candidate >= 1);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
190
|
function parseGuidancePatch(value: unknown, field: string, meta: ParseMeta): WatchdogGuidancePatch {
|
|
236
191
|
const input = parseObject(value, field, meta);
|
|
237
192
|
assertKnownFields(input, GUIDANCE_FIELDS, field, meta);
|
|
238
193
|
const patch: WatchdogGuidancePatch = {};
|
|
239
194
|
if ("watchdogMd" in input) patch.watchdogMd = parseBoolean(input.watchdogMd, `${field}.watchdogMd`, meta);
|
|
240
|
-
if ("systemPromptPath" in input) {
|
|
241
|
-
patch.systemPromptPath = input.systemPromptPath === null
|
|
242
|
-
? null
|
|
243
|
-
: parseNonEmptyString(input.systemPromptPath, `${field}.systemPromptPath`, meta);
|
|
244
|
-
}
|
|
245
|
-
return patch;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function parseAutoFollowPatch(value: unknown, field: string, meta: ParseMeta): WatchdogAutoFollowPatch {
|
|
249
|
-
const input = parseObject(value, field, meta);
|
|
250
|
-
assertKnownFields(input, AUTO_FOLLOW_FIELDS, field, meta);
|
|
251
|
-
const patch: WatchdogAutoFollowPatch = {};
|
|
252
|
-
if ("blockers" in input) patch.blockers = parseBoolean(input.blockers, `${field}.blockers`, meta);
|
|
253
|
-
if ("maxAttempts" in input) {
|
|
254
|
-
patch.maxAttempts = parseNullableInteger(input.maxAttempts, `${field}.maxAttempts`, meta, "null or a positive integer", (candidate) => candidate >= 1);
|
|
255
|
-
}
|
|
256
|
-
if ("stalemateRepeats" in input) {
|
|
257
|
-
patch.stalemateRepeats = parseInteger(input.stalemateRepeats, `${field}.stalemateRepeats`, meta, "a positive integer", (candidate) => candidate >= 1);
|
|
258
|
-
}
|
|
259
195
|
return patch;
|
|
260
196
|
}
|
|
261
197
|
|
|
@@ -296,6 +232,7 @@ function parseChildOverridePatch(value: unknown, field: string, meta: ParseMeta)
|
|
|
296
232
|
if ("enabled" in input) patch.enabled = parseBoolean(input.enabled, `${field}.enabled`, meta);
|
|
297
233
|
if ("model" in input) patch.model = parseNonEmptyString(input.model, `${field}.model`, meta);
|
|
298
234
|
if ("thinking" in input) patch.thinking = parseThinking(input.thinking, `${field}.thinking`, meta);
|
|
235
|
+
if ("cadence" in input) patch.cadence = parseCadencePatch(input.cadence, `${field}.cadence`, meta);
|
|
299
236
|
return patch;
|
|
300
237
|
}
|
|
301
238
|
|
|
@@ -309,7 +246,7 @@ function parseChildrenPatch(value: unknown, field: string, meta: ParseMeta): Wat
|
|
|
309
246
|
if ("watchdogTailTimeoutMs" in input) {
|
|
310
247
|
patch.watchdogTailTimeoutMs = parseInteger(input.watchdogTailTimeoutMs, `${field}.watchdogTailTimeoutMs`, meta, "a positive integer", (candidate) => candidate >= 1);
|
|
311
248
|
}
|
|
312
|
-
if ("
|
|
249
|
+
if ("cadence" in input) patch.cadence = parseCadencePatch(input.cadence, `${field}.cadence`, meta);
|
|
313
250
|
if ("overrides" in input) {
|
|
314
251
|
const overrides = parseObject(input.overrides, `${field}.overrides`, meta);
|
|
315
252
|
patch.overrides = {};
|
|
@@ -321,15 +258,6 @@ function parseChildrenPatch(value: unknown, field: string, meta: ParseMeta): Wat
|
|
|
321
258
|
return patch;
|
|
322
259
|
}
|
|
323
260
|
|
|
324
|
-
function parseAsyncCompletionPatch(value: unknown, field: string, meta: ParseMeta): WatchdogAsyncCompletionPatch {
|
|
325
|
-
const input = parseObject(value, field, meta);
|
|
326
|
-
assertKnownFields(input, ASYNC_COMPLETION_FIELDS, field, meta);
|
|
327
|
-
const patch: WatchdogAsyncCompletionPatch = {};
|
|
328
|
-
if ("enabled" in input) patch.enabled = parseBoolean(input.enabled, `${field}.enabled`, meta);
|
|
329
|
-
if ("autoFollowBlockers" in input) patch.autoFollowBlockers = parseBoolean(input.autoFollowBlockers, `${field}.autoFollowBlockers`, meta);
|
|
330
|
-
return patch;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
261
|
function parseLspPatch(value: unknown, field: string, meta: ParseMeta): WatchdogLspPatch {
|
|
334
262
|
const input = parseObject(value, field, meta);
|
|
335
263
|
assertKnownFields(input, LSP_FIELDS, field, meta);
|
|
@@ -341,20 +269,43 @@ function parseLspPatch(value: unknown, field: string, meta: ParseMeta): Watchdog
|
|
|
341
269
|
return patch;
|
|
342
270
|
}
|
|
343
271
|
|
|
272
|
+
function parseStringList(value: unknown, field: string, meta: ParseMeta): string[] {
|
|
273
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || !entry.trim())) throw invalid(meta, field, "an array of non-empty strings");
|
|
274
|
+
return value.map((entry: string) => entry.trim());
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function parseRoleModelRule(value: unknown, field: string, meta: ParseMeta): WatchdogRoleModelRule {
|
|
278
|
+
const input = parseObject(value, field, meta);
|
|
279
|
+
assertKnownFields(input, ROLE_MODEL_RULE_FIELDS, field, meta);
|
|
280
|
+
const rule: WatchdogRoleModelRule = {};
|
|
281
|
+
if ("allow" in input) rule.allow = parseStringList(input.allow, `${field}.allow`, meta);
|
|
282
|
+
if ("deny" in input) rule.deny = parseStringList(input.deny, `${field}.deny`, meta);
|
|
283
|
+
if ("note" in input) rule.note = parseNonEmptyString(input.note, `${field}.note`, meta);
|
|
284
|
+
return rule;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function parseRules(value: unknown, field: string, meta: ParseMeta): WatchdogRulesConfig {
|
|
288
|
+
const input = parseObject(value, field, meta);
|
|
289
|
+
assertKnownFields(input, RULES_FIELDS, field, meta);
|
|
290
|
+
const rules: WatchdogRulesConfig = { action: "warn", roleModels: {} };
|
|
291
|
+
if ("action" in input) rules.action = parseEnum(input.action, `${field}.action`, meta, ["warn", "block"] as const);
|
|
292
|
+
if ("roleModels" in input) {
|
|
293
|
+
for (const [agent, rule] of Object.entries(parseObject(input.roleModels, `${field}.roleModels`, meta))) {
|
|
294
|
+
if (!agent.trim()) throw invalid(meta, `${field}.roleModels`, "agent names to be non-empty");
|
|
295
|
+
rules.roleModels[agent] = parseRoleModelRule(rule, `${field}.roleModels.${agent}`, meta);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return rules;
|
|
299
|
+
}
|
|
300
|
+
|
|
344
301
|
function parseWatchdogPatch(value: unknown, field: string, meta: ParseMeta): WatchdogConfigPatch {
|
|
345
302
|
const input = parseObject(value, field, meta);
|
|
346
303
|
assertKnownFields(input, WATCHDOG_FIELDS, field, meta);
|
|
347
304
|
const patch: WatchdogConfigPatch = {};
|
|
348
305
|
if ("enabled" in input) patch.enabled = parseBoolean(input.enabled, `${field}.enabled`, meta);
|
|
349
|
-
if ("delivery" in input) patch.delivery = parseEnum<WatchdogDeliveryMode>(input.delivery, `${field}.delivery`, meta, WATCHDOG_DELIVERY_MODES);
|
|
350
|
-
if ("showDuringRun" in input) patch.showDuringRun = parseBoolean(input.showDuringRun, `${field}.showDuringRun`, meta);
|
|
351
|
-
if ("syncBacklog" in input) patch.syncBacklog = parseSyncBacklog(input.syncBacklog, `${field}.syncBacklog`, meta);
|
|
352
306
|
if ("agentEndTimeoutMs" in input) {
|
|
353
307
|
patch.agentEndTimeoutMs = parseInteger(input.agentEndTimeoutMs, `${field}.agentEndTimeoutMs`, meta, "a positive integer", (candidate) => candidate >= 1);
|
|
354
308
|
}
|
|
355
|
-
if ("lateWarningPolicy" in input) {
|
|
356
|
-
patch.lateWarningPolicy = parseEnum<WatchdogLateWarningPolicy>(input.lateWarningPolicy, `${field}.lateWarningPolicy`, meta, WATCHDOG_LATE_WARNING_POLICIES);
|
|
357
|
-
}
|
|
358
309
|
if ("severityThreshold" in input) {
|
|
359
310
|
patch.severityThreshold = parseEnum<WatchdogSeverity>(input.severityThreshold, `${field}.severityThreshold`, meta, WATCHDOG_WARNING_SEVERITIES);
|
|
360
311
|
}
|
|
@@ -362,22 +313,15 @@ function parseWatchdogPatch(value: unknown, field: string, meta: ParseMeta): Wat
|
|
|
362
313
|
patch.maxWarnings = parseNullableInteger(input.maxWarnings, `${field}.maxWarnings`, meta, "null or a non-negative integer", (candidate) => candidate >= 0);
|
|
363
314
|
}
|
|
364
315
|
if ("guidance" in input) patch.guidance = parseGuidancePatch(input.guidance, `${field}.guidance`, meta);
|
|
365
|
-
if ("
|
|
316
|
+
if ("stalemateRepeats" in input) {
|
|
317
|
+
patch.stalemateRepeats = parseInteger(input.stalemateRepeats, `${field}.stalemateRepeats`, meta, "a positive integer", (candidate) => candidate >= 1);
|
|
318
|
+
}
|
|
366
319
|
if ("scope" in input) patch.scope = parseScopePatch(input.scope, `${field}.scope`, meta);
|
|
367
320
|
if ("cadence" in input) patch.cadence = parseCadencePatch(input.cadence, `${field}.cadence`, meta);
|
|
368
321
|
if ("main" in input) patch.main = parseEndpointPatch(input.main, `${field}.main`, meta);
|
|
369
322
|
if ("children" in input) patch.children = parseChildrenPatch(input.children, `${field}.children`, meta);
|
|
370
|
-
if ("asyncCompletion" in input) patch.asyncCompletion = parseAsyncCompletionPatch(input.asyncCompletion, `${field}.asyncCompletion`, meta);
|
|
371
323
|
if ("lsp" in input) patch.lsp = parseLspPatch(input.lsp, `${field}.lsp`, meta);
|
|
372
|
-
if ("
|
|
373
|
-
patch.compactAtPercent = parseInteger(input.compactAtPercent, `${field}.compactAtPercent`, meta, "an integer from 50 through 95", (candidate) => candidate >= 50 && candidate <= 95);
|
|
374
|
-
}
|
|
375
|
-
if ("reviewRetryDelayMs" in input) {
|
|
376
|
-
patch.reviewRetryDelayMs = parseInteger(input.reviewRetryDelayMs, `${field}.reviewRetryDelayMs`, meta, "a positive integer", (candidate) => candidate >= 1);
|
|
377
|
-
}
|
|
378
|
-
if ("maxReviewFailures" in input) {
|
|
379
|
-
patch.maxReviewFailures = parseInteger(input.maxReviewFailures, `${field}.maxReviewFailures`, meta, "a positive integer", (candidate) => candidate >= 1);
|
|
380
|
-
}
|
|
324
|
+
if ("rules" in input) patch.rules = parseRules(input.rules, `${field}.rules`, meta);
|
|
381
325
|
return patch;
|
|
382
326
|
}
|
|
383
327
|
|
package/src/watchdog/types.ts
CHANGED
|
@@ -18,7 +18,7 @@ export type WatchdogCategory = typeof WATCHDOG_WARNING_CATEGORIES[number];
|
|
|
18
18
|
export const WATCHDOG_WARNING_CONFIDENCES = ["medium", "high"] as const;
|
|
19
19
|
export type WatchdogConfidence = typeof WATCHDOG_WARNING_CONFIDENCES[number];
|
|
20
20
|
|
|
21
|
-
export const WATCHDOG_WARNING_SOURCES = ["main", "child", "
|
|
21
|
+
export const WATCHDOG_WARNING_SOURCES = ["main", "child", "lsp"] as const;
|
|
22
22
|
export type WatchdogWarningSource = typeof WATCHDOG_WARNING_SOURCES[number];
|
|
23
23
|
|
|
24
24
|
export const WATCHDOG_LSP_DIAGNOSTIC_SEVERITIES = ["error", "warning", "info", "hint"] as const;
|
|
@@ -42,14 +42,6 @@ export const WATCHDOG_WARNING_STATES = [
|
|
|
42
42
|
] as const;
|
|
43
43
|
export type WatchdogWarningState = typeof WATCHDOG_WARNING_STATES[number];
|
|
44
44
|
|
|
45
|
-
export const WATCHDOG_LATE_WARNING_POLICIES = ["show-stale-no-autofollow"] as const;
|
|
46
|
-
export type WatchdogLateWarningPolicy = typeof WATCHDOG_LATE_WARNING_POLICIES[number];
|
|
47
|
-
|
|
48
|
-
export const WATCHDOG_DELIVERY_MODES = ["held"] as const;
|
|
49
|
-
export type WatchdogDeliveryMode = typeof WATCHDOG_DELIVERY_MODES[number];
|
|
50
|
-
|
|
51
|
-
export type WatchdogSyncBacklog = "off" | number;
|
|
52
|
-
|
|
53
45
|
export interface WatchdogWarning {
|
|
54
46
|
severity: WatchdogSeverity;
|
|
55
47
|
summary: string;
|
|
@@ -61,7 +53,6 @@ export interface WatchdogWarning {
|
|
|
61
53
|
agent?: string;
|
|
62
54
|
runId?: string;
|
|
63
55
|
stale?: boolean;
|
|
64
|
-
autoFollowAttempt?: number;
|
|
65
56
|
state?: WatchdogWarningState;
|
|
66
57
|
}
|
|
67
58
|
|
|
@@ -81,15 +72,8 @@ export interface WatchdogWarningMessage {
|
|
|
81
72
|
details: WatchdogWarningDetails;
|
|
82
73
|
}
|
|
83
74
|
|
|
84
|
-
export interface WatchdogAutoFollowConfig {
|
|
85
|
-
blockers: boolean;
|
|
86
|
-
maxAttempts: number | null;
|
|
87
|
-
stalemateRepeats: number;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
75
|
export interface WatchdogGuidanceConfig {
|
|
91
76
|
watchdogMd: boolean;
|
|
92
|
-
systemPromptPath: string | null;
|
|
93
77
|
}
|
|
94
78
|
|
|
95
79
|
export interface WatchdogScopeConfig {
|
|
@@ -110,19 +94,16 @@ export interface WatchdogChildOverrideConfig {
|
|
|
110
94
|
enabled?: boolean;
|
|
111
95
|
model?: string;
|
|
112
96
|
thinking?: string | false;
|
|
97
|
+
cadence?: Partial<WatchdogCadenceConfig>;
|
|
113
98
|
}
|
|
114
99
|
|
|
115
100
|
export interface WatchdogChildrenConfig extends WatchdogEndpointConfig {
|
|
116
101
|
watchdogTailTimeoutMs: number;
|
|
117
|
-
|
|
102
|
+
/** Mid-run cadence for child watchdogs; defaults to the top-level cadence. */
|
|
103
|
+
cadence?: Partial<WatchdogCadenceConfig>;
|
|
118
104
|
overrides: Record<string, WatchdogChildOverrideConfig>;
|
|
119
105
|
}
|
|
120
106
|
|
|
121
|
-
export interface WatchdogAsyncCompletionConfig {
|
|
122
|
-
enabled: boolean;
|
|
123
|
-
autoFollowBlockers: boolean;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
107
|
export interface WatchdogLspConfig {
|
|
127
108
|
enabled: boolean;
|
|
128
109
|
timeoutMs: number;
|
|
@@ -156,26 +137,31 @@ export interface WatchdogLspRuntimeSnapshot extends WatchdogLspResult {
|
|
|
156
137
|
updatedAt?: string;
|
|
157
138
|
}
|
|
158
139
|
|
|
140
|
+
export interface WatchdogRoleModelRule {
|
|
141
|
+
allow?: string[];
|
|
142
|
+
deny?: string[];
|
|
143
|
+
note?: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface WatchdogRulesConfig {
|
|
147
|
+
action: "warn" | "block";
|
|
148
|
+
roleModels: Record<string, WatchdogRoleModelRule>;
|
|
149
|
+
}
|
|
150
|
+
|
|
159
151
|
export interface ResolvedWatchdogConfig {
|
|
160
152
|
enabled: boolean;
|
|
161
|
-
delivery: WatchdogDeliveryMode;
|
|
162
|
-
showDuringRun: boolean;
|
|
163
|
-
syncBacklog: WatchdogSyncBacklog;
|
|
164
153
|
agentEndTimeoutMs: number;
|
|
165
|
-
lateWarningPolicy: WatchdogLateWarningPolicy;
|
|
166
154
|
severityThreshold: WatchdogSeverity;
|
|
167
155
|
maxWarnings: number | null;
|
|
168
156
|
guidance: WatchdogGuidanceConfig;
|
|
169
|
-
|
|
157
|
+
/** Consecutive identical boundary warnings before the watchdog stops continuing the run. */
|
|
158
|
+
stalemateRepeats: number;
|
|
170
159
|
scope: WatchdogScopeConfig;
|
|
171
160
|
cadence: WatchdogCadenceConfig;
|
|
172
161
|
main: WatchdogEndpointConfig;
|
|
173
162
|
children: WatchdogChildrenConfig;
|
|
174
|
-
asyncCompletion: WatchdogAsyncCompletionConfig;
|
|
175
163
|
lsp: WatchdogLspConfig;
|
|
176
|
-
|
|
177
|
-
reviewRetryDelayMs: number;
|
|
178
|
-
maxReviewFailures: number;
|
|
164
|
+
rules?: WatchdogRulesConfig;
|
|
179
165
|
}
|
|
180
166
|
|
|
181
167
|
export interface WatchdogSettingsError {
|
|
@@ -44,7 +44,6 @@ export function formatWatchdogWarningContent(warning: WatchdogWarning): string {
|
|
|
44
44
|
tag("run_id", details.runId),
|
|
45
45
|
tag("state", details.state),
|
|
46
46
|
tag("stale", details.stale),
|
|
47
|
-
tag("auto_follow_attempt", details.autoFollowAttempt),
|
|
48
47
|
];
|
|
49
48
|
return [
|
|
50
49
|
`<subagent_watchdog ${attrs.join(" ")}>`,
|
|
@@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { Details, WorkflowPreflightLaneV1, WorkflowPreflightV1 } from "../shared/types.ts";
|
|
5
|
+
import { workflowPreflightLaneForRuntimeKey } from "./workflow-preflight.ts";
|
|
5
6
|
|
|
6
7
|
export const WORKFLOW_CHAT_PROGRESS_MODES = ["auto", "off", "live-card"] as const;
|
|
7
8
|
export type WorkflowChatProgressMode = typeof WORKFLOW_CHAT_PROGRESS_MODES[number];
|
|
@@ -111,7 +112,6 @@ function cleanLabel(value: unknown): string | undefined {
|
|
|
111
112
|
|
|
112
113
|
export function buildWorkflowChatProgressRows(trace: NonNullable<Details["workflow"]>["trace"], preflight?: WorkflowPreflightV1): WorkflowChatProgressRow[] {
|
|
113
114
|
const rows = new Map<string, WorkflowChatProgressRow>();
|
|
114
|
-
for (const lane of preflight?.lanes ?? []) rows.set(lane.key, { key: lane.key, state: "planned", preflight: lane });
|
|
115
115
|
for (const entry of trace) {
|
|
116
116
|
if (entry.operation !== "run") continue;
|
|
117
117
|
const existing = rows.get(entry.key);
|
|
@@ -124,8 +124,9 @@ export function buildWorkflowChatProgressRows(trace: NonNullable<Details["workfl
|
|
|
124
124
|
}
|
|
125
125
|
continue;
|
|
126
126
|
}
|
|
127
|
+
const lane = workflowPreflightLaneForRuntimeKey(preflight, entry.key, [entry.generatedLaneKey]);
|
|
127
128
|
const next: WorkflowChatProgressRow = existing ?? { key: entry.key, state: "running" };
|
|
128
|
-
if (
|
|
129
|
+
if (lane && !next.preflight) next.preflight = lane;
|
|
129
130
|
next.state = entry.state === "completed"
|
|
130
131
|
? "complete"
|
|
131
132
|
: entry.state === "failed"
|
|
@@ -9,7 +9,14 @@ import type { AcceptanceRecoveryMetadata, HostStepNodeV1, SingleResult } from ".
|
|
|
9
9
|
import { normalizeWorkflowHostCommandParams, type WorkflowHostCommandParams, type WorkflowHostCommandResult } from "./host-command.ts";
|
|
10
10
|
|
|
11
11
|
const KEY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
12
|
+
function validGitRef(ref: unknown): ref is string {
|
|
13
|
+
if (typeof ref !== "string" || !ref || ref === "@" || Buffer.byteLength(ref, "utf-8") > 1024 || ref.startsWith("/") || ref.endsWith("/") || ref.includes("//") || ref.includes("..") || ref.includes("@{")) return false;
|
|
14
|
+
if (/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/iu.test(ref)) return false;
|
|
15
|
+
if (/[[\]\\~^:?*\u0000-\u0020\u007f]/u.test(ref) || ref.endsWith(".") || ref.endsWith(".lock")) return false;
|
|
16
|
+
return ref.split("/").every((component) => component.length > 0 && component !== "." && component !== ".." && !component.startsWith(".") && !component.endsWith(".") && !component.endsWith(".lock"));
|
|
17
|
+
}
|
|
12
18
|
const requireFromPackage = createRequire(import.meta.url);
|
|
19
|
+
const WORKFLOW_ASSEMBLY_FLUSH_TIMEOUT_MS = 5_000;
|
|
13
20
|
|
|
14
21
|
export interface WorkflowScriptValidationError {
|
|
15
22
|
message: string;
|
|
@@ -49,6 +56,12 @@ let suppressNativePromiseConsumption = 0;
|
|
|
49
56
|
const activeNativePromises = [];
|
|
50
57
|
const pending = new Map();
|
|
51
58
|
const runKeyPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
59
|
+
function validGitRef(ref) {
|
|
60
|
+
if (typeof ref !== "string" || !ref || ref === "@" || new TextEncoder().encode(ref).length > 1024 || ref.startsWith("/") || ref.endsWith("/") || ref.includes("//") || ref.includes("..") || ref.includes("@{")) return false;
|
|
61
|
+
if (/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/iu.test(ref)) return false;
|
|
62
|
+
if (/[[\]\\~^:?*\u0000-\u0020\u007f]/u.test(ref) || ref.endsWith(".") || ref.endsWith(".lock")) return false;
|
|
63
|
+
return ref.split("/").every((component) => component.length > 0 && component !== "." && component !== ".." && !component.startsWith(".") && !component.endsWith(".") && !component.endsWith(".lock"));
|
|
64
|
+
}
|
|
52
65
|
const trackedPromiseTrackers = new WeakMap();
|
|
53
66
|
const trackedPromiseTargets = new WeakMap();
|
|
54
67
|
let nativePromiseTrackers = new WeakMap();
|
|
@@ -611,6 +624,7 @@ function validateRunCall(key, params, label, fingerprints) {
|
|
|
611
624
|
}
|
|
612
625
|
if (Object.prototype.hasOwnProperty.call(params, "clarify")) throw new Error(label + " does not support clarify UI.");
|
|
613
626
|
if (params.worktree !== undefined && typeof params.worktree !== "boolean") throw new Error(label + " worktree must be true or false.");
|
|
627
|
+
if (params.baseRef !== undefined && (typeof params.baseRef !== "string" || !validGitRef(params.baseRef))) throw new Error(label + " baseRef must be a valid Git ref.");
|
|
614
628
|
validateLaneMetadata(params.lane, label + " lane", key);
|
|
615
629
|
if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) throw new Error(label + " gate must be a non-empty command string.");
|
|
616
630
|
if (params.gate !== undefined && params.acceptance !== undefined) throw new Error(label + " gate cannot be combined with acceptance; use one gate command or acceptance.verify.");
|
|
@@ -1070,6 +1084,8 @@ export interface RunWorkflowScriptOptions {
|
|
|
1070
1084
|
oneUsePermit?: { claim: (key: string) => string | undefined };
|
|
1071
1085
|
timeoutMs?: number;
|
|
1072
1086
|
signal?: AbortSignal;
|
|
1087
|
+
/** Let an async workflow flush pure result assembly after reload once every child is terminal. */
|
|
1088
|
+
continueAfterAbortWhenChildrenSettled?: (abortError: Error) => boolean;
|
|
1073
1089
|
/** Maximum children executing concurrently within this workflow. Defaults to 20. */
|
|
1074
1090
|
globalConcurrencyLimit?: number;
|
|
1075
1091
|
admit?: (calls: Array<{ key: string; params: Record<string, unknown> }>) => void | Promise<void>;
|
|
@@ -1614,7 +1630,7 @@ function resolveWorkflowParserEntry(): string {
|
|
|
1614
1630
|
}
|
|
1615
1631
|
}
|
|
1616
1632
|
|
|
1617
|
-
const AUTO_RESUME_PARAM_KEYS = ["acceptance", "agentContract", "index", "intercomBridge", "label", "lane", "maxRuntimeMs", "output", "outputMode", "outputSchema", "phase", "skill", "skills", "task", "timeoutMs", "toolBudget", "worktree"] as const;
|
|
1633
|
+
const AUTO_RESUME_PARAM_KEYS = ["acceptance", "agentContract", "baseRef", "index", "intercomBridge", "label", "lane", "maxRuntimeMs", "output", "outputMode", "outputSchema", "phase", "skill", "skills", "task", "timeoutMs", "toolBudget", "worktree"] as const;
|
|
1618
1634
|
|
|
1619
1635
|
function isZeroUsage(usage: unknown): boolean {
|
|
1620
1636
|
if (!isRecord(usage)) return false;
|
|
@@ -1682,6 +1698,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1682
1698
|
let acceptanceRecoveryBarrier: { key: string } | undefined;
|
|
1683
1699
|
let settled = false;
|
|
1684
1700
|
let finishing = false;
|
|
1701
|
+
let assemblyAbortRequested = false;
|
|
1702
|
+
let assemblyFlushTimer: ReturnType<typeof setTimeout> | undefined;
|
|
1703
|
+
let abortError: Error | undefined;
|
|
1685
1704
|
|
|
1686
1705
|
const partial = (): Omit<WorkflowScriptResult, "value"> => ({ emits, console: consoleEntries, trace, children: childOrder.flatMap((key) => {
|
|
1687
1706
|
const child = children.get(key);
|
|
@@ -1750,6 +1769,10 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1750
1769
|
const finish = (outcome: { value: unknown } | { error: Error & { workflowErrorKind?: unknown } }) => {
|
|
1751
1770
|
if (settled || finishing) return;
|
|
1752
1771
|
finishing = true;
|
|
1772
|
+
if (assemblyFlushTimer !== undefined) {
|
|
1773
|
+
clearTimeout(assemblyFlushTimer);
|
|
1774
|
+
assemblyFlushTimer = undefined;
|
|
1775
|
+
}
|
|
1753
1776
|
childController.abort("error" in outcome ? outcome.error : new Error("Workflow script completed."));
|
|
1754
1777
|
void Promise.allSettled([...steers.values(), ...hostCalls.values()].map(({ promise }) => promise)).then(() => {
|
|
1755
1778
|
if (settled) return;
|
|
@@ -1781,6 +1804,26 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1781
1804
|
: typeof signalReason === "string"
|
|
1782
1805
|
? new Error(signalReason)
|
|
1783
1806
|
: new Error("Workflow script aborted.");
|
|
1807
|
+
if (finishing) return;
|
|
1808
|
+
abortError = error;
|
|
1809
|
+
const allChildrenSettled = launches.size > 0
|
|
1810
|
+
&& [...launches.keys()].every((key) => children.has(key));
|
|
1811
|
+
let mayFlushAssembly = false;
|
|
1812
|
+
try {
|
|
1813
|
+
mayFlushAssembly = options.continueAfterAbortWhenChildrenSettled?.(error) === true;
|
|
1814
|
+
} catch (callbackError) {
|
|
1815
|
+
const callbackMessage = callbackError instanceof Error ? callbackError.message : String(callbackError);
|
|
1816
|
+
return finish({ error: new Error(`Workflow assembly flush eligibility failed: ${callbackMessage}`) });
|
|
1817
|
+
}
|
|
1818
|
+
if (mayFlushAssembly && allChildrenSettled) {
|
|
1819
|
+
// A reloaded async workflow may already be past its last child launch.
|
|
1820
|
+
// Keep the worker alive for pure result assembly, but abort the child
|
|
1821
|
+
// signal so any later launch or side effect cannot use stale context.
|
|
1822
|
+
assemblyAbortRequested = true;
|
|
1823
|
+
childController.abort(error);
|
|
1824
|
+
assemblyFlushTimer = setTimeout(() => finish({ error }), WORKFLOW_ASSEMBLY_FLUSH_TIMEOUT_MS);
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1784
1827
|
for (const key of launches.keys()) {
|
|
1785
1828
|
if (children.has(key)) continue;
|
|
1786
1829
|
stoppedLaunches.add(key);
|
|
@@ -1873,6 +1916,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
1873
1916
|
return;
|
|
1874
1917
|
}
|
|
1875
1918
|
if (message.type !== "call" || typeof message.callId !== "number" || typeof message.method !== "string" || !isRecord(message.args)) return;
|
|
1919
|
+
if (assemblyAbortRequested) return finish({ error: abortError ?? new Error("Workflow context was replaced or reloaded.") });
|
|
1876
1920
|
|
|
1877
1921
|
const respond = (promise: Promise<unknown>, responsePath?: string, onBoundaryError?: (error: unknown) => void) => {
|
|
1878
1922
|
void promise.then(
|
|
@@ -2059,6 +2103,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
2059
2103
|
if (params.worktree !== undefined && typeof params.worktree !== "boolean") {
|
|
2060
2104
|
return respond(Promise.reject(new Error(`runs.run('${key}') worktree must be true or false.`)));
|
|
2061
2105
|
}
|
|
2106
|
+
if (params.baseRef !== undefined && (typeof params.baseRef !== "string" || !validGitRef(params.baseRef))) {
|
|
2107
|
+
return respond(Promise.reject(new Error(`runs.run('${key}') baseRef must be a valid Git ref.`)));
|
|
2108
|
+
}
|
|
2062
2109
|
if (params.gate !== undefined && (typeof params.gate !== "string" || !params.gate.trim())) {
|
|
2063
2110
|
return respond(Promise.reject(new Error(`runs.run('${key}') gate must be a non-empty command string.`)));
|
|
2064
2111
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AsyncJobStep, HostStepNodeV1, WorkflowGraphNode, WorkflowGraphSnapshot, WorkflowPreflightLaneV1, WorkflowPreflightV1 } from "../shared/types.ts";
|
|
2
2
|
import { sanitizeDisplayText } from "../shared/display-text.ts";
|
|
3
|
+
import { workflowPreflightLaneForRuntimeKey } from "./workflow-preflight.ts";
|
|
3
4
|
|
|
4
5
|
export type WorkflowChecklistState = "complete" | "running" | "queued" | "blocked" | "failed" | "paused" | "stopped";
|
|
5
6
|
|
|
@@ -189,8 +190,8 @@ function duration(step: Pick<WorkflowChecklistStep, "durationMs" | "startedAt" |
|
|
|
189
190
|
return end === undefined ? undefined : Math.max(0, end - startedAt);
|
|
190
191
|
}
|
|
191
192
|
|
|
192
|
-
function laneFor(
|
|
193
|
-
return
|
|
193
|
+
function laneFor(preflight: WorkflowPreflightV1 | undefined, key: string, preferredKeys: readonly (string | undefined)[] = []): WorkflowPreflightLaneV1 | undefined {
|
|
194
|
+
return workflowPreflightLaneForRuntimeKey(preflight, key, preferredKeys);
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
function stepKey(step: WorkflowChecklistStep): string | undefined {
|
|
@@ -264,7 +265,7 @@ function traceSources(trace: readonly WorkflowChecklistTraceEntry[] | undefined)
|
|
|
264
265
|
}
|
|
265
266
|
|
|
266
267
|
function traceItem(entry: WorkflowChecklistTraceEntry, index: number, preflight: WorkflowPreflightLaneV1 | undefined): WorkflowChecklistItem {
|
|
267
|
-
const phase = keyText(entry.generatedLaneKey ?? entry.phase
|
|
268
|
+
const phase = keyText(preflight?.key ?? entry.generatedLaneKey ?? entry.phase, "Workflow");
|
|
268
269
|
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);
|
|
269
270
|
if (entry.operation === "host") item.kind = "host";
|
|
270
271
|
return item;
|
|
@@ -317,16 +318,13 @@ function finalize(phase: WorkflowChecklistPhase): void {
|
|
|
317
318
|
|
|
318
319
|
export function projectWorkflowChecklist(input: WorkflowChecklistInput): WorkflowChecklistProjection {
|
|
319
320
|
const phases = new Map<string, WorkflowChecklistPhase>();
|
|
320
|
-
const lanes = new Map((input.preflight?.lanes ?? []).map((lane) => [lane.key, lane]));
|
|
321
321
|
const steps = (input.steps ?? []) as readonly WorkflowChecklistStep[];
|
|
322
322
|
const nodes = graphNodes(input.graph);
|
|
323
323
|
const trace = traceSources(input.trace);
|
|
324
324
|
const traceByKey = new Map(trace.map((entry) => [entry.key, entry]));
|
|
325
325
|
const phaseByNode = new Map<string, string>();
|
|
326
|
-
const graphPhaseLabels = new Set<string>();
|
|
327
326
|
for (const phase of input.graph?.phases ?? []) {
|
|
328
327
|
const title = keyText(phase.title, "Workflow");
|
|
329
|
-
graphPhaseLabels.add(title);
|
|
330
328
|
phaseFor(phases, title);
|
|
331
329
|
for (const nodeId of phase.nodeIds) if (!phaseByNode.has(nodeId)) phaseByNode.set(nodeId, title);
|
|
332
330
|
}
|
|
@@ -356,28 +354,28 @@ export function projectWorkflowChecklist(input: WorkflowChecklistInput): Workflo
|
|
|
356
354
|
if (matches.length) {
|
|
357
355
|
for (const match of matches) {
|
|
358
356
|
usedSteps.add(match.index);
|
|
359
|
-
add(phases, phase, applyNow(mergeNodeStep(node, match.step, phase, traceByKey.get(stepKey(match.step) ?? node.id), laneFor(node.id, phase
|
|
357
|
+
add(phases, phase, applyNow(mergeNodeStep(node, match.step, phase, traceByKey.get(stepKey(match.step) ?? node.id), laneFor(input.preflight, node.id, [phase])), input.now));
|
|
360
358
|
}
|
|
361
359
|
continue;
|
|
362
360
|
}
|
|
363
|
-
add(phases, phase, applyNow(mergeNodeStep(node, { key: node.id, label: node.label, phase, agent: node.agent, status: node.status, outputName: node.outputName, error: node.error, acceptance: node.acceptanceStatus ? { status: node.acceptanceStatus } : undefined }, phase, traceByKey.get(node.id), laneFor(node.id, phase
|
|
361
|
+
add(phases, phase, applyNow(mergeNodeStep(node, { key: node.id, label: node.label, phase, agent: node.agent, status: node.status, outputName: node.outputName, error: node.error, acceptance: node.acceptanceStatus ? { status: node.acceptanceStatus } : undefined }, phase, traceByKey.get(node.id), laneFor(input.preflight, node.id, [phase])), input.now));
|
|
364
362
|
}
|
|
365
363
|
|
|
366
364
|
for (const [index, step] of steps.entries()) {
|
|
367
365
|
if (usedSteps.has(index)) continue;
|
|
368
366
|
const key = keyText(stepKey(step), `step-${index + 1}`);
|
|
369
367
|
const traceEntry = traceByKey.get(key);
|
|
370
|
-
const
|
|
371
|
-
|
|
368
|
+
const lane = laneFor(input.preflight, key, [step.phase, traceEntry?.generatedLaneKey, traceEntry?.phase]);
|
|
369
|
+
const phase = keyText(lane?.key ?? step.phase ?? traceEntry?.generatedLaneKey ?? traceEntry?.phase, "Workflow");
|
|
370
|
+
add(phases, phase, applyNow(stepItem(step, index, phase, key, step.label ?? step.key ?? step.agent, lane), input.now));
|
|
372
371
|
}
|
|
373
372
|
|
|
374
373
|
for (const host of hostById.values()) add(phases, keyText(host.label, "Host"), hostItem(host, keyText(host.label, "Host")));
|
|
375
374
|
for (const entry of trace) {
|
|
376
375
|
if (graphKeys.has(entry.key) || stepKeys.has(entry.key) || hostKeys.has(entry.key)) continue;
|
|
377
|
-
const item = traceItem(entry, phases.size, laneFor(entry.key, entry.generatedLaneKey
|
|
376
|
+
const item = traceItem(entry, phases.size, laneFor(input.preflight, entry.key, [entry.generatedLaneKey, entry.phase]));
|
|
378
377
|
add(phases, item.phase, item);
|
|
379
378
|
}
|
|
380
|
-
for (const lane of input.preflight?.lanes ?? []) if (!graphKeys.has(lane.key) && !graphPhaseLabels.has(lane.key) && !phases.get(lane.key)?.items.length) add(phases, lane.key, { key: lane.key, label: lane.key, phase: lane.key, state: "queued", preflight: lane });
|
|
381
379
|
|
|
382
380
|
const finalized = [...phases.values()].filter((phase) => phase.items.length > 0);
|
|
383
381
|
for (const phase of finalized) {
|