@abdullahsahmad/work-kit 0.1.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 (76) hide show
  1. package/README.md +147 -0
  2. package/cli/bin/work-kit.mjs +18 -0
  3. package/cli/src/commands/complete.ts +123 -0
  4. package/cli/src/commands/completions.ts +137 -0
  5. package/cli/src/commands/context.ts +41 -0
  6. package/cli/src/commands/doctor.ts +79 -0
  7. package/cli/src/commands/init.test.ts +116 -0
  8. package/cli/src/commands/init.ts +184 -0
  9. package/cli/src/commands/loopback.ts +64 -0
  10. package/cli/src/commands/next.ts +172 -0
  11. package/cli/src/commands/observe.ts +144 -0
  12. package/cli/src/commands/setup.ts +159 -0
  13. package/cli/src/commands/status.ts +50 -0
  14. package/cli/src/commands/uninstall.ts +89 -0
  15. package/cli/src/commands/upgrade.ts +12 -0
  16. package/cli/src/commands/validate.ts +34 -0
  17. package/cli/src/commands/workflow.ts +125 -0
  18. package/cli/src/config/agent-map.ts +62 -0
  19. package/cli/src/config/loopback-routes.ts +45 -0
  20. package/cli/src/config/phases.ts +119 -0
  21. package/cli/src/context/extractor.test.ts +77 -0
  22. package/cli/src/context/extractor.ts +73 -0
  23. package/cli/src/context/prompt-builder.ts +70 -0
  24. package/cli/src/engine/loopbacks.test.ts +33 -0
  25. package/cli/src/engine/loopbacks.ts +32 -0
  26. package/cli/src/engine/parallel.ts +60 -0
  27. package/cli/src/engine/phases.ts +23 -0
  28. package/cli/src/engine/transitions.test.ts +117 -0
  29. package/cli/src/engine/transitions.ts +97 -0
  30. package/cli/src/index.ts +248 -0
  31. package/cli/src/observer/data.ts +237 -0
  32. package/cli/src/observer/renderer.ts +316 -0
  33. package/cli/src/observer/watcher.ts +99 -0
  34. package/cli/src/state/helpers.test.ts +91 -0
  35. package/cli/src/state/helpers.ts +65 -0
  36. package/cli/src/state/schema.ts +113 -0
  37. package/cli/src/state/store.ts +82 -0
  38. package/cli/src/state/validators.test.ts +105 -0
  39. package/cli/src/state/validators.ts +81 -0
  40. package/cli/src/utils/colors.ts +12 -0
  41. package/package.json +49 -0
  42. package/skills/auto-kit/SKILL.md +214 -0
  43. package/skills/build/SKILL.md +88 -0
  44. package/skills/build/stages/commit.md +43 -0
  45. package/skills/build/stages/core.md +48 -0
  46. package/skills/build/stages/integration.md +44 -0
  47. package/skills/build/stages/migration.md +41 -0
  48. package/skills/build/stages/red.md +44 -0
  49. package/skills/build/stages/refactor.md +48 -0
  50. package/skills/build/stages/setup.md +42 -0
  51. package/skills/build/stages/ui.md +51 -0
  52. package/skills/deploy/SKILL.md +62 -0
  53. package/skills/deploy/stages/merge.md +47 -0
  54. package/skills/deploy/stages/monitor.md +39 -0
  55. package/skills/deploy/stages/remediate.md +54 -0
  56. package/skills/full-kit/SKILL.md +195 -0
  57. package/skills/plan/SKILL.md +77 -0
  58. package/skills/plan/stages/architecture.md +53 -0
  59. package/skills/plan/stages/audit.md +58 -0
  60. package/skills/plan/stages/blueprint.md +60 -0
  61. package/skills/plan/stages/clarify.md +61 -0
  62. package/skills/plan/stages/investigate.md +47 -0
  63. package/skills/plan/stages/scope.md +46 -0
  64. package/skills/plan/stages/sketch.md +44 -0
  65. package/skills/plan/stages/ux-flow.md +49 -0
  66. package/skills/review/SKILL.md +104 -0
  67. package/skills/review/stages/compliance.md +48 -0
  68. package/skills/review/stages/handoff.md +59 -0
  69. package/skills/review/stages/performance.md +45 -0
  70. package/skills/review/stages/security.md +49 -0
  71. package/skills/review/stages/self-review.md +41 -0
  72. package/skills/test/SKILL.md +83 -0
  73. package/skills/test/stages/e2e.md +44 -0
  74. package/skills/test/stages/validate.md +51 -0
  75. package/skills/test/stages/verify.md +41 -0
  76. package/skills/wrap-up/SKILL.md +107 -0
@@ -0,0 +1,97 @@
1
+ import { WorkKitState, PhaseName, SUBSTAGES_BY_PHASE } from "../state/schema.js";
2
+ import { PHASE_ORDER } from "../config/phases.js";
3
+
4
+ export interface NextStep {
5
+ type: "sub-stage" | "phase-boundary" | "complete" | "wait-for-user";
6
+ phase?: PhaseName;
7
+ subStage?: string;
8
+ message?: string;
9
+ }
10
+
11
+ /**
12
+ * Find the next pending sub-stage within a phase.
13
+ */
14
+ export function nextSubStageInPhase(state: WorkKitState, phase: PhaseName): string | null {
15
+ const phaseState = state.phases[phase];
16
+ const subStages = SUBSTAGES_BY_PHASE[phase];
17
+
18
+ for (const ss of subStages) {
19
+ const ssState = phaseState.subStages[ss];
20
+ if (ssState && (ssState.status === "pending" || ssState.status === "in-progress")) {
21
+ return ss;
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+
27
+ /**
28
+ * Check if all non-skipped sub-stages in a phase are completed.
29
+ */
30
+ export function isPhaseComplete(state: WorkKitState, phase: PhaseName): boolean {
31
+ const phaseState = state.phases[phase];
32
+ return Object.values(phaseState.subStages).every(
33
+ (ss) => ss.status === "completed" || ss.status === "skipped"
34
+ );
35
+ }
36
+
37
+ /**
38
+ * Find the next phase that needs work.
39
+ */
40
+ export function nextPhase(state: WorkKitState): PhaseName | null {
41
+ for (const phase of PHASE_ORDER) {
42
+ const ps = state.phases[phase];
43
+ if (ps.status === "pending" || ps.status === "in-progress") {
44
+ return phase;
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+
50
+ /**
51
+ * Determine the next step in the workflow.
52
+ */
53
+ export function determineNextStep(state: WorkKitState): NextStep {
54
+ if (state.status === "completed") {
55
+ return { type: "complete", message: "Work-kit is complete" };
56
+ }
57
+
58
+ const currentPhase = state.currentPhase;
59
+
60
+ if (!currentPhase) {
61
+ // Find the next phase
62
+ const next = nextPhase(state);
63
+ if (!next) {
64
+ return { type: "complete", message: "All phases complete" };
65
+ }
66
+ return { type: "phase-boundary", phase: next, message: `Starting ${next} phase` };
67
+ }
68
+
69
+ // Check if current phase is complete
70
+ if (isPhaseComplete(state, currentPhase)) {
71
+ // Find next phase
72
+ const phaseIndex = PHASE_ORDER.indexOf(currentPhase);
73
+ const remainingPhases = PHASE_ORDER.slice(phaseIndex + 1);
74
+
75
+ for (const phase of remainingPhases) {
76
+ const ps = state.phases[phase];
77
+ if (ps.status !== "skipped") {
78
+ // Phase boundary — wait for user confirmation before crossing
79
+ return {
80
+ type: "wait-for-user",
81
+ phase,
82
+ message: `${currentPhase} phase complete. Ready to start ${phase}. Proceed?`,
83
+ };
84
+ }
85
+ }
86
+
87
+ return { type: "complete", message: "All phases complete" };
88
+ }
89
+
90
+ // Find next sub-stage within current phase
91
+ const nextSS = nextSubStageInPhase(state, currentPhase);
92
+ if (nextSS) {
93
+ return { type: "sub-stage", phase: currentPhase, subStage: nextSS };
94
+ }
95
+
96
+ return { type: "complete", message: `${currentPhase} phase complete` };
97
+ }
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from "commander";
4
+ import { initCommand } from "./commands/init.js";
5
+ import { statusCommand } from "./commands/status.js";
6
+ import { nextCommand } from "./commands/next.js";
7
+ import { completeCommand } from "./commands/complete.js";
8
+ import { validateCommand } from "./commands/validate.js";
9
+ import { contextCommand } from "./commands/context.js";
10
+ import { loopbackCommand } from "./commands/loopback.js";
11
+ import { workflowCommand } from "./commands/workflow.js";
12
+ import { doctorCommand } from "./commands/doctor.js";
13
+ import { setupCommand } from "./commands/setup.js";
14
+ import { upgradeCommand } from "./commands/upgrade.js";
15
+ import { completionsCommand } from "./commands/completions.js";
16
+ import { observeCommand } from "./commands/observe.js";
17
+ import { uninstallCommand } from "./commands/uninstall.js";
18
+ import { bold, green, yellow, red } from "./utils/colors.js";
19
+ import type { Classification, PhaseName } from "./state/schema.js";
20
+
21
+ const program = new Command();
22
+
23
+ program
24
+ .name("work-kit")
25
+ .description("State machine orchestrator for work-kit development workflow")
26
+ .version("1.0.0");
27
+
28
+ // ── init ─────────────────────────────────────────────────────────────
29
+
30
+ program
31
+ .command("init")
32
+ .description("Create worktree and initialize state")
33
+ .requiredOption("--mode <mode>", "Workflow mode: full or auto")
34
+ .requiredOption("--description <text>", "Description of the work")
35
+ .option("--classification <type>", "Work classification (auto mode): bug-fix, small-change, refactor, feature, large-feature")
36
+ .option("--worktree-root <path>", "Override worktree root directory")
37
+ .action((opts) => {
38
+ try {
39
+ const result = initCommand({
40
+ mode: opts.mode as "full" | "auto",
41
+ description: opts.description,
42
+ classification: opts.classification as Classification | undefined,
43
+ worktreeRoot: opts.worktreeRoot,
44
+ });
45
+ console.log(JSON.stringify(result, null, 2));
46
+ } catch (e: any) {
47
+ console.error(JSON.stringify({ action: "error", message: e.message }));
48
+ process.exit(1);
49
+ }
50
+ });
51
+
52
+ // ── next ─────────────────────────────────────────────────────────────
53
+
54
+ program
55
+ .command("next")
56
+ .description("Get the next action to perform")
57
+ .option("--worktree-root <path>", "Override worktree root")
58
+ .action((opts) => {
59
+ try {
60
+ const result = nextCommand(opts.worktreeRoot);
61
+ console.log(JSON.stringify(result, null, 2));
62
+ } catch (e: any) {
63
+ console.error(JSON.stringify({ action: "error", message: e.message }));
64
+ process.exit(1);
65
+ }
66
+ });
67
+
68
+ // ── complete ─────────────────────────────────────────────────────────
69
+
70
+ program
71
+ .command("complete <target>")
72
+ .description("Mark a phase/sub-stage as complete (e.g., plan/clarify)")
73
+ .option("--outcome <value>", "Outcome of the step (e.g., done, revise, broken, changes_requested)")
74
+ .option("--worktree-root <path>", "Override worktree root")
75
+ .action((target, opts) => {
76
+ try {
77
+ const result = completeCommand(target, opts.outcome, opts.worktreeRoot);
78
+ console.log(JSON.stringify(result, null, 2));
79
+ } catch (e: any) {
80
+ console.error(JSON.stringify({ action: "error", message: e.message }));
81
+ process.exit(1);
82
+ }
83
+ });
84
+
85
+ // ── status ───────────────────────────────────────────────────────────
86
+
87
+ program
88
+ .command("status")
89
+ .description("Show current state summary")
90
+ .option("--worktree-root <path>", "Override worktree root")
91
+ .action((opts) => {
92
+ try {
93
+ const result = statusCommand(opts.worktreeRoot);
94
+ console.log(JSON.stringify(result, null, 2));
95
+ } catch (e: any) {
96
+ console.error(JSON.stringify({ action: "error", message: e.message }));
97
+ process.exit(1);
98
+ }
99
+ });
100
+
101
+ // ── context ──────────────────────────────────────────────────────────
102
+
103
+ program
104
+ .command("context <phase>")
105
+ .description("Extract Final sections needed for a phase's agent")
106
+ .option("--worktree-root <path>", "Override worktree root")
107
+ .action((phase, opts) => {
108
+ try {
109
+ const result = contextCommand(phase as PhaseName, opts.worktreeRoot);
110
+ console.log(JSON.stringify(result, null, 2));
111
+ } catch (e: any) {
112
+ console.error(JSON.stringify({ action: "error", message: e.message }));
113
+ process.exit(1);
114
+ }
115
+ });
116
+
117
+ // ── validate ─────────────────────────────────────────────────────────
118
+
119
+ program
120
+ .command("validate <phase>")
121
+ .description("Check prerequisites for a phase")
122
+ .option("--worktree-root <path>", "Override worktree root")
123
+ .action((phase, opts) => {
124
+ try {
125
+ const result = validateCommand(phase as PhaseName, opts.worktreeRoot);
126
+ console.log(JSON.stringify(result, null, 2));
127
+ } catch (e: any) {
128
+ console.error(JSON.stringify({ action: "error", message: e.message }));
129
+ process.exit(1);
130
+ }
131
+ });
132
+
133
+ // ── loopback ─────────────────────────────────────────────────────────
134
+
135
+ program
136
+ .command("loopback")
137
+ .description("Register a loop-back transition")
138
+ .requiredOption("--from <source>", "Source phase/sub-stage (e.g., review/handoff)")
139
+ .requiredOption("--to <target>", "Target phase/sub-stage (e.g., build/core)")
140
+ .requiredOption("--reason <text>", "Reason for loop-back")
141
+ .option("--worktree-root <path>", "Override worktree root")
142
+ .action((opts) => {
143
+ try {
144
+ const result = loopbackCommand({
145
+ from: opts.from,
146
+ to: opts.to,
147
+ reason: opts.reason,
148
+ worktreeRoot: opts.worktreeRoot,
149
+ });
150
+ console.log(JSON.stringify(result, null, 2));
151
+ } catch (e: any) {
152
+ console.error(JSON.stringify({ action: "error", message: e.message }));
153
+ process.exit(1);
154
+ }
155
+ });
156
+
157
+ // ── workflow ─────────────────────────────────────────────────────────
158
+
159
+ program
160
+ .command("workflow")
161
+ .description("Manage auto-kit dynamic workflow")
162
+ .option("--add <step>", "Add a step (e.g., review/security)")
163
+ .option("--remove <step>", "Remove a step (e.g., test/e2e)")
164
+ .option("--worktree-root <path>", "Override worktree root")
165
+ .action((opts) => {
166
+ try {
167
+ const result = workflowCommand({
168
+ add: opts.add,
169
+ remove: opts.remove,
170
+ worktreeRoot: opts.worktreeRoot,
171
+ });
172
+ console.log(JSON.stringify(result, null, 2));
173
+ } catch (e: any) {
174
+ console.error(JSON.stringify({ action: "error", message: e.message }));
175
+ process.exit(1);
176
+ }
177
+ });
178
+
179
+ // ── doctor ───────────────────────────────────────────────────────────
180
+
181
+ program
182
+ .command("doctor")
183
+ .description("Check CLI installation, skills, and environment health")
184
+ .option("--json", "Output as JSON")
185
+ .option("--worktree-root <path>", "Override worktree root")
186
+ .action((opts) => {
187
+ const result = doctorCommand(opts.worktreeRoot);
188
+ if (opts.json) {
189
+ console.log(JSON.stringify(result, null, 2));
190
+ } else {
191
+ for (const check of result.checks) {
192
+ const icon = check.status === "pass" ? green("\u2713") : check.status === "warn" ? yellow("!") : red("\u2717");
193
+ console.error(` ${icon} ${bold(check.name)}: ${check.message}`);
194
+ }
195
+ console.error();
196
+ console.error(result.ok ? green("All checks passed.") : red("Some checks failed. Fix the issues above."));
197
+ }
198
+ process.exit(result.ok ? 0 : 1);
199
+ });
200
+
201
+ // ── setup ────────────────────────────────────────────────────────────
202
+
203
+ program
204
+ .command("setup [path]")
205
+ .description("Install work-kit skills into a project")
206
+ .action(async (targetPath) => {
207
+ await setupCommand(targetPath);
208
+ });
209
+
210
+ // ── upgrade ───────────────────────────────────────────────────────────
211
+
212
+ program
213
+ .command("upgrade")
214
+ .description("Update work-kit skills to the latest version")
215
+ .option("--worktree-root <path>", "Override project path")
216
+ .action(async (opts) => {
217
+ await upgradeCommand(opts.worktreeRoot);
218
+ });
219
+
220
+ // ── completions ─────────────────────────────────────────────────────
221
+
222
+ program
223
+ .command("completions <shell>")
224
+ .description("Output shell completions (bash, zsh, fish)")
225
+ .action((shell) => {
226
+ completionsCommand(shell);
227
+ });
228
+
229
+ // ── observe ─────────────────────────────────────────────────────────
230
+
231
+ program
232
+ .command("observe")
233
+ .description("Real-time dashboard of all active work items")
234
+ .option("--repo <path>", "Main repository root")
235
+ .action(async (opts) => {
236
+ await observeCommand({ mainRepo: opts.repo });
237
+ });
238
+
239
+ // ── uninstall ────────────────────────────────────────────────────────
240
+
241
+ program
242
+ .command("uninstall [path]")
243
+ .description("Remove work-kit skills from a project")
244
+ .action(async (targetPath) => {
245
+ await uninstallCommand(targetPath);
246
+ });
247
+
248
+ program.parse();
@@ -0,0 +1,237 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { execFileSync } from "node:child_process";
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";
7
+
8
+ // ── View Types ──────────────────────────────────────────────────────
9
+
10
+ export interface WorkItemView {
11
+ slug: string;
12
+ branch: string;
13
+ mode: string;
14
+ classification?: string;
15
+ status: string;
16
+ currentPhase: string | null;
17
+ currentSubStage: string | null;
18
+ startedAt: string;
19
+ progress: { completed: number; total: number; percent: number };
20
+ phases: { name: string; status: string }[];
21
+ loopbacks: { count: number; lastReason?: string; lastFrom?: string; lastTo?: string };
22
+ }
23
+
24
+ export interface CompletedItemView {
25
+ slug: string;
26
+ pr?: string;
27
+ completedAt: string;
28
+ phases: string;
29
+ }
30
+
31
+ export interface DashboardData {
32
+ activeItems: WorkItemView[];
33
+ completedItems: CompletedItemView[];
34
+ lastUpdated: Date;
35
+ }
36
+
37
+ // ── Worktree Discovery ──────────────────────────────────────────────
38
+
39
+ export function discoverWorktrees(mainRepoRoot: string): string[] {
40
+ let output: string;
41
+ try {
42
+ output = execFileSync("git", ["worktree", "list", "--porcelain"], {
43
+ cwd: mainRepoRoot,
44
+ encoding: "utf-8",
45
+ timeout: 5000,
46
+ });
47
+ } catch {
48
+ return [];
49
+ }
50
+
51
+ const worktrees: string[] = [];
52
+ const lines = output.split("\n");
53
+ for (const line of lines) {
54
+ if (line.startsWith("worktree ")) {
55
+ const wtPath = line.slice("worktree ".length).trim();
56
+ if (stateExists(wtPath)) {
57
+ worktrees.push(wtPath);
58
+ }
59
+ }
60
+ }
61
+
62
+ if (stateExists(mainRepoRoot) && !worktrees.includes(mainRepoRoot)) {
63
+ worktrees.push(mainRepoRoot);
64
+ }
65
+
66
+ return worktrees;
67
+ }
68
+
69
+ // ── Collect Single Work Item ────────────────────────────────────────
70
+
71
+ export function collectWorkItem(worktreeRoot: string): WorkItemView | null {
72
+ if (!stateExists(worktreeRoot)) return null;
73
+
74
+ let state: WorkKitState;
75
+ try {
76
+ state = readState(worktreeRoot);
77
+ } catch {
78
+ return null;
79
+ }
80
+
81
+ // Compute progress
82
+ let completed = 0;
83
+ let total = 0;
84
+ const phaseViews: { name: string; status: string }[] = [];
85
+
86
+ const phaseList: PhaseName[] = state.mode === "auto-kit" && state.workflow
87
+ ? getAutoKitPhases(state)
88
+ : [...PHASE_NAMES];
89
+
90
+ for (const phaseName of phaseList) {
91
+ const phase = state.phases[phaseName];
92
+ if (!phase) {
93
+ phaseViews.push({ name: phaseName, status: "pending" });
94
+ // Count substages for total
95
+ const subs = SUBSTAGES_BY_PHASE[phaseName] || [];
96
+ total += subs.length;
97
+ continue;
98
+ }
99
+
100
+ phaseViews.push({ name: phaseName, status: phase.status });
101
+
102
+ const subStageKeys = Object.keys(phase.subStages);
103
+ if (subStageKeys.length === 0) {
104
+ // Use default substages
105
+ const defaults = SUBSTAGES_BY_PHASE[phaseName] || [];
106
+ total += defaults.length;
107
+ if (phase.status === "completed") completed += defaults.length;
108
+ else if (phase.status === "skipped") completed += defaults.length;
109
+ } else {
110
+ for (const key of subStageKeys) {
111
+ total++;
112
+ const sub = phase.subStages[key];
113
+ if (sub.status === "completed" || sub.status === "skipped") {
114
+ completed++;
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
121
+
122
+ // Loopback info
123
+ const loopbacks = state.loopbacks || [];
124
+ const loopbackView = {
125
+ count: loopbacks.length,
126
+ lastReason: loopbacks.length > 0 ? loopbacks[loopbacks.length - 1].reason : undefined,
127
+ lastFrom: loopbacks.length > 0
128
+ ? `${loopbacks[loopbacks.length - 1].from.phase}/${loopbacks[loopbacks.length - 1].from.subStage}`
129
+ : undefined,
130
+ lastTo: loopbacks.length > 0
131
+ ? `${loopbacks[loopbacks.length - 1].to.phase}/${loopbacks[loopbacks.length - 1].to.subStage}`
132
+ : undefined,
133
+ };
134
+
135
+ return {
136
+ slug: state.slug,
137
+ branch: state.branch,
138
+ mode: state.mode,
139
+ classification: state.classification,
140
+ status: state.status,
141
+ currentPhase: state.currentPhase,
142
+ currentSubStage: state.currentSubStage,
143
+ startedAt: state.started,
144
+ progress: { completed, total, percent },
145
+ phases: phaseViews,
146
+ loopbacks: loopbackView,
147
+ };
148
+ }
149
+
150
+ function getAutoKitPhases(state: WorkKitState): PhaseName[] {
151
+ if (!state.workflow) return [...PHASE_NAMES];
152
+ const phases = new Set<PhaseName>();
153
+ for (const step of state.workflow) {
154
+ if (step.included) phases.add(step.phase);
155
+ }
156
+ // Maintain canonical order
157
+ return PHASE_NAMES.filter(p => phases.has(p));
158
+ }
159
+
160
+ // ── Collect Completed Items ─────────────────────────────────────────
161
+
162
+ export function collectCompletedItems(mainRepoRoot: string): CompletedItemView[] {
163
+ const indexPath = path.join(mainRepoRoot, ".claude", "work-kit", "index.md");
164
+ if (!fs.existsSync(indexPath)) return [];
165
+
166
+ let content: string;
167
+ try {
168
+ content = fs.readFileSync(indexPath, "utf-8");
169
+ } catch {
170
+ return [];
171
+ }
172
+
173
+ const items: CompletedItemView[] = [];
174
+ // Parse markdown table or list entries
175
+ // Expected format: | slug | PR | date | phases |
176
+ // or list format: - slug (#PR) - date - phases
177
+ const lines = content.split("\n");
178
+ for (const line of lines) {
179
+ // Try table format: | slug | #PR | date | phases |
180
+ const tableMatch = line.match(/^\|\s*(.+?)\s*\|\s*(#?\d+)?\s*\|\s*(.+?)\s*\|\s*(.+?)\s*\|/);
181
+ if (tableMatch) {
182
+ const slug = tableMatch[1].trim();
183
+ if (slug === "Slug" || slug === "---" || slug.startsWith("-")) continue; // skip header
184
+ items.push({
185
+ slug,
186
+ pr: tableMatch[2]?.trim() || undefined,
187
+ completedAt: tableMatch[3].trim(),
188
+ phases: tableMatch[4].trim(),
189
+ });
190
+ continue;
191
+ }
192
+
193
+ // Try list format: - **slug** (#38) completed 2d ago — plan→review
194
+ const listMatch = line.match(/^[-*]\s+\*?\*?(.+?)\*?\*?\s+\(?(#\d+)?\)?\s*[-—]?\s*(.+?)?\s*[-—]\s*(.+)$/);
195
+ if (listMatch) {
196
+ items.push({
197
+ slug: listMatch[1].trim(),
198
+ pr: listMatch[2]?.trim() || undefined,
199
+ completedAt: listMatch[3]?.trim() || "",
200
+ phases: listMatch[4]?.trim() || "",
201
+ });
202
+ }
203
+ }
204
+
205
+ return items;
206
+ }
207
+
208
+ // ── Collect All Dashboard Data ──────────────────────────────────────
209
+
210
+ export function collectDashboardData(mainRepoRoot: string, cachedWorktrees?: string[]): DashboardData {
211
+ const worktrees = cachedWorktrees ?? discoverWorktrees(mainRepoRoot);
212
+ const activeItems: WorkItemView[] = [];
213
+
214
+ for (const wt of worktrees) {
215
+ const item = collectWorkItem(wt);
216
+ if (item && item.status !== "completed") {
217
+ activeItems.push(item);
218
+ }
219
+ }
220
+
221
+ // Sort: in-progress first, then paused, then by start time
222
+ activeItems.sort((a, b) => {
223
+ const statusOrder: Record<string, number> = { "in-progress": 0, "paused": 1, "failed": 2 };
224
+ const aOrder = statusOrder[a.status] ?? 3;
225
+ const bOrder = statusOrder[b.status] ?? 3;
226
+ if (aOrder !== bOrder) return aOrder - bOrder;
227
+ return new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime();
228
+ });
229
+
230
+ const completedItems = collectCompletedItems(mainRepoRoot);
231
+
232
+ return {
233
+ activeItems,
234
+ completedItems,
235
+ lastUpdated: new Date(),
236
+ };
237
+ }