@ai-ecoverse/slicc-agent 3.40.0 → 3.41.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
@@ -169,6 +169,8 @@ gone /home/notes/old.md
169
169
  - `~/.slicc/watches/<name>.json`: `{ "path": "~/notes", "glob": "**/*.md", "target": "cone", "message": "…", "debounce": 500 }`. `path` is absolute or starts with `~/`; `glob` (default `**`) is matched against paths relative to it, with `*`, `**`, `?`, `[…]` and `{a,b}`. `debounce` is 100–60000 ms. `node_modules` and `.git` are never scanned.
170
170
  - `~/.slicc/webhooks/<name>.json`: `{ "target": "cone", "message": "…" }`. The URL comes from the tray hub (milestone E); until then, `AgentControl.webhook(name, { id, headers, body })` is how a delivery arrives.
171
171
 
172
+ **Triage.** A watch or webhook file can add `"triage": "<which events the agent should see>"`, a sentence of 1–1000 characters such as `"pushes to main that touch src/"`. Then each of its events (a debounced batch of changes, or a webhook request) first goes to Clef Flash (`adobe/clef-flash`, see Classifiers) with the channel, source, title, text, body (cut at 4000 characters) and up to 50 paths. One `choice` question asks whether the event matches that sentence: `act` or `ignore`. When the answer is `ignore` with a probability of at least 0.75 (`TRIAGE_SKIP`), the lick isn't delivered and starts no turn. Instead, a passive `slicc.lick-triage` entry goes into the target's transcript. It has no model message, so neither the model nor the chat sees it, and it records the would-be lick with the triage sentence, the decision, Clef's confidence and probabilities, the model and the cost. An event whose delivery id was already seen isn't triaged again. A skipped one is remembered only after its record is written, under a `requestId` taken from the delivery id, so a crash in between neither loses the record nor writes it twice. Triage fails open: without an Adobe sign-in, or when the proxy errors, times out (15 s) or returns any other answer, the lick is delivered as usual. Watch notices (stopped, overflow), config errors, frozen notices, crontab jobs and boot licks are never triaged. A small webhook event is about 220 input tokens, which is $0.00002 against the weekly budget; a 4000-character body is about $0.0001.
173
+
172
174
  **Sources.**
173
175
  - `fswatch`: the kernel environment's `watch()` reports changed paths; those matching the glob are collected for `debounce` ms and delivered as one lick with `changed` or `gone` per path. A watch that overflows or stops says so in a lick (a stopped one with `severity="error"`, an overflow with `severity="warn"`), and saving its file restarts it. Changes made while no worker runs aren't reported. **The agent's own changes are dropped**, so it never licks itself: the kernel environment records every path its file tools write, edit, move or remove, and the time its `bash` commands run; a change to such a path within 2 s, or any change while an agent command runs or within 2 s after it, counts as the agent's. A change the user makes while an agent command runs is therefore dropped too. Background processes the agent started (`&`) aren't tracked, so their changes do lick.
174
176
  - `cron`: each line is a background durable task owned by the root conversation (`slicc.cron`), so Stop and rewinds don't affect it and it survives reloads. It sleeps until the next due time on the Harness clock, then delivers to the target the cone pointer names at that moment. Each due time is delivered exactly once, also across a worker restart: the fire's id is `<name>@<due time>`. **Missed fires** (seven was closed or the tab asleep) are delivered once, as one lick whose `count` is 1 plus the missed fires and whose text says how many were missed (counted exactly up to 100,000, then "at least 100,000"); the next fire is then computed from now. Editing a line replaces its task; removing it aborts the task.
@@ -210,12 +212,11 @@ Skills and prompt templates work as in pi's coding-agent. pi's loaders read with
210
212
  1. `~/.pi/agent/skills`;
211
213
  2. `~/.agents/skills`;
212
214
  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
+ 4. packages in the kernel's `/node_modules` whose `package.json` lists folders in `pi.skills`;
216
+ 5. the built-ins, which the worker writes to `/var/lib/slicc/agent/skills` on every start.
216
217
 
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`.
218
+ On a name collision the first skill found wins: an agent's own skills shadow the global ones, and a global skill shadows a package's and a built-in. A missing root is skipped.
219
+ - **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`, `package` or `builtin`.
219
220
  - **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.
220
221
  - **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.
221
222
  - **Untrusted folders.** A project's `.pi/skills` and `.agents/skills` are read only in trusted folders, which come with PR 21.
@@ -2,6 +2,7 @@ import { parseSchedule } from "./cron.js";
2
2
  const NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;
3
3
  const TARGET = /^(?:cone|scoop:[a-z0-9][a-z0-9._-]{0,63})$/;
4
4
  const DEBOUNCE = { min: 100, max: 6e4, default: 500 };
5
+ const TRIAGE_MAX = 1e3;
5
6
  function isName(value) {
6
7
  return NAME.test(value);
7
8
  }
@@ -95,9 +96,18 @@ function debounce(value) {
95
96
  );
96
97
  return value;
97
98
  }
99
+ function triage(value) {
100
+ if (value === void 0) return {};
101
+ const sentence = typeof value === "string" ? value.trim() : "";
102
+ if (!sentence || sentence.length > TRIAGE_MAX)
103
+ throw new Error(
104
+ `"triage" must be a sentence of 1\u2013${TRIAGE_MAX} characters saying which events the agent should see`
105
+ );
106
+ return { triage: sentence };
107
+ }
98
108
  function parseWatch(file, source, home) {
99
109
  const name = nameOf(file);
100
- const json = object(source, ["path", "glob", "target", "message", "debounce"]);
110
+ const json = object(source, ["path", "glob", "target", "message", "debounce", "triage"]);
101
111
  const glob = text(json.glob, "glob", "**");
102
112
  if (glob === "" || glob.startsWith("/"))
103
113
  throw new Error('"glob" must be relative to "path", such as "**/*.md"');
@@ -107,13 +117,19 @@ function parseWatch(file, source, home) {
107
117
  glob,
108
118
  target: target(json.target),
109
119
  message: text(json.message, "message", ""),
110
- debounce: debounce(json.debounce)
120
+ debounce: debounce(json.debounce),
121
+ ...triage(json.triage)
111
122
  };
112
123
  }
113
124
  function parseWebhook(file, source) {
114
125
  const name = nameOf(file);
115
- const json = object(source, ["target", "message"]);
116
- return { name, target: target(json.target), message: text(json.message, "message", "") };
126
+ const json = object(source, ["target", "message", "triage"]);
127
+ return {
128
+ name,
129
+ target: target(json.target),
130
+ message: text(json.message, "message", ""),
131
+ ...triage(json.triage)
132
+ };
117
133
  }
118
134
  export {
119
135
  isName,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/licks/config.ts"],
4
- "sourcesContent": ["import { parseSchedule } from './cron.ts';\nimport type { LickTarget } from './lick.ts';\n\nexport interface CronEntry {\n name: string;\n schedule: string;\n target: LickTarget;\n message: string;\n}\n\nexport interface WatchEntry {\n name: string;\n path: string;\n glob: string;\n target: LickTarget;\n message: string;\n debounce: number;\n}\n\nexport interface WebhookEntry {\n name: string;\n target: LickTarget;\n message: string;\n}\n\nexport interface ConfigError {\n file: string;\n line?: number;\n error: string;\n}\n\nexport interface Parsed<T> {\n entries: T[];\n errors: ConfigError[];\n}\n\nconst NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;\nconst TARGET = /^(?:cone|scoop:[a-z0-9][a-z0-9._-]{0,63})$/;\nconst DEBOUNCE = { min: 100, max: 60_000, default: 500 };\n\nexport function isName(value: string): boolean {\n return NAME.test(value);\n}\n\nexport function isTarget(value: string): value is LickTarget {\n return TARGET.test(value);\n}\n\nfunction scheduleOf(tokens: string[]): [string, string[]] {\n if (tokens[0]?.startsWith('@')) return [tokens[0], tokens.slice(1)];\n return [tokens.slice(0, 5).join(' '), tokens.slice(5)];\n}\n\nfunction cronLine(text: string): CronEntry {\n const [schedule, rest] = scheduleOf(text.trim().split(/\\s+/));\n parseSchedule(schedule);\n const [name, maybeTarget, ...words] = rest;\n if (name === undefined) throw new Error('a name must follow the schedule');\n if (!isName(name))\n throw new Error(\n `name \"${name}\" must be 1\u201364 of a-z, 0-9, \".\", \"_\" or \"-\", starting with a letter or digit`\n );\n const target = maybeTarget !== undefined && isTarget(maybeTarget) ? maybeTarget : 'cone';\n const message = (target === maybeTarget ? words : [maybeTarget, ...words])\n .filter(Boolean)\n .join(' ');\n return { name, schedule, target, message };\n}\n\nexport function parseCrontab(file: string, text: string): Parsed<CronEntry> {\n const entries: CronEntry[] = [];\n const errors: ConfigError[] = [];\n const seen = new Set<string>();\n text.split('\\n').forEach((raw, index) => {\n const line = raw.trim();\n if (line === '' || line.startsWith('#')) return;\n try {\n const entry = cronLine(line);\n if (seen.has(entry.name)) throw new Error(`name \"${entry.name}\" is used twice`);\n seen.add(entry.name);\n entries.push(entry);\n } catch (error) {\n errors.push({ file, line: index + 1, error: (error as Error).message });\n }\n });\n return { entries, errors };\n}\n\ntype Json = Record<string, unknown>;\n\nfunction object(text: string, keys: readonly string[]): Json {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch (error) {\n throw new Error(`not valid JSON: ${(error as Error).message}`);\n }\n if (typeof value !== 'object' || value === null || Array.isArray(value))\n throw new Error('must be a JSON object');\n const unknown = Object.keys(value).filter((key) => !keys.includes(key));\n if (unknown.length)\n throw new Error(\n `unknown field${unknown.length > 1 ? 's' : ''} ${unknown.join(', ')} (allowed: ${keys.join(', ')})`\n );\n return value as Json;\n}\n\nfunction text(value: unknown, field: string, fallback?: string): string {\n if (value === undefined && fallback !== undefined) return fallback;\n if (typeof value !== 'string') throw new Error(`\"${field}\" must be a string`);\n return value;\n}\n\nfunction target(value: unknown): LickTarget {\n const chosen = text(value, 'target', 'cone');\n if (!isTarget(chosen))\n throw new Error(`\"target\" must be \"cone\" or \"scoop:<name>\", not \"${chosen}\"`);\n return chosen;\n}\n\nexport function nameOf(file: string): string {\n const base = file.slice(file.lastIndexOf('/') + 1);\n if (!base.endsWith('.json')) throw new Error('must end in .json');\n const name = base.slice(0, -'.json'.length);\n if (!isName(name))\n throw new Error(\n `file name \"${name}\" must be 1\u201364 of a-z, 0-9, \".\", \"_\" or \"-\", starting with a letter or digit`\n );\n return name;\n}\n\nfunction absolute(path: string, home: string): string {\n if (path === '~') return home;\n if (path.startsWith('~/')) return `${home}${path.slice(1)}`;\n if (!path.startsWith('/'))\n throw new Error(`\"path\" must be absolute or start with ~/, not \"${path}\"`);\n return path;\n}\n\nfunction debounce(value: unknown): number {\n if (value === undefined) return DEBOUNCE.default;\n if (\n typeof value !== 'number' ||\n !Number.isInteger(value) ||\n value < DEBOUNCE.min ||\n value > DEBOUNCE.max\n )\n throw new Error(\n `\"debounce\" must be a whole number of milliseconds from ${DEBOUNCE.min} to ${DEBOUNCE.max}`\n );\n return value;\n}\n\nexport function parseWatch(file: string, source: string, home: string): WatchEntry {\n const name = nameOf(file);\n const json = object(source, ['path', 'glob', 'target', 'message', 'debounce']);\n const glob = text(json.glob, 'glob', '**');\n if (glob === '' || glob.startsWith('/'))\n throw new Error('\"glob\" must be relative to \"path\", such as \"**/*.md\"');\n return {\n name,\n path: absolute(text(json.path, 'path'), home).replace(/\\/+$/, '') || '/',\n glob,\n target: target(json.target),\n message: text(json.message, 'message', ''),\n debounce: debounce(json.debounce),\n };\n}\n\nexport function parseWebhook(file: string, source: string): WebhookEntry {\n const name = nameOf(file);\n const json = object(source, ['target', 'message']);\n return { name, target: target(json.target), message: text(json.message, 'message', '') };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { parseSchedule } from './cron.ts';\nimport type { LickTarget } from './lick.ts';\n\nexport interface CronEntry {\n name: string;\n schedule: string;\n target: LickTarget;\n message: string;\n}\n\nexport interface WatchEntry {\n name: string;\n path: string;\n glob: string;\n target: LickTarget;\n message: string;\n debounce: number;\n triage?: string;\n}\n\nexport interface WebhookEntry {\n name: string;\n target: LickTarget;\n message: string;\n triage?: string;\n}\n\nexport interface ConfigError {\n file: string;\n line?: number;\n error: string;\n}\n\nexport interface Parsed<T> {\n entries: T[];\n errors: ConfigError[];\n}\n\nconst NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;\nconst TARGET = /^(?:cone|scoop:[a-z0-9][a-z0-9._-]{0,63})$/;\nconst DEBOUNCE = { min: 100, max: 60_000, default: 500 };\nconst TRIAGE_MAX = 1000;\n\nexport function isName(value: string): boolean {\n return NAME.test(value);\n}\n\nexport function isTarget(value: string): value is LickTarget {\n return TARGET.test(value);\n}\n\nfunction scheduleOf(tokens: string[]): [string, string[]] {\n if (tokens[0]?.startsWith('@')) return [tokens[0], tokens.slice(1)];\n return [tokens.slice(0, 5).join(' '), tokens.slice(5)];\n}\n\nfunction cronLine(text: string): CronEntry {\n const [schedule, rest] = scheduleOf(text.trim().split(/\\s+/));\n parseSchedule(schedule);\n const [name, maybeTarget, ...words] = rest;\n if (name === undefined) throw new Error('a name must follow the schedule');\n if (!isName(name))\n throw new Error(\n `name \"${name}\" must be 1\u201364 of a-z, 0-9, \".\", \"_\" or \"-\", starting with a letter or digit`\n );\n const target = maybeTarget !== undefined && isTarget(maybeTarget) ? maybeTarget : 'cone';\n const message = (target === maybeTarget ? words : [maybeTarget, ...words])\n .filter(Boolean)\n .join(' ');\n return { name, schedule, target, message };\n}\n\nexport function parseCrontab(file: string, text: string): Parsed<CronEntry> {\n const entries: CronEntry[] = [];\n const errors: ConfigError[] = [];\n const seen = new Set<string>();\n text.split('\\n').forEach((raw, index) => {\n const line = raw.trim();\n if (line === '' || line.startsWith('#')) return;\n try {\n const entry = cronLine(line);\n if (seen.has(entry.name)) throw new Error(`name \"${entry.name}\" is used twice`);\n seen.add(entry.name);\n entries.push(entry);\n } catch (error) {\n errors.push({ file, line: index + 1, error: (error as Error).message });\n }\n });\n return { entries, errors };\n}\n\ntype Json = Record<string, unknown>;\n\nfunction object(text: string, keys: readonly string[]): Json {\n let value: unknown;\n try {\n value = JSON.parse(text);\n } catch (error) {\n throw new Error(`not valid JSON: ${(error as Error).message}`);\n }\n if (typeof value !== 'object' || value === null || Array.isArray(value))\n throw new Error('must be a JSON object');\n const unknown = Object.keys(value).filter((key) => !keys.includes(key));\n if (unknown.length)\n throw new Error(\n `unknown field${unknown.length > 1 ? 's' : ''} ${unknown.join(', ')} (allowed: ${keys.join(', ')})`\n );\n return value as Json;\n}\n\nfunction text(value: unknown, field: string, fallback?: string): string {\n if (value === undefined && fallback !== undefined) return fallback;\n if (typeof value !== 'string') throw new Error(`\"${field}\" must be a string`);\n return value;\n}\n\nfunction target(value: unknown): LickTarget {\n const chosen = text(value, 'target', 'cone');\n if (!isTarget(chosen))\n throw new Error(`\"target\" must be \"cone\" or \"scoop:<name>\", not \"${chosen}\"`);\n return chosen;\n}\n\nexport function nameOf(file: string): string {\n const base = file.slice(file.lastIndexOf('/') + 1);\n if (!base.endsWith('.json')) throw new Error('must end in .json');\n const name = base.slice(0, -'.json'.length);\n if (!isName(name))\n throw new Error(\n `file name \"${name}\" must be 1\u201364 of a-z, 0-9, \".\", \"_\" or \"-\", starting with a letter or digit`\n );\n return name;\n}\n\nfunction absolute(path: string, home: string): string {\n if (path === '~') return home;\n if (path.startsWith('~/')) return `${home}${path.slice(1)}`;\n if (!path.startsWith('/'))\n throw new Error(`\"path\" must be absolute or start with ~/, not \"${path}\"`);\n return path;\n}\n\nfunction debounce(value: unknown): number {\n if (value === undefined) return DEBOUNCE.default;\n if (\n typeof value !== 'number' ||\n !Number.isInteger(value) ||\n value < DEBOUNCE.min ||\n value > DEBOUNCE.max\n )\n throw new Error(\n `\"debounce\" must be a whole number of milliseconds from ${DEBOUNCE.min} to ${DEBOUNCE.max}`\n );\n return value;\n}\n\nfunction triage(value: unknown): { triage?: string } {\n if (value === undefined) return {};\n const sentence = typeof value === 'string' ? value.trim() : '';\n if (!sentence || sentence.length > TRIAGE_MAX)\n throw new Error(\n `\"triage\" must be a sentence of 1\u2013${TRIAGE_MAX} characters saying which events the agent should see`\n );\n return { triage: sentence };\n}\n\nexport function parseWatch(file: string, source: string, home: string): WatchEntry {\n const name = nameOf(file);\n const json = object(source, ['path', 'glob', 'target', 'message', 'debounce', 'triage']);\n const glob = text(json.glob, 'glob', '**');\n if (glob === '' || glob.startsWith('/'))\n throw new Error('\"glob\" must be relative to \"path\", such as \"**/*.md\"');\n return {\n name,\n path: absolute(text(json.path, 'path'), home).replace(/\\/+$/, '') || '/',\n glob,\n target: target(json.target),\n message: text(json.message, 'message', ''),\n debounce: debounce(json.debounce),\n ...triage(json.triage),\n };\n}\n\nexport function parseWebhook(file: string, source: string): WebhookEntry {\n const name = nameOf(file);\n const json = object(source, ['target', 'message', 'triage']);\n return {\n name,\n target: target(json.target),\n message: text(json.message, 'message', ''),\n ...triage(json.triage),\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["text", "target"]
7
7
  }
@@ -60,7 +60,8 @@ class Watches {
60
60
  text: `${paths.length} path${many ? "s" : ""} changed under ${entry.path} matching ${entry.glob}`,
61
61
  ...entry.message ? { body: entry.message } : {},
62
62
  items,
63
- target: entry.target
63
+ target: entry.target,
64
+ ...entry.triage ? { triage: entry.triage } : {}
64
65
  },
65
66
  context
66
67
  );
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/licks/fswatch.ts"],
4
- "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { ExecutionEnv, FileWatcher, WatchChange } from '@earendil-works/pi-durable/env';\nimport type { Activity } from '../kernel/activity.ts';\nimport { type ConfigError, parseWatch, type WatchEntry } from './config.ts';\nimport { glob } from './glob.ts';\nimport type { LickEvent } from './licks.ts';\n\nconst IGNORED = ['node_modules', '.git'];\n\ninterface Watching {\n json: string;\n entry: WatchEntry;\n match: (path: string) => boolean;\n watcher?: FileWatcher;\n timer?: ReturnType<typeof setTimeout>;\n paths: Set<string>;\n}\n\nexport interface ConfigFiles {\n read(path: string, context: Context): Promise<string | undefined>;\n list(path: string, context: Context): Promise<string[]>;\n}\n\nexport type Deliver = (event: LickEvent, context: Context) => Promise<void>;\n\nexport function relativeTo(base: string, path: string): string | undefined {\n if (path === base) return '';\n const prefix = base === '/' ? '/' : `${base}/`;\n return path.startsWith(prefix) ? path.slice(prefix.length) : undefined;\n}\n\nexport function configFiles(env: ExecutionEnv): ConfigFiles {\n return {\n async read(path, context) {\n const result = await env.readTextFile(path, context);\n return result.ok ? result.value : undefined;\n },\n async list(path, context) {\n const result = await env.listDir(path, context);\n if (!result.ok) return [];\n return result.value\n .filter(\n (info) => info.kind === 'file' && !info.name.startsWith('.') && !info.name.endsWith('~')\n )\n .map((info) => info.name)\n .sort();\n },\n };\n}\n\nfunction title(entry: WatchEntry): string {\n return `${entry.name}: ${entry.path}/${entry.glob}`;\n}\n\nexport class Watches {\n readonly #env: ExecutionEnv;\n readonly #deliver: Deliver;\n readonly #files: ConfigFiles;\n readonly #home: string;\n readonly #watching = new Map<string, Watching>();\n readonly #activity: Activity | undefined;\n\n constructor(env: ExecutionEnv, deliver: Deliver, home: string, activity?: Activity) {\n this.#env = env;\n this.#deliver = deliver;\n this.#files = configFiles(env);\n this.#home = home;\n this.#activity = activity;\n }\n\n get names(): string[] {\n return [...this.#watching.keys()];\n }\n\n async #fire(watch: Watching, context: Context): Promise<void> {\n const paths = [...watch.paths].sort();\n watch.paths.clear();\n if (!paths.length) return;\n const items: string[] = [];\n for (const path of paths) {\n const exists = await this.#env.exists(path, context);\n items.push(`${exists.ok && !exists.value ? 'gone' : 'changed'} ${path}`);\n }\n const { entry } = watch;\n const many = paths.length > 1;\n await this.#deliver(\n {\n channel: 'fswatch',\n source: entry.name,\n title: title(entry),\n text: `${paths.length} path${many ? 's' : ''} changed under ${entry.path} matching ${entry.glob}`,\n ...(entry.message ? { body: entry.message } : {}),\n items,\n target: entry.target,\n },\n context\n );\n }\n\n #paths(watch: Watching, paths: readonly string[], context: Context): void {\n for (const path of paths) {\n const relative = relativeTo(watch.entry.path, path);\n if (relative && watch.match(relative) && !this.#activity?.agentMade(path))\n watch.paths.add(path);\n }\n if (!watch.paths.size) return;\n clearTimeout(watch.timer);\n watch.timer = setTimeout(() => {\n void this.#fire(watch, context).catch(() => undefined);\n }, watch.entry.debounce);\n }\n\n #changed(watch: Watching, change: WatchChange, context: Context): void {\n if ('paths' in change) {\n this.#paths(watch, change.paths, context);\n return;\n }\n const stopped = 'error' in change;\n const text = stopped\n ? `The watch stopped: ${change.error.message}. Save its file again to restart it.`\n : 'Changes may have been missed for a while; look at the watched files again.';\n const { entry } = watch;\n if (stopped && this.#watching.get(entry.name) === watch) {\n this.#watching.delete(entry.name);\n clearTimeout(watch.timer);\n void watch.watcher?.close(context).catch(() => undefined);\n }\n void this.#deliver(\n {\n channel: 'fswatch',\n source: entry.name,\n title: title(entry),\n text,\n target: entry.target,\n severity: stopped ? 'error' : 'warn',\n },\n context\n ).catch(() => undefined);\n }\n\n async #start(entry: WatchEntry, json: string, file: string, context: Context) {\n const watch: Watching = { json, entry, match: glob(entry.glob), paths: new Set() };\n const result = await this.#env.watch(\n [{ path: entry.path, recursive: true, exclude: { names: IGNORED } }],\n (change) => this.#changed(watch, change, context),\n context\n );\n if (!result.ok) return { file, error: `can't watch ${entry.path}: ${result.error.message}` };\n watch.watcher = result.value;\n this.#watching.set(entry.name, watch);\n return undefined;\n }\n\n async stop(name: string, context: Context): Promise<void> {\n const watch = this.#watching.get(name);\n if (!watch) return;\n this.#watching.delete(name);\n clearTimeout(watch.timer);\n await watch.watcher?.close(context);\n }\n\n async reconcile(dir: string, context: Context): Promise<ConfigError[]> {\n const errors: ConfigError[] = [];\n const seen = new Set<string>();\n for (const name of await this.#files.list(dir, context)) {\n const file = `${dir}/${name}`;\n const json = await this.#files.read(file, context);\n if (json === undefined) continue;\n let entry: WatchEntry;\n try {\n entry = parseWatch(file, json, this.#home);\n } catch (error) {\n errors.push({ file, error: (error as Error).message });\n continue;\n }\n seen.add(entry.name);\n if (this.#watching.get(entry.name)?.json === json) continue;\n await this.stop(entry.name, context);\n const failed = await this.#start(entry, json, file, context);\n if (failed) errors.push(failed);\n }\n for (const name of this.names) if (!seen.has(name)) await this.stop(name, context);\n return errors;\n }\n\n async close(context: Context): Promise<void> {\n for (const name of this.names) await this.stop(name, context);\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { ExecutionEnv, FileWatcher, WatchChange } from '@earendil-works/pi-durable/env';\nimport type { Activity } from '../kernel/activity.ts';\nimport { type ConfigError, parseWatch, type WatchEntry } from './config.ts';\nimport { glob } from './glob.ts';\nimport type { LickEvent } from './licks.ts';\n\nconst IGNORED = ['node_modules', '.git'];\n\ninterface Watching {\n json: string;\n entry: WatchEntry;\n match: (path: string) => boolean;\n watcher?: FileWatcher;\n timer?: ReturnType<typeof setTimeout>;\n paths: Set<string>;\n}\n\nexport interface ConfigFiles {\n read(path: string, context: Context): Promise<string | undefined>;\n list(path: string, context: Context): Promise<string[]>;\n}\n\nexport type Deliver = (event: LickEvent, context: Context) => Promise<void>;\n\nexport function relativeTo(base: string, path: string): string | undefined {\n if (path === base) return '';\n const prefix = base === '/' ? '/' : `${base}/`;\n return path.startsWith(prefix) ? path.slice(prefix.length) : undefined;\n}\n\nexport function configFiles(env: ExecutionEnv): ConfigFiles {\n return {\n async read(path, context) {\n const result = await env.readTextFile(path, context);\n return result.ok ? result.value : undefined;\n },\n async list(path, context) {\n const result = await env.listDir(path, context);\n if (!result.ok) return [];\n return result.value\n .filter(\n (info) => info.kind === 'file' && !info.name.startsWith('.') && !info.name.endsWith('~')\n )\n .map((info) => info.name)\n .sort();\n },\n };\n}\n\nfunction title(entry: WatchEntry): string {\n return `${entry.name}: ${entry.path}/${entry.glob}`;\n}\n\nexport class Watches {\n readonly #env: ExecutionEnv;\n readonly #deliver: Deliver;\n readonly #files: ConfigFiles;\n readonly #home: string;\n readonly #watching = new Map<string, Watching>();\n readonly #activity: Activity | undefined;\n\n constructor(env: ExecutionEnv, deliver: Deliver, home: string, activity?: Activity) {\n this.#env = env;\n this.#deliver = deliver;\n this.#files = configFiles(env);\n this.#home = home;\n this.#activity = activity;\n }\n\n get names(): string[] {\n return [...this.#watching.keys()];\n }\n\n async #fire(watch: Watching, context: Context): Promise<void> {\n const paths = [...watch.paths].sort();\n watch.paths.clear();\n if (!paths.length) return;\n const items: string[] = [];\n for (const path of paths) {\n const exists = await this.#env.exists(path, context);\n items.push(`${exists.ok && !exists.value ? 'gone' : 'changed'} ${path}`);\n }\n const { entry } = watch;\n const many = paths.length > 1;\n await this.#deliver(\n {\n channel: 'fswatch',\n source: entry.name,\n title: title(entry),\n text: `${paths.length} path${many ? 's' : ''} changed under ${entry.path} matching ${entry.glob}`,\n ...(entry.message ? { body: entry.message } : {}),\n items,\n target: entry.target,\n ...(entry.triage ? { triage: entry.triage } : {}),\n },\n context\n );\n }\n\n #paths(watch: Watching, paths: readonly string[], context: Context): void {\n for (const path of paths) {\n const relative = relativeTo(watch.entry.path, path);\n if (relative && watch.match(relative) && !this.#activity?.agentMade(path))\n watch.paths.add(path);\n }\n if (!watch.paths.size) return;\n clearTimeout(watch.timer);\n watch.timer = setTimeout(() => {\n void this.#fire(watch, context).catch(() => undefined);\n }, watch.entry.debounce);\n }\n\n #changed(watch: Watching, change: WatchChange, context: Context): void {\n if ('paths' in change) {\n this.#paths(watch, change.paths, context);\n return;\n }\n const stopped = 'error' in change;\n const text = stopped\n ? `The watch stopped: ${change.error.message}. Save its file again to restart it.`\n : 'Changes may have been missed for a while; look at the watched files again.';\n const { entry } = watch;\n if (stopped && this.#watching.get(entry.name) === watch) {\n this.#watching.delete(entry.name);\n clearTimeout(watch.timer);\n void watch.watcher?.close(context).catch(() => undefined);\n }\n void this.#deliver(\n {\n channel: 'fswatch',\n source: entry.name,\n title: title(entry),\n text,\n target: entry.target,\n severity: stopped ? 'error' : 'warn',\n },\n context\n ).catch(() => undefined);\n }\n\n async #start(entry: WatchEntry, json: string, file: string, context: Context) {\n const watch: Watching = { json, entry, match: glob(entry.glob), paths: new Set() };\n const result = await this.#env.watch(\n [{ path: entry.path, recursive: true, exclude: { names: IGNORED } }],\n (change) => this.#changed(watch, change, context),\n context\n );\n if (!result.ok) return { file, error: `can't watch ${entry.path}: ${result.error.message}` };\n watch.watcher = result.value;\n this.#watching.set(entry.name, watch);\n return undefined;\n }\n\n async stop(name: string, context: Context): Promise<void> {\n const watch = this.#watching.get(name);\n if (!watch) return;\n this.#watching.delete(name);\n clearTimeout(watch.timer);\n await watch.watcher?.close(context);\n }\n\n async reconcile(dir: string, context: Context): Promise<ConfigError[]> {\n const errors: ConfigError[] = [];\n const seen = new Set<string>();\n for (const name of await this.#files.list(dir, context)) {\n const file = `${dir}/${name}`;\n const json = await this.#files.read(file, context);\n if (json === undefined) continue;\n let entry: WatchEntry;\n try {\n entry = parseWatch(file, json, this.#home);\n } catch (error) {\n errors.push({ file, error: (error as Error).message });\n continue;\n }\n seen.add(entry.name);\n if (this.#watching.get(entry.name)?.json === json) continue;\n await this.stop(entry.name, context);\n const failed = await this.#start(entry, json, file, context);\n if (failed) errors.push(failed);\n }\n for (const name of this.names) if (!seen.has(name)) await this.stop(name, context);\n return errors;\n }\n\n async close(context: Context): Promise<void> {\n for (const name of this.names) await this.stop(name, context);\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -6,6 +6,7 @@ import {
6
6
  lickId
7
7
  } from "./lick.js";
8
8
  import { entryLick, LICK_STATE_KIND, lickDecisions } from "./state.js";
9
+ import { skipEntry, skips } from "./triage.js";
9
10
  const LicksOutbox = defineDoc({
10
11
  kind: "slicc.licks",
11
12
  version: 1,
@@ -100,9 +101,9 @@ function createLicks(host) {
100
101
  async function previous(harness, id, context) {
101
102
  if (id === null) return "gone";
102
103
  const submission = await harness.submission(id, context);
103
- const record = await submission?.status(context);
104
- if (record?.status === "queued") return "queued";
105
- return record?.status === "unanswered" && record.reason === "aborted" ? "withdrawn" : "gone";
104
+ const record2 = await submission?.status(context);
105
+ if (record2?.status === "queued") return "queued";
106
+ return record2?.status === "unanswered" && record2.reason === "aborted" ? "withdrawn" : "gone";
106
107
  }
107
108
  async function settled(key, context) {
108
109
  const { harness } = await host;
@@ -149,20 +150,45 @@ function createLicks(host) {
149
150
  if (!lick) return void 0;
150
151
  return { lick, decision: lickDecisions(entries).get(id) };
151
152
  }
153
+ async function judged(event, context) {
154
+ if (!judge || !event.triage) return void 0;
155
+ return judge(event, context).catch(() => void 0);
156
+ }
157
+ async function seenBefore(event, context) {
158
+ if (event.eventId === void 0) return false;
159
+ const { harness } = await host;
160
+ const events = (await harness.snapshot(LicksOutbox, context))?.events ?? {};
161
+ return (events[`${event.channel}|${event.source}`] ?? []).includes(event.eventId);
162
+ }
163
+ async function record(event, verdict, now, context) {
164
+ const conversation = await (await host).resolve(event.target, event.channel, context);
165
+ await conversation.submit(skipEntry(event, verdict, now), context);
166
+ }
152
167
  let check;
168
+ let judge;
153
169
  return {
154
170
  guard(next) {
155
171
  check = next;
156
172
  },
157
- deliver(incoming, context) {
173
+ triage(next) {
174
+ judge = next;
175
+ },
176
+ async deliver(incoming, context) {
158
177
  const blocked = check?.(incoming.target);
159
178
  const event = blocked ? frozenNotice(incoming, blocked) : incoming;
179
+ if (judge && event.triage && await seenBefore(event, context)) return;
180
+ const verdict = await judged(event, context);
181
+ const skipped = skips(verdict);
160
182
  return serial(async () => {
161
183
  const { harness } = await host;
162
184
  const key = keyOf(event);
163
185
  await settled(key, context);
164
186
  const stream = `${event.channel}|${event.source}`;
165
187
  const now = event.at ?? Date.now();
188
+ if (skipped) {
189
+ if (await seenBefore(event, context)) return;
190
+ await record(event, verdict, now, context);
191
+ }
166
192
  const accepted = await harness.commit(async (tx) => {
167
193
  const doc = await tx.doc(LicksOutbox);
168
194
  const seen = doc.events[stream] ?? [];
@@ -170,6 +196,7 @@ function createLicks(host) {
170
196
  if (seen.includes(event.eventId)) return false;
171
197
  doc.events[stream] = [...seen, event.eventId].slice(-MAX_EVENTS);
172
198
  }
199
+ if (skipped) return false;
173
200
  const queue = doc.queues[key] ?? { submission: null, pending: null };
174
201
  queue.pending = queue.pending ? merge(queue.pending, event, now) : fresh(event, now, actions(event.channel));
175
202
  doc.queues[key] = queue;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/licks/licks.ts"],
4
- "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type Conversation,\n defineDoc,\n type Harness,\n type SubmissionId,\n} from '@earendil-works/pi-durable';\nimport {\n formatLick,\n type Lick,\n type LickAction,\n type LickChannel,\n type LickSeverity,\n type LickTarget,\n lickId,\n} from './lick.ts';\nimport { entryLick, LICK_STATE_KIND, type LickDecision, lickDecisions } from './state.ts';\n\nexport interface LickEvent {\n channel: LickChannel;\n source: string;\n title: string;\n text: string;\n body?: string;\n items?: string[];\n count?: number;\n target: LickTarget;\n coalesce?: boolean;\n severity?: LickSeverity;\n eventId?: string;\n at?: number;\n}\n\ntype Pending = {\n lick: Lick;\n items: string[];\n body: string | null;\n};\n\ntype Queue = {\n submission: number | null;\n pending: Pending | null;\n};\n\ntype OutboxState = {\n queues: Record<string, Queue>;\n events: Record<string, string[]>;\n};\n\nexport const LicksOutbox = defineDoc<OutboxState>({\n kind: 'slicc.licks',\n version: 1,\n scope: 'session',\n initial: () => ({ queues: {}, events: {} }),\n});\n\nexport interface LickHandler {\n confirm?(lick: Lick, reason: string | undefined, context: Context): Promise<string | undefined>;\n dismiss?(lick: Lick, reason: string | undefined, context: Context): Promise<string | undefined>;\n}\n\nexport interface LickFound {\n lick: Lick;\n decision: LickDecision | undefined;\n}\n\nexport interface Licks {\n deliver(event: LickEvent, context: Context): Promise<void>;\n flush(context: Context): Promise<void>;\n find(conversation: Conversation, id: string, context: Context): Promise<LickFound | undefined>;\n decide(\n conversation: Conversation,\n id: string,\n action: LickAction,\n reason: string | undefined,\n context: Context\n ): Promise<string>;\n handle(channel: LickChannel, handler: LickHandler): void;\n pending(context: Context): Promise<number>;\n forget(target: LickTarget, channel: LickChannel, source: string, context: Context): Promise<void>;\n guard(check: (target: LickTarget) => Blocked | undefined): void;\n}\n\nexport type Blocked = { id: string; name: string; at: number };\n\nexport function frozenNotice(event: LickEvent, blocked: Blocked): LickEvent {\n return {\n channel: event.channel,\n source: `frozen:${event.source}`,\n title: `${blocked.name} is frozen`,\n text: `${event.title} was skipped`,\n body: `${event.target} belongs to ${blocked.name}, which is frozen (${blocked.id}). Thaw it in the Freezer to get its licks again.`,\n target: 'cone',\n severity: 'warn',\n eventId: `${blocked.id}|${event.target}|${event.channel}|${event.source}|${blocked.at}`,\n };\n}\n\nexport interface LicksHost {\n harness: Harness;\n resolve(target: LickTarget, channel: LickChannel, context: Context): Promise<Conversation>;\n}\n\nexport const MAX_ITEMS = 50;\nexport const MAX_EVENTS = 64;\n\nexport function render(pending: Pending): Lick {\n const shown = pending.items.slice(0, MAX_ITEMS);\n const more = pending.items.length - shown.length;\n const lines = [\n ...(pending.body ? [pending.body] : []),\n ...shown,\n ...(more > 0 ? [`\u2026 and ${more} more`] : []),\n ];\n const { body: _, ...lick } = pending.lick;\n return lines.length ? { ...lick, body: lines.join('\\n') } : lick;\n}\n\nconst RANK: Record<LickSeverity, number> = { warn: 1, error: 2 };\n\nexport function strongest(\n a: LickSeverity | undefined,\n b: LickSeverity | undefined\n): LickSeverity | undefined {\n if (!a) return b;\n if (!b) return a;\n return RANK[b] > RANK[a] ? b : a;\n}\n\nfunction merge(pending: Pending, event: LickEvent, now: number): Pending {\n const items = [...pending.items];\n for (const item of event.items ?? []) if (!items.includes(item)) items.push(item);\n const { severity: _, ...lick } = pending.lick;\n const severity = strongest(pending.lick.severity, event.severity);\n return {\n lick: {\n ...lick,\n title: event.title,\n text: event.text,\n count: pending.lick.count + (event.count ?? 1),\n at: event.at ?? now,\n ...(severity ? { severity } : {}),\n },\n items,\n body: event.body ?? pending.body,\n };\n}\n\nfunction fresh(event: LickEvent, now: number, listed: LickAction[] | undefined): Pending {\n return {\n lick: {\n id: lickId(),\n channel: event.channel,\n source: event.source,\n title: event.title,\n text: event.text,\n count: event.count ?? 1,\n at: now,\n ...(listed ? { actions: listed } : {}),\n ...(event.severity ? { severity: event.severity } : {}),\n },\n items: [...new Set(event.items ?? [])],\n body: event.body ?? null,\n };\n}\n\nfunction keyOf(event: LickEvent): string {\n const base = `${event.target}|${event.channel}|${event.source}`;\n return event.coalesce === false ? `${base}|${event.eventId ?? lickId()}` : base;\n}\n\nexport function createLicks(host: Promise<LicksHost>): Licks {\n const handlers = new Map<LickChannel, LickHandler>();\n let lock: Promise<unknown> = Promise.resolve();\n const serial = <T>(operation: () => Promise<T>): Promise<T> => {\n const run = lock.then(operation, operation);\n lock = run.catch(() => undefined);\n return run;\n };\n\n const actions = (channel: LickChannel): LickAction[] | undefined => {\n const handler = handlers.get(channel);\n const list: LickAction[] = [];\n if (handler?.confirm) list.push('confirm');\n if (handler?.dismiss) list.push('dismiss');\n return list.length ? list : undefined;\n };\n\n async function previous(\n harness: Harness,\n id: number | null,\n context: Context\n ): Promise<'queued' | 'withdrawn' | 'gone'> {\n if (id === null) return 'gone';\n const submission = await harness.submission(id as SubmissionId, context);\n const record = await submission?.status(context);\n if (record?.status === 'queued') return 'queued';\n return record?.status === 'unanswered' && record.reason === 'aborted' ? 'withdrawn' : 'gone';\n }\n\n async function settled(key: string, context: Context): Promise<Queue | undefined> {\n const { harness } = await host;\n const queue = (await harness.snapshot(LicksOutbox, context))?.queues[key];\n if (!queue) return undefined;\n const before = await previous(harness, queue.submission, context);\n if (before === 'queued') return undefined;\n if (queue.pending && before === 'gone') return queue;\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n if (doc.queues[key]?.submission === queue.submission) delete doc.queues[key];\n }, context);\n return undefined;\n }\n\n async function flushKey(key: string, context: Context): Promise<void> {\n const { harness, resolve } = await host;\n const queue = await settled(key, context);\n if (!queue?.pending) return;\n const lick = render(queue.pending);\n const target = key.split('|')[0] as LickTarget;\n const conversation = await resolve(target, queue.pending.lick.channel, context);\n const submission = await conversation.submit(\n {\n type: 'input',\n content: formatLick(lick),\n whenBusy: 'steer',\n requestId: `lick:${lick.id}`,\n },\n context\n );\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n doc.queues[key] = { submission: submission.id, pending: null };\n }, context);\n }\n\n async function flushAll(context: Context): Promise<void> {\n const { harness } = await host;\n const keys = Object.keys((await harness.snapshot(LicksOutbox, context))?.queues ?? {});\n for (const key of keys) await flushKey(key, context);\n }\n\n async function find(\n conversation: Conversation,\n id: string,\n context: Context\n ): Promise<LickFound | undefined> {\n const { entries } = await conversation.context(context);\n const lick = entries.map(entryLick).find((candidate) => candidate?.id === id);\n if (!lick) return undefined;\n return { lick, decision: lickDecisions(entries).get(id) };\n }\n\n let check: ((target: LickTarget) => Blocked | undefined) | undefined;\n return {\n guard(next) {\n check = next;\n },\n deliver(incoming, context) {\n const blocked = check?.(incoming.target);\n const event = blocked ? frozenNotice(incoming, blocked) : incoming;\n return serial(async () => {\n const { harness } = await host;\n const key = keyOf(event);\n await settled(key, context);\n const stream = `${event.channel}|${event.source}`;\n const now = event.at ?? Date.now();\n const accepted = await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n const seen = doc.events[stream] ?? [];\n if (event.eventId !== undefined) {\n if (seen.includes(event.eventId)) return false;\n doc.events[stream] = [...seen, event.eventId].slice(-MAX_EVENTS);\n }\n const queue = doc.queues[key] ?? { submission: null, pending: null };\n queue.pending = queue.pending\n ? merge(queue.pending, event, now)\n : fresh(event, now, actions(event.channel));\n doc.queues[key] = queue;\n return true;\n }, context);\n if (accepted) await flushKey(key, context);\n });\n },\n flush: (context) => serial(() => flushAll(context)),\n find,\n async decide(conversation, id, action, reason, context) {\n const found = await find(conversation, id, context);\n if (!found) throw new Error(`There is no lick ${id} in this conversation.`);\n if (found.decision) throw new Error(`Lick ${id} is already ${found.decision}.`);\n if (!found.lick.actions?.includes(action))\n throw new Error(`Lick ${id} (${found.lick.channel}) has no ${action} action.`);\n const run = handlers.get(found.lick.channel)?.[action];\n if (!run) throw new Error(`Lick ${id} (${found.lick.channel}) has no ${action} action.`);\n return (await run(found.lick, reason, context)) ?? `Lick ${id} ${action}ed.`;\n },\n handle(channel, handler) {\n handlers.set(channel, handler);\n },\n forget(target, channel, source, context) {\n return serial(async () => {\n const { harness } = await host;\n const key = `${target}|${channel}|${source}`;\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n const queue = doc.queues[key];\n if (queue?.pending) queue.pending = null;\n }, context);\n });\n },\n async pending(context) {\n const { harness } = await host;\n const queues = (await harness.snapshot(LicksOutbox, context))?.queues ?? {};\n return Object.values(queues).filter((queue) => queue.submission !== null || queue.pending)\n .length;\n },\n };\n}\n\nexport { LICK_STATE_KIND };\n"],
5
- "mappings": "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",
6
- "names": []
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type Conversation,\n defineDoc,\n type Harness,\n type SubmissionId,\n} from '@earendil-works/pi-durable';\nimport {\n formatLick,\n type Lick,\n type LickAction,\n type LickChannel,\n type LickSeverity,\n type LickTarget,\n lickId,\n} from './lick.ts';\nimport { entryLick, LICK_STATE_KIND, type LickDecision, lickDecisions } from './state.ts';\nimport { skipEntry, skips, type Triage, type Verdict } from './triage.ts';\n\nexport interface LickEvent {\n channel: LickChannel;\n source: string;\n title: string;\n text: string;\n body?: string;\n items?: string[];\n count?: number;\n target: LickTarget;\n coalesce?: boolean;\n severity?: LickSeverity;\n eventId?: string;\n at?: number;\n triage?: string;\n}\n\ntype Pending = {\n lick: Lick;\n items: string[];\n body: string | null;\n};\n\ntype Queue = {\n submission: number | null;\n pending: Pending | null;\n};\n\ntype OutboxState = {\n queues: Record<string, Queue>;\n events: Record<string, string[]>;\n};\n\nexport const LicksOutbox = defineDoc<OutboxState>({\n kind: 'slicc.licks',\n version: 1,\n scope: 'session',\n initial: () => ({ queues: {}, events: {} }),\n});\n\nexport interface LickHandler {\n confirm?(lick: Lick, reason: string | undefined, context: Context): Promise<string | undefined>;\n dismiss?(lick: Lick, reason: string | undefined, context: Context): Promise<string | undefined>;\n}\n\nexport interface LickFound {\n lick: Lick;\n decision: LickDecision | undefined;\n}\n\nexport interface Licks {\n deliver(event: LickEvent, context: Context): Promise<void>;\n flush(context: Context): Promise<void>;\n find(conversation: Conversation, id: string, context: Context): Promise<LickFound | undefined>;\n decide(\n conversation: Conversation,\n id: string,\n action: LickAction,\n reason: string | undefined,\n context: Context\n ): Promise<string>;\n handle(channel: LickChannel, handler: LickHandler): void;\n pending(context: Context): Promise<number>;\n forget(target: LickTarget, channel: LickChannel, source: string, context: Context): Promise<void>;\n guard(check: (target: LickTarget) => Blocked | undefined): void;\n triage(judge: Triage): void;\n}\n\nexport type Blocked = { id: string; name: string; at: number };\n\nexport function frozenNotice(event: LickEvent, blocked: Blocked): LickEvent {\n return {\n channel: event.channel,\n source: `frozen:${event.source}`,\n title: `${blocked.name} is frozen`,\n text: `${event.title} was skipped`,\n body: `${event.target} belongs to ${blocked.name}, which is frozen (${blocked.id}). Thaw it in the Freezer to get its licks again.`,\n target: 'cone',\n severity: 'warn',\n eventId: `${blocked.id}|${event.target}|${event.channel}|${event.source}|${blocked.at}`,\n };\n}\n\nexport interface LicksHost {\n harness: Harness;\n resolve(target: LickTarget, channel: LickChannel, context: Context): Promise<Conversation>;\n}\n\nexport const MAX_ITEMS = 50;\nexport const MAX_EVENTS = 64;\n\nexport function render(pending: Pending): Lick {\n const shown = pending.items.slice(0, MAX_ITEMS);\n const more = pending.items.length - shown.length;\n const lines = [\n ...(pending.body ? [pending.body] : []),\n ...shown,\n ...(more > 0 ? [`\u2026 and ${more} more`] : []),\n ];\n const { body: _, ...lick } = pending.lick;\n return lines.length ? { ...lick, body: lines.join('\\n') } : lick;\n}\n\nconst RANK: Record<LickSeverity, number> = { warn: 1, error: 2 };\n\nexport function strongest(\n a: LickSeverity | undefined,\n b: LickSeverity | undefined\n): LickSeverity | undefined {\n if (!a) return b;\n if (!b) return a;\n return RANK[b] > RANK[a] ? b : a;\n}\n\nfunction merge(pending: Pending, event: LickEvent, now: number): Pending {\n const items = [...pending.items];\n for (const item of event.items ?? []) if (!items.includes(item)) items.push(item);\n const { severity: _, ...lick } = pending.lick;\n const severity = strongest(pending.lick.severity, event.severity);\n return {\n lick: {\n ...lick,\n title: event.title,\n text: event.text,\n count: pending.lick.count + (event.count ?? 1),\n at: event.at ?? now,\n ...(severity ? { severity } : {}),\n },\n items,\n body: event.body ?? pending.body,\n };\n}\n\nfunction fresh(event: LickEvent, now: number, listed: LickAction[] | undefined): Pending {\n return {\n lick: {\n id: lickId(),\n channel: event.channel,\n source: event.source,\n title: event.title,\n text: event.text,\n count: event.count ?? 1,\n at: now,\n ...(listed ? { actions: listed } : {}),\n ...(event.severity ? { severity: event.severity } : {}),\n },\n items: [...new Set(event.items ?? [])],\n body: event.body ?? null,\n };\n}\n\nfunction keyOf(event: LickEvent): string {\n const base = `${event.target}|${event.channel}|${event.source}`;\n return event.coalesce === false ? `${base}|${event.eventId ?? lickId()}` : base;\n}\n\nexport function createLicks(host: Promise<LicksHost>): Licks {\n const handlers = new Map<LickChannel, LickHandler>();\n let lock: Promise<unknown> = Promise.resolve();\n const serial = <T>(operation: () => Promise<T>): Promise<T> => {\n const run = lock.then(operation, operation);\n lock = run.catch(() => undefined);\n return run;\n };\n\n const actions = (channel: LickChannel): LickAction[] | undefined => {\n const handler = handlers.get(channel);\n const list: LickAction[] = [];\n if (handler?.confirm) list.push('confirm');\n if (handler?.dismiss) list.push('dismiss');\n return list.length ? list : undefined;\n };\n\n async function previous(\n harness: Harness,\n id: number | null,\n context: Context\n ): Promise<'queued' | 'withdrawn' | 'gone'> {\n if (id === null) return 'gone';\n const submission = await harness.submission(id as SubmissionId, context);\n const record = await submission?.status(context);\n if (record?.status === 'queued') return 'queued';\n return record?.status === 'unanswered' && record.reason === 'aborted' ? 'withdrawn' : 'gone';\n }\n\n async function settled(key: string, context: Context): Promise<Queue | undefined> {\n const { harness } = await host;\n const queue = (await harness.snapshot(LicksOutbox, context))?.queues[key];\n if (!queue) return undefined;\n const before = await previous(harness, queue.submission, context);\n if (before === 'queued') return undefined;\n if (queue.pending && before === 'gone') return queue;\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n if (doc.queues[key]?.submission === queue.submission) delete doc.queues[key];\n }, context);\n return undefined;\n }\n\n async function flushKey(key: string, context: Context): Promise<void> {\n const { harness, resolve } = await host;\n const queue = await settled(key, context);\n if (!queue?.pending) return;\n const lick = render(queue.pending);\n const target = key.split('|')[0] as LickTarget;\n const conversation = await resolve(target, queue.pending.lick.channel, context);\n const submission = await conversation.submit(\n {\n type: 'input',\n content: formatLick(lick),\n whenBusy: 'steer',\n requestId: `lick:${lick.id}`,\n },\n context\n );\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n doc.queues[key] = { submission: submission.id, pending: null };\n }, context);\n }\n\n async function flushAll(context: Context): Promise<void> {\n const { harness } = await host;\n const keys = Object.keys((await harness.snapshot(LicksOutbox, context))?.queues ?? {});\n for (const key of keys) await flushKey(key, context);\n }\n\n async function find(\n conversation: Conversation,\n id: string,\n context: Context\n ): Promise<LickFound | undefined> {\n const { entries } = await conversation.context(context);\n const lick = entries.map(entryLick).find((candidate) => candidate?.id === id);\n if (!lick) return undefined;\n return { lick, decision: lickDecisions(entries).get(id) };\n }\n\n async function judged(event: LickEvent, context: Context): Promise<Verdict | undefined> {\n if (!judge || !event.triage) return undefined;\n return judge(event, context).catch(() => undefined);\n }\n\n async function seenBefore(event: LickEvent, context: Context): Promise<boolean> {\n if (event.eventId === undefined) return false;\n const { harness } = await host;\n const events = (await harness.snapshot(LicksOutbox, context))?.events ?? {};\n return (events[`${event.channel}|${event.source}`] ?? []).includes(event.eventId);\n }\n\n async function record(event: LickEvent, verdict: Verdict, now: number, context: Context) {\n const conversation = await (await host).resolve(event.target, event.channel, context);\n await conversation.submit(skipEntry(event, verdict, now), context);\n }\n\n let check: ((target: LickTarget) => Blocked | undefined) | undefined;\n let judge: Triage | undefined;\n return {\n guard(next) {\n check = next;\n },\n triage(next) {\n judge = next;\n },\n async deliver(incoming, context) {\n const blocked = check?.(incoming.target);\n const event = blocked ? frozenNotice(incoming, blocked) : incoming;\n if (judge && event.triage && (await seenBefore(event, context))) return;\n const verdict = await judged(event, context);\n const skipped = skips(verdict);\n return serial(async () => {\n const { harness } = await host;\n const key = keyOf(event);\n await settled(key, context);\n const stream = `${event.channel}|${event.source}`;\n const now = event.at ?? Date.now();\n if (skipped) {\n if (await seenBefore(event, context)) return;\n await record(event, verdict, now, context);\n }\n const accepted = await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n const seen = doc.events[stream] ?? [];\n if (event.eventId !== undefined) {\n if (seen.includes(event.eventId)) return false;\n doc.events[stream] = [...seen, event.eventId].slice(-MAX_EVENTS);\n }\n if (skipped) return false;\n const queue = doc.queues[key] ?? { submission: null, pending: null };\n queue.pending = queue.pending\n ? merge(queue.pending, event, now)\n : fresh(event, now, actions(event.channel));\n doc.queues[key] = queue;\n return true;\n }, context);\n if (accepted) await flushKey(key, context);\n });\n },\n flush: (context) => serial(() => flushAll(context)),\n find,\n async decide(conversation, id, action, reason, context) {\n const found = await find(conversation, id, context);\n if (!found) throw new Error(`There is no lick ${id} in this conversation.`);\n if (found.decision) throw new Error(`Lick ${id} is already ${found.decision}.`);\n if (!found.lick.actions?.includes(action))\n throw new Error(`Lick ${id} (${found.lick.channel}) has no ${action} action.`);\n const run = handlers.get(found.lick.channel)?.[action];\n if (!run) throw new Error(`Lick ${id} (${found.lick.channel}) has no ${action} action.`);\n return (await run(found.lick, reason, context)) ?? `Lick ${id} ${action}ed.`;\n },\n handle(channel, handler) {\n handlers.set(channel, handler);\n },\n forget(target, channel, source, context) {\n return serial(async () => {\n const { harness } = await host;\n const key = `${target}|${channel}|${source}`;\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n const queue = doc.queues[key];\n if (queue?.pending) queue.pending = null;\n }, context);\n });\n },\n async pending(context) {\n const { harness } = await host;\n const queues = (await harness.snapshot(LicksOutbox, context))?.queues ?? {};\n return Object.values(queues).filter((queue) => queue.submission !== null || queue.pending)\n .length;\n },\n };\n}\n\nexport { LICK_STATE_KIND };\n"],
5
+ "mappings": "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",
6
+ "names": ["record"]
7
7
  }
@@ -38,7 +38,8 @@ function webhookLick(name, entry, delivery) {
38
38
  body: [headers, payload].filter(Boolean).join("\n\n"),
39
39
  target: entry.target,
40
40
  coalesce: false,
41
- ...delivery.id ? { eventId: delivery.id } : {}
41
+ ...delivery.id ? { eventId: delivery.id } : {},
42
+ ...entry.triage ? { triage: entry.triage } : {}
42
43
  };
43
44
  }
44
45
  async function webhooksIn(files, dir, context) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/licks/sources.ts"],
4
- "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { Conversation, Harness } from '@earendil-works/pi-durable';\nimport type { ExecutionEnv, FileWatcher } from '@earendil-works/pi-durable/env';\nimport type { Activity } from '../kernel/activity.ts';\nimport { type BootFacts, bootLicks } from './boot.ts';\nimport { type ConfigError, parseCrontab, parseWebhook, type WebhookEntry } from './config.ts';\nimport type { cronTask } from './extension.ts';\nimport { type ConfigFiles, configFiles, Watches } from './fswatch.ts';\nimport type { LickChannel } from './lick.ts';\nimport type { LickEvent, Licks } from './licks.ts';\nimport { reconcileCron } from './schedules.ts';\n\nexport const SLICC_DIR = '.slicc';\n\nexport interface LickSourcesOptions {\n harness: Harness;\n root: Conversation;\n cone: () => Conversation;\n env: ExecutionEnv;\n home: string;\n licks: Licks;\n cron: ReturnType<typeof cronTask>;\n now?: () => number;\n flushEvery?: number;\n activity?: Activity;\n}\n\nexport interface WebhookDelivery {\n id?: string;\n headers?: Record<string, string>;\n body: unknown;\n}\n\nexport interface LickSources {\n start(context: Context): Promise<void>;\n reconcile(context: Context): Promise<void>;\n boot(facts: BootFacts, context: Context): Promise<void>;\n webhook(name: string, delivery: WebhookDelivery, context: Context): Promise<boolean>;\n close(context: Context): Promise<void>;\n}\n\nexport function hash(text: string): string {\n let value = 2166136261;\n for (let at = 0; at < text.length; at++) value = Math.imul(value ^ text.charCodeAt(at), 16777619);\n return (value >>> 0).toString(36);\n}\n\nexport function shortPath(path: string, home: string): string {\n return path === home || path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;\n}\n\nexport function configLick(error: ConfigError, home: string): LickEvent {\n const file = shortPath(error.file, home);\n const channel: LickChannel = error.file.includes('/watches/')\n ? 'fswatch'\n : error.file.includes('/webhooks/')\n ? 'webhook'\n : 'cron';\n const text = `${error.line ? `line ${error.line}: ` : ''}${error.error}`;\n return {\n channel,\n source: `config:${file}`,\n title: `Invalid ${file}`,\n text,\n body: 'Ignored until the file is fixed.\\nThe formats are in the slicc-agent README.',\n target: 'cone',\n severity: 'error',\n eventId: hash(`${file}\\n${text}`),\n };\n}\n\nexport function webhookLick(\n name: string,\n entry: WebhookEntry,\n delivery: WebhookDelivery\n): LickEvent {\n const payload =\n typeof delivery.body === 'string' ? delivery.body : JSON.stringify(delivery.body, null, 2);\n const headers = Object.entries(delivery.headers ?? {})\n .map(([key, value]) => `${key}: ${value}`)\n .join('\\n');\n return {\n channel: 'webhook',\n source: name,\n title: name,\n text: entry.message || `Webhook ${name} received a request`,\n body: [headers, payload].filter(Boolean).join('\\n\\n'),\n target: entry.target,\n coalesce: false,\n ...(delivery.id ? { eventId: delivery.id } : {}),\n };\n}\n\nasync function webhooksIn(\n files: ConfigFiles,\n dir: string,\n context: Context\n): Promise<{ entries: Map<string, WebhookEntry>; errors: ConfigError[] }> {\n const errors: ConfigError[] = [];\n const into = new Map<string, WebhookEntry>();\n for (const name of await files.list(dir, context)) {\n const file = `${dir}/${name}`;\n const json = await files.read(file, context);\n if (json === undefined) continue;\n try {\n const entry = parseWebhook(file, json);\n into.set(entry.name, entry);\n } catch (error) {\n errors.push({ file, error: (error as Error).message });\n }\n }\n return { entries: into, errors };\n}\n\nexport function createLickSources(options: LickSourcesOptions): LickSources {\n const { harness, env, licks, home } = options;\n const dir = `${home}/${SLICC_DIR}`;\n const now = options.now ?? Date.now;\n const files = configFiles(env);\n const deliver = (event: LickEvent, context: Context) => licks.deliver(event, context);\n const watches = new Watches(env, deliver, home, options.activity);\n let webhooks = new Map<string, WebhookEntry>();\n let control: FileWatcher | undefined;\n let reconcileTimer: ReturnType<typeof setTimeout> | undefined;\n let flushTimer: ReturnType<typeof setInterval> | undefined;\n let closed = false;\n let lock: Promise<unknown> = Promise.resolve();\n const serial = <T>(operation: () => Promise<T>): Promise<T> => {\n const run = lock.then(operation, operation);\n lock = run.catch(() => undefined);\n return run;\n };\n\n async function reconcileNow(context: Context): Promise<void> {\n if (closed) return;\n const crontabFile = `${dir}/crontab`;\n const crontab = parseCrontab(crontabFile, (await files.read(crontabFile, context)) ?? '');\n await reconcileCron({ ...options, now }, crontab.entries, context);\n const watchErrors = await watches.reconcile(`${dir}/watches`, context);\n const found = await webhooksIn(files, `${dir}/webhooks`, context);\n webhooks = found.entries;\n const errors = [...crontab.errors, ...watchErrors, ...found.errors];\n for (const error of errors) await deliver(configLick(error, home), context);\n }\n\n const reconcile = (context: Context) => serial(() => reconcileNow(context));\n\n function scheduleReconcile(context: Context): void {\n clearTimeout(reconcileTimer);\n reconcileTimer = setTimeout(() => {\n void reconcile(context).catch(() => undefined);\n }, 300);\n }\n\n async function flushIfWaiting(context: Context): Promise<void> {\n if (await licks.pending(context)) await licks.flush(context);\n }\n\n return {\n async start(context) {\n await licks.flush(context);\n await reconcile(context);\n const result = await env.watch(\n [{ path: dir, recursive: true }],\n () => scheduleReconcile(context),\n context\n );\n if (result.ok) control = result.value;\n flushTimer = setInterval(() => {\n void flushIfWaiting(context).catch(() => undefined);\n }, options.flushEvery ?? 2000);\n },\n reconcile,\n boot: (facts, context) => bootLicks(harness, options.cone(), deliver, facts, context),\n async webhook(name, delivery, context) {\n const entry = webhooks.get(name);\n if (!entry) return false;\n await deliver(webhookLick(name, entry, delivery), context);\n return true;\n },\n async close(context) {\n closed = true;\n clearTimeout(reconcileTimer);\n clearInterval(flushTimer);\n await control?.close(context);\n await watches.close(context);\n },\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { Conversation, Harness } from '@earendil-works/pi-durable';\nimport type { ExecutionEnv, FileWatcher } from '@earendil-works/pi-durable/env';\nimport type { Activity } from '../kernel/activity.ts';\nimport { type BootFacts, bootLicks } from './boot.ts';\nimport { type ConfigError, parseCrontab, parseWebhook, type WebhookEntry } from './config.ts';\nimport type { cronTask } from './extension.ts';\nimport { type ConfigFiles, configFiles, Watches } from './fswatch.ts';\nimport type { LickChannel } from './lick.ts';\nimport type { LickEvent, Licks } from './licks.ts';\nimport { reconcileCron } from './schedules.ts';\n\nexport const SLICC_DIR = '.slicc';\n\nexport interface LickSourcesOptions {\n harness: Harness;\n root: Conversation;\n cone: () => Conversation;\n env: ExecutionEnv;\n home: string;\n licks: Licks;\n cron: ReturnType<typeof cronTask>;\n now?: () => number;\n flushEvery?: number;\n activity?: Activity;\n}\n\nexport interface WebhookDelivery {\n id?: string;\n headers?: Record<string, string>;\n body: unknown;\n}\n\nexport interface LickSources {\n start(context: Context): Promise<void>;\n reconcile(context: Context): Promise<void>;\n boot(facts: BootFacts, context: Context): Promise<void>;\n webhook(name: string, delivery: WebhookDelivery, context: Context): Promise<boolean>;\n close(context: Context): Promise<void>;\n}\n\nexport function hash(text: string): string {\n let value = 2166136261;\n for (let at = 0; at < text.length; at++) value = Math.imul(value ^ text.charCodeAt(at), 16777619);\n return (value >>> 0).toString(36);\n}\n\nexport function shortPath(path: string, home: string): string {\n return path === home || path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;\n}\n\nexport function configLick(error: ConfigError, home: string): LickEvent {\n const file = shortPath(error.file, home);\n const channel: LickChannel = error.file.includes('/watches/')\n ? 'fswatch'\n : error.file.includes('/webhooks/')\n ? 'webhook'\n : 'cron';\n const text = `${error.line ? `line ${error.line}: ` : ''}${error.error}`;\n return {\n channel,\n source: `config:${file}`,\n title: `Invalid ${file}`,\n text,\n body: 'Ignored until the file is fixed.\\nThe formats are in the slicc-agent README.',\n target: 'cone',\n severity: 'error',\n eventId: hash(`${file}\\n${text}`),\n };\n}\n\nexport function webhookLick(\n name: string,\n entry: WebhookEntry,\n delivery: WebhookDelivery\n): LickEvent {\n const payload =\n typeof delivery.body === 'string' ? delivery.body : JSON.stringify(delivery.body, null, 2);\n const headers = Object.entries(delivery.headers ?? {})\n .map(([key, value]) => `${key}: ${value}`)\n .join('\\n');\n return {\n channel: 'webhook',\n source: name,\n title: name,\n text: entry.message || `Webhook ${name} received a request`,\n body: [headers, payload].filter(Boolean).join('\\n\\n'),\n target: entry.target,\n coalesce: false,\n ...(delivery.id ? { eventId: delivery.id } : {}),\n ...(entry.triage ? { triage: entry.triage } : {}),\n };\n}\n\nasync function webhooksIn(\n files: ConfigFiles,\n dir: string,\n context: Context\n): Promise<{ entries: Map<string, WebhookEntry>; errors: ConfigError[] }> {\n const errors: ConfigError[] = [];\n const into = new Map<string, WebhookEntry>();\n for (const name of await files.list(dir, context)) {\n const file = `${dir}/${name}`;\n const json = await files.read(file, context);\n if (json === undefined) continue;\n try {\n const entry = parseWebhook(file, json);\n into.set(entry.name, entry);\n } catch (error) {\n errors.push({ file, error: (error as Error).message });\n }\n }\n return { entries: into, errors };\n}\n\nexport function createLickSources(options: LickSourcesOptions): LickSources {\n const { harness, env, licks, home } = options;\n const dir = `${home}/${SLICC_DIR}`;\n const now = options.now ?? Date.now;\n const files = configFiles(env);\n const deliver = (event: LickEvent, context: Context) => licks.deliver(event, context);\n const watches = new Watches(env, deliver, home, options.activity);\n let webhooks = new Map<string, WebhookEntry>();\n let control: FileWatcher | undefined;\n let reconcileTimer: ReturnType<typeof setTimeout> | undefined;\n let flushTimer: ReturnType<typeof setInterval> | undefined;\n let closed = false;\n let lock: Promise<unknown> = Promise.resolve();\n const serial = <T>(operation: () => Promise<T>): Promise<T> => {\n const run = lock.then(operation, operation);\n lock = run.catch(() => undefined);\n return run;\n };\n\n async function reconcileNow(context: Context): Promise<void> {\n if (closed) return;\n const crontabFile = `${dir}/crontab`;\n const crontab = parseCrontab(crontabFile, (await files.read(crontabFile, context)) ?? '');\n await reconcileCron({ ...options, now }, crontab.entries, context);\n const watchErrors = await watches.reconcile(`${dir}/watches`, context);\n const found = await webhooksIn(files, `${dir}/webhooks`, context);\n webhooks = found.entries;\n const errors = [...crontab.errors, ...watchErrors, ...found.errors];\n for (const error of errors) await deliver(configLick(error, home), context);\n }\n\n const reconcile = (context: Context) => serial(() => reconcileNow(context));\n\n function scheduleReconcile(context: Context): void {\n clearTimeout(reconcileTimer);\n reconcileTimer = setTimeout(() => {\n void reconcile(context).catch(() => undefined);\n }, 300);\n }\n\n async function flushIfWaiting(context: Context): Promise<void> {\n if (await licks.pending(context)) await licks.flush(context);\n }\n\n return {\n async start(context) {\n await licks.flush(context);\n await reconcile(context);\n const result = await env.watch(\n [{ path: dir, recursive: true }],\n () => scheduleReconcile(context),\n context\n );\n if (result.ok) control = result.value;\n flushTimer = setInterval(() => {\n void flushIfWaiting(context).catch(() => undefined);\n }, options.flushEvery ?? 2000);\n },\n reconcile,\n boot: (facts, context) => bootLicks(harness, options.cone(), deliver, facts, context),\n async webhook(name, delivery, context) {\n const entry = webhooks.get(name);\n if (!entry) return false;\n await deliver(webhookLick(name, entry, delivery), context);\n return true;\n },\n async close(context) {\n closed = true;\n clearTimeout(reconcileTimer);\n clearInterval(flushTimer);\n await control?.close(context);\n await watches.close(context);\n },\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,86 @@
1
+ import { ADOBE } from "../adobe.js";
2
+ import { lickId } from "./lick.js";
3
+ const TRIAGE_MODEL = "clef-flash";
4
+ const TRIAGE_SKIP = 0.75;
5
+ const TRIAGE_KIND = "slicc.lick-triage";
6
+ const TRIAGE_LIMIT = 4e3;
7
+ const TRIAGE_TIMEOUT = 15e3;
8
+ function clip(text) {
9
+ return text.length > TRIAGE_LIMIT ? `${text.slice(0, TRIAGE_LIMIT)}\u2026` : text;
10
+ }
11
+ function stateOf(event) {
12
+ return {
13
+ channel: event.channel,
14
+ source: event.source,
15
+ title: event.title,
16
+ text: event.text,
17
+ ...event.body ? { body: clip(event.body) } : {},
18
+ ...event.items?.length ? { items: event.items.slice(0, 50) } : {}
19
+ };
20
+ }
21
+ function skips(verdict) {
22
+ return verdict?.decision === "ignore" && (verdict.probabilities.ignore ?? 0) >= TRIAGE_SKIP;
23
+ }
24
+ function lickTriage(models) {
25
+ return async (event) => {
26
+ const model = models.getModelOfType("classifier", ADOBE, TRIAGE_MODEL);
27
+ if (!model || !event.triage) return void 0;
28
+ const result = await models.classify(
29
+ model,
30
+ {
31
+ state: stateOf(event),
32
+ questions: {
33
+ triage: {
34
+ type: "choice",
35
+ instructions: `The state is an event from a ${event.channel} source the user configured. The user wants the agent woken only for events that match this: ${event.triage}`,
36
+ criteria: {
37
+ act: "the event matches what the user wants the agent to act on",
38
+ ignore: "the event does not match it, so the agent need not see it"
39
+ }
40
+ }
41
+ }
42
+ },
43
+ { timeoutMs: TRIAGE_TIMEOUT, maxRetries: 1 }
44
+ );
45
+ const answer = result.answers.triage;
46
+ if (result.stopReason !== "stop" || answer?.type !== "choice") return void 0;
47
+ if (answer.choice !== "act" && answer.choice !== "ignore") return void 0;
48
+ return {
49
+ decision: answer.choice,
50
+ confidence: answer.confidence,
51
+ probabilities: answer.probabilities,
52
+ model: `${ADOBE}/${TRIAGE_MODEL}`,
53
+ cost: result.usage?.cost.total ?? null
54
+ };
55
+ };
56
+ }
57
+ function skipEntry(event, verdict, now) {
58
+ const id = lickId();
59
+ const delivery = event.eventId ? `${event.channel}|${event.source}|${event.eventId}` : id;
60
+ return {
61
+ type: "write",
62
+ requestId: `lick-triage:${delivery}`,
63
+ entry: {
64
+ kind: TRIAGE_KIND,
65
+ data: {
66
+ lick: id,
67
+ ...stateOf(event),
68
+ count: event.count ?? 1,
69
+ at: now,
70
+ triage: event.triage ?? "",
71
+ ...verdict
72
+ }
73
+ }
74
+ };
75
+ }
76
+ export {
77
+ TRIAGE_KIND,
78
+ TRIAGE_LIMIT,
79
+ TRIAGE_MODEL,
80
+ TRIAGE_SKIP,
81
+ clip,
82
+ lickTriage,
83
+ skipEntry,
84
+ skips
85
+ };
86
+ //# sourceMappingURL=triage.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/licks/triage.ts"],
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { Models } from '@earendil-works/pi-ai/models';\nimport { ADOBE } from '../adobe.ts';\nimport { lickId } from './lick.ts';\nimport type { LickEvent } from './licks.ts';\n\nexport const TRIAGE_MODEL = 'clef-flash';\nexport const TRIAGE_SKIP = 0.75;\nexport const TRIAGE_KIND = 'slicc.lick-triage';\nexport const TRIAGE_LIMIT = 4000;\nconst TRIAGE_TIMEOUT = 15_000;\n\nexport interface Verdict {\n decision: 'act' | 'ignore';\n confidence: number;\n probabilities: Record<string, number>;\n model: string;\n cost: number | null;\n}\n\nexport type Triage = (event: LickEvent, context: Context) => Promise<Verdict | undefined>;\n\nexport function clip(text: string): string {\n return text.length > TRIAGE_LIMIT ? `${text.slice(0, TRIAGE_LIMIT)}\u2026` : text;\n}\n\nfunction stateOf(event: LickEvent) {\n return {\n channel: event.channel,\n source: event.source,\n title: event.title,\n text: event.text,\n ...(event.body ? { body: clip(event.body) } : {}),\n ...(event.items?.length ? { items: event.items.slice(0, 50) } : {}),\n };\n}\n\nexport function skips(verdict: Verdict | undefined): verdict is Verdict {\n return verdict?.decision === 'ignore' && (verdict.probabilities.ignore ?? 0) >= TRIAGE_SKIP;\n}\n\nexport function lickTriage(models: Pick<Models, 'getModelOfType' | 'classify'>): Triage {\n return async (event) => {\n const model = models.getModelOfType('classifier', ADOBE, TRIAGE_MODEL);\n if (!model || !event.triage) return undefined;\n const result = await models.classify(\n model,\n {\n state: stateOf(event),\n questions: {\n triage: {\n type: 'choice',\n instructions: `The state is an event from a ${event.channel} source the user configured. The user wants the agent woken only for events that match this: ${event.triage}`,\n criteria: {\n act: 'the event matches what the user wants the agent to act on',\n ignore: 'the event does not match it, so the agent need not see it',\n },\n },\n },\n },\n { timeoutMs: TRIAGE_TIMEOUT, maxRetries: 1 }\n );\n const answer = result.answers.triage;\n if (result.stopReason !== 'stop' || answer?.type !== 'choice') return undefined;\n if (answer.choice !== 'act' && answer.choice !== 'ignore') return undefined;\n return {\n decision: answer.choice,\n confidence: answer.confidence,\n probabilities: answer.probabilities,\n model: `${ADOBE}/${TRIAGE_MODEL}`,\n cost: result.usage?.cost.total ?? null,\n };\n };\n}\n\nexport function skipEntry(event: LickEvent, verdict: Verdict, now: number) {\n const id = lickId();\n const delivery = event.eventId ? `${event.channel}|${event.source}|${event.eventId}` : id;\n return {\n type: 'write' as const,\n requestId: `lick-triage:${delivery}`,\n entry: {\n kind: TRIAGE_KIND,\n data: {\n lick: id,\n ...stateOf(event),\n count: event.count ?? 1,\n at: now,\n triage: event.triage ?? '',\n ...verdict,\n },\n },\n };\n}\n"],
5
+ "mappings": "AAEA,SAAS,aAAa;AACtB,SAAS,cAAc;AAGhB,MAAM,eAAe;AACrB,MAAM,cAAc;AACpB,MAAM,cAAc;AACpB,MAAM,eAAe;AAC5B,MAAM,iBAAiB;AAYhB,SAAS,KAAK,MAAsB;AACzC,SAAO,KAAK,SAAS,eAAe,GAAG,KAAK,MAAM,GAAG,YAAY,CAAC,WAAM;AAC1E;AAEA,SAAS,QAAQ,OAAkB;AACjC,SAAO;AAAA,IACL,SAAS,MAAM;AAAA,IACf,QAAQ,MAAM;AAAA,IACd,OAAO,MAAM;AAAA,IACb,MAAM,MAAM;AAAA,IACZ,GAAI,MAAM,OAAO,EAAE,MAAM,KAAK,MAAM,IAAI,EAAE,IAAI,CAAC;AAAA,IAC/C,GAAI,MAAM,OAAO,SAAS,EAAE,OAAO,MAAM,MAAM,MAAM,GAAG,EAAE,EAAE,IAAI,CAAC;AAAA,EACnE;AACF;AAEO,SAAS,MAAM,SAAkD;AACtE,SAAO,SAAS,aAAa,aAAa,QAAQ,cAAc,UAAU,MAAM;AAClF;AAEO,SAAS,WAAW,QAA6D;AACtF,SAAO,OAAO,UAAU;AACtB,UAAM,QAAQ,OAAO,eAAe,cAAc,OAAO,YAAY;AACrE,QAAI,CAAC,SAAS,CAAC,MAAM,OAAQ,QAAO;AACpC,UAAM,SAAS,MAAM,OAAO;AAAA,MAC1B;AAAA,MACA;AAAA,QACE,OAAO,QAAQ,KAAK;AAAA,QACpB,WAAW;AAAA,UACT,QAAQ;AAAA,YACN,MAAM;AAAA,YACN,cAAc,gCAAgC,MAAM,OAAO,gGAAgG,MAAM,MAAM;AAAA,YACvK,UAAU;AAAA,cACR,KAAK;AAAA,cACL,QAAQ;AAAA,YACV;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,MACA,EAAE,WAAW,gBAAgB,YAAY,EAAE;AAAA,IAC7C;AACA,UAAM,SAAS,OAAO,QAAQ;AAC9B,QAAI,OAAO,eAAe,UAAU,QAAQ,SAAS,SAAU,QAAO;AACtE,QAAI,OAAO,WAAW,SAAS,OAAO,WAAW,SAAU,QAAO;AAClE,WAAO;AAAA,MACL,UAAU,OAAO;AAAA,MACjB,YAAY,OAAO;AAAA,MACnB,eAAe,OAAO;AAAA,MACtB,OAAO,GAAG,KAAK,IAAI,YAAY;AAAA,MAC/B,MAAM,OAAO,OAAO,KAAK,SAAS;AAAA,IACpC;AAAA,EACF;AACF;AAEO,SAAS,UAAU,OAAkB,SAAkB,KAAa;AACzE,QAAM,KAAK,OAAO;AAClB,QAAM,WAAW,MAAM,UAAU,GAAG,MAAM,OAAO,IAAI,MAAM,MAAM,IAAI,MAAM,OAAO,KAAK;AACvF,SAAO;AAAA,IACL,MAAM;AAAA,IACN,WAAW,eAAe,QAAQ;AAAA,IAClC,OAAO;AAAA,MACL,MAAM;AAAA,MACN,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,GAAG,QAAQ,KAAK;AAAA,QAChB,OAAO,MAAM,SAAS;AAAA,QACtB,IAAI;AAAA,QACJ,QAAQ,MAAM,UAAU;AAAA,QACxB,GAAG;AAAA,MACL;AAAA,IACF;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
package/dist/main.js CHANGED
@@ -24,6 +24,7 @@ import { HOME, kernelEnvironment, SliccKernelEnv } from "./kernel/env.js";
24
24
  import { processGroups } from "./kernel/groups.js";
25
25
  import { adminOver, userClients } from "./kernel/users.js";
26
26
  import { setupLicks } from "./licks/index.js";
27
+ import { lickTriage } from "./licks/triage.js";
27
28
  import { setupMcp } from "./mcp/index.js";
28
29
  import { setupMemory } from "./memory/index.js";
29
30
  import { modelsCommand } from "./models/index.js";
@@ -110,6 +111,7 @@ async function start(scope, ports, options) {
110
111
  const codemode = await codemodeTools(client, options, native, mcp.codemode);
111
112
  registry.install(codemodeExtension(codemode));
112
113
  const licks = setupLicks(registry);
114
+ licks.licks.triage(lickTriage(models));
113
115
  const scoops = setupScoops(registry, licks.licks, [...coding, ...codemode, ...memory.tools]);
114
116
  const skills = setupSkills(registry, licks.licks);
115
117
  const activity = createActivity();
package/dist/main.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/main.ts"],
4
- "sourcesContent": ["import { attachKernel } from '@ai-ecoverse/slicc-kernel';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { CredentialStore, Provider } from '@earendil-works/pi-ai';\nimport {\n createRegistry,\n type ModelRef,\n type Storage,\n type ToolRegistration,\n} from '@earendil-works/pi-durable';\nimport { CodingTools } from '@earendil-works/pi-durable/tools';\nimport { type Agent, openAgent } from './agent.ts';\nimport { attachChanges, CHANGES_MS } from './changes/index.ts';\nimport {\n codemodeExtension,\n codemodeTool,\n disabledBySettings,\n locateWasm,\n sandboxFactory,\n} from './codemode/index.ts';\nimport { EncryptedCredentialStore } from './credentials.ts';\nimport { attachFreezer } from './freezer/index.ts';\nimport { settingsText, titler } from './freezer/title.ts';\nimport { lateGelatiere } from './gelatiere/index.ts';\nimport { type AgentHost, hostAgent } from './host.ts';\nimport { imageReads } from './images/index.ts';\nimport { createActivity } from './kernel/activity.ts';\nimport type { KernelClient } from './kernel/client.ts';\nimport { HOME, kernelEnvironment, SliccKernelEnv } from './kernel/env.ts';\nimport { processGroups } from './kernel/groups.ts';\nimport { adminOver, type UserClient, type UserClients, userClients } from './kernel/users.ts';\nimport { setupLicks } from './licks/index.ts';\nimport { setupMcp } from './mcp/index.ts';\nimport { setupMemory } from './memory/index.ts';\nimport { modelsCommand } from './models/index.ts';\nimport { type Transport, transportFetch } from './net.ts';\nimport { sliccPrompt } from './prompt.ts';\nimport { identity } from './scoops/identity.ts';\nimport { type Assets, packageAssets, type ScoopsRuntime, setupScoops } from './scoops/index.ts';\nimport {\n createAgentSettings,\n createSliccModels,\n DEFAULT_MODEL,\n sliccProviders,\n} from './settings.ts';\nimport { RELOAD_MS, setupSkills } from './skills/index.ts';\nimport { setupSprinkles } from './sprinkles/index.ts';\nimport { openOpfsSqliteStorage } from './sqlite.ts';\nimport {\n agentVersion,\n browserConnected,\n commandsOnPath,\n kernelBoot,\n transportName,\n} from './system.ts';\nimport { type KernelPorts, kernelPorts, serveConnections, type WorkerScope } from './worker.ts';\n\nexport interface AgentWorkerScope extends WorkerScope {\n fetch: typeof fetch;\n location: { origin: string };\n}\n\nexport type KernelAttach = (port: MessagePort) => Promise<KernelClient & { transport: Transport }>;\n\nexport interface AgentWorkerOptions {\n model?: ModelRef;\n providers?: Provider[];\n attach?: KernelAttach;\n credentials?: () => Promise<CredentialStore>;\n storage?: () => Promise<Storage>;\n assets?: Assets;\n codemodeWasm?: () => Promise<WebAssembly.Module | undefined>;\n codemodeWorker?: string | URL;\n catalog?: string;\n users?: boolean;\n mcpRedirect?: string;\n}\n\nexport function usersOf(\n admin: MessagePort | undefined,\n wanted: boolean | undefined,\n attach: KernelAttach\n): UserClients | undefined {\n if (!admin) return undefined;\n if (wanted === false) {\n admin.close();\n return undefined;\n }\n return userClients(adminOver(admin), attach as (port: MessagePort) => Promise<UserClient>);\n}\n\nexport const STORE_DIR = '/.slicc';\n\nasync function attached(ports: Promise<KernelPorts>, options: AgentWorkerOptions) {\n const attach: KernelAttach = options.attach ?? attachKernel;\n const { kernel, admin } = await ports;\n const client = await attach(kernel);\n return { client, users: usersOf(admin, options.users, attach) };\n}\n\nfunction privateStore(env: SliccKernelEnv): Promise<unknown> {\n return env.exec(`mkdir -p ${STORE_DIR} && chmod 700 ${STORE_DIR}`, undefined, BACKGROUND_CONTEXT);\n}\n\nasync function codemodeTools(\n client: KernelClient,\n options: AgentWorkerOptions,\n native: typeof fetch,\n extra: Parameters<typeof codemodeTool>[0]['extra']\n) {\n const settings = await client.fs\n .readFile(`${HOME}/.pi/agent/settings.json`)\n .catch(() => undefined);\n if (disabledBySettings(settings === undefined ? undefined : new TextDecoder().decode(settings)))\n return [];\n const sandbox = sandboxFactory(\n options.codemodeWasm ?? (() => locateWasm(native, new URL(import.meta.url))),\n options.codemodeWorker\n );\n return [\n codemodeTool({ declared: CodingTools.tools as readonly ToolRegistration[], sandbox, extra }),\n ];\n}\n\nexport const MCP_CALLBACK = '/oauth/mcp-callback';\n\nasync function start(\n scope: AgentWorkerScope,\n ports: Promise<KernelPorts>,\n options: AgentWorkerOptions\n): Promise<AgentHost> {\n const { client, users } = await attached(ports, options);\n const home = new SliccKernelEnv(client, { cwd: HOME });\n await privateStore(home);\n const native = scope.fetch.bind(scope);\n scope.fetch = transportFetch(client.transport, scope.location.origin, native);\n const facts = {\n version: await agentVersion((url) => native(url)),\n commands: await commandsOnPath(client),\n transport: transportName(client.transport),\n browser: await browserConnected(client),\n };\n const credentials = await (options.credentials ?? (() => EncryptedCredentialStore.open()))();\n const providers = options.providers ?? sliccProviders();\n const models = createSliccModels(credentials, providers);\n const registry = createRegistry();\n registry.install(CodingTools);\n registry.install(sliccPrompt(facts));\n registry.install(\n imageReads(\n (CodingTools.tools as readonly ToolRegistration[]).find(\n (tool) => tool.name === 'read'\n ) as ToolRegistration\n )\n );\n const memory = setupMemory(registry);\n const coding = CodingTools.tools as readonly ToolRegistration[];\n const mcp = setupMcp(registry);\n const codemode = await codemodeTools(client, options, native, mcp.codemode);\n registry.install(codemodeExtension(codemode));\n const licks = setupLicks(registry);\n const scoops = setupScoops(registry, licks.licks, [...coding, ...codemode, ...memory.tools]);\n const skills = setupSkills(registry, licks.licks);\n const activity = createActivity();\n const groups = processGroups(client);\n const model = options.model ?? DEFAULT_MODEL;\n let opened: Agent | undefined;\n const environment = kernelEnvironment(client, {\n activity,\n groups,\n identify: identity(() => opened?.agents, model),\n });\n const agent = await openAgent({\n models,\n model,\n storage: await (options.storage ?? (() => openOpfsSqliteStorage()))(),\n registry,\n env: environment,\n });\n opened = agent;\n const sources = licks.attach(agent, { env: home, home: HOME, activity });\n await sources.start(BACKGROUND_CONTEXT);\n await sources.boot(\n { version: facts.version, boot: await kernelBoot(client) },\n BACKGROUND_CONTEXT\n );\n const skillsRuntime = await skills.attach(\n { env: home, home: HOME, assets: options.assets ?? packageAssets(), reloadMs: RELOAD_MS },\n BACKGROUND_CONTEXT\n );\n const mcpRuntime = await mcp.attach(\n {\n env: home,\n home: HOME,\n credentials,\n licks: licks.licks,\n fetch: scope.fetch,\n version: facts.version,\n reloadMs: RELOAD_MS,\n codemode: codemode.length > 0,\n cors: client.transport.traits?.crossOrigin === 'cors',\n redirect: options.mcpRedirect ?? new URL(MCP_CALLBACK, scope.location.origin).href,\n },\n BACKGROUND_CONTEXT\n );\n const gelatiere = lateGelatiere();\n const sprinkles = await setupSprinkles().attach(\n {\n intercept: gelatiere.intercept,\n harness: agent.harness,\n agents: agent.agents,\n licks: licks.licks,\n env: home,\n assets: options.assets ?? packageAssets(),\n reloadMs: RELOAD_MS,\n },\n BACKGROUND_CONTEXT\n );\n let roles: ScoopsRuntime | undefined;\n const memoryRuntime = await memory.attach(\n {\n harness: agent.harness,\n agents: agent.agents,\n env: home,\n home: HOME,\n reloadMs: RELOAD_MS,\n roles: async (using) => roles?.roles(using),\n },\n BACKGROUND_CONTEXT\n );\n const freezer = attachFreezer({\n harness: agent.harness,\n agents: agent.agents,\n env: home,\n extract: memory.extract,\n title: titler(models, settingsText(home, HOME)),\n });\n licks.licks.guard(freezer.frozenTarget);\n const settings = await createAgentSettings(models, credentials, providers, { mcp: mcpRuntime });\n const runtime = await scoops.attach(\n {\n harness: agent.harness,\n agents: agent.agents,\n groups,\n env: home,\n home: HOME,\n alive: alive(client),\n reads: skillsRuntime.dirs,\n sprinkle: sprinkles.command,\n memory: memoryRuntime.command,\n gelatiere: gelatiere.command,\n freezer: freezer.command,\n models: modelsCommand({ agents: agent.agents, settings, catalog: models }),\n mcp: mcp.direct,\n ...(options.assets ? { assets: options.assets } : {}),\n },\n BACKGROUND_CONTEXT\n );\n roles = runtime;\n gelatiere.attach({\n agents: agent.agents,\n licks: licks.licks,\n env: home,\n home: HOME,\n scoops: scoops.scoops,\n roles: runtime.roles,\n assets: options.assets ?? packageAssets(),\n fetch: scope.fetch,\n skills: skillsRuntime.skills,\n ...(options.catalog ? { catalog: options.catalog } : {}),\n });\n await memoryRuntime.reload(BACKGROUND_CONTEXT).catch(() => undefined);\n const changes = attachChanges({ agents: agent.agents, env: home, reloadMs: CHANGES_MS });\n return hostAgent(agent, {\n settings,\n licks: { licks: licks.licks, sources },\n scoops: { scoops: scoops.scoops, runtime },\n skills: skillsRuntime,\n sprinkles,\n memory: memoryRuntime,\n freezer,\n changes,\n mcp: mcpRuntime,\n ...(users ? { users } : {}),\n monitor: {\n kernel: client,\n groups,\n ...(users ? { users: () => users.users.list() } : {}),\n secrets: () => mcpRuntime.secrets(),\n },\n });\n}\n\nexport function alive(client: KernelClient) {\n return async (pid: number): Promise<boolean> => {\n if (!client.ps) return true;\n return (await client.ps()).some((process) => process.pid === pid);\n };\n}\n\nexport function runAgentWorker(\n scope: AgentWorkerScope,\n options: AgentWorkerOptions = {}\n): Promise<AgentHost> {\n const host = start(scope, kernelPorts(scope), options);\n host.catch(() => undefined);\n serveConnections(scope, host);\n return host;\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AAEnC;AAAA,EACE;AAAA,OAIK;AACP,SAAS,mBAAmB;AAC5B,SAAqB,iBAAiB;AACtC,SAAS,eAAe,kBAAkB;AAC1C;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gCAAgC;AACzC,SAAS,qBAAqB;AAC9B,SAAS,cAAc,cAAc;AACrC,SAAS,qBAAqB;AAC9B,SAAyB,iBAAiB;AAC1C,SAAS,kBAAkB;AAC3B,SAAS,sBAAsB;AAE/B,SAAS,MAAM,mBAAmB,sBAAsB;AACxD,SAAS,qBAAqB;AAC9B,SAAS,WAA8C,mBAAmB;AAC1E,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB;AACzB,SAAS,mBAAmB;AAC5B,SAAS,qBAAqB;AAC9B,SAAyB,sBAAsB;AAC/C,SAAS,mBAAmB;AAC5B,SAAS,gBAAgB;AACzB,SAAsB,eAAmC,mBAAmB;AAC5E;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,WAAW,mBAAmB;AACvC,SAAS,sBAAsB;AAC/B,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAA2B,aAAa,wBAA0C;AAuB3E,SAAS,QACd,OACA,QACA,QACyB;AACzB,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI,WAAW,OAAO;AACpB,UAAM,MAAM;AACZ,WAAO;AAAA,EACT;AACA,SAAO,YAAY,UAAU,KAAK,GAAG,MAAoD;AAC3F;AAEO,MAAM,YAAY;AAEzB,eAAe,SAAS,OAA6B,SAA6B;AAChF,QAAM,SAAuB,QAAQ,UAAU;AAC/C,QAAM,EAAE,QAAQ,MAAM,IAAI,MAAM;AAChC,QAAM,SAAS,MAAM,OAAO,MAAM;AAClC,SAAO,EAAE,QAAQ,OAAO,QAAQ,OAAO,QAAQ,OAAO,MAAM,EAAE;AAChE;AAEA,SAAS,aAAa,KAAuC;AAC3D,SAAO,IAAI,KAAK,YAAY,SAAS,iBAAiB,SAAS,IAAI,QAAW,kBAAkB;AAClG;AAEA,eAAe,cACb,QACA,SACA,QACA,OACA;AACA,QAAM,WAAW,MAAM,OAAO,GAC3B,SAAS,GAAG,IAAI,0BAA0B,EAC1C,MAAM,MAAM,MAAS;AACxB,MAAI,mBAAmB,aAAa,SAAY,SAAY,IAAI,YAAY,EAAE,OAAO,QAAQ,CAAC;AAC5F,WAAO,CAAC;AACV,QAAM,UAAU;AAAA,IACd,QAAQ,iBAAiB,MAAM,WAAW,QAAQ,IAAI,IAAI,YAAY,GAAG,CAAC;AAAA,IAC1E,QAAQ;AAAA,EACV;AACA,SAAO;AAAA,IACL,aAAa,EAAE,UAAU,YAAY,OAAsC,SAAS,MAAM,CAAC;AAAA,EAC7F;AACF;AAEO,MAAM,eAAe;AAE5B,eAAe,MACb,OACA,OACA,SACoB;AACpB,QAAM,EAAE,QAAQ,MAAM,IAAI,MAAM,SAAS,OAAO,OAAO;AACvD,QAAM,OAAO,IAAI,eAAe,QAAQ,EAAE,KAAK,KAAK,CAAC;AACrD,QAAM,aAAa,IAAI;AACvB,QAAM,SAAS,MAAM,MAAM,KAAK,KAAK;AACrC,QAAM,QAAQ,eAAe,OAAO,WAAW,MAAM,SAAS,QAAQ,MAAM;AAC5E,QAAM,QAAQ;AAAA,IACZ,SAAS,MAAM,aAAa,CAAC,QAAQ,OAAO,GAAG,CAAC;AAAA,IAChD,UAAU,MAAM,eAAe,MAAM;AAAA,IACrC,WAAW,cAAc,OAAO,SAAS;AAAA,IACzC,SAAS,MAAM,iBAAiB,MAAM;AAAA,EACxC;AACA,QAAM,cAAc,OAAO,QAAQ,gBAAgB,MAAM,yBAAyB,KAAK,IAAI;AAC3F,QAAM,YAAY,QAAQ,aAAa,eAAe;AACtD,QAAM,SAAS,kBAAkB,aAAa,SAAS;AACvD,QAAM,WAAW,eAAe;AAChC,WAAS,QAAQ,WAAW;AAC5B,WAAS,QAAQ,YAAY,KAAK,CAAC;AACnC,WAAS;AAAA,IACP;AAAA,MACG,YAAY,MAAsC;AAAA,QACjD,CAAC,SAAS,KAAK,SAAS;AAAA,MAC1B;AAAA,IACF;AAAA,EACF;AACA,QAAM,SAAS,YAAY,QAAQ;AACnC,QAAM,SAAS,YAAY;AAC3B,QAAM,MAAM,SAAS,QAAQ;AAC7B,QAAM,WAAW,MAAM,cAAc,QAAQ,SAAS,QAAQ,IAAI,QAAQ;AAC1E,WAAS,QAAQ,kBAAkB,QAAQ,CAAC;AAC5C,QAAM,QAAQ,WAAW,QAAQ;AACjC,QAAM,SAAS,YAAY,UAAU,MAAM,OAAO,CAAC,GAAG,QAAQ,GAAG,UAAU,GAAG,OAAO,KAAK,CAAC;AAC3F,QAAM,SAAS,YAAY,UAAU,MAAM,KAAK;AAChD,QAAM,WAAW,eAAe;AAChC,QAAM,SAAS,cAAc,MAAM;AACnC,QAAM,QAAQ,QAAQ,SAAS;AAC/B,MAAI;AACJ,QAAM,cAAc,kBAAkB,QAAQ;AAAA,IAC5C;AAAA,IACA;AAAA,IACA,UAAU,SAAS,MAAM,QAAQ,QAAQ,KAAK;AAAA,EAChD,CAAC;AACD,QAAM,QAAQ,MAAM,UAAU;AAAA,IAC5B;AAAA,IACA;AAAA,IACA,SAAS,OAAO,QAAQ,YAAY,MAAM,sBAAsB,IAAI;AAAA,IACpE;AAAA,IACA,KAAK;AAAA,EACP,CAAC;AACD,WAAS;AACT,QAAM,UAAU,MAAM,OAAO,OAAO,EAAE,KAAK,MAAM,MAAM,MAAM,SAAS,CAAC;AACvE,QAAM,QAAQ,MAAM,kBAAkB;AACtC,QAAM,QAAQ;AAAA,IACZ,EAAE,SAAS,MAAM,SAAS,MAAM,MAAM,WAAW,MAAM,EAAE;AAAA,IACzD;AAAA,EACF;AACA,QAAM,gBAAgB,MAAM,OAAO;AAAA,IACjC,EAAE,KAAK,MAAM,MAAM,MAAM,QAAQ,QAAQ,UAAU,cAAc,GAAG,UAAU,UAAU;AAAA,IACxF;AAAA,EACF;AACA,QAAM,aAAa,MAAM,IAAI;AAAA,IAC3B;AAAA,MACE,KAAK;AAAA,MACL,MAAM;AAAA,MACN;AAAA,MACA,OAAO,MAAM;AAAA,MACb,OAAO,MAAM;AAAA,MACb,SAAS,MAAM;AAAA,MACf,UAAU;AAAA,MACV,UAAU,SAAS,SAAS;AAAA,MAC5B,MAAM,OAAO,UAAU,QAAQ,gBAAgB;AAAA,MAC/C,UAAU,QAAQ,eAAe,IAAI,IAAI,cAAc,MAAM,SAAS,MAAM,EAAE;AAAA,IAChF;AAAA,IACA;AAAA,EACF;AACA,QAAM,YAAY,cAAc;AAChC,QAAM,YAAY,MAAM,eAAe,EAAE;AAAA,IACvC;AAAA,MACE,WAAW,UAAU;AAAA,MACrB,SAAS,MAAM;AAAA,MACf,QAAQ,MAAM;AAAA,MACd,OAAO,MAAM;AAAA,MACb,KAAK;AAAA,MACL,QAAQ,QAAQ,UAAU,cAAc;AAAA,MACxC,UAAU;AAAA,IACZ;AAAA,IACA;AAAA,EACF;AACA,MAAI;AACJ,QAAM,gBAAgB,MAAM,OAAO;AAAA,IACjC;AAAA,MACE,SAAS,MAAM;AAAA,MACf,QAAQ,MAAM;AAAA,MACd,KAAK;AAAA,MACL,MAAM;AAAA,MACN,UAAU;AAAA,MACV,OAAO,OAAO,UAAU,OAAO,MAAM,KAAK;AAAA,IAC5C;AAAA,IACA;AAAA,EACF;AACA,QAAM,UAAU,cAAc;AAAA,IAC5B,SAAS,MAAM;AAAA,IACf,QAAQ,MAAM;AAAA,IACd,KAAK;AAAA,IACL,SAAS,OAAO;AAAA,IAChB,OAAO,OAAO,QAAQ,aAAa,MAAM,IAAI,CAAC;AAAA,EAChD,CAAC;AACD,QAAM,MAAM,MAAM,QAAQ,YAAY;AACtC,QAAM,WAAW,MAAM,oBAAoB,QAAQ,aAAa,WAAW,EAAE,KAAK,WAAW,CAAC;AAC9F,QAAM,UAAU,MAAM,OAAO;AAAA,IAC3B;AAAA,MACE,SAAS,MAAM;AAAA,MACf,QAAQ,MAAM;AAAA,MACd;AAAA,MACA,KAAK;AAAA,MACL,MAAM;AAAA,MACN,OAAO,MAAM,MAAM;AAAA,MACnB,OAAO,cAAc;AAAA,MACrB,UAAU,UAAU;AAAA,MACpB,QAAQ,cAAc;AAAA,MACtB,WAAW,UAAU;AAAA,MACrB,SAAS,QAAQ;AAAA,MACjB,QAAQ,cAAc,EAAE,QAAQ,MAAM,QAAQ,UAAU,SAAS,OAAO,CAAC;AAAA,MACzE,KAAK,IAAI;AAAA,MACT,GAAI,QAAQ,SAAS,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC;AAAA,IACrD;AAAA,IACA;AAAA,EACF;AACA,UAAQ;AACR,YAAU,OAAO;AAAA,IACf,QAAQ,MAAM;AAAA,IACd,OAAO,MAAM;AAAA,IACb,KAAK;AAAA,IACL,MAAM;AAAA,IACN,QAAQ,OAAO;AAAA,IACf,OAAO,QAAQ;AAAA,IACf,QAAQ,QAAQ,UAAU,cAAc;AAAA,IACxC,OAAO,MAAM;AAAA,IACb,QAAQ,cAAc;AAAA,IACtB,GAAI,QAAQ,UAAU,EAAE,SAAS,QAAQ,QAAQ,IAAI,CAAC;AAAA,EACxD,CAAC;AACD,QAAM,cAAc,OAAO,kBAAkB,EAAE,MAAM,MAAM,MAAS;AACpE,QAAM,UAAU,cAAc,EAAE,QAAQ,MAAM,QAAQ,KAAK,MAAM,UAAU,WAAW,CAAC;AACvF,SAAO,UAAU,OAAO;AAAA,IACtB;AAAA,IACA,OAAO,EAAE,OAAO,MAAM,OAAO,QAAQ;AAAA,IACrC,QAAQ,EAAE,QAAQ,OAAO,QAAQ,QAAQ;AAAA,IACzC,QAAQ;AAAA,IACR;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA,KAAK;AAAA,IACL,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,IACzB,SAAS;AAAA,MACP,QAAQ;AAAA,MACR;AAAA,MACA,GAAI,QAAQ,EAAE,OAAO,MAAM,MAAM,MAAM,KAAK,EAAE,IAAI,CAAC;AAAA,MACnD,SAAS,MAAM,WAAW,QAAQ;AAAA,IACpC;AAAA,EACF,CAAC;AACH;AAEO,SAAS,MAAM,QAAsB;AAC1C,SAAO,OAAO,QAAkC;AAC9C,QAAI,CAAC,OAAO,GAAI,QAAO;AACvB,YAAQ,MAAM,OAAO,GAAG,GAAG,KAAK,CAAC,YAAY,QAAQ,QAAQ,GAAG;AAAA,EAClE;AACF;AAEO,SAAS,eACd,OACA,UAA8B,CAAC,GACX;AACpB,QAAM,OAAO,MAAM,OAAO,YAAY,KAAK,GAAG,OAAO;AACrD,OAAK,MAAM,MAAM,MAAS;AAC1B,mBAAiB,OAAO,IAAI;AAC5B,SAAO;AACT;",
4
+ "sourcesContent": ["import { attachKernel } from '@ai-ecoverse/slicc-kernel';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { CredentialStore, Provider } from '@earendil-works/pi-ai';\nimport {\n createRegistry,\n type ModelRef,\n type Storage,\n type ToolRegistration,\n} from '@earendil-works/pi-durable';\nimport { CodingTools } from '@earendil-works/pi-durable/tools';\nimport { type Agent, openAgent } from './agent.ts';\nimport { attachChanges, CHANGES_MS } from './changes/index.ts';\nimport {\n codemodeExtension,\n codemodeTool,\n disabledBySettings,\n locateWasm,\n sandboxFactory,\n} from './codemode/index.ts';\nimport { EncryptedCredentialStore } from './credentials.ts';\nimport { attachFreezer } from './freezer/index.ts';\nimport { settingsText, titler } from './freezer/title.ts';\nimport { lateGelatiere } from './gelatiere/index.ts';\nimport { type AgentHost, hostAgent } from './host.ts';\nimport { imageReads } from './images/index.ts';\nimport { createActivity } from './kernel/activity.ts';\nimport type { KernelClient } from './kernel/client.ts';\nimport { HOME, kernelEnvironment, SliccKernelEnv } from './kernel/env.ts';\nimport { processGroups } from './kernel/groups.ts';\nimport { adminOver, type UserClient, type UserClients, userClients } from './kernel/users.ts';\nimport { setupLicks } from './licks/index.ts';\nimport { lickTriage } from './licks/triage.ts';\nimport { setupMcp } from './mcp/index.ts';\nimport { setupMemory } from './memory/index.ts';\nimport { modelsCommand } from './models/index.ts';\nimport { type Transport, transportFetch } from './net.ts';\nimport { sliccPrompt } from './prompt.ts';\nimport { identity } from './scoops/identity.ts';\nimport { type Assets, packageAssets, type ScoopsRuntime, setupScoops } from './scoops/index.ts';\nimport {\n createAgentSettings,\n createSliccModels,\n DEFAULT_MODEL,\n sliccProviders,\n} from './settings.ts';\nimport { RELOAD_MS, setupSkills } from './skills/index.ts';\nimport { setupSprinkles } from './sprinkles/index.ts';\nimport { openOpfsSqliteStorage } from './sqlite.ts';\nimport {\n agentVersion,\n browserConnected,\n commandsOnPath,\n kernelBoot,\n transportName,\n} from './system.ts';\nimport { type KernelPorts, kernelPorts, serveConnections, type WorkerScope } from './worker.ts';\n\nexport interface AgentWorkerScope extends WorkerScope {\n fetch: typeof fetch;\n location: { origin: string };\n}\n\nexport type KernelAttach = (port: MessagePort) => Promise<KernelClient & { transport: Transport }>;\n\nexport interface AgentWorkerOptions {\n model?: ModelRef;\n providers?: Provider[];\n attach?: KernelAttach;\n credentials?: () => Promise<CredentialStore>;\n storage?: () => Promise<Storage>;\n assets?: Assets;\n codemodeWasm?: () => Promise<WebAssembly.Module | undefined>;\n codemodeWorker?: string | URL;\n catalog?: string;\n users?: boolean;\n mcpRedirect?: string;\n}\n\nexport function usersOf(\n admin: MessagePort | undefined,\n wanted: boolean | undefined,\n attach: KernelAttach\n): UserClients | undefined {\n if (!admin) return undefined;\n if (wanted === false) {\n admin.close();\n return undefined;\n }\n return userClients(adminOver(admin), attach as (port: MessagePort) => Promise<UserClient>);\n}\n\nexport const STORE_DIR = '/.slicc';\n\nasync function attached(ports: Promise<KernelPorts>, options: AgentWorkerOptions) {\n const attach: KernelAttach = options.attach ?? attachKernel;\n const { kernel, admin } = await ports;\n const client = await attach(kernel);\n return { client, users: usersOf(admin, options.users, attach) };\n}\n\nfunction privateStore(env: SliccKernelEnv): Promise<unknown> {\n return env.exec(`mkdir -p ${STORE_DIR} && chmod 700 ${STORE_DIR}`, undefined, BACKGROUND_CONTEXT);\n}\n\nasync function codemodeTools(\n client: KernelClient,\n options: AgentWorkerOptions,\n native: typeof fetch,\n extra: Parameters<typeof codemodeTool>[0]['extra']\n) {\n const settings = await client.fs\n .readFile(`${HOME}/.pi/agent/settings.json`)\n .catch(() => undefined);\n if (disabledBySettings(settings === undefined ? undefined : new TextDecoder().decode(settings)))\n return [];\n const sandbox = sandboxFactory(\n options.codemodeWasm ?? (() => locateWasm(native, new URL(import.meta.url))),\n options.codemodeWorker\n );\n return [\n codemodeTool({ declared: CodingTools.tools as readonly ToolRegistration[], sandbox, extra }),\n ];\n}\n\nexport const MCP_CALLBACK = '/oauth/mcp-callback';\n\nasync function start(\n scope: AgentWorkerScope,\n ports: Promise<KernelPorts>,\n options: AgentWorkerOptions\n): Promise<AgentHost> {\n const { client, users } = await attached(ports, options);\n const home = new SliccKernelEnv(client, { cwd: HOME });\n await privateStore(home);\n const native = scope.fetch.bind(scope);\n scope.fetch = transportFetch(client.transport, scope.location.origin, native);\n const facts = {\n version: await agentVersion((url) => native(url)),\n commands: await commandsOnPath(client),\n transport: transportName(client.transport),\n browser: await browserConnected(client),\n };\n const credentials = await (options.credentials ?? (() => EncryptedCredentialStore.open()))();\n const providers = options.providers ?? sliccProviders();\n const models = createSliccModels(credentials, providers);\n const registry = createRegistry();\n registry.install(CodingTools);\n registry.install(sliccPrompt(facts));\n registry.install(\n imageReads(\n (CodingTools.tools as readonly ToolRegistration[]).find(\n (tool) => tool.name === 'read'\n ) as ToolRegistration\n )\n );\n const memory = setupMemory(registry);\n const coding = CodingTools.tools as readonly ToolRegistration[];\n const mcp = setupMcp(registry);\n const codemode = await codemodeTools(client, options, native, mcp.codemode);\n registry.install(codemodeExtension(codemode));\n const licks = setupLicks(registry);\n licks.licks.triage(lickTriage(models));\n const scoops = setupScoops(registry, licks.licks, [...coding, ...codemode, ...memory.tools]);\n const skills = setupSkills(registry, licks.licks);\n const activity = createActivity();\n const groups = processGroups(client);\n const model = options.model ?? DEFAULT_MODEL;\n let opened: Agent | undefined;\n const environment = kernelEnvironment(client, {\n activity,\n groups,\n identify: identity(() => opened?.agents, model),\n });\n const agent = await openAgent({\n models,\n model,\n storage: await (options.storage ?? (() => openOpfsSqliteStorage()))(),\n registry,\n env: environment,\n });\n opened = agent;\n const sources = licks.attach(agent, { env: home, home: HOME, activity });\n await sources.start(BACKGROUND_CONTEXT);\n await sources.boot(\n { version: facts.version, boot: await kernelBoot(client) },\n BACKGROUND_CONTEXT\n );\n const skillsRuntime = await skills.attach(\n { env: home, home: HOME, assets: options.assets ?? packageAssets(), reloadMs: RELOAD_MS },\n BACKGROUND_CONTEXT\n );\n const mcpRuntime = await mcp.attach(\n {\n env: home,\n home: HOME,\n credentials,\n licks: licks.licks,\n fetch: scope.fetch,\n version: facts.version,\n reloadMs: RELOAD_MS,\n codemode: codemode.length > 0,\n cors: client.transport.traits?.crossOrigin === 'cors',\n redirect: options.mcpRedirect ?? new URL(MCP_CALLBACK, scope.location.origin).href,\n },\n BACKGROUND_CONTEXT\n );\n const gelatiere = lateGelatiere();\n const sprinkles = await setupSprinkles().attach(\n {\n intercept: gelatiere.intercept,\n harness: agent.harness,\n agents: agent.agents,\n licks: licks.licks,\n env: home,\n assets: options.assets ?? packageAssets(),\n reloadMs: RELOAD_MS,\n },\n BACKGROUND_CONTEXT\n );\n let roles: ScoopsRuntime | undefined;\n const memoryRuntime = await memory.attach(\n {\n harness: agent.harness,\n agents: agent.agents,\n env: home,\n home: HOME,\n reloadMs: RELOAD_MS,\n roles: async (using) => roles?.roles(using),\n },\n BACKGROUND_CONTEXT\n );\n const freezer = attachFreezer({\n harness: agent.harness,\n agents: agent.agents,\n env: home,\n extract: memory.extract,\n title: titler(models, settingsText(home, HOME)),\n });\n licks.licks.guard(freezer.frozenTarget);\n const settings = await createAgentSettings(models, credentials, providers, { mcp: mcpRuntime });\n const runtime = await scoops.attach(\n {\n harness: agent.harness,\n agents: agent.agents,\n groups,\n env: home,\n home: HOME,\n alive: alive(client),\n reads: skillsRuntime.dirs,\n sprinkle: sprinkles.command,\n memory: memoryRuntime.command,\n gelatiere: gelatiere.command,\n freezer: freezer.command,\n models: modelsCommand({ agents: agent.agents, settings, catalog: models }),\n mcp: mcp.direct,\n ...(options.assets ? { assets: options.assets } : {}),\n },\n BACKGROUND_CONTEXT\n );\n roles = runtime;\n gelatiere.attach({\n agents: agent.agents,\n licks: licks.licks,\n env: home,\n home: HOME,\n scoops: scoops.scoops,\n roles: runtime.roles,\n assets: options.assets ?? packageAssets(),\n fetch: scope.fetch,\n skills: skillsRuntime.skills,\n ...(options.catalog ? { catalog: options.catalog } : {}),\n });\n await memoryRuntime.reload(BACKGROUND_CONTEXT).catch(() => undefined);\n const changes = attachChanges({ agents: agent.agents, env: home, reloadMs: CHANGES_MS });\n return hostAgent(agent, {\n settings,\n licks: { licks: licks.licks, sources },\n scoops: { scoops: scoops.scoops, runtime },\n skills: skillsRuntime,\n sprinkles,\n memory: memoryRuntime,\n freezer,\n changes,\n mcp: mcpRuntime,\n ...(users ? { users } : {}),\n monitor: {\n kernel: client,\n groups,\n ...(users ? { users: () => users.users.list() } : {}),\n secrets: () => mcpRuntime.secrets(),\n },\n });\n}\n\nexport function alive(client: KernelClient) {\n return async (pid: number): Promise<boolean> => {\n if (!client.ps) return true;\n return (await client.ps()).some((process) => process.pid === pid);\n };\n}\n\nexport function runAgentWorker(\n scope: AgentWorkerScope,\n options: AgentWorkerOptions = {}\n): Promise<AgentHost> {\n const host = start(scope, kernelPorts(scope), options);\n host.catch(() => undefined);\n serveConnections(scope, host);\n return host;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -24,7 +24,6 @@ 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
26
  const GLOBAL_DIR = "/usr/share/skills";
27
- const COMPAT_DIR = "/workspace/skills";
28
27
  const RELOAD_MS = 300;
29
28
  function piResources(dir, text, kind) {
30
29
  let manifest;
@@ -110,7 +109,6 @@ function setupSkills(registry, licks) {
110
109
  { dir: `${home}/.pi/agent/skills`, source: "user" },
111
110
  { dir: `${home}/.agents/skills`, source: "user" },
112
111
  { dir: GLOBAL_DIR, source: "global" },
113
- { dir: COMPAT_DIR, source: "compat" },
114
112
  ...(await packaged(env, "skills", using)).map((dir) => ({
115
113
  dir,
116
114
  source: "package"
@@ -145,8 +143,7 @@ ${problem.message}`;
145
143
  { path: `${home}/.pi/agent/skills`, recursive: true },
146
144
  { path: `${home}/.pi/agent/prompts` },
147
145
  { path: `${home}/.agents/skills`, recursive: true },
148
- { path: GLOBAL_DIR, recursive: true },
149
- { path: COMPAT_DIR, recursive: true }
146
+ { path: GLOBAL_DIR, recursive: true }
150
147
  ],
151
148
  schedule,
152
149
  context
@@ -173,7 +170,6 @@ ${problem.message}`;
173
170
  export {
174
171
  BUILTIN_PROMPTS,
175
172
  BUILTIN_SKILLS,
176
- COMPAT_DIR,
177
173
  GLOBAL_DIR,
178
174
  PROMPTS_DIR,
179
175
  RELOAD_MS,
@@ -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 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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",
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 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 ...(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 ],\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' | '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"],
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' | '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
  }
@@ -12,11 +12,13 @@ export interface WatchEntry {
12
12
  target: LickTarget;
13
13
  message: string;
14
14
  debounce: number;
15
+ triage?: string;
15
16
  }
16
17
  export interface WebhookEntry {
17
18
  name: string;
18
19
  target: LickTarget;
19
20
  message: string;
21
+ triage?: string;
20
22
  }
21
23
  export interface ConfigError {
22
24
  file: string;
@@ -2,6 +2,7 @@ import type { Context } from '@earendil-works/chord';
2
2
  import { type Conversation, type Harness } from '@earendil-works/pi-durable';
3
3
  import { type Lick, type LickAction, type LickChannel, type LickSeverity, type LickTarget } from './lick.ts';
4
4
  import { LICK_STATE_KIND, type LickDecision } from './state.ts';
5
+ import { type Triage } from './triage.ts';
5
6
  export interface LickEvent {
6
7
  channel: LickChannel;
7
8
  source: string;
@@ -15,6 +16,7 @@ export interface LickEvent {
15
16
  severity?: LickSeverity;
16
17
  eventId?: string;
17
18
  at?: number;
19
+ triage?: string;
18
20
  }
19
21
  type Pending = {
20
22
  lick: Lick;
@@ -47,6 +49,7 @@ export interface Licks {
47
49
  pending(context: Context): Promise<number>;
48
50
  forget(target: LickTarget, channel: LickChannel, source: string, context: Context): Promise<void>;
49
51
  guard(check: (target: LickTarget) => Blocked | undefined): void;
52
+ triage(judge: Triage): void;
50
53
  }
51
54
  export type Blocked = {
52
55
  id: string;
@@ -0,0 +1,42 @@
1
+ import type { Context } from '@earendil-works/chord';
2
+ import type { Models } from '@earendil-works/pi-ai/models';
3
+ import type { LickEvent } from './licks.ts';
4
+ export declare const TRIAGE_MODEL = "clef-flash";
5
+ export declare const TRIAGE_SKIP = 0.75;
6
+ export declare const TRIAGE_KIND = "slicc.lick-triage";
7
+ export declare const TRIAGE_LIMIT = 4000;
8
+ export interface Verdict {
9
+ decision: 'act' | 'ignore';
10
+ confidence: number;
11
+ probabilities: Record<string, number>;
12
+ model: string;
13
+ cost: number | null;
14
+ }
15
+ export type Triage = (event: LickEvent, context: Context) => Promise<Verdict | undefined>;
16
+ export declare function clip(text: string): string;
17
+ export declare function skips(verdict: Verdict | undefined): verdict is Verdict;
18
+ export declare function lickTriage(models: Pick<Models, 'getModelOfType' | 'classify'>): Triage;
19
+ export declare function skipEntry(event: LickEvent, verdict: Verdict, now: number): {
20
+ type: 'write';
21
+ requestId: string;
22
+ entry: {
23
+ kind: string;
24
+ data: {
25
+ decision: 'act' | 'ignore';
26
+ confidence: number;
27
+ probabilities: Record<string, number>;
28
+ model: string;
29
+ cost: number | null;
30
+ channel: "bash" | "cherry" | "cron" | "discovery" | "fswatch" | "jshd" | "navigate" | "preview" | "scoop-idle" | "scoop-notify" | "scoop-wait" | "session-reload" | "sprinkle" | "sudo-request" | "upgrade" | "webhook" | "workflow";
31
+ source: string;
32
+ title: string;
33
+ text: string;
34
+ body?: string | undefined;
35
+ items?: string[] | undefined;
36
+ lick: string;
37
+ count: number;
38
+ at: number;
39
+ triage: string;
40
+ };
41
+ };
42
+ };
@@ -10,7 +10,6 @@ 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
12
  export declare const GLOBAL_DIR = "/usr/share/skills";
13
- export declare const COMPAT_DIR = "/workspace/skills";
14
13
  export declare const RELOAD_MS = 300;
15
14
  export interface SkillsRuntime {
16
15
  readonly commands: ReplicatedState<Command[]>;
@@ -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' | 'global' | 'compat' | 'package' | 'builtin';
6
+ export type SkillSource = 'user' | 'global' | '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.40.0",
3
+ "version": "3.41.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",
@@ -41,9 +41,8 @@ In this order; when two skills share a name, the first one found wins and the ot
41
41
  1. `~/.pi/agent/skills/`
42
42
  2. `~/.agents/skills/`
43
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)
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)
47
46
 
48
47
  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.
49
48