gsd-pi 2.31.2 → 2.32.0-dev.3d7932c

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 (138) hide show
  1. package/README.md +27 -20
  2. package/dist/cli.js +5 -5
  3. package/dist/resource-loader.js +13 -3
  4. package/dist/resources/extensions/gsd/auto-constants.ts +6 -0
  5. package/dist/resources/extensions/gsd/auto-dashboard.ts +23 -27
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
  7. package/dist/resources/extensions/gsd/auto-dispatch.ts +4 -8
  8. package/dist/resources/extensions/gsd/auto-idempotency.ts +3 -2
  9. package/dist/resources/extensions/gsd/auto-observability.ts +2 -4
  10. package/dist/resources/extensions/gsd/auto-post-unit.ts +32 -37
  11. package/dist/resources/extensions/gsd/auto-prompts.ts +84 -78
  12. package/dist/resources/extensions/gsd/auto-recovery.ts +8 -22
  13. package/dist/resources/extensions/gsd/auto-start.ts +16 -12
  14. package/dist/resources/extensions/gsd/auto-stuck-detection.ts +3 -2
  15. package/dist/resources/extensions/gsd/auto-timeout-recovery.ts +2 -1
  16. package/dist/resources/extensions/gsd/auto-timers.ts +3 -2
  17. package/dist/resources/extensions/gsd/auto-verification.ts +6 -6
  18. package/dist/resources/extensions/gsd/auto-worktree.ts +5 -4
  19. package/dist/resources/extensions/gsd/auto.ts +82 -60
  20. package/dist/resources/extensions/gsd/commands-inspect.ts +2 -1
  21. package/dist/resources/extensions/gsd/commands-workflow-templates.ts +5 -6
  22. package/dist/resources/extensions/gsd/commands.ts +19 -0
  23. package/dist/resources/extensions/gsd/complexity-classifier.ts +5 -7
  24. package/dist/resources/extensions/gsd/crash-recovery.ts +15 -2
  25. package/dist/resources/extensions/gsd/dashboard-overlay.ts +28 -0
  26. package/dist/resources/extensions/gsd/dispatch-guard.ts +2 -1
  27. package/dist/resources/extensions/gsd/doctor-environment.ts +497 -0
  28. package/dist/resources/extensions/gsd/doctor-providers.ts +343 -0
  29. package/dist/resources/extensions/gsd/doctor-types.ts +14 -1
  30. package/dist/resources/extensions/gsd/doctor.ts +6 -0
  31. package/dist/resources/extensions/gsd/error-utils.ts +6 -0
  32. package/dist/resources/extensions/gsd/export.ts +2 -1
  33. package/dist/resources/extensions/gsd/git-service.ts +12 -2
  34. package/dist/resources/extensions/gsd/guided-flow-queue.ts +1 -8
  35. package/dist/resources/extensions/gsd/guided-flow.ts +3 -2
  36. package/dist/resources/extensions/gsd/health-widget.ts +167 -0
  37. package/dist/resources/extensions/gsd/index.ts +18 -5
  38. package/dist/resources/extensions/gsd/key-manager.ts +2 -1
  39. package/dist/resources/extensions/gsd/marketplace-discovery.ts +4 -3
  40. package/dist/resources/extensions/gsd/metrics.ts +3 -3
  41. package/dist/resources/extensions/gsd/migrate-external.ts +21 -4
  42. package/dist/resources/extensions/gsd/milestone-ids.ts +2 -1
  43. package/dist/resources/extensions/gsd/native-git-bridge.ts +2 -1
  44. package/dist/resources/extensions/gsd/parallel-merge.ts +2 -1
  45. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +2 -1
  46. package/dist/resources/extensions/gsd/post-unit-hooks.ts +8 -9
  47. package/dist/resources/extensions/gsd/preferences-types.ts +8 -0
  48. package/dist/resources/extensions/gsd/preferences-validation.ts +3 -10
  49. package/dist/resources/extensions/gsd/progress-score.ts +273 -0
  50. package/dist/resources/extensions/gsd/prompts/run-uat.md +1 -42
  51. package/dist/resources/extensions/gsd/quick.ts +61 -8
  52. package/dist/resources/extensions/gsd/repo-identity.ts +22 -1
  53. package/dist/resources/extensions/gsd/session-lock.ts +12 -1
  54. package/dist/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
  55. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  56. package/dist/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
  57. package/dist/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
  58. package/dist/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
  59. package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
  60. package/dist/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
  61. package/dist/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
  62. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
  63. package/dist/resources/extensions/gsd/undo.ts +5 -7
  64. package/dist/resources/extensions/gsd/unit-id.ts +14 -0
  65. package/dist/resources/extensions/gsd/unit-runtime.ts +2 -1
  66. package/dist/resources/extensions/gsd/visualizer-data.ts +60 -2
  67. package/dist/resources/extensions/gsd/visualizer-views.ts +54 -0
  68. package/dist/resources/extensions/gsd/worktree-command.ts +8 -7
  69. package/dist/worktree-cli.d.ts +42 -6
  70. package/dist/worktree-cli.js +88 -48
  71. package/package.json +1 -1
  72. package/packages/pi-coding-agent/package.json +1 -1
  73. package/pkg/package.json +1 -1
  74. package/src/resources/extensions/gsd/auto-constants.ts +6 -0
  75. package/src/resources/extensions/gsd/auto-dashboard.ts +23 -27
  76. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
  77. package/src/resources/extensions/gsd/auto-dispatch.ts +4 -8
  78. package/src/resources/extensions/gsd/auto-idempotency.ts +3 -2
  79. package/src/resources/extensions/gsd/auto-observability.ts +2 -4
  80. package/src/resources/extensions/gsd/auto-post-unit.ts +32 -37
  81. package/src/resources/extensions/gsd/auto-prompts.ts +84 -78
  82. package/src/resources/extensions/gsd/auto-recovery.ts +8 -22
  83. package/src/resources/extensions/gsd/auto-start.ts +16 -12
  84. package/src/resources/extensions/gsd/auto-stuck-detection.ts +3 -2
  85. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -1
  86. package/src/resources/extensions/gsd/auto-timers.ts +3 -2
  87. package/src/resources/extensions/gsd/auto-verification.ts +6 -6
  88. package/src/resources/extensions/gsd/auto-worktree.ts +5 -4
  89. package/src/resources/extensions/gsd/auto.ts +82 -60
  90. package/src/resources/extensions/gsd/commands-inspect.ts +2 -1
  91. package/src/resources/extensions/gsd/commands-workflow-templates.ts +5 -6
  92. package/src/resources/extensions/gsd/commands.ts +19 -0
  93. package/src/resources/extensions/gsd/complexity-classifier.ts +5 -7
  94. package/src/resources/extensions/gsd/crash-recovery.ts +15 -2
  95. package/src/resources/extensions/gsd/dashboard-overlay.ts +28 -0
  96. package/src/resources/extensions/gsd/dispatch-guard.ts +2 -1
  97. package/src/resources/extensions/gsd/doctor-environment.ts +497 -0
  98. package/src/resources/extensions/gsd/doctor-providers.ts +343 -0
  99. package/src/resources/extensions/gsd/doctor-types.ts +14 -1
  100. package/src/resources/extensions/gsd/doctor.ts +6 -0
  101. package/src/resources/extensions/gsd/error-utils.ts +6 -0
  102. package/src/resources/extensions/gsd/export.ts +2 -1
  103. package/src/resources/extensions/gsd/git-service.ts +12 -2
  104. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -8
  105. package/src/resources/extensions/gsd/guided-flow.ts +3 -2
  106. package/src/resources/extensions/gsd/health-widget.ts +167 -0
  107. package/src/resources/extensions/gsd/index.ts +18 -5
  108. package/src/resources/extensions/gsd/key-manager.ts +2 -1
  109. package/src/resources/extensions/gsd/marketplace-discovery.ts +4 -3
  110. package/src/resources/extensions/gsd/metrics.ts +3 -3
  111. package/src/resources/extensions/gsd/migrate-external.ts +21 -4
  112. package/src/resources/extensions/gsd/milestone-ids.ts +2 -1
  113. package/src/resources/extensions/gsd/native-git-bridge.ts +2 -1
  114. package/src/resources/extensions/gsd/parallel-merge.ts +2 -1
  115. package/src/resources/extensions/gsd/parallel-orchestrator.ts +2 -1
  116. package/src/resources/extensions/gsd/post-unit-hooks.ts +8 -9
  117. package/src/resources/extensions/gsd/preferences-types.ts +8 -0
  118. package/src/resources/extensions/gsd/preferences-validation.ts +3 -10
  119. package/src/resources/extensions/gsd/progress-score.ts +273 -0
  120. package/src/resources/extensions/gsd/prompts/run-uat.md +1 -42
  121. package/src/resources/extensions/gsd/quick.ts +61 -8
  122. package/src/resources/extensions/gsd/repo-identity.ts +22 -1
  123. package/src/resources/extensions/gsd/session-lock.ts +12 -1
  124. package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
  125. package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  126. package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
  127. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
  128. package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
  129. package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
  130. package/src/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
  131. package/src/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
  132. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
  133. package/src/resources/extensions/gsd/undo.ts +5 -7
  134. package/src/resources/extensions/gsd/unit-id.ts +14 -0
  135. package/src/resources/extensions/gsd/unit-runtime.ts +2 -1
  136. package/src/resources/extensions/gsd/visualizer-data.ts +60 -2
  137. package/src/resources/extensions/gsd/visualizer-views.ts +54 -0
  138. package/src/resources/extensions/gsd/worktree-command.ts +8 -7
package/README.md CHANGED
@@ -24,21 +24,24 @@ 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
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
27
+ ## What's New in v2.32
28
+
29
+ - **Simplified pipeline** — research merged into planning, mechanical completion (ADR-003)
30
+ - **Always-on health widget** — 🟢🟡🔴 traffic-light indicator in the progress widget and visualizer health tab
31
+ - **Environment health checks** — progress scoring and status integration for auto-mode
32
+ - **Extension registry**user-managed enable/disable for bundled and custom extensions
33
+ - **Built-in skill authoring** — create and distribute custom skills from within GSD
34
+ - **Workflow templates** — right-sized workflows for every task type (research, plan, execute, complete)
35
+ - **AWS Bedrock auth** — automatic credential refresh via the new `aws-auth` extension
36
+ - **`-w` / `--worktree` CLI flag** — launch isolated worktree sessions from the command line
37
+ - **Native MCP client** replaced MCPorter with a built-in MCP client for better reliability
38
+ - **External state directory** — `.gsd/` now lives in `~/.gsd/projects/` with a symlink (ADR-002)
39
+ - **Model health indicator** — live health status based on error trends and consecutive failures
40
+ - **Quick-task branch cleanup** — `/gsd quick` branches auto-merge back after completion
41
+ - **Windows EPERM fallback** — migration rename uses copy+delete when NTFS blocks rename
42
+ - **Worktree identity fix** — stable project hash across worktrees and main repo
43
+ - **Crash recovery guidance** — actionable next-step messages based on what was interrupted
44
+ - **UAT verdict gating** — non-PASS verdicts now block slice progression instead of being ignored
42
45
 
43
46
  See the full [Changelog](./CHANGELOG.md) for details.
44
47
 
@@ -65,6 +68,7 @@ Full documentation is available in the [`docs/`](./docs/) directory:
65
68
  - **[Visualizer](./docs/visualizer.md)** — workflow visualizer with stats and discussion status
66
69
  - **[Remote Questions](./docs/remote-questions.md)** — route decisions to Slack or Discord when human input is needed
67
70
  - **[Dynamic Model Routing](./docs/dynamic-model-routing.md)** — complexity-based model selection and budget pressure
71
+ - **[Pipeline Simplification (ADR-003)](./docs/ADR-003-pipeline-simplification.md)** — merged research into planning, mechanical completion
68
72
  - **[Migration from v1](./docs/migration.md)** — `.planning` → `.gsd` migration
69
73
 
70
74
  ---
@@ -141,12 +145,12 @@ The iron rule: **a task must fit in one context window.** If it can't, it's two
141
145
  Each slice flows through phases automatically:
142
146
 
143
147
  ```
144
- Research Plan → Execute (per task) → Complete → Reassess Roadmap → Next Slice
145
- ↓ (all slices done)
146
- Validate Milestone → Complete Milestone
148
+ Plan (with integrated research) → Execute (per task) → Complete → Reassess Roadmap → Next Slice
149
+ ↓ (all slices done)
150
+ Validate Milestone → Complete Milestone
147
151
  ```
148
152
 
149
- **Research** scouts the codebase and relevant docs. **Plan** decomposes the slice into tasks with must-haves (mechanically verifiable outcomes). **Execute** runs each task in a fresh context window with only the relevant files pre-loaded — then runs configured verification commands (lint, test, etc.) with auto-fix retries. **Complete** writes the summary, UAT script, marks the roadmap, and commits with meaningful messages derived from task summaries. **Reassess** checks if the roadmap still makes sense given what was learned. **Validate Milestone** runs a reconciliation gate after all slices complete — comparing roadmap success criteria against actual results before sealing the milestone.
153
+ **Plan** scouts the codebase, researches relevant docs, and decomposes the slice into tasks with must-haves (mechanically verifiable outcomes). **Execute** runs each task in a fresh context window with only the relevant files pre-loaded — then runs configured verification commands (lint, test, etc.) with auto-fix retries. **Complete** writes the summary, UAT script, marks the roadmap, and commits with meaningful messages derived from task summaries. **Reassess** checks if the roadmap still makes sense given what was learned. **Validate Milestone** runs a reconciliation gate after all slices complete — comparing roadmap success criteria against actual results before sealing the milestone.
150
154
 
151
155
  ### `/gsd auto` — The Main Event
152
156
 
@@ -326,6 +330,7 @@ On first run, GSD launches a branded setup wizard that walks you through LLM pro
326
330
  | `gsd headless [cmd]` | Run `/gsd` commands without TUI (CI, cron, scripts) |
327
331
  | `gsd headless query` | Instant JSON snapshot — state, next dispatch, costs (no LLM) |
328
332
  | `gsd --continue` (`-c`) | Resume the most recent session for the current directory |
333
+ | `gsd --worktree` (`-w`) | Launch an isolated worktree session for the active milestone |
329
334
  | `gsd sessions` | Interactive session picker — browse and resume any saved session |
330
335
 
331
336
  ---
@@ -483,7 +488,7 @@ See the full [Token Optimization Guide](./docs/token-optimization.md) for detail
483
488
 
484
489
  ### Bundled Tools
485
490
 
486
- GSD ships with 16 extensions, all loaded automatically:
491
+ GSD ships with 18 extensions, all loaded automatically:
487
492
 
488
493
  | Extension | What it provides |
489
494
  | ---------------------- | ---------------------------------------------------------------------------------------------------------------------- |
@@ -503,6 +508,8 @@ GSD ships with 16 extensions, all loaded automatically:
503
508
  | **Secure Env Collect** | Masked secret collection without manual .env editing |
504
509
  | **Remote Questions** | Route decisions to Slack/Discord when human input is needed in headless/CI mode |
505
510
  | **Universal Config** | Discover and import MCP servers and rules from other AI coding tools |
511
+ | **AWS Auth** | Automatic Bedrock credential refresh for AWS-hosted models |
512
+ | **TTSR** | Tool-use type-safe runtime validation |
506
513
 
507
514
  ### Bundled Agents
508
515
 
package/dist/cli.js CHANGED
@@ -359,16 +359,16 @@ if (cliFlags.messages[0] === 'worktree' || cliFlags.messages[0] === 'wt') {
359
359
  const sub = cliFlags.messages[1];
360
360
  const subArgs = cliFlags.messages.slice(2);
361
361
  if (!sub || sub === 'list') {
362
- handleList(process.cwd());
362
+ await handleList(process.cwd());
363
363
  }
364
364
  else if (sub === 'merge') {
365
365
  await handleMerge(process.cwd(), subArgs);
366
366
  }
367
367
  else if (sub === 'clean') {
368
- handleClean(process.cwd());
368
+ await handleClean(process.cwd());
369
369
  }
370
370
  else if (sub === 'remove' || sub === 'rm') {
371
- handleRemove(process.cwd(), subArgs);
371
+ await handleRemove(process.cwd(), subArgs);
372
372
  }
373
373
  else {
374
374
  process.stderr.write(`Unknown worktree command: ${sub}\n`);
@@ -381,7 +381,7 @@ if (cliFlags.messages[0] === 'worktree' || cliFlags.messages[0] === 'wt') {
381
381
  // ---------------------------------------------------------------------------
382
382
  if (cliFlags.worktree) {
383
383
  const { handleWorktreeFlag } = await import('./worktree-cli.js');
384
- handleWorktreeFlag(cliFlags.worktree);
384
+ await handleWorktreeFlag(cliFlags.worktree);
385
385
  }
386
386
  // ---------------------------------------------------------------------------
387
387
  // Active worktree banner — remind user of unmerged worktrees on normal launch
@@ -389,7 +389,7 @@ if (cliFlags.worktree) {
389
389
  if (!cliFlags.worktree && !isPrintMode) {
390
390
  try {
391
391
  const { handleStatusBanner } = await import('./worktree-cli.js');
392
- handleStatusBanner(process.cwd());
392
+ await handleStatusBanner(process.cwd());
393
393
  }
394
394
  catch { /* non-fatal */ }
395
395
  }
@@ -1,7 +1,7 @@
1
1
  import { DefaultResourceLoader } from '@gsd/pi-coding-agent';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { homedir } from 'node:os';
4
- import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
4
+ import { chmodSync, copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
5
5
  import { dirname, join, relative, resolve } from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import { compareSemver } from './update-check.js';
@@ -118,7 +118,12 @@ export function getNewerManagedResourceVersion(agentDir, currentVersion) {
118
118
  function makeTreeWritable(dirPath) {
119
119
  if (!existsSync(dirPath))
120
120
  return;
121
- const stats = statSync(dirPath);
121
+ // Use lstatSync to avoid following symlinks into immutable filesystems
122
+ // (e.g., Nix store on NixOS/nix-darwin). Symlinks don't carry their own
123
+ // permissions and their targets may be read-only by design (#1298).
124
+ const stats = lstatSync(dirPath);
125
+ if (stats.isSymbolicLink())
126
+ return;
122
127
  const isDir = stats.isDirectory();
123
128
  const currentMode = stats.mode & 0o777;
124
129
  // Ensure owner-write; for directories also ensure owner-exec so they remain traversable.
@@ -127,7 +132,12 @@ function makeTreeWritable(dirPath) {
127
132
  newMode |= 0o100;
128
133
  }
129
134
  if (newMode !== currentMode) {
130
- chmodSync(dirPath, newMode);
135
+ try {
136
+ chmodSync(dirPath, newMode);
137
+ }
138
+ catch {
139
+ // Non-fatal — may fail on read-only filesystems or insufficient permissions
140
+ }
131
141
  }
132
142
  if (isDir) {
133
143
  for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Shared constants for auto-mode modules (auto.ts, auto-post-unit.ts, etc.).
3
+ */
4
+
5
+ /** Throttle STATE.md rebuilds — at most once per 30 seconds. */
6
+ export const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
@@ -20,6 +20,7 @@ import { parseRoadmap, parsePlan } from "./files.js";
20
20
  import { readFileSync, existsSync } from "node:fs";
21
21
  import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
22
22
  import { makeUI, GLYPH, INDENT } from "../shared/mod.js";
23
+ import { parseUnitId } from "./unit-id.js";
23
24
 
24
25
  // ─── Dashboard Data ───────────────────────────────────────────────────────────
25
26
 
@@ -48,40 +49,34 @@ export interface AutoDashboardData {
48
49
 
49
50
  // ─── Unit Description Helpers ─────────────────────────────────────────────────
50
51
 
52
+ /** Canonical verb and phase label for each known unit type. */
53
+ const UNIT_TYPE_INFO: Record<string, { verb: string; phaseLabel: string }> = {
54
+ "research-milestone": { verb: "researching", phaseLabel: "RESEARCH" },
55
+ "research-slice": { verb: "researching", phaseLabel: "RESEARCH" },
56
+ "plan-milestone": { verb: "planning", phaseLabel: "PLAN" },
57
+ "plan-slice": { verb: "planning", phaseLabel: "PLAN" },
58
+ "execute-task": { verb: "executing", phaseLabel: "EXECUTE" },
59
+ "complete-slice": { verb: "completing", phaseLabel: "COMPLETE" },
60
+ "replan-slice": { verb: "replanning", phaseLabel: "REPLAN" },
61
+ "rewrite-docs": { verb: "rewriting", phaseLabel: "REWRITE" },
62
+ "reassess-roadmap": { verb: "reassessing", phaseLabel: "REASSESS" },
63
+ "run-uat": { verb: "running UAT", phaseLabel: "UAT" },
64
+ };
65
+
51
66
  export function unitVerb(unitType: string): string {
52
67
  if (unitType.startsWith("hook/")) return `hook: ${unitType.slice(5)}`;
53
- switch (unitType) {
54
- case "research-milestone":
55
- case "research-slice": return "researching";
56
- case "plan-milestone":
57
- case "plan-slice": return "planning";
58
- case "execute-task": return "executing";
59
- case "complete-slice": return "completing";
60
- case "replan-slice": return "replanning";
61
- case "rewrite-docs": return "rewriting";
62
- case "reassess-roadmap": return "reassessing";
63
- case "run-uat": return "running UAT";
64
- default: return unitType;
65
- }
68
+ return UNIT_TYPE_INFO[unitType]?.verb ?? unitType;
66
69
  }
67
70
 
68
71
  export function unitPhaseLabel(unitType: string): string {
69
72
  if (unitType.startsWith("hook/")) return "HOOK";
70
- switch (unitType) {
71
- case "research-milestone": return "RESEARCH";
72
- case "research-slice": return "RESEARCH";
73
- case "plan-milestone": return "PLAN";
74
- case "plan-slice": return "PLAN";
75
- case "execute-task": return "EXECUTE";
76
- case "complete-slice": return "COMPLETE";
77
- case "replan-slice": return "REPLAN";
78
- case "rewrite-docs": return "REWRITE";
79
- case "reassess-roadmap": return "REASSESS";
80
- case "run-uat": return "UAT";
81
- default: return unitType.toUpperCase();
82
- }
73
+ return UNIT_TYPE_INFO[unitType]?.phaseLabel ?? unitType.toUpperCase();
83
74
  }
84
75
 
76
+ /**
77
+ * Describe the expected next step after the current unit completes.
78
+ * Unit types here mirror the keys in UNIT_TYPE_INFO above.
79
+ */
85
80
  function peekNext(unitType: string, state: GSDState): string {
86
81
  // Show active hook info in progress display
87
82
  const activeHookState = getActiveHook();
@@ -378,8 +373,9 @@ export function updateProgressWidget(
378
373
  lines.push("");
379
374
 
380
375
  const isHook = unitType.startsWith("hook/");
376
+ const hookParsed = isHook ? parseUnitId(unitId) : undefined;
381
377
  const target = isHook
382
- ? (unitId.split("/").pop() ?? unitId)
378
+ ? (hookParsed!.task ?? hookParsed!.slice ?? unitId)
383
379
  : (task ? `${task.id}: ${task.title}` : unitId);
384
380
  const actionLeft = `${pad}${theme.fg("accent", "▸")} ${theme.fg("accent", verb)} ${theme.fg("text", target)}`;
385
381
  const tierTag = tierBadge ? theme.fg("dim", `[${tierBadge}] `) : "";
@@ -182,15 +182,10 @@ export async function dispatchDirectPhase(
182
182
  ctx.ui.notify("Cannot dispatch run-uat: no UAT file found.", "warning");
183
183
  return;
184
184
  }
185
- const uatContent = await loadFile(uatFile);
186
- if (!uatContent) {
187
- ctx.ui.notify("Cannot dispatch run-uat: UAT file is empty.", "warning");
188
- return;
189
- }
190
185
  const uatPath = relSliceFile(base, mid, sid, "UAT");
191
186
  unitType = "run-uat";
192
187
  unitId = `${mid}/${sid}`;
193
- prompt = await buildRunUatPrompt(mid, sid, uatPath, uatContent, base);
188
+ prompt = await buildRunUatPrompt(mid, sid, uatPath, base);
194
189
  break;
195
190
  }
196
191
 
@@ -11,8 +11,7 @@
11
11
 
12
12
  import type { GSDState } from "./types.js";
13
13
  import type { GSDPreferences } from "./preferences.js";
14
- import type { UatType } from "./files.js";
15
- import { loadFile, extractUatType, loadActiveOverrides, parseRoadmap } from "./files.js";
14
+ import { loadFile, loadActiveOverrides, parseRoadmap } from "./files.js";
16
15
  import {
17
16
  resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveTaskFile,
18
17
  relSliceFile, buildMilestoneFileName,
@@ -39,7 +38,7 @@ import {
39
38
  // ─── Types ────────────────────────────────────────────────────────────────
40
39
 
41
40
  export type DispatchAction =
42
- | { action: "dispatch"; unitType: string; unitId: string; prompt: string; pauseAfterDispatch?: boolean }
41
+ | { action: "dispatch"; unitType: string; unitId: string; prompt: string }
43
42
  | { action: "stop"; reason: string; level: "info" | "warning" | "error" }
44
43
  | { action: "skip" };
45
44
 
@@ -138,17 +137,14 @@ const DISPATCH_RULES: DispatchRule[] = [
138
137
  match: async ({ state, mid, basePath, prefs }) => {
139
138
  const needsRunUat = await checkNeedsRunUat(basePath, mid, state, prefs);
140
139
  if (!needsRunUat) return null;
141
- const { sliceId, uatType } = needsRunUat;
142
- const uatFile = resolveSliceFile(basePath, mid, sliceId, "UAT")!;
143
- const uatContent = await loadFile(uatFile);
140
+ const { sliceId } = needsRunUat;
144
141
  return {
145
142
  action: "dispatch",
146
143
  unitType: "run-uat",
147
144
  unitId: `${mid}/${sliceId}`,
148
145
  prompt: await buildRunUatPrompt(
149
- mid, sliceId, relSliceFile(basePath, mid, sliceId, "UAT"), uatContent ?? "", basePath,
146
+ mid, sliceId, relSliceFile(basePath, mid, sliceId, "UAT"), basePath,
150
147
  ),
151
- pauseAfterDispatch: uatType !== "artifact-driven",
152
148
  };
153
149
  },
154
150
  },
@@ -18,6 +18,7 @@ import {
18
18
  import { resolveMilestoneFile } from "./paths.js";
19
19
  import { MAX_CONSECUTIVE_SKIPS, MAX_LIFETIME_DISPATCHES } from "./auto/session.js";
20
20
  import type { AutoSession } from "./auto/session.js";
21
+ import { parseUnitId } from "./unit-id.js";
21
22
 
22
23
  export interface IdempotencyContext {
23
24
  s: AutoSession;
@@ -54,7 +55,7 @@ export function checkIdempotency(ictx: IdempotencyContext): IdempotencyResult {
54
55
  s.unitConsecutiveSkips.set(idempotencyKey, skipCount);
55
56
  if (skipCount > MAX_CONSECUTIVE_SKIPS) {
56
57
  // Cross-check: verify the unit's milestone is still active (#790)
57
- const skippedMid = unitId.split("/")[0];
58
+ const skippedMid = parseUnitId(unitId).milestone;
58
59
  const skippedMilestoneComplete = skippedMid
59
60
  ? !!resolveMilestoneFile(basePath, skippedMid, "SUMMARY")
60
61
  : false;
@@ -110,7 +111,7 @@ export function checkIdempotency(ictx: IdempotencyContext): IdempotencyResult {
110
111
  const skipCount2 = (s.unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
111
112
  s.unitConsecutiveSkips.set(idempotencyKey, skipCount2);
112
113
  if (skipCount2 > MAX_CONSECUTIVE_SKIPS) {
113
- const skippedMid2 = unitId.split("/")[0];
114
+ const skippedMid2 = parseUnitId(unitId).milestone;
114
115
  const skippedMilestoneComplete2 = skippedMid2
115
116
  ? !!resolveMilestoneFile(basePath, skippedMid2, "SUMMARY")
116
117
  : false;
@@ -12,6 +12,7 @@ import {
12
12
  formatValidationIssues,
13
13
  } from "./observability-validator.js";
14
14
  import type { ValidationIssue } from "./observability-validator.js";
15
+ import { parseUnitId } from "./unit-id.js";
15
16
 
16
17
  export async function collectObservabilityWarnings(
17
18
  ctx: ExtensionContext,
@@ -22,10 +23,7 @@ export async function collectObservabilityWarnings(
22
23
  // Hook units have custom artifacts — skip standard observability checks
23
24
  if (unitType.startsWith("hook/")) return [];
24
25
 
25
- const parts = unitId.split("/");
26
- const mid = parts[0];
27
- const sid = parts[1];
28
- const tid = parts[2];
26
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(unitId);
29
27
 
30
28
  if (!mid || !sid) return [];
31
29
 
@@ -60,9 +60,32 @@ import {
60
60
  hideFooter,
61
61
  } from "./auto-dashboard.js";
62
62
  import { join } from "node:path";
63
+ import { STATE_REBUILD_MIN_INTERVAL_MS } from "./auto-constants.js";
64
+ import { parseUnitId } from "./unit-id.js";
63
65
 
64
- /** Throttle STATE.md rebuilds — at most once per 30 seconds */
65
- const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
66
+ /**
67
+ * Initialize a unit dispatch: stamp the current time, set `s.currentUnit`,
68
+ * and persist the initial runtime record. Returns `startedAt` for callers
69
+ * that need the timestamp.
70
+ */
71
+ function dispatchUnit(
72
+ s: AutoSession,
73
+ basePath: string,
74
+ unitType: string,
75
+ unitId: string,
76
+ ): number {
77
+ const startedAt = Date.now();
78
+ s.currentUnit = { type: unitType, id: unitId, startedAt };
79
+ writeUnitRuntimeRecord(basePath, unitType, unitId, startedAt, {
80
+ phase: "dispatched",
81
+ wrapupWarningSent: false,
82
+ timeoutAt: null,
83
+ lastProgressAt: startedAt,
84
+ progressCount: 0,
85
+ lastProgressKind: "dispatch",
86
+ });
87
+ return startedAt;
88
+ }
66
89
 
67
90
  export interface PostUnitContext {
68
91
  s: AutoSession;
@@ -112,8 +135,7 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
112
135
  let taskContext: TaskCommitContext | undefined;
113
136
 
114
137
  if (s.currentUnit.type === "execute-task") {
115
- const parts = s.currentUnit.id.split("/");
116
- const [mid, sid, tid] = parts;
138
+ const { milestone: mid, slice: sid, task: tid } = parseUnitId(s.currentUnit.id);
117
139
  if (mid && sid && tid) {
118
140
  const summaryPath = resolveTaskFile(s.basePath, mid, sid, tid, "SUMMARY");
119
141
  if (summaryPath) {
@@ -145,8 +167,8 @@ export async function postUnitPreVerification(pctx: PostUnitContext): Promise<"d
145
167
 
146
168
  // Doctor: fix mechanical bookkeeping
147
169
  try {
148
- const scopeParts = s.currentUnit.id.split("/").slice(0, 2);
149
- const doctorScope = scopeParts.join("/");
170
+ const { milestone, slice } = parseUnitId(s.currentUnit.id);
171
+ const doctorScope = slice ? `${milestone}/${slice}` : milestone;
150
172
  const sliceTerminalUnits = new Set(["complete-slice", "run-uat"]);
151
173
  const effectiveFixLevel = sliceTerminalUnits.has(s.currentUnit.type) ? "all" as const : "task" as const;
152
174
  const report = await runGSDDoctor(s.basePath, { fix: true, scope: doctorScope, fixLevel: effectiveFixLevel });
@@ -326,7 +348,7 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
326
348
  // instead of dispatching LLM sessions for complete-slice / validate-milestone.
327
349
  if (s.currentUnit?.type === "execute-task" && !s.stepMode) {
328
350
  try {
329
- const [mid, sid] = s.currentUnit.id.split("/");
351
+ const { milestone: mid, slice: sid } = parseUnitId(s.currentUnit.id);
330
352
  if (mid && sid) {
331
353
  const state = await deriveState(s.basePath);
332
354
  if (state.phase === "summarizing" && state.activeSlice?.id === sid) {
@@ -364,19 +386,10 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
364
386
  if (s.currentUnit && !s.stepMode) {
365
387
  const hookUnit = checkPostUnitHooks(s.currentUnit.type, s.currentUnit.id, s.basePath);
366
388
  if (hookUnit) {
367
- const hookStartedAt = Date.now();
368
389
  if (s.currentUnit) {
369
390
  await closeoutUnit(ctx, s.basePath, s.currentUnit.type, s.currentUnit.id, s.currentUnit.startedAt, buildSnapshotOpts(s.currentUnit.type, s.currentUnit.id));
370
391
  }
371
- s.currentUnit = { type: hookUnit.unitType, id: hookUnit.unitId, startedAt: hookStartedAt };
372
- writeUnitRuntimeRecord(s.basePath, hookUnit.unitType, hookUnit.unitId, hookStartedAt, {
373
- phase: "dispatched",
374
- wrapupWarningSent: false,
375
- timeoutAt: null,
376
- lastProgressAt: hookStartedAt,
377
- progressCount: 0,
378
- lastProgressKind: "dispatch",
379
- });
392
+ dispatchUnit(s, s.basePath, hookUnit.unitType, hookUnit.unitId);
380
393
 
381
394
  const state = await deriveState(s.basePath);
382
395
  updateProgressWidget(ctx, hookUnit.unitType, hookUnit.unitId, state);
@@ -498,16 +511,7 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
498
511
 
499
512
  const triageUnitType = "triage-captures";
500
513
  const triageUnitId = `${mid}/${sid}/triage`;
501
- const triageStartedAt = Date.now();
502
- s.currentUnit = { type: triageUnitType, id: triageUnitId, startedAt: triageStartedAt };
503
- writeUnitRuntimeRecord(s.basePath, triageUnitType, triageUnitId, triageStartedAt, {
504
- phase: "dispatched",
505
- wrapupWarningSent: false,
506
- timeoutAt: null,
507
- lastProgressAt: triageStartedAt,
508
- progressCount: 0,
509
- lastProgressKind: "dispatch",
510
- });
514
+ dispatchUnit(s, s.basePath, triageUnitType, triageUnitId);
511
515
  updateProgressWidget(ctx, triageUnitType, triageUnitId, state);
512
516
 
513
517
  const result = await s.cmdCtx!.newSession();
@@ -568,16 +572,7 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
568
572
 
569
573
  const qtUnitType = "quick-task";
570
574
  const qtUnitId = `${s.currentMilestoneId}/${capture.id}`;
571
- const qtStartedAt = Date.now();
572
- s.currentUnit = { type: qtUnitType, id: qtUnitId, startedAt: qtStartedAt };
573
- writeUnitRuntimeRecord(s.basePath, qtUnitType, qtUnitId, qtStartedAt, {
574
- phase: "dispatched",
575
- wrapupWarningSent: false,
576
- timeoutAt: null,
577
- lastProgressAt: qtStartedAt,
578
- progressCount: 0,
579
- lastProgressKind: "dispatch",
580
- });
575
+ dispatchUnit(s, s.basePath, qtUnitType, qtUnitId);
581
576
  const state = await deriveState(s.basePath);
582
577
  updateProgressWidget(ctx, qtUnitType, qtUnitId, state);
583
578