gsd-pi 2.8.2 → 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/extensions/types.d.ts +4 -2
- package/node_modules/@gsd/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- 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 +80 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +5 -0
- package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/src/core/extensions/types.ts +4 -2
- 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 +94 -2
- package/node_modules/@gsd/pi-coding-agent/src/modes/rpc/rpc-mode.ts +2 -2
- package/node_modules/@gsd/pi-coding-agent/src/modes/rpc/rpc-types.ts +2 -1
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +4 -2
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +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 +80 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts +5 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-types.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/types.ts +4 -2
- 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 +94 -2
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +2 -2
- package/packages/pi-coding-agent/src/modes/rpc/rpc-types.ts +2 -1
- package/src/resources/extensions/ask-user-questions.ts +42 -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 +126 -12
- 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 +163 -13
- 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/command-search-provider.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/native-search.ts +5 -6
- 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
|
@@ -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}`;
|
|
@@ -527,6 +639,16 @@ export class GitServiceImpl {
|
|
|
527
639
|
// Pull latest main before merging to avoid conflicts from remote changes
|
|
528
640
|
this.git(["pull", "--rebase", "origin", mainBranch], { allowFailure: true });
|
|
529
641
|
|
|
642
|
+
// Untrack runtime files that may have been manually committed (e.g. via `gsd queue`)
|
|
643
|
+
// to prevent merge conflicts on files that belong in .gitignore (#189)
|
|
644
|
+
for (const exclusion of RUNTIME_EXCLUSION_PATHS) {
|
|
645
|
+
this.git(["rm", "--cached", "-r", "--ignore-unmatch", exclusion], { allowFailure: true });
|
|
646
|
+
}
|
|
647
|
+
const untrackDiff = this.git(["diff", "--cached", "--stat"], { allowFailure: true });
|
|
648
|
+
if (untrackDiff && untrackDiff.trim()) {
|
|
649
|
+
this.git(["commit", "-m", "chore: untrack .gsd/ runtime files before merge"], { allowFailure: true });
|
|
650
|
+
}
|
|
651
|
+
|
|
530
652
|
// Merge slice branch — strategy is configurable via git.merge_strategy
|
|
531
653
|
// preference. Default: "squash" (preserves existing behavior).
|
|
532
654
|
// "merge" uses --no-ff which is more resilient to conflicts from
|
|
@@ -539,15 +661,43 @@ export class GitServiceImpl {
|
|
|
539
661
|
try {
|
|
540
662
|
this.git(mergeArgs);
|
|
541
663
|
} catch (mergeError) {
|
|
542
|
-
//
|
|
543
|
-
this.git(["
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
664
|
+
// Check if conflicts are limited to runtime files we can auto-resolve (#189)
|
|
665
|
+
const conflicted = this.git(["diff", "--name-only", "--diff-filter=U"], { allowFailure: true });
|
|
666
|
+
if (conflicted) {
|
|
667
|
+
const conflictedFiles = conflicted.split("\n").filter(Boolean);
|
|
668
|
+
const allRuntime = conflictedFiles.every(f =>
|
|
669
|
+
RUNTIME_EXCLUSION_PATHS.some(excl => f.startsWith(excl.replace(/\/$/, ""))),
|
|
670
|
+
);
|
|
671
|
+
if (allRuntime) {
|
|
672
|
+
// Runtime-only conflicts: take ours and remove from index
|
|
673
|
+
for (const f of conflictedFiles) {
|
|
674
|
+
this.git(["checkout", "--ours", "--", f], { allowFailure: true });
|
|
675
|
+
this.git(["rm", "--cached", "--ignore-unmatch", f], { allowFailure: true });
|
|
676
|
+
}
|
|
677
|
+
this.git(["add", "-A"], { allowFailure: true });
|
|
678
|
+
// Don't throw — let the merge proceed
|
|
679
|
+
} else {
|
|
680
|
+
// Non-runtime conflicts: reset and throw as before
|
|
681
|
+
this.git(["reset", "--hard", "HEAD"], { allowFailure: true });
|
|
682
|
+
const msg = mergeError instanceof Error ? mergeError.message : String(mergeError);
|
|
683
|
+
throw new Error(
|
|
684
|
+
`${strategy === "merge" ? "Merge" : "Squash-merge"} of "${branch}" into "${mainBranch}" failed with conflicts. ` +
|
|
685
|
+
`Working tree has been reset to a clean state. ` +
|
|
686
|
+
`Resolve manually: git checkout ${mainBranch} && git merge ${strategy === "merge" ? "--no-ff" : "--squash"} ${branch}\n` +
|
|
687
|
+
`Original error: ${msg}`,
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
} else {
|
|
691
|
+
// No conflicted files detected but merge still failed — reset and throw
|
|
692
|
+
this.git(["reset", "--hard", "HEAD"], { allowFailure: true });
|
|
693
|
+
const msg = mergeError instanceof Error ? mergeError.message : String(mergeError);
|
|
694
|
+
throw new Error(
|
|
695
|
+
`${strategy === "merge" ? "Merge" : "Squash-merge"} of "${branch}" into "${mainBranch}" failed. ` +
|
|
696
|
+
`Working tree has been reset to a clean state. ` +
|
|
697
|
+
`Resolve manually: git checkout ${mainBranch} && git merge ${strategy === "merge" ? "--no-ff" : "--squash"} ${branch}\n` +
|
|
698
|
+
`Original error: ${msg}`,
|
|
699
|
+
);
|
|
700
|
+
}
|
|
551
701
|
}
|
|
552
702
|
|
|
553
703
|
// Squash merge needs a separate commit; --no-ff merge already committed
|
|
@@ -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}.`,
|
|
@@ -127,7 +127,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
127
127
|
...params,
|
|
128
128
|
timeout: params.timeout ?? DEFAULT_BASH_TIMEOUT_SECS,
|
|
129
129
|
};
|
|
130
|
-
return baseBash.execute(toolCallId, paramsWithTimeout, signal, onUpdate, ctx);
|
|
130
|
+
return (baseBash as any).execute(toolCallId, paramsWithTimeout, signal, onUpdate, ctx);
|
|
131
131
|
},
|
|
132
132
|
};
|
|
133
133
|
pi.registerTool(dynamicBash as any);
|
|
@@ -148,7 +148,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
148
148
|
ctx?: any,
|
|
149
149
|
) => {
|
|
150
150
|
const fresh = createWriteTool(process.cwd());
|
|
151
|
-
return fresh.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
151
|
+
return (fresh as any).execute(toolCallId, params, signal, onUpdate, ctx);
|
|
152
152
|
},
|
|
153
153
|
};
|
|
154
154
|
pi.registerTool(dynamicWrite as any);
|
|
@@ -164,7 +164,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
164
164
|
ctx?: any,
|
|
165
165
|
) => {
|
|
166
166
|
const fresh = createReadTool(process.cwd());
|
|
167
|
-
return fresh.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
167
|
+
return (fresh as any).execute(toolCallId, params, signal, onUpdate, ctx);
|
|
168
168
|
},
|
|
169
169
|
};
|
|
170
170
|
pi.registerTool(dynamicRead as any);
|
|
@@ -180,7 +180,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
180
180
|
ctx?: any,
|
|
181
181
|
) => {
|
|
182
182
|
const fresh = createEditTool(process.cwd());
|
|
183
|
-
return fresh.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
183
|
+
return (fresh as any).execute(toolCallId, params, signal, onUpdate, ctx);
|
|
184
184
|
},
|
|
185
185
|
};
|
|
186
186
|
pi.registerTool(dynamicEdit as any);
|
|
@@ -325,14 +325,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
325
325
|
// If auto-mode is already running, advance to next unit
|
|
326
326
|
if (!isAutoActive()) return;
|
|
327
327
|
|
|
328
|
-
// If the agent was aborted (user pressed Escape)
|
|
329
|
-
//
|
|
330
|
-
//
|
|
328
|
+
// If the agent was aborted (user pressed Escape) or hit a provider
|
|
329
|
+
// error (fetch failure, rate limit, etc.), pause auto-mode instead of
|
|
330
|
+
// advancing. This preserves the conversation so the user can inspect
|
|
331
|
+
// what happened, interact with the agent, or resume.
|
|
331
332
|
const lastMsg = event.messages[event.messages.length - 1];
|
|
332
333
|
if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "aborted") {
|
|
333
334
|
await pauseAuto(ctx, pi);
|
|
334
335
|
return;
|
|
335
336
|
}
|
|
337
|
+
if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "error") {
|
|
338
|
+
const errorDetail =
|
|
339
|
+
"errorMessage" in lastMsg && lastMsg.errorMessage
|
|
340
|
+
? `: ${lastMsg.errorMessage}`
|
|
341
|
+
: "";
|
|
342
|
+
(ctx as any).log(`Auto-mode paused due to provider error${errorDetail}`);
|
|
343
|
+
await pauseAuto(ctx, pi);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
336
346
|
|
|
337
347
|
await handleAgentEnd(ctx, pi);
|
|
338
348
|
});
|
|
@@ -625,7 +625,7 @@ function validatePreferences(preferences: GSDPreferences): {
|
|
|
625
625
|
}
|
|
626
626
|
const validatedRule: GSDSkillRule = { when };
|
|
627
627
|
for (const action of SKILL_ACTIONS) {
|
|
628
|
-
const values = normalizeStringList((rule as Record<string, unknown>)[action]);
|
|
628
|
+
const values = normalizeStringList((rule as unknown as Record<string, unknown>)[action]);
|
|
629
629
|
if (values.length > 0) {
|
|
630
630
|
validatedRule[action as keyof GSDSkillRule] = values as never;
|
|
631
631
|
}
|