work-kit-cli 0.2.7 → 0.3.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/README.md +13 -13
- package/cli/src/commands/bootstrap.ts +39 -13
- package/cli/src/commands/cancel.ts +1 -16
- package/cli/src/commands/complete.ts +92 -98
- package/cli/src/commands/completions.ts +2 -2
- package/cli/src/commands/doctor.ts +1 -1
- package/cli/src/commands/init.ts +40 -32
- package/cli/src/commands/loopback.ts +8 -11
- package/cli/src/commands/next.ts +64 -51
- package/cli/src/commands/pause-resume.test.ts +142 -0
- package/cli/src/commands/pause.ts +34 -0
- package/cli/src/commands/report.ts +217 -0
- package/cli/src/commands/resume.ts +38 -0
- package/cli/src/commands/setup.ts +136 -0
- package/cli/src/commands/status.ts +6 -6
- package/cli/src/commands/uninstall.ts +8 -3
- package/cli/src/commands/workflow.ts +27 -27
- package/cli/src/config/agent-map.ts +9 -9
- package/cli/src/config/constants.ts +44 -0
- package/cli/src/config/loopback-routes.ts +13 -13
- package/cli/src/config/project-config.test.ts +127 -0
- package/cli/src/config/project-config.ts +106 -0
- package/cli/src/config/{phases.ts → workflow.ts} +40 -23
- package/cli/src/context/prompt-builder.ts +10 -9
- package/cli/src/index.ts +63 -7
- package/cli/src/observer/data.ts +64 -56
- package/cli/src/observer/renderer.ts +162 -75
- package/cli/src/state/helpers.test.ts +28 -28
- package/cli/src/state/helpers.ts +37 -25
- package/cli/src/state/schema.ts +88 -45
- package/cli/src/state/store.ts +92 -7
- package/cli/src/state/validators.test.ts +13 -13
- package/cli/src/state/validators.ts +3 -4
- package/cli/src/utils/colors.ts +2 -0
- package/cli/src/utils/json.ts +20 -0
- package/cli/src/utils/time.ts +27 -0
- package/cli/src/{engine → workflow}/loopbacks.test.ts +2 -2
- package/cli/src/workflow/loopbacks.ts +42 -0
- package/cli/src/workflow/parallel.ts +64 -0
- package/cli/src/workflow/transitions.test.ts +129 -0
- package/cli/src/{engine → workflow}/transitions.ts +18 -22
- package/package.json +2 -2
- package/skills/auto-kit/SKILL.md +22 -22
- package/skills/cancel-kit/SKILL.md +4 -4
- package/skills/full-kit/SKILL.md +23 -23
- package/skills/pause-kit/SKILL.md +25 -0
- package/skills/resume-kit/SKILL.md +28 -0
- package/skills/wk-bootstrap/SKILL.md +5 -5
- package/skills/wk-build/SKILL.md +10 -10
- package/skills/wk-build/{stages → steps}/commit.md +1 -1
- package/skills/wk-build/{stages → steps}/core.md +3 -3
- package/skills/wk-build/{stages → steps}/integration.md +2 -2
- package/skills/wk-build/{stages → steps}/migration.md +1 -1
- package/skills/wk-build/{stages → steps}/red.md +1 -1
- package/skills/wk-build/{stages → steps}/refactor.md +1 -1
- package/skills/wk-build/{stages → steps}/setup.md +1 -1
- package/skills/wk-build/{stages → steps}/ui.md +1 -1
- package/skills/wk-deploy/SKILL.md +6 -6
- package/skills/wk-deploy/{stages → steps}/merge.md +1 -1
- package/skills/wk-deploy/{stages → steps}/monitor.md +1 -1
- package/skills/wk-deploy/{stages → steps}/remediate.md +1 -1
- package/skills/wk-plan/SKILL.md +13 -13
- package/skills/wk-plan/{stages → steps}/architecture.md +1 -1
- package/skills/wk-plan/{stages → steps}/audit.md +2 -2
- package/skills/wk-plan/{stages → steps}/blueprint.md +2 -2
- package/skills/wk-plan/{stages → steps}/clarify.md +1 -1
- package/skills/wk-plan/{stages → steps}/investigate.md +1 -1
- package/skills/wk-plan/{stages → steps}/scope.md +1 -1
- package/skills/wk-plan/{stages → steps}/sketch.md +1 -1
- package/skills/wk-plan/{stages → steps}/ux-flow.md +1 -1
- package/skills/wk-review/SKILL.md +10 -10
- package/skills/wk-review/{stages → steps}/compliance.md +1 -1
- package/skills/wk-review/{stages → steps}/handoff.md +2 -2
- package/skills/wk-review/{stages → steps}/performance.md +1 -1
- package/skills/wk-review/{stages → steps}/security.md +1 -1
- package/skills/wk-review/{stages → steps}/self-review.md +1 -1
- package/skills/wk-test/SKILL.md +8 -8
- package/skills/wk-test/{stages → steps}/e2e.md +1 -1
- package/skills/wk-test/{stages → steps}/validate.md +1 -1
- package/skills/wk-test/{stages → steps}/verify.md +1 -1
- package/skills/wk-wrap-up/SKILL.md +6 -5
- package/skills/wk-wrap-up/steps/summary.md +86 -0
- package/cli/src/engine/loopbacks.ts +0 -32
- package/cli/src/engine/parallel.ts +0 -60
- package/cli/src/engine/transitions.test.ts +0 -129
- /package/cli/src/{engine/phases.ts → workflow/gates.ts} +0 -0
package/cli/src/observer/data.ts
CHANGED
|
@@ -2,10 +2,14 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
4
|
import type { WorkKitState, PhaseName } from "../state/schema.js";
|
|
5
|
-
import { PHASE_NAMES,
|
|
6
|
-
import { readState, stateExists } from "../state/store.js";
|
|
5
|
+
import { PHASE_NAMES, STEPS_BY_PHASE, MODE_AUTO } from "../state/schema.js";
|
|
6
|
+
import { readState, stateExists, STATE_DIR, AWAITING_INPUT_MARKER_FILE, IDLE_MARKER_FILE } from "../state/store.js";
|
|
7
|
+
import { TRACKER_DIR, INDEX_FILE } from "../config/constants.js";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
const AWAITING_INPUT_MARKER = path.join(STATE_DIR, AWAITING_INPUT_MARKER_FILE);
|
|
10
|
+
const IDLE_MARKER = path.join(STATE_DIR, IDLE_MARKER_FILE);
|
|
11
|
+
|
|
12
|
+
// ── View Types ─────────────────────────────────────────────────────
|
|
9
13
|
|
|
10
14
|
export interface WorkItemView {
|
|
11
15
|
slug: string;
|
|
@@ -14,15 +18,17 @@ export interface WorkItemView {
|
|
|
14
18
|
classification?: string;
|
|
15
19
|
status: string;
|
|
16
20
|
currentPhase: string | null;
|
|
17
|
-
|
|
21
|
+
currentStep: string | null;
|
|
18
22
|
currentPhaseStartedAt?: string;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
currentStepStatus?: string;
|
|
24
|
+
currentStepIndex?: number;
|
|
25
|
+
currentStepStartedAt?: string;
|
|
22
26
|
currentPhaseTotal?: number;
|
|
23
27
|
gated: boolean;
|
|
28
|
+
awaitingInput: boolean;
|
|
29
|
+
idle: boolean;
|
|
24
30
|
worktreePath: string;
|
|
25
|
-
|
|
31
|
+
phaseSteps: { name: string; status: string; startedAt?: string; completedAt?: string; outcome?: string }[];
|
|
26
32
|
startedAt: string;
|
|
27
33
|
progress: { completed: number; total: number; percent: number };
|
|
28
34
|
phases: { name: string; status: string; startedAt?: string; completedAt?: string }[];
|
|
@@ -42,7 +48,7 @@ export interface DashboardData {
|
|
|
42
48
|
lastUpdated: Date;
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
// ── Worktree Discovery
|
|
51
|
+
// ── Worktree Discovery ─────────────────────────────────────────────
|
|
46
52
|
|
|
47
53
|
export function discoverWorktrees(mainRepoRoot: string): string[] {
|
|
48
54
|
let output: string;
|
|
@@ -74,7 +80,7 @@ export function discoverWorktrees(mainRepoRoot: string): string[] {
|
|
|
74
80
|
return worktrees;
|
|
75
81
|
}
|
|
76
82
|
|
|
77
|
-
// ── Collect Single Work Item
|
|
83
|
+
// ── Collect Single Work Item ───────────────────────────────────────
|
|
78
84
|
|
|
79
85
|
export function collectWorkItem(worktreeRoot: string): WorkItemView | null {
|
|
80
86
|
if (!stateExists(worktreeRoot)) return null;
|
|
@@ -91,30 +97,29 @@ export function collectWorkItem(worktreeRoot: string): WorkItemView | null {
|
|
|
91
97
|
let total = 0;
|
|
92
98
|
const phaseViews: { name: string; status: string; startedAt?: string; completedAt?: string }[] = [];
|
|
93
99
|
|
|
94
|
-
const phaseList: PhaseName[] = state.mode ===
|
|
100
|
+
const phaseList: PhaseName[] = state.mode === MODE_AUTO && state.workflow
|
|
95
101
|
? getAutoKitPhases(state)
|
|
96
102
|
: [...PHASE_NAMES];
|
|
97
103
|
|
|
98
|
-
// Track current phase
|
|
104
|
+
// Track current phase step position
|
|
99
105
|
let currentPhaseStartedAt: string | undefined;
|
|
100
|
-
let
|
|
101
|
-
let
|
|
102
|
-
let
|
|
106
|
+
let currentStepStatus: string | undefined;
|
|
107
|
+
let currentStepIndex: number | undefined;
|
|
108
|
+
let currentStepStartedAt: string | undefined;
|
|
103
109
|
let currentPhaseTotal: number | undefined;
|
|
104
|
-
let
|
|
110
|
+
let phaseSteps: WorkItemView["phaseSteps"] = [];
|
|
105
111
|
|
|
106
112
|
for (const phaseName of phaseList) {
|
|
107
113
|
const phase = state.phases[phaseName];
|
|
108
114
|
if (!phase) {
|
|
109
115
|
phaseViews.push({ name: phaseName, status: "pending" });
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
total += subs.length;
|
|
116
|
+
const steps = STEPS_BY_PHASE[phaseName];
|
|
117
|
+
total += steps.length;
|
|
113
118
|
continue;
|
|
114
119
|
}
|
|
115
120
|
|
|
116
|
-
// If any
|
|
117
|
-
const hasWaiting = Object.values(phase.
|
|
121
|
+
// If any step is "waiting", show the phase as waiting in the view
|
|
122
|
+
const hasWaiting = Object.values(phase.steps).some(s => s.status === "waiting");
|
|
118
123
|
phaseViews.push({
|
|
119
124
|
name: phaseName,
|
|
120
125
|
status: hasWaiting ? "waiting" : phase.status,
|
|
@@ -122,21 +127,18 @@ export function collectWorkItem(worktreeRoot: string): WorkItemView | null {
|
|
|
122
127
|
completedAt: phase.completedAt,
|
|
123
128
|
});
|
|
124
129
|
|
|
125
|
-
const
|
|
126
|
-
if (
|
|
127
|
-
// Use default substages — skip entirely skipped phases
|
|
130
|
+
const stepKeys = Object.keys(phase.steps);
|
|
131
|
+
if (stepKeys.length === 0) {
|
|
128
132
|
if (phase.status === "skipped") continue;
|
|
129
|
-
const defaults =
|
|
133
|
+
const defaults = STEPS_BY_PHASE[phaseName];
|
|
130
134
|
total += defaults.length;
|
|
131
135
|
if (phase.status === "completed") completed += defaults.length;
|
|
132
136
|
} else {
|
|
133
|
-
|
|
134
|
-
const activeKeys = subStageKeys.filter(k => phase.subStages[k].status !== "skipped");
|
|
137
|
+
const activeKeys = stepKeys.filter(k => phase.steps[k].status !== "skipped");
|
|
135
138
|
for (const key of activeKeys) {
|
|
136
|
-
const
|
|
139
|
+
const s = phase.steps[key];
|
|
137
140
|
total++;
|
|
138
|
-
|
|
139
|
-
if (sub.status === "completed") {
|
|
141
|
+
if (s.status === "completed") {
|
|
140
142
|
completed++;
|
|
141
143
|
}
|
|
142
144
|
}
|
|
@@ -144,21 +146,21 @@ export function collectWorkItem(worktreeRoot: string): WorkItemView | null {
|
|
|
144
146
|
if (phaseName === state.currentPhase) {
|
|
145
147
|
currentPhaseStartedAt = phase.startedAt;
|
|
146
148
|
currentPhaseTotal = activeKeys.length;
|
|
147
|
-
if (state.
|
|
148
|
-
const idx = activeKeys.indexOf(state.
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
if (state.currentStep) {
|
|
150
|
+
const idx = activeKeys.indexOf(state.currentStep);
|
|
151
|
+
currentStepIndex = idx >= 0 ? idx + 1 : undefined;
|
|
152
|
+
const s = phase.steps[state.currentStep];
|
|
153
|
+
currentStepStatus = s?.status;
|
|
154
|
+
currentStepStartedAt = s?.startedAt;
|
|
153
155
|
}
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
+
phaseSteps = activeKeys.map(key => {
|
|
157
|
+
const s = phase.steps[key];
|
|
156
158
|
return {
|
|
157
159
|
name: key,
|
|
158
|
-
status:
|
|
159
|
-
startedAt:
|
|
160
|
-
completedAt:
|
|
161
|
-
outcome:
|
|
160
|
+
status: s.status,
|
|
161
|
+
startedAt: s.startedAt,
|
|
162
|
+
completedAt: s.completedAt,
|
|
163
|
+
outcome: s.outcome,
|
|
162
164
|
};
|
|
163
165
|
});
|
|
164
166
|
}
|
|
@@ -173,10 +175,10 @@ export function collectWorkItem(worktreeRoot: string): WorkItemView | null {
|
|
|
173
175
|
count: loopbacks.length,
|
|
174
176
|
lastReason: loopbacks.length > 0 ? loopbacks[loopbacks.length - 1].reason : undefined,
|
|
175
177
|
lastFrom: loopbacks.length > 0
|
|
176
|
-
? `${loopbacks[loopbacks.length - 1].from.phase}/${loopbacks[loopbacks.length - 1].from.
|
|
178
|
+
? `${loopbacks[loopbacks.length - 1].from.phase}/${loopbacks[loopbacks.length - 1].from.step}`
|
|
177
179
|
: undefined,
|
|
178
180
|
lastTo: loopbacks.length > 0
|
|
179
|
-
? `${loopbacks[loopbacks.length - 1].to.phase}/${loopbacks[loopbacks.length - 1].to.
|
|
181
|
+
? `${loopbacks[loopbacks.length - 1].to.phase}/${loopbacks[loopbacks.length - 1].to.step}`
|
|
180
182
|
: undefined,
|
|
181
183
|
};
|
|
182
184
|
|
|
@@ -187,15 +189,23 @@ export function collectWorkItem(worktreeRoot: string): WorkItemView | null {
|
|
|
187
189
|
classification: state.classification,
|
|
188
190
|
status: state.status,
|
|
189
191
|
currentPhase: state.currentPhase,
|
|
190
|
-
|
|
192
|
+
currentStep: state.currentStep,
|
|
191
193
|
currentPhaseStartedAt,
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
194
|
+
currentStepStatus,
|
|
195
|
+
currentStepIndex,
|
|
196
|
+
currentStepStartedAt,
|
|
195
197
|
currentPhaseTotal,
|
|
196
198
|
gated: state.gated ?? false,
|
|
199
|
+
awaitingInput: fs.existsSync(path.join(worktreeRoot, AWAITING_INPUT_MARKER)),
|
|
200
|
+
// Idle badge only fires when the agent ended its turn *mid-step*
|
|
201
|
+
// (suggesting it asked a prose question) — not during normal gaps
|
|
202
|
+
// between steps.
|
|
203
|
+
idle:
|
|
204
|
+
fs.existsSync(path.join(worktreeRoot, IDLE_MARKER)) &&
|
|
205
|
+
state.status === "in-progress" &&
|
|
206
|
+
currentStepStatus === "in-progress",
|
|
197
207
|
worktreePath: state.metadata.worktreeRoot,
|
|
198
|
-
|
|
208
|
+
phaseSteps,
|
|
199
209
|
startedAt: state.started,
|
|
200
210
|
progress: { completed, total, percent },
|
|
201
211
|
phases: phaseViews,
|
|
@@ -206,17 +216,17 @@ export function collectWorkItem(worktreeRoot: string): WorkItemView | null {
|
|
|
206
216
|
function getAutoKitPhases(state: WorkKitState): PhaseName[] {
|
|
207
217
|
if (!state.workflow) return [...PHASE_NAMES];
|
|
208
218
|
const phases = new Set<PhaseName>();
|
|
209
|
-
for (const
|
|
210
|
-
if (
|
|
219
|
+
for (const ws of state.workflow) {
|
|
220
|
+
if (ws.included) phases.add(ws.phase);
|
|
211
221
|
}
|
|
212
222
|
// Maintain canonical order
|
|
213
223
|
return PHASE_NAMES.filter(p => phases.has(p));
|
|
214
224
|
}
|
|
215
225
|
|
|
216
|
-
// ── Collect Completed Items
|
|
226
|
+
// ── Collect Completed Items ────────────────────────────────────────
|
|
217
227
|
|
|
218
228
|
export function collectCompletedItems(mainRepoRoot: string): CompletedItemView[] {
|
|
219
|
-
const indexPath = path.join(mainRepoRoot,
|
|
229
|
+
const indexPath = path.join(mainRepoRoot, TRACKER_DIR, INDEX_FILE);
|
|
220
230
|
if (!fs.existsSync(indexPath)) return [];
|
|
221
231
|
|
|
222
232
|
let content: string;
|
|
@@ -227,7 +237,6 @@ export function collectCompletedItems(mainRepoRoot: string): CompletedItemView[]
|
|
|
227
237
|
}
|
|
228
238
|
|
|
229
239
|
const items: CompletedItemView[] = [];
|
|
230
|
-
// Format: | Date | Slug | PR | Status | Phases |
|
|
231
240
|
const lines = content.split("\n");
|
|
232
241
|
for (const line of lines) {
|
|
233
242
|
const match = line.match(/^\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|/);
|
|
@@ -245,7 +254,7 @@ export function collectCompletedItems(mainRepoRoot: string): CompletedItemView[]
|
|
|
245
254
|
return items;
|
|
246
255
|
}
|
|
247
256
|
|
|
248
|
-
// ── Collect All Dashboard Data
|
|
257
|
+
// ── Collect All Dashboard Data ─────────────────────────────────────
|
|
249
258
|
|
|
250
259
|
export function collectDashboardData(mainRepoRoot: string, cachedWorktrees?: string[]): DashboardData {
|
|
251
260
|
const worktrees = cachedWorktrees ?? discoverWorktrees(mainRepoRoot);
|
|
@@ -280,7 +289,6 @@ export function collectDashboardData(mainRepoRoot: string, cachedWorktrees?: str
|
|
|
280
289
|
});
|
|
281
290
|
|
|
282
291
|
// Merge completed items from worktrees and index file, dedup by slug
|
|
283
|
-
// Also exclude any slug that is currently active (re-initialized features)
|
|
284
292
|
const activeSlugs = new Set(activeItems.map(i => i.slug));
|
|
285
293
|
const indexItems = collectCompletedItems(mainRepoRoot);
|
|
286
294
|
const seen = new Set(completedFromWorktrees.map(i => i.slug));
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
bold, dim, green, yellow, red, cyan, magenta,
|
|
3
|
-
bgYellow, bgCyan, bgRed, bgMagenta,
|
|
4
|
-
boldCyan, boldGreen,
|
|
3
|
+
bgYellow, bgCyan, bgRed, bgMagenta, bgGreen, bgBlue,
|
|
4
|
+
boldCyan, boldGreen, boldMagenta,
|
|
5
5
|
} from "../utils/colors.js";
|
|
6
|
+
import { formatDurationMs, formatDurationSince } from "../utils/time.js";
|
|
7
|
+
import { MODE_FULL } from "../state/schema.js";
|
|
6
8
|
import type { DashboardData, WorkItemView, CompletedItemView } from "./data.js";
|
|
7
9
|
|
|
8
10
|
// ── Spinners & Animation Frames ─────────────────────────────────────
|
|
@@ -63,19 +65,7 @@ function formatTimeAgo(dateStr: string): string {
|
|
|
63
65
|
return `${weeks}w ago`;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
const now = Date.now();
|
|
68
|
-
const start = new Date(startStr).getTime();
|
|
69
|
-
if (isNaN(start)) return "";
|
|
70
|
-
const diffMs = now - start;
|
|
71
|
-
const sec = Math.floor(diffMs / 1000);
|
|
72
|
-
if (sec < 60) return `${sec}s`;
|
|
73
|
-
const min = Math.floor(sec / 60);
|
|
74
|
-
if (min < 60) return `${min}m`;
|
|
75
|
-
const hr = Math.floor(min / 60);
|
|
76
|
-
const remMin = min % 60;
|
|
77
|
-
return remMin > 0 ? `${hr}h${remMin}m` : `${hr}h`;
|
|
78
|
-
}
|
|
68
|
+
const formatDuration = formatDurationSince;
|
|
79
69
|
|
|
80
70
|
// ── Box Drawing ─────────────────────────────────────────────────────
|
|
81
71
|
|
|
@@ -157,7 +147,7 @@ function phaseIndicator(status: string, tick: number = 0): string {
|
|
|
157
147
|
}
|
|
158
148
|
}
|
|
159
149
|
|
|
160
|
-
function
|
|
150
|
+
function stepIndicator(status: string, tick: number): string {
|
|
161
151
|
switch (status) {
|
|
162
152
|
case "completed": return green("●");
|
|
163
153
|
case "in-progress": return cyan(pulse(tick));
|
|
@@ -169,13 +159,20 @@ function subStageIndicator(status: string, tick: number): string {
|
|
|
169
159
|
}
|
|
170
160
|
}
|
|
171
161
|
|
|
162
|
+
function phaseDisplayName(name: string): string {
|
|
163
|
+
// Uppercase with letter-spacing to visually distinguish phases from steps.
|
|
164
|
+
// e.g. "plan" → "P L A N", "wrap-up" → "W R A P - U P"
|
|
165
|
+
return name.toUpperCase().split("").join(" ");
|
|
166
|
+
}
|
|
167
|
+
|
|
172
168
|
function phaseName(name: string, status: string, tick: number): string {
|
|
169
|
+
const display = phaseDisplayName(name);
|
|
173
170
|
switch (status) {
|
|
174
|
-
case "completed": return
|
|
175
|
-
case "in-progress": return tick % 2 === 0 ? boldCyan(
|
|
176
|
-
case "waiting": return yellow(
|
|
177
|
-
case "failed": return red(
|
|
178
|
-
default: return dim(
|
|
171
|
+
case "completed": return boldGreen(display);
|
|
172
|
+
case "in-progress": return tick % 2 === 0 ? boldCyan(display) : bold(cyan(display));
|
|
173
|
+
case "waiting": return bold(yellow(display));
|
|
174
|
+
case "failed": return bold(red(display));
|
|
175
|
+
default: return dim(display);
|
|
179
176
|
}
|
|
180
177
|
}
|
|
181
178
|
|
|
@@ -192,40 +189,112 @@ function statusDot(status: string): string {
|
|
|
192
189
|
// ── Badges ──────────────────────────────────────────────────────────
|
|
193
190
|
|
|
194
191
|
function renderModeBadge(mode: string): string {
|
|
195
|
-
return mode ===
|
|
192
|
+
return mode === MODE_FULL ? bgCyan(" Full Kit ") : bgYellow(" Auto Kit ");
|
|
196
193
|
}
|
|
197
194
|
|
|
198
195
|
function renderGatedBadge(): string {
|
|
199
196
|
return bgMagenta(" GATED ");
|
|
200
197
|
}
|
|
201
198
|
|
|
199
|
+
function renderClassificationBadge(classification: string): string {
|
|
200
|
+
const label = ` ${classification.toUpperCase()} `;
|
|
201
|
+
switch (classification) {
|
|
202
|
+
case "bug-fix": return bgRed(label);
|
|
203
|
+
case "small-change": return bgGreen(label);
|
|
204
|
+
case "refactor": return bgCyan(label);
|
|
205
|
+
case "feature": return bgBlue(label);
|
|
206
|
+
case "large-feature": return bgMagenta(label);
|
|
207
|
+
default: return bgYellow(label);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
202
211
|
// ── Phase Pipeline ──────────────────────────────────────────────────
|
|
203
212
|
|
|
213
|
+
function phaseDuration(p: { status: string; startedAt?: string; completedAt?: string }): string {
|
|
214
|
+
if (p.status === "completed" && p.startedAt && p.completedAt) {
|
|
215
|
+
const ms = new Date(p.completedAt).getTime() - new Date(p.startedAt).getTime();
|
|
216
|
+
return formatDurationMs(ms);
|
|
217
|
+
}
|
|
218
|
+
if ((p.status === "in-progress" || p.status === "waiting") && p.startedAt) {
|
|
219
|
+
return formatDuration(p.startedAt);
|
|
220
|
+
}
|
|
221
|
+
return "";
|
|
222
|
+
}
|
|
223
|
+
|
|
204
224
|
function renderPhasePipeline(
|
|
205
225
|
phases: WorkItemView["phases"],
|
|
206
|
-
tick: number
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
226
|
+
tick: number,
|
|
227
|
+
awaitingInput: boolean = false,
|
|
228
|
+
currentPhase: string | null = null,
|
|
229
|
+
): string[] {
|
|
230
|
+
const connector = dim(" ── ");
|
|
231
|
+
const connectorLen = 4; // " ── "
|
|
232
|
+
|
|
233
|
+
// Build top line (icons + names) and bottom line (timing, aligned)
|
|
234
|
+
const topParts: string[] = [];
|
|
235
|
+
const bottomParts: string[] = [];
|
|
236
|
+
|
|
237
|
+
for (let i = 0; i < phases.length; i++) {
|
|
238
|
+
const p = phases[i];
|
|
239
|
+
const isBlockedHere = awaitingInput && p.name === currentPhase && p.status === "in-progress";
|
|
240
|
+
const icon = isBlockedHere
|
|
241
|
+
? magenta(tick % 2 === 0 ? "▶" : "▷")
|
|
242
|
+
: phaseIndicator(p.status, tick);
|
|
243
|
+
const name = isBlockedHere
|
|
244
|
+
? boldMagenta(phaseDisplayName(p.name))
|
|
245
|
+
: phaseName(p.name, p.status, tick);
|
|
246
|
+
const segment = `${icon} ${name}`;
|
|
247
|
+
topParts.push(segment);
|
|
248
|
+
|
|
249
|
+
// Calculate plain width of this segment for alignment
|
|
250
|
+
const segPlainLen = stripAnsi(segment).length;
|
|
251
|
+
const dur = phaseDuration(p);
|
|
252
|
+
|
|
253
|
+
if (dur) {
|
|
254
|
+
// Color the duration based on status
|
|
255
|
+
let durStyled: string;
|
|
256
|
+
if (p.status === "completed") durStyled = dim(dur);
|
|
257
|
+
else if (p.status === "in-progress") durStyled = cyan(dur);
|
|
258
|
+
else if (p.status === "waiting") durStyled = yellow(dur);
|
|
259
|
+
else durStyled = dim(dur);
|
|
260
|
+
|
|
261
|
+
// Center the duration under the segment
|
|
262
|
+
const durPlainLen = stripAnsi(durStyled).length;
|
|
263
|
+
const pad = Math.max(0, Math.floor((segPlainLen - durPlainLen) / 2));
|
|
264
|
+
bottomParts.push(" ".repeat(pad) + durStyled + " ".repeat(Math.max(0, segPlainLen - durPlainLen - pad)));
|
|
265
|
+
} else {
|
|
266
|
+
bottomParts.push(" ".repeat(segPlainLen));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Add connector spacing to bottom line too
|
|
270
|
+
if (i < phases.length - 1) {
|
|
271
|
+
bottomParts.push(" ".repeat(connectorLen));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const topLine = topParts.join(connector);
|
|
276
|
+
const bottomLine = bottomParts.join("");
|
|
277
|
+
|
|
278
|
+
// Only show bottom line if there's at least one duration
|
|
279
|
+
const hasAnyDuration = phases.some(p => phaseDuration(p) !== "");
|
|
280
|
+
if (hasAnyDuration) {
|
|
281
|
+
return [topLine, bottomLine];
|
|
213
282
|
}
|
|
214
|
-
return
|
|
283
|
+
return [topLine];
|
|
215
284
|
}
|
|
216
285
|
|
|
217
|
-
// ──
|
|
286
|
+
// ── Step Detail Box ─────────────────────────────────────────────────
|
|
218
287
|
|
|
219
|
-
function
|
|
288
|
+
function renderStepBox(
|
|
220
289
|
item: WorkItemView,
|
|
221
290
|
innerWidth: number,
|
|
222
291
|
tick: number
|
|
223
292
|
): string[] {
|
|
224
|
-
const subs = item.
|
|
293
|
+
const subs = item.phaseSteps;
|
|
225
294
|
if (!subs || subs.length === 0 || !item.currentPhase) return [];
|
|
226
295
|
|
|
227
296
|
const lines: string[] = [];
|
|
228
|
-
const label =
|
|
297
|
+
const label = bold(phaseDisplayName(item.currentPhase));
|
|
229
298
|
const boxInner = innerWidth - 8; // indent + border padding
|
|
230
299
|
|
|
231
300
|
// Top border with phase label
|
|
@@ -233,29 +302,42 @@ function renderSubStageBox(
|
|
|
233
302
|
const topRule = dim("┌─ ") + label + dim(" " + "─".repeat(Math.max(0, boxInner - labelLen - 2)) + "┐");
|
|
234
303
|
lines.push(" " + topRule);
|
|
235
304
|
|
|
236
|
-
// Render
|
|
305
|
+
// Render steps in rows that fit the width
|
|
237
306
|
const entries: string[] = [];
|
|
238
307
|
for (const ss of subs) {
|
|
239
|
-
|
|
308
|
+
// Highlight the current in-progress step in magenta if the agent is
|
|
309
|
+
// blocked waiting for human input (permission prompt / AskUserQuestion).
|
|
310
|
+
const isBlockedHere =
|
|
311
|
+
item.awaitingInput && ss.status === "in-progress" && ss.name === item.currentStep;
|
|
312
|
+
|
|
313
|
+
let icon: string;
|
|
240
314
|
let nameStr: string;
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
315
|
+
if (isBlockedHere) {
|
|
316
|
+
// Pulsing magenta pointer + bold magenta name
|
|
317
|
+
icon = magenta(tick % 2 === 0 ? "▶" : "▷");
|
|
318
|
+
nameStr = boldMagenta(ss.name);
|
|
319
|
+
} else {
|
|
320
|
+
icon = stepIndicator(ss.status, tick);
|
|
321
|
+
switch (ss.status) {
|
|
322
|
+
case "completed": nameStr = green(ss.name); break;
|
|
323
|
+
case "in-progress": nameStr = boldCyan(ss.name); break;
|
|
324
|
+
case "waiting": nameStr = yellow(ss.name); break;
|
|
325
|
+
case "failed": nameStr = red(ss.name); break;
|
|
326
|
+
default: nameStr = dim(ss.name);
|
|
327
|
+
}
|
|
247
328
|
}
|
|
248
|
-
|
|
329
|
+
|
|
249
330
|
let duration = "";
|
|
250
331
|
if (ss.status === "completed" && ss.startedAt && ss.completedAt) {
|
|
251
332
|
const ms = new Date(ss.completedAt).getTime() - new Date(ss.startedAt).getTime();
|
|
252
|
-
|
|
253
|
-
if (sec < 60) duration = dim(` ${sec}s`);
|
|
254
|
-
else duration = dim(` ${Math.floor(sec / 60)}m`);
|
|
333
|
+
duration = dim(` ${formatDurationMs(ms)}`);
|
|
255
334
|
} else if (ss.status === "in-progress" && ss.startedAt) {
|
|
256
335
|
duration = dim(` ${formatDuration(ss.startedAt)}`);
|
|
257
336
|
}
|
|
258
|
-
|
|
337
|
+
|
|
338
|
+
// Append an inline "waiting you" hint next to the blocked step
|
|
339
|
+
const hint = isBlockedHere ? magenta(" ← waiting you") : "";
|
|
340
|
+
entries.push(`${icon} ${nameStr}${duration}${hint}`);
|
|
259
341
|
}
|
|
260
342
|
|
|
261
343
|
// Flow entries into rows
|
|
@@ -303,28 +385,24 @@ function renderWorkItem(item: WorkItemView, innerWidth: number, tick: number): s
|
|
|
303
385
|
const gap1 = Math.max(2, innerWidth - slugLen - elapsedLen);
|
|
304
386
|
lines.push(slugText + " ".repeat(gap1) + elapsedText);
|
|
305
387
|
|
|
306
|
-
// Line 2:
|
|
307
|
-
|
|
308
|
-
let badges = " " + renderModeBadge(item.mode);
|
|
388
|
+
// Line 2: mode badge + gated badge + classification + blocking state
|
|
389
|
+
let badges = renderModeBadge(item.mode);
|
|
309
390
|
if (item.gated) badges += " " + renderGatedBadge();
|
|
310
391
|
if (item.status === "paused") badges += " " + bgYellow(" PAUSED ");
|
|
311
392
|
if (item.status === "failed") badges += " " + bgRed(" FAILED ");
|
|
312
|
-
if (item.classification) badges += "
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (item.
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (item.currentSubStage && item.currentSubStageStartedAt) {
|
|
321
|
-
timingParts.push(cyan("step") + dim(`: ${formatDuration(item.currentSubStageStartedAt)}`));
|
|
322
|
-
}
|
|
323
|
-
if (timingParts.length > 0) {
|
|
324
|
-
lines.push(" " + timingParts.join(dim(" │ ")));
|
|
393
|
+
if (item.classification) badges += " " + renderClassificationBadge(item.classification);
|
|
394
|
+
if (item.awaitingInput) {
|
|
395
|
+
// Loud: agent definitely blocked on a permission prompt or AskUserQuestion
|
|
396
|
+
const arrow = tick % 2 === 0 ? "▶" : "▷";
|
|
397
|
+
badges += " " + bgMagenta(` ${arrow} AWAITING INPUT `);
|
|
398
|
+
} else if (item.idle) {
|
|
399
|
+
// Soft: turn ended but step not complete — probably asking a question in prose
|
|
400
|
+
badges += " " + dim("⏸ idle");
|
|
325
401
|
}
|
|
402
|
+
lines.push(" " + badges);
|
|
403
|
+
lines.push("");
|
|
326
404
|
|
|
327
|
-
// Line
|
|
405
|
+
// Line 3: progress bar with animated head
|
|
328
406
|
const barMaxWidth = Math.max(20, Math.min(40, innerWidth - 20));
|
|
329
407
|
lines.push(" " + renderProgressBar(
|
|
330
408
|
item.progress.completed,
|
|
@@ -334,13 +412,16 @@ function renderWorkItem(item: WorkItemView, innerWidth: number, tick: number): s
|
|
|
334
412
|
tick
|
|
335
413
|
));
|
|
336
414
|
|
|
337
|
-
// Line 5: phase pipeline with connectors and
|
|
338
|
-
|
|
415
|
+
// Line 4-5: phase pipeline with connectors, spinner, and timing row
|
|
416
|
+
const pipelineLines = renderPhasePipeline(item.phases, tick, item.awaitingInput, item.currentPhase);
|
|
417
|
+
for (const pl of pipelineLines) {
|
|
418
|
+
lines.push(" " + pl);
|
|
419
|
+
}
|
|
339
420
|
|
|
340
|
-
//
|
|
341
|
-
const
|
|
342
|
-
if (
|
|
343
|
-
for (const line of
|
|
421
|
+
// Step detail box (all steps of current phase)
|
|
422
|
+
const stepBox = renderStepBox(item, innerWidth, tick);
|
|
423
|
+
if (stepBox.length > 0) {
|
|
424
|
+
for (const line of stepBox) {
|
|
344
425
|
lines.push(line);
|
|
345
426
|
}
|
|
346
427
|
}
|
|
@@ -358,15 +439,16 @@ function renderWorkItem(item: WorkItemView, innerWidth: number, tick: number): s
|
|
|
358
439
|
lines.push(loopStr);
|
|
359
440
|
}
|
|
360
441
|
|
|
361
|
-
// Worktree path
|
|
442
|
+
// Worktree path, then branch beneath it
|
|
362
443
|
if (item.worktreePath) {
|
|
363
444
|
let displayPath = item.worktreePath;
|
|
364
|
-
const maxPathLen = innerWidth -
|
|
445
|
+
const maxPathLen = innerWidth - 8;
|
|
365
446
|
if (displayPath.length > maxPathLen) {
|
|
366
447
|
displayPath = "…" + displayPath.slice(displayPath.length - maxPathLen + 1);
|
|
367
448
|
}
|
|
368
|
-
lines.push(" " + dim(displayPath));
|
|
449
|
+
lines.push(" " + dim("⌂ " + displayPath));
|
|
369
450
|
}
|
|
451
|
+
lines.push(" " + dim("⎇ " + item.branch));
|
|
370
452
|
|
|
371
453
|
return lines;
|
|
372
454
|
}
|
|
@@ -417,11 +499,14 @@ export function renderDashboard(
|
|
|
417
499
|
|
|
418
500
|
// Header counts
|
|
419
501
|
let activeCount = 0, pausedCount = 0, failedCount = 0, waitingCount = 0;
|
|
502
|
+
let awaitingInputCount = 0, idleCount = 0;
|
|
420
503
|
for (const item of data.activeItems) {
|
|
421
504
|
if (item.status === "in-progress") activeCount++;
|
|
422
505
|
else if (item.status === "paused") pausedCount++;
|
|
423
506
|
else if (item.status === "failed") failedCount++;
|
|
424
|
-
if (item.
|
|
507
|
+
if (item.currentStepStatus === "waiting") waitingCount++;
|
|
508
|
+
if (item.awaitingInput) awaitingInputCount++;
|
|
509
|
+
else if (item.idle) idleCount++;
|
|
425
510
|
}
|
|
426
511
|
const completedCount = data.completedItems.length;
|
|
427
512
|
const hasActive = activeCount > 0;
|
|
@@ -429,7 +514,9 @@ export function renderDashboard(
|
|
|
429
514
|
// Header: mascot + title + counts
|
|
430
515
|
let headerRight = "";
|
|
431
516
|
if (activeCount > 0) headerRight += `${green("●")} ${activeCount} active`;
|
|
432
|
-
if (
|
|
517
|
+
if (awaitingInputCount > 0) headerRight += ` ${magenta("▶")} ${awaitingInputCount} awaiting`;
|
|
518
|
+
if (idleCount > 0) headerRight += ` ${dim("⏸")} ${idleCount} idle`;
|
|
519
|
+
if (waitingCount > 0) headerRight += ` ${yellow("◉")} ${waitingCount} gated`;
|
|
433
520
|
if (pausedCount > 0) headerRight += ` ${yellow("○")} ${pausedCount} paused`;
|
|
434
521
|
if (failedCount > 0) headerRight += ` ${red("✗")} ${failedCount} failed`;
|
|
435
522
|
if (completedCount > 0) headerRight += ` ${green("✓")} ${completedCount} done`;
|