skillscript-runtime 0.19.7 → 0.19.9

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.
@@ -18,12 +18,9 @@ These are features designed or anticipated but not yet implemented in the curren
18
18
 
19
19
  - **Array aggregation primitives** — `|max`, `|min`, `|sum`, `|reduce` over arrays. Today the language tops out at "shape one record" — aggregating across an array requires `foreach` accumulator ceremony. Planned as a design question: is `foreach` the deliberate ceiling for aggregation, or do we add primitives?
20
20
 
21
- ## Triggers (parse-clean today, don't fire no event-bus surface yet)
21
+ ## Triggers — note
22
22
 
23
- - `event:` — generic event-bus subscription
24
- - `agent-event:` — agent-emitted events
25
- - `file-watch:` — filesystem change events
26
- - `sensor:` — continuous sensor channels
23
+ Trigger sources are `cron` + `event` only (see the Triggers section). There is no separate `agent-event:` / `file-watch:` / `sensor:` trigger source anything that isn't time-based is an external adapter that POSTs to the `/event` ingress. So there's nothing pending here on the trigger axis; the entry remains only to point readers who expect those sources at the adapter-POST pattern.
27
24
 
28
25
  ## Synchronous agent exchange
29
26
 
@@ -48,7 +45,9 @@ $set NAME = value scope=session
48
45
 
49
46
  Scopes: skill-local (persists across fires of this skill, not visible to other skills), agent-global (visible to all skills of the same agent), session (alive for the duration of the current session, cleared at session end). Backed by a configured data-records connector.
50
47
 
51
- ## Sensors as a language category
48
+ ## Sensors as a read-channel category
49
+
50
+ > `sensor:` is not a trigger source — to fire a skill on a sensor signal, a sensor adapter POSTs to `/event` (see Triggers). The concept below is the separate *read-channel* idea — continuous values an agent reads, distinct from triggering — which remains a future possibility if demand surfaces.
52
51
 
53
52
  Distinct from triggers. Sensors are continuous channels the agent reads but doesn't emit on. Planned syntax:
54
53
 
@@ -72,7 +71,7 @@ Most "right time" reasoning is relative, not wall-clock.
72
71
 
73
72
  ## Other planned
74
73
 
75
- - **Absence-as-trigger** — `# Triggers: idle: 5m` fire-on-quiet primitive
74
+ - **Absence-as-trigger** — `# Triggers: idle: 5m` fire-on-quiet primitive (would be a new trigger source beyond `cron`|`event`, or an adapter that POSTs on a timer)
76
75
  - **Time-windowed aggregation** — filter-like primitives across firings (e.g., "user has shown frustration in 3 of 5 recent turns")
77
76
  - **Debounce / rate-limit / coalesce** — declarative queueing policy headers
78
77
  - **Suppression as valid output** — explicit "fire-and-suppress" (different from `# Output: none`)
@@ -95,35 +94,37 @@ Skillscript is a constrained domain-specific language for authoring agent workfl
95
94
 
96
95
  Every skill follows the same shape:
97
96
 
98
- 1. **Trigger** — what fires the skill: cron, command, session-start, agent-event, file-watch, webhook, etc.
97
+ 1. **Trigger** — what fires the skill autonomously: `cron` (time-based) or `event` (an external HTTP POST to the runtime's `/event` ingress). (A skill can also be invoked explicitly — by an agent mid-conversation or via the execute API — with no trigger at all.)
99
98
  2. **Process** — pull data (MCP / data store / file), classify or compose via sub-LLM + iteration, build the deliverable.
100
- 3. **Deliver** — emit the result via one or more delivery channels (see below).
99
+ 3. **Deliver** — produce the result via the **body-text output template** (declarative — the skill body *is* the output) or via `emit()` (imperative, for dynamic output), then route it through one or more delivery channels (see below; production detail in Output).
101
100
 
102
101
  Skillscript's job is to express this pipeline declaratively. When there is an agent above the skill, the agent's job is to act on the delivered artifact. When there isn't (autonomous fires), the delivery channel IS the outcome.
103
102
 
104
103
  **Declarative DAG, not imperative script.** A skillscript declares targets and their dependencies (`needs:` keyword); the interpreter topologically sorts and executes them in dependency order. Write blocks in any order — the runtime walks the graph.
105
104
 
106
- **Goal-directed, not entry-point-directed.** The `default:` declaration names the *goal target* — the terminal node whose result is the skill's output. The runtime walks dependencies backward from the goal through the topo-sort. A skill with a single target obscures this (goal == entry trivially); skills with multi-target DAGs make the shape visible.
105
+ **Goal-directed, not entry-point-directed.** The `default:` declaration names the *goal target* — the terminal node whose result is the skill's output. The runtime walks dependencies backward from the goal through the topo-sort. A skill with a single target obscures this (goal == entry trivially); skills with multi-target DAGs make the shape visible. (A skill with *no* target is also valid — a body-template-only skill; see Output.)
107
106
 
108
107
  ## Two execution paths
109
108
 
110
- **Runtime-mediated** — the interpreter walks ops and dispatches them directly through configured connectors. Used for autonomous fires (cron, session-triggered, event-triggered). Safety boundary is the connector config + per-op gating (see Ops Reference).
109
+ **Runtime-mediated** — the interpreter walks ops and dispatches them directly through configured connectors. Used for autonomous fires (cron- or event-triggered). Safety boundary is the connector config + per-op gating (see Ops Reference).
111
110
 
112
111
  **Agent-mediated** — the compiler renders the skill as a prompt; an agent reads the prompt and executes ops through its own tools. Used when an agent invokes a skill mid-conversation. Safety boundary is the agent's harness tool permissions.
113
112
 
114
113
  The language is identical in both paths. The execution model is a deployment-time + invocation-time decision.
115
114
 
116
- ## Three delivery channels
115
+ ## Output production and delivery channels
116
+
117
+ **Production:** a skill's output is produced either declaratively by the **body-text template** (the skill body, with `${var}` interpolation — the clean default for fixed-shape output) or imperatively by **`emit()`** (for variable-cardinality / conditional-shape / transcript output). See Output for the when-which rule. The template, when present, is the canonical output; `emit` feeds the transcript.
117
118
 
118
- A skill delivers its work via one or more of three channels. Delivery channel is not a property of skill type — it's just which ops a skill ends with.
119
+ **Delivery:** the produced output is routed by the `# Output:` kind to one or more channels. Delivery channel is not a property of skill type — it's just the `# Output:` declaration.
119
120
 
120
- | Channel | Op | When you'd use it |
121
+ | Channel | Declaration | When you'd use it |
121
122
  |---|---|---|
122
- | **Augmenting (context to agent)** | `emit(text="...")` + `# Output: agent: <name>` | Skill output is augment-kind payload for the receiving agent's next turn; joined emit stream becomes the delivered context. Pattern: agent-augmenting skills (briefing skills, session-start prepared context). |
123
- | **Template (playbook to agent)** | `emit(text="...")` + `# Output: template: <name>` | Skill output is a template-kind payload (recipe/playbook) the receiving agent executes. Pattern: instructional skills, reusable recipes. |
123
+ | **Augmenting (context to agent)** | body template or `emit()` + `# Output: agent: <name>` | Output is augment-kind payload for the receiving agent's next turn. Pattern: agent-augmenting skills (briefings, session-start prepared context). |
124
+ | **Template (playbook to agent)** | body template or `emit()` + `# Output: template: <name>` | Output is a template-kind payload (recipe/playbook) the receiving agent executes. Pattern: instructional skills, reusable recipes. |
124
125
  | **Data handoff** | `$ data_write content="..." recipients=[<agent>] -> R` | Skill writes data the target agent picks up via mailbox at next session. Pattern: async carrier skills, autonomous fires that hand off to a future session. |
125
126
 
126
- A single skill can use any combination. An autonomous cron-fired sweep might write data to one agent AND emit augment-kind context to another. The combinations are unconstrained — the per-op gating model governs which mutating ops are authorized, not which channels a skill uses.
127
+ A single skill can use any combination. An autonomous cron-fired sweep might write data to one agent AND deliver augment-kind context to another. The combinations are unconstrained — the per-op gating model governs which mutating ops are authorized, not which channels a skill uses.
127
128
 
128
129
  ## Three op classes
129
130
 
@@ -160,12 +161,12 @@ Substrate config syntax + the three-form configuration shape lives in the adopte
160
161
 
161
162
  ## Anatomy of a skill
162
163
 
163
- A canonical example exercising trigger → process → deliver against the augmenting channel:
164
+ A canonical example exercising trigger → process → deliver against the augmenting channel. (Output here is built with `emit()` because the per-issue count is variable — the right case for `emit`; a fixed-shape skill would use a body template instead.)
164
165
 
165
166
  ```
166
167
  # Skill: morning-showstopper-sweep
167
168
  # Description: Pre-triage open showstoppers before the human arrives; deliver as augmenting context to the on-call agent.
168
- # Triggers: cron:"0 8 * * MON-FRI"
169
+ # Triggers: cron: 0 8 * * MON-FRI
169
170
  # Output: agent: oncall
170
171
  # Vars: PROJECT = "INFRA"
171
172
 
@@ -187,7 +188,7 @@ sweep:
187
188
  default: sweep
188
189
  ```
189
190
 
190
- The joined `emit()` stream becomes the augment-kind payload delivered to the on-call agent (per `# Output: agent: oncall` declaration). The agent sees the briefing inline at next-turn dispatch.
191
+ The joined `emit()` stream becomes the augment-kind payload delivered to the on-call agent (per `# Output: agent: oncall`). The agent sees the briefing inline at next-turn dispatch.
191
192
 
192
193
  **Three layers of declaration:**
193
194
  1. **Header metadata** (`# Key: value` lines) — name, description, declared variables (`# Vars:`), declared returns (`# Returns:` — the export surface, output-side mirror of `# Vars:`; see Composition), triggers, `# Output:` routing, optional `# Autonomous:` flag, error fallbacks
@@ -276,7 +277,7 @@ The conditional grammar (`==` / `<` / `>` / `in` / `and` / `or` / `not`) lives i
276
277
 
277
278
  When you reach for a primitive that isn't in the language (`|max` over an array, modular arithmetic, regex substitution, date math), the stopping rule isn't "feature shelved." It's: *can a tool do this work, and can the skill body invoke that tool via `$` or `shell`?* Almost always yes.
278
279
 
279
- **The universal computation escape is `shell + standard CLI tools.**
280
+ **The universal computation escape is `shell` + standard CLI tools.**
280
281
 
281
282
  ```
282
283
  shell(command="echo ${RAW|shell} | jq -r '[.weather[0].hourly[] | .chanceofrain | tonumber] | max'", unsafe=true) -> MAX_RAIN
@@ -418,20 +419,17 @@ Per-output-kind consumption semantics: presentation surfaces (`# Output: agent:
418
419
  ```
419
420
  notify(agent="oncall", message="Threshold breached at ${COUNT}")
420
421
  notify(agent="ops", message="ticket TR-1234 is a showstopper", event_type="ticket-911", correlation_id="${INCIDENT_ID}")
421
- notify(agent="cc@kitchen-terminal", message="look here now")
422
422
  ```
423
423
 
424
424
  Synchronous alert to a named agent via wired AgentConnector(s). **Contrast with `emit`:** `emit` accumulates into end-of-skill bulk delivery via the `# Output: agent: <name>` lifecycle hook; `notify` fires mid-execution to interrupt or page an agent before the skill completes.
425
425
 
426
- - `agent` — target agent id (required). **Address-routing**: a bare identifier (`"perry"`) routes to `AgentConnector.deliver()` (mailbox-class — the agent's inbox); an `agent@session` composite (`"cc@kitchen-terminal"`) routes to `AgentConnector.wake()` (session-targeted interrupt). The `@session` suffix IS the wake signal — there's no `wake=true` kwarg. Substrate sees the opaque composite on `wake()` and decomposes per the v0.18.2 contract.
427
- - `message` — alert body (optional; defaults to accumulated emissions so far). For wake-routed dispatches, the message rides as `WakeOpts.context` (preamble for the interrupt signal).
428
- - `event_type` — adopter-defined routing label (optional; flows to `DeliveryMeta.event_type` on deliver-class dispatches; ignored on wake-class since wake has no envelope).
429
- - `correlation_id` — reply-correlation id (optional; required for future `exchange()` / `request_response()` paths).
430
- - `connectors` — JSON array restricting which wired AgentConnector(s) receive the dispatch (optional).
431
-
432
- Returns ACK `{agent, dispatched: [{connector, ok, route?, error?}]}` — `route` is `"deliver"` or `"wake"` per address-routing. Fire-and-forget callers ignore the binding; check-delivery callers inspect ACK.
426
+ - `agent` — target agent id (required)
427
+ - `message` — alert body (optional; defaults to accumulated emissions so far)
428
+ - `event_type` — adopter-defined routing label (optional; flows to `DeliveryMeta.event_type`; overrides `# Event-type:` frontmatter)
429
+ - `correlation_id` — reply-correlation id (optional; required for future `exchange()` / `request_response()` paths)
430
+ - `connectors` — JSON array restricting which wired AgentConnector(s) receive the dispatch (optional)
433
431
 
434
- The same address rule applies to lifecycle hooks: `# Output: agent: perry` routes to deliver; `# Output: agent: cc@kitchen-terminal` routes to wake. See Output targets below.
432
+ Returns ACK `{agent, dispatched: [{connector, ok, error?}]}` fire-and-forget callers ignore the binding; check-delivery callers inspect ACK.
435
433
 
436
434
  ### `shell` — sandboxed or unsafe shell exec
437
435
 
@@ -1224,36 +1222,32 @@ Skills are orchestration, not computation. When the conditional logic feels Turi
1224
1222
 
1225
1223
  ## Triggers — # Triggers: header, declarative + imperative registration, source types
1226
1224
 
1227
- Triggers declare what events fire a skill autonomously. A skill without triggers must be invoked explicitly (via a compile/execute API call); a skill with triggers fires automatically when matching events occur.
1225
+ Triggers declare what fires a skill autonomously. A skill without triggers must be invoked explicitly (the compile/execute API); a skill with triggers fires automatically when its trigger condition occurs.
1228
1226
 
1229
- ## Declarative registration via `# Triggers:` header
1227
+ The trigger surface is **two sources: `cron` (time) and `event` (external signal)**. `session`, `agent-event`, `file-watch`, and `sensor` are not trigger sources — anything that isn't time-based is an external adapter that POSTs to the `/event` ingress. The runtime owns time (`cron`) and the event endpoint; source-specific watching (a file-watcher, a sensor stream, an agent-event bridge) is the adapter's job.
1230
1228
 
1231
- The skill body declares triggers via metadata header. Multiple triggers permitted, comma-separated or one per line.
1229
+ ## Declarative registration via `# Triggers:` header
1232
1230
 
1233
1231
  ```
1234
- # Triggers: cron: 0 8 * * *, session: start
1232
+ # Triggers: cron: 0 8 * * *
1233
+ # Triggers: event: deploy-finished
1235
1234
  ```
1236
1235
 
1237
- On skill write, the runtime's trigger registry parses the header and auto-registers each trigger. Editing the skill body updates registrations.
1236
+ The trigger registry parses the header and registers each trigger. A skill re-registering **its own** event_name is a silent upsert — the declarative re-save path stays friction-free.
1238
1237
 
1239
1238
  ## Imperative registration
1240
1239
 
1241
- For dynamic, one-shot, or runtime-decided triggers, use the imperative `registerTrigger` API:
1242
-
1243
1240
  ```
1244
1241
  registerTrigger({
1245
- skill_name: "my-skill",
1246
- source: "cron",
1247
- name: "55 2 * * *",
1248
- expires_at: 1779107400 // optional auto-cleanup
1242
+ skill_name: "notify-deploy",
1243
+ source: "event", // "cron" | "event"
1244
+ name: "deploy-finished" // cron expression for cron; the event_name for event
1249
1245
  })
1250
1246
  ```
1251
1247
 
1252
- Imperative triggers default to a 30-day expiration (cleanup via expiry sweep). Pass `null` for indefinite retention; author must clean up via the corresponding `unregisterTrigger` API.
1248
+ Imperative triggers default to 30-day expiry (`expires_at`); pass `null` for indefinite and clean up via `unregisterTrigger`.
1253
1249
 
1254
- ## Trigger sources
1255
-
1256
- ### `cron: <expression>` — time-based
1250
+ ## Source: `cron: <expr>` — time-based
1257
1251
 
1258
1252
  Standard 5-field cron. Sliding-window evaluation by a 30s poll loop. No catch-up replay if the runtime was down at fire time.
1259
1253
 
@@ -1261,184 +1255,212 @@ Standard 5-field cron. Sliding-window evaluation by a 30s poll loop. No catch-up
1261
1255
  # Triggers: cron: 0 3 * * *
1262
1256
  ```
1263
1257
 
1264
- ### `session: start | end` session lifecycle hooks
1258
+ ## Source: `event: <event_name>`external HTTP signal
1265
1259
 
1266
- Fires when an agent session begins (`session: start`) or ends (`session: end`). The load-bearing primitive for prepping context at session boundaries — a session-start skill produces `agent:` output that prepends to the next inference.
1260
+ A skill fires when an external caller POSTs its `event_name` to the runtime's `/event` ingress.
1267
1261
 
1268
- ```
1269
- # Triggers: session: start
1270
- # Output: agent: <agent-name>
1271
- ```
1262
+ **`event_name` is the public contract.**
1263
+ - Case-insensitive (normalized at register + lookup).
1264
+ - **1:1** one `event_name` → exactly one skill, unique per deployment.
1265
+ - Posters address the **event_name**, never the skill — so you can swap the skill behind an event without breaking callers, and no POST can reach an arbitrary skill.
1266
+ - **Rebind:** re-registering an already-bound `event_name` is allowed (last-write-wins). Same skill → silent upsert. A repoint to a **different** skill is **audit-logged** (`event_name 'X' rebound: skill A → skill B`), so a cross-skill takeover is never silent.
1272
1267
 
1273
- ### `event: <event-name>`runtime-host-emitted events (parse-only)
1268
+ **Params auto-derive from the skill's `# Vars:`.** You do NOT declare params separately on the trigger the event's accepted parameter set *is* the skill's declared `# Vars:`. A POST supplies values for those names. (Both the declarative and imperative registration paths derive params from `# Vars:`.)
1274
1269
 
1275
- Header parses cleanly today but the event bus that would emit `event:` triggers isn't wired yet. Cross-reference: see "Not yet implemented, but planned" at top.
1270
+ ```
1271
+ # Skill: notify-deploy
1272
+ # Vars: SERVICE="", VERSION=""
1273
+ # Triggers: event: deploy-finished
1274
+ ```
1275
+ → the `deploy-finished` event accepts params `SERVICE`, `VERSION`.
1276
1276
 
1277
- Example event categories (deployment-defined):
1278
- - `event: thread.replied` — a thread receives a new reply
1279
- - `event: mailbox.dangle` — an addressed item expires unprocessed
1280
- - `event: classifier.flagged` — a background classifier surfaces an urgent finding
1281
- - (extensible via runtime-host event registration)
1277
+ ## The `/event` HTTP ingress
1282
1278
 
1283
- ### `agent-event: <agent>.<event>` cross-agent event hooks (parse-only)
1279
+ - **Off by default.** Enable with `SKILLSCRIPT_EVENT_INGRESS_ENABLED=true`.
1280
+ - Runs on the **DashboardServer** (reuses the dashboard port — no second server).
1281
+ - **Binds localhost by default.** Reach beyond localhost only via explicit bind config.
1282
+ - **Optional bearer auth:** set `SKILLSCRIPT_EVENT_INGRESS_AUTH_TOKEN`; when set, every POST must carry `Authorization: Bearer <token>`.
1284
1283
 
1285
- Subscribes to another agent's events. Same parse-only status as `event:`.
1284
+ **Request:** `POST /event`, JSON body `{ "event_name": "...", "params": { ... } }`.
1286
1285
 
1287
- ```
1288
- # Triggers: agent-event: builder.task.completed
1289
- ```
1286
+ **Param validation (strict):** all declared params present, no unknown params, no defaults, no type coercion (JSON types pass through; a type mismatch surfaces at the consuming op, not at the ingress).
1290
1287
 
1291
- ### `file-watch: <path>` — filesystem change (parse-only)
1288
+ **Responses:**
1289
+ - `200` — accepted + queued. Body: `{ "run_id": "<trace_id>", "durability": "in-process" }`
1290
+ - `400` — missing or unknown params (body names the offending set)
1291
+ - `401` — missing/wrong bearer token (when auth is configured)
1292
+ - `404` — unknown `event_name`
1293
+ - `405` — wrong method
1292
1294
 
1293
- Fires when the named path changes. Relies on inotify (Linux) or kqueue (macOS) on the host.
1295
+ **Async semantics (important):** `200` means **accepted into the in-process queue, NOT skill-completed.** The skill runs asynchronously; the response never reflects skill success/failure. `durability: "in-process"` is self-describing: the queue is **best-effort, not durable across a restart** — the same property `cron` has (no catch-up replay). For at-least-once delivery the caller retries; durable queuing is not provided.
1294
1296
 
1295
- Open spec question: recursive vs directory-only default. Current lean: directory-only by default, opt-in via `file-watch-recursive:` or `file-watch: <path> (recursive)`.
1297
+ **`run_id` = `trace_id`.** The `run_id` in the 200 is the fire's `trace_id` — it round-trips to the fire record (the `fires` query / dashboard `/fires` view / the trace file on disk), so a caller can later check whether its fire completed.
1296
1298
 
1297
- ### `sensor: <sensor-name>`external sensor stream (parse-only)
1299
+ ## Canonical examplecalling `/event`
1298
1300
 
1299
- Extension surface for multimodal inputs — camera, microphone, presence, screen state. Designed as a category distinct from tools: sensors are continuous channels the agent reads but doesn't emit on. Privacy gating is a structural precondition.
1301
+ The `notify-deploy` skill above, fired by an external deploy pipeline:
1300
1302
 
1301
1303
  ```
1302
- # Triggers: sensor: presence
1304
+ curl -X POST http://localhost:7878/event \
1305
+ -H "Authorization: Bearer $SKILLSCRIPT_EVENT_INGRESS_AUTH_TOKEN" \
1306
+ -H "Content-Type: application/json" \
1307
+ -d '{"event_name":"deploy-finished","params":{"SERVICE":"api","VERSION":"1.2.3"}}'
1308
+ # → 200 {"run_id":"7f3c...","durability":"in-process"}
1303
1309
  ```
1304
1310
 
1305
- ## Trigger context
1311
+ **This is the pattern for every external trigger source.** A file-watcher, a sensor stream, an agent-event bridge, a webhook relay — each is just an external adapter that watches its source and POSTs to `/event`. (A file-watch adapter, e.g.: watch a path; on change, `POST {event_name, params:{path, kind}}`.)
1306
1312
 
1307
- When a skill fires from a trigger, the runtime populates ambient refs accessible inside the skill body:
1313
+ ## Trigger context
1308
1314
 
1309
- - `${TRIGGER_TYPE}` the trigger source (`cron`, `session`, etc.)
1315
+ When a skill fires from a trigger, the runtime populates ambient refs:
1316
+ - `${TRIGGER_TYPE}` — `cron` | `event`
1310
1317
  - `${TRIGGER_PAYLOAD}` — source-specific data
1311
- - `${EVENT.*}` event-payload fields for `event:` / `agent-event:` triggers
1318
+ - For event fires, the POSTed params land in the skill's vars (per `# Vars:`).
1312
1319
 
1313
1320
  ## Trigger lifecycle
1314
1321
 
1315
- - **Registration:** declarative via header (auto on skill write) or imperative via the `registerTrigger` API
1316
- - **Storage:** registered triggers are records owned by the registering agent, indexed by source + name + agent_id + skill_id; the storage backend is connector-defined
1317
- - **Inspection:** `listTriggers({ skill_name?, agent_id?, source? })` returns the live registry
1318
- - **Archival:** `unregisterTrigger(trigger_id)` archives the trigger (audit trail preserved); declarative triggers are removed by editing the skill body to drop the declaration
1322
+ - **Register:** declarative (auto on skill write) or imperative (`registerTrigger`).
1323
+ - **Inspect:** `listTriggers({ skill?, source? })`.
1324
+ - **Remove:** `unregisterTrigger(trigger_id)` (archives; audit trail preserved). Declarative triggers are removed by editing the `# Triggers:` line out of the skill body.
1319
1325
 
1320
1326
  ## Multiple triggers
1321
1327
 
1322
- A skill may declare multiple triggers; each fires an independent execution. The compiled output is identical regardless of trigger; the runtime distinguishes via `${TRIGGER_TYPE}`.
1323
-
1324
- Open spec question: dedup on near-simultaneous fires. If `cron: 0 8 * * *` and `event: user.present` both fire within seconds, the runtime currently runs the skill twice (one per trigger). Author dedups via state if needed.
1328
+ A skill may declare multiple triggers; each fires an independent execution. The compiled output is identical regardless of trigger; the runtime distinguishes via `${TRIGGER_TYPE}`. Near-simultaneous dedup is the author's responsibility (via state).
1325
1329
 
1326
1330
  ## Output targets — # Output: header, delivery kinds
1327
1331
 
1328
- The `# Output:` header declares where a skill's result is delivered. Default behavior (no header) is `text` — return string to caller.
1332
+ A skill's **body text is its output template**, and the `# Output:` header declares where that output is delivered. Default delivery (no `# Output:` header) is `text` — returned to the caller.
1329
1333
 
1330
- ## Output kinds
1334
+ ## Two ways to produce output: the template vs `emit` — and when to use each
1331
1335
 
1332
- ### `text` (default, bare-only)
1336
+ A skill produces output two ways. Choosing the right one is the difference between a clean skill and a confusing one:
1337
+
1338
+ - **Body-text template (declarative) — for fixed-shape output.** Write the output literally in the body, interpolating `${vars}`. Use it when you know the output's shape and are just filling in values.
1339
+ - **`emit(...)` (imperative) — for output whose shape or count is decided at runtime.** `emit` is NOT deprecated; it's the right tool for:
1340
+ 1. **Variable-cardinality output** — `foreach IT in ${LIST}: emit(...)` produces a number of lines decided at runtime, which a single template can't express.
1341
+ 2. **Conditional whole-output** — branches that produce genuinely different output *shapes* (not merely different values).
1342
+ 3. **Reasoning / transcript trace** — `emit` always writes to the transcript channel for debugging.
1343
+
1344
+ **Rule of thumb:** if you can write the output as one block of text with `${vars}` filled in, use the template. If the output's *shape or count* varies at runtime, use `emit`.
1345
+
1346
+ **They are complementary channels, not competitors.** When a body template is present, it owns the *canonical output*; `emit` always feeds the *transcript* channel. A skill may use both — a clean declarative output plus a rich transcript — with no conflict.
1333
1347
 
1334
- Returns the skill's result as a string to whatever invoked the skill via API or read the compiled prompt artifact. Bare-only no target accepted; parse error if a target is supplied.
1348
+ ## The body-text output template
1349
+
1350
+ Body text — at the top, the bottom, or between targets — is the output template (it may appear **anywhere a target doesn't**; see Grammar). It interpolates, at runtime:
1351
+ - `# Vars:` declared inputs, and
1352
+ - any variable bound by the body (`$set`, `-> VAR`).
1335
1353
 
1336
1354
  ```
1337
- # Output: text
1355
+ # Skill: get-weather
1356
+ # Vars: LOCATION="", UNITS="imperial"
1357
+
1358
+ ${AREA}: ${TEMP}${UNIT} and ${DESC}. High ${HIGH}${UNIT}, low ${LOW}${UNIT}.
1359
+
1360
+ fetch:
1361
+ shell(command="curl -s --max-time 10 https://wttr.in/${LOCATION|url}?format=j1") -> RAW
1362
+ $ json_parse ${RAW} -> W
1363
+ $set AREA = "${W.nearest_area.0.areaName.0.value}"
1364
+ $set DESC = "${W.current_condition.0.weatherDesc.0.value|trim}"
1365
+ if ${UNITS} == "metric":
1366
+ $set TEMP = "${W.current_condition.0.temp_C}" $set UNIT = "°C" # + HIGH/LOW
1367
+ else:
1368
+ $set TEMP = "${W.current_condition.0.temp_F}" $set UNIT = "°F" # + HIGH/LOW
1369
+ default: fetch
1338
1370
  ```
1371
+ Renders to the canonical output: `Valdese: 81°F and Sunny. High 93°F, low 64°F.` — no `emit`.
1339
1372
 
1340
- ### `agent: <agent-name>` augmenting context to a named agent
1373
+ **Lint:** every `${var}` in the template must be a `# Vars:` input or bound by the body. An unbound reference is a tier-1 error (`unset-template-var`). The coupling is mechanical (variable names), so it is checkable.
1341
1374
 
1342
- The Augmenting-kind delivery. Output prepends to the named agent's next-turn prompt context as augment-kind payload.
1375
+ **Branching stays single-shape:** resolve a fork *inside the block* into vars (metric/imperial `TEMP`/`UNIT`) so the template is one shape. Whole-output conditionals are an `emit` job, not a template job.
1343
1376
 
1377
+ **A body-template-only skill is valid.** A skill that is just a template + inputs, with **no compute block / no target**, compiles and runs — the template alone is the skill. `hello-world` is exactly this:
1344
1378
  ```
1345
- # Output: agent: <agent-name>
1346
- # Output: agent: cc@kitchen-terminal
1379
+ # Skill: hello-world
1380
+ # Status: Approved
1381
+ # Vars: WHO=world
1382
+
1383
+ Hello, ${WHO}!
1347
1384
  ```
1385
+ No target required. (A tier-3 `body-template-detected` advisory fires on a static template with no `${...}` interpolations and no text-consuming `# Output:` — a gentle "is this output, or did you mean a `#` comment?" intent check; harmless on a genuinely-static line.)
1348
1386
 
1349
- Used to bring an agent into the next turn pre-shaped — context that would normally require a session-start retrieval is pre-positioned. Wired end-to-end via the runtime host's prompt-prepend surface + a synchronous trigger-fire endpoint with timeout-fallback so the next-turn dispatch isn't blocked on slow skill execution.
1387
+ ## `# Output:` delivery kinds
1350
1388
 
1351
- **Address-routing** same rule as `notify()`. A bare `<agent-name>` routes to `AgentConnector.deliver()` (mailbox-class). An `<agent>@<session>` composite routes to `AgentConnector.wake()` (session-targeted interrupt); the joined emit stream rides as `WakeOpts.context`. The `@session` suffix IS the wake signal — no separate `wake=true` form.
1389
+ The body template (or, absent one, the emit/`-> VAR` result) is delivered according to the `# Output:` kind:
1352
1390
 
1353
- ### `template: <agent-name>` — playbook delivered to a named agent
1391
+ ### `text` (default, bare-only)
1392
+ Returns the result as a string to whatever invoked the skill via API or read the compiled artifact. Bare-only — a target is a parse error.
1393
+ ```
1394
+ # Output: text
1395
+ ```
1354
1396
 
1355
- The Template-kind delivery. Output renders as a playbook the named agent executes itself — the runtime doesn't dispatch the ops, it hands the agent a recipe to follow.
1397
+ ### `agent: <agent-name>` augmenting context to a named agent
1398
+ The output prepends to the named agent's next-turn prompt context as augment-kind payload — bringing an agent into the next turn pre-shaped. Wired via the runtime host's prompt-prepend surface + a synchronous trigger-fire endpoint with timeout-fallback so next-turn dispatch isn't blocked on slow execution.
1399
+ ```
1400
+ # Output: agent: <agent-name>
1401
+ ```
1356
1402
 
1403
+ ### `template: <agent-name>` — playbook delivered to a named agent
1404
+ The output renders as a playbook the named agent executes itself — the runtime hands over a recipe rather than dispatching the ops.
1357
1405
  ```
1358
1406
  # Output: template: <agent-name>
1359
- # Output: template: cc@browser-tab-3
1360
1407
  ```
1361
1408
 
1362
- Used for reusable recipes: a skill that, when compiled, produces instructions another agent follows.
1363
-
1364
- **Address-routing**: same as `agent:` above. Bare → deliver(); `@session` → wake().
1365
-
1366
1409
  ### `file: <path>` — write to file
1367
-
1368
- Header parses; file router not yet implemented. See "Not yet implemented, but planned" at top.
1410
+ Header parses; file router not yet implemented. See "Not yet implemented, but planned."
1369
1411
 
1370
1412
  ### `none` (bare-only)
1371
-
1372
- Side-effects only — the skill's purpose is the writes / shell ops it performs, not the returned value. Bare-only; parse error if a target is supplied.
1373
-
1413
+ Side-effects only — the skill's purpose is its writes / shell ops, not a returned value. Bare-only.
1374
1414
  ```
1375
1415
  # Output: none
1376
1416
  ```
1377
1417
 
1378
1418
  ## Multiple output targets
1379
-
1380
- A skill may declare multiple output targets, one per line. Each target receives the same content.
1381
-
1419
+ A skill may declare multiple output targets, one per line; each receives the same content.
1382
1420
  ```
1383
1421
  # Output: agent: ops-channel
1384
1422
  # Output: agent: assistant
1385
1423
  ```
1386
1424
 
1387
- A morning-brief skill, for example, can deliver to a team-channel agent and to an assistant agent's session-start prompt context simultaneously.
1388
-
1389
- ## Skill categories — Augmenting / Template / Headless
1390
-
1391
- The output kind declaration determines the skill's category for discovery purposes (see SkillStore `skill_list` discovery surface):
1392
-
1393
- | Category | Determined by | Discovery group |
1394
- |---|---|---|
1395
- | **Augmenting** | Has `# Output: agent: <name>` declared | `receives` |
1396
- | **Template** | Has `# Output: template: <name>` declared OR no agent/template output but agent-invokable (no triggers) | `skills` |
1397
- | **Headless** | Output is `text` / `file:` / `none` AND has autonomous triggers | `headless` (filtered out of default agent discovery) |
1398
-
1399
- The derivation: ANY `output.kind === "agent"` → Augmenting; else ANY `output.kind === "template"` → Template; else if no autonomous triggers → Template (agent-invokable inference); else → Headless.
1400
-
1401
- Agent discovery via `skill_list()` defaults to `receives` + `skills` groups. Headless skills are filtered out of the default view (admin views can opt in via `filter: { audience: "all" }`).
1402
-
1403
1425
  ## Per-kind output value semantics
1404
1426
 
1405
- Different output kinds consume the skill's execution result differently:
1427
+ What each kind consumes as the output, in precedence order:
1428
+ - **Presentation surfaces** (`agent:`, `template:`): the body template if present; otherwise joined emissions (all `emit()` ops concatenated in execution order).
1429
+ - **Programmatic surfaces** (`text`, `file:`): the body template if present; otherwise the `lastBoundVar` (most recent `-> VAR`).
1406
1430
 
1407
- - **Presentation surfaces** (`agent:`, `template:`) consume joined emissions all `emit()` ops in the skill body concatenated in execution order
1408
- - **Programmatic surfaces** (`text`, `file:`) consume the `lastBoundVar` — the most recently bound `-> VAR` value from any op
1431
+ A present body template is the canonical output for every kind; `emit` output continues to populate the transcript independently. Single source of truth in the executor's `perKindOutput()`; routers stay dumb. (Note: the `lastBoundVar` fallback is forgiving — a skill that binds a value but writes no template still outputs that last value — but it *masks* a forgotten template; prefer an explicit template when the output is fixed-shape.)
1409
1432
 
1410
- Single source of truth in the executor's `perKindOutput()` function; routers stay dumb (just consume what the executor hands them per kind).
1433
+ ## Skill categories Augmenting / Template / Headless
1411
1434
 
1412
- ## Augmenting / Template companion header
1435
+ The output kind determines the skill's discovery category (see `skill_list`):
1413
1436
 
1414
- Skills with `agent:` or `template:` output kinds can declare a companion header that rides along with the delivery payload. Optional; has no effect on Headless skills.
1437
+ | Category | Determined by | Discovery group |
1438
+ |---|---|---|
1439
+ | **Augmenting** | Has `# Output: agent: <name>` | `receives` |
1440
+ | **Template** | Has `# Output: template: <name>`, OR no agent/template output but agent-invokable (no triggers) | `skills` |
1441
+ | **Headless** | Output is `text` / `file:` / `none` AND has autonomous triggers | `headless` (filtered from default agent discovery) |
1415
1442
 
1416
- ### `# Event-type: <string>`
1443
+ Derivation: ANY `output.kind === "agent"` → Augmenting; else ANY `template` → Template; else no autonomous triggers → Template; else → Headless. `skill_list()` defaults to `receives` + `skills`; headless is opt-in via `filter:{audience:"all"}`.
1417
1444
 
1418
- Adopter-defined routing vocabulary; flows to `DeliveryMeta.event_type` on lifecycle-hook deliveries as the frontmatter fallback. `notify(event_type=...)` kwarg takes precedence per-emit.
1445
+ ## Augmenting / Template companion header `# Event-type:`
1419
1446
 
1447
+ Skills with `agent:`/`template:` output may declare a routing tag that rides with the delivery payload (no effect on Headless):
1420
1448
  ```
1421
1449
  # Output: agent: perry
1422
1450
  # Event-type: ticket-911
1423
1451
  ```
1424
-
1425
- The receiving agent reads `event_type` for routing decisions ("this is a 911 — surface immediately" vs "this is a routine check — fold into next brief").
1426
-
1427
- ### Lint coverage
1428
-
1429
- A tier-2 lint rule `unused-augmenting-header` fires when `# Event-type:` appears on a Headless skill (no `agent:` or `template:` output declared). Headless skills have no AgentConnector dispatch path, so the header would silently no-op — the lint warns the author to either change the output kind or remove the header.
1430
-
1431
- Legacy `# Delivery-context:` header was renamed to `# Event-type:` for vocab consistency. A tier-2 advisory `legacy-frontmatter-header` fires on the legacy form with a rename suggestion.
1452
+ The receiving agent reads `event_type` for routing ("911 — surface now" vs "routine — fold into next brief"). Flows to `DeliveryMeta.event_type`; `notify(event_type=...)` takes precedence per-emit. A tier-2 lint `unused-augmenting-header` fires when `# Event-type:` appears on a Headless skill. (Legacy `# Delivery-context:` → renamed `# Event-type:`; tier-2 `legacy-frontmatter-header` advisory on the old form.)
1432
1453
 
1433
1454
  ## Grammar
1434
1455
 
1435
- - Kinds with no target (`text`, `none`) are bare-only — `# Output: text` is valid, `# Output: text: anything` is a parse error.
1436
- - Kinds with a target (`agent`, `template`, `file`) require `<kind>: <target>` `# Output: agent` without a target is a parse error.
1437
- - Authoring friction-fix: parse errors on bare-only kinds suggest the corrected shape inline.
1456
+ - Bare-only kinds (`text`, `none`): `# Output: text` valid; `# Output: text: anything` is a parse error.
1457
+ - Targeted kinds (`agent`, `template`, `file`): require `<kind>: <target>`; bare form is a parse error.
1458
+ - **Template/target boundary (position-free):** a **target** is a `<name>:` line followed by an indented op-block. **Any other line** — including a `word: value` line with no op-block beneath it (`Summary: hot today`) — is **template text**, wherever it sits (top, bottom, or between targets). So the output template can go where the author's instinct puts it. Declaring **two separate template regions** in one skill is an error — pick one location.
1459
+ - Parse errors on bare-only kinds suggest the corrected shape inline.
1438
1460
 
1439
1461
  ## Output routing failures
1440
1462
 
1441
- If `# Output: agent: <name>` fires and the wired AgentConnector throws, the delivery routes through `else:` / `# OnError:` machinery. The receipt surface (`agent_delivery_receipts[]`) records the failure for the scheduler to log.
1463
+ 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.
1442
1464
 
1443
1465
  ## Lifecycle and status — # Status: header, six canonical states, compile + runtime enforcement
1444
1466
 
@@ -1966,16 +1988,18 @@ Skill discovery via `skill_list()` (see SkillStore docs) closes the related visi
1966
1988
 
1967
1989
  Design rationale for planned features. The user-facing list of "what's coming" lives in the "Not yet implemented, but planned" section at top; this section documents the *why* behind each planned addition. When the language extends, the relevant grammar moves into its canonical section (Ops reference, Variables, Triggers, etc.) and the design-rationale entry here is replaced with a cross-reference.
1968
1990
 
1969
- ## Sensors as a language category
1991
+ ## Sensors as a read-channel category
1970
1992
 
1971
- Currently `# Triggers:` includes `sensor:` as a trigger source. The planned redesign splits sensors into their own category:
1993
+ > `sensor:` is **not a trigger source** — to *fire* a skill on a sensor signal, a sensor adapter POSTs to the `/event` ingress like any other external source (see Triggers). What remains a future idea is the separate concept below: sensors as a continuous **read channel**, decoupled from triggering.
1994
+
1995
+ The read-channel idea: a skill declares sensors it reads, distinct from what fires it:
1972
1996
 
1973
1997
  ```
1974
1998
  # Sensors: presence, screen-state, voice-prosody
1975
1999
  # Triggers: cron: 0 8 * * *
1976
2000
  ```
1977
2001
 
1978
- **Distinction:** Sensors are continuous channels the agent reads but doesn't emit on. Triggers are discrete events that fire the skill. Conflating them in one header produces a worse language for both — sensors need different semantics (continuous read, accessible via ambient refs, privacy-gated) than triggers (discrete fire, dispatch semantics).
2002
+ **Distinction:** Sensors (read-channel) are continuous values the agent reads but doesn't emit on. Triggers are discrete signals that fire the skill. The two are different semantics — sensors need continuous read, ambient-ref access, and privacy gating; triggers need discrete fire + dispatch. (Conflating them in one `sensor:` trigger source was the old design; the trigger half folds into `event`, the read-channel half stays future.)
1979
2003
 
1980
2004
  Pending: ambient refs for sensor values (`${SENSOR.presence}`, `${SENSOR.voice-prosody.affect}`) and the privacy-gating discipline that determines when a sensor is readable.
1981
2005
 
@@ -1999,7 +2023,7 @@ Different shape from event triggers — "fire if user hasn't messaged in N minut
1999
2023
  # Triggers: idle: 5m
2000
2024
  ```
2001
2025
 
2002
- Runtime tracks the relevant idleness counter and fires when the threshold crosses. Separate dispatch mechanism from event triggers.
2026
+ Runtime tracks the relevant idleness counter and fires when the threshold crosses. Would be a **new trigger source beyond the current `cron`|`event`** — or, absent a new source, an adapter that POSTs to `/event` on a timer. Separate dispatch mechanism from event triggers either way.
2003
2027
 
2004
2028
  ## Time-windowed aggregation
2005
2029
 
@@ -2132,7 +2156,7 @@ Runtime fails-fast on missing capabilities. Trust precondition for sensor work
2132
2156
  ## Build order rationale
2133
2157
 
2134
2158
  Some features depend on others:
2135
- - Suppression + persistent state should land before sensors (sensor work would compound problems without them)
2159
+ - Suppression + persistent state should land before the sensor read-channel (sensor work would compound problems without them)
2136
2160
  - Pub-sub needs sensors producing traffic before it has anything to route
2137
2161
  - Introspection is ergonomic, not foundational — useful but skippable
2138
2162
  - Capability declarations are the trust gate that makes sensor + privacy work socially defensible
@@ -2158,33 +2182,31 @@ Should `${ERROR}` be an ambient ref inside `else:` blocks, populated with the er
2158
2182
 
2159
2183
  ## 3. Multiple triggers — concurrency
2160
2184
 
2161
- If `cron: 0 8 * * *` and `event: user.present` both fire within seconds, does the skill run twice (independent) or get deduped? Lean: independent. Author dedups via state if needed. Affects dispatch layer.
2185
+ If `cron: 0 8 * * *` and `event: user-present` both fire within seconds, does the skill run twice (independent) or get deduped? Lean: independent. Author dedups via state if needed. Affects dispatch layer.
2162
2186
 
2163
2187
  ## 4. `execute_skill()` invocation vs trigger firing
2164
2188
 
2165
2189
  When skill A invokes skill B via `execute_skill()`, do skill B's `# Triggers:` fire? Almost certainly no — `execute_skill()` is direct invocation, distinct from the trigger event surface. Worth saying explicitly.
2166
2190
 
2167
- ## 5. File-watch path semantics
2168
-
2169
- Recursive or directory-only by default? Inotify supports both. Lean: directory-only default; offer recursive via `file-watch-recursive:` or `file-watch: <path> (recursive)`. Affects dispatch layer when the file-watch trigger source actually fires (currently parse-only).
2170
-
2171
- ## 6. Output target delivery failures
2191
+ ## 5. Output target delivery failures
2172
2192
 
2173
2193
  If a delivery target is unreachable when the skill fires, what happens? Lean: delivery failure is its own retryable error; queue if possible, else error to caller. Worth a separate small spec section. Affects dispatch layer.
2174
2194
 
2175
- ## 7. Skill versioning rollback UX
2195
+ ## 6. Skill versioning rollback UX
2176
2196
 
2177
2197
  Edits via upsert preserve history through substrate versioning (SqliteSkillStore's `skill_versions` table, FilesystemSkillStore's git history), but no first-class "rollback" affordance. Probably needs a `--version <N>` flag on the compile API or a sister tool.
2178
2198
 
2179
- ## 8. Connector capability declarations
2199
+ ## 7. Connector capability declarations
2180
2200
 
2181
2201
  Skills can declare required connector capabilities via `# Requires:` for var resolution. Extending this to "needs semantic search" / "needs structured-extraction model with 32K context" capabilities would be useful for the substrate-portable story. Pending design.
2182
2202
 
2183
- ## 9. Per-op timeouts
2203
+ ## 8. Per-op timeouts
2204
+
2205
+ Hung dispatches hang the skill without explicit timeout configuration. Lean: skill-level `# Timeout:` header + per-op `timeout=N` kwarg + runtime defaults. Pending implementation; see "Not yet implemented, but planned" at top.
2184
2206
 
2185
- Hung dispatches hang the skill without explicit timeout configuration. Lean: skill-level `# Timeout:` header + per-op `timeoutSeconds=N` kwarg + runtime defaults. Pending implementation; see "Not yet implemented, but planned" at top.
2207
+ *(The former "file-watch path semantics" question was removed `file-watch` is no longer a trigger source; filesystem watching is now an external adapter that POSTs to the `/event` ingress, so there are no in-language file-watch path semantics to settle.)*
2186
2208
 
2187
2209
  ---
2188
2210
 
2189
- *Rendered from `skillscript/skillscript-language-reference` — 2026-06-04 11:28 EDT*
2190
- *Source of truth: AMP (`amp_render_document("skillscript/skillscript-language-reference")`)*
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")`)*
@@ -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-08T18:14:44.579Z",
5
+ "compiled_at": "2026-06-09T13:43:10.260Z",
6
6
  "source_skill": {
7
7
  "name": "hello-world"
8
8
  },