gsd-pi 2.23.0 → 2.24.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 +12 -3
- package/dist/headless.d.ts +4 -0
- package/dist/headless.js +118 -10
- package/dist/help-text.js +22 -7
- package/dist/resource-loader.js +64 -9
- package/dist/resources/extensions/gsd/auto-dispatch.ts +51 -2
- package/dist/resources/extensions/gsd/auto-prompts.ts +73 -0
- package/dist/resources/extensions/gsd/auto-recovery.ts +41 -2
- package/dist/resources/extensions/gsd/auto-worktree.ts +15 -3
- package/dist/resources/extensions/gsd/auto.ts +123 -41
- package/dist/resources/extensions/gsd/commands.ts +176 -10
- package/dist/resources/extensions/gsd/complexity.ts +1 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +38 -0
- package/dist/resources/extensions/gsd/doctor.ts +56 -11
- package/dist/resources/extensions/gsd/exit-command.ts +2 -2
- package/dist/resources/extensions/gsd/gitignore.ts +1 -0
- package/dist/resources/extensions/gsd/guided-flow.ts +75 -0
- package/dist/resources/extensions/gsd/index.ts +34 -1
- package/dist/resources/extensions/gsd/parallel-eligibility.ts +233 -0
- package/dist/resources/extensions/gsd/parallel-merge.ts +156 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
- package/dist/resources/extensions/gsd/preferences.ts +65 -1
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
- package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +40 -61
- package/dist/resources/extensions/gsd/provider-error-pause.ts +29 -2
- package/dist/resources/extensions/gsd/session-status-io.ts +197 -0
- package/dist/resources/extensions/gsd/state.ts +72 -30
- package/dist/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
- package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
- package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +202 -2
- package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
- package/dist/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
- package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
- package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
- package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
- package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
- package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
- package/dist/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
- package/dist/resources/extensions/gsd/types.ts +15 -1
- package/dist/resources/extensions/subagent/index.ts +5 -0
- package/dist/resources/extensions/subagent/worker-registry.ts +99 -0
- package/dist/update-check.d.ts +9 -0
- package/dist/update-check.js +97 -0
- package/package.json +6 -1
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +16 -7
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.js +12 -4
- package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-vertex.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google-vertex.js +21 -9
- package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +12 -4
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js +12 -4
- package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic.ts +21 -8
- package/packages/pi-ai/src/providers/azure-openai-responses.ts +16 -4
- package/packages/pi-ai/src/providers/google-vertex.ts +32 -17
- package/packages/pi-ai/src/providers/openai-completions.ts +16 -4
- package/packages/pi-ai/src/providers/openai-responses.ts +16 -4
- package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
- package/packages/pi-coding-agent/src/core/settings-manager.ts +2 -2
- package/scripts/postinstall.js +7 -109
- package/src/resources/extensions/gsd/auto-dispatch.ts +51 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +73 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +41 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +15 -3
- package/src/resources/extensions/gsd/auto.ts +123 -41
- package/src/resources/extensions/gsd/commands.ts +176 -10
- package/src/resources/extensions/gsd/complexity.ts +1 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +38 -0
- package/src/resources/extensions/gsd/doctor.ts +56 -11
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/gitignore.ts +1 -0
- package/src/resources/extensions/gsd/guided-flow.ts +75 -0
- package/src/resources/extensions/gsd/index.ts +34 -1
- package/src/resources/extensions/gsd/parallel-eligibility.ts +233 -0
- package/src/resources/extensions/gsd/parallel-merge.ts +156 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
- package/src/resources/extensions/gsd/preferences.ts +65 -1
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +40 -61
- package/src/resources/extensions/gsd/provider-error-pause.ts +29 -2
- package/src/resources/extensions/gsd/session-status-io.ts +197 -0
- package/src/resources/extensions/gsd/state.ts +72 -30
- package/src/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
- package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +202 -2
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
- package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
- package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
- package/src/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
- package/src/resources/extensions/gsd/types.ts +15 -1
- package/src/resources/extensions/subagent/index.ts +5 -0
- package/src/resources/extensions/subagent/worker-registry.ts +99 -0
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { loadStoredEnvKeys } from './wizard.js';
|
|
|
8
8
|
import { getPiDefaultModelAndProvider, migratePiCredentials } from './pi-migration.js';
|
|
9
9
|
import { shouldRunOnboarding, runOnboarding } from './onboarding.js';
|
|
10
10
|
import chalk from 'chalk';
|
|
11
|
-
import { checkForUpdates } from './update-check.js';
|
|
11
|
+
import { checkForUpdates, checkAndPromptForUpdates } from './update-check.js';
|
|
12
12
|
import { printHelp, printSubcommandHelp } from './help-text.js';
|
|
13
13
|
function exitIfManagedResourcesAreNewer(currentAgentDir) {
|
|
14
14
|
const currentVersion = process.env.GSD_VERSION || '0.0.0';
|
|
@@ -169,9 +169,18 @@ if (!isPrintMode && shouldRunOnboarding(authStorage, settingsManager.getDefaultP
|
|
|
169
169
|
process.stdin.setRawMode(false);
|
|
170
170
|
process.stdin.pause();
|
|
171
171
|
}
|
|
172
|
-
//
|
|
172
|
+
// Update check — interactive prompt when stdin is a TTY, passive banner otherwise
|
|
173
173
|
if (!isPrintMode) {
|
|
174
|
-
|
|
174
|
+
if (process.stdin.isTTY) {
|
|
175
|
+
const updated = await checkAndPromptForUpdates().catch(() => false);
|
|
176
|
+
if (updated) {
|
|
177
|
+
// User chose to update — exit so they relaunch with the new version
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
checkForUpdates().catch(() => { });
|
|
183
|
+
}
|
|
175
184
|
}
|
|
176
185
|
// Warn if terminal is too narrow for readable output
|
|
177
186
|
if (!isPrintMode && process.stdout.columns && process.stdout.columns < 40) {
|
package/dist/headless.d.ts
CHANGED
|
@@ -16,6 +16,10 @@ export interface HeadlessOptions {
|
|
|
16
16
|
model?: string;
|
|
17
17
|
command: string;
|
|
18
18
|
commandArgs: string[];
|
|
19
|
+
context?: string;
|
|
20
|
+
contextText?: string;
|
|
21
|
+
auto?: boolean;
|
|
22
|
+
verbose?: boolean;
|
|
19
23
|
}
|
|
20
24
|
export declare function parseHeadlessArgs(argv: string[]): HeadlessOptions;
|
|
21
25
|
export declare function runHeadless(options: HeadlessOptions): Promise<void>;
|
package/dist/headless.js
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* 1 — error or timeout
|
|
11
11
|
* 2 — blocked (command reported a blocker)
|
|
12
12
|
*/
|
|
13
|
-
import { existsSync } from 'node:fs';
|
|
14
|
-
import { join } from 'node:path';
|
|
13
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { join, resolve } from 'node:path';
|
|
15
15
|
// RpcClient is not in @gsd/pi-coding-agent's public exports — import from dist directly.
|
|
16
16
|
// This relative path resolves correctly from both src/ (via tsx) and dist/ (compiled).
|
|
17
17
|
import { RpcClient } from '../packages/pi-coding-agent/dist/modes/rpc/rpc-client.js';
|
|
@@ -46,6 +46,18 @@ export function parseHeadlessArgs(argv) {
|
|
|
46
46
|
// --model can also be passed from the main CLI; headless-specific takes precedence
|
|
47
47
|
options.model = args[++i];
|
|
48
48
|
}
|
|
49
|
+
else if (arg === '--context' && i + 1 < args.length) {
|
|
50
|
+
options.context = args[++i];
|
|
51
|
+
}
|
|
52
|
+
else if (arg === '--context-text' && i + 1 < args.length) {
|
|
53
|
+
options.contextText = args[++i];
|
|
54
|
+
}
|
|
55
|
+
else if (arg === '--auto') {
|
|
56
|
+
options.auto = true;
|
|
57
|
+
}
|
|
58
|
+
else if (arg === '--verbose') {
|
|
59
|
+
options.verbose = true;
|
|
60
|
+
}
|
|
49
61
|
}
|
|
50
62
|
else if (!positionalStarted) {
|
|
51
63
|
positionalStarted = true;
|
|
@@ -99,18 +111,23 @@ function handleExtensionUIRequest(event, writeToStdin) {
|
|
|
99
111
|
// ---------------------------------------------------------------------------
|
|
100
112
|
// Progress Formatter
|
|
101
113
|
// ---------------------------------------------------------------------------
|
|
102
|
-
function formatProgress(event) {
|
|
114
|
+
function formatProgress(event, verbose) {
|
|
103
115
|
const type = String(event.type ?? '');
|
|
104
116
|
switch (type) {
|
|
105
117
|
case 'tool_execution_start':
|
|
106
|
-
|
|
118
|
+
if (verbose)
|
|
119
|
+
return ` [tool] ${event.toolName ?? 'unknown'}`;
|
|
120
|
+
return null;
|
|
107
121
|
case 'agent_start':
|
|
108
|
-
return '[agent]
|
|
122
|
+
return '[agent] Session started';
|
|
109
123
|
case 'agent_end':
|
|
110
|
-
return '[agent]
|
|
124
|
+
return '[agent] Session ended';
|
|
111
125
|
case 'extension_ui_request':
|
|
112
126
|
if (event.method === 'notify') {
|
|
113
|
-
return `[gsd]
|
|
127
|
+
return `[gsd] ${event.message ?? ''}`;
|
|
128
|
+
}
|
|
129
|
+
if (event.method === 'setStatus') {
|
|
130
|
+
return `[status] ${event.message ?? ''}`;
|
|
114
131
|
}
|
|
115
132
|
return null;
|
|
116
133
|
default:
|
|
@@ -133,6 +150,11 @@ function isBlockedNotification(event) {
|
|
|
133
150
|
return false;
|
|
134
151
|
return String(event.message ?? '').toLowerCase().includes('blocked');
|
|
135
152
|
}
|
|
153
|
+
function isMilestoneReadyNotification(event) {
|
|
154
|
+
if (event.type !== 'extension_ui_request' || event.method !== 'notify')
|
|
155
|
+
return false;
|
|
156
|
+
return /milestone\s+m\d+.*ready/i.test(String(event.message ?? ''));
|
|
157
|
+
}
|
|
136
158
|
// ---------------------------------------------------------------------------
|
|
137
159
|
// Quick Command Detection
|
|
138
160
|
// ---------------------------------------------------------------------------
|
|
@@ -148,11 +170,69 @@ function isQuickCommand(command) {
|
|
|
148
170
|
// ---------------------------------------------------------------------------
|
|
149
171
|
// Main Orchestrator
|
|
150
172
|
// ---------------------------------------------------------------------------
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// Context Loading (new-milestone)
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
async function readStdin() {
|
|
177
|
+
const chunks = [];
|
|
178
|
+
for await (const chunk of process.stdin) {
|
|
179
|
+
chunks.push(chunk);
|
|
180
|
+
}
|
|
181
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
182
|
+
}
|
|
183
|
+
async function loadContext(options) {
|
|
184
|
+
if (options.contextText)
|
|
185
|
+
return options.contextText;
|
|
186
|
+
if (options.context === '-') {
|
|
187
|
+
return readStdin();
|
|
188
|
+
}
|
|
189
|
+
if (options.context) {
|
|
190
|
+
return readFileSync(resolve(options.context), 'utf-8');
|
|
191
|
+
}
|
|
192
|
+
throw new Error('No context provided. Use --context <file> or --context-text <text>');
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Bootstrap .gsd/ directory structure for headless new-milestone.
|
|
196
|
+
* Mirrors the bootstrap logic from guided-flow.ts showSmartEntry().
|
|
197
|
+
*/
|
|
198
|
+
function bootstrapGsdProject(basePath) {
|
|
199
|
+
const gsdDir = join(basePath, '.gsd');
|
|
200
|
+
mkdirSync(join(gsdDir, 'milestones'), { recursive: true });
|
|
201
|
+
mkdirSync(join(gsdDir, 'runtime'), { recursive: true });
|
|
202
|
+
}
|
|
151
203
|
export async function runHeadless(options) {
|
|
152
204
|
const startTime = Date.now();
|
|
153
|
-
|
|
205
|
+
const isNewMilestone = options.command === 'new-milestone';
|
|
206
|
+
// For new-milestone, load context and bootstrap .gsd/ before spawning RPC child
|
|
207
|
+
if (isNewMilestone) {
|
|
208
|
+
if (!options.context && !options.contextText) {
|
|
209
|
+
process.stderr.write('[headless] Error: new-milestone requires --context <file> or --context-text <text>\n');
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
let contextContent;
|
|
213
|
+
try {
|
|
214
|
+
contextContent = await loadContext(options);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
process.stderr.write(`[headless] Error loading context: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
// Bootstrap .gsd/ if needed
|
|
221
|
+
const gsdDir = join(process.cwd(), '.gsd');
|
|
222
|
+
if (!existsSync(gsdDir)) {
|
|
223
|
+
if (!options.json) {
|
|
224
|
+
process.stderr.write('[headless] Bootstrapping .gsd/ project structure...\n');
|
|
225
|
+
}
|
|
226
|
+
bootstrapGsdProject(process.cwd());
|
|
227
|
+
}
|
|
228
|
+
// Write context to temp file for the RPC child to read
|
|
229
|
+
const runtimeDir = join(gsdDir, 'runtime');
|
|
230
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
231
|
+
writeFileSync(join(runtimeDir, 'headless-context.md'), contextContent, 'utf-8');
|
|
232
|
+
}
|
|
233
|
+
// Validate .gsd/ directory (skip for new-milestone since we just bootstrapped it)
|
|
154
234
|
const gsdDir = join(process.cwd(), '.gsd');
|
|
155
|
-
if (!existsSync(gsdDir)) {
|
|
235
|
+
if (!isNewMilestone && !existsSync(gsdDir)) {
|
|
156
236
|
process.stderr.write('[headless] Error: No .gsd/ directory found in current directory.\n');
|
|
157
237
|
process.stderr.write("[headless] Run 'gsd' interactively first to initialize a project.\n");
|
|
158
238
|
process.exit(1);
|
|
@@ -178,6 +258,7 @@ export async function runHeadless(options) {
|
|
|
178
258
|
let blocked = false;
|
|
179
259
|
let completed = false;
|
|
180
260
|
let exitCode = 0;
|
|
261
|
+
let milestoneReady = false; // tracks "Milestone X ready." for auto-chaining
|
|
181
262
|
const recentEvents = [];
|
|
182
263
|
function trackEvent(event) {
|
|
183
264
|
totalEvents++;
|
|
@@ -231,7 +312,7 @@ export async function runHeadless(options) {
|
|
|
231
312
|
}
|
|
232
313
|
else {
|
|
233
314
|
// Progress output to stderr
|
|
234
|
-
const line = formatProgress(eventObj);
|
|
315
|
+
const line = formatProgress(eventObj, !!options.verbose);
|
|
235
316
|
if (line)
|
|
236
317
|
process.stderr.write(line + '\n');
|
|
237
318
|
}
|
|
@@ -241,6 +322,10 @@ export async function runHeadless(options) {
|
|
|
241
322
|
if (isBlockedNotification(eventObj)) {
|
|
242
323
|
blocked = true;
|
|
243
324
|
}
|
|
325
|
+
// Detect "Milestone X ready." for auto-mode chaining
|
|
326
|
+
if (isMilestoneReadyNotification(eventObj)) {
|
|
327
|
+
milestoneReady = true;
|
|
328
|
+
}
|
|
244
329
|
if (isTerminalNotification(eventObj)) {
|
|
245
330
|
completed = true;
|
|
246
331
|
}
|
|
@@ -319,6 +404,29 @@ export async function runHeadless(options) {
|
|
|
319
404
|
if (exitCode === 0 || exitCode === 2) {
|
|
320
405
|
await completionPromise;
|
|
321
406
|
}
|
|
407
|
+
// Auto-mode chaining: if --auto and milestone creation succeeded, send /gsd auto
|
|
408
|
+
if (isNewMilestone && options.auto && milestoneReady && !blocked && exitCode === 0) {
|
|
409
|
+
if (!options.json) {
|
|
410
|
+
process.stderr.write('[headless] Milestone ready — chaining into auto-mode...\n');
|
|
411
|
+
}
|
|
412
|
+
// Reset completion state for the auto-mode phase
|
|
413
|
+
completed = false;
|
|
414
|
+
milestoneReady = false;
|
|
415
|
+
blocked = false;
|
|
416
|
+
const autoCompletionPromise = new Promise((resolve) => {
|
|
417
|
+
resolveCompletion = resolve;
|
|
418
|
+
});
|
|
419
|
+
try {
|
|
420
|
+
await client.prompt('/gsd auto');
|
|
421
|
+
}
|
|
422
|
+
catch (err) {
|
|
423
|
+
process.stderr.write(`[headless] Error: Failed to start auto-mode: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
424
|
+
exitCode = 1;
|
|
425
|
+
}
|
|
426
|
+
if (exitCode === 0 || exitCode === 2) {
|
|
427
|
+
await autoCompletionPromise;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
322
430
|
// Cleanup
|
|
323
431
|
clearTimeout(timeoutTimer);
|
|
324
432
|
if (idleTimer)
|
package/dist/help-text.js
CHANGED
|
@@ -35,15 +35,30 @@ const SUBCOMMAND_HELP = {
|
|
|
35
35
|
'Run /gsd commands without the TUI. Default command: auto',
|
|
36
36
|
'',
|
|
37
37
|
'Flags:',
|
|
38
|
-
' --timeout N
|
|
39
|
-
' --json
|
|
40
|
-
' --model ID
|
|
38
|
+
' --timeout N Overall timeout in ms (default: 300000)',
|
|
39
|
+
' --json JSONL event stream to stdout',
|
|
40
|
+
' --model ID Override model',
|
|
41
|
+
'',
|
|
42
|
+
'Commands:',
|
|
43
|
+
' auto Run all queued units continuously (default)',
|
|
44
|
+
' next Run one unit',
|
|
45
|
+
' status Show progress dashboard',
|
|
46
|
+
' new-milestone Create a milestone from a specification document',
|
|
47
|
+
'',
|
|
48
|
+
'new-milestone flags:',
|
|
49
|
+
' --context <path> Path to spec/PRD file (use \'-\' for stdin)',
|
|
50
|
+
' --context-text <txt> Inline specification text',
|
|
51
|
+
' --auto Start auto-mode after milestone creation',
|
|
52
|
+
' --verbose Show tool calls in progress output',
|
|
41
53
|
'',
|
|
42
54
|
'Examples:',
|
|
43
|
-
' gsd headless
|
|
44
|
-
' gsd headless next
|
|
45
|
-
' gsd headless --json status
|
|
46
|
-
' gsd headless --timeout 60000
|
|
55
|
+
' gsd headless Run /gsd auto',
|
|
56
|
+
' gsd headless next Run one unit',
|
|
57
|
+
' gsd headless --json status Machine-readable status',
|
|
58
|
+
' gsd headless --timeout 60000 With 1-minute timeout',
|
|
59
|
+
' gsd headless new-milestone --context spec.md Create milestone from file',
|
|
60
|
+
' cat spec.md | gsd headless new-milestone --context - From stdin',
|
|
61
|
+
' gsd headless new-milestone --context spec.md --auto Create + auto-execute',
|
|
47
62
|
'',
|
|
48
63
|
'Exit codes: 0 = complete, 1 = error/timeout, 2 = blocked',
|
|
49
64
|
].join('\n'),
|
package/dist/resource-loader.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DefaultResourceLoader } from '@gsd/pi-coding-agent';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, join, relative, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { compareSemver } from './update-check.js';
|
|
@@ -113,6 +113,38 @@ export function getNewerManagedResourceVersion(agentDir, currentVersion) {
|
|
|
113
113
|
}
|
|
114
114
|
return compareSemver(managedVersion, currentVersion) > 0 ? managedVersion : null;
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* Recursively makes all files and directories under dirPath owner-writable.
|
|
118
|
+
*
|
|
119
|
+
* Files copied from the Nix store inherit read-only modes (0444/0555).
|
|
120
|
+
* Calling this before cpSync prevents overwrite failures on subsequent upgrades,
|
|
121
|
+
* and calling it after ensures the next run can overwrite the copies too.
|
|
122
|
+
*
|
|
123
|
+
* Preserves existing permission bits (including executability) and only adds
|
|
124
|
+
* owner-write (and for directories, owner-exec) without widening group/other
|
|
125
|
+
* permissions.
|
|
126
|
+
*/
|
|
127
|
+
function makeTreeWritable(dirPath) {
|
|
128
|
+
if (!existsSync(dirPath))
|
|
129
|
+
return;
|
|
130
|
+
const stats = statSync(dirPath);
|
|
131
|
+
const isDir = stats.isDirectory();
|
|
132
|
+
const currentMode = stats.mode & 0o777;
|
|
133
|
+
// Ensure owner-write; for directories also ensure owner-exec so they remain traversable.
|
|
134
|
+
let newMode = currentMode | 0o200;
|
|
135
|
+
if (isDir) {
|
|
136
|
+
newMode |= 0o100;
|
|
137
|
+
}
|
|
138
|
+
if (newMode !== currentMode) {
|
|
139
|
+
chmodSync(dirPath, newMode);
|
|
140
|
+
}
|
|
141
|
+
if (isDir) {
|
|
142
|
+
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
|
|
143
|
+
const entryPath = join(dirPath, entry.name);
|
|
144
|
+
makeTreeWritable(entryPath);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
116
148
|
/**
|
|
117
149
|
* Syncs all bundled resources to agentDir (~/.gsd/agent/) on every launch.
|
|
118
150
|
*
|
|
@@ -130,26 +162,49 @@ export function getNewerManagedResourceVersion(agentDir, currentVersion) {
|
|
|
130
162
|
*/
|
|
131
163
|
export function initResources(agentDir) {
|
|
132
164
|
mkdirSync(agentDir, { recursive: true });
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (managedVersion && managedVersion === currentVersion) {
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
// Sync extensions — overwrite so updates land on next launch
|
|
165
|
+
// Sync extensions — clean bundled subdirs first to remove stale leftover files,
|
|
166
|
+
// then overwrite so updates land on next launch. Only bundled subdirs are removed;
|
|
167
|
+
// user-created extension directories are preserved.
|
|
140
168
|
const destExtensions = join(agentDir, 'extensions');
|
|
169
|
+
makeTreeWritable(destExtensions);
|
|
170
|
+
for (const entry of readdirSync(bundledExtensionsDir, { withFileTypes: true })) {
|
|
171
|
+
if (entry.isDirectory()) {
|
|
172
|
+
const target = join(destExtensions, entry.name);
|
|
173
|
+
if (existsSync(target))
|
|
174
|
+
rmSync(target, { recursive: true, force: true });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
141
177
|
cpSync(bundledExtensionsDir, destExtensions, { recursive: true, force: true });
|
|
178
|
+
makeTreeWritable(destExtensions);
|
|
142
179
|
// Sync agents
|
|
143
180
|
const destAgents = join(agentDir, 'agents');
|
|
144
181
|
const srcAgents = join(resourcesDir, 'agents');
|
|
145
182
|
if (existsSync(srcAgents)) {
|
|
183
|
+
makeTreeWritable(destAgents);
|
|
184
|
+
for (const entry of readdirSync(srcAgents, { withFileTypes: true })) {
|
|
185
|
+
if (entry.isDirectory()) {
|
|
186
|
+
const target = join(destAgents, entry.name);
|
|
187
|
+
if (existsSync(target))
|
|
188
|
+
rmSync(target, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
146
191
|
cpSync(srcAgents, destAgents, { recursive: true, force: true });
|
|
192
|
+
makeTreeWritable(destAgents);
|
|
147
193
|
}
|
|
148
|
-
// Sync skills
|
|
194
|
+
// Sync skills
|
|
149
195
|
const destSkills = join(agentDir, 'skills');
|
|
150
196
|
const srcSkills = join(resourcesDir, 'skills');
|
|
151
197
|
if (existsSync(srcSkills)) {
|
|
198
|
+
makeTreeWritable(destSkills);
|
|
199
|
+
for (const entry of readdirSync(srcSkills, { withFileTypes: true })) {
|
|
200
|
+
if (entry.isDirectory()) {
|
|
201
|
+
const target = join(destSkills, entry.name);
|
|
202
|
+
if (existsSync(target))
|
|
203
|
+
rmSync(target, { recursive: true, force: true });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
152
206
|
cpSync(srcSkills, destSkills, { recursive: true, force: true });
|
|
207
|
+
makeTreeWritable(destSkills);
|
|
153
208
|
}
|
|
154
209
|
writeManagedResourceManifest(agentDir);
|
|
155
210
|
}
|
|
@@ -14,9 +14,11 @@ import type { GSDPreferences } from "./preferences.js";
|
|
|
14
14
|
import type { UatType } from "./files.js";
|
|
15
15
|
import { loadFile, extractUatType, loadActiveOverrides } from "./files.js";
|
|
16
16
|
import {
|
|
17
|
-
resolveMilestoneFile, resolveSliceFile,
|
|
18
|
-
relSliceFile,
|
|
17
|
+
resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveTaskFile,
|
|
18
|
+
relSliceFile, buildMilestoneFileName,
|
|
19
19
|
} from "./paths.js";
|
|
20
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
20
22
|
import {
|
|
21
23
|
buildResearchMilestonePrompt,
|
|
22
24
|
buildPlanMilestonePrompt,
|
|
@@ -25,6 +27,7 @@ import {
|
|
|
25
27
|
buildExecuteTaskPrompt,
|
|
26
28
|
buildCompleteSlicePrompt,
|
|
27
29
|
buildCompleteMilestonePrompt,
|
|
30
|
+
buildValidateMilestonePrompt,
|
|
28
31
|
buildReplanSlicePrompt,
|
|
29
32
|
buildRunUatPrompt,
|
|
30
33
|
buildReassessRoadmapPrompt,
|
|
@@ -246,6 +249,20 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|
|
246
249
|
const sTitle = state.activeSlice!.title;
|
|
247
250
|
const tid = state.activeTask.id;
|
|
248
251
|
const tTitle = state.activeTask.title;
|
|
252
|
+
|
|
253
|
+
// Guard: refuse to dispatch execute-task when the task plan file is missing.
|
|
254
|
+
// This prevents the agent from running blind after a failed plan-slice that
|
|
255
|
+
// wrote S{sid}-PLAN.md but omitted the individual T{tid}-PLAN.md files.
|
|
256
|
+
// (See issue #739 — missing task plan caused runaway execution and EPIPE crash.)
|
|
257
|
+
const taskPlanPath = resolveTaskFile(basePath, mid, sid, tid, "PLAN");
|
|
258
|
+
if (!taskPlanPath || !existsSync(taskPlanPath)) {
|
|
259
|
+
return {
|
|
260
|
+
action: "stop",
|
|
261
|
+
reason: `Task plan ${tid}-PLAN.md is missing for ${mid}/${sid}/${tid}. Re-run plan-slice to regenerate task plans, or create the file manually and resume.`,
|
|
262
|
+
level: "error",
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
249
266
|
return {
|
|
250
267
|
action: "dispatch",
|
|
251
268
|
unitType: "execute-task",
|
|
@@ -254,6 +271,38 @@ const DISPATCH_RULES: DispatchRule[] = [
|
|
|
254
271
|
};
|
|
255
272
|
},
|
|
256
273
|
},
|
|
274
|
+
{
|
|
275
|
+
name: "validating-milestone → validate-milestone",
|
|
276
|
+
match: async ({ state, mid, midTitle, basePath, prefs }) => {
|
|
277
|
+
if (state.phase !== "validating-milestone") return null;
|
|
278
|
+
// Skip preference: write a minimal pass-through VALIDATION file
|
|
279
|
+
if (prefs?.phases?.skip_milestone_validation) {
|
|
280
|
+
const mDir = resolveMilestonePath(basePath, mid);
|
|
281
|
+
if (mDir) {
|
|
282
|
+
if (!existsSync(mDir)) mkdirSync(mDir, { recursive: true });
|
|
283
|
+
const validationPath = join(mDir, buildMilestoneFileName(mid, "VALIDATION"));
|
|
284
|
+
const content = [
|
|
285
|
+
"---",
|
|
286
|
+
"verdict: pass",
|
|
287
|
+
"remediation_round: 0",
|
|
288
|
+
"---",
|
|
289
|
+
"",
|
|
290
|
+
"# Milestone Validation (skipped by preference)",
|
|
291
|
+
"",
|
|
292
|
+
"Milestone validation was skipped via `skip_milestone_validation` preference.",
|
|
293
|
+
].join("\n");
|
|
294
|
+
writeFileSync(validationPath, content, "utf-8");
|
|
295
|
+
}
|
|
296
|
+
return { action: "skip" };
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
action: "dispatch",
|
|
300
|
+
unitType: "validate-milestone",
|
|
301
|
+
unitId: mid,
|
|
302
|
+
prompt: await buildValidateMilestonePrompt(mid, midTitle, basePath),
|
|
303
|
+
};
|
|
304
|
+
},
|
|
305
|
+
},
|
|
257
306
|
{
|
|
258
307
|
name: "completing-milestone → complete-milestone",
|
|
259
308
|
match: async ({ state, mid, midTitle, basePath }) => {
|
|
@@ -855,6 +855,79 @@ export async function buildCompleteMilestonePrompt(
|
|
|
855
855
|
});
|
|
856
856
|
}
|
|
857
857
|
|
|
858
|
+
export async function buildValidateMilestonePrompt(
|
|
859
|
+
mid: string, midTitle: string, base: string, level?: InlineLevel,
|
|
860
|
+
): Promise<string> {
|
|
861
|
+
const inlineLevel = level ?? resolveInlineLevel();
|
|
862
|
+
const roadmapPath = resolveMilestoneFile(base, mid, "ROADMAP");
|
|
863
|
+
const roadmapRel = relMilestoneFile(base, mid, "ROADMAP");
|
|
864
|
+
|
|
865
|
+
const inlined: string[] = [];
|
|
866
|
+
inlined.push(await inlineFile(roadmapPath, roadmapRel, "Milestone Roadmap"));
|
|
867
|
+
|
|
868
|
+
// Inline all slice summaries and UAT results
|
|
869
|
+
const roadmapContent = roadmapPath ? await loadFile(roadmapPath) : null;
|
|
870
|
+
if (roadmapContent) {
|
|
871
|
+
const roadmap = parseRoadmap(roadmapContent);
|
|
872
|
+
const seenSlices = new Set<string>();
|
|
873
|
+
for (const slice of roadmap.slices) {
|
|
874
|
+
if (seenSlices.has(slice.id)) continue;
|
|
875
|
+
seenSlices.add(slice.id);
|
|
876
|
+
const summaryPath = resolveSliceFile(base, mid, slice.id, "SUMMARY");
|
|
877
|
+
const summaryRel = relSliceFile(base, mid, slice.id, "SUMMARY");
|
|
878
|
+
inlined.push(await inlineFile(summaryPath, summaryRel, `${slice.id} Summary`));
|
|
879
|
+
|
|
880
|
+
const uatPath = resolveSliceFile(base, mid, slice.id, "UAT-RESULT");
|
|
881
|
+
const uatRel = relSliceFile(base, mid, slice.id, "UAT-RESULT");
|
|
882
|
+
const uatInline = await inlineFileOptional(uatPath, uatRel, `${slice.id} UAT Result`);
|
|
883
|
+
if (uatInline) inlined.push(uatInline);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Inline existing VALIDATION file if this is a re-validation round
|
|
888
|
+
const validationPath = resolveMilestoneFile(base, mid, "VALIDATION");
|
|
889
|
+
const validationRel = relMilestoneFile(base, mid, "VALIDATION");
|
|
890
|
+
const validationContent = validationPath ? await loadFile(validationPath) : null;
|
|
891
|
+
let remediationRound = 0;
|
|
892
|
+
if (validationContent) {
|
|
893
|
+
const roundMatch = validationContent.match(/remediation_round:\s*(\d+)/);
|
|
894
|
+
remediationRound = roundMatch ? parseInt(roundMatch[1], 10) + 1 : 1;
|
|
895
|
+
inlined.push(`### Previous Validation (re-validation round ${remediationRound})\nSource: \`${validationRel}\`\n\n${validationContent.trim()}`);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// Inline root GSD files
|
|
899
|
+
if (inlineLevel !== "minimal") {
|
|
900
|
+
const requirementsInline = await inlineRequirementsFromDb(base);
|
|
901
|
+
if (requirementsInline) inlined.push(requirementsInline);
|
|
902
|
+
const decisionsInline = await inlineDecisionsFromDb(base, mid);
|
|
903
|
+
if (decisionsInline) inlined.push(decisionsInline);
|
|
904
|
+
const projectInline = await inlineProjectFromDb(base);
|
|
905
|
+
if (projectInline) inlined.push(projectInline);
|
|
906
|
+
}
|
|
907
|
+
const knowledgeInline = await inlineGsdRootFile(base, "knowledge.md", "Project Knowledge");
|
|
908
|
+
if (knowledgeInline) inlined.push(knowledgeInline);
|
|
909
|
+
// Inline milestone context file
|
|
910
|
+
const contextPath = resolveMilestoneFile(base, mid, "CONTEXT");
|
|
911
|
+
const contextRel = relMilestoneFile(base, mid, "CONTEXT");
|
|
912
|
+
const contextInline = await inlineFileOptional(contextPath, contextRel, "Milestone Context");
|
|
913
|
+
if (contextInline) inlined.push(contextInline);
|
|
914
|
+
|
|
915
|
+
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
916
|
+
|
|
917
|
+
const validationOutputPath = join(base, `${relMilestonePath(base, mid)}/${mid}-VALIDATION.md`);
|
|
918
|
+
const roadmapOutputPath = `${relMilestonePath(base, mid)}/${mid}-ROADMAP.md`;
|
|
919
|
+
|
|
920
|
+
return loadPrompt("validate-milestone", {
|
|
921
|
+
workingDirectory: base,
|
|
922
|
+
milestoneId: mid,
|
|
923
|
+
milestoneTitle: midTitle,
|
|
924
|
+
roadmapPath: roadmapOutputPath,
|
|
925
|
+
inlinedContext,
|
|
926
|
+
validationPath: validationOutputPath,
|
|
927
|
+
remediationRound: String(remediationRound),
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
858
931
|
export async function buildReplanSlicePrompt(
|
|
859
932
|
mid: string, midTitle: string, sid: string, sTitle: string, base: string,
|
|
860
933
|
): Promise<string> {
|
|
@@ -11,7 +11,7 @@ import type { ExtensionContext } from "@gsd/pi-coding-agent";
|
|
|
11
11
|
import {
|
|
12
12
|
clearUnitRuntimeRecord,
|
|
13
13
|
} from "./unit-runtime.js";
|
|
14
|
-
import { clearParseCache } from "./files.js";
|
|
14
|
+
import { clearParseCache, parseRoadmap, parsePlan } from "./files.js";
|
|
15
15
|
import {
|
|
16
16
|
nativeConflictFiles,
|
|
17
17
|
nativeCommit,
|
|
@@ -36,7 +36,6 @@ import {
|
|
|
36
36
|
clearPathCache,
|
|
37
37
|
resolveGsdRootFile,
|
|
38
38
|
} from "./paths.js";
|
|
39
|
-
import { parseRoadmap } from "./files.js";
|
|
40
39
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, renameSync } from "node:fs";
|
|
41
40
|
import { dirname, join } from "node:path";
|
|
42
41
|
|
|
@@ -83,6 +82,10 @@ export function resolveExpectedArtifactPath(unitType: string, unitId: string, ba
|
|
|
83
82
|
const dir = resolveSlicePath(base, mid, sid!);
|
|
84
83
|
return dir ? join(dir, buildSliceFileName(sid!, "SUMMARY")) : null;
|
|
85
84
|
}
|
|
85
|
+
case "validate-milestone": {
|
|
86
|
+
const dir = resolveMilestonePath(base, mid);
|
|
87
|
+
return dir ? join(dir, buildMilestoneFileName(mid, "VALIDATION")) : null;
|
|
88
|
+
}
|
|
86
89
|
case "complete-milestone": {
|
|
87
90
|
const dir = resolveMilestonePath(base, mid);
|
|
88
91
|
return dir ? join(dir, buildMilestoneFileName(mid, "SUMMARY")) : null;
|
|
@@ -157,6 +160,31 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
|
|
|
157
160
|
}
|
|
158
161
|
}
|
|
159
162
|
|
|
163
|
+
// plan-slice must also produce individual task plan files for every task listed
|
|
164
|
+
// in the slice plan. Without this check, a plan-slice that wrote S{sid}-PLAN.md
|
|
165
|
+
// but omitted T{tid}-PLAN.md files would be marked complete, causing execute-task
|
|
166
|
+
// to dispatch with a missing task plan (see issue #739).
|
|
167
|
+
if (unitType === "plan-slice") {
|
|
168
|
+
const parts = unitId.split("/");
|
|
169
|
+
const mid = parts[0];
|
|
170
|
+
const sid = parts[1];
|
|
171
|
+
if (mid && sid) {
|
|
172
|
+
try {
|
|
173
|
+
const planContent = readFileSync(absPath, "utf-8");
|
|
174
|
+
const plan = parsePlan(planContent);
|
|
175
|
+
const tasksDir = resolveTasksDir(base, mid, sid);
|
|
176
|
+
if (plan.tasks.length > 0 && tasksDir) {
|
|
177
|
+
for (const task of plan.tasks) {
|
|
178
|
+
const taskPlanFile = join(tasksDir, `${task.id}-PLAN.md`);
|
|
179
|
+
if (!existsSync(taskPlanFile)) return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
// Parse failure — don't block; slice plan may have non-standard format
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
160
188
|
// complete-slice must also produce a UAT file AND mark the slice [x] in the roadmap.
|
|
161
189
|
// Without the roadmap check, a crash after writing SUMMARY+UAT but before updating
|
|
162
190
|
// the roadmap causes an infinite skip loop: the idempotency key says "done" but the
|
|
@@ -244,6 +272,8 @@ export function diagnoseExpectedArtifact(unitType: string, unitId: string, base:
|
|
|
244
272
|
return `${relSliceFile(base, mid!, sid!, "ASSESSMENT")} (roadmap reassessment)`;
|
|
245
273
|
case "run-uat":
|
|
246
274
|
return `${relSliceFile(base, mid!, sid!, "UAT-RESULT")} (UAT result)`;
|
|
275
|
+
case "validate-milestone":
|
|
276
|
+
return `${relMilestoneFile(base, mid!, "VALIDATION")} (milestone validation report)`;
|
|
247
277
|
case "complete-milestone":
|
|
248
278
|
return `${relMilestoneFile(base, mid!, "SUMMARY")} (milestone summary)`;
|
|
249
279
|
default:
|
|
@@ -537,6 +567,15 @@ export function buildLoopRemediationSteps(unitType: string, unitId: string, base
|
|
|
537
567
|
` 4. Resume auto-mode`,
|
|
538
568
|
].join("\n");
|
|
539
569
|
}
|
|
570
|
+
case "validate-milestone": {
|
|
571
|
+
if (!mid) break;
|
|
572
|
+
const artifactRel = relMilestoneFile(base, mid, "VALIDATION");
|
|
573
|
+
return [
|
|
574
|
+
` 1. Write ${artifactRel} with verdict: pass`,
|
|
575
|
+
` 2. Run \`gsd doctor\``,
|
|
576
|
+
` 3. Resume auto-mode`,
|
|
577
|
+
].join("\n");
|
|
578
|
+
}
|
|
540
579
|
default:
|
|
541
580
|
break;
|
|
542
581
|
}
|
|
@@ -158,7 +158,15 @@ export function createAutoWorktree(basePath: string, milestoneId: string): strin
|
|
|
158
158
|
// Planning artifacts may be untracked if the project's .gitignore had a
|
|
159
159
|
// blanket .gsd/ rule (pre-v2.14.0). Without this copy, auto-mode loops
|
|
160
160
|
// on plan-slice because the plan file doesn't exist in the worktree.
|
|
161
|
-
|
|
161
|
+
//
|
|
162
|
+
// IMPORTANT: Skip when re-attaching to an existing branch (#759).
|
|
163
|
+
// The branch checkout already has committed artifacts with correct state
|
|
164
|
+
// (e.g. [x] for completed slices). Copying from the project root would
|
|
165
|
+
// overwrite them with stale data ([ ] checkboxes) because the root is
|
|
166
|
+
// not always fully synced.
|
|
167
|
+
if (!branchExists) {
|
|
168
|
+
copyPlanningArtifacts(basePath, info.path);
|
|
169
|
+
}
|
|
162
170
|
|
|
163
171
|
// Run user-configured post-create hook (#597) — e.g. copy .env, symlink assets
|
|
164
172
|
const hookError = runWorktreePostCreateHook(basePath, info.path);
|
|
@@ -429,8 +437,12 @@ export function mergeMilestoneToMain(
|
|
|
429
437
|
const integrationBranch = readIntegrationBranch(originalBasePath_, milestoneId);
|
|
430
438
|
const mainBranch = integrationBranch ?? prefs.main_branch ?? "main";
|
|
431
439
|
|
|
432
|
-
// 5. Checkout integration branch
|
|
433
|
-
|
|
440
|
+
// 5. Checkout integration branch (skip if already current — avoids git error
|
|
441
|
+
// when main is already checked out in the project-root worktree, #757)
|
|
442
|
+
const currentBranchAtBase = nativeGetCurrentBranch(originalBasePath_);
|
|
443
|
+
if (currentBranchAtBase !== mainBranch) {
|
|
444
|
+
nativeCheckoutBranch(originalBasePath_, mainBranch);
|
|
445
|
+
}
|
|
434
446
|
|
|
435
447
|
// 6. Build rich commit message
|
|
436
448
|
const milestoneTitle = roadmap.title.replace(/^M\d+:\s*/, "").trim() || milestoneId;
|