@compilr-dev/sdk 0.24.2 → 0.25.1

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/agent.js CHANGED
@@ -15,6 +15,8 @@ import { CapabilityManager } from './capabilities/manager.js';
15
15
  import { CapabilityContext } from './capabilities/context.js';
16
16
  import { createCapabilityHook } from './capabilities/hook.js';
17
17
  import { createLoadCapabilityTool } from './capabilities/load-tool.js';
18
+ import { createLoadSkillTool } from './skills/load-tool.js';
19
+ import { generateSkillCatalog, toCatalogEntry } from './skills/catalog.js';
18
20
  import { resolveProfileGroups, resolveUpfrontGroups } from './capabilities/profile-resolver.js';
19
21
  import { MetaToolsRegistry, createMetaTools } from './meta-tools/registry.js';
20
22
  /**
@@ -116,13 +118,40 @@ class CompilrAgentImpl {
116
118
  else {
117
119
  systemPrompt = preset.systemPrompt;
118
120
  }
121
+ /*
122
+ Skills the model may choose from. The host has already filtered these for this agent — see
123
+ `CompilrAgentConfig.skills`. Two things follow: a catalogue of names and DESCRIPTIONS in the
124
+ prompt, and `load_skill` to fetch a body on demand. Bodies never go in the prompt; `design`
125
+ alone is 3,724 characters.
126
+ */
127
+ const eligibleSkills = config?.skills ?? [];
128
+ const loadedSkillNames = [];
119
129
  // Assemble all tools from preset + config
120
130
  let allTools = deduplicateTools(assembleTools(preset, config?.tools));
131
+ if (eligibleSkills.length > 0) {
132
+ allTools = [
133
+ ...allTools,
134
+ createLoadSkillTool({
135
+ // Re-read on every call: an edit rebuilds the agent, so this closure is rebuilt too,
136
+ // but reading through the array keeps the tool honest if that ever stops being true.
137
+ available: () => eligibleSkills,
138
+ onLoaded: (name) => {
139
+ if (!loadedSkillNames.includes(name))
140
+ loadedSkillNames.push(name);
141
+ },
142
+ }),
143
+ ];
144
+ }
121
145
  // Replace default suggest tool with callback-wired version when onSuggest is provided
122
146
  if (config?.onSuggest) {
123
147
  const wiredSuggest = createSuggestTool({ onSuggest: config.onSuggest });
124
148
  allTools = allTools.map((t) => t.definition.name === 'suggest' ? wiredSuggest : t);
125
149
  }
150
+ if (eligibleSkills.length > 0) {
151
+ const catalog = generateSkillCatalog(eligibleSkills.map(toCatalogEntry), loadedSkillNames);
152
+ if (catalog)
153
+ systemPrompt = systemPrompt ? `${systemPrompt}\n\n${catalog}` : catalog;
154
+ }
126
155
  // Build context manager if configured
127
156
  let contextManager;
128
157
  const extendedContext = config?.context?.extendedContext ?? false;
package/dist/config.d.ts CHANGED
@@ -5,6 +5,7 @@ import type { LLMProvider, Message, Tool, ToolPermission, HooksConfig, AnchorInp
5
5
  import type { Preset } from './presets/types.js';
6
6
  import type { ToolProfile } from './team/tool-config.js';
7
7
  import type { ConditionalModule } from './capabilities/hook.js';
8
+ import type { CustomSkill } from './skills/types.js';
8
9
  /**
9
10
  * Dynamic capability loading configuration.
10
11
  *
@@ -298,6 +299,18 @@ export interface CompilrAgentConfig {
298
299
  * Omit or set enabled: false for the legacy all-tools-upfront mode.
299
300
  */
300
301
  capabilities?: CapabilitiesConfig;
302
+ /**
303
+ * Skills this agent may load, ALREADY filtered for eligibility by the host.
304
+ *
305
+ * ⚠️ THE HOST FILTERS, THE SDK PRESENTS — the same split as MCP grants. The host owns the
306
+ * skill directories and knows which agent it is building (the factory receives the TeamAgent),
307
+ * so it applies `resolveSkillsForAgent`; the SDK turns the result into a catalogue the model
308
+ * reads and a `load_skill` tool it can call. Passing an unfiltered pool here would hand every
309
+ * agent every skill, which is the defect this whole line of work started from.
310
+ *
311
+ * Omit for no skills — the catalogue is then absent rather than empty.
312
+ */
313
+ skills?: CustomSkill[];
301
314
  }
302
315
  /**
303
316
  * High-level agent interface
package/dist/index.d.ts CHANGED
@@ -68,9 +68,11 @@ export { createPlatformTools, createProjectTools, createWorkItemTools, createDoc
68
68
  export type { ProjectAnchorStoreConfig, FileArtifactServiceConfig, ImageToolsConfig, ImageResizer, } from './platform/index.js';
69
69
  export { STEP_ORDER, GUIDED_STEP_CRITERIA, getNextStep, isValidTransition, getStepCriteria, formatStepDisplay, getStepNumber, } from './platform/index.js';
70
70
  export type { CustomSkill, CompilrSkillExtension, ForkedFromMarker, SkillEligibilityContext, SkillCollision, SkillDiffLine, SkillValidationIssue, ScopeConfig, SkillResolution, } from './skills/index.js';
71
- export { RESERVED_SKILL_NAMES, isReservedSkillName, parseSkillMarkdown, loadSkillsFromDir, resolveLayeredSkills, resolveSkillsForAgent, detectCollisions, formatCollisionWarnings, diffForkVsUpstream, buildForkContent, buildNewSkillContent, validateSkill as validateSkillQuality, getSkillsDir, getSkillFolder, getSkillFile, ensureSkillsDir, isValidSkillName, getScopeConfigPath, readSkillScopeConfig, readSkillScopeConfigSync, writeSkillScopeConfig, getSkillBindings, resolveSkillBinding, } from './skills/index.js';
71
+ export { RESERVED_SKILL_NAMES, isReservedSkillName, parseSkillMarkdown, loadSkillsFromDir, loadInstalledSkills, resolveLayeredSkills, resolveSkillsForAgent, resolveSkillsForTeamAgent, detectCollisions, formatCollisionWarnings, diffForkVsUpstream, buildForkContent, buildNewSkillContent, validateSkill as validateSkillQuality, getSkillsDir, getSkillFolder, getSkillFile, ensureSkillsDir, isValidSkillName, getScopeConfigPath, readSkillScopeConfig, readSkillScopeConfigSync, writeSkillScopeConfig, getSkillBindings, resolveSkillBinding, } from './skills/index.js';
72
72
  export type { SkillScope, SkillPromptResolution, AvailableSkillEntry, SkillSources, } from './skills/index.js';
73
73
  export { resolveSkillPrompt, getAllAvailableSkills } from './skills/index.js';
74
+ export { generateSkillCatalog, toCatalogEntry, createLoadSkillTool } from './skills/index.js';
75
+ export type { SkillCatalogEntry, SkillSource } from './skills/index.js';
74
76
  export { platformSkills, designSkill, sketchSkill, prdSkill, refineSkill, refineItemSkill, architectureSkill, sessionNotesSkill, buildSkill, scaffoldSkill, outlineSkill, literatureReviewSkill, draftSectionSkill, peerReviewSkill, researchScaffoldSkill, businessVisionSkill, marketAnalysisSkill, competitorAnalysisSkill, financialModelSkill, pitchOutlineSkill, businessReviewSkill, brandSetupSkill, contentStrategySkill, contentCalendarSkill, createContentSkill, contentReviewSkill, curriculumDesignSkill, lessonPlanSkill, assessmentDesignSkill, courseReviewSkill, bookOutlineSkill, characterDesignSkill, plotThreadsSkill, sceneBreakdownSkill, bookReviewSkill, } from './skills/index.js';
75
77
  export { ACTION_REGISTRY, getActionsForContext, getActionById, resolveActionPrompt, buildContextSummary, getSuggestedRole, } from './actions/index.js';
76
78
  export type { ActionContext, ActionDefinition } from './actions/index.js';
package/dist/index.js CHANGED
@@ -152,8 +152,9 @@ export { createPlatformTools, createProjectTools, createWorkItemTools, createDoc
152
152
  // Platform Workflow (pure step-criteria logic)
153
153
  // =============================================================================
154
154
  export { STEP_ORDER, GUIDED_STEP_CRITERIA, getNextStep, isValidTransition, getStepCriteria, formatStepDisplay, getStepNumber, } from './platform/index.js';
155
- export { RESERVED_SKILL_NAMES, isReservedSkillName, parseSkillMarkdown, loadSkillsFromDir, resolveLayeredSkills, resolveSkillsForAgent, detectCollisions, formatCollisionWarnings, diffForkVsUpstream, buildForkContent, buildNewSkillContent, validateSkill as validateSkillQuality, getSkillsDir, getSkillFolder, getSkillFile, ensureSkillsDir, isValidSkillName, getScopeConfigPath, readSkillScopeConfig, readSkillScopeConfigSync, writeSkillScopeConfig, getSkillBindings, resolveSkillBinding, } from './skills/index.js';
155
+ export { RESERVED_SKILL_NAMES, isReservedSkillName, parseSkillMarkdown, loadSkillsFromDir, loadInstalledSkills, resolveLayeredSkills, resolveSkillsForAgent, resolveSkillsForTeamAgent, detectCollisions, formatCollisionWarnings, diffForkVsUpstream, buildForkContent, buildNewSkillContent, validateSkill as validateSkillQuality, getSkillsDir, getSkillFolder, getSkillFile, ensureSkillsDir, isValidSkillName, getScopeConfigPath, readSkillScopeConfig, readSkillScopeConfigSync, writeSkillScopeConfig, getSkillBindings, resolveSkillBinding, } from './skills/index.js';
156
156
  export { resolveSkillPrompt, getAllAvailableSkills } from './skills/index.js';
157
+ export { generateSkillCatalog, toCatalogEntry, createLoadSkillTool } from './skills/index.js';
157
158
  export { platformSkills, designSkill, sketchSkill, prdSkill, refineSkill, refineItemSkill, architectureSkill, sessionNotesSkill, buildSkill, scaffoldSkill, outlineSkill, literatureReviewSkill, draftSectionSkill, peerReviewSkill, researchScaffoldSkill, businessVisionSkill, marketAnalysisSkill, competitorAnalysisSkill, financialModelSkill, pitchOutlineSkill, businessReviewSkill, brandSetupSkill, contentStrategySkill, contentCalendarSkill, createContentSkill, contentReviewSkill, curriculumDesignSkill, lessonPlanSkill, assessmentDesignSkill, courseReviewSkill, bookOutlineSkill, characterDesignSkill, plotThreadsSkill, sceneBreakdownSkill, bookReviewSkill, } from './skills/index.js';
158
159
  // =============================================================================
159
160
  // Contextual Actions (skill invocations with context)
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Skill catalogue — what the model reads to decide which skill to load.
3
+ *
4
+ * ⚠️ THIS IS THE PIECE THAT MAKES A SKILL A SKILL. Until it existed, `SKILL.md` files were
5
+ * user-invoked prompt macros: the host pasted a body into the chat when someone typed a slash
6
+ * command, the model never saw a list, and `description` — the field the Anthropic format exists
7
+ * to make the model read — was decoration. Every per-agent skill setting in the product was
8
+ * therefore inert, because there was nothing for it to restrict.
9
+ *
10
+ * Modelled deliberately on `capabilities/catalog.ts`: the same shape already works for tools
11
+ * (`## Available Capabilities` + `load_capability`). Skills are that pattern with prompts.
12
+ *
13
+ * ⚠️ DESCRIPTIONS ONLY, NEVER BODIES. A body is what `load_skill` returns. Putting bodies here
14
+ * would be the mistake the capability system was built to avoid — measured on the same codebase,
15
+ * making language packs lazy saved ~8.4K tokens per turn. `design` alone is 3,724 characters.
16
+ */
17
+ import type { CustomSkill } from './types.js';
18
+ /** A skill as the model sees it before loading: what it is for, not what it says. */
19
+ export interface SkillCatalogEntry {
20
+ name: string;
21
+ description: string;
22
+ }
23
+ /**
24
+ * Build the system-prompt section listing the skills this agent may load.
25
+ *
26
+ * @param skills - the skills this agent is eligible for, already filtered by
27
+ * `resolveSkillsForAgent` (grants, targeting, project type, tool guards)
28
+ * @param loadedNames - skills already loaded this session, so the model does not reload them
29
+ * @returns the section, or '' when there is nothing to offer — an empty heading reads as a
30
+ * capability the agent has and cannot use
31
+ */
32
+ export declare function generateSkillCatalog(skills: readonly SkillCatalogEntry[], loadedNames?: readonly string[]): string;
33
+ /** Narrow a resolved skill to what the catalogue publishes. */
34
+ export declare function toCatalogEntry(skill: CustomSkill): SkillCatalogEntry;
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Skill catalogue — what the model reads to decide which skill to load.
3
+ *
4
+ * ⚠️ THIS IS THE PIECE THAT MAKES A SKILL A SKILL. Until it existed, `SKILL.md` files were
5
+ * user-invoked prompt macros: the host pasted a body into the chat when someone typed a slash
6
+ * command, the model never saw a list, and `description` — the field the Anthropic format exists
7
+ * to make the model read — was decoration. Every per-agent skill setting in the product was
8
+ * therefore inert, because there was nothing for it to restrict.
9
+ *
10
+ * Modelled deliberately on `capabilities/catalog.ts`: the same shape already works for tools
11
+ * (`## Available Capabilities` + `load_capability`). Skills are that pattern with prompts.
12
+ *
13
+ * ⚠️ DESCRIPTIONS ONLY, NEVER BODIES. A body is what `load_skill` returns. Putting bodies here
14
+ * would be the mistake the capability system was built to avoid — measured on the same codebase,
15
+ * making language packs lazy saved ~8.4K tokens per turn. `design` alone is 3,724 characters.
16
+ */
17
+ /**
18
+ * Build the system-prompt section listing the skills this agent may load.
19
+ *
20
+ * @param skills - the skills this agent is eligible for, already filtered by
21
+ * `resolveSkillsForAgent` (grants, targeting, project type, tool guards)
22
+ * @param loadedNames - skills already loaded this session, so the model does not reload them
23
+ * @returns the section, or '' when there is nothing to offer — an empty heading reads as a
24
+ * capability the agent has and cannot use
25
+ */
26
+ export function generateSkillCatalog(skills, loadedNames = []) {
27
+ if (skills.length === 0)
28
+ return '';
29
+ const lines = [
30
+ '## Available Skills',
31
+ '',
32
+ 'Procedures you can load when a task calls for one. Read the descriptions and call ' +
33
+ '`load_skill` with the name when it fits what the user is asking for — do not load one ' +
34
+ 'speculatively, and do not describe a skill to the user instead of using it.',
35
+ '',
36
+ ];
37
+ for (const skill of skills) {
38
+ lines.push(`- **${skill.name}** — ${collapse(skill.description)}`);
39
+ }
40
+ if (loadedNames.length > 0) {
41
+ lines.push('');
42
+ lines.push(`Already loaded: ${loadedNames.join(', ')}`);
43
+ }
44
+ return lines.join('\n');
45
+ }
46
+ /**
47
+ * Anthropic-format descriptions are deliberately long and often multi-line — the format asks for
48
+ * trigger conditions and example phrasings. A newline inside a list item breaks the list, so the
49
+ * catalogue reads as prose and the model loses the one-skill-per-line structure it is scanning.
50
+ */
51
+ function collapse(description) {
52
+ return description.replace(/\s*\n+\s*/g, ' ').trim();
53
+ }
54
+ /** Narrow a resolved skill to what the catalogue publishes. */
55
+ export function toCatalogEntry(skill) {
56
+ return { name: skill.name, description: skill.description };
57
+ }
@@ -3,9 +3,13 @@
3
3
  */
4
4
  export type { CustomSkill, CompilrSkillExtension, ForkedFromMarker } from './types.js';
5
5
  export { RESERVED_SKILL_NAMES, isReservedSkillName } from './types.js';
6
- export { parseSkillMarkdown, loadSkillsFromDir } from './loader.js';
6
+ export { parseSkillMarkdown, loadSkillsFromDir, loadInstalledSkills } from './loader.js';
7
7
  export type { SkillEligibilityContext } from './resolver.js';
8
- export { resolveLayeredSkills, resolveSkillsForAgent } from './resolver.js';
8
+ export { resolveLayeredSkills, resolveSkillsForAgent, resolveSkillsForTeamAgent, } from './resolver.js';
9
+ export { generateSkillCatalog, toCatalogEntry } from './catalog.js';
10
+ export type { SkillCatalogEntry } from './catalog.js';
11
+ export { createLoadSkillTool } from './load-tool.js';
12
+ export type { SkillSource } from './load-tool.js';
9
13
  export type { SkillCollision, SkillDiffLine, SkillValidationIssue, ScopeConfig, SkillResolution, } from './operations.js';
10
14
  export { detectCollisions, formatCollisionWarnings, diffForkVsUpstream, buildForkContent, buildNewSkillContent, validateSkill, } from './operations.js';
11
15
  export type { SkillScope } from './paths.js';
@@ -1,6 +1,8 @@
1
1
  export { RESERVED_SKILL_NAMES, isReservedSkillName } from './types.js';
2
- export { parseSkillMarkdown, loadSkillsFromDir } from './loader.js';
3
- export { resolveLayeredSkills, resolveSkillsForAgent } from './resolver.js';
2
+ export { parseSkillMarkdown, loadSkillsFromDir, loadInstalledSkills } from './loader.js';
3
+ export { resolveLayeredSkills, resolveSkillsForAgent, resolveSkillsForTeamAgent, } from './resolver.js';
4
+ export { generateSkillCatalog, toCatalogEntry } from './catalog.js';
5
+ export { createLoadSkillTool } from './load-tool.js';
4
6
  export { detectCollisions, formatCollisionWarnings, diffForkVsUpstream, buildForkContent, buildNewSkillContent, validateSkill, } from './operations.js';
5
7
  export { getSkillsDir, getSkillFolder, getSkillFile, ensureSkillsDir, isValidSkillName, getScopeConfigPath, readScopeConfig as readSkillScopeConfig, readScopeConfigSync as readSkillScopeConfigSync, writeScopeConfig as writeSkillScopeConfig, getAllBindings as getSkillBindings, resolveBinding as resolveSkillBinding, } from './paths.js';
6
8
  export { resolveSkillPrompt, getAllAvailableSkills } from './prompt-resolver.js';
@@ -0,0 +1,30 @@
1
+ /**
2
+ * load_skill — how an agent reaches a skill body.
3
+ *
4
+ * ⚠️ BEFORE THIS TOOL THERE WAS NO PATH FROM AN AGENT TO A SKILL. Skills were delivered only by
5
+ * the HOST, when a user typed a slash command; the body arrived as a user message and the model
6
+ * had no way to ask for one. `canvas_guide` exists precisely as a hand-built escape hatch around
7
+ * that gap, exposing 8 of ~48 bodies, and its own comment says so. Meanwhile ~30 platform prompts
8
+ * instruct the model to "use the refine skill" — advice it could only relay to the user.
9
+ *
10
+ * Deliberately unlike `load_capability`, which loads a pack and lets the NEXT turn see new tools:
11
+ * a skill body is content, not capability, so it is returned directly and is usable in the same
12
+ * turn. Nothing needs to be rebuilt.
13
+ */
14
+ import type { Tool } from '@compilr-dev/agents';
15
+ interface LoadSkillInput {
16
+ skill: string;
17
+ }
18
+ /** What `load_skill` can reach: the skills this agent is eligible for, resolved per turn. */
19
+ export interface SkillSource {
20
+ /** Eligible skills — already filtered by grants, targeting, project type and tool guards. */
21
+ available: () => {
22
+ name: string;
23
+ description: string;
24
+ prompt: string;
25
+ }[];
26
+ /** Called when a skill is successfully loaded, so the catalogue can mark it. */
27
+ onLoaded?: (name: string) => void;
28
+ }
29
+ export declare function createLoadSkillTool(source: SkillSource): Tool<LoadSkillInput>;
30
+ export {};
@@ -0,0 +1,53 @@
1
+ /**
2
+ * load_skill — how an agent reaches a skill body.
3
+ *
4
+ * ⚠️ BEFORE THIS TOOL THERE WAS NO PATH FROM AN AGENT TO A SKILL. Skills were delivered only by
5
+ * the HOST, when a user typed a slash command; the body arrived as a user message and the model
6
+ * had no way to ask for one. `canvas_guide` exists precisely as a hand-built escape hatch around
7
+ * that gap, exposing 8 of ~48 bodies, and its own comment says so. Meanwhile ~30 platform prompts
8
+ * instruct the model to "use the refine skill" — advice it could only relay to the user.
9
+ *
10
+ * Deliberately unlike `load_capability`, which loads a pack and lets the NEXT turn see new tools:
11
+ * a skill body is content, not capability, so it is returned directly and is usable in the same
12
+ * turn. Nothing needs to be rebuilt.
13
+ */
14
+ import { defineTool, createSuccessResult, createErrorResult } from '@compilr-dev/agents';
15
+ export function createLoadSkillTool(source) {
16
+ return defineTool({
17
+ name: 'load_skill',
18
+ description: 'Load a skill: returns the full procedure for a task. ' +
19
+ 'See the "Available Skills" section in your instructions for names and what each is for. ' +
20
+ 'Load one when the user asks for something it covers, then follow it.',
21
+ inputSchema: {
22
+ type: 'object',
23
+ properties: {
24
+ skill: {
25
+ type: 'string',
26
+ description: 'Skill name exactly as listed in "Available Skills".',
27
+ },
28
+ },
29
+ required: ['skill'],
30
+ },
31
+ execute: (input) => {
32
+ const { skill } = input;
33
+ const available = source.available();
34
+ const found = available.find((s) => s.name === skill);
35
+ if (!found) {
36
+ /*
37
+ ⚠️ NAME WHAT IS REACHABLE. A bare "not found" leaves the model guessing between "typo",
38
+ "not granted to me" and "does not exist" — and the usual recovery from guessing is to
39
+ invent a plausible name and try again. The eligible list is short by construction.
40
+ */
41
+ const names = available.map((s) => s.name);
42
+ return Promise.resolve(createErrorResult(names.length === 0
43
+ ? `No skills are available to you. Do not retry — proceed without one.`
44
+ : `No skill named "${skill}" is available to you. Available: ${names.join(', ')}.`));
45
+ }
46
+ source.onLoaded?.(found.name);
47
+ return Promise.resolve(createSuccessResult({
48
+ skill: found.name,
49
+ instructions: found.prompt,
50
+ }));
51
+ },
52
+ });
53
+ }
@@ -28,3 +28,19 @@ export declare function parseSkillMarkdown(content: string, sourcePath?: string,
28
28
  * Returns [] if the directory doesn't exist.
29
29
  */
30
30
  export declare function loadSkillsFromDir(dir: string, source?: CustomSkill['source'], onWarn?: (msg: string) => void): Promise<CustomSkill[]>;
31
+ /**
32
+ * Every installed skill an agent could be granted: the user's and the project's, project first.
33
+ *
34
+ * ⚠️ PLATFORM SKILLS ARE NOT IN HERE, DELIBERATELY. The 42 shipped prompts are Commands — modes a
35
+ * user forces (`You are in DESIGN MODE`), not capabilities a model selects. Putting them in the
36
+ * catalogue would invite the model to load a mode nobody asked for, and would cost catalogue
37
+ * tokens for 42 entries that are already reachable by typing. See the Commands/Skills split.
38
+ *
39
+ * ⚠️ SYNC, BECAUSE AGENT CONSTRUCTION IS. Both hosts build agents inside a factory callback that
40
+ * is not a good place to await disk I/O on every rebuild. The directories are small and the
41
+ * result is intended to be cached by the host and refreshed when skills change.
42
+ *
43
+ * Project scope wins on a name clash, matching `resolveSkillPrompt`'s precedence — a project can
44
+ * override a personal skill, and the two hosts must agree about that.
45
+ */
46
+ export declare function loadInstalledSkills(projectDir?: string): CustomSkill[];
@@ -11,8 +11,9 @@
11
11
  *
12
12
  * Spec: /workspace/project-docs/00-requirements/compilr-dev-sdk/custom-skills-spec.md
13
13
  */
14
- import { promises as fs } from 'node:fs';
14
+ import { promises as fs, readdirSync, readFileSync } from 'node:fs';
15
15
  import { join } from 'node:path';
16
+ import { getSkillsDir } from './paths.js';
16
17
  import { parse as parseYamlReal } from 'yaml';
17
18
  /**
18
19
  * Parse a SKILL.md string into a CustomSkill.
@@ -131,3 +132,60 @@ function parseYaml(text) {
131
132
  return null;
132
133
  }
133
134
  }
135
+ /**
136
+ * Every installed skill an agent could be granted: the user's and the project's, project first.
137
+ *
138
+ * ⚠️ PLATFORM SKILLS ARE NOT IN HERE, DELIBERATELY. The 42 shipped prompts are Commands — modes a
139
+ * user forces (`You are in DESIGN MODE`), not capabilities a model selects. Putting them in the
140
+ * catalogue would invite the model to load a mode nobody asked for, and would cost catalogue
141
+ * tokens for 42 entries that are already reachable by typing. See the Commands/Skills split.
142
+ *
143
+ * ⚠️ SYNC, BECAUSE AGENT CONSTRUCTION IS. Both hosts build agents inside a factory callback that
144
+ * is not a good place to await disk I/O on every rebuild. The directories are small and the
145
+ * result is intended to be cached by the host and refreshed when skills change.
146
+ *
147
+ * Project scope wins on a name clash, matching `resolveSkillPrompt`'s precedence — a project can
148
+ * override a personal skill, and the two hosts must agree about that.
149
+ */
150
+ export function loadInstalledSkills(projectDir) {
151
+ const seen = new Map();
152
+ const scopes = projectDir
153
+ ? [
154
+ { scope: 'project', dir: projectDir },
155
+ { scope: 'user', dir: undefined },
156
+ ]
157
+ : [{ scope: 'user', dir: undefined }];
158
+ for (const { scope, dir } of scopes) {
159
+ for (const skill of loadSkillsFromDirSync(getSkillsDir(scope, dir), scope)) {
160
+ if (!seen.has(skill.name))
161
+ seen.set(skill.name, skill);
162
+ }
163
+ }
164
+ return [...seen.values()];
165
+ }
166
+ /** Synchronous sibling of `loadSkillsFromDir`, for the construction path. */
167
+ function loadSkillsFromDirSync(dir, scope) {
168
+ const source = scope;
169
+ let entries;
170
+ try {
171
+ entries = readdirSync(dir, { withFileTypes: true })
172
+ .filter((e) => e.isDirectory())
173
+ .map((e) => e.name);
174
+ }
175
+ catch {
176
+ return []; // no skills directory is the normal case, not an error
177
+ }
178
+ const skills = [];
179
+ for (const name of entries) {
180
+ try {
181
+ const parsed = parseSkillMarkdown(readFileSync(join(dir, name, 'SKILL.md'), 'utf-8'));
182
+ if (!parsed)
183
+ continue; // malformed frontmatter — skipped, as the async loader does
184
+ skills.push({ ...parsed, source, compilr: { ...parsed.compilr, scope } });
185
+ }
186
+ catch {
187
+ continue; // no SKILL.md in this folder
188
+ }
189
+ }
190
+ return skills;
191
+ }
@@ -53,3 +53,30 @@ export declare function resolveLayeredSkills(layers: {
53
53
  * activation flow and lives at the call site, not here.
54
54
  */
55
55
  export declare function resolveSkillsForAgent(skills: CustomSkill[], context: SkillEligibilityContext): CustomSkill[];
56
+ /**
57
+ * Eligibility for a TeamAgent — the one call both hosts make.
58
+ *
59
+ * ⚠️ SHARED SO THE HOSTS CANNOT DISAGREE. Desktop and the CLI each own their skill directories
60
+ * and each build agents through a factory, so each could assemble this context itself — and then
61
+ * drift, which is how the same skill came to be offered in one host and refused in the other more
62
+ * than once. Every judgement lives here: which fields of the agent matter, and what an absent
63
+ * allowlist means.
64
+ *
65
+ * ⚠️ `enabledSkills` DOES NOT FOLLOW THE MCP GRANT CONTRACT, and must not be made to. There,
66
+ * `[]` means "granted nothing" because the field was new. Here both `undefined` and `[]` mean
67
+ * "unrestricted": the field predates choosable skills, `createCustomAgentDefinition` normalises
68
+ * `undefined` to `[]`, and every UI that collects it says empty means all. Only a NON-EMPTY list
69
+ * filters.
70
+ *
71
+ * @param pool every skill installed for this machine/project, unfiltered
72
+ * @param agent the agent being built — role, id and its allowlist come from here
73
+ * @param opts context the agent does not carry: the project type, and the tools it ended up with
74
+ */
75
+ export declare function resolveSkillsForTeamAgent(pool: CustomSkill[], agent: {
76
+ id: string;
77
+ role?: string;
78
+ enabledSkills?: readonly string[];
79
+ }, opts?: {
80
+ projectType?: string;
81
+ toolNames?: readonly string[];
82
+ }): CustomSkill[];
@@ -46,7 +46,21 @@ export function resolveLayeredSkills(layers) {
46
46
  */
47
47
  export function resolveSkillsForAgent(skills, context) {
48
48
  const toolSet = context.toolNames instanceof Set ? context.toolNames : new Set(context.toolNames ?? []);
49
- const allowlist = context.enabledSkills ? new Set(context.enabledSkills) : null;
49
+ /*
50
+ ⚠️ AN EMPTY ALLOWLIST MEANS "UNRESTRICTED", NOT "NOTHING" — the opposite of the MCP grant
51
+ contract, and deliberately so. `enabledSkills` predates model-choosable skills and already had
52
+ a meaning, stated in three places: `CustomAgentDefinition.enabledSkills` is documented "Empty =
53
+ all skills, non-empty = filtered", `createCustomAgentDefinition` normalises `undefined` to `[]`
54
+ with the same comment, and Desktop's spawn dialog tells the user "Empty = all skills enabled".
55
+
56
+ 0.25.0 copied the MCP reading (`[]` = granted nothing) onto this field and inverted it. Because
57
+ the definition normalises to `[]`, EVERY custom agent ever created carries one — so every one
58
+ of them silently got no skills at all, having been told empty meant everything.
59
+
60
+ MCP grants could afford the stricter reading because they were new and nothing had a prior
61
+ meaning. This field is not new. Honour what the UI has been promising.
62
+ */
63
+ const allowlist = context.enabledSkills?.length ? new Set(context.enabledSkills) : null;
50
64
  return skills.filter((skill) => {
51
65
  if (skill.enabled === false)
52
66
  return false;
@@ -78,3 +92,31 @@ export function resolveSkillsForAgent(skills, context) {
78
92
  return true;
79
93
  });
80
94
  }
95
+ /**
96
+ * Eligibility for a TeamAgent — the one call both hosts make.
97
+ *
98
+ * ⚠️ SHARED SO THE HOSTS CANNOT DISAGREE. Desktop and the CLI each own their skill directories
99
+ * and each build agents through a factory, so each could assemble this context itself — and then
100
+ * drift, which is how the same skill came to be offered in one host and refused in the other more
101
+ * than once. Every judgement lives here: which fields of the agent matter, and what an absent
102
+ * allowlist means.
103
+ *
104
+ * ⚠️ `enabledSkills` DOES NOT FOLLOW THE MCP GRANT CONTRACT, and must not be made to. There,
105
+ * `[]` means "granted nothing" because the field was new. Here both `undefined` and `[]` mean
106
+ * "unrestricted": the field predates choosable skills, `createCustomAgentDefinition` normalises
107
+ * `undefined` to `[]`, and every UI that collects it says empty means all. Only a NON-EMPTY list
108
+ * filters.
109
+ *
110
+ * @param pool every skill installed for this machine/project, unfiltered
111
+ * @param agent the agent being built — role, id and its allowlist come from here
112
+ * @param opts context the agent does not carry: the project type, and the tools it ended up with
113
+ */
114
+ export function resolveSkillsForTeamAgent(pool, agent, opts = {}) {
115
+ return resolveSkillsForAgent(pool, {
116
+ role: agent.role,
117
+ agentId: agent.id,
118
+ enabledSkills: agent.enabledSkills,
119
+ projectType: opts.projectType,
120
+ toolNames: opts.toolNames,
121
+ });
122
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/sdk",
3
- "version": "0.24.2",
3
+ "version": "0.25.1",
4
4
  "description": "Universal agent runtime for building AI-powered applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",