skillscript-runtime 0.19.12 → 0.19.14

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.
@@ -379,7 +379,7 @@ Closed list of language-intrinsic ops the runtime knows directly. Each is a func
379
379
  | `notify` | `notify(agent="...", message="...", [event_type=...], [correlation_id=...]) -> ACK` | optional | Mid-skill agent alert; synchronous send via configured AgentConnector. |
380
380
  | `inline` | `inline(skill="<data-skill-name>")` | none | Compile-time inline of an Approved `# Type: data` skill. Resolves at compile, records `content_hash` in provenance. |
381
381
  | `execute_skill` | `execute_skill(skill_name="...", inputs={...}) -> R` | optional | Composition primitive. Runtime-resolved. See Composition section. |
382
- | `shell` | `shell(command="...") -> R` / `shell(command="...", unsafe=true) -> R` | optional | Sandboxed shell exec (default) or full-shell exec (`unsafe=true`, gated by `runtime.enable_unsafe_shell`). stdout binds. |
382
+ | `shell` | `shell(command="...") -> R` / `shell(argv=[...]) -> R` / `shell(command="...", unsafe=true) -> R` | optional | Structural spawn (default), explicit-argv spawn (`argv=[...]`, no tokenizer), or full-shell exec (`unsafe=true`, gated by `runtime.enable_unsafe_shell`). Binary gated by the operator allowlist (see below). stdout binds. |
383
383
  | `file_read` | `file_read(path="...") -> R` | required | Read a file at `path`; binds string contents. |
384
384
  | `file_write` | `file_write(path="...", content="...")` | none | Write `content` to `path`. `mkdir -p` semantics for parent directories. Mutation-classified. |
385
385
 
@@ -431,18 +431,25 @@ Synchronous alert to a named agent via wired AgentConnector(s). **Contrast with
431
431
 
432
432
  Returns ACK `{agent, dispatched: [{connector, ok, error?}]}` — fire-and-forget callers ignore the binding; check-delivery callers inspect ACK.
433
433
 
434
- ### `shell` — sandboxed or unsafe shell exec
434
+ ### `shell` — structural, explicit-argv, or unsafe exec
435
435
 
436
- **Sandboxed default:**
436
+ Three forms.
437
+
438
+ **1. `shell(command="...")` — structural spawn (default).** The command string is whitespace-tokenized and quote-stripped, then one binary is spawned with the resulting tokens. No shell, no metacharacters, no pipes/redirects. The tokenizer is **quote-aware**: quotes are respected during the whitespace split, so a literal `'hello world'` stays one token (the surrounding quotes are then stripped). stdout binds; non-zero exit → op-error routed through `else:` / `# OnError:`.
437
439
 
438
440
  ```
439
441
  shell(command="curl -s 'wttr.in/${LOCATION|url}?format=j1'") -> RAW
440
442
  shell(command="git status") -> STATUS
441
443
  ```
442
444
 
443
- Structured-spawn sandbox: one binary per call, args parsed structurally, no shell metacharacter interpretation. The structural constraints ARE the security model. stdout binds; non-zero exit op-error routed through `else:` / `# OnError:`.
445
+ **2. `shell(argv=["bin","arg1","${VAR}",...])` — explicit-argv spawn.** Each list element is exactly one argv token; `${VAR}` substitutes per element and the result is **not re-split**, so an arg containing whitespace, quote characters, JSON, or any dynamic content stays one intact arg. No tokenizer, no quote-matching, no shell strictly safer than `unsafe=true` (injection-surface zero). This is the right form whenever an arg may contain dynamic or whitespace-bearing content. **Mutex:** `argv=` does not compose with `command=` or `unsafe=true` — it's an execv-class spawn, there is no shell to opt into.
446
+
447
+ ```
448
+ shell(argv=["say","-v","${VOICE}","-f","${PATH}"]) -> OUT
449
+ shell(argv=["jq","-c","${FILTER}","/tmp/data.json"]) -> RESULT
450
+ ```
444
451
 
445
- **Unsafe mode:**
452
+ **3. `shell(command="...", unsafe=true)` — full bash.** Required for pipes, redirects, shell built-ins.
446
453
 
447
454
  ```
448
455
  shell(command="for i in $(seq 1 10); do echo $i; done", unsafe=true) -> R
@@ -452,11 +459,26 @@ shell(command="curl -s example.com | jq '.field' > /tmp/out", unsafe=true)
452
459
  - Lint flags every `unsafe=true` call as tier-2.
453
460
  - Runtime refuses with `UnsafeShellDisabledError` unless deployment sets `runtime.enable_unsafe_shell = true` (default `false`). Compile-time `unsafe-shell-disabled` tier-1 catches at authoring.
454
461
  - Audit-visible at every fire.
462
+ - Bash's `$(command)` and arithmetic `$((expr))` pass through to bash without escape because skillscript's substitution is braced (`${VAR}`).
455
463
 
456
- Bash's `$(command)` and arithmetic `$((expr))` pass through to bash without escape because skillscript's substitution is braced (`${VAR}`).
464
+ **TWO security gates, not one — structural shape AND a binary allowlist.** The structural constraint (above) governs *how* a command runs (no shell/metacharacters on the safe path). It is HALF the model. The other half is an **operator-owned binary allowlist (default-deny)** governing *which* binary may run at all:
465
+
466
+ - Every shell op's `argv[0]` (or the first token of `command=`) is checked against the allowlist; a non-allowlisted binary is refused with `ShellBinaryNotAllowedError`, regardless of safe-vs-`unsafe`.
467
+ - **Default is deny-all** — if no allowlist is wired, *every* shell op is refused.
468
+ - Configure via `SKILLSCRIPT_SHELL_ALLOWLIST` env (comma-separated), the `shellAllowlist` field in `skillscript.config.json`, or `bootstrap({ shellAllowlist: [...] })`. The runtime must **restart** to pick up changes. `skillfile shell-audit` scans the corpus and prints the binary union ready to paste.
469
+ - For `unsafe=true`, `bash` itself must be on the allowlist (binary-scope is independent of unsafe-vs-safe).
470
+ - The current allowlist is reported by `runtime_capabilities.shellExecution.allowlist`.
471
+ - **This is an operator boundary the skill author cannot escape** — not via `unsafe`, not via any in-skill mechanism. It scopes *which binary*, not *what the binary does*: allowlisting a powerful authenticated CLI (e.g. `gh`) grants its whole surface, so wrap powerful binaries to least-privilege (a read-only wrapper script on the allowlist instead of the raw binary) before allowlisting.
472
+
473
+ **All three forms honor the universal op-level kwargs below**, including `(fallback:)` — the fallback fires on op throw OR empty stdout.
457
474
 
458
475
  **Pipes need unsafe; sandboxed multi-call + temp file is the unsafe-free alternative.** A pipe (`curl ... | jq ...`) is a shell metacharacter and requires `unsafe=true`. To compute-in-tools without unsafe, split into sequential sandboxed calls sharing a temp file: `shell(command="curl -s -o /tmp/x.json ...")` then `shell(command="jq -c '<filter>' /tmp/x.json") -> R`. Sequential sandboxed calls share the runtime's filesystem within an execution. Caveat: a fixed temp path races under concurrent invocation (no built-in uniquifier short of a `mktemp` call) — for a large fetched intermediate, keeping it in an execution-scoped var + filtering exports via `# Returns:` is usually cleaner than either shell form.
459
476
 
477
+ **The `'${VAR}'` quote trap — works in test, breaks on edge input.** Because the structural tokenizer is quote-aware, `shell(command="say -v Jamie '${TEXT}'")` DOES pass a simple multi-word value as one argument — it works for `TEXT="Perry here now"`. That is the trap: it's fragile on the *content* of the substituted value. If `TEXT` contains a quote character (`Jamie's turn`) the quote-matching drifts and the arg breaks — and it fails silently, only on certain inputs, after the pattern already "worked" in testing. Tier-2 lint `shell-quoted-var-in-command` flags the `'${VAR}'`-in-`command=` pattern for exactly this reason and points at `argv=[...]`. Three safe ways to pass an arg that may contain whitespace or quote characters:
478
+ - **`shell(argv=[...])` (preferred):** explicit-token list (form 2 above) — no tokenizer touches the value; the safest because there is no shell and no quote-matching at all.
479
+ - **File-roundtrip:** `file_write(path="/tmp/x.txt", content="${TEXT}")` then `shell(command="say -f /tmp/x.txt")` — the binary reads the value off disk via its own file-input flag; no tokenizer involved. Good when the binary has a file-input mode.
480
+ - **`unsafe=true` + `${TEXT|shell}`:** bash quoting via the POSIX-escape filter; tier-2 lint flags it; injection surface remains if `TEXT` is untrusted.
481
+
460
482
  ### `file_read` / `file_write` — file I/O
461
483
 
462
484
  ```
@@ -546,7 +568,7 @@ Typed-contract ops (`data_*`, `skill_*`, `json_parse`, `llm`) auto-wire from the
546
568
 
547
569
  ### Universal op-level kwargs
548
570
 
549
- Four surfaces apply to every `$` dispatch regardless of tool. The runtime intercepts these before forwarding the remaining kwargs to the connector.
571
+ Four surfaces apply to every `$` dispatch and to `shell()` (notably `(fallback:)`), regardless of tool. The runtime intercepts these before forwarding the remaining kwargs to the connector.
550
572
 
551
573
  **`timeout=N`** — Per-op timeout in **seconds**. Integer literal or `${VAR}` ref. Resolution chain (most-specific wins):
552
574
 
@@ -566,13 +588,14 @@ $ amp.amp_olsen_task task_type="scan" timeout=120 -> DISPATCH
566
588
  $ data_write content="${REPORT}" tags=["oncall"] approved="morning roundup" -> ACK
567
589
  ```
568
590
 
569
- **`(fallback: "value")` trailer** — Fires on dispatch throw OR empty bound value (empty string after trim, empty array, null/undefined). Coerce-on-bind: the fallback value binds to the output var transparently, downstream targets need no conditional. Permissive value parsing — bare identifiers, quoted strings, bracketed array literals all accepted.
591
+ **`(fallback: "value")` trailer** — Fires on dispatch throw OR empty bound value (empty string after trim, empty array, null/undefined). Honored on `$` dispatch and on `shell()` (fires on shell op throw or empty stdout). Coerce-on-bind: the fallback value binds to the output var transparently, downstream targets need no conditional. Permissive value parsing — bare identifiers, quoted strings, bracketed array literals all accepted. A fired fallback is recorded in `result.fallbacks[]`.
570
592
 
571
593
  Note: an envelope object like `{items: []}` is a non-empty object and does NOT trigger the fallback even though its contained array is empty. To handle envelope-empty downstream, test the contained collection (`if ${R.items|length} == "0":`) or apply a filter (`${R.items|fallback:[]}`).
572
594
 
573
595
  ```
574
596
  $ llm prompt="Classify: ${INPUT}" -> VERDICT (fallback: "unknown")
575
597
  $ amp.amp_query_memories query="${TOPIC}" -> RESULTS (fallback: [])
598
+ shell(argv=["gh","pr","list","--repo","acme/foo"]) -> PRS (fallback: "No open PRs")
576
599
  $ ticketing.search query="..." -> ISSUES (fallback: "search-unavailable")
577
600
  ```
578
601
 
@@ -657,7 +680,7 @@ Tool args are unconstrained `key=value` pairs — the connector forwards them to
657
680
 
658
681
  See the adopter playbook for the substrate config reference + the full Case 2 tradeoff.
659
682
 
660
- **Discovery surface.** `runtime_capabilities` (MCP tool) exposes the registered substrate state. Every entry across all four substrate slots (SkillStore / DataStore / LocalModel / McpConnector) carries its instance `manifest()` payload alongside the static features. Three observable states per entry: working `manifest:{...}`, runtime failure `manifest:null, manifest_error:"..."`, structural absence `manifest:null, manifest_unsupported:true` (AgentConnector only — the contract has no `manifest()` method). The bridge `wraps` convention re-exposes the underlying substrate's full manifest, so adopters reading the discovery surface see the full bound state without traversing multiple entries.
683
+ **Discovery surface.** `runtime_capabilities` (MCP tool) exposes the registered substrate state. Every entry across all four substrate slots (SkillStore / DataStore / LocalModel / McpConnector) carries its instance `manifest()` payload alongside the static features. Three observable states per entry: working `manifest:{...}`, runtime failure `manifest:null, manifest_error:"..."`, structural absence `manifest:null, manifest_unsupported:true` (AgentConnector only — the contract has no `manifest()` method). The bridge `wraps` convention re-exposes the underlying substrate's full manifest, so adopters reading the discovery surface see the full bound state without traversing multiple entries. The `shellExecution` entry reports the operator shell allowlist + mode.
661
684
 
662
685
  Connector entries also surface `features` declarations (`supports_identity_propagation`, `supports_streaming_responses`, `supports_batch`). Capability flags that span multiple layers — `supports_identity_propagation` requires both the connector's ctx-honoring AND the substrate's per-identity scope honoring — gate via `RuntimeCapabilitiesConformance` auto-coverage: declaring a feature flag true requires the adopter to wire both Level-1 (substrate-independent: ctx reaches transport) and Level-2 (substrate-coupled: distinct identities yield distinct observable scopes) probes via `flagProbes`. Missing probes fail the gate before runtime accepts the flag as true. This is the structural close that prevents the discipline-only-contract pattern (capability claim without honoring impl) from recurring at the capability-flag surface.
663
686
 
@@ -667,7 +690,7 @@ Connector entries also surface `features` declarations (`supports_identity_propa
667
690
 
668
691
  ## Per-op gating
669
692
 
670
- Mutation ops require an authorization signal. The signal is per-op, not a mode binary.
693
+ Two independent authorization layers govern a shell op: **(0) the binary allowlist** — can this binary run at all (operator-owned, default-deny; see the shell section above; refusal is `ShellBinaryNotAllowedError`) — and **(1) the mutation gate** below. The allowlist is checked first and the author cannot bypass it. Beyond shell, mutation ops require an authorization signal. The signal is per-op, not a mode binary.
671
694
 
672
695
  **Mutation-classified ops:**
673
696
  - `file_write(...)` (runtime-intrinsic)
@@ -678,7 +701,7 @@ Mutation ops require an authorization signal. The signal is per-op, not a mode b
678
701
 
679
702
  **Read-only ops (always allowed, no authorization needed):**
680
703
  - `file_read`, `emit`, `notify`, `inline`, `execute_skill`
681
- - `shell(command=...)` with read-only verb
704
+ - `shell(command=...)` with read-only verb (still subject to the binary allowlist)
682
705
  - `$ <connector>.<tool>` against tools declared `mutating: false` (or unspecified, default false for query-shaped tools)
683
706
  - `$set`, `$append`
684
707
 
@@ -718,7 +741,7 @@ deliver:
718
741
  | Runtime-intrinsic | `notify` | `notify(agent="...", [message=...], [event_type=...], [correlation_id=...]) -> ACK` | optional |
719
742
  | Runtime-intrinsic | `inline` | `inline(skill="<name>")` | none (compile-time) |
720
743
  | Runtime-intrinsic | `execute_skill` | `execute_skill(skill_name="...", inputs={...}) -> R` | optional |
721
- | Runtime-intrinsic | `shell` | `shell(command="...", [unsafe=true], [approved="..."]) -> R` | optional |
744
+ | Runtime-intrinsic | `shell` | `shell(command="...", [unsafe=true], [approved="..."]) [-> R] [(fallback: "...")]` or `shell(argv=[...], [approved="..."]) [-> R] [(fallback: "...")]` (mutually exclusive forms; binary allowlist applies to both) | optional |
722
745
  | Runtime-intrinsic | `file_read` | `file_read(path="...") -> R` | required |
723
746
  | Runtime-intrinsic | `file_write` | `file_write(path="...", content="...", [approved="..."])` | none |
724
747
  | External MCP — substrate-specific | `$ <connector>.<tool>` | `$ <connector>.<tool> kwarg=value, ... [timeout=N] [approved="..."] [-> R] [(fallback: "...")]` | optional |
@@ -863,7 +886,7 @@ Pipe filters apply transforms to resolved variables before substitution. Syntax:
863
886
  | `trim` | Whitespace trim | `${VERDICT|trim}` for `"urgent\n"` | `urgent` |
864
887
  | `length` | Count of items (array) or characters (string) | `${ITEMS|length}` for `["a","b","c"]` | `3` |
865
888
  | `contains:"X"` | Boolean: type-aware substring / element membership | `${MSG|contains:"urgent"}` for `"Yes, urgent"` | `true` |
866
- | `fallback:"X"` | Coalesce on missing/undefined ref | `${VAR.missing|fallback:"-"}` | `-` |
889
+ | `fallback:"X"` | Coalesce on missing/undefined/empty ref | `${VAR.missing|fallback:"-"}` | `-` |
867
890
  | `isodate` | Epoch seconds → ISO-8601 timestamp | `${EPOCH|isodate}` for `1779660000` | `2026-05-24T22:00:00.000Z` |
868
891
 
869
892
  ### `length` semantics
@@ -915,16 +938,17 @@ Empty-string match: `${VAR|contains:""}` returns `true` if VAR is bound. Documen
915
938
 
916
939
  ### `fallback:"X"` semantics
917
940
 
918
- Coalesce-on-missing. Emits the literal string `X` when the ref resolves to missing/null/undefined. Strict-by-default semantics preserved everywhere else; `|fallback:` is the explicit opt-out at the call site.
941
+ Coalesce-on-empty-or-missing. Emits the literal string `X` when the ref resolves to missing/null/undefined OR to an empty string. Strict-by-default semantics preserved everywhere else; `|fallback:` is the explicit opt-out at the call site.
919
942
 
920
943
  ```
921
944
  emit:
922
- emit(text="present: ${PRESENT|fallback:\"missing\"}") # → "hello" (PRESENT is bound)
923
- emit(text="missing: ${NOT_DECLARED|fallback:\"-\"}") # → "-" (NOT_DECLARED isn't)
945
+ emit(text="present: ${PRESENT|fallback:\"missing\"}") # → "hello" (PRESENT is bound and non-empty)
946
+ emit(text="missing: ${NOT_DECLARED|fallback:\"-\"}") # → "-" (NOT_DECLARED isn't declared)
947
+ emit(text="empty: ${BOUND_EMPTY|fallback:\"-\"}") # → "-" (bound to "")
924
948
  emit(text="nested: ${ISSUE.customFields.Assignee|fallback:\"unassigned\"}")
925
949
  ```
926
950
 
927
- **Why filter-shape, not ref-level `(fallback:)`.** Op-level `(fallback: ...)` exists on `$` dispatch for **error recovery** (dispatch happened, failed). Ref-level `|fallback:` is **coalesce** (lookup found nothing). They rhyme but are adjacent concepts. The filter-chain attachment keeps composition clean (`${VAR|json_parse|fallback:"-"}` works as a chain step) and the vocabulary alignment with op-level `(fallback:)` lets cold authors learn "fallback" as the universal concept while the syntax disambiguates the attachment site.
951
+ **Why filter-shape, not ref-level `(fallback:)`.** Op-level `(fallback: ...)` exists on `$` dispatch and `shell()` for **error/empty recovery** (the op ran, then failed or returned empty). Ref-level `|fallback:` is **coalesce** (the lookup itself found nothing — missing, null, or empty). They rhyme but are adjacent concepts. The filter-chain attachment keeps composition clean (`${VAR|json_parse|fallback:"-"}` works as a chain step) and the vocabulary alignment with op-level `(fallback:)` lets cold authors learn "fallback" as the universal concept while the syntax disambiguates the attachment site.
928
952
 
929
953
  **Closes the missing-field strict-error trap**: `${ISSUE.customFields.Assignee}` against an object without that key threw `UnresolvedVariableError` and aborted whole-render. The filter is the per-ref opt-out.
930
954
 
@@ -1422,6 +1446,15 @@ A skill may declare multiple output targets, one per line; each receives the sam
1422
1446
  # Output: agent: assistant
1423
1447
  ```
1424
1448
 
1449
+ ## Output target resolution
1450
+
1451
+ For the `agent:` and `template:` kinds, the runtime resolves the target agent_id via a **2-level chain** (first match wins):
1452
+
1453
+ 1. **Explicit name** — `# Output: agent: perry` delivers to agent_id `perry`.
1454
+ 2. **`${VAR}` compile-time substitution** — `# Output: agent: ${RECIPIENT}` resolves against the resolved inputs map (`# Vars:` defaults, `# Requires:` cascade, caller-supplied `inputs`) at compile time. Only compile-time inputs resolve here — a runtime-bound ref (an op's `-> VAR` output, an ambient ref) passes through verbatim and fails at delivery if still unresolved.
1455
+
1456
+ There is **no** invocation-context inheritance and **no** runtime `default_agent_id` fallback: a skill must name its target explicitly or pass it as an input var. See the Connectors section (AgentConnector) for the full delivery contract behind the resolved id.
1457
+
1425
1458
  ## Per-kind output value semantics
1426
1459
 
1427
1460
  What each kind consumes as the output, in precedence order:
@@ -1462,6 +1495,290 @@ The receiving agent reads `event_type` for routing ("911 — surface now" vs "ro
1462
1495
 
1463
1496
  If `# Output: agent: <name>` fires and the wired AgentConnector throws, delivery routes through `else:` / `# OnError:`; the failure is recorded on `agent_delivery_receipts[]` for the scheduler to log.
1464
1497
 
1498
+ ## Connectors — substrate routing, the five connector types, agent_id resolution
1499
+
1500
+ The substrate-routing ops (`$ connector.tool`, `$ data_read`, `$ llm`) and the agent-bound `# Output:` kinds (`agent:`, `template:`) don't call any specific backend directly. They route through thin connector interfaces. Skill source persistence follows the same pattern via a dedicated contract. This is the programmable surface through which authors compose information topology per skill and per moment. Skills are portable across substrates because the language doesn't bake substrate identity into the source.
1501
+
1502
+ ## Five connector types
1503
+
1504
+ ### MemoryStore
1505
+
1506
+ Routes `$ data_read` retrieval ops. Interface: `MemoryStore.query(filters) → PortableMemory[]`.
1507
+
1508
+ Implementations vary by deployment — a knowledge-substrate-backed store, a SQLite-backed store, a vector-DB-backed store, an in-memory test store. All conform to the `MemoryStore.query` contract and return `PortableMemory[]`.
1509
+
1510
+ ### LocalModel
1511
+
1512
+ Routes `$ llm` local-model ops. Interface: `LocalModel.run(prompt, opts) → string`.
1513
+
1514
+ Default impl wraps a local-model HTTP service (e.g., Ollama). Constructor takes `{ model: string }` (required) — no class-level implicit default. Multiple instances by name in the registry; each backed by a distinct model tag.
1515
+
1516
+ ### McpConnector
1517
+
1518
+ Routes `$ connector.tool` MCP-tool ops. Interface: `McpConnector.call(toolName, args, ctxOverrides?) → unknown`.
1519
+
1520
+ Implementations include adapters wrapping in-process tool dispatch (when the runtime is embedded in a host that already has MCP tools) and HTTP-based MCP clients (when calling out to remote MCP servers). All conform to the `McpConnector.call` contract.
1521
+
1522
+ **Bare-name dispatch.** `$ TOOL` (no connector prefix) routes through the `primary` McpConnector entry in the registry. If no `primary` is wired, the runtime throws `ConnectorNotFoundError` at runtime AND fires tier-1 `unwired-primary-connector` lint at compile time. The remediation diagnostic includes both fix paths: add `primary` to connectors.json, or qualify the op as `$ named.TOOL`. Failing loud is the correct mode for autonomous-pattern skills — a silent stub would let an autonomous skill appear to succeed while doing nothing.
1523
+
1524
+ ### AgentConnector
1525
+
1526
+ Routes the agent-bound `# Output:` kinds — `agent:` (Augmenting) and `template:` (Template) per the skill-kind taxonomy in Section 1. Interface:
1527
+
1528
+ ```typescript
1529
+ interface AgentConnector {
1530
+ list_agents(): Promise<AgentDescriptor[]>;
1531
+ deliver(agent_id: string, payload: DeliveryPayload): Promise<DeliveryReceipt>;
1532
+ wake(agent_id: string, opts?: WakeOpts): Promise<WakeReceipt>;
1533
+ agent_status?(agent_id: string): Promise<AgentStatus>;
1534
+ }
1535
+
1536
+ type DeliveryPayload =
1537
+ | { kind: "augment"; content: string; format?: "text" | "markdown"; source_skill?: string; triggered_by?: TriggerProvenance; delivery_context?: string; templates?: string[] }
1538
+ | { kind: "template"; prompt: string; source_skill?: string; triggered_by?: TriggerProvenance; delivery_context?: string; templates?: string[] };
1539
+
1540
+ type TriggerProvenance = {
1541
+ source: "cron" | "session" | "event" | "agent-event" | "file-watch" | "sensor" | "manual";
1542
+ name: string; // e.g. "0 8 * * *", "session:start", "manual"
1543
+ fired_at_ms: number; // unix ms timestamp
1544
+ };
1545
+
1546
+ type DeliveryReceipt = { delivered_at: number; delivery_id?: string };
1547
+
1548
+ type WakeOpts = {
1549
+ context?: string;
1550
+ when?: "immediate" | number;
1551
+ };
1552
+
1553
+ type WakeReceipt = { woken_at: number; session_id?: string };
1554
+
1555
+ type AgentDescriptor = {
1556
+ agent_id: string;
1557
+ agent_name?: string;
1558
+ capabilities?: ("deliver" | "wake" | "augment" | "template")[];
1559
+ };
1560
+
1561
+ type AgentStatus = "active" | "idle" | "asleep" | "unknown";
1562
+ ```
1563
+
1564
+ The `agent:` Output kind produces a `DeliveryPayload` of kind `augment`; `template:` produces one of kind `template`.
1565
+
1566
+ Two primary verbs (`deliver` + `wake`), one mandatory discovery method (`list_agents`), one optional status method. The contract is substrate-neutral; adopters wire any delivery mechanism behind it:
1567
+
1568
+ | Substrate | `deliver` impl | `wake` impl |
1569
+ |---|---|---|
1570
+ | tmux session | `tmux send-keys` to a pane | `tmux send-keys` with wake prompt |
1571
+ | webhook | POST to `/augment` or `/template` endpoint | POST to `/wake` endpoint |
1572
+ | memory store | write a memory record with delivery tag | write addressed memory + push notification |
1573
+ | file-watch | write to `<path>/augment-<id>.txt` | write to `<path>/wake-<id>.txt` |
1574
+ | chat thread | post to monitored thread | post + @mention |
1575
+ | IPC named pipe | write to delivery pipe | write to wake pipe |
1576
+
1577
+ Default impl `NoOpAgentConnector` logs warnings and resolves; lets the runtime ship without an agent-delivery substrate wired. Adopter impls run the bundled `AgentConnectorConformance` suite to verify their substrate wiring.
1578
+
1579
+ #### DeliveryPayload provenance fields
1580
+
1581
+ Every `deliver` call carries optional provenance the receiving agent uses to disambiguate the source and context of the delivery:
1582
+
1583
+ - `source_skill?: string` — name of the skill that produced this delivery. Lets the receiver attribute the content to a specific authored skill, distinguishing "this is from the stock-monitor skill" from "this is from the news-brief skill."
1584
+ - `triggered_by?: TriggerProvenance` — why the skill fired. Receiver disambiguates cron tick (autonomous), session-start (lifecycle), event-driven (external signal), manual (user-requested), etc. Carries the trigger source + name + unix-ms timestamp.
1585
+ - `delivery_context?: string` — prose explanation of why the agent is being notified and what to do with the content. Populated from the `# Delivery-context:` header (see Section 7).
1586
+ - `templates?: string[]` — list of Template-kind skill names the receiver can fetch as follow-on actions. Populated from the `# Templates:` header (see Section 7).
1587
+
1588
+ The runtime threads these from `ExecuteContext.triggerCtx` (set at dispatch) and `ParsedSkill.deliveryContext` / `ParsedSkill.templates` (set at parse) through to the `deliver` call site.
1589
+
1590
+ #### `agent_id` resolution
1591
+
1592
+ When `# Output: agent:` or `# Output: template:` fires, the runtime resolves the target agent_id via a 2-level chain (first match wins):
1593
+
1594
+ 1. **Explicit name in the `# Output:` line** — `# Output: agent: perry` dispatches to agent_id `perry`.
1595
+ 2. **`${VAR}` compile-time substitution** — `# Output: agent: ${RECIPIENT}` resolves against the resolved inputs map (`# Vars:` defaults + `# Requires:` cascade + caller-supplied `inputs`) at compile time. Caveat: only compile-time inputs resolve here — runtime-bound refs (a target's output var, ambient refs) pass through verbatim and fail at delivery if still unresolved.
1596
+
1597
+ Invocation-context inheritance and a runtime-config `default_agent_id` fallback are NOT implemented: `# Output: agent:` does not auto-inherit the caller's identity, and there is no default-agent fallback. A skill must name its target explicitly or pass it as an input var.
1598
+
1599
+ #### Output-kind classification in the runtime
1600
+
1601
+ The runtime's `TEXT_COERCED_OUTPUT_KINDS` set classifies output kinds by payload shape (text vs structured), not by semantic destination. Membership controls payload coercion; it doesn't bake destination identity into the runtime.
1602
+
1603
+ ### SkillStore
1604
+
1605
+ Routes skill source persistence. Interface:
1606
+
1607
+ ```typescript
1608
+ interface SkillStore {
1609
+ get(name: string): Promise<SkillRecord | null>;
1610
+ write(name: string, body: string): Promise<void>;
1611
+ list(): Promise<SkillDescriptor[]>;
1612
+ delete(name: string): Promise<void>;
1613
+ }
1614
+ ```
1615
+
1616
+ Bundled impls: `FilesystemSkillStore` reads and writes `.skill.md` source plus `.skill` compiled output and `.skill.provenance.json` sidecar in a configured directory; the standard for file-backed deployments. Substrate-specific impls live in adopter packages (memory-backed stores live in the substrate's adapter repo).
1617
+
1618
+ Skill records are infrastructure, not knowledge atoms — adopter impls should treat skills as first-class long-lived records, not as candidates for substrate-level garbage collection.
1619
+
1620
+ ## Capabilities discovery
1621
+
1622
+ All connector types expose `capabilities()` for runtime discovery. Consumers:
1623
+ 1. `# Requires:` matching against the registered set
1624
+ 2. Dynamic queries via `listMemoryStores()` / `listLocalModels()` / `listMcpConnectors()` / `listAgentConnectors()` to pick a connector for the moment
1625
+ 3. Authoring tools that surface the registered set
1626
+
1627
+ ## Multi-instance by design
1628
+
1629
+ Multiple instances of the same connector type are the *normal case*, not the exception.
1630
+
1631
+ ```
1632
+ {
1633
+ primary: MemoryStoreImplA,
1634
+ project: SqliteProjectStore,
1635
+ scratch: InMemoryStore
1636
+ }
1637
+ ```
1638
+
1639
+ ```
1640
+ {
1641
+ default: OllamaLocalModel({model: "gemma2:9b"}),
1642
+ gemma2: OllamaLocalModel({model: "gemma2:9b"}),
1643
+ qwen: OllamaLocalModel({model: "qwen2.5:7b"})
1644
+ }
1645
+ ```
1646
+
1647
+ ```
1648
+ {
1649
+ primary: PrimaryMcpConnector,
1650
+ personal: HttpMcpConnector,
1651
+ project: HttpMcpConnector
1652
+ }
1653
+ ```
1654
+
1655
+ Per-skill resolution against named connectors is first-class; an unnamed lookup returns the configured default. Multiple keys pointing at the same underlying instance configuration are allowed and useful — see the `default`/`gemma2` alias below.
1656
+
1657
+ ## Model selection — choosing among LocalModel instances
1658
+
1659
+ The LocalModel registry holds multiple instances by design. Skill authors choose which to dispatch to via `$ llm model="<name>"`. Two layers of indirection are involved, and the distinction matters for both authoring and adopter configuration:
1660
+
1661
+ 1. **Skillscript name → registered instance.** `$ llm model="qwen"` references the instance keyed `qwen` in the registry. The registry resolves to the configured connector implementation.
1662
+ 2. **Registered instance → underlying model.** Each `OllamaLocalModel` is constructed with the actual model tag (e.g. `qwen2.5:7b`). The skill never sees the tag directly.
1663
+
1664
+ ### Example instance names
1665
+
1666
+ | Name | Underlying model | Notes |
1667
+ | --- | --- | --- |
1668
+ | `default` | `gemma2:9b` | Resolved when `model=` is omitted; alias of `gemma2` |
1669
+ | `gemma2` | `gemma2:9b` | Explicit name; matches the convention below |
1670
+ | `qwen` | `qwen2.5:7b` | Interactive, latency-sensitive |
1671
+
1672
+ `default` and `gemma2` can point at the same `OllamaLocalModel` configuration. The alias exists so skill syntax can match a tier convention ("use gemma2 for batch") rather than the back-compat name (`default`). Skills that write `model="default"` work unchanged; prefer the explicit name.
1673
+
1674
+ ### Convention: model tier by use case
1675
+
1676
+ - **Small classification-class model** (e.g., `gemma2`) for *batch and scan work* — atomization, large-batch classification, anything async or background-scheduled.
1677
+ - **Longer-context dispatch-class model** (e.g., `qwen`) for *interactive verdicts in skills* — single-shot decisions inside an active dispatch where latency matters and queue contention with batch work would block forward progress.
1678
+
1679
+ When in doubt: small model if the call is asynchronous from a user/agent's perspective, larger model if a downstream op depends on the response.
1680
+
1681
+ ### Contention property
1682
+
1683
+ Any skill that calls `$ llm` shares the underlying local-model service with every other process on the deployment that dispatches to the same model. Most local-model services serialize per-model dispatch. A skill that fires asynchronous batch work via `$` (e.g. invoking a batch-classification tool that dispatches N calls to model X) and then immediately calls `$ llm model="X"` will race itself — the synchronous call queues behind the dispatched batch.
1684
+
1685
+ The runtime does not promise concurrency-safe model dispatch. Skill authors and operators own model-tier allocation. The canonical mitigation: use distinct models for the synchronous and asynchronous paths (a smaller model for interactive verdicts, a larger model for batch).
1686
+
1687
+ ### Adopter deployments
1688
+
1689
+ Adopters override the bundled set via `connectors.json`:
1690
+
1691
+ ```jsonc
1692
+ {
1693
+ "localModels": {
1694
+ "default": { "type": "OllamaLocalModel", "model": "llama3.2:3b" },
1695
+ "fast": { "type": "OllamaLocalModel", "model": "phi3:mini" }
1696
+ }
1697
+ }
1698
+ ```
1699
+
1700
+ Adopters with no local models register no LocalModel instances. Skills with `$ llm` ops fail at dispatch with `LocalModel '<name>' not registered`. A `# Requires:` capability declaration promotes this to a compile-time fail-fast — a skill that requires LocalModel won't compile if none is configured. Substrate-blind skills (no `$ llm` ops) work unchanged.
1701
+
1702
+ ## Configuration: substrate selection vs operator config
1703
+
1704
+ Two distinct concerns, often conflated. Keep them separate.
1705
+
1706
+ **Substrate-class selection** — *which* connector implementation fills each slot. Configured via `connectors.json` `substrate.<slot>` entries, or programmatically via `bootstrap()` options. This is what makes a skill portable: the slot is named in config, never in the skill source. Connectors are runtime-resolved — the compiler stays pure read+transform, and compiled artifacts are generic, so any runtime can dispatch them through whatever substrates it has wired.
1707
+
1708
+ **Operator-runtime config** — the per-knob runtime settings: `SKILLSCRIPT_*` env vars, `skillscript.config.json`, and `bootstrap()` options. These tune runtime behavior; they do not select substrate classes.
1709
+
1710
+ Precedence (first wins): explicit `bootstrap()` option > env var > config file > bundled default.
1711
+
1712
+ Per-deployment naming lives in config, not the contract. A given deployment registers concrete instances under whatever names make sense locally; skill authors reference those names.
1713
+
1714
+ The `connectors.json` loader handles literal + `${ENV_VAR}` credential resolution, file-discovery via `$SKILLSCRIPT_HOME`, and a class registry recognizing the bundled connector classes (including `CallbackMcpConnector` and `RemoteMcpConnector`). The per-connector `allowed_tools` field constrains dispatch surface for safe defaults — a cold-author skill against the connector can only invoke allowlisted tools. See ERD §3 + §4 for the full credential-discipline contract.
1715
+
1716
+ ## Per-call identity overrides (McpConnector)
1717
+
1718
+ A skill running under one identity can dispatch against a personal MCP server under a different identity without needing connector-internal state. The merge order at dispatch (top wins):
1719
+
1720
+ 1. **Registry-configured per-connector identity** — set in `connectors.json` (`identity: { agentId: "<id>", isAdmin: false }`) at connector instantiation. Locks an identity to a connector.
1721
+ 2. **Per-call `ctxOverrides`** — threaded by the runtime per the security boundary contract. A skill running as agent X passes `{ agentId: "X", isAdmin: false }` into every `$` op.
1722
+ 3. **(no intrinsic identity)** — adapter forwards whatever the merge produces.
1723
+
1724
+ Configured identity is a *partial merge* — unmentioned keys (e.g., `isAdmin`) flow through from the per-call ctx. Lets a connector lock `agentId` without clobbering the runtime's admin-drop discipline. Default connectors should configure no intrinsic identity, so `ctxOverrides` always wins — preserving the runtime's authority-flow guarantees intact.
1725
+
1726
+ ## Portable shapes
1727
+
1728
+ ```typescript
1729
+ interface PortableMemory {
1730
+ // Core fields — mandatory on every connector return.
1731
+ id: string;
1732
+ summary: string;
1733
+ detail?: string;
1734
+ score?: number;
1735
+
1736
+ // Curated substrate subset — concept-portable, value-substrate-specific.
1737
+ // Top-level access via $(MEMORY.field). Connectors populate when the
1738
+ // concept applies. MUST NOT also be duplicated into metadata.
1739
+ thread_status?: string;
1740
+ pinned?: boolean;
1741
+ confidence?: number;
1742
+ domain_tags?: string[];
1743
+ payload_type?: string;
1744
+ knowledge_type?: string;
1745
+ recipients?: string[];
1746
+ expires_at?: number;
1747
+ created_at?: number;
1748
+ agent_id?: string;
1749
+ vault?: string;
1750
+
1751
+ // Substrate-specific bag. Accessed via $(MEMORY.metadata.X).
1752
+ metadata?: Record<string, unknown>;
1753
+ }
1754
+
1755
+ interface QueryFilters {
1756
+ query: string;
1757
+ limit: number;
1758
+ mode: "fts" | "semantic" | "rerank" | string;
1759
+ [key: string]: unknown;
1760
+ }
1761
+
1762
+ interface McpDispatchCtx {
1763
+ agentId?: string;
1764
+ isAdmin?: boolean;
1765
+ }
1766
+ ```
1767
+
1768
+ ## Field access semantics
1769
+
1770
+ `$(MEMORY.field)` resolves in tiers:
1771
+ 1. Core fields (id, summary, detail, score)
1772
+ 2. Curated substrate subset (thread_status, pinned, etc.)
1773
+ 3. `metadata.X` for everything else
1774
+ 4. Ambient passthrough as literal `$(MEMORY.field)` if unresolved
1775
+
1776
+ **Connector duplication is a contract violation.** If a field is in the curated subset, the connector populates it at top-level only — `metadata.<same_name>` MUST be absent. Otherwise `$(M.thread_status)` and `$(M.metadata.thread_status)` can return different values (silent data divergence). Connectors enforce.
1777
+
1778
+ ## Why connector abstraction matters
1779
+
1780
+ Hard-coupling skills to specific substrates would make information-flow decisions infrastructural rather than skill-authored, defeating the point of skills as the agent's programming language. The connector layer is what lets the same skill body run against substrate A today and run against substrate B tomorrow without rewriting.
1781
+
1465
1782
  ## Lifecycle and status — # Status: header, six canonical states, compile + runtime enforcement
1466
1783
 
1467
1784
  Skillscripts carry an explicit lifecycle state via the `# Status:` header. The compiler and runtime enforce status — a Disabled skillscript cannot fire under any path, regardless of who invokes it.
@@ -1595,16 +1912,17 @@ Nested `# OnError:` is *not* supported. If `# OnError: degraded-skill` fires and
1595
1912
 
1596
1913
  ## Layer 3: Op-level fallback values
1597
1914
 
1598
- Inline fallback declared on the op line. Used when the call fails or returns empty. Supported on `$` (MCP dispatch) ops with coerce-on-bind semantics.
1915
+ Inline fallback declared on the op line. Used when the call fails or returns empty. Supported on `$` (MCP dispatch) ops and on `shell()` with coerce-on-bind semantics. For `shell()` the fallback fires on op throw OR empty stdout (e.g. `gh pr list` against a repo with no open PRs binds the fallback string).
1599
1916
 
1600
1917
  ```
1601
1918
  weather:
1602
1919
  $ data_read mode=fts query="weather ${LOCATION}" limit=1 -> CURRENT (fallback: "weather unavailable")
1603
1920
  $ llm prompt="Summarize: ${CURRENT}" -> SUMMARY (fallback: "summary unavailable")
1604
1921
  $ slack.post channel=${CHANNEL} text=${SUMMARY} (fallback: "post failed silently") -> ACK
1922
+ shell(argv=["gh","pr","list","--repo","acme/foo"]) -> PRS (fallback: "No open PRs")
1605
1923
  ```
1606
1924
 
1607
- Same pattern as the `# Requires:` cascade's `(fallback: ...)` syntax — consistent across compile-time (`# Requires:`) and runtime (`$` dispatch).
1925
+ Same pattern as the `# Requires:` cascade's `(fallback: ...)` syntax — consistent across compile-time (`# Requires:`) and runtime (`$` dispatch and `shell()`).
1608
1926
 
1609
1927
  **Fallback value parsing.** Permissive: bare identifiers, quoted strings, and bracketed array literals all accepted. Matches the `# Requires:` cascade convention.
1610
1928
 
@@ -1632,7 +1950,7 @@ Open spec question: should `${ERROR}` be ambient inside `else:` blocks (same sha
1632
1950
 
1633
1951
  Same idea at every scope:
1634
1952
  - Compile-time: `# Requires: ... (fallback: value)`
1635
- - Runtime op: `$ dispatch ... (fallback: value)`
1953
+ - Runtime op: `$ dispatch ... (fallback: value)` and `shell(...) ... (fallback: value)`
1636
1954
  - Runtime target: `else:` block
1637
1955
  - Whole skill: `# OnError:` header
1638
1956
 
@@ -1640,7 +1958,7 @@ Authors composing complex skills use these in combination — op-level for trans
1640
1958
 
1641
1959
  ## Connection to runtime observability
1642
1960
 
1643
- Per-op error contract is what makes cascading fallbacks work. When `$` returns `isError: true`, the executor throws via `makeOpError` rather than binding the error text to the output var. The throw routes through `else:` / `# OnError:` machinery and surfaces in `result.errors[]` for the scheduler to log. Without this discipline, op-level failures wouldn't propagate to the fallback layers and silent-fail would be the default.
1961
+ Per-op error contract is what makes cascading fallbacks work. When `$` returns `isError: true`, the executor throws via `makeOpError` rather than binding the error text to the output var. The throw routes through `else:` / `# OnError:` machinery and surfaces in `result.errors[]` for the scheduler to log. Without this discipline, op-level failures wouldn't propagate to the fallback layers and silent-fail would be the default. Op-level `(fallback:)` is distinct: it intercepts the throw/empty-result at the op and binds the fallback value rather than propagating — a fired fallback is recorded in `result.fallbacks[]`, not `result.errors[]`.
1644
1962
 
1645
1963
  ## Composition — skills calling skills
1646
1964
 
@@ -2208,5 +2526,5 @@ Hung dispatches hang the skill without explicit timeout configuration. Lean: ski
2208
2526
 
2209
2527
  ---
2210
2528
 
2211
- *Rendered from `skillscript/skillscript-language-reference` — 2026-06-08 17:41 EDT*
2212
- *Source of truth: AMP (`amp_render_document("skillscript/skillscript-language-reference")`)*
2529
+ *Rendered from `skillscript/skillscript-language-reference` — 2026-06-15 18:44 EDT*
2530
+ *Source of truth: AMP (`amp_render_document("skillscript/skillscript-language-reference")`)*
@@ -1,5 +1,5 @@
1
1
  # Skill: classify-support-ticket
2
- # Status: Approved v1:670a94cf
2
+ # Status: Approved v1:ae27d3f9
3
3
  # Autonomous: true
4
4
  # Description: Read an incoming support ticket and route it: severity-1 tickets get paged to ops-channel, billing tickets get tagged for finance review, everything else gets a draft reply queued for human review
5
5
  # Vars: TICKET_TEXT, TICKET_ID
@@ -14,13 +14,13 @@ severity_check:
14
14
 
15
15
  route:
16
16
  needs: severity_check
17
- if ${CATEGORY|trim} == "sev-1":
17
+ if ${CATEGORY|contains:"sev-1"}:
18
18
  emit(text="PAGE: sev-1 ticket ${TICKET_ID} - ${TICKET_TEXT}")
19
19
  $ data_write content="sev-1 ticket ${TICKET_ID}: ${TICKET_TEXT}" tags=["support","sev-1","page"]
20
- elif ${IS_SEV1|trim} == "yes":
20
+ elif ${IS_SEV1|contains:"yes"}:
21
21
  emit(text="PAGE: classifier said ${CATEGORY|trim} but severity-check flagged this as sev-1: ${TICKET_ID}")
22
22
  $ data_write content="sev-1 escalation ${TICKET_ID}: category=${CATEGORY|trim} but severity-check=yes" tags=["support","sev-1","disagreement"]
23
- elif ${CATEGORY|trim} == "billing":
23
+ elif ${CATEGORY|contains:"billing"}:
24
24
  $set TAG_FOR = "finance"
25
25
  emit(text="Tagged for ${TAG_FOR} review: ${TICKET_ID}")
26
26
  $ data_write content="billing ticket ${TICKET_ID}: ${TICKET_TEXT}" tags=["support","billing"]
@@ -1,29 +1,18 @@
1
1
  # Skill: feedback-sentiment-scan
2
- # Status: Approved v1:4484f7e0
3
- # Autonomous: true
4
- # Description: Each night, scan the previous 24h of customer feedback records, classify sentiment via local model, surface entries where sentiment is "frustrated" or "blocking" so the team sees them at start-of-day; skip entries already seen on prior nights
2
+ # Status: Approved v1:2b4868c1
3
+ # Description: Each night, scan the previous 24h of customer feedback records, classify sentiment via the local model, and surface entries that read 'frustrated' or 'blocking' so the team sees them at start-of-day.
5
4
  # Triggers: cron: 0 3 * * *
6
5
  # Vars: SCAN_LIMIT=50
7
6
  # Output: agent: support-lead
8
7
 
9
- fetch_new:
8
+ scan:
10
9
  $ data_read mode=fts query="customer feedback" limit=${SCAN_LIMIT} -> FEEDBACK
11
-
12
- fetch_seen:
13
- $ data_read mode=fts query="sentiment-scan seen marker" limit=200 domain_tags=["sentiment-scan-seen"] -> SEEN_MARKERS
14
-
15
- classify_and_emit:
16
- needs: fetch_new, fetch_seen
17
10
  emit(text="Sentiment scan results for ${NOW}:")
18
11
  foreach F in ${FEEDBACK.items}:
19
- if ${F.id|trim} in ${SEEN_MARKERS.items}:
20
- emit(text="- skipped (already classified): ${F.id|trim}")
21
- elif ${F.id|trim} not in ${SEEN_MARKERS.items}:
22
- $ llm prompt="Classify the sentiment of this customer feedback. Respond with ONE word: 'frustrated', 'blocking', 'satisfied', 'neutral'. No explanation.\n\nFeedback: ${F.summary}\nDetail: ${F.detail}" maxTokens=10 -> VERDICT
23
- if ${VERDICT|trim} == "frustrated":
24
- emit(text="- FRUSTRATED [${F.id|trim}] ${F.summary}")
25
- elif ${VERDICT|trim} == "blocking":
26
- emit(text="- BLOCKING [${F.id|trim}] ${F.summary}")
27
- $ data_write content="sentiment-scan seen ${F.id|trim} verdict=${VERDICT|trim} on ${EVENT.fired_at_unix}" tags=["sentiment-scan-seen"] expires_at=${EVENT.fired_at_plus_7d_unix} -> ACK
12
+ $ llm prompt="Classify the sentiment of this customer feedback. Respond with ONE word: 'frustrated', 'blocking', 'satisfied', 'neutral'. No explanation.\n\nFeedback: ${F.summary}\nDetail: ${F.detail}" maxTokens=10 -> VERDICT
13
+ if ${VERDICT|contains:"frustrated"}:
14
+ emit(text="- FRUSTRATED [${F.id|trim}] ${F.summary}")
15
+ elif ${VERDICT|contains:"blocking"}:
16
+ emit(text="- BLOCKING [${F.id|trim}] ${F.summary}")
28
17
 
29
- default: classify_and_emit
18
+ default: scan
@@ -2,7 +2,7 @@
2
2
  "provenance_version": "1.0",
3
3
  "language_version": "1.0",
4
4
  "compiler_version": "0.1.0-dev",
5
- "compiled_at": "2026-06-14T23:43:07.465Z",
5
+ "compiled_at": "2026-06-15T23:23:33.053Z",
6
6
  "source_skill": {
7
7
  "name": "hello-world"
8
8
  },
@@ -1,6 +1,6 @@
1
1
  # Skill: morning-brief
2
- # Status: Approved v1:b79223f1
3
- # Description: Compose a daily morning brief from calendar, mailbox, and overnight data writes when the cron trigger fires at 7am. Delivers via the agent: lifecycle hook to the receiving agent, who decides whether to surface to Slack / Discord / etc. The `model=qwen` value below is a representative alias — adopters register a LocalModel under whatever name fits their setup (the bundled bootstrap registers one as `default`).
2
+ # Status: Approved v1:1530981d
3
+ # Description: Compose a daily morning brief from calendar, mailbox, and overnight notes when the cron trigger fires at 7am. Delivers via the agent: lifecycle hook to the receiving agent, who decides whether to surface to Slack / Discord / etc. Requires a `calendar` connector configured in connectors.json (the dotted `$ calendar.*` form). `model=qwen` is a representative LocalModel alias — adopters register one under whatever name fits (the bundled bootstrap registers `default`).
4
4
  # Vars: AGENT, BRIEF_HORIZON_HOURS=24
5
5
  # Triggers: cron: 0 7 * * *
6
6
  # OnError: morning-brief-degraded
@@ -12,10 +12,10 @@ calendar:
12
12
  $ calendar.list_events horizon_hours=${BRIEF_HORIZON_HOURS} -> EVENTS
13
13
 
14
14
  mailbox:
15
- $ data_read mode=fts query="addressed:${AGENT} created_after:${EVENT.fired_at_unix}" limit=10 -> MAIL
15
+ $ data_read mode=fts query="messages for ${AGENT}" limit=10 -> MAIL
16
16
 
17
17
  overnight:
18
- $ data_read mode=rerank query="overnight writes since:${EVENT.fired_at_plus_1d_unix}" limit=15 -> NOTES
18
+ $ data_read mode=rerank query="overnight notes and writes" limit=15 -> NOTES
19
19
 
20
20
  compose: needs: calendar, mailbox, overnight
21
21
  $ llm prompt="Compose a concise morning brief. Calendar: ${EVENTS|json}. Mailbox: ${MAIL|json}. Overnight notes: ${NOTES|json}. Three sections, six bullets max each." model=qwen maxTokens=1200 -> BRIEF
@@ -1,6 +1,6 @@
1
1
  # Skill: queue-length-monitor
2
- # Description: Count pending items in a queue and alert when the count exceeds threshold
3
- # Status: Approved v1:1ad57c87
2
+ # Status: Approved v1:c6bc06df
3
+ # Description: Count pending items in a queue and alert when the count exceeds threshold. Requires `cat` on the operator's shell allowlist (default-deny: a non-allowlisted binary is refused).
4
4
  # Vars: QUEUE_PATH=/var/queue/pending.json, THRESHOLD=10
5
5
  # Triggers: cron: */5 * * * *
6
6