work-kit-cli 0.2.8 → 0.4.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 (99) hide show
  1. package/README.md +24 -13
  2. package/cli/src/commands/bootstrap.test.ts +40 -0
  3. package/cli/src/commands/bootstrap.ts +77 -13
  4. package/cli/src/commands/cancel.ts +1 -16
  5. package/cli/src/commands/complete.ts +92 -98
  6. package/cli/src/commands/completions.ts +2 -2
  7. package/cli/src/commands/doctor.ts +1 -1
  8. package/cli/src/commands/extract.ts +217 -0
  9. package/cli/src/commands/init.test.ts +50 -0
  10. package/cli/src/commands/init.ts +70 -35
  11. package/cli/src/commands/learn.test.ts +217 -0
  12. package/cli/src/commands/learn.ts +104 -0
  13. package/cli/src/commands/loopback.ts +8 -11
  14. package/cli/src/commands/next.ts +93 -60
  15. package/cli/src/commands/observe.ts +16 -21
  16. package/cli/src/commands/pause-resume.test.ts +142 -0
  17. package/cli/src/commands/pause.ts +34 -0
  18. package/cli/src/commands/report.ts +217 -0
  19. package/cli/src/commands/resume.ts +126 -0
  20. package/cli/src/commands/setup.ts +280 -0
  21. package/cli/src/commands/status.ts +8 -6
  22. package/cli/src/commands/uninstall.ts +8 -3
  23. package/cli/src/commands/workflow.ts +43 -33
  24. package/cli/src/config/agent-map.ts +9 -9
  25. package/cli/src/config/constants.ts +54 -0
  26. package/cli/src/config/loopback-routes.ts +13 -13
  27. package/cli/src/config/model-routing.test.ts +190 -0
  28. package/cli/src/config/model-routing.ts +208 -0
  29. package/cli/src/config/project-config.test.ts +127 -0
  30. package/cli/src/config/project-config.ts +106 -0
  31. package/cli/src/config/{phases.ts → workflow.ts} +40 -23
  32. package/cli/src/context/prompt-builder.ts +10 -9
  33. package/cli/src/index.ts +130 -9
  34. package/cli/src/observer/data.ts +196 -65
  35. package/cli/src/observer/renderer.ts +127 -107
  36. package/cli/src/observer/watcher.ts +28 -16
  37. package/cli/src/state/helpers.test.ts +28 -28
  38. package/cli/src/state/helpers.ts +37 -25
  39. package/cli/src/state/schema.ts +135 -45
  40. package/cli/src/state/store.ts +127 -7
  41. package/cli/src/state/validators.test.ts +13 -13
  42. package/cli/src/state/validators.ts +3 -4
  43. package/cli/src/utils/colors.ts +2 -0
  44. package/cli/src/utils/fs.ts +13 -0
  45. package/cli/src/utils/json.ts +20 -0
  46. package/cli/src/utils/knowledge.ts +471 -0
  47. package/cli/src/utils/time.ts +27 -0
  48. package/cli/src/{engine → workflow}/loopbacks.test.ts +2 -2
  49. package/cli/src/workflow/loopbacks.ts +42 -0
  50. package/cli/src/workflow/parallel.ts +64 -0
  51. package/cli/src/workflow/transitions.test.ts +129 -0
  52. package/cli/src/{engine → workflow}/transitions.ts +18 -22
  53. package/package.json +2 -2
  54. package/skills/auto-kit/SKILL.md +44 -27
  55. package/skills/cancel-kit/SKILL.md +4 -4
  56. package/skills/full-kit/SKILL.md +45 -28
  57. package/skills/pause-kit/SKILL.md +25 -0
  58. package/skills/resume-kit/SKILL.md +64 -0
  59. package/skills/wk-bootstrap/SKILL.md +11 -5
  60. package/skills/wk-build/SKILL.md +12 -11
  61. package/skills/wk-build/{stages → steps}/commit.md +1 -1
  62. package/skills/wk-build/{stages → steps}/core.md +3 -3
  63. package/skills/wk-build/{stages → steps}/integration.md +2 -2
  64. package/skills/wk-build/{stages → steps}/migration.md +1 -1
  65. package/skills/wk-build/{stages → steps}/red.md +1 -1
  66. package/skills/wk-build/{stages → steps}/refactor.md +1 -1
  67. package/skills/wk-build/{stages → steps}/setup.md +1 -1
  68. package/skills/wk-build/{stages → steps}/ui.md +1 -1
  69. package/skills/wk-deploy/SKILL.md +7 -6
  70. package/skills/wk-deploy/{stages → steps}/merge.md +1 -1
  71. package/skills/wk-deploy/{stages → steps}/monitor.md +1 -1
  72. package/skills/wk-deploy/{stages → steps}/remediate.md +1 -1
  73. package/skills/wk-plan/SKILL.md +15 -14
  74. package/skills/wk-plan/{stages → steps}/architecture.md +1 -1
  75. package/skills/wk-plan/{stages → steps}/audit.md +2 -2
  76. package/skills/wk-plan/{stages → steps}/blueprint.md +2 -2
  77. package/skills/wk-plan/{stages → steps}/clarify.md +1 -1
  78. package/skills/wk-plan/{stages → steps}/investigate.md +1 -1
  79. package/skills/wk-plan/{stages → steps}/scope.md +1 -1
  80. package/skills/wk-plan/{stages → steps}/sketch.md +1 -1
  81. package/skills/wk-plan/{stages → steps}/ux-flow.md +1 -1
  82. package/skills/wk-review/SKILL.md +11 -10
  83. package/skills/wk-review/{stages → steps}/compliance.md +1 -1
  84. package/skills/wk-review/{stages → steps}/handoff.md +2 -2
  85. package/skills/wk-review/{stages → steps}/performance.md +1 -1
  86. package/skills/wk-review/{stages → steps}/security.md +1 -1
  87. package/skills/wk-review/{stages → steps}/self-review.md +1 -1
  88. package/skills/wk-test/SKILL.md +9 -8
  89. package/skills/wk-test/steps/e2e.md +56 -0
  90. package/skills/wk-test/{stages → steps}/validate.md +1 -1
  91. package/skills/wk-test/{stages → steps}/verify.md +1 -1
  92. package/skills/wk-wrap-up/SKILL.md +19 -5
  93. package/skills/wk-wrap-up/steps/knowledge.md +76 -0
  94. package/skills/wk-wrap-up/steps/summary.md +86 -0
  95. package/cli/src/engine/loopbacks.ts +0 -32
  96. package/cli/src/engine/parallel.ts +0 -60
  97. package/cli/src/engine/transitions.test.ts +0 -129
  98. package/skills/wk-test/stages/e2e.md +0 -53
  99. /package/cli/src/{engine/phases.ts → workflow/gates.ts} +0 -0
@@ -1,8 +1,11 @@
1
+ import * as path from "node:path";
1
2
  import {
2
3
  bold, dim, green, yellow, red, cyan, magenta,
3
- bgYellow, bgCyan, bgRed, bgMagenta,
4
- boldCyan, boldGreen,
4
+ bgYellow, bgCyan, bgRed, bgMagenta, bgGreen, bgBlue, bgDim,
5
+ boldCyan, boldGreen, boldMagenta, boldRed,
5
6
  } from "../utils/colors.js";
7
+ import { formatDurationMs, formatDurationSince } from "../utils/time.js";
8
+ import { MODE_FULL } from "../state/schema.js";
6
9
  import type { DashboardData, WorkItemView, CompletedItemView } from "./data.js";
7
10
 
8
11
  // ── Spinners & Animation Frames ─────────────────────────────────────
@@ -63,19 +66,7 @@ function formatTimeAgo(dateStr: string): string {
63
66
  return `${weeks}w ago`;
64
67
  }
65
68
 
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
- }
69
+ const formatDuration = formatDurationSince;
79
70
 
80
71
  // ── Box Drawing ─────────────────────────────────────────────────────
81
72
 
@@ -157,7 +148,7 @@ function phaseIndicator(status: string, tick: number = 0): string {
157
148
  }
158
149
  }
159
150
 
160
- function subStageIndicator(status: string, tick: number): string {
151
+ function stepIndicator(status: string, tick: number): string {
161
152
  switch (status) {
162
153
  case "completed": return green("●");
163
154
  case "in-progress": return cyan(pulse(tick));
@@ -169,13 +160,20 @@ function subStageIndicator(status: string, tick: number): string {
169
160
  }
170
161
  }
171
162
 
163
+ function phaseDisplayName(name: string): string {
164
+ // Uppercase with letter-spacing to visually distinguish phases from steps.
165
+ // e.g. "plan" → "P L A N", "wrap-up" → "W R A P - U P"
166
+ return name.toUpperCase().split("").join(" ");
167
+ }
168
+
172
169
  function phaseName(name: string, status: string, tick: number): string {
170
+ const display = phaseDisplayName(name);
173
171
  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);
172
+ case "completed": return boldGreen(display);
173
+ case "in-progress": return tick % 2 === 0 ? boldCyan(display) : bold(cyan(display));
174
+ case "waiting": return bold(yellow(display));
175
+ case "failed": return bold(red(display));
176
+ default: return dim(display);
179
177
  }
180
178
  }
181
179
 
@@ -192,30 +190,33 @@ function statusDot(status: string): string {
192
190
  // ── Badges ──────────────────────────────────────────────────────────
193
191
 
194
192
  function renderModeBadge(mode: string): string {
195
- return mode === "full-kit" ? bgCyan(" Full Kit ") : bgYellow(" Auto Kit ");
193
+ return mode === MODE_FULL ? bgCyan(" Full Kit ") : bgYellow(" Auto Kit ");
196
194
  }
197
195
 
198
196
  function renderGatedBadge(): string {
199
197
  return bgMagenta(" GATED ");
200
198
  }
201
199
 
200
+ function renderClassificationBadge(classification: string): string {
201
+ const label = ` ${classification.toUpperCase()} `;
202
+ switch (classification) {
203
+ case "bug-fix": return bgRed(label);
204
+ case "small-change": return bgGreen(label);
205
+ case "refactor": return bgCyan(label);
206
+ case "feature": return bgBlue(label);
207
+ case "large-feature": return bgMagenta(label);
208
+ default: return bgYellow(label);
209
+ }
210
+ }
211
+
202
212
  // ── Phase Pipeline ──────────────────────────────────────────────────
203
213
 
204
214
  function phaseDuration(p: { status: string; startedAt?: string; completedAt?: string }): string {
205
215
  if (p.status === "completed" && p.startedAt && p.completedAt) {
206
216
  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);
217
+ return formatDurationMs(ms);
217
218
  }
218
- if (p.status === "waiting" && p.startedAt) {
219
+ if ((p.status === "in-progress" || p.status === "waiting") && p.startedAt) {
219
220
  return formatDuration(p.startedAt);
220
221
  }
221
222
  return "";
@@ -223,7 +224,9 @@ function phaseDuration(p: { status: string; startedAt?: string; completedAt?: st
223
224
 
224
225
  function renderPhasePipeline(
225
226
  phases: WorkItemView["phases"],
226
- tick: number
227
+ tick: number,
228
+ awaitingInput: boolean = false,
229
+ currentPhase: string | null = null,
227
230
  ): string[] {
228
231
  const connector = dim(" ── ");
229
232
  const connectorLen = 4; // " ── "
@@ -234,8 +237,13 @@ function renderPhasePipeline(
234
237
 
235
238
  for (let i = 0; i < phases.length; i++) {
236
239
  const p = phases[i];
237
- const icon = phaseIndicator(p.status, tick);
238
- const name = phaseName(p.name, p.status, tick);
240
+ const isBlockedHere = awaitingInput && p.name === currentPhase && p.status === "in-progress";
241
+ const icon = isBlockedHere
242
+ ? magenta(tick % 2 === 0 ? "▶" : "▷")
243
+ : phaseIndicator(p.status, tick);
244
+ const name = isBlockedHere
245
+ ? boldMagenta(phaseDisplayName(p.name))
246
+ : phaseName(p.name, p.status, tick);
239
247
  const segment = `${icon} ${name}`;
240
248
  topParts.push(segment);
241
249
 
@@ -276,18 +284,18 @@ function renderPhasePipeline(
276
284
  return [topLine];
277
285
  }
278
286
 
279
- // ── Sub-Stage Detail Box ────────────────────────────────────────────
287
+ // ── Step Detail Box ─────────────────────────────────────────────────
280
288
 
281
- function renderSubStageBox(
289
+ function renderStepBox(
282
290
  item: WorkItemView,
283
291
  innerWidth: number,
284
292
  tick: number
285
293
  ): string[] {
286
- const subs = item.phaseSubStages;
294
+ const subs = item.phaseSteps;
287
295
  if (!subs || subs.length === 0 || !item.currentPhase) return [];
288
296
 
289
297
  const lines: string[] = [];
290
- const label = dim(item.currentPhase);
298
+ const label = bold(phaseDisplayName(item.currentPhase));
291
299
  const boxInner = innerWidth - 8; // indent + border padding
292
300
 
293
301
  // Top border with phase label
@@ -295,29 +303,42 @@ function renderSubStageBox(
295
303
  const topRule = dim("┌─ ") + label + dim(" " + "─".repeat(Math.max(0, boxInner - labelLen - 2)) + "┐");
296
304
  lines.push(" " + topRule);
297
305
 
298
- // Render sub-stages in rows that fit the width
306
+ // Render steps in rows that fit the width
299
307
  const entries: string[] = [];
300
308
  for (const ss of subs) {
301
- const icon = subStageIndicator(ss.status, tick);
309
+ // Highlight the current in-progress step in magenta if the agent is
310
+ // blocked waiting for human input (permission prompt / AskUserQuestion).
311
+ const isBlockedHere =
312
+ item.awaitingInput && ss.status === "in-progress" && ss.name === item.currentStep;
313
+
314
+ let icon: string;
302
315
  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);
316
+ if (isBlockedHere) {
317
+ // Pulsing magenta pointer + bold magenta name
318
+ icon = magenta(tick % 2 === 0 ? "" : "▷");
319
+ nameStr = boldMagenta(ss.name);
320
+ } else {
321
+ icon = stepIndicator(ss.status, tick);
322
+ switch (ss.status) {
323
+ case "completed": nameStr = green(ss.name); break;
324
+ case "in-progress": nameStr = boldCyan(ss.name); break;
325
+ case "waiting": nameStr = yellow(ss.name); break;
326
+ case "failed": nameStr = red(ss.name); break;
327
+ default: nameStr = dim(ss.name);
328
+ }
309
329
  }
310
- // Add duration for completed or in-progress
330
+
311
331
  let duration = "";
312
332
  if (ss.status === "completed" && ss.startedAt && ss.completedAt) {
313
333
  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`);
334
+ duration = dim(` ${formatDurationMs(ms)}`);
317
335
  } else if (ss.status === "in-progress" && ss.startedAt) {
318
336
  duration = dim(` ${formatDuration(ss.startedAt)}`);
319
337
  }
320
- entries.push(`${icon} ${nameStr}${duration}`);
338
+
339
+ // Append an inline "waiting you" hint next to the blocked step
340
+ const hint = isBlockedHere ? magenta(" ← waiting you") : "";
341
+ entries.push(`${icon} ${nameStr}${duration}${hint}`);
321
342
  }
322
343
 
323
344
  // Flow entries into rows
@@ -356,56 +377,51 @@ function renderSubStageBox(
356
377
  function renderWorkItem(item: WorkItemView, innerWidth: number, tick: number): string[] {
357
378
  const lines: string[] = [];
358
379
 
359
- // Line 1: status dot + bold slug + elapsed time (right)
360
- const slugText = `${statusDot(item.status)} ${bold(item.slug)}`;
361
- const elapsed = formatTimeAgo(item.startedAt);
362
- const elapsedText = dim(`⏱ ${elapsed}`);
363
- const slugLen = stripAnsi(slugText).length;
364
- const elapsedLen = stripAnsi(elapsedText).length;
365
- const gap1 = Math.max(2, innerWidth - slugLen - elapsedLen);
366
- lines.push(slugText + " ".repeat(gap1) + elapsedText);
380
+ const repoPrefix = item.repoName ? dim(`${item.repoName} `) : "";
381
+ const titleText = `${statusDot(item.status)} ${repoPrefix}${bold(item.slug)}`;
367
382
 
368
- // Line 2: branch + mode badge + gated badge + classification
369
- const branchText = dim(" " + item.branch);
370
- let badges = " " + renderModeBadge(item.mode);
383
+ let badges = renderModeBadge(item.mode);
384
+ if (item.classification) badges += " " + renderClassificationBadge(item.classification);
385
+ if (item.status === "paused") badges += " " + bgDim(" ⏸ PAUSED ");
386
+ if (item.status === "failed") badges += " " + boldRed("✗ FAILED");
371
387
  if (item.gated) badges += " " + renderGatedBadge();
372
- if (item.status === "paused") badges += " " + bgYellow(" PAUSED ");
373
- if (item.status === "failed") badges += " " + bgRed(" FAILED ");
374
- if (item.classification) badges += " " + dim(item.classification);
375
- lines.push(" " + branchText + badges);
376
-
377
- // Line 3: timingphase 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(" │ ")));
388
+ if (item.awaitingInput) {
389
+ // Loud: agent definitely blocked on a permission prompt or AskUserQuestion
390
+ const arrow = tick % 2 === 0 ? "" : "▷";
391
+ badges += " " + bgMagenta(` ${arrow} AWAITING INPUT `);
392
+ } else if (item.idle) {
393
+ // Soft: turn ended but step not complete probably asking a question in prose
394
+ badges += " " + dim("⏸ idle");
387
395
  }
388
396
 
389
- // Line 4: progress bar with animated head
390
- const barMaxWidth = Math.max(20, Math.min(40, innerWidth - 20));
391
- lines.push(" " + renderProgressBar(
392
- item.progress.completed,
393
- item.progress.total,
394
- item.progress.percent,
395
- barMaxWidth,
396
- tick
397
- ));
398
-
399
- // Line 5-6: phase pipeline with connectors, spinner, and timing row
400
- const pipelineLines = renderPhasePipeline(item.phases, tick);
397
+ const leftLine = `${titleText} ${badges}`;
398
+ const elapsed = formatTimeAgo(item.startedAt);
399
+ const elapsedText = dim(`⏱ ${elapsed}`);
400
+ const leftLen = stripAnsi(leftLine).length;
401
+ const elapsedLen = stripAnsi(elapsedText).length;
402
+ const gap1 = Math.max(2, innerWidth - leftLen - elapsedLen);
403
+ lines.push(leftLine + " ".repeat(gap1) + elapsedText);
404
+ lines.push("");
405
+
406
+ // TODO(progress-bar): re-enable after redesign — hidden per user request
407
+ // const barMaxWidth = Math.max(20, Math.min(40, innerWidth - 20));
408
+ // lines.push(" " + renderProgressBar(
409
+ // item.progress.completed,
410
+ // item.progress.total,
411
+ // item.progress.percent,
412
+ // barMaxWidth,
413
+ // tick
414
+ // ));
415
+
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,15 +439,11 @@ function renderWorkItem(item: WorkItemView, innerWidth: number, tick: number): s
423
439
  lines.push(loopStr);
424
440
  }
425
441
 
426
- // Worktree path
442
+ // Worktree (basename only) and branch
427
443
  if (item.worktreePath) {
428
- let displayPath = item.worktreePath;
429
- const maxPathLen = innerWidth - 8;
430
- if (displayPath.length > maxPathLen) {
431
- displayPath = "…" + displayPath.slice(displayPath.length - maxPathLen + 1);
432
- }
433
- lines.push(" " + dim("⌂ " + displayPath));
444
+ lines.push(" " + dim("worktree: " + path.basename(item.worktreePath)));
434
445
  }
446
+ lines.push(" " + dim("⎇ " + item.branch));
435
447
 
436
448
  return lines;
437
449
  }
@@ -439,28 +451,31 @@ function renderWorkItem(item: WorkItemView, innerWidth: number, tick: number): s
439
451
  // ── Render Completed Item ───────────────────────────────────────────
440
452
 
441
453
  interface CompletedColumnWidths {
454
+ repo: number;
442
455
  slug: number;
443
456
  pr: number;
444
457
  date: number;
445
458
  }
446
459
 
447
460
  function computeCompletedWidths(items: CompletedItemView[]): CompletedColumnWidths {
448
- let slug = 4, pr = 2, date = 4;
461
+ let repo = 4, slug = 4, pr = 2, date = 4;
449
462
  for (const item of items) {
463
+ repo = Math.max(repo, (item.repoName || "").length);
450
464
  slug = Math.max(slug, item.slug.length);
451
465
  pr = Math.max(pr, (item.pr || "—").length);
452
466
  date = Math.max(date, (item.completedAt || "").length);
453
467
  }
454
- return { slug, pr, date };
468
+ return { repo, slug, pr, date };
455
469
  }
456
470
 
457
471
  function renderCompletedItem(item: CompletedItemView, cols: CompletedColumnWidths): string {
458
472
  const check = green("✓");
473
+ const repo = padRight(dim(item.repoName || ""), cols.repo);
459
474
  const slug = padRight(item.slug, cols.slug);
460
475
  const pr = padRight(dim(item.pr || "—"), cols.pr);
461
476
  const date = padRight(dim(item.completedAt || ""), cols.date);
462
477
  const phases = item.phases ? dim(item.phases) : "";
463
- return `${check} ${slug} ${pr} ${date} ${phases}`;
478
+ return `${check} ${repo} ${slug} ${pr} ${date} ${phases}`;
464
479
  }
465
480
 
466
481
  // ── Main Render Function ────────────────────────────────────────────
@@ -482,11 +497,14 @@ export function renderDashboard(
482
497
 
483
498
  // Header counts
484
499
  let activeCount = 0, pausedCount = 0, failedCount = 0, waitingCount = 0;
500
+ let awaitingInputCount = 0, idleCount = 0;
485
501
  for (const item of data.activeItems) {
486
502
  if (item.status === "in-progress") activeCount++;
487
503
  else if (item.status === "paused") pausedCount++;
488
504
  else if (item.status === "failed") failedCount++;
489
- if (item.currentSubStageStatus === "waiting") waitingCount++;
505
+ if (item.currentStepStatus === "waiting") waitingCount++;
506
+ if (item.awaitingInput) awaitingInputCount++;
507
+ else if (item.idle) idleCount++;
490
508
  }
491
509
  const completedCount = data.completedItems.length;
492
510
  const hasActive = activeCount > 0;
@@ -494,7 +512,9 @@ export function renderDashboard(
494
512
  // Header: mascot + title + counts
495
513
  let headerRight = "";
496
514
  if (activeCount > 0) headerRight += `${green("●")} ${activeCount} active`;
497
- if (waitingCount > 0) headerRight += ` ${yellow("")} ${waitingCount} waiting`;
515
+ if (awaitingInputCount > 0) headerRight += ` ${magenta("")} ${awaitingInputCount} awaiting`;
516
+ if (idleCount > 0) headerRight += ` ${dim("⏸")} ${idleCount} idle`;
517
+ if (waitingCount > 0) headerRight += ` ${yellow("◉")} ${waitingCount} gated`;
498
518
  if (pausedCount > 0) headerRight += ` ${yellow("○")} ${pausedCount} paused`;
499
519
  if (failedCount > 0) headerRight += ` ${red("✗")} ${failedCount} failed`;
500
520
  if (completedCount > 0) headerRight += ` ${green("✓")} ${completedCount} done`;
@@ -1,21 +1,21 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { discoverWorktrees } from "./data.js";
3
+ import { discoverWorktrees, type WorktreeEntry } from "./data.js";
4
4
 
5
5
  export interface WatcherHandle {
6
6
  stop: () => void;
7
- getWorktrees: () => string[];
7
+ getWorktrees: () => WorktreeEntry[];
8
8
  }
9
9
 
10
10
  export function startWatching(
11
- mainRepoRoot: string,
11
+ mainRepoRoots: string[],
12
12
  onUpdate: () => void
13
13
  ): WatcherHandle {
14
14
  const watchers = new Map<string, fs.FSWatcher>();
15
15
  let debounceTimer: ReturnType<typeof setTimeout> | null = null;
16
16
  let pollTimer: ReturnType<typeof setInterval> | null = null;
17
17
  let stopped = false;
18
- let cachedWorktrees: string[] = [];
18
+ let cachedEntries: WorktreeEntry[] = [];
19
19
 
20
20
  function debouncedUpdate(): void {
21
21
  if (stopped) return;
@@ -60,19 +60,29 @@ export function startWatching(
60
60
 
61
61
  function refreshWorktrees(): void {
62
62
  if (stopped) return;
63
- const current = discoverWorktrees(mainRepoRoot);
64
- const currentSet = new Set(current);
63
+ const seen = new Set<string>();
64
+ const current: WorktreeEntry[] = [];
65
+ for (const root of mainRepoRoots) {
66
+ for (const wt of discoverWorktrees(root)) {
67
+ if (seen.has(wt)) continue;
68
+ seen.add(wt);
69
+ current.push({ root, worktree: wt });
70
+ }
71
+ }
65
72
 
66
- // Only trigger update if worktree list actually changed
67
- const changed = current.length !== cachedWorktrees.length
68
- || current.some((wt, i) => wt !== cachedWorktrees[i]);
73
+ // Only trigger update if the entry list actually changed. Compare
74
+ // both fields so a worktree path reused under a different root is
75
+ // detected as a real change.
76
+ const changed = current.length !== cachedEntries.length
77
+ || current.some((e, i) =>
78
+ e.worktree !== cachedEntries[i].worktree || e.root !== cachedEntries[i].root);
69
79
 
70
- for (const wt of current) {
71
- watchStateFile(wt);
80
+ for (const e of current) {
81
+ watchStateFile(e.worktree);
72
82
  }
73
- unwatchRemoved(currentSet);
83
+ unwatchRemoved(seen);
74
84
 
75
- cachedWorktrees = current;
85
+ cachedEntries = current;
76
86
 
77
87
  if (changed) {
78
88
  debouncedUpdate();
@@ -82,10 +92,12 @@ export function startWatching(
82
92
  // Initial setup
83
93
  refreshWorktrees();
84
94
 
85
- // Poll for new/removed worktrees every 5 seconds
95
+ // Poll for new/removed worktrees. Each tick spawns one `git worktree
96
+ // list` per root, so back off when watching many repos under --all.
97
+ const pollIntervalMs = mainRepoRoots.length > 1 ? 30_000 : 5_000;
86
98
  pollTimer = setInterval(() => {
87
99
  if (!stopped) refreshWorktrees();
88
- }, 5000);
100
+ }, pollIntervalMs);
89
101
 
90
102
  return {
91
103
  stop() {
@@ -98,7 +110,7 @@ export function startWatching(
98
110
  watchers.clear();
99
111
  },
100
112
  getWorktrees() {
101
- return cachedWorktrees;
113
+ return cachedEntries;
102
114
  },
103
115
  };
104
116
  }
@@ -1,27 +1,27 @@
1
1
  import { describe, it } from "node:test";
2
2
  import * as assert from "node:assert/strict";
3
3
  import { parseLocation, resetToLocation } from "./helpers.js";
4
- import type { WorkKitState, PhaseName, PhaseState, SubStageState } from "./schema.js";
5
- import { PHASE_NAMES, SUBSTAGES_BY_PHASE } from "./schema.js";
4
+ import type { WorkKitState, PhaseName, PhaseState, StepState } from "./schema.js";
5
+ import { PHASE_NAMES, STEPS_BY_PHASE } from "./schema.js";
6
6
 
7
7
  function makeState(): WorkKitState {
8
8
  const phases = {} as Record<PhaseName, PhaseState>;
9
9
  for (const phase of PHASE_NAMES) {
10
- const subStages: Record<string, SubStageState> = {};
11
- for (const ss of SUBSTAGES_BY_PHASE[phase]) {
12
- subStages[ss] = { status: "pending" };
10
+ const steps: Record<string, StepState> = {};
11
+ for (const s of STEPS_BY_PHASE[phase]) {
12
+ steps[s] = { status: "pending" };
13
13
  }
14
- phases[phase] = { status: "pending", subStages };
14
+ phases[phase] = { status: "pending", steps };
15
15
  }
16
16
  return {
17
- version: 1,
17
+ version: 2,
18
18
  slug: "test",
19
19
  branch: "feature/test",
20
20
  started: "2026-01-01",
21
21
  mode: "full-kit",
22
22
  status: "in-progress",
23
23
  currentPhase: "plan",
24
- currentSubStage: "clarify",
24
+ currentStep: "clarify",
25
25
  phases,
26
26
  loopbacks: [],
27
27
  metadata: { worktreeRoot: "/tmp/test", mainRepoRoot: "/tmp/test" },
@@ -31,7 +31,7 @@ function makeState(): WorkKitState {
31
31
  describe("parseLocation", () => {
32
32
  it("parses plan/clarify correctly", () => {
33
33
  const loc = parseLocation("plan/clarify");
34
- assert.deepStrictEqual(loc, { phase: "plan", subStage: "clarify" });
34
+ assert.deepStrictEqual(loc, { phase: "plan", step: "clarify" });
35
35
  });
36
36
 
37
37
  it("throws on invalid format (no slash)", () => {
@@ -42,8 +42,8 @@ describe("parseLocation", () => {
42
42
  assert.throws(() => parseLocation("foobar/baz"), /Unknown phase/);
43
43
  });
44
44
 
45
- it("throws on unknown sub-stage", () => {
46
- assert.throws(() => parseLocation("plan/nonexistent"), /Unknown sub-stage/);
45
+ it("throws on unknown step", () => {
46
+ assert.throws(() => parseLocation("plan/nonexistent"), /Unknown step/);
47
47
  });
48
48
  });
49
49
 
@@ -52,40 +52,40 @@ describe("resetToLocation", () => {
52
52
  const state = makeState();
53
53
 
54
54
  // Mark plan and build as completed
55
- for (const ss of Object.values(state.phases.plan.subStages)) {
56
- ss.status = "completed";
57
- ss.completedAt = "2026-01-01";
55
+ for (const s of Object.values(state.phases.plan.steps)) {
56
+ s.status = "completed";
57
+ s.completedAt = "2026-01-01";
58
58
  }
59
59
  state.phases.plan.status = "completed";
60
60
  state.phases.plan.completedAt = "2026-01-01";
61
61
 
62
- for (const ss of Object.values(state.phases.build.subStages)) {
63
- ss.status = "completed";
64
- ss.completedAt = "2026-01-02";
62
+ for (const s of Object.values(state.phases.build.steps)) {
63
+ s.status = "completed";
64
+ s.completedAt = "2026-01-02";
65
65
  }
66
66
  state.phases.build.status = "completed";
67
67
  state.phases.build.completedAt = "2026-01-02";
68
68
 
69
69
  // Reset to plan/blueprint
70
- resetToLocation(state, { phase: "plan", subStage: "blueprint" });
70
+ resetToLocation(state, { phase: "plan", step: "blueprint" });
71
71
 
72
- // Sub-stages before blueprint should stay completed
73
- assert.equal(state.phases.plan.subStages.clarify.status, "completed");
74
- assert.equal(state.phases.plan.subStages.investigate.status, "completed");
75
- assert.equal(state.phases.plan.subStages.sketch.status, "completed");
76
- assert.equal(state.phases.plan.subStages.scope.status, "completed");
77
- assert.equal(state.phases.plan.subStages["ux-flow"].status, "completed");
78
- assert.equal(state.phases.plan.subStages.architecture.status, "completed");
72
+ // Steps before blueprint should stay completed
73
+ assert.equal(state.phases.plan.steps.clarify.status, "completed");
74
+ assert.equal(state.phases.plan.steps.investigate.status, "completed");
75
+ assert.equal(state.phases.plan.steps.sketch.status, "completed");
76
+ assert.equal(state.phases.plan.steps.scope.status, "completed");
77
+ assert.equal(state.phases.plan.steps["ux-flow"].status, "completed");
78
+ assert.equal(state.phases.plan.steps.architecture.status, "completed");
79
79
 
80
80
  // Blueprint and audit should be reset
81
- assert.equal(state.phases.plan.subStages.blueprint.status, "pending");
82
- assert.equal(state.phases.plan.subStages.audit.status, "pending");
81
+ assert.equal(state.phases.plan.steps.blueprint.status, "pending");
82
+ assert.equal(state.phases.plan.steps.audit.status, "pending");
83
83
 
84
84
  // Plan phase should be in-progress
85
85
  assert.equal(state.phases.plan.status, "in-progress");
86
86
 
87
87
  // Build (later phase) should be reset
88
88
  assert.equal(state.phases.build.status, "pending");
89
- assert.equal(state.phases.build.subStages.core.status, "pending");
89
+ assert.equal(state.phases.build.steps.core.status, "pending");
90
90
  });
91
91
  });