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.
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 +101 -79
  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 boldGreen(name);
175
- case "in-progress": return tick % 2 === 0 ? boldCyan(name) : bold(cyan(name));
176
- case "waiting": return bold(yellow(name));
177
- case "failed": return bold(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,30 +189,33 @@ 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
 
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
- const sec = Math.floor(ms / 1000);
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 icon = phaseIndicator(p.status, tick);
238
- const name = phaseName(p.name, p.status, tick);
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
- // ── Sub-Stage Detail Box ────────────────────────────────────────────
286
+ // ── Step Detail Box ─────────────────────────────────────────────────
280
287
 
281
- function renderSubStageBox(
288
+ function renderStepBox(
282
289
  item: WorkItemView,
283
290
  innerWidth: number,
284
291
  tick: number
285
292
  ): string[] {
286
- const subs = item.phaseSubStages;
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 = dim(item.currentPhase);
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 sub-stages in rows that fit the width
305
+ // Render steps in rows that fit the width
299
306
  const entries: string[] = [];
300
307
  for (const ss of subs) {
301
- 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;
302
314
  let nameStr: string;
303
- switch (ss.status) {
304
- case "completed": nameStr = green(ss.name); break;
305
- case "in-progress": nameStr = boldCyan(ss.name); break;
306
- case "waiting": nameStr = yellow(ss.name); break;
307
- case "failed": nameStr = red(ss.name); break;
308
- 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
+ }
309
328
  }
310
- // Add duration for completed or in-progress
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
- const sec = Math.floor(ms / 1000);
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
- 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}`);
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: branch + mode badge + gated badge + classification
369
- const branchText = dim("⎇ " + item.branch);
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 += " " + dim(item.classification);
375
- lines.push(" " + branchText + badges);
376
-
377
- // Line 3: timing phase elapsed + sub-stage elapsed
378
- const timingParts: string[] = [];
379
- if (item.currentPhase && item.currentPhaseStartedAt) {
380
- timingParts.push(cyan("phase") + dim(`: ${formatDuration(item.currentPhaseStartedAt)}`));
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 4: progress bar with animated head
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-6: phase pipeline with connectors, spinner, and timing row
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
- // Line 6: sub-stage detail box (all sub-stages of current phase)
406
- const subStageBox = renderSubStageBox(item, innerWidth, tick);
407
- if (subStageBox.length > 0) {
408
- 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) {
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.currentSubStageStatus === "waiting") waitingCount++;
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 (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`;
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`;