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.
Files changed (86) hide show
  1. package/README.md +13 -13
  2. package/cli/src/commands/bootstrap.ts +39 -13
  3. package/cli/src/commands/cancel.ts +1 -16
  4. package/cli/src/commands/complete.ts +92 -98
  5. package/cli/src/commands/completions.ts +2 -2
  6. package/cli/src/commands/doctor.ts +1 -1
  7. package/cli/src/commands/init.ts +40 -32
  8. package/cli/src/commands/loopback.ts +8 -11
  9. package/cli/src/commands/next.ts +64 -51
  10. package/cli/src/commands/pause-resume.test.ts +142 -0
  11. package/cli/src/commands/pause.ts +34 -0
  12. package/cli/src/commands/report.ts +217 -0
  13. package/cli/src/commands/resume.ts +38 -0
  14. package/cli/src/commands/setup.ts +136 -0
  15. package/cli/src/commands/status.ts +6 -6
  16. package/cli/src/commands/uninstall.ts +8 -3
  17. package/cli/src/commands/workflow.ts +27 -27
  18. package/cli/src/config/agent-map.ts +9 -9
  19. package/cli/src/config/constants.ts +44 -0
  20. package/cli/src/config/loopback-routes.ts +13 -13
  21. package/cli/src/config/project-config.test.ts +127 -0
  22. package/cli/src/config/project-config.ts +106 -0
  23. package/cli/src/config/{phases.ts → workflow.ts} +40 -23
  24. package/cli/src/context/prompt-builder.ts +10 -9
  25. package/cli/src/index.ts +63 -7
  26. package/cli/src/observer/data.ts +64 -56
  27. package/cli/src/observer/renderer.ts +162 -75
  28. package/cli/src/state/helpers.test.ts +28 -28
  29. package/cli/src/state/helpers.ts +37 -25
  30. package/cli/src/state/schema.ts +88 -45
  31. package/cli/src/state/store.ts +92 -7
  32. package/cli/src/state/validators.test.ts +13 -13
  33. package/cli/src/state/validators.ts +3 -4
  34. package/cli/src/utils/colors.ts +2 -0
  35. package/cli/src/utils/json.ts +20 -0
  36. package/cli/src/utils/time.ts +27 -0
  37. package/cli/src/{engine → workflow}/loopbacks.test.ts +2 -2
  38. package/cli/src/workflow/loopbacks.ts +42 -0
  39. package/cli/src/workflow/parallel.ts +64 -0
  40. package/cli/src/workflow/transitions.test.ts +129 -0
  41. package/cli/src/{engine → workflow}/transitions.ts +18 -22
  42. package/package.json +2 -2
  43. package/skills/auto-kit/SKILL.md +22 -22
  44. package/skills/cancel-kit/SKILL.md +4 -4
  45. package/skills/full-kit/SKILL.md +23 -23
  46. package/skills/pause-kit/SKILL.md +25 -0
  47. package/skills/resume-kit/SKILL.md +28 -0
  48. package/skills/wk-bootstrap/SKILL.md +5 -5
  49. package/skills/wk-build/SKILL.md +10 -10
  50. package/skills/wk-build/{stages → steps}/commit.md +1 -1
  51. package/skills/wk-build/{stages → steps}/core.md +3 -3
  52. package/skills/wk-build/{stages → steps}/integration.md +2 -2
  53. package/skills/wk-build/{stages → steps}/migration.md +1 -1
  54. package/skills/wk-build/{stages → steps}/red.md +1 -1
  55. package/skills/wk-build/{stages → steps}/refactor.md +1 -1
  56. package/skills/wk-build/{stages → steps}/setup.md +1 -1
  57. package/skills/wk-build/{stages → steps}/ui.md +1 -1
  58. package/skills/wk-deploy/SKILL.md +6 -6
  59. package/skills/wk-deploy/{stages → steps}/merge.md +1 -1
  60. package/skills/wk-deploy/{stages → steps}/monitor.md +1 -1
  61. package/skills/wk-deploy/{stages → steps}/remediate.md +1 -1
  62. package/skills/wk-plan/SKILL.md +13 -13
  63. package/skills/wk-plan/{stages → steps}/architecture.md +1 -1
  64. package/skills/wk-plan/{stages → steps}/audit.md +2 -2
  65. package/skills/wk-plan/{stages → steps}/blueprint.md +2 -2
  66. package/skills/wk-plan/{stages → steps}/clarify.md +1 -1
  67. package/skills/wk-plan/{stages → steps}/investigate.md +1 -1
  68. package/skills/wk-plan/{stages → steps}/scope.md +1 -1
  69. package/skills/wk-plan/{stages → steps}/sketch.md +1 -1
  70. package/skills/wk-plan/{stages → steps}/ux-flow.md +1 -1
  71. package/skills/wk-review/SKILL.md +10 -10
  72. package/skills/wk-review/{stages → steps}/compliance.md +1 -1
  73. package/skills/wk-review/{stages → steps}/handoff.md +2 -2
  74. package/skills/wk-review/{stages → steps}/performance.md +1 -1
  75. package/skills/wk-review/{stages → steps}/security.md +1 -1
  76. package/skills/wk-review/{stages → steps}/self-review.md +1 -1
  77. package/skills/wk-test/SKILL.md +8 -8
  78. package/skills/wk-test/{stages → steps}/e2e.md +1 -1
  79. package/skills/wk-test/{stages → steps}/validate.md +1 -1
  80. package/skills/wk-test/{stages → steps}/verify.md +1 -1
  81. package/skills/wk-wrap-up/SKILL.md +6 -5
  82. package/skills/wk-wrap-up/steps/summary.md +86 -0
  83. package/cli/src/engine/loopbacks.ts +0 -32
  84. package/cli/src/engine/parallel.ts +0 -60
  85. package/cli/src/engine/transitions.test.ts +0 -129
  86. /package/cli/src/{engine/phases.ts → workflow/gates.ts} +0 -0
@@ -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, SUBSTAGES_BY_PHASE } from "../state/schema.js";
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
- // ── View Types ──────────────────────────────────────────────────────
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
- currentSubStage: string | null;
21
+ currentStep: string | null;
18
22
  currentPhaseStartedAt?: string;
19
- currentSubStageStatus?: string;
20
- currentSubStageIndex?: number;
21
- currentSubStageStartedAt?: string;
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
- phaseSubStages: { name: string; status: string; startedAt?: string; completedAt?: string; outcome?: string }[];
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 === "auto-kit" && state.workflow
100
+ const phaseList: PhaseName[] = state.mode === MODE_AUTO && state.workflow
95
101
  ? getAutoKitPhases(state)
96
102
  : [...PHASE_NAMES];
97
103
 
98
- // Track current phase substage position
104
+ // Track current phase step position
99
105
  let currentPhaseStartedAt: string | undefined;
100
- let currentSubStageStatus: string | undefined;
101
- let currentSubStageIndex: number | undefined;
102
- let currentSubStageStartedAt: string | undefined;
106
+ let currentStepStatus: string | undefined;
107
+ let currentStepIndex: number | undefined;
108
+ let currentStepStartedAt: string | undefined;
103
109
  let currentPhaseTotal: number | undefined;
104
- let phaseSubStages: WorkItemView["phaseSubStages"] = [];
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
- // Count substages for total
111
- const subs = SUBSTAGES_BY_PHASE[phaseName] || [];
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 sub-stage is "waiting", show the phase as waiting in the view
117
- const hasWaiting = Object.values(phase.subStages).some(ss => ss.status === "waiting");
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 subStageKeys = Object.keys(phase.subStages);
126
- if (subStageKeys.length === 0) {
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 = SUBSTAGES_BY_PHASE[phaseName] || [];
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
- let phaseIdx = 0;
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 sub = phase.subStages[key];
139
+ const s = phase.steps[key];
137
140
  total++;
138
- phaseIdx++;
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.currentSubStage) {
148
- const idx = activeKeys.indexOf(state.currentSubStage);
149
- currentSubStageIndex = idx >= 0 ? idx + 1 : undefined;
150
- const ss = phase.subStages[state.currentSubStage];
151
- currentSubStageStatus = ss?.status;
152
- currentSubStageStartedAt = ss?.startedAt;
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
- phaseSubStages = activeKeys.map(key => {
155
- const ss = phase.subStages[key];
156
+ phaseSteps = activeKeys.map(key => {
157
+ const s = phase.steps[key];
156
158
  return {
157
159
  name: key,
158
- status: ss.status,
159
- startedAt: ss.startedAt,
160
- completedAt: ss.completedAt,
161
- outcome: ss.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.subStage}`
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.subStage}`
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
- currentSubStage: state.currentSubStage,
192
+ currentStep: state.currentStep,
191
193
  currentPhaseStartedAt,
192
- currentSubStageStatus,
193
- currentSubStageIndex,
194
- currentSubStageStartedAt,
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
- phaseSubStages,
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 step of state.workflow) {
210
- if (step.included) phases.add(step.phase);
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, ".work-kit-tracker", "index.md");
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
- function formatDuration(startStr: string): string {
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 subStageIndicator(status: string, tick: number): string {
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 green(name);
175
- case "in-progress": return tick % 2 === 0 ? boldCyan(name) : cyan(name);
176
- case "waiting": return yellow(name);
177
- case "failed": return red(name);
178
- default: return dim(name);
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 === "full-kit" ? bgCyan(" Full Kit ") : bgYellow(" Auto Kit ");
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
- ): string {
208
- const segments: string[] = [];
209
- for (const p of phases) {
210
- const icon = phaseIndicator(p.status, tick);
211
- const name = phaseName(p.name, p.status, tick);
212
- segments.push(`${icon} ${name}`);
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 segments.join(dim(" ── "));
283
+ return [topLine];
215
284
  }
216
285
 
217
- // ── Sub-Stage Detail Box ────────────────────────────────────────────
286
+ // ── Step Detail Box ─────────────────────────────────────────────────
218
287
 
219
- function renderSubStageBox(
288
+ function renderStepBox(
220
289
  item: WorkItemView,
221
290
  innerWidth: number,
222
291
  tick: number
223
292
  ): string[] {
224
- const subs = item.phaseSubStages;
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 = dim(item.currentPhase);
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 sub-stages in rows that fit the width
305
+ // Render steps in rows that fit the width
237
306
  const entries: string[] = [];
238
307
  for (const ss of subs) {
239
- const icon = subStageIndicator(ss.status, tick);
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
- switch (ss.status) {
242
- case "completed": nameStr = green(ss.name); break;
243
- case "in-progress": nameStr = boldCyan(ss.name); break;
244
- case "waiting": nameStr = yellow(ss.name); break;
245
- case "failed": nameStr = red(ss.name); break;
246
- default: nameStr = dim(ss.name);
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
- // Add duration for completed or in-progress
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
- const sec = Math.floor(ms / 1000);
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
- entries.push(`${icon} ${nameStr}${duration}`);
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: branch + mode badge + gated badge + classification
307
- const branchText = dim(item.branch);
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 += " " + dim(item.classification);
313
- lines.push(" " + branchText + badges);
314
-
315
- // Line 3: timing phase elapsed + sub-stage elapsed
316
- const timingParts: string[] = [];
317
- if (item.currentPhase && item.currentPhaseStartedAt) {
318
- timingParts.push(cyan("phase") + dim(`: ${formatDuration(item.currentPhaseStartedAt)}`));
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 4: progress bar with animated head
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 spinner
338
- lines.push(" " + 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);
417
+ for (const pl of pipelineLines) {
418
+ lines.push(" " + pl);
419
+ }
339
420
 
340
- // Line 6: sub-stage detail box (all sub-stages of current phase)
341
- const subStageBox = renderSubStageBox(item, innerWidth, tick);
342
- if (subStageBox.length > 0) {
343
- for (const line of subStageBox) {
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 - 4;
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.currentSubStageStatus === "waiting") waitingCount++;
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 (waitingCount > 0) headerRight += ` ${yellow("")} ${waitingCount} waiting`;
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`;