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
package/README.md CHANGED
@@ -24,21 +24,20 @@ One command. Walk away. Come back to a built project with clean git history.
24
24
 
25
25
  ---
26
26
 
27
- ## What's New in v2.29
28
-
29
- - **Node.js 24 LTS**CI, Docker, and package config all upgraded to Node 24 (Krypton)
30
- - **`searchExcludeDirs` setting**blacklist directories from `@` file autocomplete (e.g., `node_modules`, `dist`)
31
- - **Automated releases**prod-release now auto-generates changelogs, bumps versions, and publishes to npm
32
- - **`/gsd logs`**browse activity, debug, and metrics logs from within a session
33
- - **Configurable screenshots** browser-tools now support custom resolution, format, and quality
34
- - **Pre-commit secret scanning** automatic detection of hardcoded secrets in CI and locally
35
- - **Per-project MCP config** — `.gsd/mcp.json` for project-scoped MCP server definitions
36
- - **API request metrics** — track request counts for Copilot/subscription users
27
+ ## What's New in v2.28
28
+
29
+ - **`gsd headless query`**instant JSON snapshot of project state (~50ms, no LLM session)
30
+ - **`/gsd update`**update GSD to the latest version without leaving your session
31
+ - **`/gsd export --html --all`** generate retrospective HTML reports for all milestones at once
32
+ - **Reliability hardening**atomic file writes, OAuth fetch timeouts, RPC exit detection, blob GC, LSP init retry with backoff
33
+ - **RPC utilities** now part of the public API for headless/scripted integrations
34
+ - **npm** established as the canonical package manager
35
+ - **CI/CD Pipeline** — three-stage promotion (Dev Test → Prod) with automated versioning
36
+ - **Docker support** — containerized builds with multi-stage Dockerfile
37
37
  - **`/gsd keys`** — full API key lifecycle management (list, add, remove, test, rotate, doctor)
38
- - **Advisory verification gate** — auto-discovered checks (lint/test from package.json) no longer doom-loop on pre-existing errors
39
- - **Worktree living doc sync** — DECISIONS, REQUIREMENTS, PROJECT, and KNOWLEDGE now sync between worktree and project root
40
- - **Windows non-ASCII path support** — `cpSync` fallback for usernames with special characters
41
- - **`needs-discussion` routing** — milestones with draft context now route to the interactive discussion flow instead of stopping
38
+ - **Milestone parking** — park in-progress milestones to work on something else, unpark to resume
39
+ - **Studio** — experimental Electron desktop app (early preview)
40
+ - **Per-project MCP config** — `.gsd/mcp.json` for project-scoped MCP server definitions
42
41
 
43
42
  See the full [Changelog](./CHANGELOG.md) for details.
44
43
 
@@ -63,8 +62,6 @@ Full documentation is available in the [`docs/`](./docs/) directory:
63
62
  - **[CI/CD Pipeline](./docs/ci-cd-pipeline.md)** — three-stage promotion pipeline (Dev → Test → Prod)
64
63
  - **[VS Code Extension](./vscode-extension/README.md)** — chat participant, sidebar dashboard, RPC integration
65
64
  - **[Visualizer](./docs/visualizer.md)** — workflow visualizer with stats and discussion status
66
- - **[Remote Questions](./docs/remote-questions.md)** — route decisions to Slack or Discord when human input is needed
67
- - **[Dynamic Model Routing](./docs/dynamic-model-routing.md)** — complexity-based model selection and budget pressure
68
65
  - **[Migration from v1](./docs/migration.md)** — `.planning` → `.gsd` migration
69
66
 
70
67
  ---
@@ -178,7 +175,7 @@ Auto mode is a state machine driven by files on disk. It reads `.gsd/STATE.md`,
178
175
 
179
176
  9. **Adaptive replanning** — After each slice completes, the roadmap is reassessed. If the work revealed new information that changes the plan, slices are reordered, added, or removed before continuing.
180
177
 
181
- 10. **Verification enforcement** — Configure shell commands (`npm run lint`, `npm run test`, etc.) that run automatically after task execution. Failures trigger auto-fix retries before advancing. Auto-discovered checks from `package.json` run in advisory mode — they log warnings but don't block on pre-existing errors. Configurable via `verification_commands`, `verification_auto_fix`, and `verification_max_retries` preferences.
178
+ 10. **Verification enforcement** — Configure shell commands (`npm run lint`, `npm run test`, etc.) that run automatically after task execution. Failures trigger auto-fix retries before advancing. Configurable via `verification_commands`, `verification_auto_fix`, and `verification_max_retries` preferences.
182
179
 
183
180
  11. **Milestone validation** — After all slices complete, a `validate-milestone` gate compares roadmap success criteria against actual results before sealing the milestone.
184
181
 
@@ -310,7 +307,6 @@ On first run, GSD launches a branded setup wizard that walks you through LLM pro
310
307
  | `/gsd cleanup` | Archive phase directories from completed milestones |
311
308
  | `/gsd doctor` | Runtime health checks with auto-fix for common issues |
312
309
  | `/gsd keys` | API key manager — list, add, remove, test, rotate, doctor |
313
- | `/gsd logs` | Browse activity, debug, and metrics logs |
314
310
  | `/gsd export --html` | Generate HTML report for current or completed milestone |
315
311
  | `/worktree` (`/wt`) | Git worktree lifecycle — create, switch, merge, remove |
316
312
  | `/voice` | Toggle real-time speech-to-text (macOS, Linux) |
@@ -451,7 +447,6 @@ auto_report: true
451
447
  | `verification_max_retries` | Max retries for verification failures (default: 2) |
452
448
  | `require_slice_discussion` | Pause auto-mode before each slice for human discussion review |
453
449
  | `auto_report` | Auto-generate HTML reports after milestone completion (default: true) |
454
- | `searchExcludeDirs` | Directories to exclude from `@` file autocomplete (e.g., `["node_modules", ".git", "dist"]`) |
455
450
 
456
451
  ### Agent Instructions
457
452
 
@@ -483,7 +478,7 @@ See the full [Token Optimization Guide](./docs/token-optimization.md) for detail
483
478
 
484
479
  ### Bundled Tools
485
480
 
486
- GSD ships with 16 extensions, all loaded automatically:
481
+ GSD ships with 14 extensions, all loaded automatically:
487
482
 
488
483
  | Extension | What it provides |
489
484
  | ---------------------- | ---------------------------------------------------------------------------------------------------------------------- |
@@ -495,14 +490,12 @@ GSD ships with 16 extensions, all loaded automatically:
495
490
  | **Background Shell** | Long-running process management with readiness detection |
496
491
  | **Subagent** | Delegated tasks with isolated context windows |
497
492
  | **Mac Tools** | macOS native app automation via Accessibility APIs |
498
- | **MCP Client** | Native MCP server integration via @modelcontextprotocol/sdk |
493
+ | **MCPorter** | Lazy on-demand MCP server integration |
499
494
  | **Voice** | Real-time speech-to-text transcription (macOS, Linux — Ubuntu 22.04+) |
500
495
  | **Slash Commands** | Custom command creation |
501
496
  | **LSP** | Language Server Protocol integration — diagnostics, go-to-definition, references, hover, symbols, rename, code actions |
502
497
  | **Ask User Questions** | Structured user input with single/multi-select |
503
498
  | **Secure Env Collect** | Masked secret collection without manual .env editing |
504
- | **Remote Questions** | Route decisions to Slack/Discord when human input is needed in headless/CI mode |
505
- | **Universal Config** | Discover and import MCP servers and rules from other AI coding tools |
506
499
 
507
500
  ### Bundled Agents
508
501
 
@@ -604,7 +597,7 @@ gsd (CLI binary)
604
597
 
605
598
  ## Requirements
606
599
 
607
- - **Node.js** ≥ 22.0.0 (24 LTS recommended)
600
+ - **Node.js** ≥ 20.6.0 (22+ recommended)
608
601
  - **An LLM provider** — any of the 20+ supported providers (see [Use Any Model](#use-any-model))
609
602
  - **Git** — initialized automatically if missing
610
603
 
@@ -375,19 +375,6 @@ export function cleanupAll(): void {
375
375
  processes.clear();
376
376
  }
377
377
 
378
- /**
379
- * Kill all alive, non-persistent bg processes.
380
- * Called between auto-mode units to prevent orphaned servers from
381
- * keeping ports bound across task boundaries (#1209).
382
- */
383
- export function killSessionProcesses(): void {
384
- for (const [id, bg] of processes) {
385
- if (bg.alive && !bg.persistAcrossSessions) {
386
- killProcess(id, "SIGTERM");
387
- }
388
- }
389
- }
390
-
391
378
  async function waitForProcessExit(bg: BgProcess, timeoutMs: number): Promise<boolean> {
392
379
  if (!bg.alive) return true;
393
380
  await new Promise<void>((resolve) => {
@@ -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
  /**