gsd-pi 2.13.0 → 2.14.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/README.md +3 -3
- package/dist/cli.js +1 -0
- package/dist/loader.js +50 -6
- package/dist/resource-loader.d.ts +7 -6
- package/dist/resource-loader.js +15 -8
- package/dist/resources/extensions/gsd/auto-worktree.ts +29 -183
- package/dist/resources/extensions/gsd/auto.ts +252 -370
- package/dist/resources/extensions/gsd/commands.ts +118 -34
- package/dist/resources/extensions/gsd/doctor.ts +29 -4
- package/dist/resources/extensions/gsd/git-self-heal.ts +0 -71
- package/dist/resources/extensions/gsd/git-service.ts +8 -431
- package/dist/resources/extensions/gsd/gitignore.ts +11 -4
- package/dist/resources/extensions/gsd/guided-flow.ts +141 -5
- package/dist/resources/extensions/gsd/preferences.ts +18 -17
- package/dist/resources/extensions/gsd/prompts/discuss.md +35 -0
- package/dist/resources/extensions/gsd/prompts/queue.md +7 -1
- package/dist/resources/extensions/gsd/state.ts +26 -8
- package/dist/resources/extensions/gsd/templates/state.md +0 -1
- package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +3 -2
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +35 -0
- package/dist/resources/extensions/gsd/tests/doctor-git.test.ts +22 -4
- package/dist/resources/extensions/gsd/tests/draft-promotion.test.ts +2 -1
- package/dist/resources/extensions/gsd/tests/git-self-heal.test.ts +8 -111
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +11 -770
- package/dist/resources/extensions/gsd/tests/idle-recovery.test.ts +21 -113
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +16 -82
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +29 -50
- package/dist/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -91
- package/dist/resources/extensions/gsd/tests/worktree-integration.test.ts +28 -55
- package/dist/resources/extensions/gsd/tests/worktree.test.ts +1 -426
- package/dist/resources/extensions/gsd/types.ts +0 -1
- package/dist/resources/extensions/gsd/worktree-manager.ts +7 -3
- package/dist/resources/extensions/gsd/worktree.ts +7 -65
- package/dist/resources/extensions/search-the-web/command-search-provider.ts +3 -1
- package/package.json +1 -1
- package/packages/pi-ai/dist/providers/google.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google.js +12 -4
- package/packages/pi-ai/dist/providers/google.js.map +1 -1
- package/packages/pi-ai/dist/providers/mistral.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/mistral.js +10 -2
- package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
- package/packages/pi-ai/src/providers/google.ts +20 -8
- package/packages/pi-ai/src/providers/mistral.ts +14 -2
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +10 -7
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js +4 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +12 -3
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +13 -9
- package/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +4 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +14 -3
- package/packages/pi-tui/dist/components/input.d.ts +1 -0
- package/packages/pi-tui/dist/components/input.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/input.js +10 -0
- package/packages/pi-tui/dist/components/input.js.map +1 -1
- package/packages/pi-tui/src/components/input.ts +11 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +29 -183
- package/src/resources/extensions/gsd/auto.ts +252 -370
- package/src/resources/extensions/gsd/commands.ts +118 -34
- package/src/resources/extensions/gsd/doctor.ts +29 -4
- package/src/resources/extensions/gsd/git-self-heal.ts +0 -71
- package/src/resources/extensions/gsd/git-service.ts +8 -431
- package/src/resources/extensions/gsd/gitignore.ts +11 -4
- package/src/resources/extensions/gsd/guided-flow.ts +141 -5
- package/src/resources/extensions/gsd/preferences.ts +18 -17
- package/src/resources/extensions/gsd/prompts/discuss.md +35 -0
- package/src/resources/extensions/gsd/prompts/queue.md +7 -1
- package/src/resources/extensions/gsd/state.ts +26 -8
- package/src/resources/extensions/gsd/templates/state.md +0 -1
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/doctor-git.test.ts +22 -4
- package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/git-self-heal.test.ts +8 -111
- package/src/resources/extensions/gsd/tests/git-service.test.ts +11 -770
- package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +21 -113
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +16 -82
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +29 -50
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -91
- package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +28 -55
- package/src/resources/extensions/gsd/tests/worktree.test.ts +1 -426
- package/src/resources/extensions/gsd/types.ts +0 -1
- package/src/resources/extensions/gsd/worktree-manager.ts +7 -3
- package/src/resources/extensions/gsd/worktree.ts +7 -65
- package/src/resources/extensions/search-the-web/command-search-provider.ts +3 -1
- package/dist/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +0 -282
- package/dist/resources/extensions/gsd/tests/isolation-resolver.test.ts +0 -107
- package/dist/resources/extensions/gsd/tests/orphaned-branch.test.ts +0 -353
- package/src/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +0 -282
- package/src/resources/extensions/gsd/tests/isolation-resolver.test.ts +0 -107
- package/src/resources/extensions/gsd/tests/orphaned-branch.test.ts +0 -353
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
8
|
-
import {
|
|
8
|
+
import { AuthStorage } from "@gsd/pi-coding-agent";
|
|
9
|
+
import { existsSync, readFileSync, mkdirSync } from "node:fs";
|
|
9
10
|
import { join, dirname } from "node:path";
|
|
10
11
|
import { fileURLToPath } from "node:url";
|
|
11
12
|
import { deriveState } from "./state.js";
|
|
@@ -53,10 +54,10 @@ function dispatchDoctorHeal(pi: ExtensionAPI, scope: string | undefined, reportT
|
|
|
53
54
|
|
|
54
55
|
export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
55
56
|
pi.registerCommand("gsd", {
|
|
56
|
-
description: "GSD — Get Shit Done: /gsd next|auto|stop|status|queue|prefs|hooks|doctor|migrate|remote",
|
|
57
|
+
description: "GSD — Get Shit Done: /gsd next|auto|stop|status|queue|prefs|config|hooks|doctor|migrate|remote",
|
|
57
58
|
|
|
58
59
|
getArgumentCompletions: (prefix: string) => {
|
|
59
|
-
const subcommands = ["next", "auto", "stop", "status", "queue", "discuss", "prefs", "hooks", "doctor", "migrate", "remote"];
|
|
60
|
+
const subcommands = ["next", "auto", "stop", "status", "queue", "discuss", "prefs", "config", "hooks", "doctor", "migrate", "remote"];
|
|
60
61
|
const parts = prefix.trim().split(/\s+/);
|
|
61
62
|
|
|
62
63
|
if (parts.length <= 1) {
|
|
@@ -151,6 +152,11 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
151
152
|
return;
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
if (trimmed === "config") {
|
|
156
|
+
await handleConfig(ctx);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
154
160
|
if (trimmed === "hooks") {
|
|
155
161
|
const { formatHookStatus } = await import("./post-unit-hooks.js");
|
|
156
162
|
ctx.ui.notify(formatHookStatus(), "info");
|
|
@@ -174,7 +180,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
174
180
|
}
|
|
175
181
|
|
|
176
182
|
ctx.ui.notify(
|
|
177
|
-
`Unknown: /gsd ${trimmed}. Use /gsd, /gsd next, /gsd auto, /gsd stop, /gsd status, /gsd queue, /gsd discuss, /gsd prefs
|
|
183
|
+
`Unknown: /gsd ${trimmed}. Use /gsd, /gsd next, /gsd auto, /gsd stop, /gsd status, /gsd queue, /gsd discuss, /gsd prefs, /gsd config, /gsd hooks, /gsd doctor [audit|fix|heal] [M###/S##], /gsd migrate <path>, or /gsd remote [slack|discord|status|disconnect].`,
|
|
178
184
|
"warning",
|
|
179
185
|
);
|
|
180
186
|
},
|
|
@@ -215,20 +221,16 @@ export async function fireStatusViaCommand(
|
|
|
215
221
|
async function handlePrefs(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
216
222
|
const trimmed = args.trim();
|
|
217
223
|
|
|
218
|
-
if (trimmed === "" || trimmed === "global"
|
|
224
|
+
if (trimmed === "" || trimmed === "global" || trimmed === "wizard" || trimmed === "setup"
|
|
225
|
+
|| trimmed === "wizard global" || trimmed === "setup global") {
|
|
219
226
|
await ensurePreferencesFile(getGlobalGSDPreferencesPath(), ctx, "global");
|
|
227
|
+
await handlePrefsWizard(ctx, "global");
|
|
220
228
|
return;
|
|
221
229
|
}
|
|
222
230
|
|
|
223
|
-
if (trimmed === "project") {
|
|
231
|
+
if (trimmed === "project" || trimmed === "wizard project" || trimmed === "setup project") {
|
|
224
232
|
await ensurePreferencesFile(getProjectGSDPreferencesPath(), ctx, "project");
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (trimmed === "wizard" || trimmed === "setup" || trimmed === "wizard global" || trimmed === "setup global"
|
|
229
|
-
|| trimmed === "wizard project" || trimmed === "setup project") {
|
|
230
|
-
const scope = trimmed.includes("project") ? "project" : "global";
|
|
231
|
-
await handlePrefsWizard(ctx, scope);
|
|
233
|
+
await handlePrefsWizard(ctx, "project");
|
|
232
234
|
return;
|
|
233
235
|
}
|
|
234
236
|
|
|
@@ -319,22 +321,41 @@ async function handlePrefsWizard(
|
|
|
319
321
|
const modelPhases = ["research", "planning", "execution", "completion"] as const;
|
|
320
322
|
const models: Record<string, string> = (prefs.models as Record<string, string>) ?? {};
|
|
321
323
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
324
|
+
const availableModels = ctx.modelRegistry.getAvailable();
|
|
325
|
+
if (availableModels.length > 0) {
|
|
326
|
+
const modelOptions = availableModels.map(m => `${m.id} · ${m.provider}`);
|
|
327
|
+
modelOptions.push("(keep current)", "(clear)");
|
|
328
|
+
|
|
329
|
+
for (const phase of modelPhases) {
|
|
330
|
+
const current = models[phase] ?? "";
|
|
331
|
+
const title = `Model for ${phase} phase${current ? ` (current: ${current})` : ""}:`;
|
|
332
|
+
const choice = await ctx.ui.select(title, modelOptions);
|
|
333
|
+
|
|
334
|
+
if (choice && choice !== "(keep current)") {
|
|
335
|
+
if (choice === "(clear)") {
|
|
336
|
+
delete models[phase];
|
|
337
|
+
} else {
|
|
338
|
+
models[phase] = choice.split(" · ")[0];
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
// No authenticated models available — fall back to text input
|
|
344
|
+
for (const phase of modelPhases) {
|
|
345
|
+
const current = models[phase] ?? "";
|
|
346
|
+
const input = await ctx.ui.input(
|
|
347
|
+
`Model for ${phase} phase${current ? ` (current: ${current})` : ""}:`,
|
|
348
|
+
current || "e.g. claude-sonnet-4-20250514",
|
|
349
|
+
);
|
|
350
|
+
if (input !== null && input !== undefined) {
|
|
351
|
+
const val = input.trim();
|
|
352
|
+
if (val) {
|
|
353
|
+
models[phase] = val;
|
|
354
|
+
} else if (current) {
|
|
355
|
+
delete models[phase];
|
|
356
|
+
}
|
|
335
357
|
}
|
|
336
358
|
}
|
|
337
|
-
// null/undefined = Escape/skip — keep existing value
|
|
338
359
|
}
|
|
339
360
|
if (Object.keys(models).length > 0) {
|
|
340
361
|
prefs.models = models;
|
|
@@ -452,8 +473,7 @@ function serializePreferencesToFrontmatter(prefs: Record<string, unknown>): stri
|
|
|
452
473
|
|
|
453
474
|
if (Array.isArray(value)) {
|
|
454
475
|
if (value.length === 0) {
|
|
455
|
-
|
|
456
|
-
return;
|
|
476
|
+
return; // Omit empty arrays — avoids parse/serialize cycle bug with "[]" strings
|
|
457
477
|
}
|
|
458
478
|
lines.push(`${prefix}${key}:`);
|
|
459
479
|
for (const item of value) {
|
|
@@ -484,8 +504,7 @@ function serializePreferencesToFrontmatter(prefs: Record<string, unknown>): stri
|
|
|
484
504
|
if (typeof value === "object") {
|
|
485
505
|
const entries = Object.entries(value as Record<string, unknown>);
|
|
486
506
|
if (entries.length === 0) {
|
|
487
|
-
|
|
488
|
-
return;
|
|
507
|
+
return; // Omit empty objects — avoids parse/serialize cycle bug with "{}" strings
|
|
489
508
|
}
|
|
490
509
|
lines.push(`${prefix}${key}:`);
|
|
491
510
|
for (const [k, v] of entries) {
|
|
@@ -521,6 +540,74 @@ function serializePreferencesToFrontmatter(prefs: Record<string, unknown>): stri
|
|
|
521
540
|
return lines.join("\n") + "\n";
|
|
522
541
|
}
|
|
523
542
|
|
|
543
|
+
// ─── Tool Config Wizard ───────────────────────────────────────────────────────
|
|
544
|
+
|
|
545
|
+
const TOOL_KEYS = [
|
|
546
|
+
{ id: "tavily", env: "TAVILY_API_KEY", label: "Tavily Search", hint: "tavily.com/app/api-keys" },
|
|
547
|
+
{ id: "brave", env: "BRAVE_API_KEY", label: "Brave Search", hint: "brave.com/search/api" },
|
|
548
|
+
{ id: "context7", env: "CONTEXT7_API_KEY", label: "Context7 Docs", hint: "context7.com/dashboard" },
|
|
549
|
+
{ id: "jina", env: "JINA_API_KEY", label: "Jina Page Extract", hint: "jina.ai/api" },
|
|
550
|
+
{ id: "groq", env: "GROQ_API_KEY", label: "Groq Voice", hint: "console.groq.com" },
|
|
551
|
+
] as const;
|
|
552
|
+
|
|
553
|
+
function getConfigAuthStorage(): InstanceType<typeof AuthStorage> {
|
|
554
|
+
const authPath = join(process.env.HOME ?? "", ".gsd", "agent", "auth.json");
|
|
555
|
+
mkdirSync(dirname(authPath), { recursive: true });
|
|
556
|
+
return AuthStorage.create(authPath);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async function handleConfig(ctx: ExtensionCommandContext): Promise<void> {
|
|
560
|
+
const auth = getConfigAuthStorage();
|
|
561
|
+
|
|
562
|
+
// Show current status
|
|
563
|
+
const statusLines = ["GSD Tool Configuration\n"];
|
|
564
|
+
for (const tool of TOOL_KEYS) {
|
|
565
|
+
const hasKey = !!process.env[tool.env] || !!(auth.get(tool.id) as { key?: string })?.key;
|
|
566
|
+
statusLines.push(` ${hasKey ? "✓" : "✗"} ${tool.label}${hasKey ? "" : ` — get key at ${tool.hint}`}`);
|
|
567
|
+
}
|
|
568
|
+
ctx.ui.notify(statusLines.join("\n"), "info");
|
|
569
|
+
|
|
570
|
+
// Ask which tools to configure
|
|
571
|
+
const options = TOOL_KEYS.map(t => {
|
|
572
|
+
const hasKey = !!process.env[t.env] || !!(auth.get(t.id) as { key?: string })?.key;
|
|
573
|
+
return `${t.label} ${hasKey ? "(configured ✓)" : "(not set)"}`;
|
|
574
|
+
});
|
|
575
|
+
options.push("(done)");
|
|
576
|
+
|
|
577
|
+
let changed = false;
|
|
578
|
+
while (true) {
|
|
579
|
+
const choice = await ctx.ui.select("Configure which tool? Press Escape when done.", options);
|
|
580
|
+
if (!choice || choice === "(done)") break;
|
|
581
|
+
|
|
582
|
+
const toolIdx = TOOL_KEYS.findIndex(t => choice.startsWith(t.label));
|
|
583
|
+
if (toolIdx === -1) break;
|
|
584
|
+
|
|
585
|
+
const tool = TOOL_KEYS[toolIdx];
|
|
586
|
+
const input = await ctx.ui.input(
|
|
587
|
+
`API key for ${tool.label} (${tool.hint}):`,
|
|
588
|
+
"paste your key here",
|
|
589
|
+
);
|
|
590
|
+
|
|
591
|
+
if (input !== null && input !== undefined) {
|
|
592
|
+
const key = input.trim();
|
|
593
|
+
if (key) {
|
|
594
|
+
auth.set(tool.id, { type: "api_key", key });
|
|
595
|
+
process.env[tool.env] = key;
|
|
596
|
+
ctx.ui.notify(`${tool.label} key saved and activated.`, "info");
|
|
597
|
+
// Update option label
|
|
598
|
+
options[toolIdx] = `${tool.label} (configured ✓)`;
|
|
599
|
+
changed = true;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (changed) {
|
|
605
|
+
await ctx.waitForIdle();
|
|
606
|
+
await ctx.reload();
|
|
607
|
+
ctx.ui.notify("Configuration saved. Extensions reloaded with new keys.", "info");
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
524
611
|
async function ensurePreferencesFile(
|
|
525
612
|
path: string,
|
|
526
613
|
ctx: ExtensionCommandContext,
|
|
@@ -538,7 +625,4 @@ async function ensurePreferencesFile(
|
|
|
538
625
|
ctx.ui.notify(`Using existing ${scope} GSD skill preferences at ${path}`, "info");
|
|
539
626
|
}
|
|
540
627
|
|
|
541
|
-
await ctx.waitForIdle();
|
|
542
|
-
await ctx.reload();
|
|
543
|
-
ctx.ui.notify(`Edit ${path} to update ${scope} GSD skill preferences.`, "info");
|
|
544
628
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
2
|
import { existsSync, mkdirSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { join, sep } from "node:path";
|
|
4
4
|
|
|
5
5
|
import { loadFile, parsePlan, parseRoadmap, parseSummary, saveFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
|
|
6
6
|
import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTaskFiles, resolveTasksDir, milestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile } from "./paths.js";
|
|
@@ -31,7 +31,8 @@ export type DoctorIssueCode =
|
|
|
31
31
|
| "orphaned_auto_worktree"
|
|
32
32
|
| "stale_milestone_branch"
|
|
33
33
|
| "corrupt_merge_state"
|
|
34
|
-
| "tracked_runtime_files"
|
|
34
|
+
| "tracked_runtime_files"
|
|
35
|
+
| "legacy_slice_branches";
|
|
35
36
|
|
|
36
37
|
export interface DoctorIssue {
|
|
37
38
|
severity: DoctorSeverity;
|
|
@@ -511,7 +512,7 @@ async function checkGitHealth(
|
|
|
511
512
|
if (shouldFix("orphaned_auto_worktree")) {
|
|
512
513
|
// Never remove a worktree matching current working directory
|
|
513
514
|
const cwd = process.cwd();
|
|
514
|
-
if (wt.path === cwd || cwd.startsWith(wt.path +
|
|
515
|
+
if (wt.path === cwd || cwd.startsWith(wt.path + sep)) {
|
|
515
516
|
fixesApplied.push(`skipped removing worktree at ${wt.path} (is cwd)`);
|
|
516
517
|
} else {
|
|
517
518
|
try {
|
|
@@ -527,7 +528,9 @@ async function checkGitHealth(
|
|
|
527
528
|
|
|
528
529
|
// ── Stale milestone branches ─────────────────────────────────────────
|
|
529
530
|
try {
|
|
530
|
-
|
|
531
|
+
// Use unquoted glob — single quotes are not interpreted by cmd.exe on Windows,
|
|
532
|
+
// causing the pattern to match literally instead of as a glob.
|
|
533
|
+
const branchOutput = execSync("git branch --list milestone/*", { cwd: basePath, stdio: "pipe" }).toString().trim();
|
|
531
534
|
if (branchOutput) {
|
|
532
535
|
const branches = branchOutput.split("\n").map(b => b.trim().replace(/^\*\s*/, "")).filter(Boolean);
|
|
533
536
|
const worktreeBranches = new Set(milestoneWorktrees.map(wt => wt.branch));
|
|
@@ -640,6 +643,28 @@ async function checkGitHealth(
|
|
|
640
643
|
} catch {
|
|
641
644
|
// git ls-files failed — skip
|
|
642
645
|
}
|
|
646
|
+
|
|
647
|
+
// ── Legacy slice branches ──────────────────────────────────────────────
|
|
648
|
+
try {
|
|
649
|
+
const sliceBranches = execSync('git branch --format="%(refname:short)" --list "gsd/*/*"', {
|
|
650
|
+
cwd: basePath,
|
|
651
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
652
|
+
encoding: "utf-8",
|
|
653
|
+
}).trim();
|
|
654
|
+
if (sliceBranches) {
|
|
655
|
+
const branchList = sliceBranches.split("\n").map(b => b.trim()).filter(Boolean);
|
|
656
|
+
issues.push({
|
|
657
|
+
severity: "info",
|
|
658
|
+
code: "legacy_slice_branches",
|
|
659
|
+
scope: "project",
|
|
660
|
+
unitId: "project",
|
|
661
|
+
message: `${branchList.length} legacy slice branch(es) found: ${branchList.slice(0, 3).join(", ")}${branchList.length > 3 ? "..." : ""}. These are no longer used (branchless architecture). Delete with: git branch -D ${branchList.join(" ")}`,
|
|
662
|
+
fixable: false,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
} catch {
|
|
666
|
+
// git branch list failed — skip
|
|
667
|
+
}
|
|
643
668
|
}
|
|
644
669
|
|
|
645
670
|
export async function runGSDDoctor(basePath: string, options?: { fix?: boolean; scope?: string; fixLevel?: "task" | "all" }): Promise<DoctorReport> {
|
|
@@ -83,77 +83,6 @@ export function abortAndReset(cwd: string): AbortAndResetResult {
|
|
|
83
83
|
return { cleaned };
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
/**
|
|
87
|
-
* Wrap a merge operation with self-healing retry logic.
|
|
88
|
-
*
|
|
89
|
-
* Calls `mergeFn()`. On failure:
|
|
90
|
-
* - If conflicted files exist (via `git diff --diff-filter=U`), re-throws
|
|
91
|
-
* as MergeConflictError immediately — no retry for real code conflicts.
|
|
92
|
-
* - Otherwise, runs `abortAndReset(cwd)`, retries `mergeFn()` once.
|
|
93
|
-
* - On second failure, throws the error.
|
|
94
|
-
*
|
|
95
|
-
* @param cwd - Working directory for git operations
|
|
96
|
-
* @param mergeFn - Synchronous function that performs the merge
|
|
97
|
-
* @returns The return value of `mergeFn()`
|
|
98
|
-
*/
|
|
99
|
-
export function withMergeHeal<T>(cwd: string, mergeFn: () => T): T {
|
|
100
|
-
try {
|
|
101
|
-
return mergeFn();
|
|
102
|
-
} catch (firstError) {
|
|
103
|
-
// Check for real code conflicts — escalate immediately, no retry
|
|
104
|
-
try {
|
|
105
|
-
const conflictOutput = execSync("git diff --name-only --diff-filter=U", {
|
|
106
|
-
cwd,
|
|
107
|
-
encoding: "utf-8",
|
|
108
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
109
|
-
}).trim();
|
|
110
|
-
|
|
111
|
-
if (conflictOutput.length > 0) {
|
|
112
|
-
const conflictedFiles = conflictOutput.split("\n").filter(Boolean);
|
|
113
|
-
// If the original error is already a MergeConflictError, re-throw as-is
|
|
114
|
-
if (firstError instanceof MergeConflictError) {
|
|
115
|
-
throw firstError;
|
|
116
|
-
}
|
|
117
|
-
throw new MergeConflictError(
|
|
118
|
-
conflictedFiles,
|
|
119
|
-
"merge",
|
|
120
|
-
"unknown",
|
|
121
|
-
"unknown",
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
} catch (diffErr) {
|
|
125
|
-
// If diffErr is a MergeConflictError we just created/re-threw, propagate it
|
|
126
|
-
if (diffErr instanceof MergeConflictError) throw diffErr;
|
|
127
|
-
// Otherwise git diff itself failed — proceed with retry
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// No real conflict detected — try abort+reset+retry once
|
|
131
|
-
abortAndReset(cwd);
|
|
132
|
-
|
|
133
|
-
// Retry
|
|
134
|
-
return mergeFn();
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Recover a failed checkout by resetting first, then checking out.
|
|
140
|
-
*
|
|
141
|
-
* Performs `git reset --hard HEAD` then `git checkout <targetBranch>`.
|
|
142
|
-
* If checkout still fails after reset, throws with context.
|
|
143
|
-
*/
|
|
144
|
-
export function recoverCheckout(cwd: string, targetBranch: string): void {
|
|
145
|
-
execSync("git reset --hard HEAD", { cwd, stdio: "pipe" });
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
execSync(`git checkout ${targetBranch}`, { cwd, stdio: "pipe" });
|
|
149
|
-
} catch (err) {
|
|
150
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
151
|
-
throw new Error(
|
|
152
|
-
`recoverCheckout failed: could not checkout '${targetBranch}' after reset. ${msg}`,
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
86
|
/** Known git error patterns mapped to user-friendly messages. */
|
|
158
87
|
const ERROR_PATTERNS: Array<{ pattern: RegExp; message: string }> = [
|
|
159
88
|
{
|