@pi-agents/orchid 0.1.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/LICENSE +21 -0
- package/README.md +246 -0
- package/agents/AGENTS-MANIFEST.md +42 -0
- package/agents/brain.md +42 -0
- package/agents/context-builder.md +46 -0
- package/agents/delegate.md +12 -0
- package/agents/dev-1.md +42 -0
- package/agents/oracle.md +73 -0
- package/agents/planner.md +55 -0
- package/agents/researcher.md +52 -0
- package/agents/reviewer.md +79 -0
- package/agents/scout.md +50 -0
- package/agents/tester.md +45 -0
- package/agents/worker.md +55 -0
- package/extensions/ralph.ts +1 -0
- package/extensions/reviewer-extension.ts +125 -0
- package/extensions/task-orchestrator.ts +28 -0
- package/package.json +63 -0
- package/prompts/gather-context-and-clarify.md +13 -0
- package/prompts/parallel-cleanup.md +59 -0
- package/prompts/parallel-context-build.md +53 -0
- package/prompts/parallel-handoff-plan.md +59 -0
- package/prompts/parallel-research.md +50 -0
- package/prompts/parallel-review.md +54 -0
- package/prompts/review-loop.md +41 -0
- package/skills/orchid/SKILL.md +214 -0
- package/skills/orchid/orchid-cleanup/SKILL.md +122 -0
- package/skills/orchid/orchid-converge/SKILL.md +124 -0
- package/skills/orchid/orchid-decompose/SKILL.md +201 -0
- package/skills/orchid/orchid-doctor/SKILL.md +162 -0
- package/skills/orchid/orchid-investigate/SKILL.md +102 -0
- package/skills/orchid/orchid-launch/SKILL.md +147 -0
- package/skills/ralph/SKILL.md +73 -0
- package/skills/subagents/pi-subagents/SKILL.md +813 -0
- package/src/index.ts +7 -0
- package/src/orchestrator/abort.ts +534 -0
- package/src/orchestrator/agent-bridge-extension.ts +1020 -0
- package/src/orchestrator/agent-host.ts +954 -0
- package/src/orchestrator/cleanup.ts +776 -0
- package/src/orchestrator/config-loader.ts +1412 -0
- package/src/orchestrator/config-schema.ts +690 -0
- package/src/orchestrator/config.ts +81 -0
- package/src/orchestrator/context-window.ts +66 -0
- package/src/orchestrator/diagnostic-reports.ts +475 -0
- package/src/orchestrator/diagnostics.ts +394 -0
- package/src/orchestrator/discovery.ts +1833 -0
- package/src/orchestrator/engine-worker.ts +415 -0
- package/src/orchestrator/engine.ts +5940 -0
- package/src/orchestrator/execution.ts +3104 -0
- package/src/orchestrator/extension.ts +5934 -0
- package/src/orchestrator/formatting.ts +785 -0
- package/src/orchestrator/git.ts +88 -0
- package/src/orchestrator/index.ts +28 -0
- package/src/orchestrator/lane-runner.ts +1787 -0
- package/src/orchestrator/mailbox.ts +780 -0
- package/src/orchestrator/merge.ts +3414 -0
- package/src/orchestrator/messages.ts +1062 -0
- package/src/orchestrator/migrations.ts +278 -0
- package/src/orchestrator/naming.ts +117 -0
- package/src/orchestrator/path-resolver.ts +275 -0
- package/src/orchestrator/persistence.ts +2625 -0
- package/src/orchestrator/process-registry.ts +452 -0
- package/src/orchestrator/quality-gate.ts +1085 -0
- package/src/orchestrator/resume.ts +3488 -0
- package/src/orchestrator/sessions.ts +57 -0
- package/src/orchestrator/settings-loader.ts +136 -0
- package/src/orchestrator/settings-tui.ts +2208 -0
- package/src/orchestrator/sidecar-telemetry.ts +267 -0
- package/src/orchestrator/supervisor.ts +4548 -0
- package/src/orchestrator/task-executor-core.ts +675 -0
- package/src/orchestrator/tmux-compat.ts +37 -0
- package/src/orchestrator/tool-allowlist-constants.ts +37 -0
- package/src/orchestrator/types.ts +4465 -0
- package/src/orchestrator/verification.ts +547 -0
- package/src/orchestrator/waves.ts +1564 -0
- package/src/orchestrator/workspace.ts +707 -0
- package/src/orchestrator/worktree.ts +2725 -0
- package/src/ralph/index.ts +825 -0
- package/src/subagents/agents/agent-management.ts +648 -0
- package/src/subagents/agents/agent-scope.ts +6 -0
- package/src/subagents/agents/agent-selection.ts +23 -0
- package/src/subagents/agents/agent-serializer.ts +86 -0
- package/src/subagents/agents/agents.ts +832 -0
- package/src/subagents/agents/chain-serializer.ts +137 -0
- package/src/subagents/agents/frontmatter.ts +29 -0
- package/src/subagents/agents/identity.ts +30 -0
- package/src/subagents/agents/skills.ts +632 -0
- package/src/subagents/extension/config.ts +16 -0
- package/src/subagents/extension/control-notices.ts +92 -0
- package/src/subagents/extension/doctor.ts +199 -0
- package/src/subagents/extension/fanout-child.ts +170 -0
- package/src/subagents/extension/index.ts +573 -0
- package/src/subagents/extension/schemas.ts +168 -0
- package/src/subagents/intercom/intercom-bridge.ts +379 -0
- package/src/subagents/intercom/result-intercom.ts +377 -0
- package/src/subagents/runs/background/async-execution.ts +712 -0
- package/src/subagents/runs/background/async-job-tracker.ts +310 -0
- package/src/subagents/runs/background/async-resume.ts +345 -0
- package/src/subagents/runs/background/async-status.ts +325 -0
- package/src/subagents/runs/background/completion-dedupe.ts +63 -0
- package/src/subagents/runs/background/notify.ts +108 -0
- package/src/subagents/runs/background/parallel-groups.ts +45 -0
- package/src/subagents/runs/background/result-watcher.ts +307 -0
- package/src/subagents/runs/background/run-id-resolver.ts +83 -0
- package/src/subagents/runs/background/run-status.ts +269 -0
- package/src/subagents/runs/background/stale-run-reconciler.ts +336 -0
- package/src/subagents/runs/background/subagent-runner.ts +1808 -0
- package/src/subagents/runs/background/top-level-async.ts +13 -0
- package/src/subagents/runs/foreground/chain-clarify.ts +1333 -0
- package/src/subagents/runs/foreground/chain-execution.ts +938 -0
- package/src/subagents/runs/foreground/execution.ts +918 -0
- package/src/subagents/runs/foreground/subagent-executor.ts +2527 -0
- package/src/subagents/runs/shared/completion-guard.ts +147 -0
- package/src/subagents/runs/shared/long-running-guard.ts +175 -0
- package/src/subagents/runs/shared/mcp-direct-tool-allowlist.ts +365 -0
- package/src/subagents/runs/shared/model-fallback.ts +103 -0
- package/src/subagents/runs/shared/nested-events.ts +819 -0
- package/src/subagents/runs/shared/nested-path.ts +52 -0
- package/src/subagents/runs/shared/nested-render.ts +115 -0
- package/src/subagents/runs/shared/parallel-utils.ts +109 -0
- package/src/subagents/runs/shared/pi-args.ts +220 -0
- package/src/subagents/runs/shared/pi-spawn.ts +115 -0
- package/src/subagents/runs/shared/run-history.ts +60 -0
- package/src/subagents/runs/shared/single-output.ts +164 -0
- package/src/subagents/runs/shared/subagent-control.ts +226 -0
- package/src/subagents/runs/shared/subagent-prompt-runtime.ts +170 -0
- package/src/subagents/runs/shared/worktree.ts +577 -0
- package/src/subagents/shared/artifacts.ts +98 -0
- package/src/subagents/shared/atomic-json.ts +16 -0
- package/src/subagents/shared/file-coalescer.ts +40 -0
- package/src/subagents/shared/fork-context.ts +76 -0
- package/src/subagents/shared/formatters.ts +133 -0
- package/src/subagents/shared/jsonl-writer.ts +81 -0
- package/src/subagents/shared/model-info.ts +78 -0
- package/src/subagents/shared/post-exit-stdio-guard.ts +85 -0
- package/src/subagents/shared/session-identity.ts +10 -0
- package/src/subagents/shared/session-tokens.ts +44 -0
- package/src/subagents/shared/settings.ts +397 -0
- package/src/subagents/shared/status-format.ts +49 -0
- package/src/subagents/shared/types.ts +822 -0
- package/src/subagents/shared/utils.ts +450 -0
- package/src/subagents/slash/prompt-template-bridge.ts +397 -0
- package/src/subagents/slash/slash-bridge.ts +174 -0
- package/src/subagents/slash/slash-commands.ts +528 -0
- package/src/subagents/slash/slash-live-state.ts +292 -0
- package/src/subagents/tui/render-helpers.ts +80 -0
- package/src/subagents/tui/render.ts +1358 -0
- package/templates/agents/local/supervisor.md +33 -0
- package/templates/agents/local/task-merger.md +27 -0
- package/templates/agents/local/task-reviewer.md +30 -0
- package/templates/agents/local/task-worker.md +34 -0
- package/templates/agents/supervisor-routing.md +92 -0
- package/templates/agents/supervisor.md +229 -0
- package/templates/agents/task-merger.md +214 -0
- package/templates/agents/task-reviewer.md +260 -0
- package/templates/agents/task-worker-segment.md +44 -0
- package/templates/agents/task-worker.md +557 -0
- package/templates/tasks/CONTEXT.md +30 -0
- package/templates/tasks/EXAMPLE-001-hello-world/PROMPT.md +98 -0
- package/templates/tasks/EXAMPLE-001-hello-world/STATUS.md +73 -0
- package/templates/tasks/EXAMPLE-002-parallel-smoke/PROMPT.md +97 -0
- package/templates/tasks/EXAMPLE-002-parallel-smoke/STATUS.md +73 -0
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatting, dashboard widget, wave plan display
|
|
3
|
+
* @module orch/formatting
|
|
4
|
+
*/
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
7
|
+
|
|
8
|
+
import { parseDependencyReference } from "./discovery.ts";
|
|
9
|
+
import type {
|
|
10
|
+
LaneAssignment,
|
|
11
|
+
MonitorState,
|
|
12
|
+
OrchBatchRuntimeState,
|
|
13
|
+
OrchDashboardViewModel,
|
|
14
|
+
OrchLaneCardData,
|
|
15
|
+
OrchSummaryCounts,
|
|
16
|
+
ParsedTask,
|
|
17
|
+
WaveComputationResult,
|
|
18
|
+
} from "./types.ts";
|
|
19
|
+
import { getTaskDurationMinutes, SIZE_DURATION_MINUTES } from "./types.ts";
|
|
20
|
+
|
|
21
|
+
// โโ Wave Output Formatting โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
22
|
+
|
|
23
|
+
// โโ Dependency Graph Formatting โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Format a dependency graph for display.
|
|
27
|
+
*
|
|
28
|
+
* Shows both upstream (what each task depends on) and downstream
|
|
29
|
+
* (what depends on each task) views. Output is deterministic:
|
|
30
|
+
* tasks sorted by ID, edges sorted by target ID.
|
|
31
|
+
*
|
|
32
|
+
* If `filterTaskId` is provided, only shows edges involving that task.
|
|
33
|
+
*/
|
|
34
|
+
export function formatDependencyGraph(
|
|
35
|
+
pending: Map<string, ParsedTask>,
|
|
36
|
+
completed: Set<string>,
|
|
37
|
+
filterTaskId?: string,
|
|
38
|
+
): string {
|
|
39
|
+
const lines: string[] = [];
|
|
40
|
+
|
|
41
|
+
// Sort tasks deterministically by ID
|
|
42
|
+
const sortedTasks = [...pending.values()].sort((a, b) => a.taskId.localeCompare(b.taskId));
|
|
43
|
+
|
|
44
|
+
// Build downstream index: taskID โ tasks that depend on it
|
|
45
|
+
const downstream = new Map<string, string[]>();
|
|
46
|
+
for (const task of sortedTasks) {
|
|
47
|
+
for (const depRaw of task.dependencies) {
|
|
48
|
+
const depId = parseDependencyReference(depRaw).taskId;
|
|
49
|
+
const existing = downstream.get(depId) || [];
|
|
50
|
+
existing.push(task.taskId);
|
|
51
|
+
downstream.set(depId, existing);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// If filtering to a single task
|
|
56
|
+
if (filterTaskId) {
|
|
57
|
+
const task = pending.get(filterTaskId);
|
|
58
|
+
if (!task) {
|
|
59
|
+
lines.push(`โ Task "${filterTaskId}" not found in pending tasks.`);
|
|
60
|
+
return lines.join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
lines.push(`๐ Dependencies for ${filterTaskId} (${task.taskName}):`);
|
|
64
|
+
lines.push("");
|
|
65
|
+
|
|
66
|
+
// Upstream: what this task depends on
|
|
67
|
+
lines.push(" โฌ Upstream (depends on):");
|
|
68
|
+
if (task.dependencies.length === 0) {
|
|
69
|
+
lines.push(" (none โ no dependencies)");
|
|
70
|
+
} else {
|
|
71
|
+
const sortedDeps = [...task.dependencies].sort();
|
|
72
|
+
for (const depRaw of sortedDeps) {
|
|
73
|
+
const depId = parseDependencyReference(depRaw).taskId;
|
|
74
|
+
const status = completed.has(depId)
|
|
75
|
+
? "โ
complete"
|
|
76
|
+
: pending.has(depId)
|
|
77
|
+
? "โณ pending"
|
|
78
|
+
: "โ unknown";
|
|
79
|
+
lines.push(` ${filterTaskId} โ ${depRaw} (${status})`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Downstream: what depends on this task
|
|
84
|
+
lines.push("");
|
|
85
|
+
lines.push(" โฌ Downstream (depended on by):");
|
|
86
|
+
const downstreamTasks = (downstream.get(filterTaskId) || []).sort();
|
|
87
|
+
if (downstreamTasks.length === 0) {
|
|
88
|
+
lines.push(" (none โ no tasks depend on this)");
|
|
89
|
+
} else {
|
|
90
|
+
for (const dep of downstreamTasks) {
|
|
91
|
+
lines.push(` ${dep} โ ${filterTaskId}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return lines.join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Full graph view
|
|
99
|
+
lines.push("๐ Dependency Graph:");
|
|
100
|
+
lines.push("");
|
|
101
|
+
|
|
102
|
+
let hasDeps = false;
|
|
103
|
+
|
|
104
|
+
// Section 1: Upstream view (what each task depends on)
|
|
105
|
+
lines.push(" โฌ Upstream (task โ depends on):");
|
|
106
|
+
for (const task of sortedTasks) {
|
|
107
|
+
if (task.dependencies.length > 0) {
|
|
108
|
+
hasDeps = true;
|
|
109
|
+
const sortedDeps = [...task.dependencies].sort();
|
|
110
|
+
for (const depRaw of sortedDeps) {
|
|
111
|
+
const depId = parseDependencyReference(depRaw).taskId;
|
|
112
|
+
const status = completed.has(depId)
|
|
113
|
+
? "โ
complete"
|
|
114
|
+
: pending.has(depId)
|
|
115
|
+
? "โณ pending"
|
|
116
|
+
: "โ unknown";
|
|
117
|
+
lines.push(` ${task.taskId} โ ${depRaw} (${status})`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!hasDeps) {
|
|
122
|
+
lines.push(" (none โ all tasks are independent)");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Section 2: Downstream view (what depends on each task)
|
|
126
|
+
lines.push("");
|
|
127
|
+
lines.push(" โฌ Downstream (task โ depended on by):");
|
|
128
|
+
let hasDownstream = false;
|
|
129
|
+
const allTargets = new Set<string>();
|
|
130
|
+
for (const task of sortedTasks) {
|
|
131
|
+
for (const depRaw of task.dependencies) {
|
|
132
|
+
allTargets.add(parseDependencyReference(depRaw).taskId);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const sortedTargets = [...allTargets].sort();
|
|
136
|
+
for (const target of sortedTargets) {
|
|
137
|
+
const dependents = (downstream.get(target) || []).sort();
|
|
138
|
+
if (dependents.length > 0) {
|
|
139
|
+
hasDownstream = true;
|
|
140
|
+
const status = completed.has(target) ? "โ
" : pending.has(target) ? "โณ" : "โ";
|
|
141
|
+
lines.push(` ${target} ${status} โ ${dependents.join(", ")}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (!hasDownstream) {
|
|
145
|
+
lines.push(" (none โ no downstream dependencies)");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Section 3: Independent tasks (no deps, nothing depends on them)
|
|
149
|
+
const independentTasks = sortedTasks.filter(
|
|
150
|
+
(t) => t.dependencies.length === 0 && !downstream.get(t.taskId)?.length,
|
|
151
|
+
);
|
|
152
|
+
if (independentTasks.length > 0) {
|
|
153
|
+
lines.push("");
|
|
154
|
+
lines.push(" โ Independent (no dependencies, nothing depends on them):");
|
|
155
|
+
for (const task of independentTasks) {
|
|
156
|
+
lines.push(` ${task.taskId} [${task.size}] ${task.taskName}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return lines.join("\n");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Format wave computation results as a readable execution plan.
|
|
165
|
+
*
|
|
166
|
+
* Output sections (fixed order):
|
|
167
|
+
* 1. Wave overview header
|
|
168
|
+
* 2. Per-wave: task count, lane count, parallel/serial indicator
|
|
169
|
+
* 3. Per-lane within wave: tasks with sizes, serial notes, lane weight
|
|
170
|
+
* 4. Per-wave: estimated duration (critical path = max lane duration)
|
|
171
|
+
* 5. Summary: total estimated duration, size-to-duration table
|
|
172
|
+
*
|
|
173
|
+
* Duration calculation:
|
|
174
|
+
* - Per lane: sum of task durations for tasks in that lane
|
|
175
|
+
* - Per wave: max lane duration (parallel bottleneck / critical path)
|
|
176
|
+
* - Total: sum of wave durations (waves run sequentially)
|
|
177
|
+
*/
|
|
178
|
+
export function formatWavePlan(
|
|
179
|
+
result: WaveComputationResult,
|
|
180
|
+
sizeWeights: Record<string, number>,
|
|
181
|
+
): string {
|
|
182
|
+
const lines: string[] = [];
|
|
183
|
+
|
|
184
|
+
if (result.errors.length > 0) {
|
|
185
|
+
lines.push("โ Wave Computation Errors:");
|
|
186
|
+
for (const err of result.errors) {
|
|
187
|
+
lines.push(` [${err.code}] ${err.message}`);
|
|
188
|
+
}
|
|
189
|
+
return lines.join("\n");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (result.waves.length === 0) {
|
|
193
|
+
lines.push("No waves to schedule.");
|
|
194
|
+
return lines.join("\n");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Count total tasks
|
|
198
|
+
const totalTasks = result.waves.reduce((sum, w) => sum + w.tasks.length, 0);
|
|
199
|
+
const maxLanesUsed = Math.max(
|
|
200
|
+
...result.waves.map((w) => {
|
|
201
|
+
const lanes = new Set(w.tasks.map((t) => t.lane));
|
|
202
|
+
return lanes.size;
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
lines.push(
|
|
207
|
+
`๐ Execution Plan: ${result.waves.length} wave(s), ` +
|
|
208
|
+
`${totalTasks} task(s), up to ${maxLanesUsed} lane(s)`,
|
|
209
|
+
);
|
|
210
|
+
lines.push("");
|
|
211
|
+
|
|
212
|
+
let totalEstimate = 0;
|
|
213
|
+
for (const wave of result.waves) {
|
|
214
|
+
// Group tasks by lane (deterministic: Map preserves insertion order)
|
|
215
|
+
const laneGroups = new Map<number, LaneAssignment[]>();
|
|
216
|
+
for (const assignment of wave.tasks) {
|
|
217
|
+
const existing = laneGroups.get(assignment.lane) || [];
|
|
218
|
+
existing.push(assignment);
|
|
219
|
+
laneGroups.set(assignment.lane, existing);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const laneCount = laneGroups.size;
|
|
223
|
+
const taskCount = wave.tasks.length;
|
|
224
|
+
const parallel = laneCount > 1 ? "parallel" : "serial";
|
|
225
|
+
|
|
226
|
+
lines.push(
|
|
227
|
+
` Wave ${wave.waveNumber}: ${taskCount} task(s) across ` + `${laneCount} lane(s) [${parallel}]`,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Calculate wave duration: critical path = max lane duration
|
|
231
|
+
let maxLaneDuration = 0;
|
|
232
|
+
|
|
233
|
+
// Sort lanes deterministically by lane number
|
|
234
|
+
const sortedLanes = [...laneGroups.entries()].sort((a, b) => a[0] - b[0]);
|
|
235
|
+
|
|
236
|
+
for (const [lane, assignments] of sortedLanes) {
|
|
237
|
+
// Sort tasks within lane by task ID for deterministic output
|
|
238
|
+
const sortedAssignments = [...assignments].sort((a, b) => a.taskId.localeCompare(b.taskId));
|
|
239
|
+
const taskList = sortedAssignments.map((a) => `${a.taskId} [${a.task.size}]`).join(", ");
|
|
240
|
+
const laneDuration = sortedAssignments.reduce(
|
|
241
|
+
(sum, a) => sum + getTaskDurationMinutes(a.task.size, sizeWeights),
|
|
242
|
+
0,
|
|
243
|
+
);
|
|
244
|
+
if (laneDuration > maxLaneDuration) maxLaneDuration = laneDuration;
|
|
245
|
+
const serialNote = sortedAssignments.length > 1 ? " (serial)" : "";
|
|
246
|
+
lines.push(` Lane ${lane}: ${taskList}${serialNote} ` + `[est. ${laneDuration} min]`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Critical path for this wave
|
|
250
|
+
totalEstimate += maxLaneDuration;
|
|
251
|
+
lines.push(` โฑ Wave duration: ${maxLaneDuration} min ` + `(critical path: longest lane)`);
|
|
252
|
+
lines.push("");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Summary with size-to-duration table
|
|
256
|
+
const totalHours = (totalEstimate / 60).toFixed(1);
|
|
257
|
+
lines.push(`๐ Total estimated duration: ${totalEstimate} min (~${totalHours} hours)`);
|
|
258
|
+
lines.push(
|
|
259
|
+
` Duration model: S=${SIZE_DURATION_MINUTES["S"]}m, ` +
|
|
260
|
+
`M=${SIZE_DURATION_MINUTES["M"]}m, L=${SIZE_DURATION_MINUTES["L"]}m`,
|
|
261
|
+
);
|
|
262
|
+
lines.push(
|
|
263
|
+
" Critical path: sum of per-wave bottleneck lanes " + "(waves sequential, lanes parallel)",
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
return lines.join("\n");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// โโ Summary Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Compute summary counts from batch state + optional monitor state.
|
|
273
|
+
*
|
|
274
|
+
* Pure function โ no side effects, deterministic output.
|
|
275
|
+
*/
|
|
276
|
+
export function computeOrchSummaryCounts(
|
|
277
|
+
batchState: OrchBatchRuntimeState,
|
|
278
|
+
monitorState?: MonitorState | null,
|
|
279
|
+
): OrchSummaryCounts {
|
|
280
|
+
let running = 0;
|
|
281
|
+
let stalled = 0;
|
|
282
|
+
|
|
283
|
+
// If we have live monitor data, count running/stalled from it
|
|
284
|
+
if (monitorState) {
|
|
285
|
+
for (const lane of monitorState.lanes) {
|
|
286
|
+
if (lane.currentTaskSnapshot) {
|
|
287
|
+
if (lane.currentTaskSnapshot.status === "stalled") {
|
|
288
|
+
stalled++;
|
|
289
|
+
} else if (lane.currentTaskSnapshot.status === "running") {
|
|
290
|
+
running++;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const completed = batchState.succeededTasks;
|
|
297
|
+
const failed = batchState.failedTasks;
|
|
298
|
+
const blocked = batchState.blockedTasks;
|
|
299
|
+
const total = batchState.totalTasks;
|
|
300
|
+
const queued = Math.max(
|
|
301
|
+
0,
|
|
302
|
+
total - completed - failed - blocked - stalled - running - batchState.skippedTasks,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
return { completed, running, queued, failed, blocked, stalled, total };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Format elapsed time from start/end timestamps.
|
|
310
|
+
*
|
|
311
|
+
* @param startMs - Start epoch ms
|
|
312
|
+
* @param endMs - End epoch ms (null = use current time)
|
|
313
|
+
* @returns Human-readable string, e.g., "2m 14s" or "1h 5m 30s"
|
|
314
|
+
*/
|
|
315
|
+
export function formatElapsedTime(startMs: number, endMs?: number | null): string {
|
|
316
|
+
if (startMs <= 0) return "0s";
|
|
317
|
+
const elapsed = (endMs ?? Date.now()) - startMs;
|
|
318
|
+
if (elapsed < 0) return "0s";
|
|
319
|
+
|
|
320
|
+
const totalSec = Math.floor(elapsed / 1000);
|
|
321
|
+
const hours = Math.floor(totalSec / 3600);
|
|
322
|
+
const minutes = Math.floor((totalSec % 3600) / 60);
|
|
323
|
+
const seconds = totalSec % 60;
|
|
324
|
+
|
|
325
|
+
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
|
|
326
|
+
if (minutes > 0) return `${minutes}m ${seconds}s`;
|
|
327
|
+
return `${seconds}s`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Build the dashboard view-model from runtime state.
|
|
332
|
+
*
|
|
333
|
+
* Pure function โ deterministic mapping from OrchBatchRuntimeState +
|
|
334
|
+
* optional MonitorState to render-ready OrchDashboardViewModel.
|
|
335
|
+
*
|
|
336
|
+
* Fallback behavior:
|
|
337
|
+
* - No batch โ idle view with zeroed counts
|
|
338
|
+
* - No monitor data โ empty lane cards, counts from batch state only
|
|
339
|
+
* - Missing STATUS.md โ "no data" in lane card
|
|
340
|
+
*/
|
|
341
|
+
export function buildDashboardViewModel(
|
|
342
|
+
batchState: OrchBatchRuntimeState,
|
|
343
|
+
monitorState?: MonitorState | null,
|
|
344
|
+
): OrchDashboardViewModel {
|
|
345
|
+
const summary = computeOrchSummaryCounts(batchState, monitorState);
|
|
346
|
+
const elapsed = formatElapsedTime(batchState.startedAt, batchState.endedAt);
|
|
347
|
+
|
|
348
|
+
const waveProgress =
|
|
349
|
+
batchState.totalWaves > 0
|
|
350
|
+
? `${Math.max(0, batchState.currentWaveIndex + 1)}/${batchState.totalWaves}`
|
|
351
|
+
: "0/0";
|
|
352
|
+
|
|
353
|
+
// Build lane cards from monitor state (if available) or current lanes
|
|
354
|
+
const laneCards: OrchLaneCardData[] = [];
|
|
355
|
+
|
|
356
|
+
// TP-170: Detect stale monitor data from prior waves.
|
|
357
|
+
// When wave N+1 starts, batchState.currentLanes is updated to wave N+1's
|
|
358
|
+
// lanes, but monitorState may still hold wave N's data until the first
|
|
359
|
+
// poll of wave N+1's monitor. Detect this mismatch by checking whether
|
|
360
|
+
// the monitor's lane numbers match the current allocation.
|
|
361
|
+
const monitorIsFresh =
|
|
362
|
+
monitorState &&
|
|
363
|
+
monitorState.lanes.length > 0 &&
|
|
364
|
+
// If no current allocation, monitor data is the best we have
|
|
365
|
+
// (covers terminal phases like completed/failed/stopped)
|
|
366
|
+
(batchState.currentLanes.length === 0 ||
|
|
367
|
+
// If allocated lanes exist, verify monitor lanes match them
|
|
368
|
+
monitorState.lanes.some((ml) =>
|
|
369
|
+
batchState.currentLanes.some((cl) => cl.laneNumber === ml.laneNumber),
|
|
370
|
+
));
|
|
371
|
+
|
|
372
|
+
// TP-170: Build a laneNumber โ AllocatedLane index for identity reconciliation.
|
|
373
|
+
// In workspace mode, the monitorโs sessionName (e.g., "orch-henry-api-lane-1")
|
|
374
|
+
// may differ from the V2 registry agentId ("orch-henry-lane-3-worker").
|
|
375
|
+
// Cross-referencing with the current allocation ensures the displayed session
|
|
376
|
+
// name matches the authoritative laneSessionId for the current wave.
|
|
377
|
+
const allocatedByLaneNumber = new Map<number, { laneSessionId: string; laneId: string }>();
|
|
378
|
+
for (const cl of batchState.currentLanes) {
|
|
379
|
+
allocatedByLaneNumber.set(cl.laneNumber, { laneSessionId: cl.laneSessionId, laneId: cl.laneId });
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (monitorIsFresh && monitorState) {
|
|
383
|
+
// Sort lanes by laneNumber (deterministic)
|
|
384
|
+
const sortedLanes = [...monitorState.lanes].sort((a, b) => a.laneNumber - b.laneNumber);
|
|
385
|
+
|
|
386
|
+
for (const lane of sortedLanes) {
|
|
387
|
+
const snap = lane.currentTaskSnapshot;
|
|
388
|
+
const alloc = allocatedByLaneNumber.get(lane.laneNumber);
|
|
389
|
+
|
|
390
|
+
// TP-170: Reconcile task-level vs lane-level sessionAlive.
|
|
391
|
+
// resolveTaskMonitorState may derive sessionAlive from the lane
|
|
392
|
+
// snapshot file (snap.status === "running") while the lane-level
|
|
393
|
+
// sessionAlive comes from isV2AgentAlive (PID check). When the
|
|
394
|
+
// task snapshot says "running" but the lane session is confirmed
|
|
395
|
+
// dead, the task is effectively failed โ not still running.
|
|
396
|
+
let status: OrchLaneCardData["status"] = "idle";
|
|
397
|
+
if (lane.failedTasks.length > 0) {
|
|
398
|
+
status = "failed";
|
|
399
|
+
} else if (snap?.status === "stalled") {
|
|
400
|
+
status = "stalled";
|
|
401
|
+
} else if (snap?.status === "running") {
|
|
402
|
+
// TP-170: TOCTOU guard โ if lane session is dead but task snapshot
|
|
403
|
+
// still says "running", treat as failed instead of showing
|
|
404
|
+
// "session dead" in the card. This prevents the false positive
|
|
405
|
+
// where the lane snapshot file lags behind the PID liveness check.
|
|
406
|
+
status = lane.sessionAlive ? "running" : "failed";
|
|
407
|
+
} else if (
|
|
408
|
+
lane.completedTasks.length > 0 &&
|
|
409
|
+
lane.remainingTasks.length === 0 &&
|
|
410
|
+
!lane.currentTaskId
|
|
411
|
+
) {
|
|
412
|
+
status = "succeeded";
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
laneCards.push({
|
|
416
|
+
laneNumber: lane.laneNumber,
|
|
417
|
+
laneId: alloc?.laneId || lane.laneId,
|
|
418
|
+
// TP-170: Prefer the allocationโs laneSessionId (current-wave authority)
|
|
419
|
+
// over the monitorโs sessionName which may use a stale or workspace-local
|
|
420
|
+
// name that doesnโt match the V2 registry.
|
|
421
|
+
sessionName: alloc?.laneSessionId || lane.sessionName,
|
|
422
|
+
sessionAlive: lane.sessionAlive,
|
|
423
|
+
currentTaskId: lane.currentTaskId,
|
|
424
|
+
currentStepName: snap?.currentStepName || null,
|
|
425
|
+
totalChecked: snap?.totalChecked || 0,
|
|
426
|
+
totalItems: snap?.totalItems || 0,
|
|
427
|
+
completedTasks: lane.completedTasks.length,
|
|
428
|
+
totalLaneTasks:
|
|
429
|
+
lane.completedTasks.length +
|
|
430
|
+
lane.failedTasks.length +
|
|
431
|
+
lane.remainingTasks.length +
|
|
432
|
+
(lane.currentTaskId ? 1 : 0),
|
|
433
|
+
status,
|
|
434
|
+
stallReason: snap?.stallReason || null,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
} else if (batchState.currentLanes.length > 0) {
|
|
438
|
+
// No fresh monitor data โ show lanes from allocation.
|
|
439
|
+
// This covers both initial startup (monitor hasn't polled yet)
|
|
440
|
+
// and wave transitions (monitor data is stale from prior wave).
|
|
441
|
+
const sortedLanes = [...batchState.currentLanes].sort((a, b) => a.laneNumber - b.laneNumber);
|
|
442
|
+
for (const lane of sortedLanes) {
|
|
443
|
+
laneCards.push({
|
|
444
|
+
laneNumber: lane.laneNumber,
|
|
445
|
+
laneId: lane.laneId,
|
|
446
|
+
sessionName: lane.laneSessionId,
|
|
447
|
+
sessionAlive: true, // assumed alive during allocation
|
|
448
|
+
currentTaskId: lane.tasks.length > 0 ? lane.tasks[0].taskId : null,
|
|
449
|
+
currentStepName: null,
|
|
450
|
+
totalChecked: 0,
|
|
451
|
+
totalItems: 0,
|
|
452
|
+
completedTasks: 0,
|
|
453
|
+
totalLaneTasks: lane.tasks.length,
|
|
454
|
+
status: "running",
|
|
455
|
+
stallReason: null,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Determine attach hint
|
|
461
|
+
let attachHint = "";
|
|
462
|
+
const aliveLane = laneCards.find((l) => l.sessionAlive && l.status === "running");
|
|
463
|
+
if (aliveLane) {
|
|
464
|
+
attachHint = `Use /orch-sessions to inspect active lane sessions (${aliveLane.sessionName})`;
|
|
465
|
+
} else if (laneCards.length > 0) {
|
|
466
|
+
attachHint = "Use /orch-sessions for active lane session list";
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Determine failure policy if batch was stopped
|
|
470
|
+
let failurePolicy: string | null = null;
|
|
471
|
+
if (batchState.phase === "stopped" && batchState.waveResults.length > 0) {
|
|
472
|
+
const lastWave = batchState.waveResults[batchState.waveResults.length - 1];
|
|
473
|
+
if (lastWave.stoppedEarly && lastWave.policyApplied) {
|
|
474
|
+
failurePolicy = lastWave.policyApplied;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
phase: batchState.phase,
|
|
480
|
+
batchId: batchState.batchId,
|
|
481
|
+
orchBranch: batchState.orchBranch || batchState.baseBranch || "",
|
|
482
|
+
waveProgress,
|
|
483
|
+
elapsed,
|
|
484
|
+
summary,
|
|
485
|
+
laneCards,
|
|
486
|
+
attachHint,
|
|
487
|
+
errors: batchState.errors,
|
|
488
|
+
failurePolicy,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// โโ Lane Card Rendering โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Render a single lane card for the dashboard.
|
|
496
|
+
*
|
|
497
|
+
* Follows the task-runner `renderStepCard` pattern:
|
|
498
|
+
* bordered box with lane info, status icon, task progress.
|
|
499
|
+
*
|
|
500
|
+
* @param card - Lane card data from view-model
|
|
501
|
+
* @param colWidth - Available width for the card (including borders)
|
|
502
|
+
* @param theme - Pi theme object for color styling
|
|
503
|
+
* @returns Array of styled string lines (one per card row)
|
|
504
|
+
*/
|
|
505
|
+
export function renderLaneCard(card: OrchLaneCardData, colWidth: number, theme: any): string[] {
|
|
506
|
+
const w = colWidth - 2; // inner width (excluding โ borders)
|
|
507
|
+
const trunc = (s: string, max: number) => (s.length > max ? s.slice(0, max - 3) + "..." : s);
|
|
508
|
+
|
|
509
|
+
// Status icon and color
|
|
510
|
+
const statusIcon =
|
|
511
|
+
card.status === "succeeded"
|
|
512
|
+
? "โ"
|
|
513
|
+
: card.status === "running"
|
|
514
|
+
? "โ"
|
|
515
|
+
: card.status === "failed"
|
|
516
|
+
? "โ"
|
|
517
|
+
: card.status === "stalled"
|
|
518
|
+
? "โ "
|
|
519
|
+
: "โ";
|
|
520
|
+
const statusColor =
|
|
521
|
+
card.status === "succeeded"
|
|
522
|
+
? "success"
|
|
523
|
+
: card.status === "running"
|
|
524
|
+
? "accent"
|
|
525
|
+
: card.status === "failed"
|
|
526
|
+
? "error"
|
|
527
|
+
: card.status === "stalled"
|
|
528
|
+
? "warning"
|
|
529
|
+
: "dim";
|
|
530
|
+
|
|
531
|
+
// Line 1: Session name (e.g., "โกorch-lane-1โค")
|
|
532
|
+
const sessionLabel = `โก${card.sessionName}โค`;
|
|
533
|
+
const sessionStr = theme.fg("accent", theme.bold(trunc(sessionLabel, w)));
|
|
534
|
+
const sessionVis = Math.min(sessionLabel.length, w);
|
|
535
|
+
|
|
536
|
+
// Line 2: Status + current task
|
|
537
|
+
const taskInfo = card.currentTaskId
|
|
538
|
+
? `${statusIcon} ${card.currentTaskId}`
|
|
539
|
+
: card.status === "succeeded"
|
|
540
|
+
? `${statusIcon} done`
|
|
541
|
+
: card.status === "failed"
|
|
542
|
+
? `${statusIcon} failed`
|
|
543
|
+
: `${statusIcon} idle`;
|
|
544
|
+
const taskStr = theme.fg(statusColor, trunc(taskInfo, w));
|
|
545
|
+
const taskVis = Math.min(taskInfo.length, w);
|
|
546
|
+
|
|
547
|
+
// Line 3: Step progress
|
|
548
|
+
let stepInfo = "";
|
|
549
|
+
if (card.currentStepName) {
|
|
550
|
+
stepInfo = trunc(card.currentStepName, w - 2);
|
|
551
|
+
} else if (card.currentTaskId && card.totalItems === 0) {
|
|
552
|
+
// TP-170: Distinguish startup-grace (no STATUS.md yet) from
|
|
553
|
+
// genuine stale data. During startup, the lane is alive but
|
|
554
|
+
// hasnโt written STATUS.md yet โ show "starting..." instead of
|
|
555
|
+
// the misleading "waiting for data..." which implies a problem.
|
|
556
|
+
stepInfo = card.sessionAlive ? "starting..." : "no status data";
|
|
557
|
+
} else if (!card.currentTaskId && card.status !== "idle") {
|
|
558
|
+
stepInfo = `${card.completedTasks}/${card.totalLaneTasks} tasks`;
|
|
559
|
+
}
|
|
560
|
+
const stepStr = theme.fg("muted", trunc(stepInfo, w));
|
|
561
|
+
const stepVis = Math.min(stepInfo.length, w);
|
|
562
|
+
|
|
563
|
+
// Line 4: Checkbox progress or stall reason
|
|
564
|
+
let extraInfo = "";
|
|
565
|
+
let extraColor = "dim";
|
|
566
|
+
if (card.stallReason) {
|
|
567
|
+
extraInfo = `โ ${trunc(card.stallReason, w - 4)}`;
|
|
568
|
+
extraColor = "warning";
|
|
569
|
+
} else if (card.totalItems > 0) {
|
|
570
|
+
extraInfo = `${card.totalChecked}/${card.totalItems} โ`;
|
|
571
|
+
extraColor = card.totalChecked === card.totalItems ? "success" : "muted";
|
|
572
|
+
} else if (!card.sessionAlive && card.status === "running") {
|
|
573
|
+
// TP-170: With the TOCTOU guard in buildDashboardViewModel, a lane
|
|
574
|
+
// with a dead session and task snapshot "running" now gets status
|
|
575
|
+
// "failed" instead. This branch guards any remaining edge cases
|
|
576
|
+
// (e.g., allocation-fallback lane assumed alive but actually dead).
|
|
577
|
+
extraInfo = "session ended";
|
|
578
|
+
extraColor = "warning";
|
|
579
|
+
}
|
|
580
|
+
const extraStr = theme.fg(extraColor, trunc(extraInfo, w));
|
|
581
|
+
const extraVis = Math.min(extraInfo.length, w);
|
|
582
|
+
|
|
583
|
+
// Build bordered card
|
|
584
|
+
const top = "โ" + "โ".repeat(w) + "โ";
|
|
585
|
+
const bot = "โ" + "โ".repeat(w) + "โ";
|
|
586
|
+
const border = (content: string, vis: number) =>
|
|
587
|
+
theme.fg("dim", "โ") + content + " ".repeat(Math.max(0, w - vis)) + theme.fg("dim", "โ");
|
|
588
|
+
|
|
589
|
+
return [
|
|
590
|
+
theme.fg("dim", top),
|
|
591
|
+
border(" " + sessionStr, 1 + sessionVis),
|
|
592
|
+
border(" " + taskStr, 1 + taskVis),
|
|
593
|
+
border(" " + stepStr, 1 + stepVis),
|
|
594
|
+
border(extraInfo ? " " + extraStr : "", extraVis ? 1 + extraVis : 0),
|
|
595
|
+
theme.fg("dim", bot),
|
|
596
|
+
];
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// โโ Core Widget โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Create the widget registration callback for the orchestrator dashboard.
|
|
603
|
+
*
|
|
604
|
+
* This is the main entry point for the dashboard widget. It captures
|
|
605
|
+
* batchState and monitorState references and returns a widget that
|
|
606
|
+
* re-renders on each paint cycle using the latest state.
|
|
607
|
+
*
|
|
608
|
+
* @param getBatchState - Getter for current batch state
|
|
609
|
+
* @param getMonitorState - Getter for current monitor state (may be null)
|
|
610
|
+
* @param sessionPrefix - Session prefix for lane identification
|
|
611
|
+
*/
|
|
612
|
+
export function createOrchWidget(
|
|
613
|
+
getBatchState: () => OrchBatchRuntimeState,
|
|
614
|
+
getMonitorState: () => MonitorState | null,
|
|
615
|
+
sessionPrefix: string,
|
|
616
|
+
): (_tui: any, theme: any) => { render(width: number): string[]; invalidate(): void } {
|
|
617
|
+
return (_tui: any, theme: any) => {
|
|
618
|
+
return {
|
|
619
|
+
render(width: number): string[] {
|
|
620
|
+
const batchState = getBatchState();
|
|
621
|
+
const monitorState = getMonitorState();
|
|
622
|
+
const vm = buildDashboardViewModel(batchState, monitorState);
|
|
623
|
+
|
|
624
|
+
// โโ Idle state โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
625
|
+
if (vm.phase === "idle") {
|
|
626
|
+
return [];
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const lines: string[] = [""];
|
|
630
|
+
|
|
631
|
+
// โโ Phase-specific rendering โโโโโโโโโโโโโโโโโโ
|
|
632
|
+
const phaseIcon =
|
|
633
|
+
vm.phase === "launching"
|
|
634
|
+
? "โ"
|
|
635
|
+
: vm.phase === "planning"
|
|
636
|
+
? "โ"
|
|
637
|
+
: vm.phase === "executing"
|
|
638
|
+
? "โ"
|
|
639
|
+
: vm.phase === "merging"
|
|
640
|
+
? "๐"
|
|
641
|
+
: vm.phase === "paused"
|
|
642
|
+
? "โธ"
|
|
643
|
+
: vm.phase === "stopped"
|
|
644
|
+
? "โ"
|
|
645
|
+
: vm.phase === "completed"
|
|
646
|
+
? "โ"
|
|
647
|
+
: vm.phase === "failed"
|
|
648
|
+
? "โ"
|
|
649
|
+
: "โ";
|
|
650
|
+
const phaseColor =
|
|
651
|
+
vm.phase === "executing"
|
|
652
|
+
? "accent"
|
|
653
|
+
: vm.phase === "merging"
|
|
654
|
+
? "accent"
|
|
655
|
+
: vm.phase === "completed"
|
|
656
|
+
? "success"
|
|
657
|
+
: vm.phase === "failed" || vm.phase === "stopped"
|
|
658
|
+
? "error"
|
|
659
|
+
: vm.phase === "paused"
|
|
660
|
+
? "warning"
|
|
661
|
+
: "dim";
|
|
662
|
+
|
|
663
|
+
// Header: phase icon + batch ID + wave + elapsed
|
|
664
|
+
const header =
|
|
665
|
+
theme.fg(phaseColor, ` ${phaseIcon} `) +
|
|
666
|
+
theme.fg("accent", theme.bold(vm.batchId || "โ")) +
|
|
667
|
+
theme.fg("dim", " ") +
|
|
668
|
+
theme.fg("warning", `W${vm.waveProgress}`) +
|
|
669
|
+
theme.fg("dim", " ยท ") +
|
|
670
|
+
theme.fg("muted", vm.elapsed);
|
|
671
|
+
lines.push(truncateToWidth(header, width));
|
|
672
|
+
|
|
673
|
+
// โโ Planning state โโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
674
|
+
if (vm.phase === "planning") {
|
|
675
|
+
lines.push(truncateToWidth(theme.fg("dim", " โ Planning batch..."), width));
|
|
676
|
+
return lines;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// โโ Progress bar โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
680
|
+
const { completed, failed, total } = vm.summary;
|
|
681
|
+
const done = completed + failed;
|
|
682
|
+
const pct = total > 0 ? Math.round((done / total) * 100) : 0;
|
|
683
|
+
const barWidth = Math.min(30, width - 20);
|
|
684
|
+
const filled = Math.round((pct / 100) * barWidth);
|
|
685
|
+
const progressBar =
|
|
686
|
+
theme.fg("dim", " ") +
|
|
687
|
+
theme.fg("warning", "[") +
|
|
688
|
+
theme.fg("success", "โ".repeat(filled)) +
|
|
689
|
+
theme.fg("dim", "โ".repeat(Math.max(0, barWidth - filled))) +
|
|
690
|
+
theme.fg("warning", "]") +
|
|
691
|
+
theme.fg("dim", " ") +
|
|
692
|
+
theme.fg("accent", `${done}/${total}`) +
|
|
693
|
+
theme.fg("dim", ` (${pct}%)`);
|
|
694
|
+
lines.push(truncateToWidth(progressBar, width));
|
|
695
|
+
|
|
696
|
+
// โโ Summary counts line โโโโโโโโโโโโโโโโโโโโโโโ
|
|
697
|
+
const countParts: string[] = [];
|
|
698
|
+
if (vm.summary.completed > 0) countParts.push(theme.fg("success", `${vm.summary.completed} โ`));
|
|
699
|
+
if (vm.summary.running > 0)
|
|
700
|
+
countParts.push(theme.fg("accent", `${vm.summary.running} running`));
|
|
701
|
+
if (vm.summary.queued > 0) countParts.push(theme.fg("dim", `${vm.summary.queued} queued`));
|
|
702
|
+
if (vm.summary.failed > 0) countParts.push(theme.fg("error", `${vm.summary.failed} โ`));
|
|
703
|
+
if (vm.summary.blocked > 0)
|
|
704
|
+
countParts.push(theme.fg("warning", `${vm.summary.blocked} blocked`));
|
|
705
|
+
if (vm.summary.stalled > 0)
|
|
706
|
+
countParts.push(theme.fg("warning", `${vm.summary.stalled} stalled`));
|
|
707
|
+
if (countParts.length > 0) {
|
|
708
|
+
lines.push(truncateToWidth(" " + countParts.join(theme.fg("dim", " ยท ")), width));
|
|
709
|
+
}
|
|
710
|
+
lines.push("");
|
|
711
|
+
|
|
712
|
+
// โโ Lane cards โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
713
|
+
if (
|
|
714
|
+
vm.laneCards.length > 0 &&
|
|
715
|
+
(vm.phase === "executing" || vm.phase === "merging" || vm.phase === "paused")
|
|
716
|
+
) {
|
|
717
|
+
const arrowWidth = 3;
|
|
718
|
+
const minCardWidth = 18;
|
|
719
|
+
const maxCols = Math.max(1, Math.floor((width + arrowWidth) / (minCardWidth + arrowWidth)));
|
|
720
|
+
const cols = Math.min(vm.laneCards.length, maxCols);
|
|
721
|
+
const colWidth = Math.max(minCardWidth, Math.floor((width - arrowWidth * (cols - 1)) / cols));
|
|
722
|
+
|
|
723
|
+
for (let rowStart = 0; rowStart < vm.laneCards.length; rowStart += cols) {
|
|
724
|
+
const rowCards = vm.laneCards.slice(rowStart, rowStart + cols);
|
|
725
|
+
const rendered = rowCards.map((c) => renderLaneCard(c, colWidth, theme));
|
|
726
|
+
|
|
727
|
+
if (rendered.length > 0) {
|
|
728
|
+
const cardHeight = rendered[0].length;
|
|
729
|
+
for (let line = 0; line < cardHeight; line++) {
|
|
730
|
+
let row = rendered[0][line];
|
|
731
|
+
for (let c = 1; c < rendered.length; c++) {
|
|
732
|
+
row += " "; // spacer between cards
|
|
733
|
+
row += rendered[c][line];
|
|
734
|
+
}
|
|
735
|
+
lines.push(truncateToWidth(row, width));
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// โโ Terminal states (completed/failed/stopped) โโ
|
|
742
|
+
if (vm.phase === "completed") {
|
|
743
|
+
lines.push(truncateToWidth(theme.fg("success", " โ
Batch complete"), width));
|
|
744
|
+
} else if (vm.phase === "failed") {
|
|
745
|
+
lines.push(truncateToWidth(theme.fg("error", " โ Batch failed"), width));
|
|
746
|
+
for (const err of vm.errors.slice(0, 3)) {
|
|
747
|
+
lines.push(truncateToWidth(theme.fg("error", ` ${err.slice(0, 80)}`), width));
|
|
748
|
+
}
|
|
749
|
+
} else if (vm.phase === "stopped") {
|
|
750
|
+
lines.push(
|
|
751
|
+
truncateToWidth(theme.fg("error", ` โ Stopped by ${vm.failurePolicy || "policy"}`), width),
|
|
752
|
+
);
|
|
753
|
+
} else if (vm.phase === "merging") {
|
|
754
|
+
lines.push("");
|
|
755
|
+
lines.push(
|
|
756
|
+
truncateToWidth(
|
|
757
|
+
theme.fg("accent", ` ๐ Merging lane branches into ${vm.orchBranch || "orch branch"}...`),
|
|
758
|
+
width,
|
|
759
|
+
),
|
|
760
|
+
);
|
|
761
|
+
} else if (vm.phase === "paused") {
|
|
762
|
+
lines.push("");
|
|
763
|
+
lines.push(
|
|
764
|
+
truncateToWidth(
|
|
765
|
+
theme.fg("warning", " โธ Batch paused โ lanes will stop after current tasks"),
|
|
766
|
+
width,
|
|
767
|
+
),
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// โโ Footer: attach hint โโโโโโโโโโโโโโโโโโโโโโโ
|
|
772
|
+
if (
|
|
773
|
+
vm.attachHint &&
|
|
774
|
+
(vm.phase === "executing" || vm.phase === "merging" || vm.phase === "paused")
|
|
775
|
+
) {
|
|
776
|
+
lines.push("");
|
|
777
|
+
lines.push(truncateToWidth(theme.fg("dim", ` ๐ก ${vm.attachHint}`), width));
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
return lines;
|
|
781
|
+
},
|
|
782
|
+
invalidate() {},
|
|
783
|
+
};
|
|
784
|
+
};
|
|
785
|
+
}
|