Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/cli/src/tests/exec-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ function createSettings(
telemetryEnabled: false,
permissions,
enabledSkills: {},
enabledSkillsDefaultOff: false,
statusline: { enabled: false, refreshMs: 1000, separator: " | ", providers: [] },
};
}
Expand Down
12 changes: 9 additions & 3 deletions packages/core/src/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export type PromptToolOptions = {

type DefaultSkillPromptOptions = {
enabledSkills?: Record<string, boolean>;
enabledSkillsDefaultOff?: boolean;
};

const DEFAULT_SKILL_TEMPLATES = ["karpathy-guidelines.md"];
Expand Down Expand Up @@ -161,13 +162,18 @@ function readToolDocs(extensionRoot: string, options: PromptToolOptions = {}): s

function readDefaultSkillDocs(
extensionRoot: string,
enabledSkills: Record<string, boolean> = {}
enabledSkills: Record<string, boolean> = {},
enabledSkillsDefaultOff = false
): Array<{ name: string; content: string }> {
const skillsDir = path.join(extensionRoot, "templates", "skills");
return DEFAULT_SKILL_TEMPLATES.map((entry) => {
const fullPath = path.join(skillsDir, entry);
const name = path.basename(entry, ".md");
if (enabledSkills[name] === false) {
// Whitelist mode (#178): when enabledSkillsDefaultOff is set, only skills
// explicitly enabled with `true` are injected; otherwise exclude only
// skills explicitly disabled with `false`.
const excluded = enabledSkillsDefaultOff ? enabledSkills[name] !== true : enabledSkills[name] === false;
if (excluded) {
return null;
}
try {
Expand All @@ -182,7 +188,7 @@ function readDefaultSkillDocs(
}

export function getDefaultSkillPrompt(options: DefaultSkillPromptOptions = {}): string {
const skillDocs = readDefaultSkillDocs(getExtensionRoot(), options.enabledSkills);
const skillDocs = readDefaultSkillDocs(getExtensionRoot(), options.enabledSkills, options.enabledSkillsDefaultOff);
if (skillDocs.length === 0) {
return "";
}
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,7 @@ export class SessionManager {
mcpServers?: Record<string, McpServerConfig>;
permissions?: Required<PermissionSettings>;
enabledSkills?: Record<string, boolean>;
enabledSkillsDefaultOff?: boolean;
};
private readonly onAssistantMessage: (message: SessionMessage, shouldConnect: boolean) => void;
private readonly onSessionEntryUpdated?: (entry: SessionEntry) => void;
Expand Down Expand Up @@ -1181,7 +1182,10 @@ ${agentInstructions}
const systemMessage = this.buildSystemMessage(sessionId, systemPrompt);
this.appendSessionMessage(sessionId, systemMessage);

const defaultSkillPrompt = getDefaultSkillPrompt({ enabledSkills: this.getResolvedSettings().enabledSkills });
const defaultSkillPrompt = getDefaultSkillPrompt({
enabledSkills: this.getResolvedSettings().enabledSkills,
enabledSkillsDefaultOff: this.getResolvedSettings().enabledSkillsDefaultOff,
});
if (defaultSkillPrompt) {
const defaultSkillMessage = this.buildSystemMessage(sessionId, defaultSkillPrompt);
this.appendSessionMessage(sessionId, defaultSkillMessage);
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export type DeepcodingSettings = {
mcpServers?: Record<string, McpServerConfig>;
permissions?: PermissionSettings;
enabledSkills?: EnabledSkillsSettings;
enabledSkillsDefaultOff?: boolean;
statusline?: StatusLineSettings;
};

Expand All @@ -115,6 +116,7 @@ export type ResolvedDeepcodingSettings = {
mcpServers?: Record<string, McpServerConfig>;
permissions: Required<PermissionSettings>;
enabledSkills: EnabledSkillsSettings;
enabledSkillsDefaultOff: boolean;
statusline: ResolvedStatusLineSettings;
};

Expand Down Expand Up @@ -585,6 +587,12 @@ export function resolveSettingsSources(
trimString(userSettings?.webSearchTool) ||
"";

const enabledSkillsDefaultOff =
parseBoolean(systemEnv.ENABLED_SKILLS_DEFAULT_OFF) ??
parseBoolean(projectSettings?.enabledSkillsDefaultOff) ??
parseBoolean(userSettings?.enabledSkillsDefaultOff) ??
false;

return {
env,
apiKey: trimString(env.API_KEY) || undefined,
Expand All @@ -602,6 +610,7 @@ export function resolveSettingsSources(
mcpServers: mergeMcpServers(userSettings, projectSettings, userEnv, projectEnv, systemEnv),
permissions: mergePermissions(userSettings, projectSettings),
enabledSkills: mergeEnabledSkills(userSettings, projectSettings),
enabledSkillsDefaultOff,
statusline: mergeStatusLine(userSettings, projectSettings),
};
}
Expand Down
18 changes: 18 additions & 0 deletions packages/core/src/tests/prompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,24 @@ test("getTools includes UpdatePlan with string plan schema", () => {
assert.equal((tool.function.parameters.properties.plan as { type?: unknown }).type, "string");
});

test("getDefaultSkillPrompt honors whitelist mode via enabledSkillsDefaultOff (#178)", () => {
const name = "karpathy-guidelines";
const defaultPrompt = getDefaultSkillPrompt({});
const defaultOffPrompt = getDefaultSkillPrompt({ enabledSkillsDefaultOff: true });
const explicitOnPrompt = getDefaultSkillPrompt({
enabledSkillsDefaultOff: true,
enabledSkills: { [name]: true },
});
const explicitOffPrompt = getDefaultSkillPrompt({ enabledSkills: { [name]: false } });

assert.equal(defaultOffPrompt.length, 0, "whitelist mode with no enabled skills injects nothing");
if (defaultPrompt) {
assert.equal(defaultPrompt.includes(name), true, "default mode injects karpathy-guidelines");
assert.equal(explicitOnPrompt.includes(name), true, "whitelist mode injects explicitly enabled skill");
assert.equal(explicitOffPrompt.length, 0, "explicit false excludes the skill in default mode");
}
});

test("getTools requires bash sideEffects permission scopes", () => {
const tool = getTools().find((candidate) => candidate.function.name === "bash");
assert.ok(tool);
Expand Down
16 changes: 16 additions & 0 deletions packages/core/src/tests/settings-and-notify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@ import { applyModelConfigSelection, resolveSettings, resolveSettingsSources } fr

const TEST_PROCESS_ENV = {};

test("resolveSettings reads enabledSkillsDefaultOff from settings and env (#178)", () => {
const resolved = resolveSettings(
{ enabledSkillsDefaultOff: true, env: { API_KEY: "sk-test" } },
{ model: "default-model", baseURL: "https://default.example.com" },
TEST_PROCESS_ENV
);
assert.equal(resolved.enabledSkillsDefaultOff, true);

const fromEnv = resolveSettings(
{ env: { API_KEY: "sk-test" } },
{ model: "default-model", baseURL: "https://default.example.com" },
{ DEEPCODE_ENABLED_SKILLS_DEFAULT_OFF: "true" }
);
assert.equal(fromEnv.enabledSkillsDefaultOff, true);
});

test("resolveSettings reads top-level thinkingEnabled, notify, and webSearchTool", () => {
const resolved = resolveSettings(
{
Expand Down