skillscript-runtime 0.19.16 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +11 -1
  2. package/dist/approval.d.ts +80 -54
  3. package/dist/approval.d.ts.map +1 -1
  4. package/dist/approval.js +156 -120
  5. package/dist/approval.js.map +1 -1
  6. package/dist/bootstrap.d.ts +41 -0
  7. package/dist/bootstrap.d.ts.map +1 -1
  8. package/dist/bootstrap.js +59 -2
  9. package/dist/bootstrap.js.map +1 -1
  10. package/dist/cli.js +245 -24
  11. package/dist/cli.js.map +1 -1
  12. package/dist/compile.js +7 -0
  13. package/dist/compile.js.map +1 -1
  14. package/dist/composition.d.ts.map +1 -1
  15. package/dist/composition.js +13 -1
  16. package/dist/composition.js.map +1 -1
  17. package/dist/connectors/skill-store.d.ts.map +1 -1
  18. package/dist/connectors/skill-store.js +41 -13
  19. package/dist/connectors/skill-store.js.map +1 -1
  20. package/dist/connectors/sqlite-skill-store.d.ts.map +1 -1
  21. package/dist/connectors/sqlite-skill-store.js +21 -12
  22. package/dist/connectors/sqlite-skill-store.js.map +1 -1
  23. package/dist/dashboard/spa/app.js +186 -8
  24. package/dist/dashboard/spa/index.html +2 -0
  25. package/dist/dashboard/spa/styles.css +42 -0
  26. package/dist/errors.d.ts +25 -0
  27. package/dist/errors.d.ts.map +1 -1
  28. package/dist/errors.js +49 -0
  29. package/dist/errors.js.map +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +2 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/lint.d.ts.map +1 -1
  35. package/dist/lint.js +24 -7
  36. package/dist/lint.js.map +1 -1
  37. package/dist/mcp-server.d.ts +3 -0
  38. package/dist/mcp-server.d.ts.map +1 -1
  39. package/dist/mcp-server.js +19 -2
  40. package/dist/mcp-server.js.map +1 -1
  41. package/dist/parser.js +2 -2
  42. package/dist/parser.js.map +1 -1
  43. package/dist/runtime-config.d.ts +6 -0
  44. package/dist/runtime-config.d.ts.map +1 -1
  45. package/dist/runtime-config.js +8 -0
  46. package/dist/runtime-config.js.map +1 -1
  47. package/dist/runtime-env-resolver.d.ts +4 -0
  48. package/dist/runtime-env-resolver.d.ts.map +1 -1
  49. package/dist/runtime-env-resolver.js +27 -0
  50. package/dist/runtime-env-resolver.js.map +1 -1
  51. package/dist/runtime.d.ts +18 -0
  52. package/dist/runtime.d.ts.map +1 -1
  53. package/dist/runtime.js +84 -17
  54. package/dist/runtime.js.map +1 -1
  55. package/dist/safe-path.d.ts +22 -0
  56. package/dist/safe-path.d.ts.map +1 -1
  57. package/dist/safe-path.js +72 -1
  58. package/dist/safe-path.js.map +1 -1
  59. package/dist/scheduler.d.ts +3 -0
  60. package/dist/scheduler.d.ts.map +1 -1
  61. package/dist/scheduler.js +8 -0
  62. package/dist/scheduler.js.map +1 -1
  63. package/docs/adopter-playbook.md +75 -25
  64. package/docs/configuration.md +5 -1
  65. package/docs/connector-contract-reference.md +5 -1
  66. package/docs/language-reference.md +83 -130
  67. package/docs/sqlite-skill-store.md +5 -1
  68. package/examples/skillscripts/classify-support-ticket.skill.md +1 -1
  69. package/examples/skillscripts/data-store-roundtrip.skill.md +1 -1
  70. package/examples/skillscripts/doc-qa-with-citations.skill.md +1 -1
  71. package/examples/skillscripts/feedback-sentiment-scan.skill.md +1 -1
  72. package/examples/skillscripts/hello-world.skill.md +1 -1
  73. package/examples/skillscripts/hello-world.skill.provenance.json +1 -1
  74. package/examples/skillscripts/morning-brief.skill.md +1 -1
  75. package/examples/skillscripts/queue-length-monitor.skill.md +1 -1
  76. package/examples/skillscripts/service-health-watch.skill.md +1 -1
  77. package/examples/skillscripts/skill-store-roundtrip.skill.md +1 -1
  78. package/examples/skillscripts/youtrack-morning-sweep.skill.md +1 -1
  79. package/package.json +1 -1
  80. package/scaffold/skills/data-store-roundtrip.skill.md +1 -1
  81. package/scaffold/skills/hello-world.skill.md +1 -1
  82. package/scaffold/skills/skill-store-roundtrip.skill.md +1 -1
@@ -1,4 +1,8 @@
1
- # Adopter playbook
1
+ ---
2
+ title: Adopter Playbook
3
+ description: "Stand up the runtime, wire your substrates, supervise it as a service, and point your agent at it over MCP."
4
+ mode: wide
5
+ ---
2
6
 
3
7
  How to wire skillscript-runtime into your deployment. Written for Joe-Programmer: you have your own substrate stack (data store, agent harness, LLM endpoint, filesystem), and you want skillscript to slot in rather than dictate.
4
8
 
@@ -204,8 +208,10 @@ Create `skillscript.config.json` in your `$SKILLSCRIPT_HOME`:
204
208
 
205
209
  **For custom substrates**: write your own bootstrap. See `examples/custom-bootstrap.example.ts` and `examples/onboarding-scaffold/bootstrap.ts` for complete worked walkthroughs.
206
210
 
207
- Two security knobs that adopters wiring real substrates should know about:
211
+ Security knobs that adopters wiring real substrates should know about:
208
212
 
213
+ - **The approval boundary (secured mode).** `SKILLSCRIPT_SECURED_MODE=true` enforces that only key-signed skills perform effects. Default-deny by design — leave it off only for trusted-author / single-operator setups. Full detail, the approve flow, and key custody: [Approval + secured mode](#approval--secured-mode).
214
+ - **Filesystem path allowlist.** `SKILLSCRIPT_FS_ALLOWLIST` is default-deny — `file_read` / `file_write` refuse every path until you wire roots. See [Filesystem path allowlist](#filesystem-path-allowlist). (Keep your approval-key directory out of it.)
209
215
  - **Per-connector tool allowlists** — `allowed_tools` on each `connectors.json` MCP connector entry restricts which tools that connector can dispatch. Three-state (`undefined` = allow all, `[]` = allow none, listed = exactly those). Tier-1 `disallowed-tool` lint + runtime defense-in-depth refuse out-of-list dispatch. **`allowed_tools` belongs at the entry top-level**, sibling to `class` and `config` — NOT inside the `config` block. The loader hard-errors on misplacement (placing it inside `config:` would silently allow-all every tool — the worst-case failure mode for a security control). See `docs/configuration.md` §"Named MCP connector instances" for the JSON shape.
210
216
  - **Shell-execution discipline** — `shell(command="...")` runs structured-spawn by default (binary on PATH, whitespace-tokenized argv, no bash). `shell(command="...", unsafe=true)` opts into bash interpretation (pipes, `$VAR`, command substitution) and refuses to fire unless the runtime is configured with `enable_unsafe_shell = true` in `config.toml`. Lint flags every `unsafe=true` op tier-2 to keep audit posture visible. See `scaffold/config.toml` for the documented default + `help({topic:"lint-codes"})` for the `unsafe-shell-disabled` rule.
211
217
 
@@ -342,6 +348,49 @@ With the service running, point your agent at the runtime's `/rpc` endpoint.
342
348
  - **Port matches what the runtime binds.** Default `7878` (`SKILLSCRIPT_PORT` env var / `dashboard.port` config). Two-instance setups (dev + adopter side-by-side per §5) need distinct ports — e.g., adopter on `7879`. Whatever the supervisor unit specifies is what the agent config must point at.
343
349
  - **Auth + bind address.** `/rpc` has no auth and binds to `127.0.0.1` by default — the right posture for a local agent talking to a local runtime. If you set `SKILLSCRIPT_HOST=0.0.0.0` to reach the runtime across the network, front it with your own auth layer (reverse proxy + bearer token, mTLS, etc.) — same posture as the `/event` HTTP ingress, which has its own bearer-token gate (`SKILLSCRIPT_EVENT_INGRESS_AUTH_TOKEN`).
344
350
 
351
+ ## Approval + secured mode
352
+
353
+ Every skill carries a lifecycle status — `Draft → Approved → Disabled`. **Only `Approved` skills execute**; `Draft` is the safety gate (a skill under authoring or review), `Disabled` is retired. That status gate applies in *both* runtime modes. What the mode decides is whether an `Approved` skill must additionally be **cryptographically keyed**:
354
+
355
+ | | Unsecured | Secured (`SKILLSCRIPT_SECURED_MODE=true`) |
356
+ |---|---|---|
357
+ | `Draft` skill | refused | refused |
358
+ | `Approved` skill | runs — a bare `# Status: Approved` is sufficient (unkeyed) | runs **only** if the body carries a valid operator signature |
359
+ | Tamper-evidence | none (an Approved body can be edited freely) | yes — editing the body breaks the signature |
360
+ | Effectful ops (`$`, `shell`, `file_*`, `notify`) | run | **refused unless the skill is signed** (the `effectsAuthorized` capability gate) |
361
+ | Who can grant approval | anyone who can set the status | only a holder of the operator's private key |
362
+
363
+ **Unsecured** is the trusted-author / single-operator convenience posture: you trust whoever writes skills, the status flag is the only barrier. **Secured** is the enforced boundary — an unapproved or tampered skill cannot perform a single effectful op, regardless of how it's dispatched (CLI, cron, `/event`, MCP, in-skill composition). Unsecured is the current default; secured is the recommended posture for any multi-author or networked deployment.
364
+
365
+ > The shell + filesystem allowlists (below) are **always enforced, in both modes** — they bound *what* a running skill may touch. Secured mode adds the orthogonal boundary on *whether an unapproved skill runs at all*.
366
+
367
+ ### Asymmetric signing — how secured approval works
368
+
369
+ Approval is an Ed25519 signature, not a shared secret:
370
+
371
+ - **Private key** — held by the operator, read **only** by the approve flow (`skillfile approve`). The runtime never loads it. For full isolation, 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 still works, but isn't fully isolated (a documented 1.0 precondition; managed custody closes it later).
372
+ - **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
+
374
+ 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).
375
+
376
+ ### The approve flow
377
+
378
+ | Action | How |
379
+ |---|---|
380
+ | Approve one skill (review the body, then sign) | `skillfile approve <name>` |
381
+ | Batch-approve everything pending | `skillfile reapprove --apply` (dry-run preview: `skillfile reapprove`) |
382
+ | Approve from the dashboard | the **Approvals** queue — review the body + security signals, then run the emitted command at your terminal |
383
+
384
+ The dashboard is a **review** surface, not a signing surface: it surfaces the pending queue and per-skill security-signal summary, but signing happens at your terminal where the private key lives (the network-facing runtime never holds it). `skill_write` / `skill_status` **cannot** grant approval in secured mode — an agent-written or status-flipped skill lands `Draft` until a key-holder signs it. (In unsecured mode, `skill_status → Approved` is honored directly, unkeyed.)
385
+
386
+ ### Migrating an existing skill set
387
+
388
+ Turning secured mode on means any skill that's `Approved` without a valid signature is refused at execution. `skillfile reapprove` sweeps the store, reports the set needing re-blessing, and `--apply` re-signs them in one pass — so you don't run `skillfile approve` once per skill.
389
+
390
+ ### Bundled demos
391
+
392
+ Bundled example skills ship as `# Status: Draft` — a signature baked at package-build time could never validate on your install (the key is per-operator). `skillfile init` locally approves the three seeded demos with *your* machine's authority (secured → provision keypair + sign; unsecured → bare Approved), so they're runnable immediately after init.
393
+
345
394
  ## Shell binary allowlist
346
395
 
347
396
  **The runtime enforces a default-deny operator allowlist for binaries reachable via `shell(...)` ops.** Skill authors are agents, agents are a weak trust anchor (hallucination, prompt-injection, no human-in-loop at scale), and operator-side scoping converts "a human reviews every skill" from discipline into an enforced constraint at the language level.
@@ -502,6 +551,25 @@ Per-skill capability declaration: skills declare what shell binaries they need i
502
551
 
503
552
  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.
504
553
 
554
+ ## Filesystem path allowlist
555
+
556
+ **The runtime enforces a default-deny operator allowlist for paths reachable via `file_read(...)` / `file_write(...)` ops** — the third operator boundary, mirroring the shell allowlist with the same rationale (skill authors are agents; the operator scopes which parts of the filesystem skills may touch).
557
+
558
+ | Operator switch | Controls |
559
+ |---|---|
560
+ | `SKILLSCRIPT_FS_ALLOWLIST` | Comma-separated roots under which `file_read` / `file_write` may operate |
561
+
562
+ - **Default-deny.** An unset or empty allowlist refuses *every* file op with `FilePathNotAllowedError` — a freshly-installed runtime does no file I/O until you wire roots. Applies to `file_read` as well as `file_write` (a read-then-`emit` is an exfiltration path).
563
+ - **Canonicalized before the check.** Both the target and each allowed root are resolved to their real absolute path (realpath, component-by-component), so `..` traversal and symlink evasion can't escape an allowed root — the classic allowlist bypasses are closed.
564
+ - **Off-allowlist is final.** No in-skill keyword (`approved=`, `# Autonomous: true`) escapes it. **Keep secret / key directories OUT of the allowlist** — a skill must never be able to read the operator's approval key. (This is why the default key path lives outside `SKILLSCRIPT_HOME`.)
565
+
566
+ ```bash
567
+ # permit reads/writes under a workspace + an event-drop dir
568
+ SKILLSCRIPT_FS_ALLOWLIST=/srv/skillscript/workspace,/var/skillscript/events
569
+ ```
570
+
571
+ TOCTOU note: the check resolves the real path at call time; a symlink swapped between check and open is a residual closed by fd-based opens later. Checking the resolved real path is the standard mitigation shipped today.
572
+
505
573
  ## Trigger model
506
574
 
507
575
  **The trigger surface is two primitives.**
@@ -711,7 +779,7 @@ A skill may carry template prose in one region. Splitting it into two — some p
711
779
 
712
780
  - **`unset-template-var`** (tier-1) — every `${VAR}` in the template must resolve to a `# Vars:` / `# Requires:` input, an ambient ref (`NOW`, `USER`, ...), or a `$set` / `->` binding somewhere in the skill body. Tier-1 because an unbound ref renders empty silently.
713
781
  - **`template-looks-like-target`** (tier-2) — bare `<word>:` alone in the template region, no following op-block. Ambiguous shape — author may have meant a target.
714
- - **`connector-as-tool`** (tier-1) — `$ <connector> <tool>` space-separated catches the muscle-memory foot-gun (CLI-style `git status`). Bare-form dispatch treats the first token as the tool name, sending `name: "<connector>"` to the MCP server, which replies with a misdirecting "Tool '<connector>' not found." The two correct shapes are `$ <connector>.<tool> args` (dotted) or `$ <tool> args` (bare-tool; runtime resolves).
782
+ - **`connector-as-tool`** (tier-1) — `$ <connector> <tool>` space-separated catches the muscle-memory foot-gun (CLI-style `git status`). Bare-form dispatch treats the first token as the tool name, sending `name: "<connector>"` to the MCP server, which replies with a misdirecting `Tool '<connector>' not found.` The two correct shapes are `$ <connector>.<tool> args` (dotted) or `$ <tool> args` (bare-tool; runtime resolves).
715
783
  - **`remote-result-needs-parse`** (tier-3) — `${R|length}` on an `R` bound by `$` dispatch. Per-MCP-server result shapes vary: if the server returns prose-wrapped or non-JSON text, the value binds as a STRING and `|length` returns the string's char-count instead of element-count. Add `$ json_parse ${R} -> P` after the dispatch and use `${P|length}`. Suppressed when the skill already does `$ json_parse ${R}` somewhere — your defensive parse is taken as intent.
716
784
  - **`body-template-detected`** (tier-3) — non-blank, non-`#` lines in the body region, no `${...}` interpolations, no text-consuming `# Output:` declaration. Suggests "I wrote prose; it became template by accident." Prefix with `#` to mark as comments, or add an interpolation / `# Output:` to confirm intent.
717
785
  - **`emit-with-template`** (tier-3) — skill has both a template AND `emit(text=...)` calls. Confirms the channel-shift is intentional.
@@ -860,7 +928,7 @@ Callers reading `WakeReceipt.woken` distinguish "the substrate woke them" from "
860
928
 
861
929
  **Pattern 3 — session echo on receipts.** When your substrate routes to a specific session, echo it back on `DeliveryReceipt.session_id` / `WakeReceipt.session_id`. Dashboards rendering "delivered to alice@laptop-tab-3" rather than just "delivered to alice" depend on this.
862
930
 
863
- **Pattern 4 — read `meta.origin.caller_agent_id` to attribute, not for scope.** The `DeliveryMeta` envelope your `deliver()` receives carries `origin.caller_agent_id` = the *authenticated caller* who fired the dispatch (not the skill's owner — those are separate semantics; see [Connector Contract Reference](connector-contract-reference.md) §field semantics). Use it for *attribution* — rendering "from <caller>" on the receiving end, audit logs, accountability — not for authorization scoping. Outbound substrate scoping should derive from the *skill owner* (which the runtime applies at the connector layer via `ctx.agentId`, not via the envelope). If `caller_agent_id` is undefined on a delivery you receive, it means the chain originated from a non-human trigger (cron / event / scheduler) — your substrate should attribute it as "system-fired" or similar, not assume an identity.
931
+ **Pattern 4 — read `meta.origin.caller_agent_id` to attribute, not for scope.** The `DeliveryMeta` envelope your `deliver()` receives carries `origin.caller_agent_id` = the *authenticated caller* who fired the dispatch (not the skill's owner — those are separate semantics; see [Connector Contract Reference](connector-contract-reference.md) §field semantics). Use it for *attribution* — rendering `from <caller>` on the receiving end, audit logs, accountability — not for authorization scoping. Outbound substrate scoping should derive from the *skill owner* (which the runtime applies at the connector layer via `ctx.agentId`, not via the envelope). If `caller_agent_id` is undefined on a delivery you receive, it means the chain originated from a non-human trigger (cron / event / scheduler) — your substrate should attribute it as "system-fired" or similar, not assume an identity.
864
932
 
865
933
  **Pattern 5 — surface non-fatal notes via `DeliveryReceipt.warnings`.** When your substrate needs to signal something non-fatal about a delivery — "stripped @session because verb is deliver", "rate-limit hint", "fan-out: delivered to 3 sessions" — return them as `warnings: string[]` on the receipt instead of writing to stderr. The runtime echoes warnings onto `AgentDeliveryReceiptRecord.receipt.warnings`, where the dashboard can render them and observability tools can scrape them. Stderr noise gets lost; receipt warnings are structured + caller-visible.
866
934
 
@@ -887,12 +955,7 @@ How `author` is captured depends on how the skill gets written:
887
955
 
888
956
  Adopters whose `SkillStore` is backed by an addressable substrate (e.g., a memory store) can author skills by writing the substrate record directly — without going through the MCP `skill_write` handler. This captures `SkillMeta.author` from the substrate's own writer-identity (whatever the direct-write API authenticates as).
889
957
 
890
- **Gotcha:** direct-write must declare `# Status: Draft`, not `# Status: Approved`. The runtime's hash-token tamper gate rejects skills with `# Status: Approved` that lack a `vN:<token>` stamp; the stamp is computed by the runtime's `update_status` flow, not by the substrate. To publish:
891
-
892
- 1. Write the skill with `# Status: Draft` via your substrate's direct-write API.
893
- 2. Call `skill_status({name, new_state: "Approved"})` via MCP (or the dashboard's Approve button). This stamps the token and preserves the captured author.
894
-
895
- Write-Approved-without-stamp will fail at execute time with `ApprovalRejectedError`. Always Draft-then-promote.
958
+ **Gotcha:** in **secured mode**, a direct-write declaring `# Status: Approved` without a valid signature is forced to `Draft` the substrate can't mint approval, only the operator's key can. Publish by writing `Draft`, then signing via `skillfile approve <name>` or the dashboard Approvals queue (both preserve the captured author). In **unsecured mode** a bare `# Status: Approved` direct-write is honored as-is. Either way, in-skill `$ skill_write` always lands its child `Draft` regardless of body declaration. See [Approval + secured mode](#approval--secured-mode).
896
959
 
897
960
  ## Identity propagation — for multi-agent hosts
898
961
 
@@ -935,7 +998,7 @@ X-Agent-Id: alice
935
998
 
936
999
  ### Trust model
937
1000
 
938
- The runtime trusts the host's header attestation. There's no signature verification — anyone reaching the runtime with a forged `X-Agent-Id` could claim to be anyone. The runtime is **not** the authentication boundary; the host is. Bilateral trust:
1001
+ The runtime trusts the host's header attestation. There's no signature verification on the *identity claim* (distinct from secured-mode approval, which *is* signature-verified different boundary, different mechanism): anyone reaching the runtime with a forged `X-Agent-Id` could claim to be anyone. The runtime is **not** the authentication boundary; the host is. Bilateral trust:
939
1002
 
940
1003
  - **The host** (your MCP gateway) authenticates the agent via its own auth surface (OAuth, JWT, session cookies, mTLS — whatever fits your platform) and injects the verified identity into the outbound `X-Agent-Id` header.
941
1004
  - **The runtime** trusts the host because you configured it to (`mcpCallerIdentityHeader` is opt-in; unset means "I don't trust any inbound identity claim, fall back to my own writer identity").
@@ -1033,20 +1096,7 @@ See `examples/custom-bootstrap.example.ts` for a worked walkthrough.
1033
1096
  - **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.
1034
1097
  - **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.
1035
1098
  - **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.
1036
- - **Authorization is hash-token approval.** Skills must carry `# Status: Approved vN:<token>` where the token re-computes from the body minus its `# Status:` line. Bundled `v1:` is CRC32 — discipline-barrier strength, suited to single-operator deployments. Adversarial threat models swap a stronger function:
1037
-
1038
- ```ts
1039
- import { registerApprovalFn, setPreferredApprovalVersion } from "skillscript-runtime";
1040
- import { createHmac } from "node:crypto";
1041
-
1042
- // v2: HMAC-SHA256 with operator-held key. Agent that knows the algorithm
1043
- // can no longer self-stamp without the key.
1044
- const key = process.env["APPROVAL_HMAC_KEY"]!;
1045
- registerApprovalFn("v2", (body) => createHmac("sha256", key).update(body).digest("hex"));
1046
- setPreferredApprovalVersion("v2"); // dashboard now stamps v2 on Approve clicks
1047
- ```
1048
-
1049
- Wire this in your bootstrap BEFORE any skill is stamped — otherwise existing skills carry `v1:` tokens that still verify (CRC32 stays registered) but new approvals use the upgraded function. The runtime maintains a per-version registry, so mixed-version skill bodies coexist cleanly.
1099
+ - **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.
1050
1100
 
1051
1101
  ## Skill discovery + cross-agent composition
1052
1102
 
@@ -1,4 +1,8 @@
1
- # Configuration
1
+ ---
2
+ title: Configuration
3
+ description: "How to configure a skillscript-runtime deployment — connectors.json substrate selection, named MCP connector wiring, runtime knobs, and adopter-custom impls."
4
+ mode: wide
5
+ ---
2
6
 
3
7
  How to configure a skillscript-runtime deployment.
4
8
 
@@ -1,4 +1,8 @@
1
- # Connector Contract Reference
1
+ ---
2
+ title: Connector Contract Reference
3
+ description: "The substrate-neutral contracts adopters implement to wire their own backends. Interface signatures, payload shapes, durability + identity conventions."
4
+ mode: wide
5
+ ---
2
6
 
3
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.
4
8
 
@@ -1,91 +1,14 @@
1
- # Skillscript Language Reference — syntax, ops, semantics
1
+ ---
2
+ title: Language Reference
3
+ description: "Canonical spec — syntax, ops, semantics. Frontmatter, ops, filters, conditionals, triggers, output kinds, composition, error handling."
4
+ mode: wide
5
+ ---
2
6
 
3
7
  Canonical language reference for skillscript. Audience: skill authors (human + agent). Specifies what is valid syntax, what behavior to expect at compile + runtime, and what is currently pending implementation.
4
8
 
5
9
  Implementation state is cross-referenced to commit hashes; pending items mark v2/v3 work.
6
10
 
7
11
 
8
- ## Not yet implemented, but planned
9
-
10
- These are features designed or anticipated but not yet implemented in the current build. Authors should not use these forms; they will not compile.
11
-
12
- ## Control flow
13
-
14
- - **`while CONDITION:` loops** — today's iteration is `foreach IDENT in EXPR:` only. While loops are planned for ad-hoc orchestration patterns ("loop until response contains 'done'").
15
- - **Arithmetic in `$set`** — today accepts literals + `${VAR}` interpolation; no `+ - * /` operators. Planned alongside `while` for turn counters and orchestration bookkeeping.
16
-
17
- ## Pipe filter extensions
18
-
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
-
21
- ## Triggers — note
22
-
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.
24
-
25
- ## Synchronous agent exchange
26
-
27
- - **`exchange()` runtime-intrinsic op** — synchronous send + wait pattern for multi-agent conferences. Awaits adopter-substrate queue impl + AgentConnector contract grow.
28
-
29
- ## Tests
30
-
31
- - **`# Tests:` block** with `given:` / `expect:` assertions — author-authored test cases. Will land when adopter signal demands test infrastructure.
32
-
33
- ## Output kinds
34
-
35
- - **`# Output: file: <path>`** — file-output routing parses but no router exists today.
36
- - **`# Output: card:`** — depends on a substrate-side card render surface; not implemented.
37
-
38
- ## Persistent state with declared scope
39
-
40
- ```
41
- $set NAME = value scope=skill-local
42
- $set NAME = value scope=agent-global
43
- $set NAME = value scope=session
44
- ```
45
-
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.
47
-
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.
51
-
52
- Distinct from triggers. Sensors are continuous channels the agent reads but doesn't emit on. Planned syntax:
53
-
54
- ```
55
- # Sensors: presence, screen-state, voice-prosody
56
- ```
57
-
58
- Ambient refs `$(SENSOR.presence)`, `$(SENSOR.voice-prosody.affect)` for read access. Privacy-gating discipline determines when a sensor is readable.
59
-
60
- ## Time as first-class primitives
61
-
62
- Currently `$(NOW)` (wall-clock). Planned relative-time primitives:
63
-
64
- ```
65
- $(SECONDS_SINCE_LAST_USER_MESSAGE)
66
- $(MINUTES_SINCE_SESSION_START)
67
- $(SECONDS_SINCE_LAST_FIRE_OF.<skill-name>)
68
- ```
69
-
70
- Most "right time" reasoning is relative, not wall-clock.
71
-
72
- ## Other planned
73
-
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)
75
- - **Time-windowed aggregation** — filter-like primitives across firings (e.g., "user has shown frustration in 3 of 5 recent turns")
76
- - **Debounce / rate-limit / coalesce** — declarative queueing policy headers
77
- - **Suppression as valid output** — explicit "fire-and-suppress" (different from `# Output: none`)
78
- - **Cross-skill pub-sub** — `# Publishes: signal.X` / `# Subscribes: signal.Y` decoupling
79
- - **Confidence/threshold gating** — `# RequiresConfidence: classifier >= 0.8` / `# RequiresThreshold:`
80
- - **Invocation-control axis** — `# Invocable-By: user | agent | trigger` (sensitive ops shouldn't leak across invocation boundaries)
81
- - **Channel/locality awareness** — `$(CHANNEL_TYPE)`, `$(CHANNEL_PRIVACY)` ambient refs for routing decisions
82
- - **Introspection primitives** — `$(PROMPT_CONTEXT.size)`, `$(SKILLS_FIRED_RECENTLY.last-1h)`, `$(SELF.confidence-trend)`
83
- - **Capability declarations** — `# Requires-Capabilities: sensors=[mic, camera], tools=[...]` (audit surface for operators)
84
-
85
- ## When the language extends, this section shrinks
86
-
87
- When any of these primitives ship, the relevant grammar moves into its canonical section (Ops reference, Variables, Triggers, etc.) and the entry here is removed. This section stays alive as a continuous staging area for the next horizon of unshipped work.
88
-
89
12
  ## Overview & language model
90
13
 
91
14
  Skillscript is a constrained domain-specific language for authoring agent workflows. A skillscript is a declarative recipe: a small program with a dependency DAG of named targets, each composed of typed operations. Skillscripts are written once and executed many times.
@@ -378,7 +301,7 @@ Closed list of language-intrinsic ops the runtime knows directly. Each is a func
378
301
  | `emit` | `emit(text="...")` | none | Append to the skill's emission stream; consumed by the configured `# Output:` delivery channel. |
379
302
  | `notify` | `notify(agent="...", message="...", [event_type=...], [correlation_id=...]) -> ACK` | optional | Mid-skill agent alert; synchronous send via configured AgentConnector. |
380
303
  | `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
- | `execute_skill` | `execute_skill(skill_name="...", inputs={...}) -> R` | optional | Composition primitive. Runtime-resolved. See Composition section. |
304
+ | `execute_skill` | `execute_skill(skill_name="...", inputs=\{...}) -> R` | optional | Composition primitive. Runtime-resolved. See Composition section. |
382
305
  | `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
306
  | `file_read` | `file_read(path="...") -> R` | required | Read a file at `path`; binds string contents. |
384
307
  | `file_write` | `file_write(path="...", content="...")` | none | Write `content` to `path`. `mkdir -p` semantics for parent directories. Mutation-classified. |
@@ -735,12 +658,12 @@ deliver:
735
658
 
736
659
  | Class | Op | Shape | Binding |
737
660
  |---|---|---|---|
738
- | Mutation | `$set` | `$set NAME = value` (with `${VAR}` interpolation at bind) | NAME (no arrow) |
661
+ | Mutation | `$set` | `$set NAME = value` (with `$\{VAR}` interpolation at bind) | NAME (no arrow) |
739
662
  | Mutation | `$append` | `$append VAR <value>` (type-dispatched: list element / string concat) | VAR (no arrow) |
740
663
  | Runtime-intrinsic | `emit` | `emit(text="...")` or `emit(text="""...""")` for multi-line | none |
741
664
  | Runtime-intrinsic | `notify` | `notify(agent="...", [message=...], [event_type=...], [correlation_id=...]) -> ACK` | optional |
742
665
  | Runtime-intrinsic | `inline` | `inline(skill="<name>")` | none (compile-time) |
743
- | Runtime-intrinsic | `execute_skill` | `execute_skill(skill_name="...", inputs={...}) -> R` | optional |
666
+ | Runtime-intrinsic | `execute_skill` | `execute_skill(skill_name="...", inputs=\{...}) -> R` | optional |
744
667
  | 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 |
745
668
  | Runtime-intrinsic | `file_read` | `file_read(path="...") -> R` | required |
746
669
  | Runtime-intrinsic | `file_write` | `file_write(path="...", content="...", [approved="..."])` | none |
@@ -769,13 +692,13 @@ Injected automatically at runtime; never declared by the author.
769
692
 
770
693
  | Var | Value |
771
694
  |-----|-------|
772
- | `${NOW}` | ISO-8601 timestamp at op-dispatch time |
773
- | `${USER}` | The configured user identity |
774
- | `${SESSION_CONTEXT}` | Current session-scope context (project/entity/etc., substrate-defined) |
775
- | `${TRIGGER_TYPE}` | What event fired this skill |
776
- | `${TRIGGER_PAYLOAD}` | Event-specific data |
777
- | `${EVENT.*}` | Event-payload fields populated by the trigger source |
778
- | `${ERROR_CONTEXT}` | In `# OnError:` fallback skills: type + target where failure occurred |
695
+ | `$\{NOW}` | ISO-8601 timestamp at op-dispatch time |
696
+ | `$\{USER}` | The configured user identity |
697
+ | `$\{SESSION_CONTEXT}` | Current session-scope context (project/entity/etc., substrate-defined) |
698
+ | `$\{TRIGGER_TYPE}` | What event fired this skill |
699
+ | `$\{TRIGGER_PAYLOAD}` | Event-specific data |
700
+ | `$\{EVENT.*}` | Event-payload fields populated by the trigger source |
701
+ | `$\{ERROR_CONTEXT}` | In `# OnError:` fallback skills: type + target where failure occurred |
779
702
 
780
703
  Iterator vars from `foreach` and output bindings from runtime-intrinsic / MCP-dispatch ops also pass through ambient at compile time; the runtime substitutes them per iteration / per op completion.
781
704
 
@@ -880,14 +803,14 @@ Pipe filters apply transforms to resolved variables before substitution. Syntax:
880
803
 
881
804
  | Filter | Effect | Example | Output |
882
805
  |--------|--------|---------|--------|
883
- | `url` | `encodeURIComponent(value)` | `${location|url}` for `"Asheville, NC"` | `Asheville%2C%20NC` |
884
- | `shell` | POSIX single-quote escape with outer quotes | `${arg|shell}` for `it's safe` | `'it'\''s safe'` |
885
- | `json` | `JSON.stringify(value)` | `${payload|json}` for `{k:"v"}` | `"{\"k\":\"v\"}"` |
886
- | `trim` | Whitespace trim | `${VERDICT|trim}` for `"urgent\n"` | `urgent` |
887
- | `length` | Count of items (array) or characters (string) | `${ITEMS|length}` for `["a","b","c"]` | `3` |
888
- | `contains:"X"` | Boolean: type-aware substring / element membership | `${MSG|contains:"urgent"}` for `"Yes, urgent"` | `true` |
889
- | `fallback:"X"` | Coalesce on missing/undefined/empty ref | `${VAR.missing|fallback:"-"}` | `-` |
890
- | `isodate` | Epoch seconds → ISO-8601 timestamp | `${EPOCH|isodate}` for `1779660000` | `2026-05-24T22:00:00.000Z` |
806
+ | `url` | `encodeURIComponent(value)` | `$\{location|url}` for `"Asheville, NC"` | `Asheville%2C%20NC` |
807
+ | `shell` | POSIX single-quote escape with outer quotes | `$\{arg|shell}` for `it's safe` | `'it'\''s safe'` |
808
+ | `json` | `JSON.stringify(value)` | `$\{payload|json}` for `\{k:"v"}` | `"\{\"k\":\"v\"}"` |
809
+ | `trim` | Whitespace trim | `$\{VERDICT|trim}` for `"urgent\n"` | `urgent` |
810
+ | `length` | Count of items (array) or characters (string) | `$\{ITEMS|length}` for `["a","b","c"]` | `3` |
811
+ | `contains:"X"` | Boolean: type-aware substring / element membership | `$\{MSG|contains:"urgent"}` for `"Yes, urgent"` | `true` |
812
+ | `fallback:"X"` | Coalesce on missing/undefined/empty ref | `$\{VAR.missing|fallback:"-"}` | `-` |
813
+ | `isodate` | Epoch seconds → ISO-8601 timestamp | `$\{EPOCH|isodate}` for `1779660000` | `2026-05-24T22:00:00.000Z` |
891
814
 
892
815
  ### `length` semantics
893
816
 
@@ -1501,11 +1424,11 @@ The substrate-routing ops (`$ connector.tool`, `$ data_read`, `$ llm`) and the a
1501
1424
 
1502
1425
  ## Five connector types
1503
1426
 
1504
- ### MemoryStore
1427
+ ### DataStore
1505
1428
 
1506
- Routes `$ data_read` retrieval ops. Interface: `MemoryStore.query(filters) → PortableMemory[]`.
1429
+ Routes `$ data_read` retrieval ops. Interface: `DataStore.query(filters) → PortableMemory[]`.
1507
1430
 
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[]`.
1431
+ 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 `DataStore.query` contract and return `PortableMemory[]`.
1509
1432
 
1510
1433
  ### LocalModel
1511
1434
 
@@ -1621,7 +1544,7 @@ Skill records are infrastructure, not knowledge atoms — adopter impls should t
1621
1544
 
1622
1545
  All connector types expose `capabilities()` for runtime discovery. Consumers:
1623
1546
  1. `# Requires:` matching against the registered set
1624
- 2. Dynamic queries via `listMemoryStores()` / `listLocalModels()` / `listMcpConnectors()` / `listAgentConnectors()` to pick a connector for the moment
1547
+ 2. Dynamic queries via `listDataStores()` / `listLocalModels()` / `listMcpConnectors()` / `listAgentConnectors()` to pick a connector for the moment
1625
1548
  3. Authoring tools that surface the registered set
1626
1549
 
1627
1550
  ## Multi-instance by design
@@ -1630,7 +1553,7 @@ Multiple instances of the same connector type are the *normal case*, not the exc
1630
1553
 
1631
1554
  ```
1632
1555
  {
1633
- primary: MemoryStoreImplA,
1556
+ primary: DataStoreImplA,
1634
1557
  project: SqliteProjectStore,
1635
1558
  scratch: InMemoryStore
1636
1559
  }
@@ -1779,7 +1702,7 @@ interface McpDispatchCtx {
1779
1702
 
1780
1703
  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
1704
 
1782
- ## Lifecycle and status — # Status: header, six canonical states, compile + runtime enforcement
1705
+ ## Lifecycle and status — # Status: header, three canonical states (Draft / Approved / Disabled), compile + runtime enforcement
1783
1706
 
1784
1707
  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.
1785
1708
 
@@ -2037,6 +1960,13 @@ default: fetch
2037
1960
 
2038
1961
  **Recursion guard.** The runtime enforces a configurable recursion-depth limit (default 10) to prevent infinite-loop composition. Exceeding the limit raises a clean structured error attributable to the offending dispatch site, not a stack overflow.
2039
1962
 
1963
+ ## Error surfacing in composition
1964
+
1965
+ Two non-obvious behaviors when reading a child's failures from the parent (both confirmed by the v1.0 runtime-semantics test battery):
1966
+
1967
+ - **Nested errors do NOT surface at the top level.** When a child op fails — e.g. the recursion-depth guard fires — the structured error nests inside the child's `R.errors`, which nests inside *its* parent's `R.errors`, and so on up the chain. It does **not** bubble to the caller's top-level `result.errors`. So a top-level `errors: []` does **not** mean nothing failed downstream. To detect a child failure, inspect the bound `${R.errors}` (and deeper), or rely on `(fallback: ...)` / `# OnError:` — those fire on the structured error regardless of nesting depth.
1968
+ - **`# Returns: R` is load-bearing for observability.** If the parent binds a child via `-> R` but does not declare `# Returns: R`, the `# Returns:` filter strips `R` from the parent's `final_vars` — and any nested child errors disappear from the parent's MCP-wire response along with it. A composition skill that wants its child's failures observable from *its own* caller must declare the binding (`# Returns: R`) explicitly. Observability of child failure is opt-in, the same way value export is.
1969
+
2040
1970
  ## Forward-reference resolution
2041
1971
 
2042
1972
  Skill references (`inline(skill=...)`, `execute_skill(skill_name=...)`) are validated at compile time but allow forward references with tier-2 advisories — making it possible to author sibling skills together (chicken-and-egg).
@@ -2152,6 +2082,7 @@ For *data skills* (skills marked `# Type: data`), the compile-time inline primit
2152
2082
 
2153
2083
  - Treat composition as a real cost. Each `execute_skill()` dispatch incurs the child's full execution time + side effects. Don't compose for trivial cases that could be inlined.
2154
2084
  - Declare `# Returns:` when a caller needs structured access to a child's variables. Leave it off for emit-only skills whose consumers read `.outputs.text` — the default-empty filter keeps scratch from propagating.
2085
+ - Want a child's failures visible to your caller? Declare the child binding in `# Returns:` — nested errors are filtered out with the binding otherwise (see Error surfacing in composition), and don't trust a top-level `errors: []`.
2155
2086
  - Pair composition with `(fallback: ...)` when the child skill might fail and the parent has a sensible degraded path.
2156
2087
  - Use mechanical mode to TestFlight any multi-skill chain before shipping it as a Headless skill on a cron trigger.
2157
2088
  - Forward references work — author sibling skills in any order, validate independently. The tier-2 warning surfaces the deferred-resolution path; runtime catches genuine misses.
@@ -2481,50 +2412,72 @@ Some features depend on others:
2481
2412
 
2482
2413
  The "Not yet implemented, but planned" section at top tracks the user-facing surface; this section preserves the design-order argument for when implementation work picks up.
2483
2414
 
2484
- ## Open spec questions unresolved language design decisions
2415
+ ## Not yet implemented, but planned
2416
+
2417
+ These are features designed or anticipated but not yet implemented in the current build. Authors should not use these forms; they will not compile.
2418
+
2419
+ ## Control flow
2485
2420
 
2486
- Questions surfaced during design that haven't been resolved. Each carries a current lean where applicable. Resolved items have moved to their canonical sections; this section tracks only what's still open.
2421
+ - **`while CONDITION:` loops** today's iteration is `foreach IDENT in EXPR:` only. While loops are planned for ad-hoc orchestration patterns ("loop until response contains 'done'").
2422
+ - **Arithmetic in `$set`** — today accepts literals + `${VAR}` interpolation; no `+ - * /` operators. Planned alongside `while` for turn counters and orchestration bookkeeping.
2487
2423
 
2488
- ## 1. Block execution model — write down the rules
2424
+ ## Pipe filter extensions
2425
+
2426
+ - **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?
2427
+
2428
+ ## Triggers — note
2429
+
2430
+ 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.
2489
2431
 
2490
- Within a target body, op ordering and variable binding conventions aren't fully written down. Specific questions:
2491
- - Can `emit()` calls precede `$` ops in the same target? (Yes; `emit()` has no dependency on subsequent ops.)
2492
- - What's the default output binding when `-> NAME` is omitted? (`${target.output}` — same as bare `target` referenced from other blocks.)
2493
- - How do cross-block references work syntactically? (`${other_target.output}` or `${VAR_BOUND_THERE}`.)
2494
2432
 
2495
- **Write a "Block execution model" subsection** in the Overview or Ops reference section. No semantic change, just documentation gap.
2433
+ ## Tests
2496
2434
 
2497
- ## 2. `else:` block visibility into the error
2435
+ - **`# Tests:` block** with `given:` / `expect:` assertions — author-authored test cases. Will land when adopter signal demands test infrastructure.
2498
2436
 
2499
- Should `${ERROR}` be an ambient ref inside `else:` blocks, populated with the error type/message? Lean: yes, same shape as `${ERROR_CONTEXT}` in `# OnError:` fallback skills. Useful for logging/telemetry skills.
2437
+ ## Output kinds
2500
2438
 
2501
- ## 3. Multiple triggersconcurrency
2439
+ - **`# Output: file: <path>`** file-output routing parses but no router exists today.
2440
+ - **`# Output: card:`** — depends on a substrate-side card render surface; not implemented.
2502
2441
 
2503
- 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.
2442
+ ## Persistent state with declared scope
2504
2443
 
2505
- ## 4. `execute_skill()` invocation vs trigger firing
2444
+ ```
2445
+ $set NAME = value scope=skill-local
2446
+ $set NAME = value scope=agent-global
2447
+ $set NAME = value scope=session
2448
+ ```
2506
2449
 
2507
- 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.
2450
+ 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.
2508
2451
 
2509
- ## 5. Output target delivery failures
2510
2452
 
2511
- 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.
2453
+ ## Time as first-class primitives
2512
2454
 
2513
- ## 6. Skill versioning rollback UX
2455
+ Currently `$(NOW)` (wall-clock). Planned relative-time primitives:
2514
2456
 
2515
- 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.
2457
+ ```
2458
+ $(SECONDS_SINCE_LAST_USER_MESSAGE)
2459
+ $(MINUTES_SINCE_SESSION_START)
2460
+ $(SECONDS_SINCE_LAST_FIRE_OF.<skill-name>)
2461
+ ```
2516
2462
 
2517
- ## 7. Connector capability declarations
2463
+ Most "right time" reasoning is relative, not wall-clock.
2518
2464
 
2519
- 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.
2465
+ ## Other planned
2520
2466
 
2521
- ## 8. Per-op timeouts
2467
+ - **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)
2468
+ - **Time-windowed aggregation** — filter-like primitives across firings (e.g., "user has shown frustration in 3 of 5 recent turns")
2469
+ - **Debounce / rate-limit / coalesce** — declarative queueing policy headers
2470
+ - **Suppression as valid output** — explicit "fire-and-suppress" (different from `# Output: none`)
2471
+ - **Cross-skill pub-sub** — `# Publishes: signal.X` / `# Subscribes: signal.Y` decoupling
2472
+ - **Confidence/threshold gating** — `# RequiresConfidence: classifier >= 0.8` / `# RequiresThreshold:`
2473
+ - **Invocation-control axis** — `# Invocable-By: user | agent | trigger` (sensitive ops shouldn't leak across invocation boundaries)
2474
+ - **Introspection primitives** — `$(PROMPT_CONTEXT.size)`, `$(SKILLS_FIRED_RECENTLY.last-1h)`, `$(SELF.confidence-trend)`
2522
2475
 
2523
- 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.
2476
+ ## When the language extends, this section shrinks
2524
2477
 
2525
- *(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.)*
2478
+ When any of these primitives ship, the relevant grammar moves into its canonical section (Ops reference, Variables, Triggers, etc.) and the entry here is removed. This section stays alive as a continuous staging area for the next horizon of unshipped work.
2526
2479
 
2527
2480
  ---
2528
2481
 
2529
- *Rendered from `skillscript/skillscript-language-reference` — 2026-06-15 18:44 EDT*
2482
+ *Rendered from `skillscript/skillscript-language-reference` — 2026-06-16 15:46 EDT*
2530
2483
  *Source of truth: AMP (`amp_render_document("skillscript/skillscript-language-reference")`)*
@@ -1,4 +1,8 @@
1
- # SqliteSkillStore — example
1
+ ---
2
+ title: SqliteSkillStore
3
+ description: "Worked example of SkillStore against a SQLite database — schema, semantics, forking checklist."
4
+ mode: wide
5
+ ---
2
6
 
3
7
  Worked example of `SkillStore` against a SQLite database. Copy this directory into your codebase, customize per your storage needs, register with skillscript-runtime's `Registry`. This README is written for the agent implementing your adopter's connector — including the human reviewing the PR.
4
8
 
@@ -1,5 +1,5 @@
1
1
  # Skill: classify-support-ticket
2
- # Status: Approved v1:ae27d3f9
2
+ # Status: Draft
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
@@ -1,5 +1,5 @@
1
1
  # Skill: data-store-roundtrip
2
- # Status: Approved v1:b37bc111
2
+ # Status: Draft
3
3
  # Autonomous: true
4
4
  # Description: Round-trips the DataStore — writes a record tagged with a per-run marker in the content, reads it back via FTS query against the same marker. The exact item count depends on substrate FTS matching strictness (strict FTS substrates return 1; substrates with looser token-match semantics may return prior runs' records that share token shapes). If this skill executes and the output line shows N ≥ 1 items returned, your DataStore substrate is wired correctly. For adopters needing deterministic counts (e.g. authoring round-trip tests), the `domain_tags` filter is the portable strict-match read path — see the adopter-playbook §"Notable things..." for the pattern.
5
5
 
@@ -1,5 +1,5 @@
1
1
  # Skill: doc-qa-with-citations
2
- # Status: Approved v1:dc6e903d
2
+ # Status: Draft
3
3
  # Description: When the user asks a question that requires retrieval over the doc set, answer with inline citations to record IDs
4
4
  # Vars: QUESTION, K=6
5
5
  # Output: text
@@ -1,5 +1,5 @@
1
1
  # Skill: feedback-sentiment-scan
2
- # Status: Approved v1:2b4868c1
2
+ # Status: Draft
3
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.
4
4
  # Triggers: cron: 0 3 * * *
5
5
  # Vars: SCAN_LIMIT=50
@@ -1,5 +1,5 @@
1
1
  # Skill: hello-world
2
- # Status: Approved v1:b662ccd6
2
+ # Status: Draft
3
3
  # Description: The canonical first-run example. No substrate dependencies, no compute block — pure declarative output. If this fails to execute, your install or runtime is broken; investigate before troubleshooting deeper layers. Demonstrates the template-only shape: body text IS the skill; `# Vars:` declared inputs feed the template with `--input` override.
4
4
  # Vars: WHO=world
5
5
 
@@ -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-16T13:54:50.151Z",
5
+ "compiled_at": "2026-06-17T21:50:10.988Z",
6
6
  "source_skill": {
7
7
  "name": "hello-world"
8
8
  },
@@ -1,5 +1,5 @@
1
1
  # Skill: morning-brief
2
- # Status: Approved v1:1530981d
2
+ # Status: Draft
3
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 * * *
@@ -1,5 +1,5 @@
1
1
  # Skill: queue-length-monitor
2
- # Status: Approved v1:c6bc06df
2
+ # Status: Draft
3
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 * * * *