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.
- package/README.md +17 -24
- package/dist/resources/extensions/bg-shell/process-manager.ts +0 -13
- package/dist/resources/extensions/gsd/auto-dashboard.ts +65 -186
- package/dist/resources/extensions/gsd/auto-post-unit.ts +3 -6
- package/dist/resources/extensions/gsd/auto-recovery.ts +22 -16
- package/dist/resources/extensions/gsd/auto-worktree-sync.ts +6 -7
- package/dist/resources/extensions/gsd/auto.ts +15 -0
- package/dist/resources/extensions/gsd/commands-handlers.ts +1 -20
- package/dist/resources/extensions/gsd/commands-logs.ts +14 -13
- package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +14 -44
- package/dist/resources/extensions/gsd/commands.ts +22 -55
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +1 -2
- package/dist/resources/extensions/gsd/json-persistence.ts +1 -16
- package/dist/resources/extensions/gsd/queue-order.ts +11 -10
- package/dist/resources/extensions/gsd/session-status-io.ts +41 -23
- package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/extension-selector-separator.test.ts +38 -60
- package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
- package/dist/resources/extensions/mcporter/index.ts +525 -0
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +19 -8
- package/dist/resources/extensions/remote-questions/slack-adapter.ts +17 -11
- package/dist/resources/extensions/remote-questions/telegram-adapter.ts +19 -8
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +0 -13
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +0 -13
- package/src/resources/extensions/bg-shell/process-manager.ts +0 -13
- package/src/resources/extensions/gsd/auto-dashboard.ts +65 -186
- package/src/resources/extensions/gsd/auto-post-unit.ts +3 -6
- package/src/resources/extensions/gsd/auto-recovery.ts +22 -16
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +6 -7
- package/src/resources/extensions/gsd/auto.ts +15 -0
- package/src/resources/extensions/gsd/commands-handlers.ts +1 -20
- package/src/resources/extensions/gsd/commands-logs.ts +14 -13
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +14 -44
- package/src/resources/extensions/gsd/commands.ts +22 -55
- package/src/resources/extensions/gsd/dashboard-overlay.ts +1 -2
- package/src/resources/extensions/gsd/json-persistence.ts +1 -16
- package/src/resources/extensions/gsd/queue-order.ts +11 -10
- package/src/resources/extensions/gsd/session-status-io.ts +41 -23
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/extension-selector-separator.test.ts +38 -60
- package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
- package/src/resources/extensions/mcporter/index.ts +525 -0
- package/src/resources/extensions/remote-questions/discord-adapter.ts +19 -8
- package/src/resources/extensions/remote-questions/slack-adapter.ts +17 -11
- package/src/resources/extensions/remote-questions/telegram-adapter.ts +19 -8
- package/dist/resources/extensions/gsd/commands-workflow-templates.ts +0 -544
- package/dist/resources/extensions/gsd/prompts/workflow-start.md +0 -28
- package/dist/resources/extensions/gsd/tests/workflow-templates.test.ts +0 -173
- package/dist/resources/extensions/gsd/workflow-templates/bugfix.md +0 -87
- package/dist/resources/extensions/gsd/workflow-templates/dep-upgrade.md +0 -74
- package/dist/resources/extensions/gsd/workflow-templates/full-project.md +0 -41
- package/dist/resources/extensions/gsd/workflow-templates/hotfix.md +0 -45
- package/dist/resources/extensions/gsd/workflow-templates/refactor.md +0 -83
- package/dist/resources/extensions/gsd/workflow-templates/registry.json +0 -85
- package/dist/resources/extensions/gsd/workflow-templates/security-audit.md +0 -73
- package/dist/resources/extensions/gsd/workflow-templates/small-feature.md +0 -81
- package/dist/resources/extensions/gsd/workflow-templates/spike.md +0 -69
- package/dist/resources/extensions/gsd/workflow-templates.ts +0 -241
- package/dist/resources/extensions/mcp-client/index.ts +0 -459
- package/dist/resources/extensions/remote-questions/http-client.ts +0 -76
- package/src/resources/extensions/gsd/commands-workflow-templates.ts +0 -544
- package/src/resources/extensions/gsd/prompts/workflow-start.md +0 -28
- package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +0 -173
- package/src/resources/extensions/gsd/workflow-templates/bugfix.md +0 -87
- package/src/resources/extensions/gsd/workflow-templates/dep-upgrade.md +0 -74
- package/src/resources/extensions/gsd/workflow-templates/full-project.md +0 -41
- package/src/resources/extensions/gsd/workflow-templates/hotfix.md +0 -45
- package/src/resources/extensions/gsd/workflow-templates/refactor.md +0 -83
- package/src/resources/extensions/gsd/workflow-templates/registry.json +0 -85
- package/src/resources/extensions/gsd/workflow-templates/security-audit.md +0 -73
- package/src/resources/extensions/gsd/workflow-templates/small-feature.md +0 -81
- package/src/resources/extensions/gsd/workflow-templates/spike.md +0 -69
- package/src/resources/extensions/gsd/workflow-templates.ts +0 -241
- package/src/resources/extensions/mcp-client/index.ts +0 -459
- 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.
|
|
28
|
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
- **
|
|
34
|
-
- **
|
|
35
|
-
- **
|
|
36
|
-
- **
|
|
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
|
-
- **
|
|
39
|
-
- **
|
|
40
|
-
- **
|
|
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.
|
|
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
|
|
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
|
-
| **
|
|
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** ≥
|
|
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"))}
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
if (mid)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
397
|
-
lines.push(
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
422
|
+
lines.push(truncateToWidth(
|
|
441
423
|
`${pad}${theme.fg("dim", "→")} ${theme.fg("dim", `then ${next}`)}`,
|
|
442
|
-
|
|
424
|
+
width,
|
|
443
425
|
));
|
|
444
426
|
}
|
|
445
427
|
|
|
446
|
-
//
|
|
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
|
|
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
|
-
|
|
509
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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
|
/**
|