@selesai/code 0.13.12 → 0.13.14

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 (203) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/extensions/pi-subagents/CHANGELOG.md +77 -1
  3. package/dist/extensions/pi-subagents/VISION.md +12 -1
  4. package/dist/extensions/pi-subagents/docs/agents.md +13 -6
  5. package/dist/extensions/pi-subagents/docs/configuration.md +35 -9
  6. package/dist/extensions/pi-subagents/docs/extension-api.md +1 -1
  7. package/dist/extensions/pi-subagents/docs/missions.md +1 -1
  8. package/dist/extensions/pi-subagents/docs/models.md +6 -6
  9. package/dist/extensions/pi-subagents/docs/observability.md +6 -3
  10. package/dist/extensions/pi-subagents/docs/tool-reference.md +3 -3
  11. package/dist/extensions/pi-subagents/docs/watchdog.md +92 -114
  12. package/dist/extensions/pi-subagents/docs/workflows.md +1 -1
  13. package/dist/extensions/pi-subagents/install.mjs +1 -2
  14. package/dist/extensions/pi-subagents/package.json +1 -1
  15. package/dist/extensions/pi-subagents/skills/pi-subagents/references/constraints-and-recipes.md +1 -1
  16. package/dist/extensions/pi-subagents/skills/pi-subagents/references/execution-controls.md +6 -6
  17. package/dist/extensions/pi-subagents/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  18. package/dist/extensions/pi-subagents/skills/pi-subagents/references/multi-lane-orchestration.md +1 -1
  19. package/dist/extensions/pi-subagents/skills/pi-subagents/references/prompting-and-roles.md +4 -4
  20. package/dist/extensions/pi-subagents/skills/pi-subagents/references/review-and-validation.md +1 -1
  21. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +41 -4
  22. package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +3 -0
  23. package/dist/extensions/pi-subagents/src/agents/agents.ts +120 -124
  24. package/dist/extensions/pi-subagents/src/agents/runtime-agent-registry.ts +5 -1
  25. package/dist/extensions/pi-subagents/src/api/preflight.ts +4 -0
  26. package/dist/extensions/pi-subagents/src/api/shared-types.ts +3 -0
  27. package/dist/extensions/pi-subagents/src/extension/config.ts +20 -0
  28. package/dist/extensions/pi-subagents/src/extension/public-execution.ts +1 -0
  29. package/dist/extensions/pi-subagents/src/extension/schemas.ts +6 -2
  30. package/dist/extensions/pi-subagents/src/extension/tool-description.ts +1 -1
  31. package/dist/extensions/pi-subagents/src/inspectors/herdr/inspector-runner.ts +19 -13
  32. package/dist/extensions/pi-subagents/src/runs/background/active-async-capacity.ts +26 -8
  33. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +103 -23
  34. package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +6 -2
  35. package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +18 -2
  36. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +54 -3
  37. package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +16 -0
  38. package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +22 -2
  39. package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +63 -6
  40. package/dist/extensions/pi-subagents/src/runs/background/steering.ts +4 -1
  41. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +69 -12
  42. package/dist/extensions/pi-subagents/src/runs/background/wait-completions.ts +13 -0
  43. package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +3 -1
  44. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +21 -8
  45. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +59 -6
  46. package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +95 -18
  47. package/dist/extensions/pi-subagents/src/runs/shared/async-status-projection.ts +11 -43
  48. package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +1 -0
  49. package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +1 -1
  50. package/dist/extensions/pi-subagents/src/runs/shared/lane-metadata.ts +24 -3
  51. package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +4 -0
  52. package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +2 -6
  53. package/dist/extensions/pi-subagents/src/runs/shared/permissions.ts +1 -0
  54. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +33 -18
  55. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +73 -37
  56. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +33 -6
  57. package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +4 -2
  58. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +20 -3
  59. package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +21 -7
  60. package/dist/extensions/pi-subagents/src/runs/shared/tool-timeout.ts +1 -1
  61. package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +467 -63
  62. package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +3 -1
  63. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +0 -12
  64. package/dist/extensions/pi-subagents/src/shared/fork-session-cwd.ts +27 -0
  65. package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +3 -0
  66. package/dist/extensions/pi-subagents/src/shared/types.ts +42 -4
  67. package/dist/extensions/pi-subagents/src/shared/utils.ts +18 -7
  68. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +1 -1
  69. package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +26 -12
  70. package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +61 -2
  71. package/dist/extensions/pi-subagents/src/tui/fleet.ts +12 -7
  72. package/dist/extensions/pi-subagents/src/tui/render.ts +227 -14
  73. package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +56 -34
  74. package/dist/extensions/pi-subagents/src/watchdog/diff-tool.ts +77 -0
  75. package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +5 -3
  76. package/dist/extensions/pi-subagents/src/watchdog/guidance.ts +20 -0
  77. package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +16 -13
  78. package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +10 -9
  79. package/dist/extensions/pi-subagents/src/watchdog/render.ts +4 -5
  80. package/dist/extensions/pi-subagents/src/watchdog/review.ts +15 -4
  81. package/dist/extensions/pi-subagents/src/watchdog/rules.ts +70 -0
  82. package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +75 -92
  83. package/dist/extensions/pi-subagents/src/watchdog/scope.ts +2 -1
  84. package/dist/extensions/pi-subagents/src/watchdog/settings.ts +48 -104
  85. package/dist/extensions/pi-subagents/src/watchdog/types.ts +18 -32
  86. package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +0 -1
  87. package/dist/extensions/pi-subagents/src/workflows/chat-progress.ts +3 -2
  88. package/dist/extensions/pi-subagents/src/workflows/workflow-checklist.ts +439 -0
  89. package/dist/extensions/pi-subagents/src/workflows/workflow-preflight.ts +28 -1
  90. package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +335 -21
  91. package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +4 -1
  92. package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +1 -1
  93. package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +22 -18
  94. package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +0 -1
  95. package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +21 -18
  96. package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +307 -22
  97. package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1 -0
  98. package/dist/extensions/pi-subagents/test/support/helpers.ts +25 -0
  99. package/dist/extensions/pi-subagents/test/support/isolated-temp-root.mjs +15 -4
  100. package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +17 -2
  101. package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +96 -15
  102. package/dist/extensions/pi-subagents/test/unit/active-async-capacity.test.ts +53 -5
  103. package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +20 -0
  104. package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +221 -8
  105. package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +45 -24
  106. package/dist/extensions/pi-subagents/test/unit/agent-scan-dirs.test.ts +133 -0
  107. package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +28 -2
  108. package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +1 -1
  109. package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +2 -0
  110. package/dist/extensions/pi-subagents/test/unit/async-retention.test.ts +4 -1
  111. package/dist/extensions/pi-subagents/test/unit/async-status-projection.test.ts +18 -4
  112. package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +16 -0
  113. package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +38 -0
  114. package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +2 -2
  115. package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +21 -0
  116. package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +19 -6
  117. package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +26 -0
  118. package/dist/extensions/pi-subagents/test/unit/herdr-inspector.test.ts +22 -1
  119. package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +2 -6
  120. package/dist/extensions/pi-subagents/test/unit/notify.test.ts +66 -1
  121. package/dist/extensions/pi-subagents/test/unit/pi-args-permission-system.test.ts +1 -1
  122. package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +116 -10
  123. package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +136 -18
  124. package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +20 -0
  125. package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +22 -0
  126. package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +1 -0
  127. package/dist/extensions/pi-subagents/test/unit/project-panes-public-api.test.ts +8 -0
  128. package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +127 -48
  129. package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +68 -0
  130. package/dist/extensions/pi-subagents/test/unit/runtime-agent-registration.test.ts +17 -0
  131. package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +88 -0
  132. package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +4 -2
  133. package/dist/extensions/pi-subagents/test/unit/scripted-workflow.test.ts +9 -9
  134. package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +2 -2
  135. package/dist/extensions/pi-subagents/test/unit/steering.test.ts +9 -3
  136. package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +22 -9
  137. package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +91 -8
  138. package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +27 -0
  139. package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +50 -0
  140. package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +0 -1
  141. package/dist/extensions/pi-subagents/test/unit/tool-timeout.test.ts +2 -2
  142. package/dist/extensions/pi-subagents/test/unit/wait-completions.test.ts +34 -0
  143. package/dist/extensions/pi-subagents/test/unit/wait-subscriptions.test.ts +1 -2
  144. package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +73 -13
  145. package/dist/extensions/pi-subagents/test/unit/watchdog-diff-tool.test.ts +79 -0
  146. package/dist/extensions/pi-subagents/test/unit/watchdog-permission-arbiter.test.ts +2 -4
  147. package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +7 -8
  148. package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +48 -3
  149. package/dist/extensions/pi-subagents/test/unit/watchdog-rules.test.ts +37 -0
  150. package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +36 -118
  151. package/dist/extensions/pi-subagents/test/unit/watchdog-scope.test.ts +1 -1
  152. package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +37 -14
  153. package/dist/extensions/pi-subagents/test/unit/workflow-chat-progress.test.ts +45 -26
  154. package/dist/extensions/pi-subagents/test/unit/workflow-checklist.test.ts +223 -0
  155. package/dist/extensions/pi-subagents/test/unit/workflow-launch-params.test.ts +50 -1
  156. package/dist/extensions/pi-subagents/test/unit/workflow-preflight.test.ts +22 -0
  157. package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +136 -4
  158. package/dist/extensions/pi-zentui/README.md +19 -5
  159. package/dist/extensions/pi-zentui/docs/configuration.md +37 -9
  160. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +29 -0
  161. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +31 -2
  162. package/dist/extensions/pi-zentui/extensions/zentui/prototype-patch-registry.ts +55 -11
  163. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +115 -3
  164. package/dist/extensions/pi-zentui/extensions/zentui/settings-previews.ts +119 -2
  165. package/dist/extensions/pi-zentui/extensions/zentui/thinking-experimental.ts +1768 -0
  166. package/dist/extensions/pi-zentui/extensions/zentui/thinking-status.ts +69 -0
  167. package/dist/extensions/pi-zentui/extensions/zentui/thinking-steps.ts +155 -0
  168. package/dist/extensions/pi-zentui/extensions/zentui/ui.ts +2 -1
  169. package/dist/extensions/pi-zentui/extensions/zentui/user-message-osc.ts +16 -2
  170. package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +1 -1
  171. package/dist/extensions/pi-zentui/test/config-load-lifecycle.test.ts +124 -0
  172. package/dist/extensions/pi-zentui/test/config.test.ts +82 -2
  173. package/dist/extensions/pi-zentui/test/editor-viewport-indicators.test.ts +19 -4
  174. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +58 -9
  175. package/dist/extensions/pi-zentui/test/package-contents.mjs +33 -0
  176. package/dist/extensions/pi-zentui/test/prototype-patch-registry.test.ts +131 -0
  177. package/dist/extensions/pi-zentui/test/responsive-dependencies.test.ts +4 -4
  178. package/dist/extensions/pi-zentui/test/settings-command.test.ts +289 -7
  179. package/dist/extensions/pi-zentui/test/settings-previews.test.ts +106 -0
  180. package/dist/extensions/pi-zentui/test/thinking-experimental-docs.test.ts +51 -0
  181. package/dist/extensions/pi-zentui/test/thinking-experimental-loader-diagnostics.mjs +54 -0
  182. package/dist/extensions/pi-zentui/test/thinking-experimental-missing-export.test.ts +44 -0
  183. package/dist/extensions/pi-zentui/test/thinking-experimental-rows.test.ts +297 -0
  184. package/dist/extensions/pi-zentui/test/thinking-experimental-tui.mjs +2329 -0
  185. package/dist/extensions/pi-zentui/test/thinking-experimental.test.ts +1668 -0
  186. package/dist/extensions/pi-zentui/test/thinking-status.test.ts +85 -0
  187. package/dist/extensions/pi-zentui/test/thinking-steps.test.ts +122 -0
  188. package/dist/extensions/pi-zentui/test/user-message-osc.test.ts +58 -0
  189. package/dist/skills/brandkit/SKILL.md +0 -2
  190. package/dist/skills/{industrial-brutalist-ui → brutalist}/SKILL.md +1 -3
  191. package/dist/skills/gpt-taste/SKILL.md +0 -2
  192. package/dist/skills/image-to-code/SKILL.md +0 -2
  193. package/dist/skills/imagegen-frontend-mobile/SKILL.md +0 -2
  194. package/dist/skills/imagegen-frontend-web/SKILL.md +0 -2
  195. package/dist/skills/{minimalist-ui → minimalist}/SKILL.md +1 -3
  196. package/dist/skills/{full-output-enforcement → output}/SKILL.md +1 -3
  197. package/dist/skills/{redesign-existing-projects → redesign}/SKILL.md +1 -3
  198. package/dist/skills/{high-end-visual-design → soft}/SKILL.md +1 -3
  199. package/dist/skills/stitch/DESIGN.md +121 -0
  200. package/dist/skills/{stitch-design-taste → stitch}/SKILL.md +1 -3
  201. package/dist/skills/{design-taste-frontend → taste}/SKILL.md +1 -3
  202. package/dist/skills/{design-taste-frontend-v1 → taste-v1}/SKILL.md +2 -4
  203. package/package.json +1 -1
@@ -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"
@@ -0,0 +1,439 @@
1
+ import type { AsyncJobStep, HostStepNodeV1, WorkflowGraphNode, WorkflowGraphSnapshot, WorkflowPreflightLaneV1, WorkflowPreflightV1 } from "../shared/types.ts";
2
+ import { sanitizeDisplayText } from "../shared/display-text.ts";
3
+ import { workflowPreflightLaneForRuntimeKey } from "./workflow-preflight.ts";
4
+
5
+ export type WorkflowChecklistState = "complete" | "running" | "queued" | "blocked" | "failed" | "paused" | "stopped";
6
+
7
+ export interface WorkflowChecklistStep {
8
+ key?: string;
9
+ workflowKey?: string;
10
+ runId?: string;
11
+ label?: string;
12
+ description?: string;
13
+ phase?: string;
14
+ agent?: string;
15
+ status: string;
16
+ context?: "fresh" | "fork";
17
+ activityState?: string;
18
+ startedAt?: number;
19
+ endedAt?: number;
20
+ durationMs?: number;
21
+ currentTool?: string;
22
+ currentToolStartedAt?: number;
23
+ currentPath?: string;
24
+ turnCount?: number;
25
+ toolCount?: number;
26
+ outputName?: string;
27
+ error?: string;
28
+ toolBudgetBlocked?: boolean;
29
+ turnBudgetExceeded?: boolean;
30
+ timedOut?: boolean;
31
+ stopped?: boolean;
32
+ acceptance?: { status?: string; reviewResult?: { status?: string } };
33
+ review?: { status?: string };
34
+ watchdog?: { phase?: string };
35
+ }
36
+
37
+ export interface WorkflowChecklistTraceEntry {
38
+ operation?: string;
39
+ key: string;
40
+ state: string;
41
+ agent?: string;
42
+ runId?: string;
43
+ phase?: string;
44
+ label?: string;
45
+ generatedLaneKey?: string;
46
+ durationMs?: number;
47
+ error?: string;
48
+ }
49
+
50
+ export interface WorkflowChecklistItem {
51
+ key: string;
52
+ label: string;
53
+ phase: string;
54
+ state: WorkflowChecklistState;
55
+ agent?: string;
56
+ context?: "fresh" | "fork";
57
+ startedAt?: number;
58
+ durationMs?: number;
59
+ currentTool?: string;
60
+ currentToolStartedAt?: number;
61
+ currentPath?: string;
62
+ toolCount?: number;
63
+ outputName?: string;
64
+ error?: string;
65
+ preflight?: WorkflowPreflightLaneV1;
66
+ kind?: "child" | "host";
67
+ monitorKind?: HostStepNodeV1["monitorKind"];
68
+ provider?: string;
69
+ role?: string;
70
+ verdict?: HostStepNodeV1["verdict"];
71
+ target?: string;
72
+ reasonCode?: string;
73
+ stale?: boolean;
74
+ reportPath?: string;
75
+ }
76
+
77
+ export interface WorkflowChecklistPhase {
78
+ key: string;
79
+ label: string;
80
+ state: WorkflowChecklistState;
81
+ items: WorkflowChecklistItem[];
82
+ total: number;
83
+ done: number;
84
+ running: number;
85
+ queued: number;
86
+ blocked: number;
87
+ failed: number;
88
+ paused: number;
89
+ stopped: number;
90
+ parallel: boolean;
91
+ }
92
+
93
+ export interface WorkflowChecklistProjection {
94
+ phases: WorkflowChecklistPhase[];
95
+ total: number;
96
+ done: number;
97
+ running: number;
98
+ queued: number;
99
+ blocked: number;
100
+ failed: number;
101
+ paused: number;
102
+ stopped: number;
103
+ bottleneck?: WorkflowChecklistItem;
104
+ }
105
+
106
+ export interface WorkflowChecklistInput {
107
+ graph?: WorkflowGraphSnapshot;
108
+ steps?: readonly WorkflowChecklistStep[] | readonly AsyncJobStep[];
109
+ hostSteps?: readonly HostStepNodeV1[];
110
+ preflight?: WorkflowPreflightV1;
111
+ trace?: readonly WorkflowChecklistTraceEntry[];
112
+ now?: number;
113
+ }
114
+
115
+ const MAX_TEXT = 160;
116
+ const TERMINAL_STATES = new Set<WorkflowChecklistState>(["complete", "blocked", "failed", "paused", "stopped"]);
117
+
118
+ function text(value: unknown, fallback?: string): string | undefined {
119
+ if (typeof value !== "string") return fallback;
120
+ const clean = sanitizeDisplayText(value).replace(/\s+/g, " ").trim();
121
+ return clean ? clean.slice(0, MAX_TEXT) : fallback;
122
+ }
123
+
124
+ function keyText(value: unknown, fallback: string): string {
125
+ return text(value, fallback) ?? fallback;
126
+ }
127
+
128
+ function normalizedStatus(value: unknown): string {
129
+ return typeof value === "string" ? value.toLowerCase() : "queued";
130
+ }
131
+
132
+ function finite(value: unknown): number | undefined {
133
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
134
+ }
135
+
136
+ function count(value: unknown): number | undefined {
137
+ const number = finite(value);
138
+ return number !== undefined && number >= 0 ? Math.round(number) : undefined;
139
+ }
140
+
141
+ function explicitBlocked(source: Pick<WorkflowChecklistStep, "activityState" | "toolBudgetBlocked" | "turnBudgetExceeded" | "timedOut" | "acceptance" | "review" | "watchdog"> & { verdict?: unknown; stale?: unknown }): boolean {
142
+ const acceptance = normalizedStatus(source.acceptance?.status);
143
+ const review = normalizedStatus(source.review?.status ?? source.acceptance?.reviewResult?.status);
144
+ return source.toolBudgetBlocked === true
145
+ || source.turnBudgetExceeded === true
146
+ || source.activityState === "needs_attention"
147
+ || source.timedOut === true
148
+ || source.watchdog?.phase === "stale"
149
+ || source.stale === true
150
+ || source.verdict === "inconclusive"
151
+ || acceptance === "rejected"
152
+ || acceptance === "blockers"
153
+ || review === "blockers"
154
+ || review === "review-required";
155
+ }
156
+
157
+ function checklistState(source: Pick<WorkflowChecklistStep, "status" | "activityState" | "toolBudgetBlocked" | "turnBudgetExceeded" | "timedOut" | "acceptance" | "review" | "watchdog"> & { verdict?: unknown; stale?: unknown }): WorkflowChecklistState {
158
+ if (explicitBlocked(source)) return "blocked";
159
+ switch (normalizedStatus(source.status)) {
160
+ case "complete":
161
+ case "completed":
162
+ case "done":
163
+ case "pass":
164
+ case "accepted": return "complete";
165
+ case "running":
166
+ case "started":
167
+ case "active": return "running";
168
+ case "failed":
169
+ case "error":
170
+ case "fail": return "failed";
171
+ case "blocked":
172
+ case "rejected":
173
+ case "partial":
174
+ case "needs_attention": return "blocked";
175
+ case "paused":
176
+ case "detached": return "paused";
177
+ case "stopped":
178
+ case "cancelled":
179
+ case "canceled": return "stopped";
180
+ default: return "queued";
181
+ }
182
+ }
183
+
184
+ function duration(step: Pick<WorkflowChecklistStep, "durationMs" | "startedAt" | "endedAt">, now: number | undefined, state: WorkflowChecklistState): number | undefined {
185
+ const explicit = finite(step.durationMs);
186
+ const startedAt = finite(step.startedAt);
187
+ if (explicit !== undefined) return Math.max(0, explicit);
188
+ if (startedAt === undefined) return undefined;
189
+ const end = state === "running" ? now : finite(step.endedAt) ?? now;
190
+ return end === undefined ? undefined : Math.max(0, end - startedAt);
191
+ }
192
+
193
+ function laneFor(preflight: WorkflowPreflightV1 | undefined, key: string, preferredKeys: readonly (string | undefined)[] = []): WorkflowPreflightLaneV1 | undefined {
194
+ return workflowPreflightLaneForRuntimeKey(preflight, key, preferredKeys);
195
+ }
196
+
197
+ function stepKey(step: WorkflowChecklistStep): string | undefined {
198
+ return step.key ?? step.workflowKey ?? step.runId;
199
+ }
200
+
201
+ function stepItem(step: WorkflowChecklistStep, index: number, phase: string, key = stepKey(step) ?? `step-${index + 1}`, label = step.label ?? step.description ?? stepKey(step) ?? step.agent ?? key, preflight?: WorkflowPreflightLaneV1): WorkflowChecklistItem {
202
+ const state = checklistState(step);
203
+ return {
204
+ key: keyText(key, `step-${index + 1}`),
205
+ label: keyText(label, key),
206
+ phase,
207
+ state,
208
+ ...(text(step.agent) ? { agent: text(step.agent) } : {}),
209
+ ...(step.context ? { context: step.context } : {}),
210
+ ...(finite(step.startedAt) !== undefined ? { startedAt: finite(step.startedAt) } : {}),
211
+ ...(duration(step, undefined, state) !== undefined ? { durationMs: duration(step, undefined, state) } : {}),
212
+ ...(text(step.currentTool) ? { currentTool: text(step.currentTool) } : {}),
213
+ ...(finite(step.currentToolStartedAt) !== undefined ? { currentToolStartedAt: finite(step.currentToolStartedAt) } : {}),
214
+ ...(text(step.currentPath) ? { currentPath: text(step.currentPath) } : {}),
215
+ ...(count(step.toolCount) !== undefined ? { toolCount: count(step.toolCount) } : {}),
216
+ ...(text(step.outputName) ? { outputName: text(step.outputName) } : {}),
217
+ ...(text(step.error) ? { error: text(step.error) } : {}),
218
+ ...(preflight ? { preflight } : {}),
219
+ };
220
+ }
221
+
222
+ function hostItem(host: HostStepNodeV1, phase: string, key = host.id): WorkflowChecklistItem {
223
+ const state = checklistState({ status: host.state, verdict: host.verdict, stale: host.freshness?.stale });
224
+ return {
225
+ key: keyText(key, "host-step"),
226
+ label: keyText(host.label, "host step"),
227
+ phase,
228
+ state,
229
+ kind: "host",
230
+ monitorKind: host.monitorKind,
231
+ ...(text(host.provider) ? { provider: text(host.provider) } : {}),
232
+ ...(text(host.role) ? { role: text(host.role) } : {}),
233
+ ...(host.verdict ? { verdict: host.verdict } : {}),
234
+ ...(text(host.target) ? { target: text(host.target) } : {}),
235
+ ...(text(host.reasonCode) ? { reasonCode: text(host.reasonCode) } : {}),
236
+ ...(text(host.detail) ? { error: text(host.detail) } : {}),
237
+ ...(host.freshness?.stale !== undefined ? { stale: host.freshness.stale } : {}),
238
+ ...(text(host.reportPath) ? { reportPath: text(host.reportPath) } : {}),
239
+ };
240
+ }
241
+
242
+ function graphNodes(graph: WorkflowGraphSnapshot | undefined): WorkflowGraphNode[] {
243
+ const out: WorkflowGraphNode[] = [];
244
+ const visit = (node: WorkflowGraphNode): void => {
245
+ if (node.kind === "parallel-group" || node.kind === "dynamic-parallel-group") {
246
+ for (const child of node.children ?? []) visit(child);
247
+ if (!node.children?.length) out.push(node);
248
+ } else {
249
+ out.push(node);
250
+ }
251
+ };
252
+ for (const node of graph?.nodes ?? []) visit(node);
253
+ return out;
254
+ }
255
+
256
+ function traceSources(trace: readonly WorkflowChecklistTraceEntry[] | undefined): WorkflowChecklistTraceEntry[] {
257
+ const latest = new Map<string, WorkflowChecklistTraceEntry>();
258
+ for (const entry of trace ?? []) {
259
+ const state = normalizedStatus(entry.state);
260
+ if ((entry.operation !== undefined && entry.operation !== "run" && entry.operation !== "host") || !entry.key || state === "delivered" || state === "missed") continue;
261
+ const existing = latest.get(entry.key);
262
+ latest.set(entry.key, state === "reused" && existing ? { ...existing, ...entry, state: existing.state } : { ...existing, ...entry });
263
+ }
264
+ return [...latest.values()];
265
+ }
266
+
267
+ function traceItem(entry: WorkflowChecklistTraceEntry, index: number, preflight: WorkflowPreflightLaneV1 | undefined): WorkflowChecklistItem {
268
+ const phase = keyText(preflight?.key ?? entry.generatedLaneKey ?? entry.phase, "Workflow");
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);
270
+ if (entry.operation === "host") item.kind = "host";
271
+ return item;
272
+ }
273
+
274
+ function phaseFor(phases: Map<string, WorkflowChecklistPhase>, label: string): WorkflowChecklistPhase {
275
+ let phase = phases.get(label);
276
+ if (!phase) {
277
+ phase = { key: label, label, state: "queued", items: [], total: 0, done: 0, running: 0, queued: 0, blocked: 0, failed: 0, paused: 0, stopped: 0, parallel: false };
278
+ phases.set(label, phase);
279
+ }
280
+ return phase;
281
+ }
282
+
283
+ function add(phases: Map<string, WorkflowChecklistPhase>, phase: string, item: WorkflowChecklistItem): void {
284
+ phaseFor(phases, phase).items.push(item);
285
+ }
286
+
287
+ function mergeNodeStep(node: WorkflowGraphNode, step: WorkflowChecklistStep, phase: string, trace: WorkflowChecklistTraceEntry | undefined, preflight: WorkflowPreflightLaneV1 | undefined): WorkflowChecklistItem {
288
+ const state = checklistState(step);
289
+ const nodeState = checklistState({ status: node.status, acceptance: node.acceptanceStatus ? { status: node.acceptanceStatus } : undefined });
290
+ const status = TERMINAL_STATES.has(nodeState) && !TERMINAL_STATES.has(state)
291
+ ? nodeState
292
+ : trace && !TERMINAL_STATES.has(state)
293
+ ? (trace.state === "started" ? "running" : trace.state)
294
+ : normalizedStatus(step.status) === "pending" && normalizedStatus(node.status) !== "pending" ? node.status : step.status;
295
+ return stepItem({ ...step, status, key: node.id, label: step.label ?? node.label, phase: step.phase ?? phase, agent: step.agent ?? node.agent, outputName: step.outputName ?? node.outputName, error: step.error ?? trace?.error ?? node.error, acceptance: step.acceptance ?? (node.acceptanceStatus ? { status: node.acceptanceStatus } : undefined), durationMs: step.durationMs ?? trace?.durationMs }, node.flatIndex ?? 0, phase, node.id, step.label ?? node.label, preflight);
296
+ }
297
+
298
+ function priority(item: WorkflowChecklistItem): number {
299
+ return item.state === "blocked" ? 0 : item.state === "failed" ? 1 : item.state === "running" ? 2 : item.state === "paused" ? 3 : item.state === "stopped" ? 4 : item.state === "queued" ? 5 : 6;
300
+ }
301
+
302
+ function applyNow(item: WorkflowChecklistItem, now: number | undefined): WorkflowChecklistItem {
303
+ return item.durationMs === undefined && item.startedAt !== undefined && now !== undefined ? { ...item, durationMs: Math.max(0, now - item.startedAt) } : item;
304
+ }
305
+
306
+ function finalize(phase: WorkflowChecklistPhase): void {
307
+ phase.total = phase.items.length;
308
+ phase.done = phase.items.filter((item) => item.state === "complete").length;
309
+ phase.running = phase.items.filter((item) => item.state === "running").length;
310
+ phase.queued = phase.items.filter((item) => item.state === "queued").length;
311
+ phase.blocked = phase.items.filter((item) => item.state === "blocked").length;
312
+ phase.failed = phase.items.filter((item) => item.state === "failed").length;
313
+ phase.paused = phase.items.filter((item) => item.state === "paused").length;
314
+ phase.stopped = phase.items.filter((item) => item.state === "stopped").length;
315
+ phase.parallel = phase.total > 1;
316
+ phase.state = phase.blocked ? "blocked" : phase.failed ? "failed" : phase.running ? "running" : phase.paused ? "paused" : phase.stopped ? "stopped" : phase.queued ? "queued" : "complete";
317
+ }
318
+
319
+ export function projectWorkflowChecklist(input: WorkflowChecklistInput): WorkflowChecklistProjection {
320
+ const phases = new Map<string, WorkflowChecklistPhase>();
321
+ const steps = (input.steps ?? []) as readonly WorkflowChecklistStep[];
322
+ const nodes = graphNodes(input.graph);
323
+ const trace = traceSources(input.trace);
324
+ const traceByKey = new Map(trace.map((entry) => [entry.key, entry]));
325
+ const phaseByNode = new Map<string, string>();
326
+ for (const phase of input.graph?.phases ?? []) {
327
+ const title = keyText(phase.title, "Workflow");
328
+ phaseFor(phases, title);
329
+ for (const nodeId of phase.nodeIds) if (!phaseByNode.has(nodeId)) phaseByNode.set(nodeId, title);
330
+ }
331
+
332
+ const stepsByKey = new Map<string, Array<{ step: WorkflowChecklistStep; index: number }>>();
333
+ for (const [index, step] of steps.entries()) {
334
+ const key = stepKey(step);
335
+ if (!key) continue;
336
+ stepsByKey.set(key, [...(stepsByKey.get(key) ?? []), { step, index }]);
337
+ }
338
+
339
+ const usedSteps = new Set<number>();
340
+ const hostById = new Map((input.hostSteps ?? []).map((host) => [host.id, host]));
341
+ const graphKeys = new Set(nodes.map((node) => node.id));
342
+ const stepKeys = new Set([...stepsByKey.keys()]);
343
+ const hostKeys = new Set(hostById.keys());
344
+
345
+ for (const node of nodes) {
346
+ const host = node.hostStep ?? hostById.get(node.id);
347
+ const phase = keyText(phaseByNode.get(node.id) ?? node.phase ?? (host ? host.label : node.label), "Workflow");
348
+ if (host) {
349
+ add(phases, phase, hostItem(host, phase, node.id));
350
+ hostById.delete(node.id);
351
+ continue;
352
+ }
353
+ const matches = (stepsByKey.get(node.id) ?? []).filter(({ index }) => !usedSteps.has(index));
354
+ if (matches.length) {
355
+ for (const match of matches) {
356
+ usedSteps.add(match.index);
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));
358
+ }
359
+ continue;
360
+ }
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));
362
+ }
363
+
364
+ for (const [index, step] of steps.entries()) {
365
+ if (usedSteps.has(index)) continue;
366
+ const key = keyText(stepKey(step), `step-${index + 1}`);
367
+ const traceEntry = traceByKey.get(key);
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));
371
+ }
372
+
373
+ for (const host of hostById.values()) add(phases, keyText(host.label, "Host"), hostItem(host, keyText(host.label, "Host")));
374
+ for (const entry of trace) {
375
+ if (graphKeys.has(entry.key) || stepKeys.has(entry.key) || hostKeys.has(entry.key)) continue;
376
+ const item = traceItem(entry, phases.size, laneFor(input.preflight, entry.key, [entry.generatedLaneKey, entry.phase]));
377
+ add(phases, item.phase, item);
378
+ }
379
+
380
+ const finalized = [...phases.values()].filter((phase) => phase.items.length > 0);
381
+ for (const phase of finalized) {
382
+ phase.items = phase.items.map((item) => applyNow(item, input.now));
383
+ finalize(phase);
384
+ }
385
+ const all = finalized.flatMap((phase) => phase.items);
386
+ const counts = (state: WorkflowChecklistState): number => all.filter((item) => item.state === state).length;
387
+ const bottleneck = [...all].sort((left, right) => priority(left) - priority(right))[0];
388
+ return { phases: finalized, total: all.length, done: counts("complete"), running: counts("running"), queued: counts("queued"), blocked: counts("blocked"), failed: counts("failed"), paused: counts("paused"), stopped: counts("stopped"), ...(bottleneck && bottleneck.state !== "complete" ? { bottleneck } : {}) };
389
+ }
390
+
391
+ function stateLabel(state: WorkflowChecklistState): string {
392
+ return state === "running" ? "active" : state;
393
+ }
394
+
395
+ export function formatWorkflowChecklistSummary(projection: WorkflowChecklistProjection): string {
396
+ if (!projection.total) return "";
397
+ return [`${projection.done}/${projection.total} done`, projection.running ? `${projection.running} active` : undefined, projection.queued ? `${projection.queued} queued` : undefined, projection.blocked ? `${projection.blocked} blocked` : undefined, projection.failed ? `${projection.failed} failed` : undefined, projection.paused ? `${projection.paused} paused` : undefined, projection.stopped ? `${projection.stopped} stopped` : undefined].filter((value): value is string => Boolean(value)).join(" · ");
398
+ }
399
+
400
+ export function formatWorkflowChecklistPhase(phase: WorkflowChecklistPhase): string {
401
+ const counts = [phase.total > 1 && phase.done ? `${phase.done} done` : undefined, phase.running ? `${phase.running} active` : undefined, phase.queued ? `${phase.queued} queued` : undefined, phase.blocked ? `${phase.blocked} blocked` : undefined, phase.failed ? `${phase.failed} failed` : undefined, phase.paused ? `${phase.paused} paused` : undefined, phase.stopped ? `${phase.stopped} stopped` : undefined].filter((value): value is string => Boolean(value));
402
+ return counts.length ? `${phase.label} ${counts.join(" · ")}` : phase.label;
403
+ }
404
+
405
+ export function formatWorkflowChecklistBottleneck(item: WorkflowChecklistItem | undefined, options: { includeOutput?: boolean } = {}): string | undefined {
406
+ if (!item) return undefined;
407
+ const identity = [item.label, item.agent && item.agent !== item.label ? item.agent : undefined].filter((value): value is string => Boolean(value)).join(" · ") || item.key;
408
+ const includeOutput = options.includeOutput ?? true;
409
+ const details = [item.context ? `(${item.context})` : undefined, item.currentTool ? `${item.currentTool}${item.durationMs !== undefined ? ` ${formatDurationText(item.durationMs)}` : ""}` : undefined, !item.currentTool && item.currentPath ? item.currentPath : undefined, !item.currentTool && item.durationMs !== undefined ? formatDurationText(item.durationMs) : undefined, item.toolCount !== undefined ? `${item.toolCount} tools` : undefined, includeOutput && item.outputName ? `out:${item.outputName}` : undefined, item.error ? `error:${item.error.replace(/\bOutput:/g, "output:")}` : undefined].filter((value): value is string => Boolean(value));
410
+ return [identity, ...details].join(" · ");
411
+ }
412
+
413
+ function formatWorkflowChecklistItem(item: WorkflowChecklistItem): string {
414
+ const identity = [item.label, item.agent && item.agent !== item.label ? item.agent : undefined].filter((value): value is string => Boolean(value)).join(" · ") || item.key;
415
+ const details = [item.kind && item.monitorKind ? item.monitorKind : undefined, item.provider ? `provider:${item.provider}` : undefined, item.role ? `role:${item.role}` : undefined, item.target, item.currentTool, !item.currentTool && item.currentPath ? item.currentPath : undefined, item.durationMs !== undefined ? formatDurationText(item.durationMs) : undefined, item.toolCount !== undefined ? `${item.toolCount} tools` : undefined, item.outputName ? `out:${item.outputName}` : undefined, item.reportPath ? `out:${item.reportPath}` : undefined, item.stale ? "stale" : undefined, item.reasonCode ? `reason:${item.reasonCode}` : undefined, item.error ? `error:${item.error.replace(/\bOutput:/g, "output:")}` : undefined].filter((value): value is string => Boolean(value));
416
+ return `${identity}${item.context ? ` (${item.context})` : ""}${item.state === "complete" ? "" : ` · ${stateLabel(item.state)}`}${details.length ? ` · ${details.join(" · ")}` : ""}`;
417
+ }
418
+
419
+ function formatDurationText(ms: number): string {
420
+ const seconds = Math.max(0, Math.round(ms / 1000));
421
+ return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
422
+ }
423
+
424
+ export function formatWorkflowChecklistText(projection: WorkflowChecklistProjection, indent = "", options: { includeItems?: boolean } = {}): string[] {
425
+ if (!projection.total) return [];
426
+ const lines = [`${indent}Workflow checklist: ${formatWorkflowChecklistSummary(projection)}`];
427
+ for (const phase of projection.phases) {
428
+ const phaseMarker = phase.state === "complete" ? "✓" : phase.state === "running" ? "⠼" : phase.state === "blocked" ? "!" : phase.state === "failed" ? "✗" : phase.state === "paused" || phase.state === "stopped" ? "■" : "◦";
429
+ lines.push(`${indent} ${phaseMarker} ${formatWorkflowChecklistPhase(phase)}`);
430
+ if (options.includeItems === false) continue;
431
+ for (const item of phase.items) {
432
+ const marker = item.state === "complete" ? "✓" : item.state === "running" ? "⠼" : item.state === "blocked" ? "!" : item.state === "failed" ? "✗" : item.state === "paused" || item.state === "stopped" ? "■" : "◦";
433
+ lines.push(`${indent} ${marker} ${formatWorkflowChecklistItem(item)}`);
434
+ }
435
+ }
436
+ const bottleneck = formatWorkflowChecklistBottleneck(projection.bottleneck);
437
+ if (bottleneck) lines.push(`${indent} bottleneck · ${bottleneck}`);
438
+ return lines;
439
+ }
@@ -146,8 +146,35 @@ function declaredKeys(preflight: WorkflowPreflightV1): Set<string> {
146
146
  * Treat declared lane keys as plan roots: `lane` is the lane itself and
147
147
  * `lane.stage` is a stage by convention, even without generated provenance.
148
148
  */
149
+ export function workflowKeyMatchesPreflightLane(key: string, laneKey: string, generatedLaneKey?: string): boolean {
150
+ return generatedLaneKey === laneKey || key === laneKey || key.startsWith(`${laneKey}.`);
151
+ }
152
+
153
+ /** Select the most specific advisory lane without allowing declaration order to override an exact runtime key. */
154
+ export function workflowPreflightLaneForRuntimeKey(
155
+ preflight: WorkflowPreflightV1 | undefined,
156
+ key: string,
157
+ preferredKeys: readonly (string | undefined)[] = [],
158
+ ): WorkflowPreflightLaneV1 | undefined {
159
+ const lanes = preflight?.lanes;
160
+ if (!lanes) return undefined;
161
+ const exact = lanes.find((lane) => lane.key === key);
162
+ if (exact) return exact;
163
+ let closestRoot: WorkflowPreflightLaneV1 | undefined;
164
+ for (const lane of lanes) {
165
+ if (key.startsWith(`${lane.key}.`) && (!closestRoot || lane.key.length > closestRoot.key.length)) closestRoot = lane;
166
+ }
167
+ if (closestRoot) return closestRoot;
168
+ for (const preferredKey of preferredKeys) {
169
+ if (!preferredKey) continue;
170
+ const preferred = lanes.find((lane) => lane.key === preferredKey);
171
+ if (preferred) return preferred;
172
+ }
173
+ return undefined;
174
+ }
175
+
149
176
  function declaredLaneCoversWorkflowKey(entry: WorkflowTraceLike, laneKey: string): boolean {
150
- return entry.key === laneKey || entry.key.startsWith(`${laneKey}.`);
177
+ return workflowKeyMatchesPreflightLane(entry.key, laneKey, entry.generatedLaneKey);
151
178
  }
152
179
 
153
180
  function keyCoveredByDeclaredLane(entry: WorkflowTraceLike, declared: ReadonlySet<string>): boolean {