gsd-pi 2.31.2 → 2.32.0

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 (74) hide show
  1. package/dist/cli.js +5 -5
  2. package/dist/resources/extensions/gsd/auto-constants.ts +6 -0
  3. package/dist/resources/extensions/gsd/auto-dashboard.ts +20 -26
  4. package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
  5. package/dist/resources/extensions/gsd/auto-dispatch.ts +4 -8
  6. package/dist/resources/extensions/gsd/auto-post-unit.ts +27 -32
  7. package/dist/resources/extensions/gsd/auto-prompts.ts +38 -34
  8. package/dist/resources/extensions/gsd/auto-start.ts +8 -6
  9. package/dist/resources/extensions/gsd/auto.ts +54 -33
  10. package/dist/resources/extensions/gsd/commands-workflow-templates.ts +3 -5
  11. package/dist/resources/extensions/gsd/commands.ts +19 -0
  12. package/dist/resources/extensions/gsd/dashboard-overlay.ts +28 -0
  13. package/dist/resources/extensions/gsd/doctor-environment.ts +497 -0
  14. package/dist/resources/extensions/gsd/doctor-providers.ts +343 -0
  15. package/dist/resources/extensions/gsd/doctor-types.ts +14 -1
  16. package/dist/resources/extensions/gsd/doctor.ts +6 -0
  17. package/dist/resources/extensions/gsd/git-service.ts +9 -0
  18. package/dist/resources/extensions/gsd/guided-flow-queue.ts +1 -8
  19. package/dist/resources/extensions/gsd/health-widget.ts +167 -0
  20. package/dist/resources/extensions/gsd/index.ts +6 -0
  21. package/dist/resources/extensions/gsd/preferences-types.ts +8 -0
  22. package/dist/resources/extensions/gsd/preferences-validation.ts +3 -10
  23. package/dist/resources/extensions/gsd/progress-score.ts +273 -0
  24. package/dist/resources/extensions/gsd/prompts/run-uat.md +1 -42
  25. package/dist/resources/extensions/gsd/quick.ts +3 -5
  26. package/dist/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
  27. package/dist/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
  28. package/dist/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
  29. package/dist/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
  30. package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
  31. package/dist/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
  32. package/dist/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
  33. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
  34. package/dist/resources/extensions/gsd/visualizer-data.ts +60 -2
  35. package/dist/resources/extensions/gsd/visualizer-views.ts +54 -0
  36. package/dist/worktree-cli.d.ts +42 -6
  37. package/dist/worktree-cli.js +88 -48
  38. package/package.json +1 -1
  39. package/packages/pi-coding-agent/package.json +1 -1
  40. package/pkg/package.json +1 -1
  41. package/src/resources/extensions/gsd/auto-constants.ts +6 -0
  42. package/src/resources/extensions/gsd/auto-dashboard.ts +20 -26
  43. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
  44. package/src/resources/extensions/gsd/auto-dispatch.ts +4 -8
  45. package/src/resources/extensions/gsd/auto-post-unit.ts +27 -32
  46. package/src/resources/extensions/gsd/auto-prompts.ts +38 -34
  47. package/src/resources/extensions/gsd/auto-start.ts +8 -6
  48. package/src/resources/extensions/gsd/auto.ts +54 -33
  49. package/src/resources/extensions/gsd/commands-workflow-templates.ts +3 -5
  50. package/src/resources/extensions/gsd/commands.ts +19 -0
  51. package/src/resources/extensions/gsd/dashboard-overlay.ts +28 -0
  52. package/src/resources/extensions/gsd/doctor-environment.ts +497 -0
  53. package/src/resources/extensions/gsd/doctor-providers.ts +343 -0
  54. package/src/resources/extensions/gsd/doctor-types.ts +14 -1
  55. package/src/resources/extensions/gsd/doctor.ts +6 -0
  56. package/src/resources/extensions/gsd/git-service.ts +9 -0
  57. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -8
  58. package/src/resources/extensions/gsd/health-widget.ts +167 -0
  59. package/src/resources/extensions/gsd/index.ts +6 -0
  60. package/src/resources/extensions/gsd/preferences-types.ts +8 -0
  61. package/src/resources/extensions/gsd/preferences-validation.ts +3 -10
  62. package/src/resources/extensions/gsd/progress-score.ts +273 -0
  63. package/src/resources/extensions/gsd/prompts/run-uat.md +1 -42
  64. package/src/resources/extensions/gsd/quick.ts +3 -5
  65. package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
  66. package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
  67. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
  68. package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
  69. package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
  70. package/src/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
  71. package/src/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
  72. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
  73. package/src/resources/extensions/gsd/visualizer-data.ts +60 -2
  74. package/src/resources/extensions/gsd/visualizer-views.ts +54 -0
@@ -12,18 +12,53 @@
12
12
  * On session exit (via session_shutdown event), auto-commits dirty work
13
13
  * so nothing is lost. The GSD extension reads GSD_CLI_WORKTREE to know
14
14
  * when a session was launched via -w.
15
+ *
16
+ * Note: Extension modules are .ts files loaded via jiti (not compiled to .js).
17
+ * We use createJiti() here because this module is compiled by tsc but imports
18
+ * from resources/extensions/gsd/ which are shipped as raw .ts (#1283).
15
19
  */
16
20
  import chalk from 'chalk';
17
- import { createWorktree, listWorktrees, removeWorktree, mergeWorktreeToMain, diffWorktreeAll, diffWorktreeNumstat, worktreeBranchName, } from './resources/extensions/gsd/worktree-manager.js';
18
- import { runWorktreePostCreateHook } from './resources/extensions/gsd/auto-worktree.js';
21
+ import { createJiti } from '@mariozechner/jiti';
22
+ import { fileURLToPath } from 'node:url';
23
+ import { dirname, join } from 'node:path';
19
24
  import { generateWorktreeName } from './worktree-name-gen.js';
20
- import { nativeHasChanges, nativeDetectMainBranch, nativeCommitCountBetween, } from './resources/extensions/gsd/native-git-bridge.js';
21
- import { inferCommitType } from './resources/extensions/gsd/git-service.js';
22
25
  import { existsSync } from 'node:fs';
26
+ const __dirname = dirname(fileURLToPath(import.meta.url));
27
+ const jiti = createJiti(fileURLToPath(import.meta.url), { interopDefault: true, debug: false });
28
+ // Lazily-loaded extension modules (loaded once on first use via jiti)
29
+ let _ext = null;
30
+ async function loadExtensionModules() {
31
+ if (_ext)
32
+ return _ext;
33
+ const [wtMgr, autoWt, gitBridge, gitSvc, wt] = await Promise.all([
34
+ jiti.import(join(__dirname, 'resources/extensions/gsd/worktree-manager.ts'), {}),
35
+ jiti.import(join(__dirname, 'resources/extensions/gsd/auto-worktree.ts'), {}),
36
+ jiti.import(join(__dirname, 'resources/extensions/gsd/native-git-bridge.ts'), {}),
37
+ jiti.import(join(__dirname, 'resources/extensions/gsd/git-service.ts'), {}),
38
+ jiti.import(join(__dirname, 'resources/extensions/gsd/worktree.ts'), {}),
39
+ ]);
40
+ _ext = {
41
+ createWorktree: wtMgr.createWorktree,
42
+ listWorktrees: wtMgr.listWorktrees,
43
+ removeWorktree: wtMgr.removeWorktree,
44
+ mergeWorktreeToMain: wtMgr.mergeWorktreeToMain,
45
+ diffWorktreeAll: wtMgr.diffWorktreeAll,
46
+ diffWorktreeNumstat: wtMgr.diffWorktreeNumstat,
47
+ worktreeBranchName: wtMgr.worktreeBranchName,
48
+ worktreePath: wtMgr.worktreePath,
49
+ runWorktreePostCreateHook: autoWt.runWorktreePostCreateHook,
50
+ nativeHasChanges: gitBridge.nativeHasChanges,
51
+ nativeDetectMainBranch: gitBridge.nativeDetectMainBranch,
52
+ nativeCommitCountBetween: gitBridge.nativeCommitCountBetween,
53
+ inferCommitType: gitSvc.inferCommitType,
54
+ autoCommitCurrentBranch: wt.autoCommitCurrentBranch,
55
+ };
56
+ return _ext;
57
+ }
23
58
  // ─── Status Helpers ─────────────────────────────────────────────────────────
24
- function getWorktreeStatus(basePath, name, wtPath) {
25
- const diff = diffWorktreeAll(basePath, name);
26
- const numstat = diffWorktreeNumstat(basePath, name);
59
+ function getWorktreeStatus(ext, basePath, name, wtPath) {
60
+ const diff = ext.diffWorktreeAll(basePath, name);
61
+ const numstat = ext.diffWorktreeNumstat(basePath, name);
27
62
  const filesChanged = diff.added.length + diff.modified.length + diff.removed.length;
28
63
  let linesAdded = 0;
29
64
  let linesRemoved = 0;
@@ -33,19 +68,19 @@ function getWorktreeStatus(basePath, name, wtPath) {
33
68
  }
34
69
  let uncommitted = false;
35
70
  try {
36
- uncommitted = existsSync(wtPath) && nativeHasChanges(wtPath);
71
+ uncommitted = existsSync(wtPath) && ext.nativeHasChanges(wtPath);
37
72
  }
38
73
  catch { /* */ }
39
74
  let commits = 0;
40
75
  try {
41
- const mainBranch = nativeDetectMainBranch(basePath);
42
- commits = nativeCommitCountBetween(basePath, mainBranch, worktreeBranchName(name));
76
+ const mainBranch = ext.nativeDetectMainBranch(basePath);
77
+ commits = ext.nativeCommitCountBetween(basePath, mainBranch, ext.worktreeBranchName(name));
43
78
  }
44
79
  catch { /* */ }
45
80
  return {
46
81
  name,
47
82
  path: wtPath,
48
- branch: worktreeBranchName(name),
83
+ branch: ext.worktreeBranchName(name),
49
84
  exists: existsSync(wtPath),
50
85
  filesChanged,
51
86
  linesAdded,
@@ -71,65 +106,66 @@ function formatStatus(s) {
71
106
  return lines.join('\n');
72
107
  }
73
108
  // ─── Subcommand: list ───────────────────────────────────────────────────────
74
- function handleList(basePath) {
75
- const worktrees = listWorktrees(basePath);
109
+ async function handleList(basePath) {
110
+ const ext = await loadExtensionModules();
111
+ const worktrees = ext.listWorktrees(basePath);
76
112
  if (worktrees.length === 0) {
77
113
  process.stderr.write(chalk.dim('No worktrees. Create one with: gsd -w <name>\n'));
78
114
  return;
79
115
  }
80
116
  process.stderr.write(chalk.bold('\nWorktrees\n\n'));
81
117
  for (const wt of worktrees) {
82
- const status = getWorktreeStatus(basePath, wt.name, wt.path);
118
+ const status = getWorktreeStatus(ext, basePath, wt.name, wt.path);
83
119
  process.stderr.write(formatStatus(status) + '\n\n');
84
120
  }
85
121
  }
86
122
  // ─── Subcommand: merge ──────────────────────────────────────────────────────
87
123
  async function handleMerge(basePath, args) {
124
+ const ext = await loadExtensionModules();
88
125
  const name = args[0];
89
126
  if (!name) {
90
127
  // If only one worktree exists, merge it
91
- const worktrees = listWorktrees(basePath);
128
+ const worktrees = ext.listWorktrees(basePath);
92
129
  if (worktrees.length === 1) {
93
- await doMerge(basePath, worktrees[0].name);
130
+ await doMerge(ext, basePath, worktrees[0].name);
94
131
  return;
95
132
  }
96
133
  process.stderr.write(chalk.red('Usage: gsd worktree merge <name>\n'));
97
134
  process.stderr.write(chalk.dim('Run gsd worktree list to see worktrees.\n'));
98
135
  process.exit(1);
99
136
  }
100
- await doMerge(basePath, name);
137
+ await doMerge(ext, basePath, name);
101
138
  }
102
- async function doMerge(basePath, name) {
103
- const worktrees = listWorktrees(basePath);
139
+ async function doMerge(ext, basePath, name) {
140
+ const worktrees = ext.listWorktrees(basePath);
104
141
  const wt = worktrees.find(w => w.name === name);
105
142
  if (!wt) {
106
143
  process.stderr.write(chalk.red(`Worktree "${name}" not found.\n`));
107
144
  process.exit(1);
108
145
  }
109
- const status = getWorktreeStatus(basePath, name, wt.path);
146
+ const status = getWorktreeStatus(ext, basePath, name, wt.path);
110
147
  if (status.filesChanged === 0 && !status.uncommitted) {
111
148
  process.stderr.write(chalk.dim(`Worktree "${name}" has no changes to merge.\n`));
112
149
  // Clean up empty worktree
113
- removeWorktree(basePath, name, { deleteBranch: true });
150
+ ext.removeWorktree(basePath, name, { deleteBranch: true });
114
151
  process.stderr.write(chalk.green(`Removed empty worktree ${chalk.bold(name)}.\n`));
115
152
  return;
116
153
  }
117
154
  // Auto-commit dirty work before merge
118
155
  if (status.uncommitted) {
119
156
  try {
120
- const { autoCommitCurrentBranch } = await import('./resources/extensions/gsd/worktree.js');
121
- autoCommitCurrentBranch(wt.path, 'worktree-merge', name);
157
+ ext.autoCommitCurrentBranch(wt.path, 'worktree-merge', name);
122
158
  process.stderr.write(chalk.dim(' Auto-committed dirty work before merge.\n'));
123
159
  }
124
160
  catch { /* best-effort */ }
125
161
  }
126
- const commitType = inferCommitType(name);
162
+ const commitType = ext.inferCommitType(name);
127
163
  const commitMessage = `${commitType}(${name}): merge worktree ${name}`;
128
- process.stderr.write(`\nMerging ${chalk.bold.cyan(name)} → ${chalk.magenta(nativeDetectMainBranch(basePath))}\n`);
164
+ process.stderr.write(`\nMerging ${chalk.bold.cyan(name)} → ${chalk.magenta(ext.nativeDetectMainBranch(basePath))}\n`);
129
165
  process.stderr.write(chalk.dim(` ${status.filesChanged} files, ${chalk.green(`+${status.linesAdded}`)} ${chalk.red(`-${status.linesRemoved}`)}\n\n`));
130
166
  try {
131
- mergeWorktreeToMain(basePath, name, commitMessage);
132
- removeWorktree(basePath, name, { deleteBranch: true });
167
+ ext.mergeWorktreeToMain(basePath, name, commitMessage);
168
+ ext.removeWorktree(basePath, name, { deleteBranch: true });
133
169
  process.stderr.write(chalk.green(`✓ Merged and cleaned up ${chalk.bold(name)}\n`));
134
170
  process.stderr.write(chalk.dim(` commit: ${commitMessage}\n`));
135
171
  }
@@ -141,18 +177,19 @@ async function doMerge(basePath, name) {
141
177
  }
142
178
  }
143
179
  // ─── Subcommand: clean ──────────────────────────────────────────────────────
144
- function handleClean(basePath) {
145
- const worktrees = listWorktrees(basePath);
180
+ async function handleClean(basePath) {
181
+ const ext = await loadExtensionModules();
182
+ const worktrees = ext.listWorktrees(basePath);
146
183
  if (worktrees.length === 0) {
147
184
  process.stderr.write(chalk.dim('No worktrees to clean.\n'));
148
185
  return;
149
186
  }
150
187
  let cleaned = 0;
151
188
  for (const wt of worktrees) {
152
- const status = getWorktreeStatus(basePath, wt.name, wt.path);
189
+ const status = getWorktreeStatus(ext, basePath, wt.name, wt.path);
153
190
  if (status.filesChanged === 0 && !status.uncommitted) {
154
191
  try {
155
- removeWorktree(basePath, wt.name, { deleteBranch: true });
192
+ ext.removeWorktree(basePath, wt.name, { deleteBranch: true });
156
193
  process.stderr.write(chalk.green(` ✓ Removed ${chalk.bold(wt.name)} (clean)\n`));
157
194
  cleaned++;
158
195
  }
@@ -167,19 +204,20 @@ function handleClean(basePath) {
167
204
  process.stderr.write(chalk.dim(`\nCleaned ${cleaned} worktree${cleaned === 1 ? '' : 's'}.\n`));
168
205
  }
169
206
  // ─── Subcommand: remove ─────────────────────────────────────────────────────
170
- function handleRemove(basePath, args) {
207
+ async function handleRemove(basePath, args) {
208
+ const ext = await loadExtensionModules();
171
209
  const name = args[0];
172
210
  if (!name) {
173
211
  process.stderr.write(chalk.red('Usage: gsd worktree remove <name>\n'));
174
212
  process.exit(1);
175
213
  }
176
- const worktrees = listWorktrees(basePath);
214
+ const worktrees = ext.listWorktrees(basePath);
177
215
  const wt = worktrees.find(w => w.name === name);
178
216
  if (!wt) {
179
217
  process.stderr.write(chalk.red(`Worktree "${name}" not found.\n`));
180
218
  process.exit(1);
181
219
  }
182
- const status = getWorktreeStatus(basePath, name, wt.path);
220
+ const status = getWorktreeStatus(ext, basePath, name, wt.path);
183
221
  if (status.filesChanged > 0 || status.uncommitted) {
184
222
  process.stderr.write(chalk.yellow(`⚠ Worktree "${name}" has unmerged changes (${status.filesChanged} files).\n`));
185
223
  process.stderr.write(chalk.yellow(' Use --force to remove anyway, or merge first: gsd worktree merge ' + name + '\n'));
@@ -187,17 +225,18 @@ function handleRemove(basePath, args) {
187
225
  process.exit(1);
188
226
  }
189
227
  }
190
- removeWorktree(basePath, name, { deleteBranch: true });
228
+ ext.removeWorktree(basePath, name, { deleteBranch: true });
191
229
  process.stderr.write(chalk.green(`✓ Removed worktree ${chalk.bold(name)}\n`));
192
230
  }
193
231
  // ─── Subcommand: status (default when no args) ─────────────────────────────
194
- function handleStatusBanner(basePath) {
195
- const worktrees = listWorktrees(basePath);
232
+ async function handleStatusBanner(basePath) {
233
+ const ext = await loadExtensionModules();
234
+ const worktrees = ext.listWorktrees(basePath);
196
235
  if (worktrees.length === 0)
197
236
  return;
198
237
  const withChanges = worktrees.filter(wt => {
199
238
  try {
200
- const diff = diffWorktreeAll(basePath, wt.name);
239
+ const diff = ext.diffWorktreeAll(basePath, wt.name);
201
240
  return diff.added.length + diff.modified.length + diff.removed.length > 0;
202
241
  }
203
242
  catch {
@@ -214,14 +253,15 @@ function handleStatusBanner(basePath) {
214
253
  chalk.dim('Resume: gsd -w <name> | Merge: gsd worktree merge <name> | List: gsd worktree list\n\n'));
215
254
  }
216
255
  // ─── -w flag: create/resume worktree for interactive session ────────────────
217
- function handleWorktreeFlag(worktreeFlag) {
256
+ async function handleWorktreeFlag(worktreeFlag) {
257
+ const ext = await loadExtensionModules();
218
258
  const basePath = process.cwd();
219
259
  // gsd -w (no name) — resume most recent worktree with changes, or create new
220
260
  if (worktreeFlag === true) {
221
- const existing = listWorktrees(basePath);
261
+ const existing = ext.listWorktrees(basePath);
222
262
  const withChanges = existing.filter(wt => {
223
263
  try {
224
- const diff = diffWorktreeAll(basePath, wt.name);
264
+ const diff = ext.diffWorktreeAll(basePath, wt.name);
225
265
  return diff.added.length + diff.modified.length + diff.removed.length > 0;
226
266
  }
227
267
  catch {
@@ -243,7 +283,7 @@ function handleWorktreeFlag(worktreeFlag) {
243
283
  // Multiple active worktrees — show them and ask user to pick
244
284
  process.stderr.write(chalk.yellow(`${withChanges.length} worktrees have unmerged changes:\n\n`));
245
285
  for (const wt of withChanges) {
246
- const status = getWorktreeStatus(basePath, wt.name, wt.path);
286
+ const status = getWorktreeStatus(ext, basePath, wt.name, wt.path);
247
287
  process.stderr.write(formatStatus(status) + '\n\n');
248
288
  }
249
289
  process.stderr.write(chalk.dim('Specify which one: gsd -w <name>\n'));
@@ -251,12 +291,12 @@ function handleWorktreeFlag(worktreeFlag) {
251
291
  }
252
292
  // No active worktrees — create a new one
253
293
  const name = generateWorktreeName();
254
- createAndEnter(basePath, name);
294
+ await createAndEnter(ext, basePath, name);
255
295
  return;
256
296
  }
257
297
  // gsd -w <name> — create or resume named worktree
258
298
  const name = worktreeFlag;
259
- const existing = listWorktrees(basePath);
299
+ const existing = ext.listWorktrees(basePath);
260
300
  const found = existing.find(wt => wt.name === name);
261
301
  if (found) {
262
302
  process.chdir(found.path);
@@ -267,13 +307,13 @@ function handleWorktreeFlag(worktreeFlag) {
267
307
  process.stderr.write(chalk.dim(` branch ${found.branch}\n\n`));
268
308
  }
269
309
  else {
270
- createAndEnter(basePath, name);
310
+ await createAndEnter(ext, basePath, name);
271
311
  }
272
312
  }
273
- function createAndEnter(basePath, name) {
313
+ async function createAndEnter(ext, basePath, name) {
274
314
  try {
275
- const info = createWorktree(basePath, name);
276
- const hookError = runWorktreePostCreateHook(basePath, info.path);
315
+ const info = ext.createWorktree(basePath, name);
316
+ const hookError = ext.runWorktreePostCreateHook(basePath, info.path);
277
317
  if (hookError) {
278
318
  process.stderr.write(chalk.yellow(`[gsd] ${hookError}\n`));
279
319
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.31.2",
3
+ "version": "2.32.0",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gsd/pi-coding-agent",
3
- "version": "2.31.2",
3
+ "version": "2.32.0",
4
4
  "description": "Coding agent CLI (vendored from pi-mono)",
5
5
  "type": "module",
6
6
  "piConfig": {
package/pkg/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glittercowboy/gsd",
3
- "version": "2.31.2",
3
+ "version": "2.32.0",
4
4
  "piConfig": {
5
5
  "name": "gsd",
6
6
  "configDir": ".gsd"
@@ -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;
@@ -48,40 +48,34 @@ export interface AutoDashboardData {
48
48
 
49
49
  // ─── Unit Description Helpers ─────────────────────────────────────────────────
50
50
 
51
+ /** Canonical verb and phase label for each known unit type. */
52
+ const UNIT_TYPE_INFO: Record<string, { verb: string; phaseLabel: string }> = {
53
+ "research-milestone": { verb: "researching", phaseLabel: "RESEARCH" },
54
+ "research-slice": { verb: "researching", phaseLabel: "RESEARCH" },
55
+ "plan-milestone": { verb: "planning", phaseLabel: "PLAN" },
56
+ "plan-slice": { verb: "planning", phaseLabel: "PLAN" },
57
+ "execute-task": { verb: "executing", phaseLabel: "EXECUTE" },
58
+ "complete-slice": { verb: "completing", phaseLabel: "COMPLETE" },
59
+ "replan-slice": { verb: "replanning", phaseLabel: "REPLAN" },
60
+ "rewrite-docs": { verb: "rewriting", phaseLabel: "REWRITE" },
61
+ "reassess-roadmap": { verb: "reassessing", phaseLabel: "REASSESS" },
62
+ "run-uat": { verb: "running UAT", phaseLabel: "UAT" },
63
+ };
64
+
51
65
  export function unitVerb(unitType: string): string {
52
66
  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
- }
67
+ return UNIT_TYPE_INFO[unitType]?.verb ?? unitType;
66
68
  }
67
69
 
68
70
  export function unitPhaseLabel(unitType: string): string {
69
71
  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
- }
72
+ return UNIT_TYPE_INFO[unitType]?.phaseLabel ?? unitType.toUpperCase();
83
73
  }
84
74
 
75
+ /**
76
+ * Describe the expected next step after the current unit completes.
77
+ * Unit types here mirror the keys in UNIT_TYPE_INFO above.
78
+ */
85
79
  function peekNext(unitType: string, state: GSDState): string {
86
80
  // Show active hook info in progress display
87
81
  const activeHookState = getActiveHook();
@@ -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
  },
@@ -60,9 +60,31 @@ 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";
63
64
 
64
- /** Throttle STATE.md rebuilds — at most once per 30 seconds */
65
- const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
65
+ /**
66
+ * Initialize a unit dispatch: stamp the current time, set `s.currentUnit`,
67
+ * and persist the initial runtime record. Returns `startedAt` for callers
68
+ * that need the timestamp.
69
+ */
70
+ function dispatchUnit(
71
+ s: AutoSession,
72
+ basePath: string,
73
+ unitType: string,
74
+ unitId: string,
75
+ ): number {
76
+ const startedAt = Date.now();
77
+ s.currentUnit = { type: unitType, id: unitId, startedAt };
78
+ writeUnitRuntimeRecord(basePath, unitType, unitId, startedAt, {
79
+ phase: "dispatched",
80
+ wrapupWarningSent: false,
81
+ timeoutAt: null,
82
+ lastProgressAt: startedAt,
83
+ progressCount: 0,
84
+ lastProgressKind: "dispatch",
85
+ });
86
+ return startedAt;
87
+ }
66
88
 
67
89
  export interface PostUnitContext {
68
90
  s: AutoSession;
@@ -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