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.
- package/README.md +24 -13
- package/cli/src/commands/bootstrap.test.ts +40 -0
- package/cli/src/commands/bootstrap.ts +77 -13
- package/cli/src/commands/cancel.ts +1 -16
- package/cli/src/commands/complete.ts +92 -98
- package/cli/src/commands/completions.ts +2 -2
- package/cli/src/commands/doctor.ts +1 -1
- package/cli/src/commands/extract.ts +217 -0
- package/cli/src/commands/init.test.ts +50 -0
- package/cli/src/commands/init.ts +70 -35
- package/cli/src/commands/learn.test.ts +217 -0
- package/cli/src/commands/learn.ts +104 -0
- package/cli/src/commands/loopback.ts +8 -11
- package/cli/src/commands/next.ts +93 -60
- package/cli/src/commands/observe.ts +16 -21
- package/cli/src/commands/pause-resume.test.ts +142 -0
- package/cli/src/commands/pause.ts +34 -0
- package/cli/src/commands/report.ts +217 -0
- package/cli/src/commands/resume.ts +126 -0
- package/cli/src/commands/setup.ts +280 -0
- package/cli/src/commands/status.ts +8 -6
- package/cli/src/commands/uninstall.ts +8 -3
- package/cli/src/commands/workflow.ts +43 -33
- package/cli/src/config/agent-map.ts +9 -9
- package/cli/src/config/constants.ts +54 -0
- package/cli/src/config/loopback-routes.ts +13 -13
- package/cli/src/config/model-routing.test.ts +190 -0
- package/cli/src/config/model-routing.ts +208 -0
- package/cli/src/config/project-config.test.ts +127 -0
- package/cli/src/config/project-config.ts +106 -0
- package/cli/src/config/{phases.ts → workflow.ts} +40 -23
- package/cli/src/context/prompt-builder.ts +10 -9
- package/cli/src/index.ts +130 -9
- package/cli/src/observer/data.ts +196 -65
- package/cli/src/observer/renderer.ts +127 -107
- package/cli/src/observer/watcher.ts +28 -16
- package/cli/src/state/helpers.test.ts +28 -28
- package/cli/src/state/helpers.ts +37 -25
- package/cli/src/state/schema.ts +135 -45
- package/cli/src/state/store.ts +127 -7
- package/cli/src/state/validators.test.ts +13 -13
- package/cli/src/state/validators.ts +3 -4
- package/cli/src/utils/colors.ts +2 -0
- package/cli/src/utils/fs.ts +13 -0
- package/cli/src/utils/json.ts +20 -0
- package/cli/src/utils/knowledge.ts +471 -0
- package/cli/src/utils/time.ts +27 -0
- package/cli/src/{engine → workflow}/loopbacks.test.ts +2 -2
- package/cli/src/workflow/loopbacks.ts +42 -0
- package/cli/src/workflow/parallel.ts +64 -0
- package/cli/src/workflow/transitions.test.ts +129 -0
- package/cli/src/{engine → workflow}/transitions.ts +18 -22
- package/package.json +2 -2
- package/skills/auto-kit/SKILL.md +44 -27
- package/skills/cancel-kit/SKILL.md +4 -4
- package/skills/full-kit/SKILL.md +45 -28
- package/skills/pause-kit/SKILL.md +25 -0
- package/skills/resume-kit/SKILL.md +64 -0
- package/skills/wk-bootstrap/SKILL.md +11 -5
- package/skills/wk-build/SKILL.md +12 -11
- package/skills/wk-build/{stages → steps}/commit.md +1 -1
- package/skills/wk-build/{stages → steps}/core.md +3 -3
- package/skills/wk-build/{stages → steps}/integration.md +2 -2
- package/skills/wk-build/{stages → steps}/migration.md +1 -1
- package/skills/wk-build/{stages → steps}/red.md +1 -1
- package/skills/wk-build/{stages → steps}/refactor.md +1 -1
- package/skills/wk-build/{stages → steps}/setup.md +1 -1
- package/skills/wk-build/{stages → steps}/ui.md +1 -1
- package/skills/wk-deploy/SKILL.md +7 -6
- package/skills/wk-deploy/{stages → steps}/merge.md +1 -1
- package/skills/wk-deploy/{stages → steps}/monitor.md +1 -1
- package/skills/wk-deploy/{stages → steps}/remediate.md +1 -1
- package/skills/wk-plan/SKILL.md +15 -14
- package/skills/wk-plan/{stages → steps}/architecture.md +1 -1
- package/skills/wk-plan/{stages → steps}/audit.md +2 -2
- package/skills/wk-plan/{stages → steps}/blueprint.md +2 -2
- package/skills/wk-plan/{stages → steps}/clarify.md +1 -1
- package/skills/wk-plan/{stages → steps}/investigate.md +1 -1
- package/skills/wk-plan/{stages → steps}/scope.md +1 -1
- package/skills/wk-plan/{stages → steps}/sketch.md +1 -1
- package/skills/wk-plan/{stages → steps}/ux-flow.md +1 -1
- package/skills/wk-review/SKILL.md +11 -10
- package/skills/wk-review/{stages → steps}/compliance.md +1 -1
- package/skills/wk-review/{stages → steps}/handoff.md +2 -2
- package/skills/wk-review/{stages → steps}/performance.md +1 -1
- package/skills/wk-review/{stages → steps}/security.md +1 -1
- package/skills/wk-review/{stages → steps}/self-review.md +1 -1
- package/skills/wk-test/SKILL.md +9 -8
- package/skills/wk-test/steps/e2e.md +56 -0
- package/skills/wk-test/{stages → steps}/validate.md +1 -1
- package/skills/wk-test/{stages → steps}/verify.md +1 -1
- package/skills/wk-wrap-up/SKILL.md +19 -5
- package/skills/wk-wrap-up/steps/knowledge.md +76 -0
- package/skills/wk-wrap-up/steps/summary.md +86 -0
- package/cli/src/engine/loopbacks.ts +0 -32
- package/cli/src/engine/parallel.ts +0 -60
- package/cli/src/engine/transitions.test.ts +0 -129
- package/skills/wk-test/stages/e2e.md +0 -53
- /package/cli/src/{engine/phases.ts → workflow/gates.ts} +0 -0
package/cli/src/observer/data.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
28
|
+
currentStep: string | null;
|
|
18
29
|
currentPhaseStartedAt?: string;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
// ──
|
|
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
|
-
|
|
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 ===
|
|
196
|
+
const phaseList: PhaseName[] = state.mode === MODE_AUTO && state.workflow
|
|
95
197
|
? getAutoKitPhases(state)
|
|
96
198
|
: [...PHASE_NAMES];
|
|
97
199
|
|
|
98
|
-
// Track current phase
|
|
200
|
+
// Track current phase step position
|
|
99
201
|
let currentPhaseStartedAt: string | undefined;
|
|
100
|
-
let
|
|
101
|
-
let
|
|
102
|
-
let
|
|
202
|
+
let currentStepStatus: string | undefined;
|
|
203
|
+
let currentStepIndex: number | undefined;
|
|
204
|
+
let currentStepStartedAt: string | undefined;
|
|
103
205
|
let currentPhaseTotal: number | undefined;
|
|
104
|
-
let
|
|
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
|
-
|
|
111
|
-
|
|
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
|
|
117
|
-
const hasWaiting = Object.values(phase.
|
|
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
|
|
126
|
-
if (
|
|
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 =
|
|
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
|
-
|
|
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
|
|
235
|
+
const s = phase.steps[key];
|
|
137
236
|
total++;
|
|
138
|
-
|
|
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.
|
|
148
|
-
const idx = activeKeys.indexOf(state.
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
155
|
-
const
|
|
252
|
+
phaseSteps = activeKeys.map(key => {
|
|
253
|
+
const s = phase.steps[key];
|
|
156
254
|
return {
|
|
157
255
|
name: key,
|
|
158
|
-
status:
|
|
159
|
-
startedAt:
|
|
160
|
-
completedAt:
|
|
161
|
-
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.
|
|
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.
|
|
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
|
-
|
|
290
|
+
currentStep: state.currentStep,
|
|
191
291
|
currentPhaseStartedAt,
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
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
|
|
210
|
-
if (
|
|
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,
|
|
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(
|
|
251
|
-
|
|
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
|
|
256
|
-
const item = collectWorkItem(
|
|
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
|
|
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 =
|
|
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 =>
|
|
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 {
|