gsd-pi 2.8.3 → 2.9.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 +2 -1
- package/dist/cli.js +5 -0
- package/dist/loader.js +1 -1
- package/dist/update-check.d.ts +24 -0
- package/dist/update-check.js +93 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.d.ts +46 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.js +758 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.d.ts +23 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.js +267 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.d.ts +17 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.js +101 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.d.ts +15 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.js +46 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.d.ts +35 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.js +709 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.js +308 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.d.ts +34 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.js +136 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.d.ts +262 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.js +64 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.d.ts +50 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.js +574 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.js +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts +13 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js +4 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +10 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +2 -2
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +46 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/client.ts +880 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/config.ts +325 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/defaults.json +456 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/edits.ts +109 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/helpers.ts +54 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/index.ts +943 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +407 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lsp.md +33 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lspmux.ts +199 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/types.ts +421 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/lsp/utils.ts +682 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/slash-commands.ts +1 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/index.ts +10 -0
- package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +2 -2
- package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +59 -2
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +46 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js +758 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/config.d.ts +23 -0
- package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/config.js +267 -0
- package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/edits.d.ts +17 -0
- package/packages/pi-coding-agent/dist/core/lsp/edits.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/edits.js +101 -0
- package/packages/pi-coding-agent/dist/core/lsp/edits.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/helpers.d.ts +15 -0
- package/packages/pi-coding-agent/dist/core/lsp/helpers.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/helpers.js +46 -0
- package/packages/pi-coding-agent/dist/core/lsp/helpers.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.js +709 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js +308 -0
- package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/lspmux.d.ts +34 -0
- package/packages/pi-coding-agent/dist/core/lsp/lspmux.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/lspmux.js +136 -0
- package/packages/pi-coding-agent/dist/core/lsp/lspmux.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +262 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.js +64 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +50 -0
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/lsp/utils.js +574 -0
- package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
- package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +13 -0
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js +4 -0
- package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +10 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +2 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
- 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 +46 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/lsp/client.ts +880 -0
- package/packages/pi-coding-agent/src/core/lsp/config.ts +325 -0
- package/packages/pi-coding-agent/src/core/lsp/defaults.json +456 -0
- package/packages/pi-coding-agent/src/core/lsp/edits.ts +109 -0
- package/packages/pi-coding-agent/src/core/lsp/helpers.ts +54 -0
- package/packages/pi-coding-agent/src/core/lsp/index.ts +943 -0
- package/packages/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +407 -0
- package/packages/pi-coding-agent/src/core/lsp/lsp.md +33 -0
- package/packages/pi-coding-agent/src/core/lsp/lspmux.ts +199 -0
- package/packages/pi-coding-agent/src/core/lsp/types.ts +421 -0
- package/packages/pi-coding-agent/src/core/lsp/utils.ts +682 -0
- package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
- package/packages/pi-coding-agent/src/core/tools/index.ts +10 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +2 -2
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +59 -2
- package/src/resources/extensions/ask-user-questions.ts +2 -2
- package/src/resources/extensions/bg-shell/index.ts +34 -37
- package/src/resources/extensions/browser-tools/core.d.ts +205 -0
- package/src/resources/extensions/browser-tools/index.ts +2 -2
- package/src/resources/extensions/browser-tools/refs.ts +1 -1
- package/src/resources/extensions/browser-tools/tools/session.ts +1 -1
- package/src/resources/extensions/context7/index.ts +2 -2
- package/src/resources/extensions/get-secrets-from-user.ts +3 -2
- package/src/resources/extensions/google-search/index.ts +1 -1
- package/src/resources/extensions/gsd/auto.ts +41 -4
- package/src/resources/extensions/gsd/commands.ts +218 -3
- package/src/resources/extensions/gsd/doctor.ts +1 -1
- package/src/resources/extensions/gsd/git-service.ts +116 -4
- package/src/resources/extensions/gsd/guided-flow.ts +19 -9
- package/src/resources/extensions/gsd/index.ts +17 -7
- package/src/resources/extensions/gsd/preferences.ts +1 -1
- package/src/resources/extensions/gsd/tests/git-service.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/migrate-command.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +10 -10
- package/src/resources/extensions/gsd/tests/next-milestone-id.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/worktree.test.ts +352 -0
- package/src/resources/extensions/gsd/types.ts +1 -0
- package/src/resources/extensions/gsd/worktree.ts +20 -1
- package/src/resources/extensions/mac-tools/index.ts +1 -1
- package/src/resources/extensions/search-the-web/format.ts +1 -1
- package/src/resources/extensions/search-the-web/index.ts +5 -5
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +7 -7
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +11 -11
- package/src/resources/extensions/search-the-web/tool-search.ts +10 -10
- package/src/resources/extensions/shared/interview-ui.ts +2 -2
|
@@ -160,7 +160,7 @@ async function collectOneSecret(
|
|
|
160
160
|
): Promise<string | null> {
|
|
161
161
|
if (!ctx.hasUI) return null;
|
|
162
162
|
|
|
163
|
-
return ctx.ui.custom
|
|
163
|
+
return ctx.ui.custom((tui: any, theme: any, _kb: any, done: (r: string | null) => void) => {
|
|
164
164
|
let value = "";
|
|
165
165
|
let cachedLines: string[] | undefined;
|
|
166
166
|
|
|
@@ -286,7 +286,7 @@ export async function showSecretsSummary(
|
|
|
286
286
|
|
|
287
287
|
const existingSet = new Set(existingKeys);
|
|
288
288
|
|
|
289
|
-
await ctx.ui.custom
|
|
289
|
+
await (ctx.ui.custom as Function)((tui: any, theme: Theme, _kb: any, done: () => void) => {
|
|
290
290
|
let cachedLines: string[] | undefined;
|
|
291
291
|
|
|
292
292
|
function handleInput(_data: string) {
|
|
@@ -549,6 +549,7 @@ export default function secureEnv(pi: ExtensionAPI) {
|
|
|
549
549
|
return {
|
|
550
550
|
content: [{ type: "text", text: "Error: UI not available (interactive mode required for secure env collection)." }],
|
|
551
551
|
isError: true,
|
|
552
|
+
details: undefined as unknown,
|
|
552
553
|
};
|
|
553
554
|
}
|
|
554
555
|
|
|
@@ -261,7 +261,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
261
261
|
const d = result.details as SearchDetails | undefined;
|
|
262
262
|
|
|
263
263
|
if (isPartial) return new Text(theme.fg("warning", "Searching Google..."), 0, 0);
|
|
264
|
-
if (result.isError || d?.error) {
|
|
264
|
+
if ((result as any).isError || d?.error) {
|
|
265
265
|
return new Text(theme.fg("error", `Error: ${d?.error ?? "unknown"}`), 0, 0);
|
|
266
266
|
}
|
|
267
267
|
|
|
@@ -60,10 +60,12 @@ import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync } from
|
|
|
60
60
|
import { execSync, execFileSync } from "node:child_process";
|
|
61
61
|
import {
|
|
62
62
|
autoCommitCurrentBranch,
|
|
63
|
+
captureIntegrationBranch,
|
|
63
64
|
ensureSliceBranch,
|
|
64
65
|
getCurrentBranch,
|
|
65
66
|
getMainBranch,
|
|
66
67
|
parseSliceBranch,
|
|
68
|
+
setActiveMilestoneId,
|
|
67
69
|
switchToMain,
|
|
68
70
|
mergeSliceToMain,
|
|
69
71
|
} from "./worktree.ts";
|
|
@@ -361,6 +363,8 @@ export async function startAuto(
|
|
|
361
363
|
unitDispatchCount.clear();
|
|
362
364
|
// Re-initialize metrics in case ledger was lost during pause
|
|
363
365
|
if (!getLedger()) initMetrics(base);
|
|
366
|
+
// Ensure milestone ID is set on git service for integration branch resolution
|
|
367
|
+
if (currentMilestoneId) setActiveMilestoneId(base, currentMilestoneId);
|
|
364
368
|
ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto");
|
|
365
369
|
ctx.ui.setFooter(hideFooter);
|
|
366
370
|
ctx.ui.notify(stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
|
|
@@ -468,6 +472,15 @@ export async function startAuto(
|
|
|
468
472
|
originalModelId = ctx.model?.id ?? null;
|
|
469
473
|
originalModelProvider = ctx.model?.provider ?? null;
|
|
470
474
|
|
|
475
|
+
// Capture the integration branch — records the branch the user was on when
|
|
476
|
+
// auto-mode started. Slice branches will merge back to this branch instead
|
|
477
|
+
// of the repo's default (main/master). Idempotent: only writes if not
|
|
478
|
+
// already recorded, so restarts/resumes don't overwrite.
|
|
479
|
+
if (currentMilestoneId) {
|
|
480
|
+
captureIntegrationBranch(base, currentMilestoneId);
|
|
481
|
+
setActiveMilestoneId(base, currentMilestoneId);
|
|
482
|
+
}
|
|
483
|
+
|
|
471
484
|
// Initialize metrics — loads existing ledger from disk
|
|
472
485
|
initMetrics(base);
|
|
473
486
|
|
|
@@ -1002,8 +1015,13 @@ async function dispatchNextUnit(
|
|
|
1002
1015
|
// Reset stuck detection for new milestone
|
|
1003
1016
|
unitDispatchCount.clear();
|
|
1004
1017
|
unitRecoveryCount.clear();
|
|
1018
|
+
// Capture integration branch for the new milestone and update git service
|
|
1019
|
+
captureIntegrationBranch(basePath, mid);
|
|
1020
|
+
}
|
|
1021
|
+
if (mid) {
|
|
1022
|
+
currentMilestoneId = mid;
|
|
1023
|
+
setActiveMilestoneId(basePath, mid);
|
|
1005
1024
|
}
|
|
1006
|
-
if (mid) currentMilestoneId = mid;
|
|
1007
1025
|
|
|
1008
1026
|
if (!mid) {
|
|
1009
1027
|
// Save final session before stopping
|
|
@@ -1016,6 +1034,14 @@ async function dispatchNextUnit(
|
|
|
1016
1034
|
return;
|
|
1017
1035
|
}
|
|
1018
1036
|
|
|
1037
|
+
// Guard: mid/midTitle must be defined strings from this point onward.
|
|
1038
|
+
// The !mid check above returns early if mid is falsy; midTitle comes from
|
|
1039
|
+
// the same object so it should always be present when mid is.
|
|
1040
|
+
if (!midTitle) {
|
|
1041
|
+
await stopAuto(ctx, pi);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1019
1045
|
// ── General merge guard: merge completed slice branches before advancing ──
|
|
1020
1046
|
// If we're on a gsd/MID/SID branch and that slice is done (roadmap [x]),
|
|
1021
1047
|
// merge to main before dispatching the next unit. This handles:
|
|
@@ -1088,6 +1114,17 @@ async function dispatchNextUnit(
|
|
|
1088
1114
|
}
|
|
1089
1115
|
}
|
|
1090
1116
|
|
|
1117
|
+
// After merge, mid/midTitle may have been re-derived and could be undefined
|
|
1118
|
+
if (!mid || !midTitle) {
|
|
1119
|
+
if (currentUnit) {
|
|
1120
|
+
const modelId = ctx.model?.id ?? "unknown";
|
|
1121
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1122
|
+
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1123
|
+
}
|
|
1124
|
+
await stopAuto(ctx, pi);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1091
1128
|
// Determine next unit
|
|
1092
1129
|
let unitType: string;
|
|
1093
1130
|
let unitId: string;
|
|
@@ -1522,9 +1559,9 @@ async function dispatchNextUnit(
|
|
|
1522
1559
|
// soft timeout; only idle/stalled tasks pause early.
|
|
1523
1560
|
clearUnitTimeout();
|
|
1524
1561
|
const supervisor = resolveAutoSupervisorConfig();
|
|
1525
|
-
const softTimeoutMs = supervisor.soft_timeout_minutes * 60 * 1000;
|
|
1526
|
-
const idleTimeoutMs = supervisor.idle_timeout_minutes * 60 * 1000;
|
|
1527
|
-
const hardTimeoutMs = supervisor.hard_timeout_minutes * 60 * 1000;
|
|
1562
|
+
const softTimeoutMs = (supervisor.soft_timeout_minutes ?? 0) * 60 * 1000;
|
|
1563
|
+
const idleTimeoutMs = (supervisor.idle_timeout_minutes ?? 0) * 60 * 1000;
|
|
1564
|
+
const hardTimeoutMs = (supervisor.hard_timeout_minutes ?? 0) * 60 * 1000;
|
|
1528
1565
|
|
|
1529
1566
|
wrapupWarningHandle = setTimeout(() => {
|
|
1530
1567
|
wrapupWarningHandle = null;
|
|
@@ -74,7 +74,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
74
74
|
|
|
75
75
|
if (parts[0] === "prefs" && parts.length <= 2) {
|
|
76
76
|
const subPrefix = parts[1] ?? "";
|
|
77
|
-
return ["global", "project", "status"]
|
|
77
|
+
return ["global", "project", "status", "wizard", "setup"]
|
|
78
78
|
.filter((cmd) => cmd.startsWith(subPrefix))
|
|
79
79
|
.map((cmd) => ({ value: `prefs ${cmd}`, label: cmd }));
|
|
80
80
|
}
|
|
@@ -168,7 +168,7 @@ export function registerGSDCommand(pi: ExtensionAPI): void {
|
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
ctx.ui.notify(
|
|
171
|
-
`Unknown: /gsd ${trimmed}. Use /gsd, /gsd next, /gsd auto, /gsd stop, /gsd status, /gsd queue, /gsd discuss, /gsd prefs [global|project|status], /gsd doctor [audit|fix|heal] [M###/S##], /gsd migrate <path>, or /gsd remote [slack|discord|status|disconnect].`,
|
|
171
|
+
`Unknown: /gsd ${trimmed}. Use /gsd, /gsd next, /gsd auto, /gsd stop, /gsd status, /gsd queue, /gsd discuss, /gsd prefs [global|project|status|wizard|setup], /gsd doctor [audit|fix|heal] [M###/S##], /gsd migrate <path>, or /gsd remote [slack|discord|status|disconnect].`,
|
|
172
172
|
"warning",
|
|
173
173
|
);
|
|
174
174
|
},
|
|
@@ -219,6 +219,13 @@ async function handlePrefs(args: string, ctx: ExtensionCommandContext): Promise<
|
|
|
219
219
|
return;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
if (trimmed === "wizard" || trimmed === "setup" || trimmed === "wizard global" || trimmed === "setup global"
|
|
223
|
+
|| trimmed === "wizard project" || trimmed === "setup project") {
|
|
224
|
+
const scope = trimmed.includes("project") ? "project" : "global";
|
|
225
|
+
await handlePrefsWizard(ctx, scope);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
222
229
|
if (trimmed === "status") {
|
|
223
230
|
const globalPrefs = loadGlobalGSDPreferences();
|
|
224
231
|
const projectPrefs = loadProjectGSDPreferences();
|
|
@@ -249,7 +256,7 @@ async function handlePrefs(args: string, ctx: ExtensionCommandContext): Promise<
|
|
|
249
256
|
return;
|
|
250
257
|
}
|
|
251
258
|
|
|
252
|
-
ctx.ui.notify("Usage: /gsd prefs [global|project|status]", "info");
|
|
259
|
+
ctx.ui.notify("Usage: /gsd prefs [global|project|status|wizard|setup]", "info");
|
|
253
260
|
}
|
|
254
261
|
|
|
255
262
|
async function handleDoctor(args: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<void> {
|
|
@@ -290,6 +297,214 @@ async function handleDoctor(args: string, ctx: ExtensionCommandContext, pi: Exte
|
|
|
290
297
|
}
|
|
291
298
|
}
|
|
292
299
|
|
|
300
|
+
// ─── Preferences Wizard ───────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
async function handlePrefsWizard(
|
|
303
|
+
ctx: ExtensionCommandContext,
|
|
304
|
+
scope: "global" | "project",
|
|
305
|
+
): Promise<void> {
|
|
306
|
+
const path = scope === "project" ? getProjectGSDPreferencesPath() : getGlobalGSDPreferencesPath();
|
|
307
|
+
const existing = scope === "project" ? loadProjectGSDPreferences() : loadGlobalGSDPreferences();
|
|
308
|
+
const prefs: Record<string, unknown> = existing?.preferences ? { ...existing.preferences } : {};
|
|
309
|
+
|
|
310
|
+
ctx.ui.notify(`GSD preferences wizard (${scope}) — press Escape at any prompt to skip it.`, "info");
|
|
311
|
+
|
|
312
|
+
// ─── Models ──────────────────────────────────────────────────────────────
|
|
313
|
+
const modelPhases = ["research", "planning", "execution", "completion"] as const;
|
|
314
|
+
const models: Record<string, string> = (prefs.models as Record<string, string>) ?? {};
|
|
315
|
+
|
|
316
|
+
for (const phase of modelPhases) {
|
|
317
|
+
const current = models[phase] ?? "";
|
|
318
|
+
const input = await ctx.ui.input(
|
|
319
|
+
`Model for ${phase} phase${current ? ` (current: ${current})` : ""}:`,
|
|
320
|
+
current || "e.g. claude-sonnet-4-20250514",
|
|
321
|
+
);
|
|
322
|
+
if (input !== null && input !== undefined) {
|
|
323
|
+
const val = input.trim();
|
|
324
|
+
if (val) {
|
|
325
|
+
models[phase] = val;
|
|
326
|
+
} else if (current) {
|
|
327
|
+
// User cleared it — remove
|
|
328
|
+
delete models[phase];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
// null/undefined = Escape/skip — keep existing value
|
|
332
|
+
}
|
|
333
|
+
if (Object.keys(models).length > 0) {
|
|
334
|
+
prefs.models = models;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ─── Auto-supervisor timeouts ────────────────────────────────────────────
|
|
338
|
+
const autoSup: Record<string, unknown> = (prefs.auto_supervisor as Record<string, unknown>) ?? {};
|
|
339
|
+
const timeoutFields = [
|
|
340
|
+
{ key: "soft_timeout_minutes", label: "Soft timeout (minutes)", defaultVal: "20" },
|
|
341
|
+
{ key: "idle_timeout_minutes", label: "Idle timeout (minutes)", defaultVal: "10" },
|
|
342
|
+
{ key: "hard_timeout_minutes", label: "Hard timeout (minutes)", defaultVal: "30" },
|
|
343
|
+
] as const;
|
|
344
|
+
|
|
345
|
+
for (const field of timeoutFields) {
|
|
346
|
+
const current = autoSup[field.key];
|
|
347
|
+
const currentStr = current !== undefined && current !== null ? String(current) : "";
|
|
348
|
+
const input = await ctx.ui.input(
|
|
349
|
+
`${field.label}${currentStr ? ` (current: ${currentStr})` : ` (default: ${field.defaultVal})`}:`,
|
|
350
|
+
currentStr || field.defaultVal,
|
|
351
|
+
);
|
|
352
|
+
if (input !== null && input !== undefined) {
|
|
353
|
+
const val = input.trim();
|
|
354
|
+
if (val && /^\d+$/.test(val)) {
|
|
355
|
+
autoSup[field.key] = Number(val);
|
|
356
|
+
} else if (val && !/^\d+$/.test(val)) {
|
|
357
|
+
ctx.ui.notify(`Invalid value "${val}" for ${field.label} — must be a whole number. Keeping previous value.`, "warning");
|
|
358
|
+
} else if (!val && currentStr) {
|
|
359
|
+
delete autoSup[field.key];
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (Object.keys(autoSup).length > 0) {
|
|
364
|
+
prefs.auto_supervisor = autoSup;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ─── Git main branch ────────────────────────────────────────────────────
|
|
368
|
+
const git: Record<string, unknown> = (prefs.git as Record<string, unknown>) ?? {};
|
|
369
|
+
const currentBranch = git.main_branch ? String(git.main_branch) : "";
|
|
370
|
+
const branchInput = await ctx.ui.input(
|
|
371
|
+
`Git main branch${currentBranch ? ` (current: ${currentBranch})` : ""}:`,
|
|
372
|
+
currentBranch || "main",
|
|
373
|
+
);
|
|
374
|
+
if (branchInput !== null && branchInput !== undefined) {
|
|
375
|
+
const val = branchInput.trim();
|
|
376
|
+
if (val) {
|
|
377
|
+
git.main_branch = val;
|
|
378
|
+
} else if (currentBranch) {
|
|
379
|
+
delete git.main_branch;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (Object.keys(git).length > 0) {
|
|
383
|
+
prefs.git = git;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ─── Skill discovery mode ───────────────────────────────────────────────
|
|
387
|
+
const currentDiscovery = (prefs.skill_discovery as string) ?? "";
|
|
388
|
+
const discoveryChoice = await ctx.ui.select(
|
|
389
|
+
`Skill discovery mode${currentDiscovery ? ` (current: ${currentDiscovery})` : ""}:`,
|
|
390
|
+
["auto", "suggest", "off", "(keep current)"],
|
|
391
|
+
);
|
|
392
|
+
if (discoveryChoice && discoveryChoice !== "(keep current)") {
|
|
393
|
+
prefs.skill_discovery = discoveryChoice;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ─── Serialize to frontmatter ───────────────────────────────────────────
|
|
397
|
+
prefs.version = prefs.version || 1;
|
|
398
|
+
const frontmatter = serializePreferencesToFrontmatter(prefs);
|
|
399
|
+
|
|
400
|
+
// Preserve existing body content (everything after closing ---)
|
|
401
|
+
let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
|
|
402
|
+
if (existsSync(path)) {
|
|
403
|
+
const existingContent = readFileSync(path, "utf-8");
|
|
404
|
+
const closingIdx = existingContent.indexOf("\n---", existingContent.indexOf("---"));
|
|
405
|
+
if (closingIdx !== -1) {
|
|
406
|
+
const afterFrontmatter = existingContent.slice(closingIdx + 4); // skip past "\n---"
|
|
407
|
+
if (afterFrontmatter.trim()) {
|
|
408
|
+
body = afterFrontmatter;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const content = `---\n${frontmatter}---${body}`;
|
|
414
|
+
|
|
415
|
+
await saveFile(path, content);
|
|
416
|
+
await ctx.waitForIdle();
|
|
417
|
+
await ctx.reload();
|
|
418
|
+
ctx.ui.notify(`Saved ${scope} preferences to ${path}`, "info");
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/** Wrap a YAML value in double quotes if it contains special characters. */
|
|
422
|
+
function yamlSafeString(val: unknown): string {
|
|
423
|
+
if (typeof val !== "string") return String(val);
|
|
424
|
+
if (/[:#{\[\]'"`,|>&*!?@%]/.test(val) || val.trim() !== val || val === "") {
|
|
425
|
+
return `"${val.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
426
|
+
}
|
|
427
|
+
return val;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function serializePreferencesToFrontmatter(prefs: Record<string, unknown>): string {
|
|
431
|
+
const lines: string[] = [];
|
|
432
|
+
|
|
433
|
+
function serializeValue(key: string, value: unknown, indent: number): void {
|
|
434
|
+
const prefix = " ".repeat(indent);
|
|
435
|
+
if (value === null || value === undefined) return;
|
|
436
|
+
|
|
437
|
+
if (Array.isArray(value)) {
|
|
438
|
+
if (value.length === 0) {
|
|
439
|
+
lines.push(`${prefix}${key}: []`);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
lines.push(`${prefix}${key}:`);
|
|
443
|
+
for (const item of value) {
|
|
444
|
+
if (typeof item === "object" && item !== null) {
|
|
445
|
+
const entries = Object.entries(item as Record<string, unknown>);
|
|
446
|
+
if (entries.length > 0) {
|
|
447
|
+
const [firstKey, firstVal] = entries[0];
|
|
448
|
+
lines.push(`${prefix} - ${firstKey}: ${yamlSafeString(firstVal)}`);
|
|
449
|
+
for (let i = 1; i < entries.length; i++) {
|
|
450
|
+
const [k, v] = entries[i];
|
|
451
|
+
if (Array.isArray(v)) {
|
|
452
|
+
lines.push(`${prefix} ${k}:`);
|
|
453
|
+
for (const arrItem of v) {
|
|
454
|
+
lines.push(`${prefix} - ${yamlSafeString(arrItem)}`);
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
lines.push(`${prefix} ${k}: ${yamlSafeString(v)}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
} else {
|
|
462
|
+
lines.push(`${prefix} - ${yamlSafeString(item)}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (typeof value === "object") {
|
|
469
|
+
const entries = Object.entries(value as Record<string, unknown>);
|
|
470
|
+
if (entries.length === 0) {
|
|
471
|
+
lines.push(`${prefix}${key}: {}`);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
lines.push(`${prefix}${key}:`);
|
|
475
|
+
for (const [k, v] of entries) {
|
|
476
|
+
serializeValue(k, v, indent + 1);
|
|
477
|
+
}
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
lines.push(`${prefix}${key}: ${yamlSafeString(value)}`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Ordered keys for consistent output
|
|
485
|
+
const orderedKeys = [
|
|
486
|
+
"version", "always_use_skills", "prefer_skills", "avoid_skills",
|
|
487
|
+
"skill_rules", "custom_instructions", "models", "skill_discovery",
|
|
488
|
+
"auto_supervisor", "uat_dispatch", "budget_ceiling", "remote_questions", "git",
|
|
489
|
+
];
|
|
490
|
+
|
|
491
|
+
const seen = new Set<string>();
|
|
492
|
+
for (const key of orderedKeys) {
|
|
493
|
+
if (key in prefs) {
|
|
494
|
+
serializeValue(key, prefs[key], 0);
|
|
495
|
+
seen.add(key);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// Any remaining keys not in the ordered list
|
|
499
|
+
for (const [key, value] of Object.entries(prefs)) {
|
|
500
|
+
if (!seen.has(key)) {
|
|
501
|
+
serializeValue(key, value, 0);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return lines.join("\n") + "\n";
|
|
506
|
+
}
|
|
507
|
+
|
|
293
508
|
async function ensurePreferencesFile(
|
|
294
509
|
path: string,
|
|
295
510
|
ctx: ExtensionCommandContext,
|
|
@@ -79,7 +79,7 @@ function validatePreferenceShape(preferences: GSDPreferences): string[] {
|
|
|
79
79
|
issues.push(`skill_rules[${index}].when must be a string`);
|
|
80
80
|
}
|
|
81
81
|
for (const key of ["use", "prefer", "avoid"] as const) {
|
|
82
|
-
const value = (rule as Record<string, unknown>)[key];
|
|
82
|
+
const value = (rule as unknown as Record<string, unknown>)[key];
|
|
83
83
|
if (value !== undefined && !Array.isArray(value)) {
|
|
84
84
|
issues.push(`skill_rules[${index}].${key} must be a list`);
|
|
85
85
|
}
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { execSync } from "node:child_process";
|
|
12
|
-
import {
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { join, sep } from "node:path";
|
|
13
14
|
|
|
14
15
|
import {
|
|
15
16
|
detectWorktreeName,
|
|
@@ -68,6 +69,86 @@ export const RUNTIME_EXCLUSION_PATHS: readonly string[] = [
|
|
|
68
69
|
".gsd/STATE.md",
|
|
69
70
|
];
|
|
70
71
|
|
|
72
|
+
// ─── Integration Branch Metadata ───────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Path to the milestone metadata file that stores the integration branch.
|
|
76
|
+
* Format: .gsd/milestones/<MID>/<MID>-META.json
|
|
77
|
+
*/
|
|
78
|
+
function milestoneMetaPath(basePath: string, milestoneId: string): string {
|
|
79
|
+
return join(basePath, ".gsd", "milestones", milestoneId, `${milestoneId}-META.json`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Read the integration branch recorded for a milestone.
|
|
84
|
+
* Returns null if no metadata file exists or the branch isn't set.
|
|
85
|
+
*/
|
|
86
|
+
export function readIntegrationBranch(basePath: string, milestoneId: string): string | null {
|
|
87
|
+
try {
|
|
88
|
+
const metaFile = milestoneMetaPath(basePath, milestoneId);
|
|
89
|
+
if (!existsSync(metaFile)) return null;
|
|
90
|
+
const data = JSON.parse(readFileSync(metaFile, "utf-8"));
|
|
91
|
+
const branch = data?.integrationBranch;
|
|
92
|
+
if (typeof branch === "string" && branch.trim() !== "" && VALID_BRANCH_NAME.test(branch)) {
|
|
93
|
+
return branch;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
} catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Persist the integration branch for a milestone.
|
|
103
|
+
*
|
|
104
|
+
* Called once when auto-mode starts on a milestone. Records the branch
|
|
105
|
+
* the user was on at that point, so that slice branches merge back to it
|
|
106
|
+
* instead of the repo's default branch.
|
|
107
|
+
*
|
|
108
|
+
* The file is committed immediately so it survives branch switches — the
|
|
109
|
+
* pre-switch auto-commit excludes `.gsd/` to avoid merge conflicts, and
|
|
110
|
+
* uncommitted `.gsd/` files are discarded during checkout.
|
|
111
|
+
*
|
|
112
|
+
* Skips writing if an integration branch is already recorded (idempotent
|
|
113
|
+
* across restarts) or if the current branch is already a GSD slice branch.
|
|
114
|
+
*/
|
|
115
|
+
export function writeIntegrationBranch(basePath: string, milestoneId: string, branch: string): void {
|
|
116
|
+
// Don't record slice branches as the integration target
|
|
117
|
+
if (SLICE_BRANCH_RE.test(branch)) return;
|
|
118
|
+
// Don't overwrite an existing integration branch
|
|
119
|
+
if (readIntegrationBranch(basePath, milestoneId) !== null) return;
|
|
120
|
+
// Validate
|
|
121
|
+
if (!VALID_BRANCH_NAME.test(branch)) return;
|
|
122
|
+
|
|
123
|
+
const metaFile = milestoneMetaPath(basePath, milestoneId);
|
|
124
|
+
mkdirSync(join(basePath, ".gsd", "milestones", milestoneId), { recursive: true });
|
|
125
|
+
|
|
126
|
+
// Merge with existing metadata if present
|
|
127
|
+
let existing: Record<string, unknown> = {};
|
|
128
|
+
try {
|
|
129
|
+
if (existsSync(metaFile)) {
|
|
130
|
+
existing = JSON.parse(readFileSync(metaFile, "utf-8"));
|
|
131
|
+
}
|
|
132
|
+
} catch { /* corrupt file — overwrite */ }
|
|
133
|
+
|
|
134
|
+
existing.integrationBranch = branch;
|
|
135
|
+
writeFileSync(metaFile, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
136
|
+
|
|
137
|
+
// Commit immediately — .gsd/ files are discarded during branch switches
|
|
138
|
+
// (ensureSliceBranch excludes .gsd/ from pre-switch auto-commit and runs
|
|
139
|
+
// git checkout -- .gsd/ to prevent checkout conflicts). Without this
|
|
140
|
+
// commit, the metadata would be lost on the first branch switch.
|
|
141
|
+
try {
|
|
142
|
+
runGit(basePath, ["add", "--force", metaFile]);
|
|
143
|
+
runGit(basePath, ["commit", "-F", "-"], {
|
|
144
|
+
input: `chore(${milestoneId}): record integration branch`,
|
|
145
|
+
});
|
|
146
|
+
} catch {
|
|
147
|
+
// Non-fatal — file is on disk even if commit fails (e.g. nothing to commit
|
|
148
|
+
// because the file was already tracked with identical content)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
71
152
|
// ─── Git Helper ────────────────────────────────────────────────────────────
|
|
72
153
|
|
|
73
154
|
/**
|
|
@@ -115,11 +196,23 @@ export class GitServiceImpl {
|
|
|
115
196
|
readonly basePath: string;
|
|
116
197
|
readonly prefs: GitPreferences;
|
|
117
198
|
|
|
199
|
+
/** Active milestone ID — used to resolve the integration branch. */
|
|
200
|
+
private _milestoneId: string | null = null;
|
|
201
|
+
|
|
118
202
|
constructor(basePath: string, prefs: GitPreferences = {}) {
|
|
119
203
|
this.basePath = basePath;
|
|
120
204
|
this.prefs = prefs;
|
|
121
205
|
}
|
|
122
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Set the active milestone ID for integration branch resolution.
|
|
209
|
+
* When set, getMainBranch() will check the milestone's metadata file
|
|
210
|
+
* for a recorded integration branch before falling back to repo defaults.
|
|
211
|
+
*/
|
|
212
|
+
setMilestoneId(milestoneId: string | null): void {
|
|
213
|
+
this._milestoneId = milestoneId;
|
|
214
|
+
}
|
|
215
|
+
|
|
123
216
|
/** Convenience wrapper: run git in this repo's basePath. */
|
|
124
217
|
private git(args: string[], options: { allowFailure?: boolean; input?: string } = {}): string {
|
|
125
218
|
return runGit(this.basePath, args, options);
|
|
@@ -212,9 +305,18 @@ export class GitServiceImpl {
|
|
|
212
305
|
// ─── Branch Queries ────────────────────────────────────────────────────
|
|
213
306
|
|
|
214
307
|
/**
|
|
215
|
-
* Get the "main" branch for this repo.
|
|
216
|
-
*
|
|
217
|
-
*
|
|
308
|
+
* Get the "main" (integration) branch for this repo.
|
|
309
|
+
*
|
|
310
|
+
* Resolution order:
|
|
311
|
+
* 1. Explicit `main_branch` preference (user override, highest priority)
|
|
312
|
+
* 2. Milestone integration branch from metadata file (recorded at milestone start)
|
|
313
|
+
* 3. Worktree base branch (worktree/<name>)
|
|
314
|
+
* 4. origin/HEAD symbolic-ref → main/master fallback → current branch
|
|
315
|
+
*
|
|
316
|
+
* The integration branch (step 2) is what makes feature-branch workflows
|
|
317
|
+
* work correctly: when a user starts GSD on `f-123-new-thing`, that branch
|
|
318
|
+
* is recorded as the integration target, and all slice branches merge back
|
|
319
|
+
* to it instead of the repo's default branch.
|
|
218
320
|
*/
|
|
219
321
|
getMainBranch(): string {
|
|
220
322
|
// Explicit preference takes priority (double-check validity as defense-in-depth)
|
|
@@ -222,6 +324,16 @@ export class GitServiceImpl {
|
|
|
222
324
|
return this.prefs.main_branch;
|
|
223
325
|
}
|
|
224
326
|
|
|
327
|
+
// Check milestone integration branch — recorded when auto-mode starts
|
|
328
|
+
if (this._milestoneId) {
|
|
329
|
+
const integrationBranch = readIntegrationBranch(this.basePath, this._milestoneId);
|
|
330
|
+
if (integrationBranch) {
|
|
331
|
+
// Verify the branch still exists locally (could have been deleted)
|
|
332
|
+
const exists = this.git(["show-ref", "--verify", `refs/heads/${integrationBranch}`], { allowFailure: true });
|
|
333
|
+
if (exists) return integrationBranch;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
225
337
|
const wtName = detectWorktreeName(this.basePath);
|
|
226
338
|
if (wtName) {
|
|
227
339
|
const wtBranch = `worktree/${wtName}`;
|
|
@@ -112,6 +112,19 @@ function findMilestoneIds(basePath: string): string[] {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/** Return the highest numeric suffix among milestone IDs (0 when the list is empty or has no numeric IDs). */
|
|
116
|
+
export function maxMilestoneNum(milestoneIds: string[]): number {
|
|
117
|
+
return milestoneIds.reduce((max, id) => {
|
|
118
|
+
const num = parseInt(id.replace(/^M/, ""), 10);
|
|
119
|
+
return num > max ? num : max;
|
|
120
|
+
}, 0);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Derive the next milestone ID from existing IDs using max-based approach to avoid collisions after deletions. */
|
|
124
|
+
export function nextMilestoneId(milestoneIds: string[]): string {
|
|
125
|
+
return `M${String(maxMilestoneNum(milestoneIds) + 1).padStart(3, "0")}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
115
128
|
// ─── Queue ─────────────────────────────────────────────────────────────────────
|
|
116
129
|
|
|
117
130
|
/**
|
|
@@ -153,12 +166,9 @@ export async function showQueue(
|
|
|
153
166
|
const existingContext = await buildExistingMilestonesContext(basePath, milestoneIds, state);
|
|
154
167
|
|
|
155
168
|
// ── Determine next milestone ID ─────────────────────────────────────
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}, 0);
|
|
160
|
-
const nextId = `M${String(maxNum + 1).padStart(3, "0")}`;
|
|
161
|
-
const nextIdPlus1 = `M${String(maxNum + 2).padStart(3, "0")}`;
|
|
169
|
+
const max = maxMilestoneNum(milestoneIds);
|
|
170
|
+
const nextId = `M${String(max + 1).padStart(3, "0")}`;
|
|
171
|
+
const nextIdPlus1 = `M${String(max + 2).padStart(3, "0")}`;
|
|
162
172
|
|
|
163
173
|
// ── Build preamble ──────────────────────────────────────────────────
|
|
164
174
|
const activePart = state.activeMilestone
|
|
@@ -508,7 +518,7 @@ export async function showSmartEntry(
|
|
|
508
518
|
}
|
|
509
519
|
|
|
510
520
|
const milestoneIds = findMilestoneIds(basePath);
|
|
511
|
-
const nextId =
|
|
521
|
+
const nextId = nextMilestoneId(milestoneIds);
|
|
512
522
|
const isFirst = milestoneIds.length === 0;
|
|
513
523
|
|
|
514
524
|
if (isFirst) {
|
|
@@ -570,7 +580,7 @@ export async function showSmartEntry(
|
|
|
570
580
|
|
|
571
581
|
if (choice === "new_milestone") {
|
|
572
582
|
const milestoneIds = findMilestoneIds(basePath);
|
|
573
|
-
const nextId =
|
|
583
|
+
const nextId = nextMilestoneId(milestoneIds);
|
|
574
584
|
|
|
575
585
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
576
586
|
dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
|
@@ -638,7 +648,7 @@ export async function showSmartEntry(
|
|
|
638
648
|
}));
|
|
639
649
|
} else if (choice === "skip_milestone") {
|
|
640
650
|
const milestoneIds = findMilestoneIds(basePath);
|
|
641
|
-
const nextId =
|
|
651
|
+
const nextId = nextMilestoneId(milestoneIds);
|
|
642
652
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
643
653
|
dispatchWorkflow(pi, buildDiscussPrompt(nextId,
|
|
644
654
|
`New milestone ${nextId}.`,
|