gsd-pi 2.8.1 → 2.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/loader.js +5 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/bash-executor.js +2 -2
- package/node_modules/@gsd/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
- 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/tools/bash.js +2 -2
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.js +13 -2
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.d.ts +2 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.d.ts.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.js +57 -0
- package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.js.map +1 -0
- package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.js +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/index.js.map +1 -1
- 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 +41 -5
- 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/dist/utils/shell.d.ts +7 -0
- package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.js +11 -0
- package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.js.map +1 -1
- package/node_modules/@gsd/pi-coding-agent/src/core/bash-executor.ts +2 -2
- package/node_modules/@gsd/pi-coding-agent/src/core/extensions/types.ts +4 -2
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash.ts +2 -2
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/path-utils.test.ts +66 -0
- package/node_modules/@gsd/pi-coding-agent/src/core/tools/path-utils.ts +14 -2
- package/node_modules/@gsd/pi-coding-agent/src/index.ts +1 -1
- package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +41 -4
- 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/node_modules/@gsd/pi-coding-agent/src/utils/shell.ts +11 -0
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/bash-executor.js +2 -2
- package/packages/pi-coding-agent/dist/core/bash-executor.js.map +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/tools/bash.js +2 -2
- package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.js +13 -2
- package/packages/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/path-utils.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/path-utils.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/path-utils.test.js +57 -0
- package/packages/pi-coding-agent/dist/core/tools/path-utils.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/index.d.ts +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +1 -1
- package/packages/pi-coding-agent/dist/index.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 +41 -5
- 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/dist/utils/shell.d.ts +7 -0
- package/packages/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/utils/shell.js +11 -0
- package/packages/pi-coding-agent/dist/utils/shell.js.map +1 -1
- package/packages/pi-coding-agent/src/core/bash-executor.ts +2 -2
- package/packages/pi-coding-agent/src/core/extensions/types.ts +4 -2
- package/packages/pi-coding-agent/src/core/tools/bash.ts +2 -2
- package/packages/pi-coding-agent/src/core/tools/path-utils.test.ts +66 -0
- package/packages/pi-coding-agent/src/core/tools/path-utils.ts +14 -2
- package/packages/pi-coding-agent/src/index.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +41 -4
- 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/packages/pi-coding-agent/src/utils/shell.ts +11 -0
- package/src/resources/extensions/ask-user-questions.ts +40 -0
- package/src/resources/extensions/bg-shell/index.ts +2 -1
- package/src/resources/extensions/gsd/auto.ts +103 -44
- package/src/resources/extensions/gsd/docs/preferences-reference.md +76 -0
- package/src/resources/extensions/gsd/git-service.ts +47 -9
- package/src/resources/extensions/gsd/gitignore.ts +27 -0
- package/src/resources/extensions/gsd/guided-flow.ts +10 -9
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -2
- package/src/resources/extensions/gsd/prompts/complete-slice.md +3 -3
- package/src/resources/extensions/gsd/prompts/discuss.md +2 -2
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -3
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -2
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +3 -3
- package/src/resources/extensions/gsd/prompts/replan-slice.md +2 -2
- package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/run-uat.md +4 -4
- package/src/resources/extensions/gsd/tests/git-service.test.ts +53 -1
- package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +5 -5
- package/src/resources/extensions/gsd/tests/replan-slice.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +2 -4
- package/src/resources/extensions/search-the-web/command-search-provider.ts +1 -1
- package/src/resources/extensions/search-the-web/native-search.ts +5 -6
|
@@ -824,10 +824,12 @@ export class InteractiveMode {
|
|
|
824
824
|
|
|
825
825
|
// Try parent directories (package manager stores directory paths)
|
|
826
826
|
let current = p;
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
const
|
|
830
|
-
if (
|
|
827
|
+
let parent = path.dirname(current);
|
|
828
|
+
while (parent !== current) {
|
|
829
|
+
const meta = metadata.get(parent);
|
|
830
|
+
if (meta) return meta;
|
|
831
|
+
current = parent;
|
|
832
|
+
parent = path.dirname(current);
|
|
831
833
|
}
|
|
832
834
|
|
|
833
835
|
return undefined;
|
|
@@ -3693,6 +3695,21 @@ export class InteractiveMode {
|
|
|
3693
3695
|
this.session.modelRegistry.authStorage.logout(providerId);
|
|
3694
3696
|
this.session.modelRegistry.refresh();
|
|
3695
3697
|
await this.updateAvailableProviderCount();
|
|
3698
|
+
|
|
3699
|
+
// Auto-switch model if current model belongs to the logged-out provider
|
|
3700
|
+
const currentModel = this.session.model;
|
|
3701
|
+
if (currentModel?.provider === providerId) {
|
|
3702
|
+
try {
|
|
3703
|
+
const available = this.session.modelRegistry.getAvailable();
|
|
3704
|
+
const fallback = available.find((m) => m.provider !== providerId);
|
|
3705
|
+
if (fallback) {
|
|
3706
|
+
await this.session.setModel(fallback);
|
|
3707
|
+
}
|
|
3708
|
+
} catch {
|
|
3709
|
+
// Model switch failed — user can manually switch via /model
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3696
3713
|
this.showStatus(`Logged out of ${providerName}`);
|
|
3697
3714
|
} catch (error: unknown) {
|
|
3698
3715
|
this.showError(`Logout failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -3787,6 +3804,26 @@ export class InteractiveMode {
|
|
|
3787
3804
|
restoreEditor();
|
|
3788
3805
|
this.session.modelRegistry.refresh();
|
|
3789
3806
|
await this.updateAvailableProviderCount();
|
|
3807
|
+
|
|
3808
|
+
// Auto-switch model if current model has no valid API key
|
|
3809
|
+
try {
|
|
3810
|
+
const currentModel = this.session.model;
|
|
3811
|
+
if (currentModel) {
|
|
3812
|
+
const currentKey = await this.session.modelRegistry.getApiKey(currentModel);
|
|
3813
|
+
if (!currentKey) {
|
|
3814
|
+
const available = this.session.modelRegistry.getAvailable();
|
|
3815
|
+
const newProviderModel = available.find((m) => m.provider === providerId);
|
|
3816
|
+
if (newProviderModel) {
|
|
3817
|
+
await this.session.setModel(newProviderModel);
|
|
3818
|
+
} else if (available.length > 0) {
|
|
3819
|
+
await this.session.setModel(available[0]);
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
}
|
|
3823
|
+
} catch (error: unknown) {
|
|
3824
|
+
// Model switch failed — user can manually switch via /model
|
|
3825
|
+
}
|
|
3826
|
+
|
|
3790
3827
|
this.showStatus(`Logged in to ${providerName}. Credentials saved to ${getAuthPath()}`);
|
|
3791
3828
|
} catch (error: unknown) {
|
|
3792
3829
|
restoreEditor();
|
|
@@ -119,8 +119,8 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
119
119
|
*/
|
|
120
120
|
const createExtensionUIContext = (): ExtensionUIContext => ({
|
|
121
121
|
select: (title, options, opts) =>
|
|
122
|
-
createDialogPromise(opts, undefined, { method: "select", title, options, timeout: opts?.timeout }, (r) =>
|
|
123
|
-
"cancelled" in r && r.cancelled ? undefined : "value" in r ? r.value : undefined,
|
|
122
|
+
createDialogPromise(opts, undefined, { method: "select", title, options, timeout: opts?.timeout, allowMultiple: opts?.allowMultiple }, (r) =>
|
|
123
|
+
"cancelled" in r && r.cancelled ? undefined : "values" in r ? r.values : "value" in r ? r.value : undefined,
|
|
124
124
|
),
|
|
125
125
|
|
|
126
126
|
confirm: (title, message, opts) =>
|
|
@@ -210,7 +210,7 @@ export type RpcResponse =
|
|
|
210
210
|
|
|
211
211
|
/** Emitted when an extension needs user input */
|
|
212
212
|
export type RpcExtensionUIRequest =
|
|
213
|
-
| { type: "extension_ui_request"; id: string; method: "select"; title: string; options: string[]; timeout?: number }
|
|
213
|
+
| { type: "extension_ui_request"; id: string; method: "select"; title: string; options: string[]; timeout?: number; allowMultiple?: boolean }
|
|
214
214
|
| { type: "extension_ui_request"; id: string; method: "confirm"; title: string; message: string; timeout?: number }
|
|
215
215
|
| {
|
|
216
216
|
type: "extension_ui_request";
|
|
@@ -253,6 +253,7 @@ export type RpcExtensionUIRequest =
|
|
|
253
253
|
/** Response to an extension UI request */
|
|
254
254
|
export type RpcExtensionUIResponse =
|
|
255
255
|
| { type: "extension_ui_response"; id: string; value: string }
|
|
256
|
+
| { type: "extension_ui_response"; id: string; values: string[] }
|
|
256
257
|
| { type: "extension_ui_response"; id: string; confirmed: boolean }
|
|
257
258
|
| { type: "extension_ui_response"; id: string; cancelled: true };
|
|
258
259
|
|
|
@@ -118,6 +118,17 @@ export function getShellConfig(): { shell: string; args: string[] } {
|
|
|
118
118
|
return cachedShellConfig;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* On Windows + Git Bash, rewrite Windows-style NUL redirects to /dev/null.
|
|
123
|
+
* Git Bash doesn't recognize NUL as a device name and creates a literal file
|
|
124
|
+
* that is undeletable due to NUL being a reserved Windows device name.
|
|
125
|
+
* No-op on non-Windows platforms.
|
|
126
|
+
*/
|
|
127
|
+
export function sanitizeCommand(command: string): string {
|
|
128
|
+
if (process.platform !== "win32") return command;
|
|
129
|
+
return command.replace(/(\d*>>?) *\bNUL\b(?=\s|;|\||&|\)|$)/gi, "$1 /dev/null");
|
|
130
|
+
}
|
|
131
|
+
|
|
121
132
|
export function getShellEnv(): NodeJS.ProcessEnv {
|
|
122
133
|
const binDir = getBinDir();
|
|
123
134
|
const pathKey = Object.keys(process.env).find((key) => key.toLowerCase() === "path") ?? "PATH";
|
|
@@ -144,6 +144,46 @@ export default function AskUserQuestions(pi: ExtensionAPI) {
|
|
|
144
144
|
// Delegate to shared interview UI
|
|
145
145
|
const result = await showInterviewRound(params.questions, {}, ctx);
|
|
146
146
|
|
|
147
|
+
// RPC mode fallback: custom() returns undefined, so showInterviewRound
|
|
148
|
+
// may return undefined. Fall back to sequential ctx.ui.select() calls.
|
|
149
|
+
if (!result) {
|
|
150
|
+
const answers: Record<string, { answers: string[] }> = {};
|
|
151
|
+
for (const q of params.questions) {
|
|
152
|
+
const options = q.options.map((o) => o.label);
|
|
153
|
+
if (!q.allowMultiple) {
|
|
154
|
+
options.push(OTHER_OPTION_LABEL);
|
|
155
|
+
}
|
|
156
|
+
const selected = await ctx.ui.select(
|
|
157
|
+
`${q.header}: ${q.question}`,
|
|
158
|
+
options,
|
|
159
|
+
{ signal, ...(q.allowMultiple ? { allowMultiple: true } : {}) },
|
|
160
|
+
);
|
|
161
|
+
if (selected === undefined) {
|
|
162
|
+
return errorResult("ask_user_questions was cancelled", params.questions);
|
|
163
|
+
}
|
|
164
|
+
answers[q.id] = {
|
|
165
|
+
answers: Array.isArray(selected) ? selected : [selected],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const roundResult: RoundResult = {
|
|
169
|
+
endInterview: false,
|
|
170
|
+
answers: Object.fromEntries(
|
|
171
|
+
Object.entries(answers).map(([id, a]) => [
|
|
172
|
+
id,
|
|
173
|
+
{ selected: a.answers.length === 1 ? a.answers[0] : a.answers, notes: "" },
|
|
174
|
+
]),
|
|
175
|
+
),
|
|
176
|
+
};
|
|
177
|
+
return {
|
|
178
|
+
content: [{ type: "text" as const, text: JSON.stringify({ answers }) }],
|
|
179
|
+
details: {
|
|
180
|
+
questions: params.questions,
|
|
181
|
+
response: roundResult,
|
|
182
|
+
cancelled: false,
|
|
183
|
+
} satisfies LocalResultDetails,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
147
187
|
// Check if cancelled (empty answers = user exited)
|
|
148
188
|
const hasAnswers = Object.keys(result.answers).length > 0;
|
|
149
189
|
if (!hasAnswers) {
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
DEFAULT_MAX_BYTES,
|
|
35
35
|
DEFAULT_MAX_LINES,
|
|
36
36
|
getShellConfig,
|
|
37
|
+
sanitizeCommand,
|
|
37
38
|
} from "@gsd/pi-coding-agent";
|
|
38
39
|
import {
|
|
39
40
|
Text,
|
|
@@ -582,7 +583,7 @@ function startProcess(opts: StartOptions): BgProcess {
|
|
|
582
583
|
const env = { ...process.env, ...(opts.env || {}) };
|
|
583
584
|
|
|
584
585
|
const { shell, args: shellArgs } = getShellConfig();
|
|
585
|
-
const proc = spawn(shell, [...shellArgs, opts.command], {
|
|
586
|
+
const proc = spawn(shell, [...shellArgs, sanitizeCommand(opts.command)], {
|
|
586
587
|
cwd: opts.cwd,
|
|
587
588
|
stdio: ["pipe", "pipe", "pipe"],
|
|
588
589
|
env,
|
|
@@ -48,14 +48,14 @@ import {
|
|
|
48
48
|
validateCompleteBoundary,
|
|
49
49
|
formatValidationIssues,
|
|
50
50
|
} from "./observability-validator.js";
|
|
51
|
-
import { ensureGitignore } from "./gitignore.js";
|
|
51
|
+
import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
|
|
52
52
|
import { runGSDDoctor, rebuildState } from "./doctor.js";
|
|
53
53
|
import { snapshotSkills, clearSkillSnapshot } from "./skill-discovery.js";
|
|
54
54
|
import {
|
|
55
55
|
initMetrics, resetMetrics, snapshotUnitMetrics, getLedger,
|
|
56
56
|
getProjectTotals, formatCost, formatTokenCount,
|
|
57
57
|
} from "./metrics.js";
|
|
58
|
-
import { join } from "node:path";
|
|
58
|
+
import { dirname, join } from "node:path";
|
|
59
59
|
import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
60
60
|
import { execSync, execFileSync } from "node:child_process";
|
|
61
61
|
import {
|
|
@@ -152,6 +152,7 @@ let currentMilestoneId: string | null = null;
|
|
|
152
152
|
|
|
153
153
|
/** Model the user had selected before auto-mode started */
|
|
154
154
|
let originalModelId: string | null = null;
|
|
155
|
+
let originalModelProvider: string | null = null;
|
|
155
156
|
|
|
156
157
|
/** Progress-aware timeout supervision */
|
|
157
158
|
let unitTimeoutHandle: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -257,6 +258,11 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
257
258
|
ctx?.ui.notify("Auto-mode stopped.", "info");
|
|
258
259
|
}
|
|
259
260
|
|
|
261
|
+
// Sync disk state so next resume starts from accurate state
|
|
262
|
+
if (basePath) {
|
|
263
|
+
try { await rebuildState(basePath); } catch { /* non-fatal */ }
|
|
264
|
+
}
|
|
265
|
+
|
|
260
266
|
resetMetrics();
|
|
261
267
|
active = false;
|
|
262
268
|
paused = false;
|
|
@@ -272,10 +278,11 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
272
278
|
ctx?.ui.setFooter(undefined);
|
|
273
279
|
|
|
274
280
|
// Restore the user's original model
|
|
275
|
-
if (pi && ctx && originalModelId) {
|
|
276
|
-
const original = ctx.modelRegistry.find(
|
|
281
|
+
if (pi && ctx && originalModelId && originalModelProvider) {
|
|
282
|
+
const original = ctx.modelRegistry.find(originalModelProvider, originalModelId);
|
|
277
283
|
if (original) await pi.setModel(original);
|
|
278
284
|
originalModelId = null;
|
|
285
|
+
originalModelProvider = null;
|
|
279
286
|
}
|
|
280
287
|
|
|
281
288
|
cmdCtx = null;
|
|
@@ -381,6 +388,7 @@ export async function startAuto(
|
|
|
381
388
|
|
|
382
389
|
// Ensure .gitignore has baseline patterns
|
|
383
390
|
ensureGitignore(base);
|
|
391
|
+
untrackRuntimeFiles(base);
|
|
384
392
|
|
|
385
393
|
// Bootstrap .gsd/ if it doesn't exist
|
|
386
394
|
const gsdDir = join(base, ".gsd");
|
|
@@ -458,6 +466,7 @@ export async function startAuto(
|
|
|
458
466
|
currentUnit = null;
|
|
459
467
|
currentMilestoneId = state.activeMilestone?.id ?? null;
|
|
460
468
|
originalModelId = ctx.model?.id ?? null;
|
|
469
|
+
originalModelProvider = ctx.model?.provider ?? null;
|
|
461
470
|
|
|
462
471
|
// Initialize metrics — loads existing ledger from disk
|
|
463
472
|
initMetrics(base);
|
|
@@ -1180,7 +1189,7 @@ async function dispatchNextUnit(
|
|
|
1180
1189
|
|
|
1181
1190
|
// Research before roadmap if no research exists
|
|
1182
1191
|
const researchFile = resolveMilestoneFile(basePath, mid, "RESEARCH");
|
|
1183
|
-
const hasResearch = !!
|
|
1192
|
+
const hasResearch = !!researchFile;
|
|
1184
1193
|
|
|
1185
1194
|
if (!hasResearch) {
|
|
1186
1195
|
unitType = "research-milestone";
|
|
@@ -1197,13 +1206,13 @@ async function dispatchNextUnit(
|
|
|
1197
1206
|
const sid = state.activeSlice!.id;
|
|
1198
1207
|
const sTitle = state.activeSlice!.title;
|
|
1199
1208
|
const researchFile = resolveSliceFile(basePath, mid, sid, "RESEARCH");
|
|
1200
|
-
const hasResearch = !!
|
|
1209
|
+
const hasResearch = !!researchFile;
|
|
1201
1210
|
|
|
1202
1211
|
if (!hasResearch) {
|
|
1203
1212
|
// Skip slice research for S01 when milestone research already exists —
|
|
1204
1213
|
// the milestone research already covers the same ground for the first slice.
|
|
1205
1214
|
const milestoneResearchFile = resolveMilestoneFile(basePath, mid, "RESEARCH");
|
|
1206
|
-
const hasMilestoneResearch = !!
|
|
1215
|
+
const hasMilestoneResearch = !!milestoneResearchFile;
|
|
1207
1216
|
if (hasMilestoneResearch && sid === "S01") {
|
|
1208
1217
|
unitType = "plan-slice";
|
|
1209
1218
|
unitId = `${mid}/${sid}`;
|
|
@@ -1311,6 +1320,26 @@ async function dispatchNextUnit(
|
|
|
1311
1320
|
}
|
|
1312
1321
|
unitDispatchCount.set(dispatchKey, prevCount + 1);
|
|
1313
1322
|
if (prevCount > 0) {
|
|
1323
|
+
// Self-repair: if summary exists but checkbox not marked, fix it and re-derive
|
|
1324
|
+
if (unitType === "execute-task") {
|
|
1325
|
+
const status = await inspectExecuteTaskDurability(basePath, unitId);
|
|
1326
|
+
if (status?.summaryExists && !status.taskChecked) {
|
|
1327
|
+
const [mid, sid, tid] = unitId.split("/");
|
|
1328
|
+
if (mid && sid && tid) {
|
|
1329
|
+
const repaired = skipExecuteTask(basePath, mid, sid, tid, status, "self-repair", 0);
|
|
1330
|
+
if (repaired) {
|
|
1331
|
+
ctx.ui.notify(
|
|
1332
|
+
`Self-repaired ${unitId}: summary existed but checkbox was unmarked. Marked [x] and advancing.`,
|
|
1333
|
+
"warning",
|
|
1334
|
+
);
|
|
1335
|
+
unitDispatchCount.delete(dispatchKey);
|
|
1336
|
+
await new Promise(r => setImmediate(r));
|
|
1337
|
+
await dispatchNextUnit(ctx, pi);
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1314
1343
|
ctx.ui.notify(
|
|
1315
1344
|
`${unitType} ${unitId} didn't produce expected artifact. Retrying (${prevCount + 1}/${MAX_UNIT_DISPATCHES}).`,
|
|
1316
1345
|
"warning",
|
|
@@ -1413,14 +1442,46 @@ async function dispatchNextUnit(
|
|
|
1413
1442
|
// Try primary model, then fallbacks in order if setting fails
|
|
1414
1443
|
const modelConfig = resolveModelWithFallbacksForUnit(unitType);
|
|
1415
1444
|
if (modelConfig) {
|
|
1416
|
-
const
|
|
1445
|
+
const availableModels = ctx.modelRegistry.getAvailable();
|
|
1417
1446
|
const modelsToTry = [modelConfig.primary, ...modelConfig.fallbacks];
|
|
1418
1447
|
let modelSet = false;
|
|
1419
1448
|
|
|
1420
1449
|
for (const modelId of modelsToTry) {
|
|
1421
|
-
|
|
1450
|
+
// Support "provider/model" format for explicit provider targeting
|
|
1451
|
+
const slashIdx = modelId.indexOf("/");
|
|
1452
|
+
let model;
|
|
1453
|
+
if (slashIdx !== -1) {
|
|
1454
|
+
const provider = modelId.substring(0, slashIdx);
|
|
1455
|
+
const id = modelId.substring(slashIdx + 1);
|
|
1456
|
+
model = availableModels.find(
|
|
1457
|
+
m => m.provider.toLowerCase() === provider.toLowerCase()
|
|
1458
|
+
&& m.id.toLowerCase() === id.toLowerCase(),
|
|
1459
|
+
);
|
|
1460
|
+
} else {
|
|
1461
|
+
// For bare IDs, prefer the current session's provider, then first available match
|
|
1462
|
+
const currentProvider = ctx.model?.provider;
|
|
1463
|
+
const exactProviderMatch = availableModels.find(
|
|
1464
|
+
m => m.id === modelId && m.provider === currentProvider,
|
|
1465
|
+
);
|
|
1466
|
+
const anyMatch = availableModels.find(m => m.id === modelId);
|
|
1467
|
+
model = exactProviderMatch ?? anyMatch;
|
|
1468
|
+
|
|
1469
|
+
// Warn if the ID is ambiguous across providers
|
|
1470
|
+
if (anyMatch && !exactProviderMatch) {
|
|
1471
|
+
const providers = availableModels
|
|
1472
|
+
.filter(m => m.id === modelId)
|
|
1473
|
+
.map(m => m.provider);
|
|
1474
|
+
if (providers.length > 1) {
|
|
1475
|
+
ctx.ui.notify(
|
|
1476
|
+
`Model ID "${modelId}" exists in multiple providers (${providers.join(", ")}). ` +
|
|
1477
|
+
`Resolved to ${anyMatch.provider}. Use "provider/model" format for explicit targeting.`,
|
|
1478
|
+
"warning",
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1422
1483
|
if (!model) {
|
|
1423
|
-
ctx.ui.notify(`Model ${modelId} not found in
|
|
1484
|
+
ctx.ui.notify(`Model ${modelId} not found in available models, trying fallback.`, "warning");
|
|
1424
1485
|
continue;
|
|
1425
1486
|
}
|
|
1426
1487
|
|
|
@@ -1696,13 +1757,11 @@ async function buildResearchMilestonePrompt(mid: string, midTitle: string, base:
|
|
|
1696
1757
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
1697
1758
|
|
|
1698
1759
|
const outputRelPath = relMilestoneFile(base, mid, "RESEARCH");
|
|
1699
|
-
const outputAbsPath = resolveMilestoneFile(base, mid, "RESEARCH") ?? join(base, outputRelPath);
|
|
1700
1760
|
return loadPrompt("research-milestone", {
|
|
1701
1761
|
milestoneId: mid, milestoneTitle: midTitle,
|
|
1702
1762
|
milestonePath: relMilestonePath(base, mid),
|
|
1703
1763
|
contextPath: contextRel,
|
|
1704
1764
|
outputPath: outputRelPath,
|
|
1705
|
-
outputAbsPath,
|
|
1706
1765
|
inlinedContext,
|
|
1707
1766
|
...buildSkillDiscoveryVars(),
|
|
1708
1767
|
});
|
|
@@ -1730,7 +1789,6 @@ async function buildPlanMilestonePrompt(mid: string, midTitle: string, base: str
|
|
|
1730
1789
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
1731
1790
|
|
|
1732
1791
|
const outputRelPath = relMilestoneFile(base, mid, "ROADMAP");
|
|
1733
|
-
const outputAbsPath = resolveMilestoneFile(base, mid, "ROADMAP") ?? join(base, outputRelPath);
|
|
1734
1792
|
const secretsOutputPath = relMilestoneFile(base, mid, "SECRETS");
|
|
1735
1793
|
return loadPrompt("plan-milestone", {
|
|
1736
1794
|
milestoneId: mid, milestoneTitle: midTitle,
|
|
@@ -1738,7 +1796,6 @@ async function buildPlanMilestonePrompt(mid: string, midTitle: string, base: str
|
|
|
1738
1796
|
contextPath: contextRel,
|
|
1739
1797
|
researchPath: researchRel,
|
|
1740
1798
|
outputPath: outputRelPath,
|
|
1741
|
-
outputAbsPath,
|
|
1742
1799
|
secretsOutputPath,
|
|
1743
1800
|
inlinedContext,
|
|
1744
1801
|
});
|
|
@@ -1770,7 +1827,6 @@ async function buildResearchSlicePrompt(
|
|
|
1770
1827
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
1771
1828
|
|
|
1772
1829
|
const outputRelPath = relSliceFile(base, mid, sid, "RESEARCH");
|
|
1773
|
-
const outputAbsPath = resolveSliceFile(base, mid, sid, "RESEARCH") ?? join(base, outputRelPath);
|
|
1774
1830
|
return loadPrompt("research-slice", {
|
|
1775
1831
|
milestoneId: mid, sliceId: sid, sliceTitle: sTitle,
|
|
1776
1832
|
slicePath: relSlicePath(base, mid, sid),
|
|
@@ -1778,7 +1834,6 @@ async function buildResearchSlicePrompt(
|
|
|
1778
1834
|
contextPath: contextRel,
|
|
1779
1835
|
milestoneResearchPath: milestoneResearchRel,
|
|
1780
1836
|
outputPath: outputRelPath,
|
|
1781
|
-
outputAbsPath,
|
|
1782
1837
|
inlinedContext,
|
|
1783
1838
|
dependencySummaries: depContent,
|
|
1784
1839
|
...buildSkillDiscoveryVars(),
|
|
@@ -1807,16 +1862,12 @@ async function buildPlanSlicePrompt(
|
|
|
1807
1862
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
1808
1863
|
|
|
1809
1864
|
const outputRelPath = relSliceFile(base, mid, sid, "PLAN");
|
|
1810
|
-
const outputAbsPath = resolveSliceFile(base, mid, sid, "PLAN") ?? join(base, outputRelPath);
|
|
1811
|
-
const sliceAbsPath = resolveSlicePath(base, mid, sid) ?? join(base, relSlicePath(base, mid, sid));
|
|
1812
1865
|
return loadPrompt("plan-slice", {
|
|
1813
1866
|
milestoneId: mid, sliceId: sid, sliceTitle: sTitle,
|
|
1814
1867
|
slicePath: relSlicePath(base, mid, sid),
|
|
1815
|
-
sliceAbsPath,
|
|
1816
1868
|
roadmapPath: roadmapRel,
|
|
1817
1869
|
researchPath: researchRel,
|
|
1818
1870
|
outputPath: outputRelPath,
|
|
1819
|
-
outputAbsPath,
|
|
1820
1871
|
inlinedContext,
|
|
1821
1872
|
dependencySummaries: depContent,
|
|
1822
1873
|
});
|
|
@@ -1867,8 +1918,7 @@ async function buildExecuteTaskPrompt(
|
|
|
1867
1918
|
|
|
1868
1919
|
const carryForwardSection = await buildCarryForwardSection(priorSummaries, base);
|
|
1869
1920
|
|
|
1870
|
-
const
|
|
1871
|
-
const taskSummaryAbsPath = join(sliceDirAbs, "tasks", `${tid}-SUMMARY.md`);
|
|
1921
|
+
const taskSummaryPath = `${relSlicePath(base, mid, sid)}/tasks/${tid}-SUMMARY.md`;
|
|
1872
1922
|
|
|
1873
1923
|
return loadPrompt("execute-task", {
|
|
1874
1924
|
milestoneId: mid, sliceId: sid, sliceTitle: sTitle, taskId: tid, taskTitle: tTitle,
|
|
@@ -1880,7 +1930,7 @@ async function buildExecuteTaskPrompt(
|
|
|
1880
1930
|
carryForwardSection,
|
|
1881
1931
|
resumeSection,
|
|
1882
1932
|
priorTaskLines: priorLines,
|
|
1883
|
-
|
|
1933
|
+
taskSummaryPath,
|
|
1884
1934
|
});
|
|
1885
1935
|
}
|
|
1886
1936
|
|
|
@@ -1916,17 +1966,17 @@ async function buildCompleteSlicePrompt(
|
|
|
1916
1966
|
|
|
1917
1967
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
1918
1968
|
|
|
1919
|
-
const
|
|
1920
|
-
const
|
|
1921
|
-
const
|
|
1969
|
+
const sliceRel = relSlicePath(base, mid, sid);
|
|
1970
|
+
const sliceSummaryPath = `${sliceRel}/${sid}-SUMMARY.md`;
|
|
1971
|
+
const sliceUatPath = `${sliceRel}/${sid}-UAT.md`;
|
|
1922
1972
|
|
|
1923
1973
|
return loadPrompt("complete-slice", {
|
|
1924
1974
|
milestoneId: mid, sliceId: sid, sliceTitle: sTitle,
|
|
1925
|
-
slicePath:
|
|
1975
|
+
slicePath: sliceRel,
|
|
1926
1976
|
roadmapPath: roadmapRel,
|
|
1927
1977
|
inlinedContext,
|
|
1928
|
-
|
|
1929
|
-
|
|
1978
|
+
sliceSummaryPath,
|
|
1979
|
+
sliceUatPath,
|
|
1930
1980
|
});
|
|
1931
1981
|
}
|
|
1932
1982
|
|
|
@@ -1965,15 +2015,14 @@ async function buildCompleteMilestonePrompt(
|
|
|
1965
2015
|
|
|
1966
2016
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
1967
2017
|
|
|
1968
|
-
const
|
|
1969
|
-
const milestoneSummaryAbsPath = join(milestoneDirAbs, `${mid}-SUMMARY.md`);
|
|
2018
|
+
const milestoneSummaryPath = `${relMilestonePath(base, mid)}/${mid}-SUMMARY.md`;
|
|
1970
2019
|
|
|
1971
2020
|
return loadPrompt("complete-milestone", {
|
|
1972
2021
|
milestoneId: mid,
|
|
1973
2022
|
milestoneTitle: midTitle,
|
|
1974
2023
|
roadmapPath: roadmapRel,
|
|
1975
2024
|
inlinedContext,
|
|
1976
|
-
|
|
2025
|
+
milestoneSummaryPath,
|
|
1977
2026
|
});
|
|
1978
2027
|
}
|
|
1979
2028
|
|
|
@@ -2016,8 +2065,7 @@ async function buildReplanSlicePrompt(
|
|
|
2016
2065
|
|
|
2017
2066
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
2018
2067
|
|
|
2019
|
-
const
|
|
2020
|
-
const replanAbsPath = join(sliceDirAbs, `${sid}-REPLAN.md`);
|
|
2068
|
+
const replanPath = `${relSlicePath(base, mid, sid)}/${sid}-REPLAN.md`;
|
|
2021
2069
|
|
|
2022
2070
|
return loadPrompt("replan-slice", {
|
|
2023
2071
|
milestoneId: mid,
|
|
@@ -2027,7 +2075,7 @@ async function buildReplanSlicePrompt(
|
|
|
2027
2075
|
planPath: slicePlanRel,
|
|
2028
2076
|
blockerTaskId,
|
|
2029
2077
|
inlinedContext,
|
|
2030
|
-
|
|
2078
|
+
replanPath,
|
|
2031
2079
|
});
|
|
2032
2080
|
}
|
|
2033
2081
|
|
|
@@ -2145,8 +2193,6 @@ async function buildRunUatPrompt(
|
|
|
2145
2193
|
|
|
2146
2194
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
2147
2195
|
|
|
2148
|
-
const sliceDirAbs = resolveSlicePath(base, mid, sliceId) ?? join(base, relSlicePath(base, mid, sliceId));
|
|
2149
|
-
const uatResultAbsPath = join(sliceDirAbs, `${sliceId}-UAT-RESULT.md`);
|
|
2150
2196
|
const uatResultPath = relSliceFile(base, mid, sliceId, "UAT-RESULT");
|
|
2151
2197
|
const uatType = extractUatType(uatContent) ?? "human-experience";
|
|
2152
2198
|
|
|
@@ -2154,7 +2200,6 @@ async function buildRunUatPrompt(
|
|
|
2154
2200
|
milestoneId: mid,
|
|
2155
2201
|
sliceId,
|
|
2156
2202
|
uatPath,
|
|
2157
|
-
uatResultAbsPath,
|
|
2158
2203
|
uatResultPath,
|
|
2159
2204
|
uatType,
|
|
2160
2205
|
inlinedContext,
|
|
@@ -2181,9 +2226,7 @@ async function buildReassessRoadmapPrompt(
|
|
|
2181
2226
|
|
|
2182
2227
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
2183
2228
|
|
|
2184
|
-
const
|
|
2185
|
-
const sliceDirAbs = resolveSlicePath(base, mid, completedSliceId) ?? join(base, relSlicePath(base, mid, completedSliceId));
|
|
2186
|
-
const assessmentAbsPath = join(sliceDirAbs, `${completedSliceId}-ASSESSMENT.md`);
|
|
2229
|
+
const assessmentPath = relSliceFile(base, mid, completedSliceId, "ASSESSMENT");
|
|
2187
2230
|
|
|
2188
2231
|
return loadPrompt("reassess-roadmap", {
|
|
2189
2232
|
milestoneId: mid,
|
|
@@ -2191,8 +2234,7 @@ async function buildReassessRoadmapPrompt(
|
|
|
2191
2234
|
completedSliceId,
|
|
2192
2235
|
roadmapPath: roadmapRel,
|
|
2193
2236
|
completedSliceSummaryPath: summaryRel,
|
|
2194
|
-
assessmentPath
|
|
2195
|
-
assessmentAbsPath,
|
|
2237
|
+
assessmentPath,
|
|
2196
2238
|
inlinedContext,
|
|
2197
2239
|
});
|
|
2198
2240
|
}
|
|
@@ -2790,6 +2832,23 @@ function verifyExpectedArtifact(unitType: string, unitId: string, base: string):
|
|
|
2790
2832
|
if (!absPath) return true;
|
|
2791
2833
|
if (!existsSync(absPath)) return false;
|
|
2792
2834
|
|
|
2835
|
+
// execute-task must also have its checkbox marked [x] in the slice plan
|
|
2836
|
+
if (unitType === "execute-task") {
|
|
2837
|
+
const parts = unitId.split("/");
|
|
2838
|
+
const mid = parts[0];
|
|
2839
|
+
const sid = parts[1];
|
|
2840
|
+
const tid = parts[2];
|
|
2841
|
+
if (mid && sid && tid) {
|
|
2842
|
+
const planAbs = resolveSliceFile(base, mid, sid, "PLAN");
|
|
2843
|
+
if (planAbs && existsSync(planAbs)) {
|
|
2844
|
+
const planContent = readFileSync(planAbs, "utf-8");
|
|
2845
|
+
const escapedTid = tid.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2846
|
+
const re = new RegExp(`^- \\[[xX]\\] \\*\\*${escapedTid}:`, "m");
|
|
2847
|
+
if (!re.test(planContent)) return false;
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2793
2852
|
// complete-slice must also produce a UAT file
|
|
2794
2853
|
if (unitType === "complete-slice") {
|
|
2795
2854
|
const parts = unitId.split("/");
|
|
@@ -2814,7 +2873,7 @@ function verifyExpectedArtifact(unitType: string, unitId: string, base: string):
|
|
|
2814
2873
|
export function writeBlockerPlaceholder(unitType: string, unitId: string, base: string, reason: string): string | null {
|
|
2815
2874
|
const absPath = resolveExpectedArtifactPath(unitType, unitId, base);
|
|
2816
2875
|
if (!absPath) return null;
|
|
2817
|
-
const dir =
|
|
2876
|
+
const dir = dirname(absPath);
|
|
2818
2877
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
2819
2878
|
const content = [
|
|
2820
2879
|
`# BLOCKER — auto-mode recovery failed`,
|
|
@@ -13,6 +13,61 @@ Full documentation for `~/.gsd/preferences.md` (global) and `.gsd/preferences.md
|
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
+
## Semantics
|
|
17
|
+
|
|
18
|
+
### Empty Arrays vs Omitted Fields
|
|
19
|
+
|
|
20
|
+
**Empty arrays (`[]`) are equivalent to omitting the field entirely.** During validation, GSD deletes empty arrays from the preferences object (see `validatePreferences()` in `preferences.ts`):
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
for (const key of ["always_use_skills", "prefer_skills", "avoid_skills", "custom_instructions"] as const) {
|
|
24
|
+
if (validated[key] && validated[key]!.length === 0) {
|
|
25
|
+
delete validated[key];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
These are functionally identical:
|
|
31
|
+
|
|
32
|
+
```yaml
|
|
33
|
+
# Explicit empty arrays — will be normalized away
|
|
34
|
+
prefer_skills: []
|
|
35
|
+
avoid_skills: []
|
|
36
|
+
skill_rules: []
|
|
37
|
+
|
|
38
|
+
# Omitted entirely — same result
|
|
39
|
+
# (just don't write these fields)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Recommendation:** Omit fields you don't need. Empty arrays add noise with no effect.
|
|
43
|
+
|
|
44
|
+
### Global vs Project Preferences
|
|
45
|
+
|
|
46
|
+
Preferences are loaded from two locations and merged:
|
|
47
|
+
|
|
48
|
+
1. **Global:** `~/.gsd/preferences.md` — applies to all projects
|
|
49
|
+
2. **Project:** `.gsd/preferences.md` — applies to the current project only
|
|
50
|
+
|
|
51
|
+
**Merge behavior** (see `mergePreferences()` in `preferences.ts`):
|
|
52
|
+
- **Scalar fields** (`skill_discovery`, `budget_ceiling`, etc.): Project wins if defined, otherwise global. Uses nullish coalescing (`??`).
|
|
53
|
+
- **Array fields** (`always_use_skills`, `prefer_skills`, etc.): Concatenated via `mergeStringLists()` (global first, then project).
|
|
54
|
+
- **Object fields** (`models`, `git`, `auto_supervisor`): Shallow merge via spread operator `{ ...base, ...override }`.
|
|
55
|
+
|
|
56
|
+
For `models`, project settings override global at the phase level. If global has `planning: opus` and project has `planning: sonnet`, the project wins. But if project omits `research`, global's `research` setting is preserved.
|
|
57
|
+
|
|
58
|
+
### Skill Discovery vs Skill Preferences
|
|
59
|
+
|
|
60
|
+
These are **separate concerns**:
|
|
61
|
+
|
|
62
|
+
| Field | What it controls | Code reference |
|
|
63
|
+
|-------|-----------------|----------------|
|
|
64
|
+
| `skill_discovery` | **Whether** GSD looks for relevant skills during research | `resolveSkillDiscoveryMode()` in `preferences.ts` |
|
|
65
|
+
| `always_use_skills`, `prefer_skills`, `avoid_skills` | **Which** skills to use when they're found relevant | `renderPreferencesForSystemPrompt()` in `preferences.ts` |
|
|
66
|
+
|
|
67
|
+
Setting `prefer_skills: []` does **not** disable skill discovery — it just means you have no preference overrides. Use `skill_discovery: off` to disable discovery entirely.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
16
71
|
## Field Guide
|
|
17
72
|
|
|
18
73
|
- `version`: schema version. Start at `1`.
|
|
@@ -60,6 +115,27 @@ Full documentation for `~/.gsd/preferences.md` (global) and `.gsd/preferences.md
|
|
|
60
115
|
- Use `skill_rules` for situational routing, not broad personality preferences.
|
|
61
116
|
- Prefer skill names for stable built-in skills.
|
|
62
117
|
- Prefer absolute paths for local personal skills.
|
|
118
|
+
- **Omit fields you don't need** — empty arrays add noise with no effect.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Minimal Example
|
|
123
|
+
|
|
124
|
+
The cleanest preferences file only specifies what you actually want:
|
|
125
|
+
|
|
126
|
+
```yaml
|
|
127
|
+
---
|
|
128
|
+
version: 1
|
|
129
|
+
always_use_skills:
|
|
130
|
+
- debug-like-expert
|
|
131
|
+
skill_discovery: suggest
|
|
132
|
+
models:
|
|
133
|
+
planning: claude-opus-4-6
|
|
134
|
+
execution: claude-sonnet-4-6
|
|
135
|
+
---
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Everything else uses defaults. No `prefer_skills: []`, no `avoid_skills: []`, no `auto_supervisor: {}` — those are just noise.
|
|
63
139
|
|
|
64
140
|
---
|
|
65
141
|
|