skillscript-runtime 0.27.2 → 0.27.3

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/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  Each release carries an **Upgrade impact:** line (first in its section) so a bump's requirements are visible at a glance. Tags (closed set): **BREAKING** (a manual change is needed to keep working) · **RE-APPROVE** (secured-mode signature invalidation — skills must be re-approved before they run) · **CONFIG** (`connectors.json` / config edit needed) · **none (additive)** (no action; backward-compatible). Standard from 0.20.0 forward; the pre-0.20 transitions that need action are flagged inline below (0.14.0, 0.18.8, 0.19.0). Full walkthrough: [UPGRADING.md](UPGRADING.md).
4
4
 
5
+ ## 0.27.3 — 2026-07-10 — dashboard highlighter fix + adopter-facing docs audit
6
+
7
+ **Upgrade impact:** none (additive). One dashboard rendering fix + docs; no runtime behavior change.
8
+
9
+ - **Dashboard: fixed the skill-source security highlighter leaking character offsets into the rendered body.** The approval-review source viewer was showing stray numbers glued to mutation ops and `approved=` kwargs (e.g. `$ data_write2861 … approved="…"3021`). The highlight pass's single-capture-group patterns were treating `String.replace`'s third callback argument — the match *offset*, a number — as a capture group, and rendering it. Fixed (`typeof p2 === "string"` guard); added a regression test that fails on the leak. The stored skill source was never affected — this was render-only.
10
+ - **Adopter-facing docs audit** (`adopter-agent-guide.md`, `adopter-playbook.md`, `connector-contract-reference.md`). Pre-announcement pass for accuracy + no internal leakage:
11
+ - Removed internal-process references (thread ids, internal-agent names, dev-cadence/roadmap phrasing, an internal substrate name used as an example) and stripped version-provenance citations, keeping only the genuine `v1.0` *contract-version* mentions.
12
+ - **Accuracy:** the `SkillStore` contract block + ship-status table were missing `manifest()` (claimed 8 methods, listed 7) — corrected, and `DataStore` made consistent; the `AgentConnector` `WakeReceipt` interface block was missing `warnings?: string[]` — added; the agent-guide MCP tool reference completed from 13 to the full **17** tools.
13
+ - **`language-reference.md` re-rendered from atoms** — the `# OnError:` deprecation section now states removal is planned for a post-launch release (after 0.30), replacing the earlier reference to a version that will be skipped.
14
+
5
15
  ## 0.27.2 — 2026-07-09 — launch doc-honesty + onboarding caption
6
16
 
7
17
  **Upgrade impact:** none (additive). Docs, help-content, one error message, and CLI onboarding copy; no runtime behavior change.
@@ -693,8 +693,14 @@ function renderHighlightedSkillBody(source) {
693
693
  ];
694
694
  for (const re of highPatterns) {
695
695
  body = body.replace(re, (_match, p1, p2) => {
696
- const prefix = p2 !== undefined ? p1 : "";
697
- const captured = p2 !== undefined ? p2 : p1;
696
+ // Only the `# Autonomous:` pattern has a second CAPTURE group (the
697
+ // leading newline/BOL is p1, the signal is p2). The single-group
698
+ // patterns receive the match OFFSET as the third arg — a NUMBER, not a
699
+ // capture — so test for a string, not `!== undefined`, or the offset
700
+ // gets rendered as highlighted text (e.g. `$ data_write2861`).
701
+ const hasPrefixGroup = typeof p2 === "string";
702
+ const prefix = hasPrefixGroup ? p1 : "";
703
+ const captured = hasPrefixGroup ? p2 : p1;
698
704
  return `${prefix}<span class="sig-high">${captured}</span>`;
699
705
  });
700
706
  }
@@ -28,6 +28,7 @@ DataStore — substrate-neutral data persistence
28
28
  query(filters) runtime asks: "find records matching these filters"
29
29
  write(record) runtime asks: "store this; return id + timestamp"
30
30
  get(id) runtime asks: "give me this specific record"
31
+ manifest() runtime asks: "what can you do?" (capability snapshot)
31
32
 
32
33
  SkillStore — substrate-neutral skill source persistence
33
34
  load(name) "give me this skill's source"
@@ -37,6 +38,7 @@ SkillStore — substrate-neutral skill source persistence
37
38
  versions(name) "audit trail"
38
39
  metadata(name) "header info without body"
39
40
  delete(name) "remove all versions"
41
+ manifest() "substrate capability snapshot"
40
42
 
41
43
  LocalModel — substrate-neutral LLM dispatch
42
44
  run(prompt, opts) "complete this prompt; return text"
@@ -368,7 +370,7 @@ Every skill carries a lifecycle status — `Draft → Approved → Disabled`. **
368
370
 
369
371
  Approval is an Ed25519 signature, not a shared secret:
370
372
 
371
- - **Private key** — held by the operator, read **only** by the approve flow. That flow is either `skillfile approve` at a terminal (the runtime process never loads the key) or, if you opt into in-browser approval, the dashboard's passcode-gated `/approve` — which reads the key only after a live human enters the session passcode, never on its own. For maximum isolation, keep signing terminal-side and run the runtime under a uid that cannot read the private key — then a co-resident agent can't forge approvals even with full read access to the runtime process. Same-uid (or in-browser signing) still works, but isn't fully isolated (a documented 1.0 precondition; managed custody closes it later).
373
+ - **Private key** — held by the operator, read **only** by the approve flow. That flow is either `skillfile approve` at a terminal (the runtime process never loads the key) or, if you opt into in-browser approval, the dashboard's passcode-gated `/approve` — which reads the key only after a live human enters the session passcode, never on its own. For maximum isolation, keep signing terminal-side and run the runtime under a uid that cannot read the private key — then a co-resident agent can't forge approvals even with full read access to the runtime process. Same-uid (or in-browser signing) still works, but isn't fully isolated a future managed-custody option will close this.
372
374
  - **Public key** — non-secret; the runtime reads it to *verify* signatures on every execution. No hot-path secret is the point: verification needs only the public key.
373
375
 
374
376
  First-run provisioning is automatic — starting the runtime or `skillfile init` in secured mode generates the keypair if absent (private written `0600`, default `~/.config/skillscript/approval.{key,pub}`, deliberately outside `SKILLSCRIPT_HOME`, the agent-readable data dir).
@@ -514,7 +516,7 @@ To wire your **own custom substrate** (e.g. a remote `SkillStore`), two paths:
514
516
 
515
517
  This closes the silent CLI-vs-programmatic asymmetry described below; reach for raw `bootstrap()` only when you're hand-assembling a registry that neither `connectors.json` nor `overrides` can express.
516
518
 
517
- **⚠ Migrating an existing hand-assembled bootstrap to `bootstrapFromEnv()`?** Move any options you previously hardcoded on `bootstrap()` / `new DashboardServer({...})` to their `SKILLSCRIPT_*` env equivalents — `bootstrapFromEnv()` resolves them from env, so a value you drop reverts to the default. Two to watch (adopter-verified, 0.24.0): `enableUnsafeShell: true` → `SKILLSCRIPT_ENABLE_UNSAFE_SHELL=true` (fails **loud** — unsafe ops just refuse, you'll notice); and **`mcpCallerIdentityHeader: "X-Agent-Id"` → `SKILLSCRIPT_MCP_CALLER_IDENTITY_HEADER=X-Agent-Id`, which fails *silently*** — drop it and skill-author attribution quietly reverts to the store's default writer identity, with no error. (`bootstrap()`-level opts like `enableUnsafeShell` can instead go through `overrides`; the `DashboardServer`-level `mcpCallerIdentityHeader` is env-only via `bootstrapFromEnv` — set the env var.) Verify after migrating: send your identity header on a `/rpc` `skill_write` and confirm the captured `author`.
519
+ **⚠ Migrating an existing hand-assembled bootstrap to `bootstrapFromEnv()`?** Move any options you previously hardcoded on `bootstrap()` / `new DashboardServer({...})` to their `SKILLSCRIPT_*` env equivalents — `bootstrapFromEnv()` resolves them from env, so a value you drop reverts to the default. Two to watch: `enableUnsafeShell: true` → `SKILLSCRIPT_ENABLE_UNSAFE_SHELL=true` (fails **loud** — unsafe ops just refuse, you'll notice); and **`mcpCallerIdentityHeader: "X-Agent-Id"` → `SKILLSCRIPT_MCP_CALLER_IDENTITY_HEADER=X-Agent-Id`, which fails *silently*** — drop it and skill-author attribution quietly reverts to the store's default writer identity, with no error. (`bootstrap()`-level opts like `enableUnsafeShell` can instead go through `overrides`; the `DashboardServer`-level `mcpCallerIdentityHeader` is env-only via `bootstrapFromEnv` — set the env var.) Verify after migrating: send your identity header on a `/rpc` `skill_write` and confirm the captured `author`.
518
520
 
519
521
  #### Raw `bootstrap()` — the CLI-auto-vs-programmatic-explicit asymmetry
520
522
 
@@ -608,7 +610,7 @@ Per-skill capability declaration: skills declare what shell binaries they need i
608
610
  # Status: Approved
609
611
  ```
610
612
 
611
- The operator policy validates `declared ∩ allowlist` — each skill's shell footprint becomes self-documenting and auditable. Slated for a future ring once the chokepoint + observability surfaces ship.
613
+ The operator policy validates `declared ∩ allowlist` — each skill's shell footprint becomes self-documenting and auditable. Planned for a future release.
612
614
 
613
615
  ## Filesystem path allowlist
614
616
 
@@ -1185,8 +1187,8 @@ See `examples/custom-bootstrap.example.ts` for a worked walkthrough.
1185
1187
 
1186
1188
  | Substrate | Shipped contract | Shipped impls | Shipped bridge |
1187
1189
  |---|---|---|---|
1188
- | SkillStore | ✓ 8 methods (`load` / `query` / `store` / `update_status` / `delete` / `versions` / `metadata` / `staticCapabilities`) | `FilesystemSkillStore`, `SqliteSkillStore` | n/a |
1189
- | DataStore | ✓ 3 methods (`query` / `write` / `get`) | `SqliteDataStore` | ✓ `DataStoreMcpConnector` |
1190
+ | SkillStore | ✓ 8 methods (`load` / `query` / `store` / `update_status` / `delete` / `versions` / `metadata` / `manifest`) + `staticCapabilities` | `FilesystemSkillStore`, `SqliteSkillStore` | n/a |
1191
+ | DataStore | ✓ 4 methods (`query` / `write` / `get` / `manifest`) + `staticCapabilities` | `SqliteDataStore` | ✓ `DataStoreMcpConnector` |
1190
1192
  | LocalModel | ✓ 1 method (`run`) | `OllamaLocalModel` | ✓ `LocalModelMcpConnector` |
1191
1193
  | McpConnector | ✓ 1 method (`call`) | `RemoteMcpConnector`, `CallbackMcpConnector` | n/a |
1192
1194
  | AgentConnector | ✓ 5 required (`list_agents` / `deliver` / `wake` / `health_check` / `request_response`) + 1 optional (`agent_status`) | `NoOpAgentConnector` (default), `HttpWebhookAgentConnector` | n/a |
@@ -1201,7 +1203,7 @@ See `examples/custom-bootstrap.example.ts` for a worked walkthrough.
1201
1203
  - **Your `DataStore.write()` is never called if the mutation gate rejects the skill.** The runtime gates `$ data_write` (and other mutation ops) upstream of the bridge — substrates only see authorized writes. If your own probes hit `UnconfirmedMutationError`, that's a skill-body issue (missing `# Autonomous: true` / `approved=`), not a substrate issue.
1202
1204
  - **Filter scope is enforced at the bridge.** `DataStoreMcpConnector` rejects every filter key outside the substrate's declared `manifest().supported_filters` set, throwing `UnsupportedFilterError`. This prevents silent scope leaks where unsupported filters get dropped without the caller knowing. Per-call opt-out: `permissive_filters: true` acknowledges "unknown keys are advisory; substrate may ignore them." Substrate implementors: declare every filter your `query()` actually honors so the bridge validates against your truth, not a guess.
1203
1205
  - **FTS matching strictness varies by substrate.** The `DataStore.query()` contract names the modes (`fts` / `semantic` / `rerank`) but doesn't pin down matching semantics within each mode — token-OR, phrase-tokens, fuzzy, exact, FTS5-syntax-passthrough, etc. are all conformant. The bundled `data-store-roundtrip` demo asserts `N ≥ 1` (a successful round-trip) rather than a specific count, which works across any FTS-supporting substrate. For adopters who need deterministic exact-count reads (round-trip tests, idempotency checks, exact-record-matched fetches), the portable strict-match path is `domain_tags=[...]` filtering — the bridge enforces tag-key against `supported_filters` and substrates declaring `supports_tag_filter: true` honor exact-tag any-of-match per the contract. Use FTS for relevance ranking against open content; use tag filters when you need to be sure you got the specific record you wrote.
1204
- - **Durable-forever opt-in via `expires_at: null`.** `DataWrite.expires_at` accepts a unix timestamp for finite expiry, `null` to opt into "durable forever" (the portable verb for substrates with default TTL — AMP memory vaults, Redis with default expiry, hosted memory APIs), or omitted (substrate's default lifecycle, may be durable or may have decay). Substrates that are durable-by-default (the bundled `SqliteDataStore`) treat `null` as a no-op. Substrates with default sweep should map `null` to their pin / no-decay flag.
1206
+ - **Durable-forever opt-in via `expires_at: null`.** `DataWrite.expires_at` accepts a unix timestamp for finite expiry, `null` to opt into "durable forever" (the portable verb for substrates with default TTL — Redis with default expiry, hosted memory APIs), or omitted (substrate's default lifecycle, may be durable or may have decay). Substrates that are durable-by-default (the bundled `SqliteDataStore`) treat `null` as a no-op. Substrates with default sweep should map `null` to their pin / no-decay flag.
1205
1207
  - **Two trigger primitives, both functional.** `cron` (time-based) and `event` (HTTP `/event` ingress, named registration). All other concepts that look like triggers — session-start, agent-event, file-watch, sensor — are adapter responsibilities: the adapter POSTs `/event` when relevant. Keeps the runtime substrate-neutral; the trigger surface stays tight.
1206
1208
  - **Output kinds are intentionally substrate-neutral.** `# Output:` accepts `text` / `agent: <name>` / `template: <name>` / `file: <path>` / `none`. Substrate-specific values (`slack:`, `card:`, etc.) are out of scope — adopters wanting Slack / WhatsApp / Discord / etc. delivery use either `$ slack.post ...` MCP dispatch inside the skill body OR deliver via `agent: <name>` and let the receiving agent decide.
1207
1209
  - **Authorization is signature-based in secured mode.** An `Approved` skill runs unkeyed in unsecured mode (a bare `# Status: Approved` is sufficient) and requires a valid Ed25519 operator signature in secured mode — verified on every execution against the operator's public key, with the private key held off the runtime. See [Approval + secured mode](#approval--secured-mode) for the model, the approve flow, key custody, and migration. The shell + filesystem allowlists bound blast radius in *both* modes; secured mode is what gates whether an unapproved skill runs at all.
@@ -1228,7 +1230,7 @@ The multi-layer-promise pattern (lint passes; runtime fails, or vice versa) is t
1228
1230
  2. **Runtime test** — same shape executed end-to-end (`executeSkillByName` or `executeSkillFromSource`)
1229
1231
  3. **E2E test** — the full user path (write skill → store → execute via MCP, or trigger fire → dispatch)
1230
1232
 
1231
- PR description must call out which dispatch shape is exercised. If you can't write all three for a shape, that's a signal the shape is incompletely specified — file a thread before merging.
1233
+ PR description must call out which dispatch shape is exercised. If you can't write all three for a shape, that's a signal the shape is incompletely specified — open an issue before merging.
1232
1234
 
1233
1235
  Connector class authors implementing new `McpConnectorClass`-shaped contracts should also implement `staticTools(): string[] | null` whenever the tool surface is closed and knowable at compile time. Lift `unknown-tool-on-connector` from "advisory you fix at runtime" to "tier-1 error caught at compile time" for every adopter who wires your class.
1234
1236
 
@@ -4,11 +4,11 @@ description: "The substrate-neutral contracts adopters implement to wire their o
4
4
  mode: wide
5
5
  ---
6
6
 
7
- The substrate-neutral contracts skillscript-runtime exposes for adopters to wire their own substrate behind. This doc is the **canonical source of truth** for the AgentConnector contract. The interface shape was locked at v1.0 by the v0.9.6 audit (thread `b722bbf4`); the wake/deliver receipt shapes carry post-lock refinements per the v0.18.2 session-targeting + graceful-degradation requirements.
7
+ The substrate-neutral contracts skillscript-runtime exposes for adopters to wire their own substrate behind. This doc is the **canonical source of truth** for the AgentConnector contract, whose interface shape is locked at v1.0. The wake/deliver receipt shapes carry refinements for session-targeting and graceful degradation.
8
8
 
9
9
  **Audience**: this doc is written for the agent that's implementing an adopter's AgentConnector — typically an LLM-class agent supervised by a human. If you're a human reading it directly, the same content applies; the prose is tightened for agent comprehension (literal field semantics, explicit precedence rules, worked examples).
10
10
 
11
- Other contracts (McpConnector, SkillStore, DataStore, LocalModel) audit + lock in subsequent v0.9.x slots; this doc grows with each lock.
11
+ The other contracts (McpConnector, SkillStore, DataStore, LocalModel) are covered in the sections below; this doc grows to cover each.
12
12
 
13
13
  ---
14
14
 
@@ -73,7 +73,7 @@ interface DeliveryMeta {
73
73
 
74
74
  - **`meta.origin.trigger_kind`**: how the originating skill was fired. Receiver routes on this without parsing content (cron-fired triage vs agent-initiated request vs webhook from external system).
75
75
 
76
- - **`meta.origin.caller_agent_id`**: the AUTHENTICATED CALLER who fired the dispatch — distinct from the skill's author/owner. When an MCP `/rpc` call carries the configured caller-identity header (e.g., `X-Agent-Id: cc`), that value flows here. The chain originator is preserved across composition: if `cc` invokes Alice's skill A which composes Bob's skill B, B's notify() still emits `caller_agent_id: cc`. Cron / event / cli / dashboard triggers leave it undefined (no human caller); direct `execute_skill` without an identity header also leaves it undefined (the owner is NOT used as fallback — that would be the v0.16.8-era confusion that v0.18.4 split). See [Adopter Playbook](adopter-playbook.md) §"Identity propagation" for the inbound-header wiring.
76
+ - **`meta.origin.caller_agent_id`**: the AUTHENTICATED CALLER who fired the dispatch — distinct from the skill's author/owner. When an MCP `/rpc` call carries the configured caller-identity header (e.g., `X-Agent-Id: web-ui`), that value flows here. The chain originator is preserved across composition: if `web-ui` invokes Alice's skill A which composes Bob's skill B, B's notify() still emits `caller_agent_id: web-ui`. Cron / event / cli / dashboard triggers leave it undefined (no human caller); direct `execute_skill` without an identity header also leaves it undefined (the owner is NOT used as fallback — caller-identity and ownership are deliberately distinct). See [Adopter Playbook](adopter-playbook.md) §"Identity propagation" for the inbound-header wiring.
77
77
 
78
78
  - **`meta.event_type`**: adopter-defined routing vocabulary — opaque to skillscript. Set via `notify(event_type=...)` kwarg (per-emit) OR `# Event-type:` skill frontmatter (skill-wide fallback). Kwarg takes precedence per-emit.
79
79
 
@@ -95,7 +95,7 @@ interface DeliveryReceipt {
95
95
  - **`delivery_id`**: substrate-specific id for callers to correlate later.
96
96
  - **`session_id`**: the session that received the delivery. Set when the substrate routes to a specific session (e.g., per-terminal mailbox, per-tab webhook). Omitted when the substrate is agent-level only (Slack DM, email — no session concept) or when the substrate fans out / accepts without committing to a session. See *agent@session targeting* below.
97
97
  - **`delivery_skipped`**: adopter signals "accepted but not pushed to the agent" — offline, rate-limit drop, tmux session exists but agent hasn't read, etc. Distinct from outright failure (which throws). Runtime echoes this on the receipt record for dashboard observability.
98
- - **`warnings`** (v0.18.4): non-fatal substrate notes about the delivery. Surfaced onto `AgentDeliveryReceiptRecord` so the dashboard + observability surfaces show them instead of substrate-side stderr noise. Examples: `"stripped @session suffix — deliver is mailbox-class"`, `"rate-limit hint: backoff 5s before next deliver"`, `"fan-out: delivered to 3 active sessions"`. Distinct from `delivery_skipped` (accepted-not-pushed) and from thrown errors (delivery failed) — warnings are advisory; the delivery succeeded, the substrate just has commentary.
98
+ - **`warnings`**: non-fatal substrate notes about the delivery. Surfaced onto `AgentDeliveryReceiptRecord` so the dashboard + observability surfaces show them instead of substrate-side stderr noise. Examples: `"stripped @session suffix — deliver is mailbox-class"`, `"rate-limit hint: backoff 5s before next deliver"`, `"fan-out: delivered to 3 active sessions"`. Distinct from `delivery_skipped` (accepted-not-pushed) and from thrown errors (delivery failed) — warnings are advisory; the delivery succeeded, the substrate just has commentary.
99
99
 
100
100
  ### WakeOpts + WakeReceipt
101
101
 
@@ -110,6 +110,7 @@ interface WakeReceipt {
110
110
  woken_at: number;
111
111
  woken: boolean;
112
112
  session_id?: string;
113
+ warnings?: string[];
113
114
  }
114
115
  ```
115
116
 
@@ -119,41 +120,42 @@ interface WakeReceipt {
119
120
  - **`WakeReceipt.woken_at`**: substrate's acknowledgement timestamp.
120
121
  - **`WakeReceipt.woken`** (required): honest signal of whether the substrate actually interrupted the agent. See *Graceful degradation on wake* below — this is the read every caller does to distinguish interrupted-them from delivered-only.
121
122
  - **`WakeReceipt.session_id`**: the session that received the wake (or delivery, if degraded). Set when the substrate knows; omit otherwise.
123
+ - **`WakeReceipt.warnings`**: optional non-fatal substrate notes about the wake — advisory commentary surfaced onto the receipt record for dashboard + observability instead of substrate-side stderr noise. Symmetric with `DeliveryReceipt.warnings`.
122
124
 
123
125
  ### agent@session targeting
124
126
 
125
127
  `agent_id` is an opaque string. The substrate may treat it as:
126
128
 
127
129
  - A bare agent identifier (`alice`, an email address, a Slack `@user`, a Discord user ID).
128
- - A composite `agent@session` (e.g., `"perry@kitchen-terminal"`) when the substrate tracks multiple live sessions per identity.
130
+ - A composite `agent@session` (e.g., `"alice@terminal-1"`) when the substrate tracks multiple live sessions per identity.
129
131
 
130
132
  The substrate decomposes the composite if it cares; non-session substrates ignore the suffix or treat the whole string as the address. This keeps the contract substrate-neutral while preserving session-granular routing — every messaging substrate either addresses a bare identity OR a specific live session, and the opaque-composite form covers both without locking adopters into a particular session model.
131
133
 
132
- **Address-routed dispatch (v0.18.5)**: the runtime uses the presence of `@` in `agent_id` to decide between `deliver()` and `wake()` for skill-author surfaces (`notify()` op + `# Output: agent:` / `# Output: template:` lifecycle hooks):
134
+ **Address-routed dispatch**: the runtime uses the presence of `@` in `agent_id` to decide between `deliver()` and `wake()` for skill-author surfaces (`notify()` op + `# Output: agent:` / `# Output: template:` lifecycle hooks):
133
135
 
134
136
  | Skill-author syntax | Address shape | Connector method called |
135
137
  |---|---|---|
136
- | `notify(agent="perry", …)` | bare | `deliver()` |
137
- | `notify(agent="perry@kitchen-terminal", …)` | composite | `wake()` |
138
- | `# Output: agent: perry` | bare | `deliver()` |
139
- | `# Output: agent: perry@kitchen-terminal` | composite | `wake()` |
140
- | `# Output: template: perry@browser-tab-3` | composite | `wake()` |
138
+ | `notify(agent="alice", …)` | bare | `deliver()` |
139
+ | `notify(agent="alice@terminal-1", …)` | composite | `wake()` |
140
+ | `# Output: agent: alice` | bare | `deliver()` |
141
+ | `# Output: agent: alice@terminal-1` | composite | `wake()` |
142
+ | `# Output: template: alice@tab-3` | composite | `wake()` |
141
143
 
142
- The runtime threads the FULL composite to `wake()` — substrate decomposes per the rule above. For wake-routed dispatches, the skill's content (notify message or accumulated emissions) rides as `WakeOpts.context`. Per Perry's design call (thread `c453afa2`): "the address encodes delivery class" — same rule as the broader `waiting_on` / mailbox / broker convention. No `wake=true` kwarg exists; the `@` IS the signal.
144
+ The runtime threads the FULL composite to `wake()` — substrate decomposes per the rule above. For wake-routed dispatches, the skill's content (notify message or accumulated emissions) rides as `WakeOpts.context`. The rule: "the address encodes delivery class" — same convention as the broader `waiting_on` / mailbox / broker convention. No `wake=true` kwarg exists; the `@` IS the signal.
143
145
 
144
146
  **Two forms, one wire**:
145
147
 
146
148
  ```typescript
147
149
  // Form A — composite in agent_id (works for deliver + wake)
148
- await conn.wake("perry@kitchen-terminal");
150
+ await conn.wake("alice@terminal-1");
149
151
 
150
152
  // Form B — structured WakeOpts.session_id (wake only)
151
- await conn.wake("perry", { session_id: "kitchen-terminal" });
153
+ await conn.wake("alice", { session_id: "terminal-1" });
152
154
  ```
153
155
 
154
156
  Substrates that care about sessions read both — `opts.session_id` wins if both are set. Substrates that don't care ignore both. Callers that already have agent + session as separate variables prefer Form B; callers passing an opaque user-supplied address prefer Form A.
155
157
 
156
- `DeliveryReceipt.session_id` and `WakeReceipt.session_id` echo the resolved session back to the caller. Useful for dashboards rendering "delivered to perry@kitchen-terminal" rather than just "delivered to perry."
158
+ `DeliveryReceipt.session_id` and `WakeReceipt.session_id` echo the resolved session back to the caller. Useful for dashboards rendering "delivered to alice@terminal-1" rather than just "delivered to alice."
157
159
 
158
160
  ### Graceful degradation on wake
159
161
 
@@ -185,7 +187,7 @@ The distinction `wake-capability` vs `network-fault` matters. The former is stru
185
187
  | `notify(agent=X@session, message=..., ...)` op | composite | `AgentConnector.wake()` | n/a (message as `WakeOpts.context`) | n/a |
186
188
  | `exchange(agent=X, message=..., timeout=...)` op (locked-shape, runtime support pending) | bare | `AgentConnector.request_response()` | `augment` | Same as notify; correlation_id required |
187
189
 
188
- The address-routing rule (v0.18.5) is uniform across all skill-author surfaces: `@session` present → wake-class; bare → deliver-class. See *agent@session targeting* below for the contract-level convention.
190
+ The address-routing rule is uniform across all skill-author surfaces: `@session` present → wake-class; bare → deliver-class. See *agent@session targeting* below for the contract-level convention.
189
191
 
190
192
  ---
191
193
 
@@ -208,7 +210,7 @@ Wiring failures surface at boot (health_check throws), not at first skill-fire.
208
210
 
209
211
  ### Writing your own AgentConnector
210
212
 
211
- If you're an agent implementing this contract against an adopter substrate, the canonical worked example is `HttpWebhookAgentConnector` (shipping post-audit; see `examples/` once bundled).
213
+ If you're an agent implementing this contract against an adopter substrate, the canonical worked example is `HttpWebhookAgentConnector`, bundled under `examples/connectors/`.
212
214
 
213
215
  Implementation checklist:
214
216
 
@@ -234,7 +236,7 @@ If your substrate matches the shape of a bundled connector closely (e.g., HTTP w
234
236
 
235
237
  ---
236
238
 
237
- ## Footnotes pinned during the v0.9.6 audit (Perry's thread b722bbf4)
239
+ ## Load-bearing semantic footnotes
238
240
 
239
241
  These are the load-bearing semantic rules. Internalize before implementing.
240
242
 
@@ -250,15 +252,15 @@ These are the load-bearing semantic rules. Internalize before implementing.
250
252
 
251
253
  ## Storage-layer conventions (SkillStore + DataStore)
252
254
 
253
- The cold-adopter Phase 3 dogfood (writing AmpSkillStore + AmpDataStore against AMP) surfaced several conventions that live in the bundled reference impls but aren't first-class in the typed contracts. Adopters writing their own SkillStore/DataStore impls need to know about these, or skills/memories misbehave silently.
255
+ The following conventions live in the bundled reference impls but aren't first-class in the typed contracts. Adopters writing their own SkillStore/DataStore impls need to know about these, or skills/memories misbehave silently.
254
256
 
255
257
  ### SkillStore conventions
256
258
 
257
- **`author` field on SkillMeta + filter on `query()` (v0.18.6).** `SkillMeta.author` is optional; substrates that track authorship populate it (bundled `FilesystemSkillStore` reads from `os.userInfo().username`; `SqliteSkillStore` stores at write time). Substrates without an authorship concept leave it `undefined`; the catalog layer surfaces `null` to the wire.
259
+ **`author` field on SkillMeta + filter on `query()`.** `SkillMeta.author` is optional; substrates that track authorship populate it (bundled `FilesystemSkillStore` reads from `os.userInfo().username`; `SqliteSkillStore` stores at write time). Substrates without an authorship concept leave it `undefined`; the catalog layer surfaces `null` to the wire.
258
260
 
259
- `SkillStore.query({ author: "X" })` is an optional substrate-honored filter. Substrates that natively track authorship can filter at the substrate layer; substrates that don't return all status-matching rows and the `buildSkillCatalog()` layer filters in-memory per `meta.author`. Either way the caller sees only matching authors. Per Perry's spec (thread `1f278e5e`): generic, connector-implemented, graceful-degrading. The substrate-neutrality property holds — adopters wire whichever shape fits their ownership model.
261
+ `SkillStore.query({ author: "X" })` is an optional substrate-honored filter. Substrates that natively track authorship can filter at the substrate layer; substrates that don't return all status-matching rows and the `buildSkillCatalog()` layer filters in-memory per `meta.author`. Either way the caller sees only matching authors a generic, connector-implemented, graceful-degrading filter. The substrate-neutrality property holds — adopters wire whichever shape fits their ownership model.
260
262
 
261
- Adopter substrates with their own ownership concept (e.g., AMP's `author:<id>` tag) should map the filter onto their native query so subset-fetching stays efficient. Adopters with no ownership concept can leave `query()` unchanged and let the catalog-layer in-memory filter handle narrowing.
263
+ Adopter substrates with their own ownership concept (e.g., a native `author:<id>` tag) should map the filter onto their native query so subset-fetching stays efficient. Adopters with no ownership concept can leave `query()` unchanged and let the catalog-layer in-memory filter handle narrowing.
262
264
 
263
265
 
264
266
 
@@ -276,9 +278,9 @@ Adopter substrates with their own ownership concept (e.g., AMP's `author:<id>` t
276
278
 
277
279
  ### DataStore conventions
278
280
 
279
- **`summary`/`detail` split is convention, not contract field.** The DataStore contract gives `write()` a single `content: string`. Bundled `SqliteDataStore` maps this to `summary = first line (≤200 chars)` and `detail = full content`. Adopter substrates with native summary/detail concepts (AMP's `summary` + `detail` columns) can pre-compose and pass via `metadata`, but the basic mapping convention is "first line is the preview." Diverge and the dashboard's memory rendering looks weird, but skills still work.
281
+ **`summary`/`detail` split is convention, not contract field.** The DataStore contract gives `write()` a single `content: string`. Bundled `SqliteDataStore` maps this to `summary = first line (≤200 chars)` and `detail = full content`. Adopter substrates with native summary/detail concepts (their own `summary` + `detail` columns) can pre-compose and pass via `metadata`, but the basic mapping convention is "first line is the preview." Diverge and the dashboard's memory rendering looks weird, but skills still work.
280
282
 
281
- **`get(id)` returns null on miss, doesn't throw.** Distinct from SkillStore's `load(name)` which throws `SkillNotFoundError`. DataStore's empty-set convention (`query()` returns `[]` not throws; `get()` returns `null` not throws) is **load-bearing for the runtime's control flow** — query callers branch on `result.length`, get callers branch on `result === null`. Don't change this in your impl. Per cold agent's "credit where due": *"unambiguous, and the runtime keys control flow on the specific classes."*
283
+ **`get(id)` returns null on miss, doesn't throw.** Distinct from SkillStore's `load(name)` which throws `SkillNotFoundError`. DataStore's empty-set convention (`query()` returns `[]` not throws; `get()` returns `null` not throws) is **load-bearing for the runtime's control flow** — query callers branch on `result.length`, get callers branch on `result === null`. Don't change this in your impl.
282
284
 
283
285
  ### Durability stance (both contracts)
284
286
 
@@ -298,8 +300,8 @@ Implementer responsibility: **declare every filter your `query()` actually honor
298
300
 
299
301
  ### Why these aren't in the typed interface
300
302
 
301
- The shape-vs-semantics split is deliberate (see [[ARCHITECTURE INVARIANT 88df79c1]]): the typed contract guarantees shape portability (same methods, same return types); the conventions above are semantic portability concerns that the contract chose not to encode. Bundled impls follow them; custom impls SHOULD follow them. Capability flags + manifest fields make some conventions inspectable at runtime (`regexp_fallback_active`, `supported_filters`, `supported_modes`), but most live in source comments + this doc.
303
+ The shape-vs-semantics split is deliberate: the typed contract guarantees shape portability (same methods, same return types); the conventions above are semantic portability concerns that the contract chose not to encode. Bundled impls follow them; custom impls SHOULD follow them. Capability flags + manifest fields make some conventions inspectable at runtime (`regexp_fallback_active`, `supported_filters`, `supported_modes`), but most live in source comments + this doc.
302
304
 
303
305
  ---
304
306
 
305
- *This doc reflects the v0.9.6 AgentConnector interface lock, v0.13.8 storage-conventions addition, v0.18.2 receipt-shape refinements (woken-honesty + session targeting + graceful degradation), v0.18.4 caller-identity-threading + `DeliveryReceipt.warnings`, and v0.18.5 address-routed dispatch (skill-author surfaces route deliver vs. wake on `@session` presence) + `WakeReceipt.warnings`. Future contract changes update this file alongside the code.*
307
+ *Future contract changes update this file alongside the code; the CHANGELOG owns version history.*
@@ -1861,7 +1861,7 @@ Test, Deployed, and Deprecated were considered and deferred — today's Draft/Ap
1861
1861
 
1862
1862
  Lifecycle states are the language's operational-safety answer. A Disabled skill can't fire even if every author forgets it's broken; in secured mode, an Approved skill can't fire if its body was tampered post-signature, and an agent can't approve its own work. The constraint IS the safety story, here as elsewhere.
1863
1863
 
1864
- ## Error handling — else: blocks, # OnError: fallback, op-level fallback values
1864
+ ## Error handling — else: blocks and op-level (fallback:) values
1865
1865
 
1866
1866
  Skillscript has no try/catch — error handling is authored. Two runtime mechanisms (local to global) plus a structural discipline that prevents failure outright.
1867
1867
 
@@ -1910,7 +1910,7 @@ Pre-bind defaults (`$set`) for every var the template/downstream reads, then gat
1910
1910
 
1911
1911
  ## Deprecated: `# OnError:` — parsed but not wired
1912
1912
 
1913
- A skill-level `# OnError: <fallback-skill>` header parses, but it is **not wired in the current runtime: the named fallback never fires.** A skill that relies on it has, in effect, no error handling — so don't use it. Use a target-level `else:` handler (or an op-level `(fallback:)`) instead. Removal of the header is planned for a future release (v0.28); until then it is inert, not a compile error. To recover at the whole-skill level, wrap the body in a target with an `else:` that dispatches your recovery skill:
1913
+ A skill-level `# OnError: <fallback-skill>` header parses, but it is **not wired in the current runtime: the named fallback never fires.** A skill that relies on it has, in effect, no error handling — so don't use it. Use a target-level `else:` handler (or an op-level `(fallback:)`) instead. Removal of the header is planned for a post-launch release (after 0.30); until then it is inert, not a compile error. To recover at the whole-skill level, wrap the body in a target with an `else:` that dispatches your recovery skill:
1914
1914
 
1915
1915
  ```
1916
1916
  run:
@@ -2565,5 +2565,5 @@ When any of these primitives ship, the relevant grammar moves into its canonical
2565
2565
 
2566
2566
  ---
2567
2567
 
2568
- *Rendered from `skillscript/skillscript-language-reference` — 2026-07-09 18:31 EDT*
2568
+ *Rendered from `skillscript/skillscript-language-reference` — 2026-07-10 13:59 EDT*
2569
2569
  *Source of truth: AMP (`amp_render_document("skillscript/skillscript-language-reference")`)*
@@ -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-07-09T22:36:21.217Z",
5
+ "compiled_at": "2026-07-10T18:06:49.305Z",
6
6
  "source_skill": {
7
7
  "name": "classify-support-ticket"
8
8
  },
@@ -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-07-09T22:36:21.422Z",
5
+ "compiled_at": "2026-07-10T18:06:49.486Z",
6
6
  "source_skill": {
7
7
  "name": "data-store-roundtrip"
8
8
  },
@@ -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-07-09T22:36:21.590Z",
5
+ "compiled_at": "2026-07-10T18:06:49.652Z",
6
6
  "source_skill": {
7
7
  "name": "doc-qa-with-citations"
8
8
  },
@@ -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-07-09T22:36:21.766Z",
5
+ "compiled_at": "2026-07-10T18:06:49.855Z",
6
6
  "source_skill": {
7
7
  "name": "feedback-sentiment-scan"
8
8
  },
@@ -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-07-09T22:36:21.957Z",
5
+ "compiled_at": "2026-07-10T18:06:50.004Z",
6
6
  "source_skill": {
7
7
  "name": "hello-world"
8
8
  },
@@ -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-07-09T22:36:22.134Z",
5
+ "compiled_at": "2026-07-10T18:06:50.178Z",
6
6
  "source_skill": {
7
7
  "name": "morning-brief"
8
8
  },
@@ -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-07-09T22:36:22.313Z",
5
+ "compiled_at": "2026-07-10T18:06:50.377Z",
6
6
  "source_skill": {
7
7
  "name": "queue-length-monitor"
8
8
  },
@@ -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-07-09T22:36:22.486Z",
5
+ "compiled_at": "2026-07-10T18:06:50.550Z",
6
6
  "source_skill": {
7
7
  "name": "service-health-watch"
8
8
  },
@@ -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-07-09T22:36:22.672Z",
5
+ "compiled_at": "2026-07-10T18:06:50.723Z",
6
6
  "source_skill": {
7
7
  "name": "skill-store-roundtrip"
8
8
  },
@@ -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-07-09T22:36:22.850Z",
5
+ "compiled_at": "2026-07-10T18:06:50.897Z",
6
6
  "source_skill": {
7
7
  "name": "youtrack-morning-sweep"
8
8
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillscript-runtime",
3
- "version": "0.27.2",
3
+ "version": "0.27.3",
4
4
  "description": "Runtime, compiler, lint, CLI, and dashboard for Skillscript — a small declarative language for authoring agent workflows.",
5
5
  "license": "MIT",
6
6
  "author": "Scott Shwarts <scotts@pobox.com>",