work-kit-cli 0.2.8 → 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 +101 -79
- 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 boldGreen(
|
|
175
|
-
case "in-progress": return tick % 2 === 0 ? boldCyan(
|
|
176
|
-
case "waiting": return bold(yellow(
|
|
177
|
-
case "failed": return bold(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,30 +189,33 @@ 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
|
|
|
204
213
|
function phaseDuration(p: { status: string; startedAt?: string; completedAt?: string }): string {
|
|
205
214
|
if (p.status === "completed" && p.startedAt && p.completedAt) {
|
|
206
215
|
const ms = new Date(p.completedAt).getTime() - new Date(p.startedAt).getTime();
|
|
207
|
-
|
|
208
|
-
if (sec < 60) return `${sec}s`;
|
|
209
|
-
const min = Math.floor(sec / 60);
|
|
210
|
-
if (min < 60) return `${min}m`;
|
|
211
|
-
const hr = Math.floor(min / 60);
|
|
212
|
-
const remMin = min % 60;
|
|
213
|
-
return remMin > 0 ? `${hr}h${remMin}m` : `${hr}h`;
|
|
214
|
-
}
|
|
215
|
-
if (p.status === "in-progress" && p.startedAt) {
|
|
216
|
-
return formatDuration(p.startedAt);
|
|
216
|
+
return formatDurationMs(ms);
|
|
217
217
|
}
|
|
218
|
-
if (p.status === "waiting" && p.startedAt) {
|
|
218
|
+
if ((p.status === "in-progress" || p.status === "waiting") && p.startedAt) {
|
|
219
219
|
return formatDuration(p.startedAt);
|
|
220
220
|
}
|
|
221
221
|
return "";
|
|
@@ -223,7 +223,9 @@ function phaseDuration(p: { status: string; startedAt?: string; completedAt?: st
|
|
|
223
223
|
|
|
224
224
|
function renderPhasePipeline(
|
|
225
225
|
phases: WorkItemView["phases"],
|
|
226
|
-
tick: number
|
|
226
|
+
tick: number,
|
|
227
|
+
awaitingInput: boolean = false,
|
|
228
|
+
currentPhase: string | null = null,
|
|
227
229
|
): string[] {
|
|
228
230
|
const connector = dim(" ── ");
|
|
229
231
|
const connectorLen = 4; // " ── "
|
|
@@ -234,8 +236,13 @@ function renderPhasePipeline(
|
|
|
234
236
|
|
|
235
237
|
for (let i = 0; i < phases.length; i++) {
|
|
236
238
|
const p = phases[i];
|
|
237
|
-
const
|
|
238
|
-
const
|
|
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);
|
|
239
246
|
const segment = `${icon} ${name}`;
|
|
240
247
|
topParts.push(segment);
|
|
241
248
|
|
|
@@ -276,18 +283,18 @@ function renderPhasePipeline(
|
|
|
276
283
|
return [topLine];
|
|
277
284
|
}
|
|
278
285
|
|
|
279
|
-
// ──
|
|
286
|
+
// ── Step Detail Box ─────────────────────────────────────────────────
|
|
280
287
|
|
|
281
|
-
function
|
|
288
|
+
function renderStepBox(
|
|
282
289
|
item: WorkItemView,
|
|
283
290
|
innerWidth: number,
|
|
284
291
|
tick: number
|
|
285
292
|
): string[] {
|
|
286
|
-
const subs = item.
|
|
293
|
+
const subs = item.phaseSteps;
|
|
287
294
|
if (!subs || subs.length === 0 || !item.currentPhase) return [];
|
|
288
295
|
|
|
289
296
|
const lines: string[] = [];
|
|
290
|
-
const label =
|
|
297
|
+
const label = bold(phaseDisplayName(item.currentPhase));
|
|
291
298
|
const boxInner = innerWidth - 8; // indent + border padding
|
|
292
299
|
|
|
293
300
|
// Top border with phase label
|
|
@@ -295,29 +302,42 @@ function renderSubStageBox(
|
|
|
295
302
|
const topRule = dim("┌─ ") + label + dim(" " + "─".repeat(Math.max(0, boxInner - labelLen - 2)) + "┐");
|
|
296
303
|
lines.push(" " + topRule);
|
|
297
304
|
|
|
298
|
-
// Render
|
|
305
|
+
// Render steps in rows that fit the width
|
|
299
306
|
const entries: string[] = [];
|
|
300
307
|
for (const ss of subs) {
|
|
301
|
-
|
|
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;
|
|
302
314
|
let nameStr: string;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
+
}
|
|
309
328
|
}
|
|
310
|
-
|
|
329
|
+
|
|
311
330
|
let duration = "";
|
|
312
331
|
if (ss.status === "completed" && ss.startedAt && ss.completedAt) {
|
|
313
332
|
const ms = new Date(ss.completedAt).getTime() - new Date(ss.startedAt).getTime();
|
|
314
|
-
|
|
315
|
-
if (sec < 60) duration = dim(` ${sec}s`);
|
|
316
|
-
else duration = dim(` ${Math.floor(sec / 60)}m`);
|
|
333
|
+
duration = dim(` ${formatDurationMs(ms)}`);
|
|
317
334
|
} else if (ss.status === "in-progress" && ss.startedAt) {
|
|
318
335
|
duration = dim(` ${formatDuration(ss.startedAt)}`);
|
|
319
336
|
}
|
|
320
|
-
|
|
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}`);
|
|
321
341
|
}
|
|
322
342
|
|
|
323
343
|
// Flow entries into rows
|
|
@@ -365,28 +385,24 @@ function renderWorkItem(item: WorkItemView, innerWidth: number, tick: number): s
|
|
|
365
385
|
const gap1 = Math.max(2, innerWidth - slugLen - elapsedLen);
|
|
366
386
|
lines.push(slugText + " ".repeat(gap1) + elapsedText);
|
|
367
387
|
|
|
368
|
-
// Line 2:
|
|
369
|
-
|
|
370
|
-
let badges = " " + renderModeBadge(item.mode);
|
|
388
|
+
// Line 2: mode badge + gated badge + classification + blocking state
|
|
389
|
+
let badges = renderModeBadge(item.mode);
|
|
371
390
|
if (item.gated) badges += " " + renderGatedBadge();
|
|
372
391
|
if (item.status === "paused") badges += " " + bgYellow(" PAUSED ");
|
|
373
392
|
if (item.status === "failed") badges += " " + bgRed(" FAILED ");
|
|
374
|
-
if (item.classification) badges += "
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if (item.
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
if (item.currentSubStage && item.currentSubStageStartedAt) {
|
|
383
|
-
timingParts.push(cyan("step") + dim(`: ${formatDuration(item.currentSubStageStartedAt)}`));
|
|
384
|
-
}
|
|
385
|
-
if (timingParts.length > 0) {
|
|
386
|
-
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");
|
|
387
401
|
}
|
|
402
|
+
lines.push(" " + badges);
|
|
403
|
+
lines.push("");
|
|
388
404
|
|
|
389
|
-
// Line
|
|
405
|
+
// Line 3: progress bar with animated head
|
|
390
406
|
const barMaxWidth = Math.max(20, Math.min(40, innerWidth - 20));
|
|
391
407
|
lines.push(" " + renderProgressBar(
|
|
392
408
|
item.progress.completed,
|
|
@@ -396,16 +412,16 @@ function renderWorkItem(item: WorkItemView, innerWidth: number, tick: number): s
|
|
|
396
412
|
tick
|
|
397
413
|
));
|
|
398
414
|
|
|
399
|
-
// Line 5
|
|
400
|
-
const pipelineLines = renderPhasePipeline(item.phases, tick);
|
|
415
|
+
// Line 4-5: phase pipeline with connectors, spinner, and timing row
|
|
416
|
+
const pipelineLines = renderPhasePipeline(item.phases, tick, item.awaitingInput, item.currentPhase);
|
|
401
417
|
for (const pl of pipelineLines) {
|
|
402
418
|
lines.push(" " + pl);
|
|
403
419
|
}
|
|
404
420
|
|
|
405
|
-
//
|
|
406
|
-
const
|
|
407
|
-
if (
|
|
408
|
-
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) {
|
|
409
425
|
lines.push(line);
|
|
410
426
|
}
|
|
411
427
|
}
|
|
@@ -423,7 +439,7 @@ function renderWorkItem(item: WorkItemView, innerWidth: number, tick: number): s
|
|
|
423
439
|
lines.push(loopStr);
|
|
424
440
|
}
|
|
425
441
|
|
|
426
|
-
// Worktree path
|
|
442
|
+
// Worktree path, then branch beneath it
|
|
427
443
|
if (item.worktreePath) {
|
|
428
444
|
let displayPath = item.worktreePath;
|
|
429
445
|
const maxPathLen = innerWidth - 8;
|
|
@@ -432,6 +448,7 @@ function renderWorkItem(item: WorkItemView, innerWidth: number, tick: number): s
|
|
|
432
448
|
}
|
|
433
449
|
lines.push(" " + dim("⌂ " + displayPath));
|
|
434
450
|
}
|
|
451
|
+
lines.push(" " + dim("⎇ " + item.branch));
|
|
435
452
|
|
|
436
453
|
return lines;
|
|
437
454
|
}
|
|
@@ -482,11 +499,14 @@ export function renderDashboard(
|
|
|
482
499
|
|
|
483
500
|
// Header counts
|
|
484
501
|
let activeCount = 0, pausedCount = 0, failedCount = 0, waitingCount = 0;
|
|
502
|
+
let awaitingInputCount = 0, idleCount = 0;
|
|
485
503
|
for (const item of data.activeItems) {
|
|
486
504
|
if (item.status === "in-progress") activeCount++;
|
|
487
505
|
else if (item.status === "paused") pausedCount++;
|
|
488
506
|
else if (item.status === "failed") failedCount++;
|
|
489
|
-
if (item.
|
|
507
|
+
if (item.currentStepStatus === "waiting") waitingCount++;
|
|
508
|
+
if (item.awaitingInput) awaitingInputCount++;
|
|
509
|
+
else if (item.idle) idleCount++;
|
|
490
510
|
}
|
|
491
511
|
const completedCount = data.completedItems.length;
|
|
492
512
|
const hasActive = activeCount > 0;
|
|
@@ -494,7 +514,9 @@ export function renderDashboard(
|
|
|
494
514
|
// Header: mascot + title + counts
|
|
495
515
|
let headerRight = "";
|
|
496
516
|
if (activeCount > 0) headerRight += `${green("●")} ${activeCount} active`;
|
|
497
|
-
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`;
|
|
498
520
|
if (pausedCount > 0) headerRight += ` ${yellow("○")} ${pausedCount} paused`;
|
|
499
521
|
if (failedCount > 0) headerRight += ` ${red("✗")} ${failedCount} failed`;
|
|
500
522
|
if (completedCount > 0) headerRight += ` ${green("✓")} ${completedCount} done`;
|