gsd-pi 2.30.0-dev.7e1bbce → 2.30.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/dist/cli.js +0 -51
- package/dist/help-text.js +0 -35
- package/dist/resources/extensions/gsd/auto-dashboard.ts +186 -65
- package/dist/resources/extensions/gsd/index.ts +0 -13
- package/dist/resources/extensions/gsd/roadmap-slices.ts +7 -22
- package/dist/resources/extensions/gsd/session-lock.ts +4 -53
- package/package.json +1 -1
- package/src/resources/extensions/gsd/auto-dashboard.ts +186 -65
- package/src/resources/extensions/gsd/index.ts +0 -13
- package/src/resources/extensions/gsd/roadmap-slices.ts +7 -22
- package/src/resources/extensions/gsd/session-lock.ts +4 -53
- package/dist/worktree-cli.d.ts +0 -34
- package/dist/worktree-cli.js +0 -294
- package/dist/worktree-name-gen.d.ts +0 -7
- package/dist/worktree-name-gen.js +0 -44
package/dist/cli.js
CHANGED
|
@@ -59,15 +59,6 @@ function parseCliArgs(argv) {
|
|
|
59
59
|
process.stdout.write((process.env.GSD_VERSION || '0.0.0') + '\n');
|
|
60
60
|
process.exit(0);
|
|
61
61
|
}
|
|
62
|
-
else if (arg === '--worktree' || arg === '-w') {
|
|
63
|
-
// -w with no value → auto-generate name; -w <name> → use that name
|
|
64
|
-
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
65
|
-
flags.worktree = args[++i];
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
flags.worktree = true;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
62
|
else if (arg === '--help' || arg === '-h') {
|
|
72
63
|
printHelp(process.env.GSD_VERSION || '0.0.0');
|
|
73
64
|
process.exit(0);
|
|
@@ -352,48 +343,6 @@ if (isPrintMode) {
|
|
|
352
343
|
process.exit(0);
|
|
353
344
|
}
|
|
354
345
|
// ---------------------------------------------------------------------------
|
|
355
|
-
// Worktree subcommand — `gsd worktree <list|merge|clean|remove>`
|
|
356
|
-
// ---------------------------------------------------------------------------
|
|
357
|
-
if (cliFlags.messages[0] === 'worktree' || cliFlags.messages[0] === 'wt') {
|
|
358
|
-
const { handleList, handleMerge, handleClean, handleRemove } = await import('./worktree-cli.js');
|
|
359
|
-
const sub = cliFlags.messages[1];
|
|
360
|
-
const subArgs = cliFlags.messages.slice(2);
|
|
361
|
-
if (!sub || sub === 'list') {
|
|
362
|
-
handleList(process.cwd());
|
|
363
|
-
}
|
|
364
|
-
else if (sub === 'merge') {
|
|
365
|
-
await handleMerge(process.cwd(), subArgs);
|
|
366
|
-
}
|
|
367
|
-
else if (sub === 'clean') {
|
|
368
|
-
handleClean(process.cwd());
|
|
369
|
-
}
|
|
370
|
-
else if (sub === 'remove' || sub === 'rm') {
|
|
371
|
-
handleRemove(process.cwd(), subArgs);
|
|
372
|
-
}
|
|
373
|
-
else {
|
|
374
|
-
process.stderr.write(`Unknown worktree command: ${sub}\n`);
|
|
375
|
-
process.stderr.write('Commands: list, merge [name], clean, remove <name>\n');
|
|
376
|
-
}
|
|
377
|
-
process.exit(0);
|
|
378
|
-
}
|
|
379
|
-
// ---------------------------------------------------------------------------
|
|
380
|
-
// Worktree flag (-w) — create/resume a worktree for the interactive session
|
|
381
|
-
// ---------------------------------------------------------------------------
|
|
382
|
-
if (cliFlags.worktree) {
|
|
383
|
-
const { handleWorktreeFlag } = await import('./worktree-cli.js');
|
|
384
|
-
handleWorktreeFlag(cliFlags.worktree);
|
|
385
|
-
}
|
|
386
|
-
// ---------------------------------------------------------------------------
|
|
387
|
-
// Active worktree banner — remind user of unmerged worktrees on normal launch
|
|
388
|
-
// ---------------------------------------------------------------------------
|
|
389
|
-
if (!cliFlags.worktree && !isPrintMode) {
|
|
390
|
-
try {
|
|
391
|
-
const { handleStatusBanner } = await import('./worktree-cli.js');
|
|
392
|
-
handleStatusBanner(process.cwd());
|
|
393
|
-
}
|
|
394
|
-
catch { /* non-fatal */ }
|
|
395
|
-
}
|
|
396
|
-
// ---------------------------------------------------------------------------
|
|
397
346
|
// Interactive mode — normal TTY session
|
|
398
347
|
// ---------------------------------------------------------------------------
|
|
399
348
|
// Per-directory session storage — same encoding as the upstream SDK so that
|
package/dist/help-text.js
CHANGED
|
@@ -29,37 +29,6 @@ const SUBCOMMAND_HELP = {
|
|
|
29
29
|
'',
|
|
30
30
|
'Compare with --continue (-c) which always resumes the most recent session.',
|
|
31
31
|
].join('\n'),
|
|
32
|
-
worktree: [
|
|
33
|
-
'Usage: gsd worktree <command> [args]',
|
|
34
|
-
'',
|
|
35
|
-
'Manage isolated git worktrees for parallel work streams.',
|
|
36
|
-
'',
|
|
37
|
-
'Commands:',
|
|
38
|
-
' list List worktrees with status (files changed, commits, dirty)',
|
|
39
|
-
' merge [name] Squash-merge a worktree into main and clean up',
|
|
40
|
-
' clean Remove all worktrees that have been merged or are empty',
|
|
41
|
-
' remove <name> Remove a worktree (--force to remove with unmerged changes)',
|
|
42
|
-
'',
|
|
43
|
-
'The -w flag creates/resumes worktrees for interactive sessions:',
|
|
44
|
-
' gsd -w Auto-name a new worktree, or resume the only active one',
|
|
45
|
-
' gsd -w my-feature Create or resume a named worktree',
|
|
46
|
-
'',
|
|
47
|
-
'Lifecycle:',
|
|
48
|
-
' 1. gsd -w Create worktree, start session inside it',
|
|
49
|
-
' 2. (work normally) All changes happen on the worktree branch',
|
|
50
|
-
' 3. Ctrl+C Exit — dirty work is auto-committed',
|
|
51
|
-
' 4. gsd -w Resume where you left off',
|
|
52
|
-
' 5. gsd worktree merge Squash-merge into main when done',
|
|
53
|
-
'',
|
|
54
|
-
'Examples:',
|
|
55
|
-
' gsd -w Start in a new auto-named worktree',
|
|
56
|
-
' gsd -w auth-refactor Create/resume "auth-refactor" worktree',
|
|
57
|
-
' gsd worktree list See all worktrees and their status',
|
|
58
|
-
' gsd worktree merge auth-refactor Merge and clean up',
|
|
59
|
-
' gsd worktree clean Remove all merged/empty worktrees',
|
|
60
|
-
' gsd worktree remove old-branch Remove a specific worktree',
|
|
61
|
-
' gsd worktree remove old-branch --force Remove even with unmerged changes',
|
|
62
|
-
].join('\n'),
|
|
63
32
|
headless: [
|
|
64
33
|
'Usage: gsd headless [flags] [command] [args...]',
|
|
65
34
|
'',
|
|
@@ -103,8 +72,6 @@ const SUBCOMMAND_HELP = {
|
|
|
103
72
|
'Exit codes: 0 = complete, 1 = error/timeout, 2 = blocked',
|
|
104
73
|
].join('\n'),
|
|
105
74
|
};
|
|
106
|
-
// Alias: `gsd wt --help` → same as `gsd worktree --help`
|
|
107
|
-
SUBCOMMAND_HELP['wt'] = SUBCOMMAND_HELP['worktree'];
|
|
108
75
|
export function printHelp(version) {
|
|
109
76
|
process.stdout.write(`GSD v${version} — Get Shit Done\n\n`);
|
|
110
77
|
process.stdout.write('Usage: gsd [options] [message...]\n\n');
|
|
@@ -112,7 +79,6 @@ export function printHelp(version) {
|
|
|
112
79
|
process.stdout.write(' --mode <text|json|rpc|mcp> Output mode (default: interactive)\n');
|
|
113
80
|
process.stdout.write(' --print, -p Single-shot print mode\n');
|
|
114
81
|
process.stdout.write(' --continue, -c Resume the most recent session\n');
|
|
115
|
-
process.stdout.write(' --worktree, -w [name] Start in an isolated worktree (auto-named if omitted)\n');
|
|
116
82
|
process.stdout.write(' --model <id> Override model (e.g. claude-opus-4-6)\n');
|
|
117
83
|
process.stdout.write(' --no-session Disable session persistence\n');
|
|
118
84
|
process.stdout.write(' --extension <path> Load additional extension\n');
|
|
@@ -124,7 +90,6 @@ export function printHelp(version) {
|
|
|
124
90
|
process.stdout.write(' config Re-run the setup wizard\n');
|
|
125
91
|
process.stdout.write(' update Update GSD to the latest version\n');
|
|
126
92
|
process.stdout.write(' sessions List and resume a past session\n');
|
|
127
|
-
process.stdout.write(' worktree <cmd> Manage worktrees (list, merge, clean, remove)\n');
|
|
128
93
|
process.stdout.write(' headless [cmd] [args] Run /gsd commands without TUI (default: auto)\n');
|
|
129
94
|
process.stdout.write('\nRun gsd <subcommand> --help for subcommand-specific help.\n');
|
|
130
95
|
}
|
|
@@ -205,6 +205,13 @@ export function estimateTimeRemaining(): string | null {
|
|
|
205
205
|
|
|
206
206
|
// ─── Slice Progress Cache ─────────────────────────────────────────────────────
|
|
207
207
|
|
|
208
|
+
/** Cached task detail for the widget task checklist */
|
|
209
|
+
interface CachedTaskDetail {
|
|
210
|
+
id: string;
|
|
211
|
+
title: string;
|
|
212
|
+
done: boolean;
|
|
213
|
+
}
|
|
214
|
+
|
|
208
215
|
/** Cached slice progress for the widget — avoid async in render */
|
|
209
216
|
let cachedSliceProgress: {
|
|
210
217
|
done: number;
|
|
@@ -212,6 +219,8 @@ let cachedSliceProgress: {
|
|
|
212
219
|
milestoneId: string;
|
|
213
220
|
/** Real task progress for the active slice, if its plan file exists */
|
|
214
221
|
activeSliceTasks: { done: number; total: number } | null;
|
|
222
|
+
/** Full task list for the active slice checklist */
|
|
223
|
+
taskDetails: CachedTaskDetail[] | null;
|
|
215
224
|
} | null = null;
|
|
216
225
|
|
|
217
226
|
export function updateSliceProgressCache(base: string, mid: string, activeSid?: string): void {
|
|
@@ -222,6 +231,7 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
|
|
|
222
231
|
const roadmap = parseRoadmap(content);
|
|
223
232
|
|
|
224
233
|
let activeSliceTasks: { done: number; total: number } | null = null;
|
|
234
|
+
let taskDetails: CachedTaskDetail[] | null = null;
|
|
225
235
|
if (activeSid) {
|
|
226
236
|
try {
|
|
227
237
|
const planFile = resolveSliceFile(base, mid, activeSid, "PLAN");
|
|
@@ -232,6 +242,7 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
|
|
|
232
242
|
done: plan.tasks.filter(t => t.done).length,
|
|
233
243
|
total: plan.tasks.length,
|
|
234
244
|
};
|
|
245
|
+
taskDetails = plan.tasks.map(t => ({ id: t.id, title: t.title, done: t.done }));
|
|
235
246
|
}
|
|
236
247
|
} catch {
|
|
237
248
|
// Non-fatal — just omit task count
|
|
@@ -243,13 +254,19 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
|
|
|
243
254
|
total: roadmap.slices.length,
|
|
244
255
|
milestoneId: mid,
|
|
245
256
|
activeSliceTasks,
|
|
257
|
+
taskDetails,
|
|
246
258
|
};
|
|
247
259
|
} catch {
|
|
248
260
|
// Non-fatal — widget just won't show progress bar
|
|
249
261
|
}
|
|
250
262
|
}
|
|
251
263
|
|
|
252
|
-
export function getRoadmapSlicesSync(): {
|
|
264
|
+
export function getRoadmapSlicesSync(): {
|
|
265
|
+
done: number;
|
|
266
|
+
total: number;
|
|
267
|
+
activeSliceTasks: { done: number; total: number } | null;
|
|
268
|
+
taskDetails: CachedTaskDetail[] | null;
|
|
269
|
+
} | null {
|
|
253
270
|
return cachedSliceProgress;
|
|
254
271
|
}
|
|
255
272
|
|
|
@@ -350,87 +367,84 @@ export function updateProgressWidget(
|
|
|
350
367
|
const lines: string[] = [];
|
|
351
368
|
const pad = INDENT.base;
|
|
352
369
|
|
|
353
|
-
// ──
|
|
370
|
+
// ── Top bar ─────────────────────────────────────────────────────
|
|
354
371
|
lines.push(...ui.bar());
|
|
355
372
|
|
|
373
|
+
// ── Header: GSD AUTO ... elapsed ────────────────────────────────
|
|
356
374
|
const dot = pulseBright
|
|
357
375
|
? theme.fg("accent", GLYPH.statusActive)
|
|
358
376
|
: theme.fg("dim", GLYPH.statusPending);
|
|
359
377
|
const elapsed = formatAutoElapsed(accessors.getAutoStartTime());
|
|
360
378
|
const modeTag = accessors.isStepMode() ? "NEXT" : "AUTO";
|
|
361
|
-
const headerLeft = `${pad}${dot} ${theme.fg("accent", theme.bold("GSD"))}
|
|
379
|
+
const headerLeft = `${pad}${dot} ${theme.fg("accent", theme.bold("GSD"))} ${theme.fg("success", modeTag)}`;
|
|
362
380
|
const headerRight = elapsed ? theme.fg("dim", elapsed) : "";
|
|
363
381
|
lines.push(rightAlign(headerLeft, headerRight, width));
|
|
364
382
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
if (mid)
|
|
368
|
-
lines.push(truncateToWidth(`${pad}${theme.fg("dim", mid.title)}`, width));
|
|
369
|
-
}
|
|
370
|
-
|
|
383
|
+
// ── Context: project · slice · action (merged into one line) ────
|
|
384
|
+
const contextParts: string[] = [];
|
|
385
|
+
if (mid) contextParts.push(theme.fg("dim", mid.title));
|
|
371
386
|
if (slice && unitType !== "research-milestone" && unitType !== "plan-milestone") {
|
|
372
|
-
|
|
373
|
-
`${pad}${theme.fg("text", theme.bold(`${slice.id}: ${slice.title}`))}`,
|
|
374
|
-
width,
|
|
375
|
-
));
|
|
387
|
+
contextParts.push(theme.fg("text", theme.bold(`${slice.id}: ${slice.title}`)));
|
|
376
388
|
}
|
|
377
|
-
|
|
378
|
-
lines.push("");
|
|
379
|
-
|
|
380
389
|
const isHook = unitType.startsWith("hook/");
|
|
381
390
|
const target = isHook
|
|
382
391
|
? (unitId.split("/").pop() ?? unitId)
|
|
383
392
|
: (task ? `${task.id}: ${task.title}` : unitId);
|
|
384
|
-
|
|
393
|
+
contextParts.push(`${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`);
|
|
394
|
+
|
|
385
395
|
const tierTag = tierBadge ? theme.fg("dim", `[${tierBadge}] `) : "";
|
|
386
396
|
const phaseBadge = `${tierTag}${theme.fg("dim", phaseLabel)}`;
|
|
387
|
-
|
|
388
|
-
lines.push(
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
397
|
+
const contextLine = contextParts.join(theme.fg("dim", " · "));
|
|
398
|
+
lines.push(rightAlign(`${pad}${contextLine}`, phaseBadge, width));
|
|
399
|
+
|
|
400
|
+
// ── Two-column body ─────────────────────────────────────────────
|
|
401
|
+
// Left: progress, ETA, next, stats (fixed) | Right: task checklist (fixed, adjacent)
|
|
402
|
+
// Both columns sit left-to-center; empty space is on the right.
|
|
403
|
+
const divider = theme.fg("dim", "│");
|
|
404
|
+
const minTwoColWidth = 100;
|
|
405
|
+
const rightColFixed = 44;
|
|
406
|
+
const colGap = 5; // breathing room between columns
|
|
407
|
+
// Left column takes remaining space — no truncation on wide terminals
|
|
408
|
+
const useTwoCol = width >= minTwoColWidth;
|
|
409
|
+
const rightColWidth = useTwoCol ? rightColFixed : 0;
|
|
410
|
+
const leftColWidth = useTwoCol ? width - rightColWidth - colGap : width;
|
|
411
|
+
|
|
412
|
+
const roadmapSlices = mid ? getRoadmapSlicesSync() : null;
|
|
413
|
+
|
|
414
|
+
// Build left column: progress bar, ETA, next step, token stats
|
|
415
|
+
const leftLines: string[] = [];
|
|
416
|
+
|
|
417
|
+
if (roadmapSlices) {
|
|
418
|
+
const { done, total, activeSliceTasks } = roadmapSlices;
|
|
419
|
+
const barWidth = Math.max(6, Math.min(18, Math.floor(leftColWidth * 0.4)));
|
|
420
|
+
const pct = total > 0 ? done / total : 0;
|
|
421
|
+
const filled = Math.round(pct * barWidth);
|
|
422
|
+
const bar = theme.fg("success", "█".repeat(filled))
|
|
423
|
+
+ theme.fg("dim", "░".repeat(barWidth - filled));
|
|
424
|
+
|
|
425
|
+
let meta = theme.fg("dim", `${done}/${total} slices`);
|
|
426
|
+
if (activeSliceTasks && activeSliceTasks.total > 0) {
|
|
427
|
+
const taskNum = isHook
|
|
428
|
+
? Math.max(activeSliceTasks.done, 1)
|
|
429
|
+
: Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
|
|
430
|
+
meta += theme.fg("dim", ` · task ${taskNum}/${activeSliceTasks.total}`);
|
|
431
|
+
}
|
|
432
|
+
leftLines.push(truncateToWidth(`${pad}${bar} ${meta}`, leftColWidth));
|
|
415
433
|
|
|
416
|
-
|
|
434
|
+
const eta = estimateTimeRemaining();
|
|
435
|
+
if (eta) {
|
|
436
|
+
leftLines.push(truncateToWidth(`${pad}${theme.fg("dim", eta)}`, leftColWidth));
|
|
417
437
|
}
|
|
418
438
|
}
|
|
419
439
|
|
|
420
|
-
lines.push("");
|
|
421
|
-
|
|
422
440
|
if (next) {
|
|
423
|
-
|
|
441
|
+
leftLines.push(truncateToWidth(
|
|
424
442
|
`${pad}${theme.fg("dim", "→")} ${theme.fg("dim", `then ${next}`)}`,
|
|
425
|
-
|
|
443
|
+
leftColWidth,
|
|
426
444
|
));
|
|
427
445
|
}
|
|
428
446
|
|
|
429
|
-
//
|
|
430
|
-
lines.push("");
|
|
431
|
-
lines.push(truncateToWidth(theme.fg("dim", `${pad}${widgetPwd}`), width, theme.fg("dim", "…")));
|
|
432
|
-
|
|
433
|
-
// Token stats from current unit session + cumulative cost from metrics
|
|
447
|
+
// Token stats
|
|
434
448
|
{
|
|
435
449
|
const cmdCtx = accessors.getCmdCtx();
|
|
436
450
|
let totalInput = 0, totalOutput = 0;
|
|
@@ -465,7 +479,6 @@ export function updateProgressWidget(
|
|
|
465
479
|
if (totalOutput) sp.push(`↓${formatWidgetTokens(totalOutput)}`);
|
|
466
480
|
if (totalCacheRead) sp.push(`R${formatWidgetTokens(totalCacheRead)}`);
|
|
467
481
|
if (totalCacheWrite) sp.push(`W${formatWidgetTokens(totalCacheWrite)}`);
|
|
468
|
-
// Cache hit rate for current unit
|
|
469
482
|
if (totalCacheRead + totalInput > 0) {
|
|
470
483
|
const hitRate = Math.round((totalCacheRead / (totalCacheRead + totalInput)) * 100);
|
|
471
484
|
sp.push(`\u26A1${hitRate}%`);
|
|
@@ -484,33 +497,134 @@ export function updateProgressWidget(
|
|
|
484
497
|
sp.push(cxDisplay);
|
|
485
498
|
}
|
|
486
499
|
|
|
487
|
-
const
|
|
500
|
+
const tokenLine = sp.map(p => p.includes("\x1b[") ? p : theme.fg("dim", p))
|
|
488
501
|
.join(theme.fg("dim", " "));
|
|
502
|
+
leftLines.push(truncateToWidth(`${pad}${tokenLine}`, leftColWidth));
|
|
489
503
|
|
|
490
504
|
const modelId = cmdCtx?.model?.id ?? "";
|
|
491
505
|
const modelProvider = cmdCtx?.model?.provider ?? "";
|
|
492
|
-
const modelPhase = phaseLabel ? theme.fg("dim", `[${phaseLabel}] `) : "";
|
|
493
506
|
const modelDisplay = modelProvider && modelId
|
|
494
507
|
? `${modelProvider}/${modelId}`
|
|
495
508
|
: modelId;
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
lines.push(rightAlign(`${pad}${sLeft}`, sRight, width));
|
|
509
|
+
if (modelDisplay) {
|
|
510
|
+
leftLines.push(truncateToWidth(`${pad}${theme.fg("dim", modelDisplay)}`, leftColWidth));
|
|
511
|
+
}
|
|
500
512
|
|
|
501
|
-
// Dynamic routing savings
|
|
513
|
+
// Dynamic routing savings
|
|
502
514
|
if (mLedger && mLedger.units.some(u => u.tier)) {
|
|
503
515
|
const savings = formatTierSavings(mLedger.units);
|
|
504
516
|
if (savings) {
|
|
505
|
-
|
|
517
|
+
leftLines.push(truncateToWidth(`${pad}${theme.fg("dim", savings)}`, leftColWidth));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Build right column: task checklist (pegged to right edge)
|
|
523
|
+
const rightLines: string[] = [];
|
|
524
|
+
const taskDetails = roadmapSlices?.taskDetails ?? null;
|
|
525
|
+
const maxVisibleTasks = 8;
|
|
526
|
+
const rpad = " ";
|
|
527
|
+
|
|
528
|
+
if (useTwoCol) {
|
|
529
|
+
if (taskDetails && taskDetails.length > 0) {
|
|
530
|
+
const visibleTasks = taskDetails.slice(0, maxVisibleTasks);
|
|
531
|
+
for (const t of visibleTasks) {
|
|
532
|
+
const isCurrent = task && t.id === task.id;
|
|
533
|
+
const glyph = t.done
|
|
534
|
+
? theme.fg("success", GLYPH.statusDone)
|
|
535
|
+
: isCurrent
|
|
536
|
+
? theme.fg("accent", "▸")
|
|
537
|
+
: theme.fg("dim", " ");
|
|
538
|
+
const label = isCurrent
|
|
539
|
+
? theme.fg("text", `${t.id}: ${t.title}`)
|
|
540
|
+
: t.done
|
|
541
|
+
? theme.fg("dim", `${t.id}: ${t.title}`)
|
|
542
|
+
: theme.fg("text", `${t.id}: ${t.title}`);
|
|
543
|
+
rightLines.push(truncateToWidth(`${rpad}${glyph} ${label}`, rightColWidth));
|
|
544
|
+
}
|
|
545
|
+
if (taskDetails.length > maxVisibleTasks) {
|
|
546
|
+
rightLines.push(truncateToWidth(
|
|
547
|
+
`${rpad}${theme.fg("dim", ` …+${taskDetails.length - maxVisibleTasks} more`)}`,
|
|
548
|
+
rightColWidth,
|
|
549
|
+
));
|
|
550
|
+
}
|
|
551
|
+
} else if (roadmapSlices?.activeSliceTasks) {
|
|
552
|
+
const { done: tDone, total: tTotal } = roadmapSlices.activeSliceTasks;
|
|
553
|
+
rightLines.push(`${rpad}${theme.fg("dim", `${tDone}/${tTotal} tasks`)}`);
|
|
554
|
+
}
|
|
555
|
+
} else {
|
|
556
|
+
// Narrow single-column: task list goes into left column
|
|
557
|
+
if (taskDetails && taskDetails.length > 0) {
|
|
558
|
+
for (const t of taskDetails.slice(0, maxVisibleTasks)) {
|
|
559
|
+
const isCurrent = task && t.id === task.id;
|
|
560
|
+
const glyph = t.done
|
|
561
|
+
? theme.fg("success", GLYPH.statusDone)
|
|
562
|
+
: isCurrent
|
|
563
|
+
? theme.fg("accent", "▸")
|
|
564
|
+
: theme.fg("dim", " ");
|
|
565
|
+
const label = isCurrent
|
|
566
|
+
? theme.fg("text", `${t.id}: ${t.title}`)
|
|
567
|
+
: t.done
|
|
568
|
+
? theme.fg("dim", `${t.id}: ${t.title}`)
|
|
569
|
+
: theme.fg("text", `${t.id}: ${t.title}`);
|
|
570
|
+
leftLines.push(truncateToWidth(`${pad}${glyph} ${label}`, leftColWidth));
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
// Add progress bar inline
|
|
574
|
+
if (roadmapSlices) {
|
|
575
|
+
const { done, total, activeSliceTasks } = roadmapSlices;
|
|
576
|
+
const barWidth = Math.max(6, Math.min(18, Math.floor(leftColWidth * 0.4)));
|
|
577
|
+
const pct = total > 0 ? done / total : 0;
|
|
578
|
+
const filled = Math.round(pct * barWidth);
|
|
579
|
+
const bar = theme.fg("success", "█".repeat(filled))
|
|
580
|
+
+ theme.fg("dim", "░".repeat(barWidth - filled));
|
|
581
|
+
let meta = theme.fg("dim", `${done}/${total} slices`);
|
|
582
|
+
if (activeSliceTasks && activeSliceTasks.total > 0) {
|
|
583
|
+
const taskNum = isHook
|
|
584
|
+
? Math.max(activeSliceTasks.done, 1)
|
|
585
|
+
: Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
|
|
586
|
+
meta += theme.fg("dim", ` · task ${taskNum}/${activeSliceTasks.total}`);
|
|
506
587
|
}
|
|
588
|
+
const eta = estimateTimeRemaining();
|
|
589
|
+
if (eta) meta += theme.fg("dim", ` · ${eta}`);
|
|
590
|
+
leftLines.push(truncateToWidth(`${pad}${bar} ${meta}`, leftColWidth));
|
|
591
|
+
}
|
|
592
|
+
if (next) {
|
|
593
|
+
leftLines.push(truncateToWidth(
|
|
594
|
+
`${pad}${theme.fg("dim", "→")} ${theme.fg("dim", `then ${next}`)}`,
|
|
595
|
+
leftColWidth,
|
|
596
|
+
));
|
|
507
597
|
}
|
|
508
598
|
}
|
|
509
599
|
|
|
600
|
+
// Compose columns
|
|
601
|
+
if (useTwoCol) {
|
|
602
|
+
const maxRows = Math.max(leftLines.length, rightLines.length);
|
|
603
|
+
if (maxRows > 0) {
|
|
604
|
+
lines.push(""); // spacer before columns
|
|
605
|
+
for (let i = 0; i < maxRows; i++) {
|
|
606
|
+
const left = padToWidth(leftLines[i] ?? "", leftColWidth);
|
|
607
|
+
const gap = " ".repeat(colGap - 2); // colGap minus divider and its trailing space
|
|
608
|
+
const right = rightLines[i] ?? "";
|
|
609
|
+
lines.push(truncateToWidth(`${left}${gap}${divider} ${right}`, width));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
} else {
|
|
613
|
+
// Narrow single-column: just stack
|
|
614
|
+
if (leftLines.length > 0) {
|
|
615
|
+
lines.push("");
|
|
616
|
+
for (const l of leftLines) lines.push(l);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// ── Footer: pwd + hints ─────────────────────────────────────────
|
|
621
|
+
lines.push("");
|
|
510
622
|
const hintParts: string[] = [];
|
|
511
623
|
hintParts.push("esc pause");
|
|
512
624
|
hintParts.push(process.platform === "darwin" ? "⌃⌥G dashboard" : "Ctrl+Alt+G dashboard");
|
|
513
|
-
|
|
625
|
+
const hintStr = theme.fg("dim", hintParts.join(" | "));
|
|
626
|
+
const pwdStr = theme.fg("dim", widgetPwd);
|
|
627
|
+
lines.push(rightAlign(`${pad}${pwdStr}`, hintStr, width));
|
|
514
628
|
|
|
515
629
|
lines.push(...ui.bar());
|
|
516
630
|
|
|
@@ -628,3 +742,10 @@ function rightAlign(left: string, right: string, width: number): string {
|
|
|
628
742
|
const gap = Math.max(1, width - leftVis - rightVis);
|
|
629
743
|
return truncateToWidth(left + " ".repeat(gap) + right, width);
|
|
630
744
|
}
|
|
745
|
+
|
|
746
|
+
/** Pad a string with trailing spaces to fill exactly `colWidth` (ANSI-aware). */
|
|
747
|
+
function padToWidth(s: string, colWidth: number): string {
|
|
748
|
+
const vis = visibleWidth(s);
|
|
749
|
+
if (vis >= colWidth) return truncateToWidth(s, colWidth);
|
|
750
|
+
return s + " ".repeat(colWidth - vis);
|
|
751
|
+
}
|
|
@@ -1048,19 +1048,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
1048
1048
|
} catch { /* best-effort */ }
|
|
1049
1049
|
}
|
|
1050
1050
|
|
|
1051
|
-
// Auto-commit dirty work in CLI-spawned worktrees so nothing is lost.
|
|
1052
|
-
// The CLI sets GSD_CLI_WORKTREE when launched with -w.
|
|
1053
|
-
const cliWorktree = process.env.GSD_CLI_WORKTREE;
|
|
1054
|
-
if (cliWorktree) {
|
|
1055
|
-
try {
|
|
1056
|
-
const { autoCommitCurrentBranch } = await import("./worktree.js");
|
|
1057
|
-
const msg = autoCommitCurrentBranch(process.cwd(), "session-end", cliWorktree);
|
|
1058
|
-
if (msg) {
|
|
1059
|
-
ctx.ui.notify(`Auto-committed worktree ${cliWorktree} before exit.`, "info");
|
|
1060
|
-
}
|
|
1061
|
-
} catch { /* best-effort */ }
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
1051
|
if (!isAutoActive() && !isAutoPaused()) return;
|
|
1065
1052
|
|
|
1066
1053
|
// Save the current session — the lock file stays on disk
|
|
@@ -96,39 +96,24 @@ export function parseRoadmapSlices(content: string): RoadmapSliceEntry[] {
|
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
98
|
* Fallback parser for prose-style roadmaps where the LLM wrote
|
|
99
|
-
*
|
|
100
|
-
* Extracts slice IDs and titles so auto-mode
|
|
101
|
-
* slices and plan them.
|
|
99
|
+
* `## Slice S01: Title` headers instead of the machine-readable
|
|
100
|
+
* `## Slices` checklist. Extracts slice IDs and titles so auto-mode
|
|
101
|
+
* can at least identify slices and plan them.
|
|
102
102
|
*
|
|
103
|
-
*
|
|
104
|
-
* ## S01: Title (H2, colon separator)
|
|
105
|
-
* ### S01: Title (H3)
|
|
106
|
-
* #### S01: Title (H4)
|
|
107
|
-
* ## Slice S01: Title (with "Slice" prefix)
|
|
108
|
-
* ## S01 — Title (em dash)
|
|
109
|
-
* ## S01 – Title (en dash)
|
|
110
|
-
* ## S01 - Title (hyphen)
|
|
111
|
-
* ## S01. Title (dot separator)
|
|
112
|
-
* ## S01 Title (space only, no separator)
|
|
113
|
-
* ## **S01: Title** (bold-wrapped)
|
|
114
|
-
* ## **S01**: Title (bold ID only)
|
|
115
|
-
* ## S1: Title (non-zero-padded ID)
|
|
103
|
+
* Also handles `## S01: Title` and `## S01 — Title` variants.
|
|
116
104
|
*/
|
|
117
105
|
function parseProseSliceHeaders(content: string): RoadmapSliceEntry[] {
|
|
118
106
|
const slices: RoadmapSliceEntry[] = [];
|
|
119
|
-
|
|
120
|
-
// Separator after the ID is flexible: colon, dash, em/en dash, dot, or just whitespace.
|
|
121
|
-
const headerPattern = /^#{1,4}\s+\*{0,2}(?:Slice\s+)?(S\d+)\*{0,2}[:\s.—–-]*\s*(.+)/gm;
|
|
107
|
+
const headerPattern = /^##\s+(?:Slice\s+)?(S\d+)[:\s—–-]+\s*(.+)/gm;
|
|
122
108
|
let match: RegExpExecArray | null;
|
|
123
109
|
|
|
124
110
|
while ((match = headerPattern.exec(content)) !== null) {
|
|
125
111
|
const id = match[1]!;
|
|
126
|
-
|
|
127
|
-
if (!title) continue; // skip if we only matched the ID with no title
|
|
112
|
+
const title = match[2]!.trim();
|
|
128
113
|
|
|
129
114
|
// Try to extract depends from prose: "Depends on: S01" or "**Depends on:** S01, S02"
|
|
130
115
|
const afterHeader = content.slice(match.index + match[0].length);
|
|
131
|
-
const nextHeader = afterHeader.search(
|
|
116
|
+
const nextHeader = afterHeader.search(/^##\s/m);
|
|
132
117
|
const section = nextHeader !== -1 ? afterHeader.slice(0, nextHeader) : afterHeader.slice(0, 500);
|
|
133
118
|
|
|
134
119
|
const depsMatch = section.match(/\*{0,2}Depends\s+on:?\*{0,2}\s*(.+)/i);
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { createRequire } from "node:module";
|
|
20
|
-
import { existsSync, readFileSync, mkdirSync, unlinkSync
|
|
20
|
+
import { existsSync, readFileSync, mkdirSync, unlinkSync } from "node:fs";
|
|
21
21
|
import { join, dirname } from "node:path";
|
|
22
22
|
import { gsdRoot } from "./paths.js";
|
|
23
23
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
@@ -92,12 +92,11 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
|
|
|
92
92
|
return acquireFallbackLock(basePath, lp, lockData);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
const gsdDir = gsdRoot(basePath);
|
|
96
|
-
|
|
97
95
|
try {
|
|
98
96
|
// Try to acquire an exclusive OS-level lock on the lock file.
|
|
99
97
|
// We lock the directory (gsdRoot) since proper-lockfile works best
|
|
100
98
|
// on directories, and the lock file itself may not exist yet.
|
|
99
|
+
const gsdDir = gsdRoot(basePath);
|
|
101
100
|
mkdirSync(gsdDir, { recursive: true });
|
|
102
101
|
|
|
103
102
|
const release = lockfile.lockSync(gsdDir, {
|
|
@@ -110,53 +109,16 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
|
|
|
110
109
|
_lockedPath = basePath;
|
|
111
110
|
_lockPid = process.pid;
|
|
112
111
|
|
|
113
|
-
// Safety net: clean up lock dir on process exit if _releaseFunction
|
|
114
|
-
// wasn't called (e.g., normal exit after clean completion) (#1245).
|
|
115
|
-
const lockDirForCleanup = join(gsdDir + ".lock");
|
|
116
|
-
process.once("exit", () => {
|
|
117
|
-
try {
|
|
118
|
-
if (_releaseFunction) { _releaseFunction(); _releaseFunction = null; }
|
|
119
|
-
} catch { /* best-effort */ }
|
|
120
|
-
try {
|
|
121
|
-
if (existsSync(lockDirForCleanup)) rmSync(lockDirForCleanup, { recursive: true, force: true });
|
|
122
|
-
} catch { /* best-effort */ }
|
|
123
|
-
});
|
|
124
|
-
|
|
125
112
|
// Write the informational lock data
|
|
126
113
|
atomicWriteSync(lp, JSON.stringify(lockData, null, 2));
|
|
127
114
|
|
|
128
115
|
return { acquired: true };
|
|
129
116
|
} catch (err) {
|
|
130
|
-
// Lock is held by another process
|
|
131
|
-
// Check: if auto.lock is gone and no process is alive, the lock dir is stale.
|
|
117
|
+
// Lock is held by another process
|
|
132
118
|
const existingData = readExistingLockData(lp);
|
|
133
119
|
const existingPid = existingData?.pid;
|
|
134
|
-
|
|
135
|
-
// If no lock file or no alive process, try to clean up and re-acquire (#1245)
|
|
136
|
-
if (!existingData || (existingPid && !isPidAlive(existingPid))) {
|
|
137
|
-
try {
|
|
138
|
-
const lockDir = join(gsdDir + ".lock");
|
|
139
|
-
if (existsSync(lockDir)) rmSync(lockDir, { recursive: true, force: true });
|
|
140
|
-
if (existsSync(lp)) unlinkSync(lp);
|
|
141
|
-
|
|
142
|
-
// Retry acquisition after cleanup
|
|
143
|
-
const release = lockfile.lockSync(gsdDir, {
|
|
144
|
-
realpath: false,
|
|
145
|
-
stale: 300_000,
|
|
146
|
-
update: 10_000,
|
|
147
|
-
});
|
|
148
|
-
_releaseFunction = release;
|
|
149
|
-
_lockedPath = basePath;
|
|
150
|
-
_lockPid = process.pid;
|
|
151
|
-
atomicWriteSync(lp, JSON.stringify(lockData, null, 2));
|
|
152
|
-
return { acquired: true };
|
|
153
|
-
} catch {
|
|
154
|
-
// Retry also failed — fall through to the error path
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
120
|
const reason = existingPid
|
|
159
|
-
? `Another auto-mode session (PID ${existingPid})
|
|
121
|
+
? `Another auto-mode session (PID ${existingPid}) is already running on this project.`
|
|
160
122
|
: `Another auto-mode session is already running on this project.`;
|
|
161
123
|
|
|
162
124
|
return { acquired: false, reason, existingPid };
|
|
@@ -271,17 +233,6 @@ export function releaseSessionLock(basePath: string): void {
|
|
|
271
233
|
// Non-fatal
|
|
272
234
|
}
|
|
273
235
|
|
|
274
|
-
// Remove the proper-lockfile directory (.gsd.lock/) if it exists.
|
|
275
|
-
// proper-lockfile creates this directory as the OS-level lock mechanism.
|
|
276
|
-
// If the process exits without calling _releaseFunction (SIGKILL, crash),
|
|
277
|
-
// this directory is stranded and blocks the next session (#1245).
|
|
278
|
-
try {
|
|
279
|
-
const lockDir = join(gsdRoot(basePath) + ".lock");
|
|
280
|
-
if (existsSync(lockDir)) rmSync(lockDir, { recursive: true, force: true });
|
|
281
|
-
} catch {
|
|
282
|
-
// Non-fatal
|
|
283
|
-
}
|
|
284
|
-
|
|
285
236
|
_lockedPath = null;
|
|
286
237
|
_lockPid = 0;
|
|
287
238
|
}
|