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.
Files changed (107) hide show
  1. package/CHANGELOG.md +57 -1
  2. package/README.md +2 -2
  3. package/agents/reviewer.md +1 -1
  4. package/agents/scout.md +1 -1
  5. package/docs/agents.md +18 -16
  6. package/docs/configuration.md +7 -15
  7. package/docs/extension-api.md +15 -6
  8. package/docs/missions.md +2 -0
  9. package/docs/observability.md +10 -12
  10. package/docs/tool-reference.md +7 -6
  11. package/docs/watchdog.md +92 -114
  12. package/docs/workflows.md +5 -3
  13. package/package.json +3 -4
  14. package/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  15. package/skills/pi-subagents/references/execution-controls.md +6 -5
  16. package/src/agents/agent-management.ts +47 -15
  17. package/src/api/capability-ceiling.ts +0 -1
  18. package/src/api/{pi-args.ts → child-tool-plan.ts} +1 -1
  19. package/src/api/preflight.ts +2 -3
  20. package/src/extension/doctor.ts +2 -10
  21. package/src/extension/fanout-child.ts +9 -11
  22. package/src/extension/index.ts +27 -5
  23. package/src/extension/public-execution.ts +14 -0
  24. package/src/extension/rpc.ts +3 -2
  25. package/src/extension/schemas.ts +2 -1
  26. package/src/extension/tool-description.ts +9 -8
  27. package/src/intercom/native-supervisor-channel.ts +138 -60
  28. package/src/intercom/supervisor-ui.ts +243 -0
  29. package/src/runs/background/async-execution.ts +46 -24
  30. package/src/runs/background/async-job-tracker.ts +11 -0
  31. package/src/runs/background/async-resume.ts +11 -2
  32. package/src/runs/background/control-channel.ts +2 -204
  33. package/src/runs/background/notify.ts +41 -2
  34. package/src/runs/background/process-terminal.ts +1 -1
  35. package/src/runs/background/run-child-session.ts +613 -0
  36. package/src/runs/background/run-status.ts +0 -1
  37. package/src/runs/background/runner-aliases.ts +125 -0
  38. package/src/runs/background/runner-child-sessions.ts +31 -0
  39. package/src/runs/background/scheduled-runs.ts +18 -4
  40. package/src/runs/background/subagent-runner.ts +203 -898
  41. package/src/runs/foreground/async-steering-action.ts +1 -17
  42. package/src/runs/foreground/execution.ts +196 -378
  43. package/src/runs/foreground/foreground-control.ts +4 -0
  44. package/src/runs/foreground/subagent-executor.ts +121 -58
  45. package/src/runs/foreground/workflow-foreground-steering.ts +24 -98
  46. package/src/runs/shared/abort-recovery.ts +3 -3
  47. package/src/runs/shared/acceptance.ts +10 -0
  48. package/src/runs/shared/async-status-projection.ts +11 -43
  49. package/src/runs/shared/capability-ceiling.ts +1 -2
  50. package/src/runs/shared/child-hooks.ts +25 -0
  51. package/src/runs/shared/child-identity.ts +13 -2
  52. package/src/runs/shared/child-launch.ts +314 -0
  53. package/src/runs/shared/child-lifecycle.ts +25 -0
  54. package/src/runs/shared/child-runtime-config.ts +126 -0
  55. package/src/runs/shared/child-session.ts +342 -0
  56. package/src/runs/shared/child-tool-plan.ts +530 -0
  57. package/src/runs/shared/claude-code-adapter.ts +5 -1
  58. package/src/runs/shared/completion-guard.ts +1 -1
  59. package/src/runs/shared/external-cli-preflight.ts +16 -0
  60. package/src/runs/shared/mcp-direct-tool-allowlist.ts +5 -4
  61. package/src/runs/shared/model-exclusions.ts +82 -14
  62. package/src/runs/shared/model-fallback.ts +47 -4
  63. package/src/runs/shared/nested-events.ts +29 -45
  64. package/src/runs/shared/nested-path.ts +0 -14
  65. package/src/runs/shared/orca-progress-tabs.ts +12 -7
  66. package/src/runs/shared/parallel-utils.ts +0 -2
  67. package/src/runs/shared/permissions.ts +0 -13
  68. package/src/runs/shared/process-signal.ts +4 -1
  69. package/src/runs/shared/run-fanout-budget.ts +0 -13
  70. package/src/runs/shared/runtime-acknowledged-extensions.ts +0 -27
  71. package/src/runs/shared/structured-output.ts +17 -4
  72. package/src/runs/shared/subagent-control.ts +6 -2
  73. package/src/runs/shared/subagent-prompt-runtime.ts +87 -384
  74. package/src/runs/shared/tool-availability.ts +18 -62
  75. package/src/runs/shared/tool-budget.ts +0 -14
  76. package/src/runs/shared/worktree-cleanup-plan.ts +25 -6
  77. package/src/runs/shared/worktree.ts +117 -30
  78. package/src/shared/child-session-name.ts +1 -1
  79. package/src/shared/jsonl-writer.ts +11 -0
  80. package/src/shared/thinking-ceiling.ts +0 -6
  81. package/src/shared/types.ts +64 -30
  82. package/src/shared/utils.ts +3 -4
  83. package/src/slash/slash-commands.ts +0 -6
  84. package/src/tui/fleet.ts +0 -1
  85. package/src/tui/render.ts +238 -31
  86. package/src/watchdog/child-status.ts +54 -34
  87. package/src/watchdog/diff-tool.ts +77 -0
  88. package/src/watchdog/emission-guard.ts +5 -3
  89. package/src/watchdog/guidance.ts +20 -0
  90. package/src/watchdog/register-child.ts +28 -25
  91. package/src/watchdog/register-main.ts +10 -9
  92. package/src/watchdog/render.ts +4 -5
  93. package/src/watchdog/review.ts +15 -4
  94. package/src/watchdog/rules.ts +70 -0
  95. package/src/watchdog/runtime.ts +75 -92
  96. package/src/watchdog/scope.ts +0 -11
  97. package/src/watchdog/settings.ts +48 -104
  98. package/src/watchdog/types.ts +18 -32
  99. package/src/watchdog/warning-format.ts +0 -1
  100. package/src/workflows/chat-progress.ts +3 -2
  101. package/src/workflows/scripted-workflow.ts +48 -1
  102. package/src/workflows/workflow-checklist.ts +10 -12
  103. package/src/workflows/workflow-preflight.ts +28 -1
  104. package/src/runs/shared/child-protocol.ts +0 -415
  105. package/src/runs/shared/pi-args.ts +0 -1062
  106. package/src/runs/shared/subagent-startup-retry.ts +0 -116
  107. package/src/shared/post-exit-stdio-guard.ts +0 -85
@@ -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, "autoFollow" | "overrides">> & {
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" | "autoFollow" | "scope" | "cadence" | "main" | "children" | "asyncCompletion" | "lsp">> & {
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
- "autoFollow",
99
+ "stalemateRepeats",
134
100
  "scope",
135
101
  "cadence",
136
102
  "main",
137
103
  "children",
138
- "asyncCompletion",
139
104
  "lsp",
140
- "compactAtPercent",
141
- "reviewRetryDelayMs",
142
- "maxReviewFailures",
105
+ "rules",
143
106
  ]);
144
- const GUIDANCE_FIELDS = new Set(["watchdogMd", "systemPromptPath"]);
145
- const AUTO_FOLLOW_FIELDS = new Set(["blockers", "maxAttempts", "stalemateRepeats"]);
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", "autoFollow", "overrides"]);
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 ("autoFollow" in input) patch.autoFollow = parseAutoFollowPatch(input.autoFollow, `${field}.autoFollow`, meta);
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 ("autoFollow" in input) patch.autoFollow = parseAutoFollowPatch(input.autoFollow, `${field}.autoFollow`, meta);
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 ("compactAtPercent" in input) {
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
 
@@ -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", "async-completion", "lsp"] as const;
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
- autoFollow: WatchdogAutoFollowConfig;
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
- autoFollow: WatchdogAutoFollowConfig;
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
- compactAtPercent: number;
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 (existing?.preflight) next.preflight = existing.preflight;
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(key: string, phase: string, lanes: ReadonlyMap<string, WorkflowPreflightLaneV1>): WorkflowPreflightLaneV1 | undefined {
193
- return lanes.get(key) ?? lanes.get(phase) ?? [...lanes.values()].find((lane) => key.startsWith(`${lane.key}.`));
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 ?? preflight?.key, "Workflow");
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, lanes)), input.now));
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, lanes)), input.now));
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 phase = keyText(step.phase ?? laneFor(key, "", lanes)?.key ?? traceEntry?.generatedLaneKey ?? traceEntry?.phase, "Workflow");
371
- add(phases, phase, applyNow(stepItem(step, index, phase, key, step.label ?? step.key ?? step.agent, laneFor(key, phase, lanes)), input.now));
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 ?? entry.phase ?? "", lanes));
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) {