gsd-pi 2.30.0-dev.92a3417 → 2.30.0-dev.ab42fba

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/cli.js +51 -0
  2. package/dist/help-text.js +35 -0
  3. package/dist/resources/extensions/aws-auth/index.ts +144 -0
  4. package/dist/resources/extensions/gsd/auto-dashboard.ts +65 -186
  5. package/dist/resources/extensions/gsd/auto-prompts.ts +2 -10
  6. package/dist/resources/extensions/gsd/auto-start.ts +3 -10
  7. package/dist/resources/extensions/gsd/auto-worktree.ts +12 -8
  8. package/dist/resources/extensions/gsd/auto.ts +2 -2
  9. package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +2 -12
  10. package/dist/resources/extensions/gsd/docs/preferences-reference.md +2 -4
  11. package/dist/resources/extensions/gsd/git-service.ts +4 -22
  12. package/dist/resources/extensions/gsd/gitignore.ts +6 -7
  13. package/dist/resources/extensions/gsd/guided-flow-queue.ts +3 -7
  14. package/dist/resources/extensions/gsd/guided-flow.ts +8 -11
  15. package/dist/resources/extensions/gsd/index.ts +13 -0
  16. package/dist/resources/extensions/gsd/init-wizard.ts +2 -30
  17. package/dist/resources/extensions/gsd/preferences-types.ts +0 -2
  18. package/dist/resources/extensions/gsd/preferences-validation.ts +1 -2
  19. package/dist/resources/extensions/gsd/roadmap-slices.ts +22 -7
  20. package/dist/resources/extensions/gsd/session-lock.ts +53 -4
  21. package/dist/resources/extensions/gsd/templates/preferences.md +0 -1
  22. package/dist/resources/extensions/gsd/tests/git-service.test.ts +14 -42
  23. package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +3 -9
  24. package/dist/resources/extensions/gsd/tests/preferences.test.ts +1 -9
  25. package/dist/resources/extensions/gsd/tests/worktree.test.ts +1 -4
  26. package/dist/resources/extensions/gsd/worktree.ts +2 -2
  27. package/dist/worktree-cli.d.ts +34 -0
  28. package/dist/worktree-cli.js +294 -0
  29. package/dist/worktree-name-gen.d.ts +7 -0
  30. package/dist/worktree-name-gen.js +44 -0
  31. package/package.json +1 -1
  32. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  33. package/packages/pi-coding-agent/dist/core/agent-session.js +14 -0
  34. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  35. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  36. package/packages/pi-coding-agent/dist/core/extensions/loader.js +4 -0
  37. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  38. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  39. package/packages/pi-coding-agent/dist/core/extensions/runner.js +1 -0
  40. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  41. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
  42. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  43. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  44. package/packages/pi-coding-agent/src/core/agent-session.ts +14 -0
  45. package/packages/pi-coding-agent/src/core/extensions/loader.ts +5 -0
  46. package/packages/pi-coding-agent/src/core/extensions/runner.ts +1 -0
  47. package/packages/pi-coding-agent/src/core/extensions/types.ts +8 -0
  48. package/src/resources/extensions/aws-auth/index.ts +144 -0
  49. package/src/resources/extensions/gsd/auto-dashboard.ts +65 -186
  50. package/src/resources/extensions/gsd/auto-prompts.ts +2 -10
  51. package/src/resources/extensions/gsd/auto-start.ts +3 -10
  52. package/src/resources/extensions/gsd/auto-worktree.ts +12 -8
  53. package/src/resources/extensions/gsd/auto.ts +2 -2
  54. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +2 -12
  55. package/src/resources/extensions/gsd/docs/preferences-reference.md +2 -4
  56. package/src/resources/extensions/gsd/git-service.ts +4 -22
  57. package/src/resources/extensions/gsd/gitignore.ts +6 -7
  58. package/src/resources/extensions/gsd/guided-flow-queue.ts +3 -7
  59. package/src/resources/extensions/gsd/guided-flow.ts +8 -11
  60. package/src/resources/extensions/gsd/index.ts +13 -0
  61. package/src/resources/extensions/gsd/init-wizard.ts +2 -30
  62. package/src/resources/extensions/gsd/preferences-types.ts +0 -2
  63. package/src/resources/extensions/gsd/preferences-validation.ts +1 -2
  64. package/src/resources/extensions/gsd/roadmap-slices.ts +22 -7
  65. package/src/resources/extensions/gsd/session-lock.ts +53 -4
  66. package/src/resources/extensions/gsd/templates/preferences.md +0 -1
  67. package/src/resources/extensions/gsd/tests/git-service.test.ts +14 -42
  68. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +3 -9
  69. package/src/resources/extensions/gsd/tests/preferences.test.ts +1 -9
  70. package/src/resources/extensions/gsd/tests/worktree.test.ts +1 -4
  71. package/src/resources/extensions/gsd/worktree.ts +2 -2
@@ -205,13 +205,6 @@ 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
-
215
208
  /** Cached slice progress for the widget — avoid async in render */
216
209
  let cachedSliceProgress: {
217
210
  done: number;
@@ -219,8 +212,6 @@ let cachedSliceProgress: {
219
212
  milestoneId: string;
220
213
  /** Real task progress for the active slice, if its plan file exists */
221
214
  activeSliceTasks: { done: number; total: number } | null;
222
- /** Full task list for the active slice checklist */
223
- taskDetails: CachedTaskDetail[] | null;
224
215
  } | null = null;
225
216
 
226
217
  export function updateSliceProgressCache(base: string, mid: string, activeSid?: string): void {
@@ -231,7 +222,6 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
231
222
  const roadmap = parseRoadmap(content);
232
223
 
233
224
  let activeSliceTasks: { done: number; total: number } | null = null;
234
- let taskDetails: CachedTaskDetail[] | null = null;
235
225
  if (activeSid) {
236
226
  try {
237
227
  const planFile = resolveSliceFile(base, mid, activeSid, "PLAN");
@@ -242,7 +232,6 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
242
232
  done: plan.tasks.filter(t => t.done).length,
243
233
  total: plan.tasks.length,
244
234
  };
245
- taskDetails = plan.tasks.map(t => ({ id: t.id, title: t.title, done: t.done }));
246
235
  }
247
236
  } catch {
248
237
  // Non-fatal — just omit task count
@@ -254,19 +243,13 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
254
243
  total: roadmap.slices.length,
255
244
  milestoneId: mid,
256
245
  activeSliceTasks,
257
- taskDetails,
258
246
  };
259
247
  } catch {
260
248
  // Non-fatal — widget just won't show progress bar
261
249
  }
262
250
  }
263
251
 
264
- export function getRoadmapSlicesSync(): {
265
- done: number;
266
- total: number;
267
- activeSliceTasks: { done: number; total: number } | null;
268
- taskDetails: CachedTaskDetail[] | null;
269
- } | null {
252
+ export function getRoadmapSlicesSync(): { done: number; total: number; activeSliceTasks: { done: number; total: number } | null } | null {
270
253
  return cachedSliceProgress;
271
254
  }
272
255
 
@@ -367,84 +350,87 @@ export function updateProgressWidget(
367
350
  const lines: string[] = [];
368
351
  const pad = INDENT.base;
369
352
 
370
- // ── Top bar ─────────────────────────────────────────────────────
353
+ // ── Line 1: Top bar ───────────────────────────────────────────────
371
354
  lines.push(...ui.bar());
372
355
 
373
- // ── Header: GSD AUTO ... elapsed ────────────────────────────────
374
356
  const dot = pulseBright
375
357
  ? theme.fg("accent", GLYPH.statusActive)
376
358
  : theme.fg("dim", GLYPH.statusPending);
377
359
  const elapsed = formatAutoElapsed(accessors.getAutoStartTime());
378
360
  const modeTag = accessors.isStepMode() ? "NEXT" : "AUTO";
379
- const headerLeft = `${pad}${dot} ${theme.fg("accent", theme.bold("GSD"))} ${theme.fg("success", modeTag)}`;
361
+ const headerLeft = `${pad}${dot} ${theme.fg("accent", theme.bold("GSD"))} ${theme.fg("success", modeTag)}`;
380
362
  const headerRight = elapsed ? theme.fg("dim", elapsed) : "";
381
363
  lines.push(rightAlign(headerLeft, headerRight, width));
382
364
 
383
- // ── Context: project · slice · action (merged into one line) ────
384
- const contextParts: string[] = [];
385
- if (mid) contextParts.push(theme.fg("dim", mid.title));
365
+ lines.push("");
366
+
367
+ if (mid) {
368
+ lines.push(truncateToWidth(`${pad}${theme.fg("dim", mid.title)}`, width));
369
+ }
370
+
386
371
  if (slice && unitType !== "research-milestone" && unitType !== "plan-milestone") {
387
- contextParts.push(theme.fg("text", theme.bold(`${slice.id}: ${slice.title}`)));
372
+ lines.push(truncateToWidth(
373
+ `${pad}${theme.fg("text", theme.bold(`${slice.id}: ${slice.title}`))}`,
374
+ width,
375
+ ));
388
376
  }
377
+
378
+ lines.push("");
379
+
389
380
  const isHook = unitType.startsWith("hook/");
390
381
  const target = isHook
391
382
  ? (unitId.split("/").pop() ?? unitId)
392
383
  : (task ? `${task.id}: ${task.title}` : unitId);
393
- contextParts.push(`${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`);
394
-
384
+ const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
395
385
  const tierTag = tierBadge ? theme.fg("dim", `[${tierBadge}] `) : "";
396
386
  const phaseBadge = `${tierTag}${theme.fg("dim", phaseLabel)}`;
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));
387
+ lines.push(rightAlign(actionLeft, phaseBadge, width));
388
+ lines.push("");
433
389
 
434
- const eta = estimateTimeRemaining();
435
- if (eta) {
436
- leftLines.push(truncateToWidth(`${pad}${theme.fg("dim", eta)}`, leftColWidth));
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
+ }
415
+
416
+ lines.push(truncateToWidth(`${pad}${bar} ${meta}`, width));
437
417
  }
438
418
  }
439
419
 
420
+ lines.push("");
421
+
440
422
  if (next) {
441
- leftLines.push(truncateToWidth(
423
+ lines.push(truncateToWidth(
442
424
  `${pad}${theme.fg("dim", "→")} ${theme.fg("dim", `then ${next}`)}`,
443
- leftColWidth,
425
+ width,
444
426
  ));
445
427
  }
446
428
 
447
- // Token stats
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
448
434
  {
449
435
  const cmdCtx = accessors.getCmdCtx();
450
436
  let totalInput = 0, totalOutput = 0;
@@ -479,6 +465,7 @@ export function updateProgressWidget(
479
465
  if (totalOutput) sp.push(`↓${formatWidgetTokens(totalOutput)}`);
480
466
  if (totalCacheRead) sp.push(`R${formatWidgetTokens(totalCacheRead)}`);
481
467
  if (totalCacheWrite) sp.push(`W${formatWidgetTokens(totalCacheWrite)}`);
468
+ // Cache hit rate for current unit
482
469
  if (totalCacheRead + totalInput > 0) {
483
470
  const hitRate = Math.round((totalCacheRead / (totalCacheRead + totalInput)) * 100);
484
471
  sp.push(`\u26A1${hitRate}%`);
@@ -497,134 +484,33 @@ export function updateProgressWidget(
497
484
  sp.push(cxDisplay);
498
485
  }
499
486
 
500
- const tokenLine = sp.map(p => p.includes("\x1b[") ? p : theme.fg("dim", p))
487
+ const sLeft = sp.map(p => p.includes("\x1b[") ? p : theme.fg("dim", p))
501
488
  .join(theme.fg("dim", " "));
502
- leftLines.push(truncateToWidth(`${pad}${tokenLine}`, leftColWidth));
503
489
 
504
490
  const modelId = cmdCtx?.model?.id ?? "";
505
491
  const modelProvider = cmdCtx?.model?.provider ?? "";
492
+ const modelPhase = phaseLabel ? theme.fg("dim", `[${phaseLabel}] `) : "";
506
493
  const modelDisplay = modelProvider && modelId
507
494
  ? `${modelProvider}/${modelId}`
508
495
  : modelId;
509
- if (modelDisplay) {
510
- leftLines.push(truncateToWidth(`${pad}${theme.fg("dim", modelDisplay)}`, leftColWidth));
511
- }
496
+ const sRight = modelDisplay
497
+ ? `${modelPhase}${theme.fg("dim", modelDisplay)}`
498
+ : "";
499
+ lines.push(rightAlign(`${pad}${sLeft}`, sRight, width));
512
500
 
513
- // Dynamic routing savings
501
+ // Dynamic routing savings summary
514
502
  if (mLedger && mLedger.units.some(u => u.tier)) {
515
503
  const savings = formatTierSavings(mLedger.units);
516
504
  if (savings) {
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}`);
505
+ lines.push(truncateToWidth(theme.fg("dim", `${pad}${savings}`), width));
587
506
  }
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
- ));
597
507
  }
598
508
  }
599
509
 
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("");
622
510
  const hintParts: string[] = [];
623
511
  hintParts.push("esc pause");
624
512
  hintParts.push(process.platform === "darwin" ? "⌃⌥G dashboard" : "Ctrl+Alt+G dashboard");
625
- const hintStr = theme.fg("dim", hintParts.join(" | "));
626
- const pwdStr = theme.fg("dim", widgetPwd);
627
- lines.push(rightAlign(`${pad}${pwdStr}`, hintStr, width));
513
+ lines.push(...ui.hints(hintParts));
628
514
 
629
515
  lines.push(...ui.bar());
630
516
 
@@ -742,10 +628,3 @@ function rightAlign(left: string, right: string, width: number): string {
742
628
  const gap = Math.max(1, width - leftVis - rightVis);
743
629
  return truncateToWidth(left + " ".repeat(gap) + right, width);
744
630
  }
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
- }
@@ -732,11 +732,7 @@ export async function buildPlanSlicePrompt(
732
732
  const executorContextConstraints = formatExecutorConstraints();
733
733
 
734
734
  const outputRelPath = relSliceFile(base, mid, sid, "PLAN");
735
- const prefs = loadEffectiveGSDPreferences();
736
- const commitDocsEnabled = prefs?.preferences?.git?.commit_docs !== false;
737
- const commitInstruction = commitDocsEnabled
738
- ? `Commit the plan files only: \`git add ${relSlicePath(base, mid, sid)}/ .gsd/DECISIONS.md .gitignore && git commit -m "docs(${sid}): add slice plan"\`. Do not stage .gsd/STATE.md or other runtime files — the system manages those.`
739
- : "Do not commit — planning docs are not tracked in git for this project.";
735
+ const commitInstruction = "Do not commit planning artifacts — .gsd/ is managed externally.";
740
736
  return loadPrompt("plan-slice", {
741
737
  workingDirectory: base,
742
738
  milestoneId: mid, sliceId: sid, sliceTitle: sTitle,
@@ -1194,11 +1190,7 @@ export async function buildReassessRoadmapPrompt(
1194
1190
  // Non-fatal — captures module may not be available
1195
1191
  }
1196
1192
 
1197
- const reassessPrefs = loadEffectiveGSDPreferences();
1198
- const reassessCommitDocsEnabled = reassessPrefs?.preferences?.git?.commit_docs !== false;
1199
- const reassessCommitInstruction = reassessCommitDocsEnabled
1200
- ? `Commit: \`docs(${mid}): reassess roadmap after ${completedSliceId}\`. Stage only the .gsd/milestones/ files you changed — do not stage .gsd/STATE.md or other runtime files.`
1201
- : "Do not commit — planning docs are not tracked in git for this project.";
1193
+ const reassessCommitInstruction = "Do not commit planning artifacts — .gsd/ is managed externally.";
1202
1194
 
1203
1195
  return loadPrompt("reassess-roadmap", {
1204
1196
  workingDirectory: base,
@@ -37,7 +37,7 @@ import {
37
37
  } from "./session-lock.js";
38
38
  import { selfHealRuntimeRecords } from "./auto-recovery.js";
39
39
  import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
40
- import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit } from "./native-git-bridge.js";
40
+ import { nativeIsRepo, nativeInit } from "./native-git-bridge.js";
41
41
  import { GitServiceImpl } from "./git-service.js";
42
42
  import {
43
43
  captureIntegrationBranch,
@@ -109,9 +109,8 @@ export async function bootstrapAutoSession(
109
109
 
110
110
  // Ensure .gitignore has baseline patterns
111
111
  const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git;
112
- const commitDocs = gitPrefs?.commit_docs;
113
112
  const manageGitignore = gitPrefs?.manage_gitignore;
114
- ensureGitignore(base, { commitDocs, manageGitignore });
113
+ ensureGitignore(base, { manageGitignore });
115
114
  if (manageGitignore !== false) untrackRuntimeFiles(base);
116
115
 
117
116
  // Migrate legacy in-project .gsd/ to external state directory
@@ -127,12 +126,6 @@ export async function bootstrapAutoSession(
127
126
  const gsdDir = gsdRoot(base);
128
127
  if (!existsSync(gsdDir)) {
129
128
  mkdirSync(join(gsdDir, "milestones"), { recursive: true });
130
- if (commitDocs !== false) {
131
- try {
132
- nativeAddAll(base);
133
- nativeCommit(base, "chore: init gsd");
134
- } catch { /* nothing to commit */ }
135
- }
136
129
  }
137
130
 
138
131
  // Initialize GitServiceImpl
@@ -323,7 +316,7 @@ export async function bootstrapAutoSession(
323
316
  // Capture integration branch
324
317
  if (s.currentMilestoneId) {
325
318
  if (getIsolationMode() !== "none") {
326
- captureIntegrationBranch(base, s.currentMilestoneId, { commitDocs });
319
+ captureIntegrationBranch(base, s.currentMilestoneId);
327
320
  }
328
321
  setActiveMilestoneId(base, s.currentMilestoneId);
329
322
  }
@@ -6,7 +6,7 @@
6
6
  * manages create, enter, detect, and teardown for auto-mode worktrees.
7
7
  */
8
8
 
9
- import { existsSync, readFileSync, realpathSync, unlinkSync, statSync } from "node:fs";
9
+ import { existsSync, readFileSync, realpathSync, unlinkSync, statSync, rmSync } from "node:fs";
10
10
  import { isAbsolute, join, sep } from "node:path";
11
11
  import { GSDError, GSD_IO_ERROR, GSD_GIT_ERROR } from "./errors.js";
12
12
  import { execSync, execFileSync } from "node:child_process";
@@ -370,14 +370,18 @@ export function mergeMilestoneToMain(
370
370
  // squash merge can fail with "Your local changes would be overwritten" (#1127).
371
371
  autoCommitDirtyState(originalBasePath_);
372
372
 
373
- // 3b. Remove untracked .gsd/ files that syncStateToProjectRoot copied.
374
- // autoCommitDirtyState stages and commits everything (git add -A), but if
375
- // the project root branch has no .gsd/ tracking (e.g., .gsd/ is gitignored),
376
- // these files remain untracked and cause "untracked working tree files would
377
- // be overwritten by merge" during squash-merge (#1237).
373
+ // 3b. Remove untracked .gsd/ runtime files that syncStateToProjectRoot copied.
374
+ // Only clean specific runtime files NEVER touch milestones/, decisions, or
375
+ // other planning artifacts that represent user work (#1250).
376
+ const runtimeFilesToClean = ["STATE.md", "completed-units.json", "auto.lock", "gsd.db"];
377
+ for (const f of runtimeFilesToClean) {
378
+ const p = join(originalBasePath_, ".gsd", f);
379
+ try { if (existsSync(p)) unlinkSync(p); } catch { /* non-fatal */ }
380
+ }
378
381
  try {
379
- execFileSync("git", ["clean", "-fd", ".gsd/"], { cwd: originalBasePath_, stdio: "pipe" });
380
- } catch { /* non-fatal clean failure shouldn't block merge attempt */ }
382
+ const runtimeDir = join(originalBasePath_, ".gsd", "runtime");
383
+ if (existsSync(runtimeDir)) rmSync(runtimeDir, { recursive: true, force: true });
384
+ } catch { /* non-fatal */ }
381
385
 
382
386
  // 4. Resolve integration branch — prefer milestone metadata, fall back to preferences / "main"
383
387
  const prefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
@@ -1132,7 +1132,7 @@ async function dispatchNextUnit(
1132
1132
  midTitle = state.activeMilestone?.title;
1133
1133
 
1134
1134
  if (mid) {
1135
- captureIntegrationBranch(s.basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
1135
+ captureIntegrationBranch(s.basePath, mid);
1136
1136
  try {
1137
1137
  const wtPath = createAutoWorktree(s.basePath, mid);
1138
1138
  s.basePath = wtPath;
@@ -1147,7 +1147,7 @@ async function dispatchNextUnit(
1147
1147
  }
1148
1148
  } else {
1149
1149
  if (getIsolationMode() !== "none") {
1150
- captureIntegrationBranch(s.originalBasePath || s.basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
1150
+ captureIntegrationBranch(s.originalBasePath || s.basePath, mid);
1151
1151
  }
1152
1152
  }
1153
1153
 
@@ -469,16 +469,6 @@ async function configureGit(ctx: ExtensionCommandContext, prefs: Record<string,
469
469
  git.isolation = isolationChoice;
470
470
  }
471
471
 
472
- // commit_docs
473
- const currentCommitDocs = git.commit_docs;
474
- const commitDocsChoice = await ctx.ui.select(
475
- `Track .gsd/ planning docs in git${currentCommitDocs !== undefined ? ` (current: ${currentCommitDocs})` : ""}:`,
476
- ["true", "false", "(keep current)"],
477
- );
478
- if (commitDocsChoice && commitDocsChoice !== "(keep current)") {
479
- git.commit_docs = commitDocsChoice === "true";
480
- }
481
-
482
472
  if (Object.keys(git).length > 0) {
483
473
  prefs.git = git;
484
474
  }
@@ -598,13 +588,13 @@ export async function configureMode(ctx: ExtensionCommandContext, prefs: Record<
598
588
  if (modeStr.startsWith("solo")) {
599
589
  prefs.mode = "solo";
600
590
  ctx.ui.notify(
601
- "Mode: solo — defaults: auto_push=true, push_branches=false, pre_merge_check=false, merge_strategy=squash, isolation=worktree, commit_docs=true, unique_milestone_ids=false",
591
+ "Mode: solo — defaults: auto_push=true, push_branches=false, pre_merge_check=false, merge_strategy=squash, isolation=worktree, unique_milestone_ids=false",
602
592
  "info",
603
593
  );
604
594
  } else if (modeStr.startsWith("team")) {
605
595
  prefs.mode = "team";
606
596
  ctx.ui.notify(
607
- "Mode: team — defaults: auto_push=false, push_branches=true, pre_merge_check=true, merge_strategy=squash, isolation=worktree, commit_docs=true, unique_milestone_ids=true",
597
+ "Mode: team — defaults: auto_push=false, push_branches=true, pre_merge_check=true, merge_strategy=squash, isolation=worktree, unique_milestone_ids=true",
608
598
  "info",
609
599
  );
610
600
  } else {
@@ -82,7 +82,6 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
82
82
  | `git.pre_merge_check` | `false` | `true` |
83
83
  | `git.merge_strategy` | `"squash"` | `"squash"` |
84
84
  | `git.isolation` | `"worktree"` | `"worktree"` |
85
- | `git.commit_docs` | `true` | `true` |
86
85
  | `unique_milestone_ids` | `false` | `true` |
87
86
 
88
87
  Quick setup: `/gsd mode` (global) or `/gsd mode project` (project-level).
@@ -127,7 +126,6 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
127
126
  - `main_branch`: string — the primary branch name for new git repos (e.g., `"main"`, `"master"`, `"trunk"`). Also used by `getMainBranch()` as the preferred branch when auto-detection is ambiguous. Default: `"main"`.
128
127
  - `merge_strategy`: `"squash"` or `"merge"` — controls how worktree branches are merged back. `"squash"` combines all commits into one; `"merge"` preserves individual commits. Default: `"squash"`.
129
128
  - `isolation`: `"worktree"`, `"branch"`, or `"none"` — controls auto-mode git isolation strategy. `"worktree"` creates a milestone worktree for isolated work; `"branch"` works directly in the project root but creates a milestone branch (useful for submodule-heavy repos); `"none"` works directly on the current branch with no worktree or milestone branch (ideal for step-mode with hot reloads). Default: `"worktree"`.
130
- - `commit_docs`: boolean — when `false`, prevents GSD from committing `.gsd/` planning artifacts to git. The `.gsd/` folder is added to `.gitignore` and kept local-only. Useful for teams where only some members use GSD, or when company policy requires a clean repository. Default: `true`.
131
129
  - `manage_gitignore`: boolean — when `false`, GSD will not touch `.gitignore` at all. Useful when your project has a strictly managed `.gitignore` and you don't want GSD adding entries. Default: `true`.
132
130
  - `worktree_post_create`: string — script to run after a worktree is created (both auto-mode and manual `/worktree`). Receives `SOURCE_DIR` and `WORKTREE_DIR` as environment variables. Can be absolute or relative to project root. Runs with 30-second timeout. Failure is non-fatal (logged as warning). Default: none.
133
131
 
@@ -244,7 +242,7 @@ mode: solo
244
242
  ---
245
243
  ```
246
244
 
247
- Equivalent to setting `git.auto_push: true`, `git.push_branches: false`, `git.pre_merge_check: false`, `git.merge_strategy: squash`, `git.isolation: worktree`, `git.commit_docs: true`, `unique_milestone_ids: false`.
245
+ Equivalent to setting `git.auto_push: true`, `git.push_branches: false`, `git.pre_merge_check: false`, `git.merge_strategy: squash`, `git.isolation: worktree`, `unique_milestone_ids: false`.
248
246
 
249
247
  **Team — unique IDs, push branches, pre-merge checks:**
250
248
 
@@ -255,7 +253,7 @@ mode: team
255
253
  ---
256
254
  ```
257
255
 
258
- Equivalent to setting `git.auto_push: false`, `git.push_branches: true`, `git.pre_merge_check: true`, `git.merge_strategy: squash`, `git.isolation: worktree`, `git.commit_docs: true`, `unique_milestone_ids: true`.
256
+ Equivalent to setting `git.auto_push: false`, `git.push_branches: true`, `git.pre_merge_check: true`, `git.merge_strategy: squash`, `git.isolation: worktree`, `unique_milestone_ids: true`.
259
257
 
260
258
  **Mode with overrides — team mode but with auto-push:**
261
259
 
@@ -50,11 +50,6 @@ export interface GitPreferences {
50
50
  * - "none": no git isolation — commits land on the user's current branch directly
51
51
  */
52
52
  isolation?: "worktree" | "branch" | "none";
53
- /** When false, prevents GSD from committing .gsd/ planning artifacts to git.
54
- * The .gsd/ folder is added to .gitignore and kept local-only.
55
- * Default: true (planning docs are tracked in git).
56
- */
57
- commit_docs?: boolean;
58
53
  /** When false, GSD will not modify .gitignore at all — no baseline patterns
59
54
  * are added and no self-healing occurs. Use this if you manage your own
60
55
  * .gitignore and don't want GSD touching it.
@@ -226,7 +221,7 @@ export function readIntegrationBranch(basePath: string, milestoneId: string): st
226
221
  *
227
222
  * The file is committed immediately so the metadata is persisted in git.
228
223
  */
229
- export function writeIntegrationBranch(basePath: string, milestoneId: string, branch: string, options?: { commitDocs?: boolean }): void {
224
+ export function writeIntegrationBranch(basePath: string, milestoneId: string, branch: string): void {
230
225
  // Don't record slice branches as the integration target
231
226
  if (SLICE_BRANCH_RE.test(branch)) return;
232
227
  // Validate
@@ -250,18 +245,7 @@ export function writeIntegrationBranch(basePath: string, milestoneId: string, br
250
245
 
251
246
  existing.integrationBranch = branch;
252
247
  writeFileSync(metaFile, JSON.stringify(existing, null, 2) + "\n", "utf-8");
253
-
254
- // Commit immediately so the metadata is persisted in git.
255
- // Skip when commit_docs is explicitly false — .gsd/ is local-only.
256
- if (options?.commitDocs !== false) {
257
- try {
258
- nativeAddPaths(basePath, [metaFile]);
259
- nativeCommit(basePath, `chore(${milestoneId}): record integration branch`, { allowEmpty: false });
260
- } catch {
261
- // Non-fatal — file is on disk even if commit fails (e.g. nothing to commit
262
- // because the file was already tracked with identical content)
263
- }
264
- }
248
+ // .gsd/ is managed externally (symlinked) — metadata is not committed to git.
265
249
  }
266
250
 
267
251
  // ─── Git Helper ────────────────────────────────────────────────────────────
@@ -350,10 +334,8 @@ export class GitServiceImpl {
350
334
  * @param extraExclusions Additional pathspec exclusions beyond RUNTIME_EXCLUSION_PATHS.
351
335
  */
352
336
  private smartStage(extraExclusions: readonly string[] = []): void {
353
- // When commit_docs is false, exclude the entire .gsd/ directory from staging
354
- const commitDocsDisabled = this.prefs.commit_docs === false;
355
- const gsdExclusion = commitDocsDisabled ? [".gsd/"] : [];
356
- const allExclusions = [...RUNTIME_EXCLUSION_PATHS, ...gsdExclusion, ...extraExclusions];
337
+ // Always exclude .gsd/ — state is managed externally (symlinked to ~/.gsd/projects/<hash>/)
338
+ const allExclusions = [".gsd/", ...extraExclusions];
357
339
 
358
340
  // One-time cleanup: if runtime files are already tracked in the index
359
341
  // (from older versions where the fallback bug staged them), untrack them
@@ -79,15 +79,14 @@ const BASELINE_PATTERNS = [
79
79
  ];
80
80
 
81
81
  /**
82
- * Ensure basePath/.gitignore contains all baseline patterns.
83
- * Creates the file if missing; appends only missing lines if it exists.
82
+ * Ensure basePath/.gitignore contains a blanket `.gsd/` ignore.
83
+ * Creates the file if missing; appends `.gsd/` if not present.
84
84
  * Returns true if the file was created or modified, false if already complete.
85
85
  *
86
- * When `commitDocs` is false, the entire `.gsd/` directory is added to
87
- * .gitignore instead of individual runtime patterns, keeping all GSD
88
- * artifacts local-only.
86
+ * `.gsd/` state is managed externally (symlinked to `~/.gsd/projects/<hash>/`),
87
+ * so the entire directory is always gitignored.
89
88
  */
90
- export function ensureGitignore(basePath: string, options?: { commitDocs?: boolean; manageGitignore?: boolean }): boolean {
89
+ export function ensureGitignore(basePath: string, options?: { manageGitignore?: boolean }): boolean {
91
90
  // If manage_gitignore is explicitly false, do not touch .gitignore at all
92
91
  if (options?.manageGitignore === false) return false;
93
92
 
@@ -192,7 +191,7 @@ See \`~/.gsd/agent/extensions/gsd/docs/preferences-reference.md\` for full field
192
191
  - \`models\`: Model preferences for specific task types
193
192
  - \`skill_discovery\`: Automatic skill detection preferences
194
193
  - \`auto_supervisor\`: Supervision and gating rules for autonomous modes
195
- - \`git\`: Git preferences — \`main_branch\` (default branch name for new repos, e.g., "main", "master", "trunk"), \`auto_push\`, \`snapshots\`, \`commit_docs\` (set to \`false\` to keep .gsd/ local-only), etc.
194
+ - \`git\`: Git preferences — \`main_branch\` (default branch name for new repos, e.g., "main", "master", "trunk"), \`auto_push\`, \`snapshots\`, etc.
196
195
 
197
196
  ## Examples
198
197