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,28 +1,41 @@
1
1
  import * as fs from "node:fs";
2
+ import * as os from "node:os";
2
3
  import * as path from "node:path";
3
4
  import { execFileSync } from "node:child_process";
4
5
  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";
6
+ import { PHASE_NAMES, STEPS_BY_PHASE, MODE_AUTO } from "../state/schema.js";
7
+ import { readState, stateExists, STATE_DIR, AWAITING_INPUT_MARKER_FILE, IDLE_MARKER_FILE, gitMainRepoRoot } from "../state/store.js";
8
+ import { TRACKER_DIR, INDEX_FILE } from "../config/constants.js";
7
9
 
8
- // ── View Types ──────────────────────────────────────────────────────
10
+ const AWAITING_INPUT_MARKER = path.join(STATE_DIR, AWAITING_INPUT_MARKER_FILE);
11
+ const IDLE_MARKER = path.join(STATE_DIR, IDLE_MARKER_FILE);
12
+
13
+ // ── View Types ─────────────────────────────────────────────────────
14
+
15
+ export interface WorktreeEntry {
16
+ root: string; // main repo root that owns this worktree
17
+ worktree: string; // worktree path (may equal root)
18
+ }
9
19
 
10
20
  export interface WorkItemView {
11
21
  slug: string;
22
+ repoName: string;
12
23
  branch: string;
13
24
  mode: string;
14
25
  classification?: string;
15
26
  status: string;
16
27
  currentPhase: string | null;
17
- currentSubStage: string | null;
28
+ currentStep: string | null;
18
29
  currentPhaseStartedAt?: string;
19
- currentSubStageStatus?: string;
20
- currentSubStageIndex?: number;
21
- currentSubStageStartedAt?: string;
30
+ currentStepStatus?: string;
31
+ currentStepIndex?: number;
32
+ currentStepStartedAt?: string;
22
33
  currentPhaseTotal?: number;
23
34
  gated: boolean;
35
+ awaitingInput: boolean;
36
+ idle: boolean;
24
37
  worktreePath: string;
25
- phaseSubStages: { name: string; status: string; startedAt?: string; completedAt?: string; outcome?: string }[];
38
+ phaseSteps: { name: string; status: string; startedAt?: string; completedAt?: string; outcome?: string }[];
26
39
  startedAt: string;
27
40
  progress: { completed: number; total: number; percent: number };
28
41
  phases: { name: string; status: string; startedAt?: string; completedAt?: string }[];
@@ -31,6 +44,7 @@ export interface WorkItemView {
31
44
 
32
45
  export interface CompletedItemView {
33
46
  slug: string;
47
+ repoName: string;
34
48
  pr?: string;
35
49
  completedAt: string;
36
50
  phases: string;
@@ -42,7 +56,7 @@ export interface DashboardData {
42
56
  lastUpdated: Date;
43
57
  }
44
58
 
45
- // ── Worktree Discovery ──────────────────────────────────────────────
59
+ // ── Worktree Discovery ─────────────────────────────────────────────
46
60
 
47
61
  export function discoverWorktrees(mainRepoRoot: string): string[] {
48
62
  let output: string;
@@ -74,9 +88,97 @@ export function discoverWorktrees(mainRepoRoot: string): string[] {
74
88
  return worktrees;
75
89
  }
76
90
 
77
- // ── Collect Single Work Item ────────────────────────────────────────
91
+ // ── Discover All Work-Kit Projects on the System ───────────────────
92
+ //
93
+ // Scans ~/.claude/projects/ for sessions, reads `cwd` from session
94
+ // jsonl files, resolves each to a git toplevel, and keeps only those
95
+ // roots that have at least one work-kit-tracked worktree.
96
+
97
+ const CLAUDE_PROJECTS_DIR = path.join(os.homedir(), ".claude", "projects");
98
+
99
+ function extractCwdFromJsonl(filePath: string): string | null {
100
+ let fd: number | null = null;
101
+ try {
102
+ const buf = Buffer.alloc(16 * 1024);
103
+ fd = fs.openSync(filePath, "r");
104
+ const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
105
+ const content = buf.subarray(0, bytesRead).toString("utf-8");
106
+ const lines = content.split("\n");
107
+ for (const line of lines) {
108
+ if (!line.includes('"cwd"')) continue;
109
+ try {
110
+ const obj = JSON.parse(line);
111
+ if (typeof obj.cwd === "string" && obj.cwd.length > 0) return obj.cwd;
112
+ } catch {
113
+ // Likely a truncated final line — skip
114
+ }
115
+ }
116
+ return null;
117
+ } catch {
118
+ return null;
119
+ } finally {
120
+ if (fd !== null) {
121
+ try { fs.closeSync(fd); } catch { /* ignore */ }
122
+ }
123
+ }
124
+ }
125
+
126
+ export function discoverWorkKitProjects(): string[] {
127
+ let entries: string[];
128
+ try {
129
+ entries = fs.readdirSync(CLAUDE_PROJECTS_DIR);
130
+ } catch {
131
+ return [];
132
+ }
78
133
 
79
- export function collectWorkItem(worktreeRoot: string): WorkItemView | null {
134
+ const cwds = new Set<string>();
135
+ for (const entry of entries) {
136
+ const projDir = path.join(CLAUDE_PROJECTS_DIR, entry);
137
+ let files: string[];
138
+ try {
139
+ files = fs.readdirSync(projDir).filter(f => f.endsWith(".jsonl"));
140
+ } catch {
141
+ continue;
142
+ }
143
+ if (files.length === 0) continue;
144
+
145
+ // Single-pass scan for the most-recently-modified jsonl — avoids
146
+ // re-statting in a sort comparator (which is O(N log N) stats).
147
+ let newestFile: string | null = null;
148
+ let newestMtime = -Infinity;
149
+ for (const f of files) {
150
+ try {
151
+ const m = fs.statSync(path.join(projDir, f)).mtimeMs;
152
+ if (m > newestMtime) {
153
+ newestMtime = m;
154
+ newestFile = f;
155
+ }
156
+ } catch { /* ignore */ }
157
+ }
158
+ if (!newestFile) continue;
159
+
160
+ const cwd = extractCwdFromJsonl(path.join(projDir, newestFile));
161
+ if (cwd) cwds.add(cwd);
162
+ }
163
+
164
+ // Resolve each cwd to its main repo root (works whether the session
165
+ // was run from the main repo or from inside a worktree) and keep
166
+ // only roots that actually have work-kit setup.
167
+ const roots = new Set<string>();
168
+ for (const cwd of cwds) {
169
+ const mainRoot = gitMainRepoRoot(cwd);
170
+ if (!mainRoot || roots.has(mainRoot)) continue;
171
+ if (discoverWorktrees(mainRoot).length > 0) {
172
+ roots.add(mainRoot);
173
+ }
174
+ }
175
+
176
+ return Array.from(roots);
177
+ }
178
+
179
+ // ── Collect Single Work Item ───────────────────────────────────────
180
+
181
+ export function collectWorkItem(worktreeRoot: string, mainRepoRoot?: string): WorkItemView | null {
80
182
  if (!stateExists(worktreeRoot)) return null;
81
183
 
82
184
  let state: WorkKitState;
@@ -91,30 +193,29 @@ export function collectWorkItem(worktreeRoot: string): WorkItemView | null {
91
193
  let total = 0;
92
194
  const phaseViews: { name: string; status: string; startedAt?: string; completedAt?: string }[] = [];
93
195
 
94
- const phaseList: PhaseName[] = state.mode === "auto-kit" && state.workflow
196
+ const phaseList: PhaseName[] = state.mode === MODE_AUTO && state.workflow
95
197
  ? getAutoKitPhases(state)
96
198
  : [...PHASE_NAMES];
97
199
 
98
- // Track current phase substage position
200
+ // Track current phase step position
99
201
  let currentPhaseStartedAt: string | undefined;
100
- let currentSubStageStatus: string | undefined;
101
- let currentSubStageIndex: number | undefined;
102
- let currentSubStageStartedAt: string | undefined;
202
+ let currentStepStatus: string | undefined;
203
+ let currentStepIndex: number | undefined;
204
+ let currentStepStartedAt: string | undefined;
103
205
  let currentPhaseTotal: number | undefined;
104
- let phaseSubStages: WorkItemView["phaseSubStages"] = [];
206
+ let phaseSteps: WorkItemView["phaseSteps"] = [];
105
207
 
106
208
  for (const phaseName of phaseList) {
107
209
  const phase = state.phases[phaseName];
108
210
  if (!phase) {
109
211
  phaseViews.push({ name: phaseName, status: "pending" });
110
- // Count substages for total
111
- const subs = SUBSTAGES_BY_PHASE[phaseName] || [];
112
- total += subs.length;
212
+ const steps = STEPS_BY_PHASE[phaseName];
213
+ total += steps.length;
113
214
  continue;
114
215
  }
115
216
 
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");
217
+ // If any step is "waiting", show the phase as waiting in the view
218
+ const hasWaiting = Object.values(phase.steps).some(s => s.status === "waiting");
118
219
  phaseViews.push({
119
220
  name: phaseName,
120
221
  status: hasWaiting ? "waiting" : phase.status,
@@ -122,21 +223,18 @@ export function collectWorkItem(worktreeRoot: string): WorkItemView | null {
122
223
  completedAt: phase.completedAt,
123
224
  });
124
225
 
125
- const subStageKeys = Object.keys(phase.subStages);
126
- if (subStageKeys.length === 0) {
127
- // Use default substages — skip entirely skipped phases
226
+ const stepKeys = Object.keys(phase.steps);
227
+ if (stepKeys.length === 0) {
128
228
  if (phase.status === "skipped") continue;
129
- const defaults = SUBSTAGES_BY_PHASE[phaseName] || [];
229
+ const defaults = STEPS_BY_PHASE[phaseName];
130
230
  total += defaults.length;
131
231
  if (phase.status === "completed") completed += defaults.length;
132
232
  } else {
133
- let phaseIdx = 0;
134
- const activeKeys = subStageKeys.filter(k => phase.subStages[k].status !== "skipped");
233
+ const activeKeys = stepKeys.filter(k => phase.steps[k].status !== "skipped");
135
234
  for (const key of activeKeys) {
136
- const sub = phase.subStages[key];
235
+ const s = phase.steps[key];
137
236
  total++;
138
- phaseIdx++;
139
- if (sub.status === "completed") {
237
+ if (s.status === "completed") {
140
238
  completed++;
141
239
  }
142
240
  }
@@ -144,21 +242,21 @@ export function collectWorkItem(worktreeRoot: string): WorkItemView | null {
144
242
  if (phaseName === state.currentPhase) {
145
243
  currentPhaseStartedAt = phase.startedAt;
146
244
  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;
245
+ if (state.currentStep) {
246
+ const idx = activeKeys.indexOf(state.currentStep);
247
+ currentStepIndex = idx >= 0 ? idx + 1 : undefined;
248
+ const s = phase.steps[state.currentStep];
249
+ currentStepStatus = s?.status;
250
+ currentStepStartedAt = s?.startedAt;
153
251
  }
154
- phaseSubStages = activeKeys.map(key => {
155
- const ss = phase.subStages[key];
252
+ phaseSteps = activeKeys.map(key => {
253
+ const s = phase.steps[key];
156
254
  return {
157
255
  name: key,
158
- status: ss.status,
159
- startedAt: ss.startedAt,
160
- completedAt: ss.completedAt,
161
- outcome: ss.outcome,
256
+ status: s.status,
257
+ startedAt: s.startedAt,
258
+ completedAt: s.completedAt,
259
+ outcome: s.outcome,
162
260
  };
163
261
  });
164
262
  }
@@ -173,29 +271,39 @@ export function collectWorkItem(worktreeRoot: string): WorkItemView | null {
173
271
  count: loopbacks.length,
174
272
  lastReason: loopbacks.length > 0 ? loopbacks[loopbacks.length - 1].reason : undefined,
175
273
  lastFrom: loopbacks.length > 0
176
- ? `${loopbacks[loopbacks.length - 1].from.phase}/${loopbacks[loopbacks.length - 1].from.subStage}`
274
+ ? `${loopbacks[loopbacks.length - 1].from.phase}/${loopbacks[loopbacks.length - 1].from.step}`
177
275
  : undefined,
178
276
  lastTo: loopbacks.length > 0
179
- ? `${loopbacks[loopbacks.length - 1].to.phase}/${loopbacks[loopbacks.length - 1].to.subStage}`
277
+ ? `${loopbacks[loopbacks.length - 1].to.phase}/${loopbacks[loopbacks.length - 1].to.step}`
180
278
  : undefined,
181
279
  };
182
280
 
281
+ const repoRoot = mainRepoRoot ?? worktreeRoot;
183
282
  return {
184
283
  slug: state.slug,
284
+ repoName: path.basename(repoRoot),
185
285
  branch: state.branch,
186
286
  mode: state.mode,
187
287
  classification: state.classification,
188
288
  status: state.status,
189
289
  currentPhase: state.currentPhase,
190
- currentSubStage: state.currentSubStage,
290
+ currentStep: state.currentStep,
191
291
  currentPhaseStartedAt,
192
- currentSubStageStatus,
193
- currentSubStageIndex,
194
- currentSubStageStartedAt,
292
+ currentStepStatus,
293
+ currentStepIndex,
294
+ currentStepStartedAt,
195
295
  currentPhaseTotal,
196
296
  gated: state.gated ?? false,
297
+ awaitingInput: fs.existsSync(path.join(worktreeRoot, AWAITING_INPUT_MARKER)),
298
+ // Idle badge only fires when the agent ended its turn *mid-step*
299
+ // (suggesting it asked a prose question) — not during normal gaps
300
+ // between steps.
301
+ idle:
302
+ fs.existsSync(path.join(worktreeRoot, IDLE_MARKER)) &&
303
+ state.status === "in-progress" &&
304
+ currentStepStatus === "in-progress",
197
305
  worktreePath: state.metadata.worktreeRoot,
198
- phaseSubStages,
306
+ phaseSteps,
199
307
  startedAt: state.started,
200
308
  progress: { completed, total, percent },
201
309
  phases: phaseViews,
@@ -206,17 +314,17 @@ export function collectWorkItem(worktreeRoot: string): WorkItemView | null {
206
314
  function getAutoKitPhases(state: WorkKitState): PhaseName[] {
207
315
  if (!state.workflow) return [...PHASE_NAMES];
208
316
  const phases = new Set<PhaseName>();
209
- for (const step of state.workflow) {
210
- if (step.included) phases.add(step.phase);
317
+ for (const ws of state.workflow) {
318
+ if (ws.included) phases.add(ws.phase);
211
319
  }
212
320
  // Maintain canonical order
213
321
  return PHASE_NAMES.filter(p => phases.has(p));
214
322
  }
215
323
 
216
- // ── Collect Completed Items ─────────────────────────────────────────
324
+ // ── Collect Completed Items ────────────────────────────────────────
217
325
 
218
326
  export function collectCompletedItems(mainRepoRoot: string): CompletedItemView[] {
219
- const indexPath = path.join(mainRepoRoot, ".work-kit-tracker", "index.md");
327
+ const indexPath = path.join(mainRepoRoot, TRACKER_DIR, INDEX_FILE);
220
328
  if (!fs.existsSync(indexPath)) return [];
221
329
 
222
330
  let content: string;
@@ -227,7 +335,6 @@ export function collectCompletedItems(mainRepoRoot: string): CompletedItemView[]
227
335
  }
228
336
 
229
337
  const items: CompletedItemView[] = [];
230
- // Format: | Date | Slug | PR | Status | Phases |
231
338
  const lines = content.split("\n");
232
339
  for (const line of lines) {
233
340
  const match = line.match(/^\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|/);
@@ -236,6 +343,7 @@ export function collectCompletedItems(mainRepoRoot: string): CompletedItemView[]
236
343
  if (col1 === "Date" || col1.startsWith("-")) continue;
237
344
  items.push({
238
345
  slug: match[2].trim(),
346
+ repoName: path.basename(mainRepoRoot),
239
347
  pr: match[3].trim() !== "n/a" ? match[3].trim() : undefined,
240
348
  completedAt: col1,
241
349
  phases: match[5].trim(),
@@ -245,15 +353,31 @@ export function collectCompletedItems(mainRepoRoot: string): CompletedItemView[]
245
353
  return items;
246
354
  }
247
355
 
248
- // ── Collect All Dashboard Data ──────────────────────────────────────
249
-
250
- export function collectDashboardData(mainRepoRoot: string, cachedWorktrees?: string[]): DashboardData {
251
- const worktrees = cachedWorktrees ?? discoverWorktrees(mainRepoRoot);
356
+ // ── Collect All Dashboard Data ─────────────────────────────────────
357
+
358
+ export function collectDashboardData(
359
+ mainRepoRoots: string[],
360
+ cachedEntries?: WorktreeEntry[]
361
+ ): DashboardData {
362
+ let entries: WorktreeEntry[];
363
+ if (cachedEntries) {
364
+ entries = cachedEntries;
365
+ } else {
366
+ entries = [];
367
+ const seen = new Set<string>();
368
+ for (const root of mainRepoRoots) {
369
+ for (const wt of discoverWorktrees(root)) {
370
+ if (seen.has(wt)) continue;
371
+ seen.add(wt);
372
+ entries.push({ root, worktree: wt });
373
+ }
374
+ }
375
+ }
252
376
  const activeItems: WorkItemView[] = [];
253
377
  const completedFromWorktrees: CompletedItemView[] = [];
254
378
 
255
- for (const wt of worktrees) {
256
- const item = collectWorkItem(wt);
379
+ for (const entry of entries) {
380
+ const item = collectWorkItem(entry.worktree, entry.root);
257
381
  if (!item) continue;
258
382
  if (item.status === "completed") {
259
383
  const phaseNames = item.phases
@@ -262,6 +386,7 @@ export function collectDashboardData(mainRepoRoot: string, cachedWorktrees?: str
262
386
  .join("→");
263
387
  completedFromWorktrees.push({
264
388
  slug: item.slug,
389
+ repoName: item.repoName,
265
390
  completedAt: item.startedAt,
266
391
  phases: phaseNames,
267
392
  });
@@ -279,14 +404,20 @@ export function collectDashboardData(mainRepoRoot: string, cachedWorktrees?: str
279
404
  return new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime();
280
405
  });
281
406
 
282
- // Merge completed items from worktrees and index file, dedup by slug
283
- // Also exclude any slug that is currently active (re-initialized features)
407
+ // Merge completed items from worktrees and index files, dedup by slug
284
408
  const activeSlugs = new Set(activeItems.map(i => i.slug));
285
- const indexItems = collectCompletedItems(mainRepoRoot);
409
+ const indexItems: CompletedItemView[] = [];
410
+ for (const root of mainRepoRoots) {
411
+ indexItems.push(...collectCompletedItems(root));
412
+ }
286
413
  const seen = new Set(completedFromWorktrees.map(i => i.slug));
287
414
  const completedItems = [
288
415
  ...completedFromWorktrees.filter(i => !activeSlugs.has(i.slug)),
289
- ...indexItems.filter(i => !seen.has(i.slug) && !activeSlugs.has(i.slug)),
416
+ ...indexItems.filter(i => {
417
+ if (activeSlugs.has(i.slug) || seen.has(i.slug)) return false;
418
+ seen.add(i.slug);
419
+ return true;
420
+ }),
290
421
  ];
291
422
 
292
423
  return {