gsd-pi 2.29.0-dev.77f06e2 → 2.29.0-dev.953d788

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 (80) hide show
  1. package/README.md +17 -24
  2. package/dist/resources/extensions/bg-shell/process-manager.ts +0 -13
  3. package/dist/resources/extensions/gsd/auto-dashboard.ts +65 -186
  4. package/dist/resources/extensions/gsd/auto-post-unit.ts +3 -6
  5. package/dist/resources/extensions/gsd/auto-recovery.ts +22 -16
  6. package/dist/resources/extensions/gsd/auto-worktree-sync.ts +6 -7
  7. package/dist/resources/extensions/gsd/auto.ts +15 -0
  8. package/dist/resources/extensions/gsd/commands-handlers.ts +1 -20
  9. package/dist/resources/extensions/gsd/commands-logs.ts +14 -13
  10. package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +14 -44
  11. package/dist/resources/extensions/gsd/commands.ts +22 -55
  12. package/dist/resources/extensions/gsd/dashboard-overlay.ts +1 -2
  13. package/dist/resources/extensions/gsd/json-persistence.ts +1 -16
  14. package/dist/resources/extensions/gsd/queue-order.ts +11 -10
  15. package/dist/resources/extensions/gsd/session-status-io.ts +41 -23
  16. package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  17. package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
  18. package/dist/resources/extensions/gsd/tests/extension-selector-separator.test.ts +38 -60
  19. package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
  20. package/dist/resources/extensions/mcporter/index.ts +525 -0
  21. package/dist/resources/extensions/remote-questions/discord-adapter.ts +19 -8
  22. package/dist/resources/extensions/remote-questions/slack-adapter.ts +17 -11
  23. package/dist/resources/extensions/remote-questions/telegram-adapter.ts +19 -8
  24. package/package.json +1 -1
  25. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  26. package/packages/pi-coding-agent/dist/core/extensions/loader.js +0 -13
  27. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  28. package/packages/pi-coding-agent/src/core/extensions/loader.ts +0 -13
  29. package/src/resources/extensions/bg-shell/process-manager.ts +0 -13
  30. package/src/resources/extensions/gsd/auto-dashboard.ts +65 -186
  31. package/src/resources/extensions/gsd/auto-post-unit.ts +3 -6
  32. package/src/resources/extensions/gsd/auto-recovery.ts +22 -16
  33. package/src/resources/extensions/gsd/auto-worktree-sync.ts +6 -7
  34. package/src/resources/extensions/gsd/auto.ts +15 -0
  35. package/src/resources/extensions/gsd/commands-handlers.ts +1 -20
  36. package/src/resources/extensions/gsd/commands-logs.ts +14 -13
  37. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +14 -44
  38. package/src/resources/extensions/gsd/commands.ts +22 -55
  39. package/src/resources/extensions/gsd/dashboard-overlay.ts +1 -2
  40. package/src/resources/extensions/gsd/json-persistence.ts +1 -16
  41. package/src/resources/extensions/gsd/queue-order.ts +11 -10
  42. package/src/resources/extensions/gsd/session-status-io.ts +41 -23
  43. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  44. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
  45. package/src/resources/extensions/gsd/tests/extension-selector-separator.test.ts +38 -60
  46. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
  47. package/src/resources/extensions/mcporter/index.ts +525 -0
  48. package/src/resources/extensions/remote-questions/discord-adapter.ts +19 -8
  49. package/src/resources/extensions/remote-questions/slack-adapter.ts +17 -11
  50. package/src/resources/extensions/remote-questions/telegram-adapter.ts +19 -8
  51. package/dist/resources/extensions/gsd/commands-workflow-templates.ts +0 -544
  52. package/dist/resources/extensions/gsd/prompts/workflow-start.md +0 -28
  53. package/dist/resources/extensions/gsd/tests/workflow-templates.test.ts +0 -173
  54. package/dist/resources/extensions/gsd/workflow-templates/bugfix.md +0 -87
  55. package/dist/resources/extensions/gsd/workflow-templates/dep-upgrade.md +0 -74
  56. package/dist/resources/extensions/gsd/workflow-templates/full-project.md +0 -41
  57. package/dist/resources/extensions/gsd/workflow-templates/hotfix.md +0 -45
  58. package/dist/resources/extensions/gsd/workflow-templates/refactor.md +0 -83
  59. package/dist/resources/extensions/gsd/workflow-templates/registry.json +0 -85
  60. package/dist/resources/extensions/gsd/workflow-templates/security-audit.md +0 -73
  61. package/dist/resources/extensions/gsd/workflow-templates/small-feature.md +0 -81
  62. package/dist/resources/extensions/gsd/workflow-templates/spike.md +0 -69
  63. package/dist/resources/extensions/gsd/workflow-templates.ts +0 -241
  64. package/dist/resources/extensions/mcp-client/index.ts +0 -459
  65. package/dist/resources/extensions/remote-questions/http-client.ts +0 -76
  66. package/src/resources/extensions/gsd/commands-workflow-templates.ts +0 -544
  67. package/src/resources/extensions/gsd/prompts/workflow-start.md +0 -28
  68. package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +0 -173
  69. package/src/resources/extensions/gsd/workflow-templates/bugfix.md +0 -87
  70. package/src/resources/extensions/gsd/workflow-templates/dep-upgrade.md +0 -74
  71. package/src/resources/extensions/gsd/workflow-templates/full-project.md +0 -41
  72. package/src/resources/extensions/gsd/workflow-templates/hotfix.md +0 -45
  73. package/src/resources/extensions/gsd/workflow-templates/refactor.md +0 -83
  74. package/src/resources/extensions/gsd/workflow-templates/registry.json +0 -85
  75. package/src/resources/extensions/gsd/workflow-templates/security-audit.md +0 -73
  76. package/src/resources/extensions/gsd/workflow-templates/small-feature.md +0 -81
  77. package/src/resources/extensions/gsd/workflow-templates/spike.md +0 -69
  78. package/src/resources/extensions/gsd/workflow-templates.ts +0 -241
  79. package/src/resources/extensions/mcp-client/index.ts +0 -459
  80. package/src/resources/extensions/remote-questions/http-client.ts +0 -76
@@ -204,13 +204,6 @@ export function estimateTimeRemaining(): string | null {
204
204
 
205
205
  // ─── Slice Progress Cache ─────────────────────────────────────────────────────
206
206
 
207
- /** Cached task detail for the widget task checklist */
208
- interface CachedTaskDetail {
209
- id: string;
210
- title: string;
211
- done: boolean;
212
- }
213
-
214
207
  /** Cached slice progress for the widget — avoid async in render */
215
208
  let cachedSliceProgress: {
216
209
  done: number;
@@ -218,8 +211,6 @@ let cachedSliceProgress: {
218
211
  milestoneId: string;
219
212
  /** Real task progress for the active slice, if its plan file exists */
220
213
  activeSliceTasks: { done: number; total: number } | null;
221
- /** Full task list for the active slice checklist */
222
- taskDetails: CachedTaskDetail[] | null;
223
214
  } | null = null;
224
215
 
225
216
  export function updateSliceProgressCache(base: string, mid: string, activeSid?: string): void {
@@ -230,7 +221,6 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
230
221
  const roadmap = parseRoadmap(content);
231
222
 
232
223
  let activeSliceTasks: { done: number; total: number } | null = null;
233
- let taskDetails: CachedTaskDetail[] | null = null;
234
224
  if (activeSid) {
235
225
  try {
236
226
  const planFile = resolveSliceFile(base, mid, activeSid, "PLAN");
@@ -241,7 +231,6 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
241
231
  done: plan.tasks.filter(t => t.done).length,
242
232
  total: plan.tasks.length,
243
233
  };
244
- taskDetails = plan.tasks.map(t => ({ id: t.id, title: t.title, done: t.done }));
245
234
  }
246
235
  } catch {
247
236
  // Non-fatal — just omit task count
@@ -253,19 +242,13 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
253
242
  total: roadmap.slices.length,
254
243
  milestoneId: mid,
255
244
  activeSliceTasks,
256
- taskDetails,
257
245
  };
258
246
  } catch {
259
247
  // Non-fatal — widget just won't show progress bar
260
248
  }
261
249
  }
262
250
 
263
- export function getRoadmapSlicesSync(): {
264
- done: number;
265
- total: number;
266
- activeSliceTasks: { done: number; total: number } | null;
267
- taskDetails: CachedTaskDetail[] | null;
268
- } | null {
251
+ export function getRoadmapSlicesSync(): { done: number; total: number; activeSliceTasks: { done: number; total: number } | null } | null {
269
252
  return cachedSliceProgress;
270
253
  }
271
254
 
@@ -366,84 +349,87 @@ export function updateProgressWidget(
366
349
  const lines: string[] = [];
367
350
  const pad = INDENT.base;
368
351
 
369
- // ── Top bar ─────────────────────────────────────────────────────
352
+ // ── Line 1: Top bar ───────────────────────────────────────────────
370
353
  lines.push(...ui.bar());
371
354
 
372
- // ── Header: GSD AUTO ... elapsed ────────────────────────────────
373
355
  const dot = pulseBright
374
356
  ? theme.fg("accent", GLYPH.statusActive)
375
357
  : theme.fg("dim", GLYPH.statusPending);
376
358
  const elapsed = formatAutoElapsed(accessors.getAutoStartTime());
377
359
  const modeTag = accessors.isStepMode() ? "NEXT" : "AUTO";
378
- const headerLeft = `${pad}${dot} ${theme.fg("accent", theme.bold("GSD"))} ${theme.fg("success", modeTag)}`;
360
+ const headerLeft = `${pad}${dot} ${theme.fg("accent", theme.bold("GSD"))} ${theme.fg("success", modeTag)}`;
379
361
  const headerRight = elapsed ? theme.fg("dim", elapsed) : "";
380
362
  lines.push(rightAlign(headerLeft, headerRight, width));
381
363
 
382
- // ── Context: project · slice · action (merged into one line) ────
383
- const contextParts: string[] = [];
384
- if (mid) contextParts.push(theme.fg("dim", mid.title));
364
+ lines.push("");
365
+
366
+ if (mid) {
367
+ lines.push(truncateToWidth(`${pad}${theme.fg("dim", mid.title)}`, width));
368
+ }
369
+
385
370
  if (slice && unitType !== "research-milestone" && unitType !== "plan-milestone") {
386
- contextParts.push(theme.fg("text", theme.bold(`${slice.id}: ${slice.title}`)));
371
+ lines.push(truncateToWidth(
372
+ `${pad}${theme.fg("text", theme.bold(`${slice.id}: ${slice.title}`))}`,
373
+ width,
374
+ ));
387
375
  }
376
+
377
+ lines.push("");
378
+
388
379
  const isHook = unitType.startsWith("hook/");
389
380
  const target = isHook
390
381
  ? (unitId.split("/").pop() ?? unitId)
391
382
  : (task ? `${task.id}: ${task.title}` : unitId);
392
- contextParts.push(`${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`);
393
-
383
+ const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
394
384
  const tierTag = tierBadge ? theme.fg("dim", `[${tierBadge}] `) : "";
395
385
  const phaseBadge = `${tierTag}${theme.fg("dim", phaseLabel)}`;
396
- const contextLine = contextParts.join(theme.fg("dim", " · "));
397
- lines.push(rightAlign(`${pad}${contextLine}`, phaseBadge, width));
398
-
399
- // ── Two-column body ─────────────────────────────────────────────
400
- // Left: progress, ETA, next, stats (fixed) | Right: task checklist (fixed, adjacent)
401
- // Both columns sit left-to-center; empty space is on the right.
402
- const divider = theme.fg("dim", "│");
403
- const minTwoColWidth = 100;
404
- const rightColFixed = 44;
405
- const colGap = 5; // breathing room between columns
406
- // Left column takes remaining space — no truncation on wide terminals
407
- const useTwoCol = width >= minTwoColWidth;
408
- const rightColWidth = useTwoCol ? rightColFixed : 0;
409
- const leftColWidth = useTwoCol ? width - rightColWidth - colGap : width;
410
-
411
- const roadmapSlices = mid ? getRoadmapSlicesSync() : null;
412
-
413
- // Build left column: progress bar, ETA, next step, token stats
414
- const leftLines: string[] = [];
415
-
416
- if (roadmapSlices) {
417
- const { done, total, activeSliceTasks } = roadmapSlices;
418
- const barWidth = Math.max(6, Math.min(18, Math.floor(leftColWidth * 0.4)));
419
- const pct = total > 0 ? done / total : 0;
420
- const filled = Math.round(pct * barWidth);
421
- const bar = theme.fg("success", "█".repeat(filled))
422
- + theme.fg("dim", "░".repeat(barWidth - filled));
423
-
424
- let meta = theme.fg("dim", `${done}/${total} slices`);
425
- if (activeSliceTasks && activeSliceTasks.total > 0) {
426
- const taskNum = isHook
427
- ? Math.max(activeSliceTasks.done, 1)
428
- : Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
429
- meta += theme.fg("dim", ` · task ${taskNum}/${activeSliceTasks.total}`);
430
- }
431
- leftLines.push(truncateToWidth(`${pad}${bar} ${meta}`, leftColWidth));
386
+ lines.push(rightAlign(actionLeft, phaseBadge, width));
387
+ lines.push("");
432
388
 
433
- const eta = estimateTimeRemaining();
434
- if (eta) {
435
- leftLines.push(truncateToWidth(`${pad}${theme.fg("dim", eta)}`, leftColWidth));
389
+ if (mid) {
390
+ const roadmapSlices = getRoadmapSlicesSync();
391
+ if (roadmapSlices) {
392
+ const { done, total, activeSliceTasks } = roadmapSlices;
393
+ const barWidth = Math.max(8, Math.min(24, Math.floor(width * 0.3)));
394
+ const pct = total > 0 ? done / total : 0;
395
+ const filled = Math.round(pct * barWidth);
396
+ const bar = theme.fg("success", "█".repeat(filled))
397
+ + theme.fg("dim", "░".repeat(barWidth - filled));
398
+
399
+ let meta = theme.fg("dim", `${done}/${total} slices`);
400
+
401
+ if (activeSliceTasks && activeSliceTasks.total > 0) {
402
+ // For hooks, show the trigger task number (done), not the next task (done + 1)
403
+ const taskNum = isHook
404
+ ? Math.max(activeSliceTasks.done, 1)
405
+ : Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
406
+ meta += theme.fg("dim", ` · task ${taskNum}/${activeSliceTasks.total}`);
407
+ }
408
+
409
+ // ETA estimate
410
+ const eta = estimateTimeRemaining();
411
+ if (eta) {
412
+ meta += theme.fg("dim", ` · ${eta}`);
413
+ }
414
+
415
+ lines.push(truncateToWidth(`${pad}${bar} ${meta}`, width));
436
416
  }
437
417
  }
438
418
 
419
+ lines.push("");
420
+
439
421
  if (next) {
440
- leftLines.push(truncateToWidth(
422
+ lines.push(truncateToWidth(
441
423
  `${pad}${theme.fg("dim", "→")} ${theme.fg("dim", `then ${next}`)}`,
442
- leftColWidth,
424
+ width,
443
425
  ));
444
426
  }
445
427
 
446
- // Token stats
428
+ // ── Footer info (pwd, tokens, cost, context, model) ──────────────
429
+ lines.push("");
430
+ lines.push(truncateToWidth(theme.fg("dim", `${pad}${widgetPwd}`), width, theme.fg("dim", "…")));
431
+
432
+ // Token stats from current unit session + cumulative cost from metrics
447
433
  {
448
434
  const cmdCtx = accessors.getCmdCtx();
449
435
  let totalInput = 0, totalOutput = 0;
@@ -478,6 +464,7 @@ export function updateProgressWidget(
478
464
  if (totalOutput) sp.push(`↓${formatWidgetTokens(totalOutput)}`);
479
465
  if (totalCacheRead) sp.push(`R${formatWidgetTokens(totalCacheRead)}`);
480
466
  if (totalCacheWrite) sp.push(`W${formatWidgetTokens(totalCacheWrite)}`);
467
+ // Cache hit rate for current unit
481
468
  if (totalCacheRead + totalInput > 0) {
482
469
  const hitRate = Math.round((totalCacheRead / (totalCacheRead + totalInput)) * 100);
483
470
  sp.push(`\u26A1${hitRate}%`);
@@ -496,134 +483,33 @@ export function updateProgressWidget(
496
483
  sp.push(cxDisplay);
497
484
  }
498
485
 
499
- const tokenLine = sp.map(p => p.includes("\x1b[") ? p : theme.fg("dim", p))
486
+ const sLeft = sp.map(p => p.includes("\x1b[") ? p : theme.fg("dim", p))
500
487
  .join(theme.fg("dim", " "));
501
- leftLines.push(truncateToWidth(`${pad}${tokenLine}`, leftColWidth));
502
488
 
503
489
  const modelId = cmdCtx?.model?.id ?? "";
504
490
  const modelProvider = cmdCtx?.model?.provider ?? "";
491
+ const modelPhase = phaseLabel ? theme.fg("dim", `[${phaseLabel}] `) : "";
505
492
  const modelDisplay = modelProvider && modelId
506
493
  ? `${modelProvider}/${modelId}`
507
494
  : modelId;
508
- if (modelDisplay) {
509
- leftLines.push(truncateToWidth(`${pad}${theme.fg("dim", modelDisplay)}`, leftColWidth));
510
- }
495
+ const sRight = modelDisplay
496
+ ? `${modelPhase}${theme.fg("dim", modelDisplay)}`
497
+ : "";
498
+ lines.push(rightAlign(`${pad}${sLeft}`, sRight, width));
511
499
 
512
- // Dynamic routing savings
500
+ // Dynamic routing savings summary
513
501
  if (mLedger && mLedger.units.some(u => u.tier)) {
514
502
  const savings = formatTierSavings(mLedger.units);
515
503
  if (savings) {
516
- leftLines.push(truncateToWidth(`${pad}${theme.fg("dim", savings)}`, leftColWidth));
517
- }
518
- }
519
- }
520
-
521
- // Build right column: task checklist (pegged to right edge)
522
- const rightLines: string[] = [];
523
- const taskDetails = roadmapSlices?.taskDetails ?? null;
524
- const maxVisibleTasks = 8;
525
- const rpad = " ";
526
-
527
- if (useTwoCol) {
528
- if (taskDetails && taskDetails.length > 0) {
529
- const visibleTasks = taskDetails.slice(0, maxVisibleTasks);
530
- for (const t of visibleTasks) {
531
- const isCurrent = task && t.id === task.id;
532
- const glyph = t.done
533
- ? theme.fg("success", GLYPH.statusDone)
534
- : isCurrent
535
- ? theme.fg("accent", "▸")
536
- : theme.fg("dim", " ");
537
- const label = isCurrent
538
- ? theme.fg("text", `${t.id}: ${t.title}`)
539
- : t.done
540
- ? theme.fg("dim", `${t.id}: ${t.title}`)
541
- : theme.fg("text", `${t.id}: ${t.title}`);
542
- rightLines.push(truncateToWidth(`${rpad}${glyph} ${label}`, rightColWidth));
543
- }
544
- if (taskDetails.length > maxVisibleTasks) {
545
- rightLines.push(truncateToWidth(
546
- `${rpad}${theme.fg("dim", ` …+${taskDetails.length - maxVisibleTasks} more`)}`,
547
- rightColWidth,
548
- ));
549
- }
550
- } else if (roadmapSlices?.activeSliceTasks) {
551
- const { done: tDone, total: tTotal } = roadmapSlices.activeSliceTasks;
552
- rightLines.push(`${rpad}${theme.fg("dim", `${tDone}/${tTotal} tasks`)}`);
553
- }
554
- } else {
555
- // Narrow single-column: task list goes into left column
556
- if (taskDetails && taskDetails.length > 0) {
557
- for (const t of taskDetails.slice(0, maxVisibleTasks)) {
558
- const isCurrent = task && t.id === task.id;
559
- const glyph = t.done
560
- ? theme.fg("success", GLYPH.statusDone)
561
- : isCurrent
562
- ? theme.fg("accent", "▸")
563
- : theme.fg("dim", " ");
564
- const label = isCurrent
565
- ? theme.fg("text", `${t.id}: ${t.title}`)
566
- : t.done
567
- ? theme.fg("dim", `${t.id}: ${t.title}`)
568
- : theme.fg("text", `${t.id}: ${t.title}`);
569
- leftLines.push(truncateToWidth(`${pad}${glyph} ${label}`, leftColWidth));
570
- }
571
- }
572
- // Add progress bar inline
573
- if (roadmapSlices) {
574
- const { done, total, activeSliceTasks } = roadmapSlices;
575
- const barWidth = Math.max(6, Math.min(18, Math.floor(leftColWidth * 0.4)));
576
- const pct = total > 0 ? done / total : 0;
577
- const filled = Math.round(pct * barWidth);
578
- const bar = theme.fg("success", "█".repeat(filled))
579
- + theme.fg("dim", "░".repeat(barWidth - filled));
580
- let meta = theme.fg("dim", `${done}/${total} slices`);
581
- if (activeSliceTasks && activeSliceTasks.total > 0) {
582
- const taskNum = isHook
583
- ? Math.max(activeSliceTasks.done, 1)
584
- : Math.min(activeSliceTasks.done + 1, activeSliceTasks.total);
585
- meta += theme.fg("dim", ` · task ${taskNum}/${activeSliceTasks.total}`);
504
+ lines.push(truncateToWidth(theme.fg("dim", `${pad}${savings}`), width));
586
505
  }
587
- const eta = estimateTimeRemaining();
588
- if (eta) meta += theme.fg("dim", ` · ${eta}`);
589
- leftLines.push(truncateToWidth(`${pad}${bar} ${meta}`, leftColWidth));
590
- }
591
- if (next) {
592
- leftLines.push(truncateToWidth(
593
- `${pad}${theme.fg("dim", "→")} ${theme.fg("dim", `then ${next}`)}`,
594
- leftColWidth,
595
- ));
596
506
  }
597
507
  }
598
508
 
599
- // Compose columns
600
- if (useTwoCol) {
601
- const maxRows = Math.max(leftLines.length, rightLines.length);
602
- if (maxRows > 0) {
603
- lines.push(""); // spacer before columns
604
- for (let i = 0; i < maxRows; i++) {
605
- const left = padToWidth(leftLines[i] ?? "", leftColWidth);
606
- const gap = " ".repeat(colGap - 2); // colGap minus divider and its trailing space
607
- const right = rightLines[i] ?? "";
608
- lines.push(truncateToWidth(`${left}${gap}${divider} ${right}`, width));
609
- }
610
- }
611
- } else {
612
- // Narrow single-column: just stack
613
- if (leftLines.length > 0) {
614
- lines.push("");
615
- for (const l of leftLines) lines.push(l);
616
- }
617
- }
618
-
619
- // ── Footer: pwd + hints ─────────────────────────────────────────
620
- lines.push("");
621
509
  const hintParts: string[] = [];
622
510
  hintParts.push("esc pause");
623
511
  hintParts.push(process.platform === "darwin" ? "⌃⌥G dashboard" : "Ctrl+Alt+G dashboard");
624
- const hintStr = theme.fg("dim", hintParts.join(" | "));
625
- const pwdStr = theme.fg("dim", widgetPwd);
626
- lines.push(rightAlign(`${pad}${pwdStr}`, hintStr, width));
512
+ lines.push(...ui.hints(hintParts));
627
513
 
628
514
  lines.push(...ui.bar());
629
515
 
@@ -711,10 +597,3 @@ function rightAlign(left: string, right: string, width: number): string {
711
597
  const gap = Math.max(1, width - leftVis - rightVis);
712
598
  return truncateToWidth(left + " ".repeat(gap) + right, width);
713
599
  }
714
-
715
- /** Pad a string with trailing spaces to fill exactly `colWidth` (ANSI-aware). */
716
- function padToWidth(s: string, colWidth: number): string {
717
- const vis = visibleWidth(s);
718
- if (vis >= colWidth) return truncateToWidth(s, colWidth);
719
- return s + " ".repeat(colWidth - vis);
720
- }
@@ -176,7 +176,7 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
176
176
  );
177
177
  try {
178
178
  const { formatDoctorIssuesForPrompt, formatDoctorReport } = await import("./doctor.js");
179
- const { dispatchDoctorHeal } = await import("./commands-handlers.js");
179
+ const { dispatchDoctorHeal } = await import("./commands.js");
180
180
  const actionable = report.issues.filter(i => i.severity === "error");
181
181
  const reportText = formatDoctorReport(report, { scope: doctorScope, includeWarnings: true });
182
182
  const structuredIssues = formatDoctorIssuesForPrompt(actionable);
@@ -202,13 +202,10 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
202
202
  }
203
203
  }
204
204
 
205
- // Prune dead bg-shell processes and kill non-persistent live ones.
206
- // Without killing live processes between units, dev servers spawned during
207
- // one task keep ports bound, causing conflicts in subsequent tasks (#1209).
205
+ // Prune dead bg-shell processes
208
206
  try {
209
- const { pruneDeadProcesses, killSessionProcesses } = await import("../bg-shell/process-manager.js");
207
+ const { pruneDeadProcesses } = await import("../bg-shell/process-manager.js");
210
208
  pruneDeadProcesses();
211
- killSessionProcesses();
212
209
  } catch {
213
210
  // Non-fatal
214
211
  }
@@ -39,7 +39,6 @@ import {
39
39
  import { isValidationTerminal } from "./state.js";
40
40
  import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
41
41
  import { atomicWriteSync } from "./atomic-write.js";
42
- import { loadJsonFileOrNull } from "./json-persistence.js";
43
42
  import { dirname, join } from "node:path";
44
43
 
45
44
  // ─── Artifact Resolution & Verification ───────────────────────────────────────
@@ -355,10 +354,6 @@ export function skipExecuteTask(
355
354
 
356
355
  // ─── Disk-backed completed-unit helpers ───────────────────────────────────────
357
356
 
358
- function isStringArray(data: unknown): data is string[] {
359
- return Array.isArray(data) && data.every(item => typeof item === "string");
360
- }
361
-
362
357
  /** Path to the persisted completed-unit keys file. */
363
358
  export function completedKeysPath(base: string): string {
364
359
  return join(base, ".gsd", "completed-units.json");
@@ -367,7 +362,12 @@ export function completedKeysPath(base: string): string {
367
362
  /** Write a completed unit key to disk (read-modify-write append to set). */
368
363
  export function persistCompletedKey(base: string, key: string): void {
369
364
  const file = completedKeysPath(base);
370
- const keys = loadJsonFileOrNull(file, isStringArray) ?? [];
365
+ let keys: string[] = [];
366
+ try {
367
+ if (existsSync(file)) {
368
+ keys = JSON.parse(readFileSync(file, "utf-8"));
369
+ }
370
+ } catch (e) { /* corrupt file — start fresh */ void e; }
371
371
  const keySet = new Set(keys);
372
372
  if (!keySet.has(key)) {
373
373
  keys.push(key);
@@ -378,21 +378,27 @@ export function persistCompletedKey(base: string, key: string): void {
378
378
  /** Remove a stale completed unit key from disk. */
379
379
  export function removePersistedKey(base: string, key: string): void {
380
380
  const file = completedKeysPath(base);
381
- const keys = loadJsonFileOrNull(file, isStringArray);
382
- if (!keys) return;
383
- const filtered = keys.filter(k => k !== key);
384
- if (filtered.length !== keys.length) {
385
- atomicWriteSync(file, JSON.stringify(filtered));
386
- }
381
+ try {
382
+ if (existsSync(file)) {
383
+ const keys: string[] = JSON.parse(readFileSync(file, "utf-8"));
384
+ const filtered = keys.filter(k => k !== key);
385
+ // Only write if the key was actually present
386
+ if (filtered.length !== keys.length) {
387
+ atomicWriteSync(file, JSON.stringify(filtered));
388
+ }
389
+ }
390
+ } catch (e) { /* non-fatal: removePersistedKey failure */ void e; }
387
391
  }
388
392
 
389
393
  /** Load all completed unit keys from disk into the in-memory set. */
390
394
  export function loadPersistedKeys(base: string, target: Set<string>): void {
391
395
  const file = completedKeysPath(base);
392
- const keys = loadJsonFileOrNull(file, isStringArray);
393
- if (keys) {
394
- for (const k of keys) target.add(k);
395
- }
396
+ try {
397
+ if (existsSync(file)) {
398
+ const keys: string[] = JSON.parse(readFileSync(file, "utf-8"));
399
+ for (const k of keys) target.add(k);
400
+ }
401
+ } catch (e) { /* non-fatal: loadPersistedKeys failure */ void e; }
396
402
  }
397
403
 
398
404
  // ─── Merge State Reconciliation ───────────────────────────────────────────────
@@ -11,7 +11,6 @@
11
11
  */
12
12
 
13
13
  import { existsSync, mkdirSync, readFileSync, cpSync, unlinkSync, readdirSync } from "node:fs";
14
- import { loadJsonFileOrNull } from "./json-persistence.js";
15
14
  import { join, sep as pathSep } from "node:path";
16
15
  import { homedir } from "node:os";
17
16
  import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
@@ -113,15 +112,15 @@ export function syncStateToProjectRoot(worktreePath: string, projectRoot: string
113
112
  * Uses gsdVersion instead of syncedAt so that launching a second session
114
113
  * doesn't falsely trigger staleness (#804).
115
114
  */
116
- function isManifestWithVersion(data: unknown): data is { gsdVersion: string } {
117
- return data !== null && typeof data === "object" && "gsdVersion" in data! && typeof (data as Record<string, unknown>).gsdVersion === "string";
118
- }
119
-
120
115
  export function readResourceVersion(): string | null {
121
116
  const agentDir = process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
122
117
  const manifestPath = join(agentDir, "managed-resources.json");
123
- const manifest = loadJsonFileOrNull(manifestPath, isManifestWithVersion);
124
- return manifest?.gsdVersion ?? null;
118
+ try {
119
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
120
+ return typeof manifest?.gsdVersion === "string" ? manifest.gsdVersion : null;
121
+ } catch {
122
+ return null;
123
+ }
125
124
  }
126
125
 
127
126
  /**
@@ -22,6 +22,7 @@ import { loadFile, getManifestStatus, resolveAllOverrides, parsePlan, parseSumma
22
22
  import { loadPrompt } from "./prompt-loader.js";
23
23
  import { runVerificationGate, formatFailureContext, captureRuntimeErrors, runDependencyAudit } from "./verification-gate.js";
24
24
  import { writeVerificationJSON } from "./verification-evidence.js";
25
+ export { inlinePriorMilestoneSummary } from "./files.js";
25
26
  import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
26
27
  import {
27
28
  gsdRoot, resolveMilestoneFile, resolveSliceFile, resolveSlicePath,
@@ -191,6 +192,12 @@ import {
191
192
  NEW_SESSION_TIMEOUT_MS, DISPATCH_HANG_TIMEOUT_MS,
192
193
  } from "./auto/session.js";
193
194
  import type { CompletedUnit, CurrentUnit, UnitRouting, StartModel, PendingVerificationRetry } from "./auto/session.js";
195
+ export {
196
+ MAX_UNIT_DISPATCHES, STUB_RECOVERY_THRESHOLD, MAX_LIFETIME_DISPATCHES,
197
+ MAX_CONSECUTIVE_SKIPS, DISPATCH_GAP_TIMEOUT_MS, MAX_SKIP_DEPTH,
198
+ NEW_SESSION_TIMEOUT_MS, DISPATCH_HANG_TIMEOUT_MS,
199
+ } from "./auto/session.js";
200
+ export type { CompletedUnit, CurrentUnit, UnitRouting, StartModel } from "./auto/session.js";
194
201
 
195
202
  // ── ENCAPSULATION INVARIANT ─────────────────────────────────────────────────
196
203
  // ALL mutable auto-mode state lives in the AutoSession class (auto/session.ts).
@@ -261,6 +268,8 @@ export function shouldUseWorktreeIsolation(): boolean {
261
268
  * Maps toolCallId → start timestamp (ms) so the idle watchdog can detect tools that have been
262
269
  * running suspiciously long (e.g., a Bash command hung because `&` kept stdout open).
263
270
  */
271
+ // Re-export budget utilities for external consumers
272
+ export { getBudgetAlertLevel, getNewBudgetAlertLevel, getBudgetEnforcementAction } from "./auto-budget.js";
264
273
 
265
274
  /** Wrapper: register SIGTERM handler and store reference. */
266
275
  function registerSigtermHandler(currentBasePath: string): void {
@@ -273,6 +282,8 @@ function deregisterSigtermHandler(): void {
273
282
  s.sigtermHandler = null;
274
283
  }
275
284
 
285
+ export { type AutoDashboardData } from "./auto-dashboard.js";
286
+
276
287
  export function getAutoDashboardData(): AutoDashboardData {
277
288
  const ledger = getLedger();
278
289
  const totals = ledger ? getProjectTotals(ledger.units) : null;
@@ -923,6 +934,8 @@ async function showStepWizard(
923
934
  }
924
935
  }
925
936
 
937
+ // describeNextUnit is imported from auto-dashboard.ts and re-exported
938
+ export { describeNextUnit } from "./auto-dashboard.js";
926
939
 
927
940
  /** Thin wrapper: delegates to auto-dashboard.ts, passing state accessors. */
928
941
  function updateProgressWidget(
@@ -1892,3 +1905,5 @@ export async function dispatchHookUnit(
1892
1905
  }
1893
1906
 
1894
1907
 
1908
+ // Direct phase dispatch → auto-direct-dispatch.ts
1909
+ export { dispatchDirectPhase } from "./auto-direct-dispatch.js";
@@ -19,26 +19,7 @@ import {
19
19
  filterDoctorIssues,
20
20
  } from "./doctor.js";
21
21
  import { isAutoActive } from "./auto.js";
22
- import { projectRoot } from "./commands.js";
23
- import { loadPrompt } from "./prompt-loader.js";
24
-
25
- export function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportText: string, structuredIssues: string): void {
26
- const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".pi", "GSD-WORKFLOW.md");
27
- const workflow = readFileSync(workflowPath, "utf-8");
28
- const prompt = loadPrompt("doctor-heal", {
29
- doctorSummary: reportText,
30
- structuredIssues,
31
- scopeLabel: scope ?? "active milestone / blocking scope",
32
- doctorCommandSuffix: scope ? ` ${scope}` : "",
33
- });
34
-
35
- const content = `Read the following GSD workflow protocol and execute exactly.\n\n${workflow}\n\n## Your Task\n\n${prompt}`;
36
-
37
- pi.sendMessage(
38
- { customType: "gsd-doctor-heal", content, display: false },
39
- { triggerTurn: true },
40
- );
41
- }
22
+ import { projectRoot, dispatchDoctorHeal } from "./commands.js";
42
23
 
43
24
  export async function handleDoctor(args: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
44
25
  const trimmed = args.trim();
@@ -14,7 +14,6 @@ import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
14
14
  import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
15
15
  import { join } from "node:path";
16
16
  import { gsdRoot } from "./paths.js";
17
- import { loadJsonFileOrNull } from "./json-persistence.js";
18
17
 
19
18
  // ─── Types ──────────────────────────────────────────────────────────────────
20
19
 
@@ -332,18 +331,20 @@ async function handleLogsList(basePath: string, ctx: ExtensionCommandContext): P
332
331
 
333
332
  // Metrics summary
334
333
  const metricsPath = join(gsdRoot(basePath), "metrics.json");
335
- const isMetrics = (d: unknown): d is { units: Array<Record<string, unknown>> } =>
336
- d !== null && typeof d === "object" && "units" in d! && Array.isArray((d as Record<string, unknown>).units);
337
- const metrics = loadJsonFileOrNull(metricsPath, isMetrics);
338
- if (metrics && metrics.units.length > 0) {
339
- const units = metrics.units;
340
- const totalCost = units.reduce((sum: number, u) => sum + ((u.cost as number) ?? 0), 0);
341
- const totalTokens = units.reduce((sum: number, u) => {
342
- const t = u.tokens as Record<string, number> | undefined;
343
- return sum + (t?.total ?? 0);
344
- }, 0);
345
- lines.push("");
346
- lines.push(`Metrics: ${units.length} units tracked · $${totalCost.toFixed(2)} · ${(totalTokens / 1000).toFixed(0)}K tokens`);
334
+ if (existsSync(metricsPath)) {
335
+ try {
336
+ const metrics = JSON.parse(readFileSync(metricsPath, "utf-8"));
337
+ const units = metrics?.units;
338
+ if (Array.isArray(units) && units.length > 0) {
339
+ const totalCost = units.reduce((sum: number, u: Record<string, unknown>) => sum + ((u.cost as number) ?? 0), 0);
340
+ const totalTokens = units.reduce((sum: number, u: Record<string, unknown>) => {
341
+ const t = u.tokens as Record<string, number> | undefined;
342
+ return sum + (t?.total ?? 0);
343
+ }, 0);
344
+ lines.push("");
345
+ lines.push(`Metrics: ${units.length} units tracked · $${totalCost.toFixed(2)} · ${(totalTokens / 1000).toFixed(0)}K tokens`);
346
+ }
347
+ } catch { /* ignore */ }
347
348
  }
348
349
 
349
350
  lines.push("");