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
|
@@ -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
|
-
|
|
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
|
|
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(
|
|
175
|
-
case "in-progress": return tick % 2 === 0 ? boldCyan(
|
|
176
|
-
case "waiting": return bold(yellow(
|
|
177
|
-
case "failed": return bold(red(
|
|
178
|
-
default: return dim(
|
|
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 ===
|
|
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
|
-
|
|
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
|
|
238
|
-
const
|
|
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
|
-
// ──
|
|
287
|
+
// ── Step Detail Box ─────────────────────────────────────────────────
|
|
280
288
|
|
|
281
|
-
function
|
|
289
|
+
function renderStepBox(
|
|
282
290
|
item: WorkItemView,
|
|
283
291
|
innerWidth: number,
|
|
284
292
|
tick: number
|
|
285
293
|
): string[] {
|
|
286
|
-
const subs = item.
|
|
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 =
|
|
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
|
|
306
|
+
// Render steps in rows that fit the width
|
|
299
307
|
const entries: string[] = [];
|
|
300
308
|
for (const ss of subs) {
|
|
301
|
-
|
|
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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
const
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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.
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
//
|
|
400
|
-
|
|
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
|
-
//
|
|
406
|
-
const
|
|
407
|
-
if (
|
|
408
|
-
for (const line of
|
|
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
|
|
442
|
+
// Worktree (basename only) and branch
|
|
427
443
|
if (item.worktreePath) {
|
|
428
|
-
|
|
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.
|
|
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 (
|
|
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: () =>
|
|
7
|
+
getWorktrees: () => WorktreeEntry[];
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export function startWatching(
|
|
11
|
-
|
|
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
|
|
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
|
|
64
|
-
const
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
71
|
-
watchStateFile(
|
|
80
|
+
for (const e of current) {
|
|
81
|
+
watchStateFile(e.worktree);
|
|
72
82
|
}
|
|
73
|
-
unwatchRemoved(
|
|
83
|
+
unwatchRemoved(seen);
|
|
74
84
|
|
|
75
|
-
|
|
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
|
|
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
|
-
},
|
|
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
|
|
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,
|
|
5
|
-
import { PHASE_NAMES,
|
|
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
|
|
11
|
-
for (const
|
|
12
|
-
|
|
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",
|
|
14
|
+
phases[phase] = { status: "pending", steps };
|
|
15
15
|
}
|
|
16
16
|
return {
|
|
17
|
-
version:
|
|
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
|
-
|
|
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",
|
|
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
|
|
46
|
-
assert.throws(() => parseLocation("plan/nonexistent"), /Unknown
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
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",
|
|
70
|
+
resetToLocation(state, { phase: "plan", step: "blueprint" });
|
|
71
71
|
|
|
72
|
-
//
|
|
73
|
-
assert.equal(state.phases.plan.
|
|
74
|
-
assert.equal(state.phases.plan.
|
|
75
|
-
assert.equal(state.phases.plan.
|
|
76
|
-
assert.equal(state.phases.plan.
|
|
77
|
-
assert.equal(state.phases.plan.
|
|
78
|
-
assert.equal(state.phases.plan.
|
|
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.
|
|
82
|
-
assert.equal(state.phases.plan.
|
|
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.
|
|
89
|
+
assert.equal(state.phases.build.steps.core.status, "pending");
|
|
90
90
|
});
|
|
91
91
|
});
|