@ai-ecoverse/slicc-agent 3.39.0 → 3.40.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 CHANGED
@@ -209,11 +209,13 @@ Skills and prompt templates work as in pi's coding-agent. pi's loaders read with
209
209
  - **Where skills are found.** These roots are scanned in this order:
210
210
  1. `~/.pi/agent/skills`;
211
211
  2. `~/.agents/skills`;
212
- 3. `/workspace/skills`, read-only, for skills copied from SLICC v6;
213
- 4. packages in the kernel's `/node_modules` whose `package.json` lists folders in `pi.skills`;
214
- 5. the built-ins, which the worker writes to `/var/lib/slicc/agent/skills` on every start.
212
+ 3. `/usr/share/skills`, the global skills every cone and scoop sees, such as those imported from SLICC 6;
213
+ 4. `/workspace/skills`, read-only, for skills copied from SLICC v6;
214
+ 5. packages in the kernel's `/node_modules` whose `package.json` lists folders in `pi.skills`;
215
+ 6. the built-ins, which the worker writes to `/var/lib/slicc/agent/skills` on every start.
215
216
 
216
- On a name collision the first skill found wins.
217
+ On a name collision the first skill found wins: an agent's own skills shadow the global ones, and a global skill shadows a v6 copy, a package's and a built-in. A missing root is skipped.
218
+ - **The global folder.** On every start the worker, as root, creates `/usr/share/skills` if it's missing and sets it to 0755, so it's root's and readable by everyone; once kernel users give each cone its own user and the kernel checks file modes, only root (the worker, sudo or the human) writes there, and every cone and scoop reads it. Each skill's `source` is `user`, `global`, `compat`, `package` or `builtin`.
217
219
  - **Inside a root.** Folders with `SKILL.md` are found at any depth, and a `SKILL.md` ends its branch. A `.md` file with a description directly in a root counts too. Folders starting with `.` and `node_modules` are skipped. Symbolic links are followed, but each folder is read once by its real path, so a link back up a tree can't loop. Scoops may `read` the folders of the loaded skills, even outside their own folders.
218
220
  - **Problems.** A problem (a missing description, a bad name, a collision, a line the frontmatter parser can't read) becomes one lick with `severity="warn"` per distinct problem. A skill without a description is skipped; any other problem only warns.
219
221
  - **Untrusted folders.** A project's `.pi/skills` and `.agents/skills` are read only in trusted folders, which come with PR 21.
@@ -23,6 +23,7 @@ const BUILTIN_SKILLS = [
23
23
  const BUILTIN_PROMPTS = ["parallel-review", "review-loop"];
24
24
  const SKILLS_DIR = "/var/lib/slicc/agent/skills";
25
25
  const PROMPTS_DIR = "/var/lib/slicc/agent/prompts";
26
+ const GLOBAL_DIR = "/usr/share/skills";
26
27
  const COMPAT_DIR = "/workspace/skills";
27
28
  const RELOAD_MS = 300;
28
29
  function piResources(dir, text, kind) {
@@ -100,6 +101,7 @@ function setupSkills(registry, licks) {
100
101
  const { env, home } = options;
101
102
  if (options.assets)
102
103
  await installBuiltins(env, options.assets, context).catch(() => void 0);
104
+ await env.exec(`mkdir -p ${GLOBAL_DIR} && chmod 755 ${GLOBAL_DIR}`, void 0, context);
103
105
  let templates = [];
104
106
  const commands = replicatedState([]);
105
107
  const reported = /* @__PURE__ */ new Set();
@@ -107,6 +109,7 @@ function setupSkills(registry, licks) {
107
109
  const roots = [
108
110
  { dir: `${home}/.pi/agent/skills`, source: "user" },
109
111
  { dir: `${home}/.agents/skills`, source: "user" },
112
+ { dir: GLOBAL_DIR, source: "global" },
110
113
  { dir: COMPAT_DIR, source: "compat" },
111
114
  ...(await packaged(env, "skills", using)).map((dir) => ({
112
115
  dir,
@@ -142,6 +145,7 @@ ${problem.message}`;
142
145
  { path: `${home}/.pi/agent/skills`, recursive: true },
143
146
  { path: `${home}/.pi/agent/prompts` },
144
147
  { path: `${home}/.agents/skills`, recursive: true },
148
+ { path: GLOBAL_DIR, recursive: true },
145
149
  { path: COMPAT_DIR, recursive: true }
146
150
  ],
147
151
  schedule,
@@ -170,6 +174,7 @@ export {
170
174
  BUILTIN_PROMPTS,
171
175
  BUILTIN_SKILLS,
172
176
  COMPAT_DIR,
177
+ GLOBAL_DIR,
173
178
  PROMPTS_DIR,
174
179
  RELOAD_MS,
175
180
  SKILLS_DIR,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/skills/index.ts"],
4
- "sourcesContent": ["import { type Context, type ReplicatedState, replicatedState } from '@earendil-works/chord';\nimport {\n defineExtension,\n type Extension,\n type Registry,\n section,\n} from '@earendil-works/pi-durable';\nimport type { ExecutionEnv, FileWatcher } from '@earendil-works/pi-durable/env';\nimport type { Licks } from '../licks/licks.ts';\nimport { manifestDirs } from '../roles/roles.ts';\nimport type { Command } from '../services.ts';\nimport {\n type Diagnostic,\n expandSkillCommand,\n formatSkillsForPrompt,\n loadSkills,\n type Skill,\n type SkillRoot,\n} from './skills.ts';\nimport { expandPromptTemplate, loadTemplates, type PromptTemplate } from './templates.ts';\n\nexport const BUILTIN_SKILLS = [\n 'agent',\n 'browser',\n 'gelatiere',\n 'licks',\n 'memory',\n 'skill-authoring',\n 'sprinkles',\n 'welcome',\n];\nexport const BUILTIN_PROMPTS = ['parallel-review', 'review-loop'];\nexport const SKILLS_DIR = '/var/lib/slicc/agent/skills';\nexport const PROMPTS_DIR = '/var/lib/slicc/agent/prompts';\nexport const COMPAT_DIR = '/workspace/skills';\nexport const RELOAD_MS = 300;\n\nexport interface SkillsRuntime {\n readonly commands: ReplicatedState<Command[]>;\n skills(): readonly Skill[];\n dirs(): string[];\n templates(): readonly PromptTemplate[];\n expand(text: string, context: Context): Promise<string>;\n reload(context: Context): Promise<void>;\n close(context: Context): Promise<void>;\n}\n\nexport interface SkillsAttach {\n env: ExecutionEnv;\n home: string;\n assets?: (path: string) => Promise<string>;\n reloadMs: number;\n}\n\nexport interface SkillsSetup {\n extension: Extension;\n attach(options: SkillsAttach, context: Context): Promise<SkillsRuntime>;\n}\n\ntype Manifest = { pi?: { skills?: unknown; prompts?: unknown } };\n\nexport function piResources(dir: string, text: string, kind: 'skills' | 'prompts'): string[] {\n let manifest: Manifest;\n try {\n manifest = JSON.parse(text) as Manifest;\n } catch {\n return [];\n }\n const listed = manifest.pi?.[kind];\n const paths = Array.isArray(listed) ? listed : typeof listed === 'string' ? [listed] : [];\n return paths\n .filter((path): path is string => typeof path === 'string')\n .map((path) => `${dir}/${path.replace(/^\\.\\//, '').replace(/\\/$/, '')}`);\n}\n\nasync function packaged(env: ExecutionEnv, kind: 'skills' | 'prompts', context: Context) {\n const out: string[] = [];\n for (const dir of await manifestDirs(env, '/node_modules', context)) {\n const read = await env.readTextFile(`${dir}/package.json`, context);\n if (read.ok) out.push(...piResources(dir, read.value, kind));\n }\n return out;\n}\n\nasync function installBuiltins(\n env: ExecutionEnv,\n assets: (path: string) => Promise<string>,\n context: Context\n): Promise<void> {\n await env.remove(SKILLS_DIR, { recursive: true, force: true }, context);\n await env.remove(PROMPTS_DIR, { recursive: true, force: true }, context);\n for (const name of BUILTIN_SKILLS) {\n const text = await assets(`packages/vfs-root/skills/${name}/SKILL.md`).catch(() => undefined);\n if (text === undefined) continue;\n await env.createDir(`${SKILLS_DIR}/${name}`, { recursive: true }, context);\n await env.writeFile(`${SKILLS_DIR}/${name}/SKILL.md`, text, context);\n }\n await env.createDir(PROMPTS_DIR, { recursive: true }, context);\n for (const name of BUILTIN_PROMPTS) {\n const text = await assets(`packages/vfs-root/prompts/${name}.md`).catch(() => undefined);\n if (text !== undefined) await env.writeFile(`${PROMPTS_DIR}/${name}.md`, text, context);\n }\n}\n\nexport function problemLick(problem: Diagnostic, home: string) {\n const path = problem.path.startsWith(`${home}/`)\n ? `~${problem.path.slice(home.length)}`\n : problem.path;\n return {\n channel: 'fswatch' as const,\n source: `skills:${path}`,\n title: `Skill or prompt template problem: ${path}`,\n text: problem.message,\n body: 'The file is used with this problem, or skipped if it has no description.\\nThe rules are in the slicc-agent README.',\n target: 'cone' as const,\n severity: 'warn' as const,\n eventId: `${path}\\n${problem.message}`,\n };\n}\n\nexport function commandsOf(\n skills: readonly Skill[],\n templates: readonly PromptTemplate[]\n): Command[] {\n return [\n ...templates.map((template) => ({\n name: template.name,\n description: template.description,\n kind: 'prompt' as const,\n })),\n ...skills.map((skill) => ({\n name: `skill:${skill.name}`,\n description: skill.description,\n kind: 'skill' as const,\n })),\n ];\n}\n\nexport function setupSkills(registry: Registry, licks: Licks): SkillsSetup {\n let current: Skill[] = [];\n const extension = defineExtension({\n name: 'slicc-skills',\n sections: [section('skills', async () => formatSkillsForPrompt(current) || undefined)],\n });\n registry.install(extension);\n return {\n extension,\n async attach(options, context) {\n const { env, home } = options;\n if (options.assets)\n await installBuiltins(env, options.assets, context).catch(() => undefined);\n let templates: PromptTemplate[] = [];\n const commands = replicatedState<Command[]>([]);\n const reported = new Set<string>();\n const reload = async (using: Context) => {\n const roots: SkillRoot[] = [\n { dir: `${home}/.pi/agent/skills`, source: 'user' },\n { dir: `${home}/.agents/skills`, source: 'user' },\n { dir: COMPAT_DIR, source: 'compat' },\n ...(await packaged(env, 'skills', using)).map((dir) => ({\n dir,\n source: 'package' as const,\n })),\n { dir: SKILLS_DIR, source: 'builtin' },\n ];\n const loaded = await loadSkills(env, roots, using);\n const prompts = await loadTemplates(\n env,\n [`${home}/.pi/agent/prompts`, ...(await packaged(env, 'prompts', using)), PROMPTS_DIR],\n using\n );\n current = loaded.skills;\n templates = prompts.templates;\n commands.replace(using, commandsOf(current, templates));\n for (const problem of [...loaded.diagnostics, ...prompts.diagnostics]) {\n const key = `${problem.path}\\n${problem.message}`;\n if (reported.has(key)) continue;\n reported.add(key);\n await licks.deliver(problemLick(problem, home), using).catch(() => undefined);\n }\n };\n await reload(context);\n let timer: ReturnType<typeof setTimeout> | undefined;\n const schedule = () => {\n clearTimeout(timer);\n timer = setTimeout(() => void reload(context).catch(() => undefined), options.reloadMs);\n };\n const watched = await env.watch(\n [\n { path: `${home}/.pi/agent/skills`, recursive: true },\n { path: `${home}/.pi/agent/prompts` },\n { path: `${home}/.agents/skills`, recursive: true },\n { path: COMPAT_DIR, recursive: true },\n ],\n schedule,\n context\n );\n const watcher: FileWatcher | undefined = watched.ok ? watched.value : undefined;\n return {\n commands,\n skills: () => current,\n dirs: () => current.map((skill) => skill.baseDir),\n templates: () => templates,\n async expand(text, using) {\n const expanded = await expandSkillCommand(text, current, env, using);\n return expanded === text ? expandPromptTemplate(text, templates) : expanded;\n },\n reload,\n async close(using) {\n clearTimeout(timer);\n await watcher?.close(using);\n },\n };\n },\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { type Context, type ReplicatedState, replicatedState } from '@earendil-works/chord';\nimport {\n defineExtension,\n type Extension,\n type Registry,\n section,\n} from '@earendil-works/pi-durable';\nimport type { ExecutionEnv, FileWatcher } from '@earendil-works/pi-durable/env';\nimport type { Licks } from '../licks/licks.ts';\nimport { manifestDirs } from '../roles/roles.ts';\nimport type { Command } from '../services.ts';\nimport {\n type Diagnostic,\n expandSkillCommand,\n formatSkillsForPrompt,\n loadSkills,\n type Skill,\n type SkillRoot,\n} from './skills.ts';\nimport { expandPromptTemplate, loadTemplates, type PromptTemplate } from './templates.ts';\n\nexport const BUILTIN_SKILLS = [\n 'agent',\n 'browser',\n 'gelatiere',\n 'licks',\n 'memory',\n 'skill-authoring',\n 'sprinkles',\n 'welcome',\n];\nexport const BUILTIN_PROMPTS = ['parallel-review', 'review-loop'];\nexport const SKILLS_DIR = '/var/lib/slicc/agent/skills';\nexport const PROMPTS_DIR = '/var/lib/slicc/agent/prompts';\nexport const GLOBAL_DIR = '/usr/share/skills';\nexport const COMPAT_DIR = '/workspace/skills';\nexport const RELOAD_MS = 300;\n\nexport interface SkillsRuntime {\n readonly commands: ReplicatedState<Command[]>;\n skills(): readonly Skill[];\n dirs(): string[];\n templates(): readonly PromptTemplate[];\n expand(text: string, context: Context): Promise<string>;\n reload(context: Context): Promise<void>;\n close(context: Context): Promise<void>;\n}\n\nexport interface SkillsAttach {\n env: ExecutionEnv;\n home: string;\n assets?: (path: string) => Promise<string>;\n reloadMs: number;\n}\n\nexport interface SkillsSetup {\n extension: Extension;\n attach(options: SkillsAttach, context: Context): Promise<SkillsRuntime>;\n}\n\ntype Manifest = { pi?: { skills?: unknown; prompts?: unknown } };\n\nexport function piResources(dir: string, text: string, kind: 'skills' | 'prompts'): string[] {\n let manifest: Manifest;\n try {\n manifest = JSON.parse(text) as Manifest;\n } catch {\n return [];\n }\n const listed = manifest.pi?.[kind];\n const paths = Array.isArray(listed) ? listed : typeof listed === 'string' ? [listed] : [];\n return paths\n .filter((path): path is string => typeof path === 'string')\n .map((path) => `${dir}/${path.replace(/^\\.\\//, '').replace(/\\/$/, '')}`);\n}\n\nasync function packaged(env: ExecutionEnv, kind: 'skills' | 'prompts', context: Context) {\n const out: string[] = [];\n for (const dir of await manifestDirs(env, '/node_modules', context)) {\n const read = await env.readTextFile(`${dir}/package.json`, context);\n if (read.ok) out.push(...piResources(dir, read.value, kind));\n }\n return out;\n}\n\nasync function installBuiltins(\n env: ExecutionEnv,\n assets: (path: string) => Promise<string>,\n context: Context\n): Promise<void> {\n await env.remove(SKILLS_DIR, { recursive: true, force: true }, context);\n await env.remove(PROMPTS_DIR, { recursive: true, force: true }, context);\n for (const name of BUILTIN_SKILLS) {\n const text = await assets(`packages/vfs-root/skills/${name}/SKILL.md`).catch(() => undefined);\n if (text === undefined) continue;\n await env.createDir(`${SKILLS_DIR}/${name}`, { recursive: true }, context);\n await env.writeFile(`${SKILLS_DIR}/${name}/SKILL.md`, text, context);\n }\n await env.createDir(PROMPTS_DIR, { recursive: true }, context);\n for (const name of BUILTIN_PROMPTS) {\n const text = await assets(`packages/vfs-root/prompts/${name}.md`).catch(() => undefined);\n if (text !== undefined) await env.writeFile(`${PROMPTS_DIR}/${name}.md`, text, context);\n }\n}\n\nexport function problemLick(problem: Diagnostic, home: string) {\n const path = problem.path.startsWith(`${home}/`)\n ? `~${problem.path.slice(home.length)}`\n : problem.path;\n return {\n channel: 'fswatch' as const,\n source: `skills:${path}`,\n title: `Skill or prompt template problem: ${path}`,\n text: problem.message,\n body: 'The file is used with this problem, or skipped if it has no description.\\nThe rules are in the slicc-agent README.',\n target: 'cone' as const,\n severity: 'warn' as const,\n eventId: `${path}\\n${problem.message}`,\n };\n}\n\nexport function commandsOf(\n skills: readonly Skill[],\n templates: readonly PromptTemplate[]\n): Command[] {\n return [\n ...templates.map((template) => ({\n name: template.name,\n description: template.description,\n kind: 'prompt' as const,\n })),\n ...skills.map((skill) => ({\n name: `skill:${skill.name}`,\n description: skill.description,\n kind: 'skill' as const,\n })),\n ];\n}\n\nexport function setupSkills(registry: Registry, licks: Licks): SkillsSetup {\n let current: Skill[] = [];\n const extension = defineExtension({\n name: 'slicc-skills',\n sections: [section('skills', async () => formatSkillsForPrompt(current) || undefined)],\n });\n registry.install(extension);\n return {\n extension,\n async attach(options, context) {\n const { env, home } = options;\n if (options.assets)\n await installBuiltins(env, options.assets, context).catch(() => undefined);\n await env.exec(`mkdir -p ${GLOBAL_DIR} && chmod 755 ${GLOBAL_DIR}`, undefined, context);\n let templates: PromptTemplate[] = [];\n const commands = replicatedState<Command[]>([]);\n const reported = new Set<string>();\n const reload = async (using: Context) => {\n const roots: SkillRoot[] = [\n { dir: `${home}/.pi/agent/skills`, source: 'user' },\n { dir: `${home}/.agents/skills`, source: 'user' },\n { dir: GLOBAL_DIR, source: 'global' },\n { dir: COMPAT_DIR, source: 'compat' },\n ...(await packaged(env, 'skills', using)).map((dir) => ({\n dir,\n source: 'package' as const,\n })),\n { dir: SKILLS_DIR, source: 'builtin' },\n ];\n const loaded = await loadSkills(env, roots, using);\n const prompts = await loadTemplates(\n env,\n [`${home}/.pi/agent/prompts`, ...(await packaged(env, 'prompts', using)), PROMPTS_DIR],\n using\n );\n current = loaded.skills;\n templates = prompts.templates;\n commands.replace(using, commandsOf(current, templates));\n for (const problem of [...loaded.diagnostics, ...prompts.diagnostics]) {\n const key = `${problem.path}\\n${problem.message}`;\n if (reported.has(key)) continue;\n reported.add(key);\n await licks.deliver(problemLick(problem, home), using).catch(() => undefined);\n }\n };\n await reload(context);\n let timer: ReturnType<typeof setTimeout> | undefined;\n const schedule = () => {\n clearTimeout(timer);\n timer = setTimeout(() => void reload(context).catch(() => undefined), options.reloadMs);\n };\n const watched = await env.watch(\n [\n { path: `${home}/.pi/agent/skills`, recursive: true },\n { path: `${home}/.pi/agent/prompts` },\n { path: `${home}/.agents/skills`, recursive: true },\n { path: GLOBAL_DIR, recursive: true },\n { path: COMPAT_DIR, recursive: true },\n ],\n schedule,\n context\n );\n const watcher: FileWatcher | undefined = watched.ok ? watched.value : undefined;\n return {\n commands,\n skills: () => current,\n dirs: () => current.map((skill) => skill.baseDir),\n templates: () => templates,\n async expand(text, using) {\n const expanded = await expandSkillCommand(text, current, env, using);\n return expanded === text ? expandPromptTemplate(text, templates) : expanded;\n },\n reload,\n async close(using) {\n clearTimeout(timer);\n await watcher?.close(using);\n },\n };\n },\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/skills/skills.ts"],
4
- "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { ExecutionEnv } from '@earendil-works/pi-durable/env';\nimport { parseFrontmatter } from '../roles/frontmatter.ts';\n\nexport const MAX_NAME_LENGTH = 64;\nexport const MAX_DESCRIPTION_LENGTH = 1024;\n\nexport type SkillFiles = Pick<ExecutionEnv, 'readTextFile' | 'listDir' | 'canonicalPath'>;\n\nexport type SkillSource = 'user' | 'compat' | 'package' | 'builtin';\n\nexport interface Skill {\n name: string;\n description: string;\n filePath: string;\n baseDir: string;\n source: SkillSource;\n disableModelInvocation: boolean;\n}\n\nexport interface Diagnostic {\n path: string;\n message: string;\n}\n\nexport interface Loaded {\n skills: Skill[];\n diagnostics: Diagnostic[];\n}\n\nexport interface SkillRoot {\n dir: string;\n source: SkillSource;\n}\n\nfunction dirname(path: string): string {\n return path.slice(0, path.lastIndexOf('/')) || '/';\n}\n\nfunction basename(path: string): string {\n return path.slice(path.lastIndexOf('/') + 1);\n}\n\nexport function validateName(name: string): string[] {\n const errors: string[] = [];\n if (name.length > MAX_NAME_LENGTH)\n errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);\n if (!/^[a-z0-9-]+$/.test(name))\n errors.push('name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)');\n if (name.startsWith('-') || name.endsWith('-'))\n errors.push('name must not start or end with a hyphen');\n if (name.includes('--')) errors.push('name must not contain consecutive hyphens');\n return errors;\n}\n\nfunction validateDescription(description: unknown): string[] {\n if (typeof description !== 'string' || description.trim() === '')\n return ['description is required'];\n if (description.length > MAX_DESCRIPTION_LENGTH)\n return [`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`];\n return [];\n}\n\nexport function skillFromText(text: string, filePath: string, source: SkillSource): Loaded {\n const declared = basename(filePath) === 'SKILL.md';\n const { fields, problems } = parseFrontmatter(text);\n const diagnostics: Diagnostic[] = [];\n const description = typeof fields.description === 'string' ? fields.description.trim() : '';\n if (!declared && !description) return { skills: [], diagnostics };\n for (const problem of problems.filter((item) => !item.includes('nested values')))\n diagnostics.push({ path: filePath, message: problem });\n for (const message of validateDescription(description))\n diagnostics.push({ path: filePath, message });\n const baseDir = dirname(filePath);\n const name = (typeof fields.name === 'string' && fields.name) || basename(baseDir);\n for (const message of validateName(name)) diagnostics.push({ path: filePath, message });\n if (!description) return { skills: [], diagnostics };\n const skill: Skill = {\n name,\n description,\n filePath,\n baseDir,\n source,\n disableModelInvocation: fields['disable-model-invocation'] === true,\n };\n return { skills: [skill], diagnostics };\n}\n\nasync function loadFile(\n files: SkillFiles,\n filePath: string,\n source: SkillSource,\n context: Context\n): Promise<Loaded> {\n const read = await files.readTextFile(filePath, context);\n if (!read.ok)\n return { skills: [], diagnostics: [{ path: filePath, message: read.error.message }] };\n return skillFromText(read.value, filePath, source);\n}\n\nexport async function loadSkillsFromDir(\n files: SkillFiles,\n dir: string,\n source: SkillSource,\n context: Context,\n root = true,\n visited = new Set<string>()\n): Promise<Loaded> {\n const out: Loaded = { skills: [], diagnostics: [] };\n const real = await files.canonicalPath(dir, context);\n const key = real.ok ? real.value : dir;\n if (visited.has(key)) return out;\n visited.add(key);\n const listed = await files.listDir(dir, context);\n if (!listed.ok) return out;\n const merge = (found: Loaded) => {\n out.skills.push(...found.skills);\n out.diagnostics.push(...found.diagnostics);\n };\n if (listed.value.some((entry) => entry.name === 'SKILL.md' && entry.kind !== 'directory')) {\n merge(await loadFile(files, `${dir}/SKILL.md`, source, context));\n return out;\n }\n const entries = [...listed.value].sort((a, b) => a.name.localeCompare(b.name));\n for (const entry of entries) {\n if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;\n const path = `${dir}/${entry.name}`;\n if (entry.kind === 'directory' || entry.kind === 'symlink')\n merge(await loadSkillsFromDir(files, path, source, context, false, visited));\n if (entry.kind !== 'directory' && root && entry.name.endsWith('.md'))\n merge(await loadFile(files, path, source, context));\n }\n return out;\n}\n\nexport async function loadSkills(\n files: SkillFiles,\n roots: readonly SkillRoot[],\n context: Context\n): Promise<Loaded> {\n const byName = new Map<string, Skill>();\n const seen = new Set<string>();\n const diagnostics: Diagnostic[] = [];\n const collisions: Diagnostic[] = [];\n for (const root of roots) {\n const found = await loadSkillsFromDir(files, root.dir, root.source, context);\n diagnostics.push(...found.diagnostics);\n for (const skill of found.skills) {\n if (seen.has(skill.filePath)) continue;\n const existing = byName.get(skill.name);\n if (existing)\n collisions.push({\n path: skill.filePath,\n message: `name \"${skill.name}\" collision; ${existing.filePath} is used`,\n });\n else {\n byName.set(skill.name, skill);\n seen.add(skill.filePath);\n }\n }\n }\n return { skills: [...byName.values()], diagnostics: [...diagnostics, ...collisions] };\n}\n\nfunction escapeXml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n\nexport function formatSkillsForPrompt(skills: readonly Skill[]): string {\n const visible = skills.filter((skill) => !skill.disableModelInvocation);\n if (!visible.length) return '';\n const lines = [\n 'The following skills provide specialized instructions for specific tasks.',\n \"Use the read tool to load a skill's file when the task matches its description.\",\n 'When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.',\n '',\n '<available_skills>',\n ];\n for (const skill of visible)\n lines.push(\n ' <skill>',\n ` <name>${escapeXml(skill.name)}</name>`,\n ` <description>${escapeXml(skill.description)}</description>`,\n ` <location>${escapeXml(skill.filePath)}</location>`,\n ' </skill>'\n );\n lines.push('</available_skills>');\n return lines.join('\\n');\n}\n\nexport async function expandSkillCommand(\n text: string,\n skills: readonly Skill[],\n files: SkillFiles,\n context: Context\n): Promise<string> {\n if (!text.startsWith('/skill:')) return text;\n const space = text.indexOf(' ');\n const name = space === -1 ? text.slice(7) : text.slice(7, space);\n const args = space === -1 ? '' : text.slice(space + 1).trim();\n const skill = skills.find((candidate) => candidate.name === name);\n if (!skill) return text;\n const read = await files.readTextFile(skill.filePath, context);\n if (!read.ok) return text;\n const body = parseFrontmatter(read.value).body.trim();\n const block = `<skill name=\"${skill.name}\" location=\"${skill.filePath}\">\\nReferences are relative to ${skill.baseDir}.\\n\\n${body}\\n</skill>`;\n return args ? `${block}\\n\\n${args}` : block;\n}\n"],
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { ExecutionEnv } from '@earendil-works/pi-durable/env';\nimport { parseFrontmatter } from '../roles/frontmatter.ts';\n\nexport const MAX_NAME_LENGTH = 64;\nexport const MAX_DESCRIPTION_LENGTH = 1024;\n\nexport type SkillFiles = Pick<ExecutionEnv, 'readTextFile' | 'listDir' | 'canonicalPath'>;\n\nexport type SkillSource = 'user' | 'global' | 'compat' | 'package' | 'builtin';\n\nexport interface Skill {\n name: string;\n description: string;\n filePath: string;\n baseDir: string;\n source: SkillSource;\n disableModelInvocation: boolean;\n}\n\nexport interface Diagnostic {\n path: string;\n message: string;\n}\n\nexport interface Loaded {\n skills: Skill[];\n diagnostics: Diagnostic[];\n}\n\nexport interface SkillRoot {\n dir: string;\n source: SkillSource;\n}\n\nfunction dirname(path: string): string {\n return path.slice(0, path.lastIndexOf('/')) || '/';\n}\n\nfunction basename(path: string): string {\n return path.slice(path.lastIndexOf('/') + 1);\n}\n\nexport function validateName(name: string): string[] {\n const errors: string[] = [];\n if (name.length > MAX_NAME_LENGTH)\n errors.push(`name exceeds ${MAX_NAME_LENGTH} characters (${name.length})`);\n if (!/^[a-z0-9-]+$/.test(name))\n errors.push('name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)');\n if (name.startsWith('-') || name.endsWith('-'))\n errors.push('name must not start or end with a hyphen');\n if (name.includes('--')) errors.push('name must not contain consecutive hyphens');\n return errors;\n}\n\nfunction validateDescription(description: unknown): string[] {\n if (typeof description !== 'string' || description.trim() === '')\n return ['description is required'];\n if (description.length > MAX_DESCRIPTION_LENGTH)\n return [`description exceeds ${MAX_DESCRIPTION_LENGTH} characters (${description.length})`];\n return [];\n}\n\nexport function skillFromText(text: string, filePath: string, source: SkillSource): Loaded {\n const declared = basename(filePath) === 'SKILL.md';\n const { fields, problems } = parseFrontmatter(text);\n const diagnostics: Diagnostic[] = [];\n const description = typeof fields.description === 'string' ? fields.description.trim() : '';\n if (!declared && !description) return { skills: [], diagnostics };\n for (const problem of problems.filter((item) => !item.includes('nested values')))\n diagnostics.push({ path: filePath, message: problem });\n for (const message of validateDescription(description))\n diagnostics.push({ path: filePath, message });\n const baseDir = dirname(filePath);\n const name = (typeof fields.name === 'string' && fields.name) || basename(baseDir);\n for (const message of validateName(name)) diagnostics.push({ path: filePath, message });\n if (!description) return { skills: [], diagnostics };\n const skill: Skill = {\n name,\n description,\n filePath,\n baseDir,\n source,\n disableModelInvocation: fields['disable-model-invocation'] === true,\n };\n return { skills: [skill], diagnostics };\n}\n\nasync function loadFile(\n files: SkillFiles,\n filePath: string,\n source: SkillSource,\n context: Context\n): Promise<Loaded> {\n const read = await files.readTextFile(filePath, context);\n if (!read.ok)\n return { skills: [], diagnostics: [{ path: filePath, message: read.error.message }] };\n return skillFromText(read.value, filePath, source);\n}\n\nexport async function loadSkillsFromDir(\n files: SkillFiles,\n dir: string,\n source: SkillSource,\n context: Context,\n root = true,\n visited = new Set<string>()\n): Promise<Loaded> {\n const out: Loaded = { skills: [], diagnostics: [] };\n const real = await files.canonicalPath(dir, context);\n const key = real.ok ? real.value : dir;\n if (visited.has(key)) return out;\n visited.add(key);\n const listed = await files.listDir(dir, context);\n if (!listed.ok) return out;\n const merge = (found: Loaded) => {\n out.skills.push(...found.skills);\n out.diagnostics.push(...found.diagnostics);\n };\n if (listed.value.some((entry) => entry.name === 'SKILL.md' && entry.kind !== 'directory')) {\n merge(await loadFile(files, `${dir}/SKILL.md`, source, context));\n return out;\n }\n const entries = [...listed.value].sort((a, b) => a.name.localeCompare(b.name));\n for (const entry of entries) {\n if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;\n const path = `${dir}/${entry.name}`;\n if (entry.kind === 'directory' || entry.kind === 'symlink')\n merge(await loadSkillsFromDir(files, path, source, context, false, visited));\n if (entry.kind !== 'directory' && root && entry.name.endsWith('.md'))\n merge(await loadFile(files, path, source, context));\n }\n return out;\n}\n\nexport async function loadSkills(\n files: SkillFiles,\n roots: readonly SkillRoot[],\n context: Context\n): Promise<Loaded> {\n const byName = new Map<string, Skill>();\n const seen = new Set<string>();\n const diagnostics: Diagnostic[] = [];\n const collisions: Diagnostic[] = [];\n for (const root of roots) {\n const found = await loadSkillsFromDir(files, root.dir, root.source, context);\n diagnostics.push(...found.diagnostics);\n for (const skill of found.skills) {\n if (seen.has(skill.filePath)) continue;\n const existing = byName.get(skill.name);\n if (existing)\n collisions.push({\n path: skill.filePath,\n message: `name \"${skill.name}\" collision; ${existing.filePath} is used`,\n });\n else {\n byName.set(skill.name, skill);\n seen.add(skill.filePath);\n }\n }\n }\n return { skills: [...byName.values()], diagnostics: [...diagnostics, ...collisions] };\n}\n\nfunction escapeXml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n\nexport function formatSkillsForPrompt(skills: readonly Skill[]): string {\n const visible = skills.filter((skill) => !skill.disableModelInvocation);\n if (!visible.length) return '';\n const lines = [\n 'The following skills provide specialized instructions for specific tasks.',\n \"Use the read tool to load a skill's file when the task matches its description.\",\n 'When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.',\n '',\n '<available_skills>',\n ];\n for (const skill of visible)\n lines.push(\n ' <skill>',\n ` <name>${escapeXml(skill.name)}</name>`,\n ` <description>${escapeXml(skill.description)}</description>`,\n ` <location>${escapeXml(skill.filePath)}</location>`,\n ' </skill>'\n );\n lines.push('</available_skills>');\n return lines.join('\\n');\n}\n\nexport async function expandSkillCommand(\n text: string,\n skills: readonly Skill[],\n files: SkillFiles,\n context: Context\n): Promise<string> {\n if (!text.startsWith('/skill:')) return text;\n const space = text.indexOf(' ');\n const name = space === -1 ? text.slice(7) : text.slice(7, space);\n const args = space === -1 ? '' : text.slice(space + 1).trim();\n const skill = skills.find((candidate) => candidate.name === name);\n if (!skill) return text;\n const read = await files.readTextFile(skill.filePath, context);\n if (!read.ok) return text;\n const body = parseFrontmatter(read.value).body.trim();\n const block = `<skill name=\"${skill.name}\" location=\"${skill.filePath}\">\\nReferences are relative to ${skill.baseDir}.\\n\\n${body}\\n</skill>`;\n return args ? `${block}\\n\\n${args}` : block;\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -9,6 +9,7 @@ export declare const BUILTIN_SKILLS: string[];
9
9
  export declare const BUILTIN_PROMPTS: string[];
10
10
  export declare const SKILLS_DIR = "/var/lib/slicc/agent/skills";
11
11
  export declare const PROMPTS_DIR = "/var/lib/slicc/agent/prompts";
12
+ export declare const GLOBAL_DIR = "/usr/share/skills";
12
13
  export declare const COMPAT_DIR = "/workspace/skills";
13
14
  export declare const RELOAD_MS = 300;
14
15
  export interface SkillsRuntime {
@@ -3,7 +3,7 @@ import type { ExecutionEnv } from '@earendil-works/pi-durable/env';
3
3
  export declare const MAX_NAME_LENGTH = 64;
4
4
  export declare const MAX_DESCRIPTION_LENGTH = 1024;
5
5
  export type SkillFiles = Pick<ExecutionEnv, 'readTextFile' | 'listDir' | 'canonicalPath'>;
6
- export type SkillSource = 'user' | 'compat' | 'package' | 'builtin';
6
+ export type SkillSource = 'user' | 'global' | 'compat' | 'package' | 'builtin';
7
7
  export interface Skill {
8
8
  name: string;
9
9
  description: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-ecoverse/slicc-agent",
3
- "version": "3.39.0",
3
+ "version": "3.40.0",
4
4
  "description": "SLICC's agent core on pi durable: an agent worker, session storage, remoting and pi extensions",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -40,9 +40,10 @@ In this order; when two skills share a name, the first one found wins and the ot
40
40
 
41
41
  1. `~/.pi/agent/skills/`
42
42
  2. `~/.agents/skills/`
43
- 3. `/workspace/skills/`, for skills copied from SLICC v6
44
- 4. packages under `/node_modules` whose `package.json` lists folders in `pi.skills`
45
- 5. SLICC's built-in skills in `/var/lib/slicc/agent/skills/` (rewritten on every start; don't edit them there)
43
+ 3. `/usr/share/skills/`, global skills every cone and scoop sees; it's root's, so write there as root
44
+ 4. `/workspace/skills/`, for skills copied from SLICC v6
45
+ 5. packages under `/node_modules` whose `package.json` lists folders in `pi.skills`
46
+ 6. SLICC's built-in skills in `/var/lib/slicc/agent/skills/` (rewritten on every start; don't edit them there)
46
47
 
47
48
  Folders containing `SKILL.md` are found at any depth; a plain `.md` file with a `description` directly in one of these folders counts as a skill too. Folders starting with `.` and `node_modules` are skipped. A project's `.pi/skills` or `.agents/skills` folder is read only once the folder is trusted, which SLICC doesn't support yet.
48
49