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.
- package/dist/cli.js +5 -5
- package/dist/resources/extensions/gsd/auto-constants.ts +6 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +20 -26
- package/dist/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
- package/dist/resources/extensions/gsd/auto-dispatch.ts +4 -8
- package/dist/resources/extensions/gsd/auto-post-unit.ts +27 -32
- package/dist/resources/extensions/gsd/auto-prompts.ts +38 -34
- package/dist/resources/extensions/gsd/auto-start.ts +8 -6
- package/dist/resources/extensions/gsd/auto.ts +54 -33
- package/dist/resources/extensions/gsd/commands-workflow-templates.ts +3 -5
- package/dist/resources/extensions/gsd/commands.ts +19 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +28 -0
- package/dist/resources/extensions/gsd/doctor-environment.ts +497 -0
- package/dist/resources/extensions/gsd/doctor-providers.ts +343 -0
- package/dist/resources/extensions/gsd/doctor-types.ts +14 -1
- package/dist/resources/extensions/gsd/doctor.ts +6 -0
- package/dist/resources/extensions/gsd/git-service.ts +9 -0
- package/dist/resources/extensions/gsd/guided-flow-queue.ts +1 -8
- package/dist/resources/extensions/gsd/health-widget.ts +167 -0
- package/dist/resources/extensions/gsd/index.ts +6 -0
- package/dist/resources/extensions/gsd/preferences-types.ts +8 -0
- package/dist/resources/extensions/gsd/preferences-validation.ts +3 -10
- package/dist/resources/extensions/gsd/progress-score.ts +273 -0
- package/dist/resources/extensions/gsd/prompts/run-uat.md +1 -42
- package/dist/resources/extensions/gsd/quick.ts +3 -5
- package/dist/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
- package/dist/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
- package/dist/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
- package/dist/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
- package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
- package/dist/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
- package/dist/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
- package/dist/resources/extensions/gsd/visualizer-data.ts +60 -2
- package/dist/resources/extensions/gsd/visualizer-views.ts +54 -0
- package/dist/worktree-cli.d.ts +42 -6
- package/dist/worktree-cli.js +88 -48
- package/package.json +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto-constants.ts +6 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +20 -26
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -6
- package/src/resources/extensions/gsd/auto-dispatch.ts +4 -8
- package/src/resources/extensions/gsd/auto-post-unit.ts +27 -32
- package/src/resources/extensions/gsd/auto-prompts.ts +38 -34
- package/src/resources/extensions/gsd/auto-start.ts +8 -6
- package/src/resources/extensions/gsd/auto.ts +54 -33
- package/src/resources/extensions/gsd/commands-workflow-templates.ts +3 -5
- package/src/resources/extensions/gsd/commands.ts +19 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +28 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +497 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +343 -0
- package/src/resources/extensions/gsd/doctor-types.ts +14 -1
- package/src/resources/extensions/gsd/doctor.ts +6 -0
- package/src/resources/extensions/gsd/git-service.ts +9 -0
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -8
- package/src/resources/extensions/gsd/health-widget.ts +167 -0
- package/src/resources/extensions/gsd/index.ts +6 -0
- package/src/resources/extensions/gsd/preferences-types.ts +8 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +3 -10
- package/src/resources/extensions/gsd/progress-score.ts +273 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +1 -42
- package/src/resources/extensions/gsd/quick.ts +3 -5
- package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +127 -0
- package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
- package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
- package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
- package/src/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +56 -7
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +60 -2
- package/src/resources/extensions/gsd/visualizer-views.ts +54 -0
package/dist/worktree-cli.js
CHANGED
|
@@ -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 {
|
|
18
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
package/pkg/package.json
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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"),
|
|
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
|
-
/**
|
|
65
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|