gsd-pi 2.30.0-dev.7e1bbce → 2.30.0-dev.92a3417

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 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(): { done: number; total: number; activeSliceTasks: { done: number; total: number } | null } | null {
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
- // ── Line 1: Top bar ───────────────────────────────────────────────
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"))} ${theme.fg("success", modeTag)}`;
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
- lines.push("");
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
- lines.push(truncateToWidth(
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
- const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
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
- lines.push(rightAlign(actionLeft, phaseBadge, width));
388
- lines.push("");
389
-
390
- if (mid) {
391
- const roadmapSlices = getRoadmapSlicesSync();
392
- if (roadmapSlices) {
393
- const { done, total, activeSliceTasks } = roadmapSlices;
394
- const barWidth = Math.max(8, Math.min(24, Math.floor(width * 0.3)));
395
- const pct = total > 0 ? done / total : 0;
396
- const filled = Math.round(pct * barWidth);
397
- const bar = theme.fg("success", "█".repeat(filled))
398
- + theme.fg("dim", "░".repeat(barWidth - filled));
399
-
400
- let meta = theme.fg("dim", `${done}/${total} slices`);
401
-
402
- if (activeSliceTasks && activeSliceTasks.total > 0) {
403
- // For hooks, show the trigger task number (done), not the next task (done + 1)
404
- const taskNum = isHook
405
- ? Math.max(activeSliceTasks.done, 1)
406
- : Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
407
- meta += theme.fg("dim", ` · task ${taskNum}/${activeSliceTasks.total}`);
408
- }
409
-
410
- // ETA estimate
411
- const eta = estimateTimeRemaining();
412
- if (eta) {
413
- meta += theme.fg("dim", ` · ${eta}`);
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
- lines.push(truncateToWidth(`${pad}${bar} ${meta}`, width));
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
- lines.push(truncateToWidth(
441
+ leftLines.push(truncateToWidth(
424
442
  `${pad}${theme.fg("dim", "→")} ${theme.fg("dim", `then ${next}`)}`,
425
- width,
443
+ leftColWidth,
426
444
  ));
427
445
  }
428
446
 
429
- // ── Footer info (pwd, tokens, cost, context, model) ──────────────
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 sLeft = sp.map(p => p.includes("\x1b[") ? p : theme.fg("dim", p))
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
- const sRight = modelDisplay
497
- ? `${modelPhase}${theme.fg("dim", modelDisplay)}`
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 summary
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
- lines.push(truncateToWidth(theme.fg("dim", `${pad}${savings}`), width));
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
- lines.push(...ui.hints(hintParts));
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
- * slice headers instead of the machine-readable `## Slices` checklist.
100
- * Extracts slice IDs and titles so auto-mode can at least identify
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
- * Handles these LLM-generated variants:
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
- // Match H1–H4 headers containing S<digits> with optional "Slice" prefix and bold markers.
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
- let title = match[2]!.trim().replace(/\*{1,2}$/g, "").trim(); // strip trailing bold markers
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(/^#{1,4}\s/m);
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, rmSync, statSync } from "node:fs";
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 — or the .gsd.lock/ directory is stranded.
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}) appears to be running.\nStop it with \`kill ${existingPid}\` before starting a new session.`
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.30.0-dev.7e1bbce",
3
+ "version": "2.30.0-dev.92a3417",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {