skillscript-runtime 0.27.1 → 0.27.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -301,7 +301,7 @@ Closed list of language-intrinsic ops the runtime knows directly. Each is a func
301
301
  | `emit` | `emit(text="...")` | none | Append to the skill's emission stream; consumed by the configured `# Output:` delivery channel. |
302
302
  | `notify` | `notify(agent="...", message="...", [event_type=...], [correlation_id=...]) -> ACK` | optional | Mid-skill agent alert; synchronous send via configured AgentConnector. |
303
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. |
304
- | `execute_skill` | `execute_skill(skill_name="...", inputs=\{...}) -> R` | optional | Composition primitive. Runtime-resolved. See Composition section. |
304
+ | `execute_skill` | `execute_skill(name="...", inputs=\{...}) -> R` | optional | Composition primitive. Runtime-resolved. `skill_name=` accepted as back-compat alias. See Composition section. |
305
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. |
306
306
  | `file_read` | `file_read(path="...") -> R` | required | Read a file at `path`; binds string contents. Optional `encoding="utf8"\|"base64"` kwarg (default `utf8`). |
307
307
  | `file_write` | `file_write(path="...", content="...")` | none | Write `content` to `path`. `mkdir -p` semantics for parent directories. Mutation-classified. |
@@ -358,7 +358,7 @@ Returns ACK `{agent, dispatched: [{connector, ok, error?}]}` — fire-and-forget
358
358
 
359
359
  Three forms.
360
360
 
361
- **1. `shell(command="...")` — structural spawn (default).** The command string is whitespace-tokenized and quote-stripped, then one binary is spawned with the resulting tokens. No shell, no metacharacters, no pipes/redirects. The tokenizer is **quote-aware**: quotes are respected during the whitespace split, so a literal `'hello world'` stays one token (the surrounding quotes are then stripped). stdout binds; non-zero exit → op-error routed through `else:` / `# OnError:`.
361
+ **1. `shell(command="...")` — structural spawn (default).** The command string is whitespace-tokenized and quote-stripped, then one binary is spawned with the resulting tokens. No shell, no metacharacters, no pipes/redirects. The tokenizer is **quote-aware**: quotes are respected during the whitespace split, so a literal `'hello world'` stays one token (the surrounding quotes are then stripped). stdout binds; non-zero exit → op-error routed through the target's `else:` handler.
362
362
 
363
363
  ```
364
364
  shell(command="curl -s 'wttr.in/${LOCATION|url}?format=j1'") -> RAW
@@ -437,10 +437,10 @@ See Composition section for the distinction between `inline` (compile-time), `ex
437
437
 
438
438
  ```
439
439
  classify:
440
- execute_skill(skill_name="classifier", inputs={"text": "${INPUT}"}) -> VERDICT
440
+ execute_skill(name="classifier", inputs={"text": "${INPUT}"}) -> VERDICT
441
441
  ```
442
442
 
443
- Runtime-resolved against the SkillStore. Recursion-depth-guarded (default 10).
443
+ Runtime-resolved against the SkillStore. Recursion-depth-guarded (default 10). Canonical kwarg is `name=`; `skill_name=` is an accepted back-compat alias.
444
444
 
445
445
  **Returns.** The caller's `-> R` binding receives `outputs` + `transcript` + execution metadata always, plus the child's declared `# Returns:` surface (each declared var accessible as `${R.X}`). Undeclared scratch is filtered — a child with no `# Returns:` yields `final_vars: {}`. This is what keeps composition from compounding (a child's internal state never propagates up). Full contract in Composition § Return contract.
446
446
 
@@ -517,7 +517,7 @@ $ amp.amp_olsen_task task_type="scan" timeout=120 -> DISPATCH
517
517
  $ data_write content="${REPORT}" tags=["oncall"] approved="morning roundup" -> ACK
518
518
  ```
519
519
 
520
- **`(fallback: "value")` trailer** — Fires on dispatch throw (including a `timeout=N` expiry) OR empty bound value (empty string after trim, empty array, null/undefined). Honored on `$` dispatch and on `shell()` (fires on shell op throw or empty stdout). Coerce-on-bind: the fallback value binds to the output var transparently, downstream targets need no conditional. Permissive value parsing — bare identifiers, quoted strings, bracketed array literals all accepted. A fired fallback is recorded in `result.fallbacks[]`.
520
+ **`(fallback: "value")` trailer** — Uniform as of v0.27.0: fires on ANY op failure — a dispatch throw (including a `timeout=N` expiry), an empty bound value (empty string after trim, empty array, null/undefined), OR a raised throw from an `execute_skill` child or a `$ json_parse` off-shape input. Honored on `$` dispatch and on `shell()` (fires on shell op throw or empty stdout). Coerce-on-bind: the fallback value binds to the output var transparently, downstream targets need no conditional. Permissive value parsing — bare identifiers, quoted strings, bracketed array literals all accepted. A fired fallback is recorded in `result.fallbacks[]` (with its `.reason`).
521
521
 
522
522
  Note: an envelope object like `{items: []}` is a non-empty object and does NOT trigger the fallback even though its contained array is empty. To handle envelope-empty downstream, test the contained collection (`if ${R.items|length} == "0":`) or apply a filter (`${R.items|fallback:[]}`).
523
523
 
@@ -599,7 +599,7 @@ $ data_write content="${SUMMARY}" tags=["oncall"] approved="morning roundup, 202
599
599
  $ ticketing.search query="project:INFRA state:Open" limit=20 -> ISSUES
600
600
  ```
601
601
 
602
- Tool args are unconstrained `key=value` pairs — the connector forwards them to the underlying MCP tool. If a dispatched call returns `isError: true`, the executor throws via `makeOpError`, which routes through `else:` / `# OnError:` machinery. The inner tool's error text is preserved in `result.errors[]`.
602
+ Tool args are unconstrained `key=value` pairs — the connector forwards them to the underlying MCP tool. If a dispatched call returns `isError: true`, the executor throws via `makeOpError`, which routes through the target's `else:` handler. The inner tool's error text is preserved in `result.errors[]`.
603
603
 
604
604
  **Substrate-neutrality.** Typed-contract bare ops (`$ llm`, `$ data_read`, `$ data_write`, `$ skill_*`, `$ json_parse`) are not reserved built-ins — they're typed-contract bridges the adopter wires through `substrate.local_model` / `substrate.data_store` / `substrate.skill_store` in `skillscript.config.json`. Substrate-specific tools (`$ amp.*`, `$ github.*`, etc.) are whatever the adopter declares in `connectors.json`. For external MCP servers, three bundled wiring paths cover the common cases:
605
605
 
@@ -669,23 +669,13 @@ deliver:
669
669
  | Runtime-intrinsic | `emit` | `emit(text="...")` or `emit(text="""...""")` for multi-line | none |
670
670
  | Runtime-intrinsic | `notify` | `notify(agent="...", [message=...], [event_type=...], [correlation_id=...]) -> ACK` | optional |
671
671
  | Runtime-intrinsic | `inline` | `inline(skill="<name>")` | none (compile-time) |
672
- | Runtime-intrinsic | `execute_skill` | `execute_skill(skill_name="...", inputs=\{...}) -> R` | optional |
672
+ | Runtime-intrinsic | `execute_skill` | `execute_skill(name="...", inputs=\{...}) -> R` (`skill_name=` back-compat alias) | optional |
673
673
  | 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 |
674
674
  | Runtime-intrinsic | `file_read` | `file_read(path="...", [encoding="utf8"\|"base64"]) -> R` | required |
675
675
  | Runtime-intrinsic | `file_write` | `file_write(path="...", content="...", [approved="..."])` | none |
676
676
  | External MCP — substrate-specific | `$ <connector>.<tool>` | `$ <connector>.<tool> kwarg=value, ... [timeout=N] [approved="..."] [-> R] [(fallback: "...")]` | optional |
677
677
  | External MCP — typed-contract | `$ <tool>` | `$ <tool> kwarg=value, ... [timeout=N] [approved="..."] [-> R] [(fallback: "...")]` (typed-contract ops only: `data_*`, `skill_*`, `json_parse`, `llm`) | optional |
678
678
 
679
- ---
680
-
681
- ## Current-runtime corrections (v0.27.0)
682
-
683
- A few points above predate the 0.26/0.27 error-handling changes — read them with these corrections:
684
-
685
- - **`(fallback:)` is now UNIFORM (v0.27.0).** Beyond `$` dispatch and `shell()`, the op-level `(fallback:)` trailer now also contains a raised throw from `execute_skill` (a child skill that throws) and `$ json_parse` (malformed input). It contains ANY failure of the op — a raised throw OR an empty/missing result — on every fallible op. A fired fallback is recorded in `result.fallbacks[].reason`.
686
- - **`# OnError:` is parsed but NOT wired** in the current runtime — the named fallback skill never fires. Wherever the text above says an op-error "routes through `else:` / `# OnError:`", read it as `else:` only. Use `else:` (or an op-level `(fallback:)`, or a structural guard) for containment. See the Error handling and Robustness sections.
687
- - **`execute_skill` canonical kwarg is `name=`** (`skill_name=` is an accepted silent back-compat alias). Prefer `execute_skill(name="...", inputs={...}) -> R` in new skills; the `skill_name=` examples above still work but aren't canonical.
688
-
689
679
  ## Variable resolution — ${VAR} canonical, substitution + ambient refs + # Requires: cascade
690
680
 
691
681
  Skillscript supports four tiers of variables, each with distinct resolution timing and scope. Substitution uses **`${VAR}` as the canonical form**.
@@ -714,7 +704,7 @@ Injected automatically at runtime; never declared by the author.
714
704
  | `$\{TRIGGER_TYPE}` | What event fired this skill |
715
705
  | `$\{TRIGGER_PAYLOAD}` | Event-specific data |
716
706
  | `$\{EVENT.*}` | Event-payload fields populated by the trigger source |
717
- | `$\{ERROR_CONTEXT}` | In `# OnError:` fallback skills: type + target where failure occurred |
707
+ | `$\{ERROR_CONTEXT}` | Inside a target's `else:` error-handler: kind + message + target of the failure. |
718
708
 
719
709
  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.
720
710
 
@@ -811,10 +801,6 @@ Missing-ref in the RHS produces a tier-1 runtime error.
811
801
  - `foreach IDENT in EXPR:` iterator vars are loop-local — `$set` bindings inside the loop don't persist after the loop ends
812
802
  - Target outputs (`${target.output}`) are accessible after the target completes
813
803
 
814
- ## Current-runtime correction (v0.27.0)
815
-
816
- **`${ERROR_CONTEXT}` is surfaced in `else:` error-handler blocks, not `# OnError:`.** The Tier-1 ambient table above describes `${ERROR_CONTEXT}` as available "In `# OnError:` fallback skills" — that is stale. `# OnError:` is parsed but inert (never fires). The working error-handler is the target-level `else:` block (runs when any op in the target body throws), and `${ERROR_CONTEXT}` (failure type + target) is populated inside that `else:` block. Read the table row as: "In an `else:` error-handler: type + target where failure occurred." See the Error handling and Conditionals sections for `else:`-handler semantics.
817
-
818
804
  ## Secrets — secret.NAME references, {{secret.NAME}} sink markers, SKILLSCRIPT_SECRET_ provisioning, use-only enforcement
819
805
 
820
806
  ## Secrets
@@ -968,6 +954,8 @@ emit:
968
954
  emit(text="nested: ${ISSUE.customFields.Assignee|fallback:\"unassigned\"}")
969
955
  ```
970
956
 
957
+ **Order-independent in a filter chain (v0.26.2+).** A `|fallback` anywhere in the chain rescues an unresolved base — `${x|trim|fallback:"d"}` and `${x|fallback:"d"|trim}` both degrade an unresolved `x` to `"d"`. Nuance: it rescues a genuinely-unresolved/missing base; a **present-but-empty value still flows through the other filters**, so `${x|length|fallback:"0"}` on `x=" "` returns `"2"`, not `"0"`. See the Robustness & error containment section for the chain semantics.
958
+
971
959
  **Why filter-shape, not ref-level `(fallback:)`.** Op-level `(fallback: ...)` exists on `$` dispatch and `shell()` for **error/empty recovery** (the op ran, then failed or returned empty). Ref-level `|fallback:` is **coalesce** (the lookup itself found nothing — missing, null, or empty). They rhyme but are adjacent concepts. The filter-chain attachment keeps composition clean (`${VAR|json_parse|fallback:"-"}` works as a chain step) and the vocabulary alignment with op-level `(fallback:)` lets cold authors learn "fallback" as the universal concept while the syntax disambiguates the attachment site.
972
960
 
973
961
  **Closes the missing-field strict-error trap**: `${ISSUE.customFields.Assignee}` against an object without that key threw `UnresolvedVariableError` and aborted whole-render. The filter is the per-ref opt-out.
@@ -1032,7 +1020,7 @@ elif ${BODY|length} > 1000:
1032
1020
 
1033
1021
  ## Error handling
1034
1022
 
1035
- Unknown filter on a resolved variable produces a tier-1 `unknown-filter` compile error. Catches both bare (`|unknown`) and colon-positional (`|unknown:"arg"`) shapes. Filter chains that fail at runtime (e.g., `|json` on a non-serializable value, `|length` on a number, `|isodate` on a non-numeric value) produce op errors that route through `else:` / `# OnError:` machinery.
1023
+ Unknown filter on a resolved variable produces a tier-1 `unknown-filter` compile error. Catches both bare (`|unknown`) and colon-positional (`|unknown:"arg"`) shapes. Filter chains that fail at runtime (e.g., `|json` on a non-serializable value, `|length` on a number, `|isodate` on a non-numeric value) produce op errors that route through the target's `else:` handler.
1036
1024
 
1037
1025
  Bare `${NAME}` without a filter is unchanged.
1038
1026
 
@@ -1061,13 +1049,6 @@ Filters are pure functions (input → output, no side effects). Stay small and o
1061
1049
 
1062
1050
  `length`, `fallback:`, `isodate`, and `contains:` were all added in response to cold-author harness signal — authored skills demonstrated the gap was load-bearing before each filter shipped. `contains:` is also notable as the first filter to operate on structured types; filter-as-conditional-primitive is the design line that warranted the cross.
1063
1051
 
1064
- ---
1065
-
1066
- ## Current-runtime corrections
1067
-
1068
- - **`|fallback` is order-independent in a filter chain (v0.26.2+).** A `|fallback` anywhere in the chain rescues an unresolved base — `${x|trim|fallback:"d"}` and `${x|fallback:"d"|trim}` both degrade an unresolved `x` to `"d"`. Nuance: it rescues a genuinely-unresolved/missing base; a present-but-empty value still flows through the other filters, so `${x|length|fallback:"0"}` on `x=" "` returns `"2"`, not `"0"`. See the Robustness & error containment section for the chain semantics.
1069
- - The "Error handling" note above ("Filter chains that fail at runtime … route through `else:` / `# OnError:` machinery") should read **`else:`** — `# OnError:` is parsed but not wired in the current runtime.
1070
-
1071
1052
  ## Conditionals & iteration — if/elif/else, foreach, supported operators
1072
1053
 
1073
1054
  Skillscript supports narrow conditionals and bounded iteration. Both are deliberately constrained — composition over expressiveness.
@@ -1531,11 +1512,7 @@ The receiving agent reads `event_type` for routing ("911 — surface now" vs "ro
1531
1512
 
1532
1513
  ## Output routing failures
1533
1514
 
1534
- 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.
1535
-
1536
- ## Current-runtime correction (v0.27.0)
1537
-
1538
- **Output routing failures land in `else:`, not `# OnError:`.** The "Output routing failures" section says a throwing AgentConnector "routes through `else:` / `# OnError:`." `# OnError:` is parsed but inert (never fires) — read that as `else:` only. The target-level `else:` handler catches the delivery throw; the failure is still recorded on `agent_delivery_receipts[]` for the scheduler regardless.
1515
+ If `# Output: agent: <name>` fires and the wired AgentConnector throws, delivery routes through the target's `else:` handler; the failure is recorded on `agent_delivery_receipts[]` for the scheduler to log.
1539
1516
 
1540
1517
  ## Connectors — substrate routing, the five connector types, agent_id resolution
1541
1518
 
@@ -1776,7 +1753,7 @@ interface PortableMemory {
1776
1753
  score?: number;
1777
1754
 
1778
1755
  // Curated substrate subset — concept-portable, value-substrate-specific.
1779
- // Top-level access via $(MEMORY.field). Connectors populate when the
1756
+ // Top-level access via ${MEMORY.field}. Connectors populate when the
1780
1757
  // concept applies. MUST NOT also be duplicated into metadata.
1781
1758
  thread_status?: string;
1782
1759
  pinned?: boolean;
@@ -1790,7 +1767,7 @@ interface PortableMemory {
1790
1767
  agent_id?: string;
1791
1768
  vault?: string;
1792
1769
 
1793
- // Substrate-specific bag. Accessed via $(MEMORY.metadata.X).
1770
+ // Substrate-specific bag. Accessed via ${MEMORY.metadata.X}.
1794
1771
  metadata?: Record<string, unknown>;
1795
1772
  }
1796
1773
 
@@ -1809,22 +1786,18 @@ interface McpDispatchCtx {
1809
1786
 
1810
1787
  ## Field access semantics
1811
1788
 
1812
- `$(MEMORY.field)` resolves in tiers:
1789
+ `${MEMORY.field}` resolves in tiers:
1813
1790
  1. Core fields (id, summary, detail, score)
1814
1791
  2. Curated substrate subset (thread_status, pinned, etc.)
1815
1792
  3. `metadata.X` for everything else
1816
- 4. Ambient passthrough as literal `$(MEMORY.field)` if unresolved
1793
+ 4. Ambient passthrough as literal `${MEMORY.field}` if unresolved
1817
1794
 
1818
- **Connector duplication is a contract violation.** If a field is in the curated subset, the connector populates it at top-level only — `metadata.<same_name>` MUST be absent. Otherwise `$(M.thread_status)` and `$(M.metadata.thread_status)` can return different values (silent data divergence). Connectors enforce.
1795
+ **Connector duplication is a contract violation.** If a field is in the curated subset, the connector populates it at top-level only — `metadata.<same_name>` MUST be absent. Otherwise `${M.thread_status}` and `${M.metadata.thread_status}` can return different values (silent data divergence). Connectors enforce.
1819
1796
 
1820
1797
  ## Why connector abstraction matters
1821
1798
 
1822
1799
  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.
1823
1800
 
1824
- ## Notation correction
1825
-
1826
- The "Portable shapes" and "Field access semantics" sections write memory-field access as `$(MEMORY.field)` / `$(M.thread_status)` / `$(MEMORY.metadata.X)`. That is the wrong sigil. Skillscript's canonical substitution form is `${...}` (brace), not `$(...)` (paren) — the paren form is deliberately reserved to avoid collision with bash `$(command)` inside `shell(command=..., unsafe=true)` (see the Variable resolution section). Read every `$(MEMORY.field)` here as `${MEMORY.field}`. The tiered field-access resolution (core → curated subset → `metadata.X` → ambient passthrough) is otherwise accurate; only the sigil is wrong.
1827
-
1828
1801
  ## Lifecycle and status — # Status: header, three canonical states (Draft / Approved / Disabled), compile + runtime enforcement
1829
1802
 
1830
1803
  Skillscripts carry an explicit lifecycle state via the `# Status:` header. The compiler and runtime enforce it — a Disabled skill cannot fire under any path, and (in secured mode) an unapproved skill cannot perform effectful ops under any path.
@@ -1888,11 +1861,11 @@ Test, Deployed, and Deprecated were considered and deferred — today's Draft/Ap
1888
1861
 
1889
1862
  Lifecycle states are the language's operational-safety answer. A Disabled skill can't fire even if every author forgets it's broken; in secured mode, an Approved skill can't fire if its body was tampered post-signature, and an agent can't approve its own work. The constraint IS the safety story, here as elsewhere.
1890
1863
 
1891
- ## Error handling — else: blocks, # OnError: fallback, op-level fallback values
1864
+ ## Error handling — else: blocks and op-level (fallback:) values
1892
1865
 
1893
- Skillscript has no try/catch — error handling is authored. Three mechanisms, local to global. **Note each one's current wiring status.**
1866
+ Skillscript has no try/catch — error handling is authored. Two runtime mechanisms (local to global) plus a structural discipline that prevents failure outright.
1894
1867
 
1895
- ## Layer 1: Target-level `else:` block — the reliable throw-container
1868
+ ## Layer 1: Target-level `else:` block — the throw-container
1896
1869
 
1897
1870
  Runs if any op in the target's primary body throws. Local to the failing target; downstream targets that depend on it can still proceed with whatever the `else:` branch produced. `${ERROR_CONTEXT}` (`.kind` / `.message` / `.target`) is available inside it. This is the way to contain a raised throw — including an `execute_skill` child-throw and a `$ json_parse` on malformed input (both verified v0.27.0).
1898
1871
 
@@ -1906,11 +1879,7 @@ else:
1906
1879
 
1907
1880
  Distinguished from conditional `else:` (which appears after an `if:`/`elif:` chain inside a body) by the parser's scope-stack; both can coexist in a target. An `else:` block may not declare its own error handler.
1908
1881
 
1909
- ## Layer 2: Skill-level `# OnError:` header PARSED BUT NOT YET WIRED
1910
-
1911
- `# OnError: <fallback-skill>` is accepted and existence-checked at compile time, but it is **NOT wired in the current runtime — the named fallback skill never fires.** Do not rely on it for containment; use `else:` (or a structural guard) instead. (Tracked: wire it or remove the header in a future release.)
1912
-
1913
- ## Layer 3: Op-level `(fallback:)` value — uniform (v0.27.0+)
1882
+ ## Layer 2: Op-level `(fallback:)` valueuniform (v0.27.0+)
1914
1883
 
1915
1884
  Inline fallback on the op line. As of **v0.27.0 it is uniform: it contains ANY failure of that op** — a raised throw OR an empty/missing result. It works on `$` (MCP dispatch), `shell()`, `file_read`, and — since v0.27.0 — `execute_skill` (child throw) and `$ json_parse` (malformed input). On failure the fallback value binds to the output var via the same path as a success (coerce-on-bind), so downstream sees it transparently and needs no "did this fail?" check. A fired fallback is recorded in `result.fallbacks[].reason` (not `result.errors[]`) — degrade-loud.
1916
1885
 
@@ -1929,18 +1898,33 @@ $ llm prompt="..." -> VERDICT (fallback: unknown) # bare identifi
1929
1898
  $ slack.post text="..." -> ACK (fallback: "post failed") # quoted string
1930
1899
  ```
1931
1900
 
1901
+ ## Structural guard — prevent the failure
1902
+
1903
+ Pre-bind defaults (`$set`) for every var the template/downstream reads, then gate the risky op behind a `contains`/shape check (`if ${RAW|contains:"expected_key"}:`) so the throw never happens. Most robust of all — no error to contain because none is raised. See *Robustness & error containment* for the full discipline.
1904
+
1932
1905
  ## Choosing a mechanism
1933
1906
 
1934
- - **`(fallback:)`** — "any failure of this op → this default value." Uniform; the per-op opt-out.
1907
+ - **`(fallback:)`** — "any failure of this op → this default value." Uniform; the per-op opt-out. Reach for it first.
1935
1908
  - **`else:`** — target-level; when you need the error's details (`${ERROR_CONTEXT}`) to branch, or to contain a throw spanning several ops.
1936
- - **Structural guard** (pre-bind defaults + a `contains`/shape check before the risky op) prevents the failure entirely; most robust. See *Robustness & error containment* for the discipline.
1937
- - **`# OnError:`** — not currently wired (Layer 2); don't depend on it.
1909
+ - **Structural guard** when you can cheaply validate the shape before the risky op; prevents the failure entirely.
1910
+
1911
+ ## Deprecated: `# OnError:` — parsed but not wired
1912
+
1913
+ A skill-level `# OnError: <fallback-skill>` header parses, but it is **not wired in the current runtime: the named fallback never fires.** A skill that relies on it has, in effect, no error handling — so don't use it. Use a target-level `else:` handler (or an op-level `(fallback:)`) instead. Removal of the header is planned for a post-launch release (after 0.30); until then it is inert, not a compile error. To recover at the whole-skill level, wrap the body in a target with an `else:` that dispatches your recovery skill:
1914
+
1915
+ ```
1916
+ run:
1917
+ execute_skill(name="do-the-work") -> R
1918
+ else:
1919
+ execute_skill(name="recovery", inputs={"REASON": "${ERROR_CONTEXT.message}"}) -> R
1920
+ ```
1938
1921
 
1939
1922
  ## Propagation
1940
1923
 
1941
1924
  - Op throw → contained by a `(fallback:)` on that op if present; else by the target's `else:`; else propagates to the caller (surfaces in `result.errors[]`).
1942
1925
  - A fired op-level `(fallback:)` is recorded in `result.fallbacks[]`, not `result.errors[]`.
1943
1926
  - Under the hood: when a `$` op returns `isError: true` the executor throws via `makeOpError` rather than binding the error text — that throw is what routes to `(fallback:)` / `else:` and surfaces in `result.errors[]`. Op-level `(fallback:)` intercepts it and binds the fallback value instead.
1927
+ - **Composition caveat:** a child skill's failures do NOT bubble to the parent's top-level `result.errors[]` — they nest in the bound `${R.errors}`. A top-level `errors: []` does not mean nothing failed downstream. Inspect `${R.errors}` or rely on `(fallback:)`/`else:`, which fire regardless of nesting depth. (See Composition.)
1944
1928
 
1945
1929
  ## Robustness & error containment — best practices (no try/catch by design)
1946
1930
 
@@ -1986,23 +1970,23 @@ Bad input skips the `if`, the default stands, the child never throws — so a pa
1986
1970
 
1987
1971
  Skillscript supports skill-to-skill composition via the runtime's public composition primitive. A parent skill invokes a child skill, optionally passes inputs, optionally binds the child's result. The runtime threads variable state, propagates errors, and enforces a recursion-depth guard.
1988
1972
 
1989
- Composition is exposed as a runtime-intrinsic op: `execute_skill(skill_name="...", inputs={...}) -> R`. Symmetric with `compile_skill` and `lint_skill` — same surface, same naming convention, no external-namespace dependency.
1973
+ Composition is exposed as a runtime-intrinsic op: `execute_skill(name="...", inputs={...}) -> R`. Symmetric with `compile_skill` and `lint_skill` — same surface, same naming convention, no external-namespace dependency. (`skill_name=` is an accepted back-compat alias for `name=`; prefer `name=` in new skills.)
1990
1974
 
1991
1975
  ## Surface
1992
1976
 
1993
1977
  ```
1994
1978
  parent:
1995
- execute_skill(skill_name="child") -> RESULT
1979
+ execute_skill(name="child") -> RESULT
1996
1980
  emit(text="Child returned: ${RESULT}")
1997
1981
  ```
1998
1982
 
1999
- The runtime resolves `skill_name` against the configured SkillStore at dispatch time, runs the child to completion against the runtime's wired connectors, and binds the result.
1983
+ The runtime resolves `name` against the configured SkillStore at dispatch time, runs the child to completion against the runtime's wired connectors, and binds the result.
2000
1984
 
2001
1985
  ## Tool signature
2002
1986
 
2003
1987
  ```
2004
1988
  execute_skill({
2005
- skill_name: string, // required — resolves via SkillStore
1989
+ name: string, // required — resolves via SkillStore (skill_name= accepted as back-compat alias)
2006
1990
  inputs?: Record<string,string>, // optional — Vars override map
2007
1991
  mechanical?: boolean // optional — dry-run mode (default false)
2008
1992
  })
@@ -2049,7 +2033,7 @@ default: fetch
2049
2033
 
2050
2034
  ## Semantics
2051
2035
 
2052
- **Skill resolution.** Missing skills produce a clean structured error (`MissingSkillReferenceError extends OpError`) — the parent's `(fallback: ...)` discipline applies if specified, otherwise the parent skill's `# OnError:` fallback fires if declared, otherwise the parent fails with the error propagated through.
2036
+ **Skill resolution.** Missing skills produce a clean structured error (`MissingSkillReferenceError extends OpError`) — the parent's `(fallback: ...)` discipline applies if specified, otherwise a target-level `else:` handler catches it if declared, otherwise the parent fails with the error propagated through.
2053
2037
 
2054
2038
  **Input override.** `inputs` map keys must match the child's `# Vars:` declarations. Undeclared keys are ignored. Required vars without defaults must be supplied or dispatch fails before the child starts.
2055
2039
 
@@ -2063,28 +2047,27 @@ default: fetch
2063
2047
 
2064
2048
  Two non-obvious behaviors when reading a child's failures from the parent (both confirmed by the v1.0 runtime-semantics test battery):
2065
2049
 
2066
- - **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.
2050
+ - **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: ...)` / `else:` — those fire on the structured error regardless of nesting depth. As of v0.27.0 the op-level `(fallback: ...)` trailer is uniform: it contains a raised `execute_skill` child-throw (recursion-guard fire, missing-skill, or any error nested in the child), so `execute_skill(...) -> R (fallback: "...")` reliably degrades on a child failure regardless of nesting depth, and the fired fallback lands in `result.fallbacks[].reason`.
2067
2051
  - **`# 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.
2068
2052
 
2069
2053
  ## Forward-reference resolution
2070
2054
 
2071
- 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).
2055
+ Skill references (`inline(skill=...)`, `execute_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).
2072
2056
 
2073
2057
  **Lint behavior:**
2074
2058
  - `unknown-skill-reference` (tier-2) — covers `inline()` and `execute_skill()` with missing targets
2075
2059
  - `deferred-skill-reference` (tier-3 advisory) — teaching message: *"Skill 'X' referenced via `<op>` is not currently in the SkillStore. Will resolve at execute time if the skill exists by then, or throw `SkillNotFoundError` if not. If this is a typo, fix it now; if it's a forward reference, this advisory will clear once you store 'X'."*
2076
2060
 
2077
- **Runtime behavior:** when a deferred reference still can't resolve at execute time, the runtime throws `MissingSkillReferenceError extends OpError` with structured fields (`missingSkillName`, `viaOp` for the op kind, inherited `target` and `opKind`). The error flows through `# OnError:` fallback chain naturally.
2061
+ **Runtime behavior:** when a deferred reference still can't resolve at execute time, the runtime throws `MissingSkillReferenceError extends OpError` with structured fields (`missingSkillName`, `viaOp` for the op kind, inherited `target` and `opKind`). The error is containable by a target-level `else:` handler (or an op-level `(fallback: ...)`) naturally.
2078
2062
 
2079
2063
  **Stronger contracts kept tier-1:**
2080
- - `# OnError: <missing>` — error-handler missing-at-runtime is the worst possible UX moment to discover a missing reference; explicit at compile is the right call.
2081
2064
  - `disabled-skill-reference` — pointing at a Disabled skill is a stronger contract than "missing yet to be authored"; explicit at compile.
2082
2065
 
2083
2066
  ## When to use composition vs other primitives
2084
2067
 
2085
2068
  Three distinct cases that look similar but have different intents:
2086
2069
 
2087
- 1. **Get a value back from another skill.** Use `execute_skill(skill_name="...") -> RESULT` and use `${RESULT}` locally — reaching declared returns (`${RESULT.X}`) or the output stream (`${RESULT.outputs.text}`). This is the composition primitive case.
2070
+ 1. **Get a value back from another skill.** Use `execute_skill(name="...") -> RESULT` and use `${RESULT}` locally — reaching declared returns (`${RESULT.X}`) or the output stream (`${RESULT.outputs.text}`). This is the composition primitive case.
2088
2071
 
2089
2072
  2. **Delegate work to an agent as a task.** Use `# Output: template: <agent>` to route a compiled artifact through AgentConnector. The receiving agent acts on the prompt. *This is the Template-skill story* — uses compile-as-delivery, not execute-and-bind.
2090
2073
 
@@ -2112,7 +2095,7 @@ default: greet
2112
2095
  # Status: Approved
2113
2096
 
2114
2097
  call_greeting:
2115
- execute_skill(skill_name="greeting") -> GREETING_RESULT
2098
+ execute_skill(name="greeting") -> GREETING_RESULT
2116
2099
  emit(text="Greeting skill said: ${GREETING_RESULT.outputs.text}")
2117
2100
 
2118
2101
  default: call_greeting
@@ -2141,7 +2124,7 @@ default: classify
2141
2124
  # Vars: INPUT="some text"
2142
2125
 
2143
2126
  call_with_inputs:
2144
- execute_skill(skill_name="classifier", inputs={"TEXT": "${INPUT}"}) -> R
2127
+ execute_skill(name="classifier", inputs={"TEXT": "${INPUT}"}) -> R
2145
2128
  emit(text="Classified as: ${R.VERDICT}")
2146
2129
 
2147
2130
  default: call_with_inputs
@@ -2154,17 +2137,19 @@ default: call_with_inputs
2154
2137
  # Status: Approved
2155
2138
 
2156
2139
  call_maybe_missing:
2157
- execute_skill(skill_name="might-not-exist") -> RESULT (fallback: "child unavailable")
2140
+ execute_skill(name="might-not-exist") -> RESULT (fallback: "child unavailable")
2158
2141
  emit(text="Result: ${RESULT.outputs.text}")
2159
2142
 
2160
2143
  default: call_maybe_missing
2161
2144
  ```
2162
2145
 
2146
+ As of v0.27.0 the `(fallback:)` trailer is uniform, so it contains an `execute_skill` child-throw (missing-skill, recursion-guard fire, or any error raised inside the child) as well as a missing/empty bind — `RESULT` degrades to `"child unavailable"` on any of those, and the fired fallback is recorded in `result.fallbacks[].reason`.
2147
+
2163
2148
  **TestFlight preview (from the runtime caller, not from inside a skill):**
2164
2149
 
2165
2150
  ```
2166
2151
  execute_skill({
2167
- skill_name: "parent",
2152
+ name: "parent",
2168
2153
  mechanical: true
2169
2154
  })
2170
2155
  ```
@@ -2182,7 +2167,7 @@ For *data skills* (skills marked `# Type: data`), the compile-time inline primit
2182
2167
  - 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.
2183
2168
  - 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.
2184
2169
  - 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: []`.
2185
- - Pair composition with `(fallback: ...)` when the child skill might fail and the parent has a sensible degraded path.
2170
+ - Pair composition with `(fallback: ...)` when the child skill might fail and the parent has a sensible degraded path. As of v0.27.0 the `(fallback:)` catches a raised child throw, not just a missing bind.
2186
2171
  - Use mechanical mode to TestFlight any multi-skill chain before shipping it as a Headless skill on a cron trigger.
2187
2172
  - Forward references work — author sibling skills in any order, validate independently. The tier-2 warning surfaces the deferred-resolution path; runtime catches genuine misses.
2188
2173
  - Recursion is legal but bounded. If your design requires deeper recursion than the configured limit, reshape the workflow — almost always a sign of an iteration that should be expressed as `foreach` rather than recursion.
@@ -2190,18 +2175,6 @@ For *data skills* (skills marked `# Type: data`), the compile-time inline primit
2190
2175
  ## Session isolation — a skill can't mutate the calling agent's session
2191
2176
  A skill executes in its OWN session; agent session state (context, persona) is session-local. Calling `amp_set_session_context` (or similar) inside a skill sets it for the SKILL's session, not the caller's — the caller sees no change. The contract: **skills assemble and RETURN data; the agent owns its SESSION STATE.** To "enter a project," a skill returns the instruction bodies and the AGENT applies its own session context.
2192
2177
 
2193
- ## Current-runtime corrections (v0.27.0)
2194
-
2195
- This section predates the v0.27.0 error-containment model. Corrections to how it describes failure handling:
2196
-
2197
- 1. **`# OnError:` is parsed but INERT — it never fires.** Everywhere this section says a "`# OnError:` fallback fires if declared" (skill resolution, `MissingSkillReferenceError` handling, the deferred-reference runtime path, the fallback-chain in Error surfacing), that is aspirational, not current behavior. The `fallbackSkillExecutor` is declared but never wired. For reliable throw-containment of a child failure, use the target-level `else:` handler (`${ERROR_CONTEXT}` available) or the op-level `(fallback: ...)` trailer. Read every "`# OnError:`" in this section as "`else:`".
2198
-
2199
- 2. **`(fallback: ...)` is uniform (v0.27.0+).** The op-trailer now contains ANY op failure — a missing/unresolved value OR a raised throw — including an `execute_skill` child-throw (recursion-guard fire, missing-skill, or any error nested in the child). So `execute_skill(...) -> R (fallback: "child unavailable")` reliably degrades on a child failure regardless of nesting depth, and the fired fallback lands in `result.fallbacks[].reason`. This strengthens the "Pair composition with `(fallback: ...)`" guidance: it now works against raised child errors, not just a missing bind.
2200
-
2201
- 3. **`execute_skill` canonical kwarg is `name=`.** Examples here use `skill_name=`, which remains a back-compat alias and still works. New authoring should prefer `execute_skill(name="child", inputs={...})`.
2202
-
2203
- The "Nested errors do NOT surface at the top level" observation remains accurate and important — inspect the bound `${R.errors}` or rely on `(fallback:)`/`else:`; a top-level `errors: []` does not mean nothing failed downstream.
2204
-
2205
2178
  ## Static vs Dynamic — skill execution model
2206
2179
 
2207
2180
  Orthogonal to the three skill categories (Headless / Augmenting / Template, which describe the skill's relationship to the frontier agent), every skill has an *execution model* that describes its relationship to the Skillscript runtime.
@@ -2323,7 +2296,6 @@ Normal `prompt` / `prose` compilation ignores the `# Tests:` section entirely
2323
2296
  ### Runtime assertions (for `format: "test"` execution)
2324
2297
 
2325
2298
  - `target_else_executed: "<target_name>"` — verifies the `else:` branch ran
2326
- - `onerror_invoked: "<fallback_skill>"` — verifies the `# OnError:` skill was called
2327
2299
  - `op_fallback_used: "<target.op_index>"` — verifies an op-level fallback value was substituted
2328
2300
  - `result_value: "<expected_string>"` — the skill's final output value
2329
2301
 
@@ -2566,12 +2538,12 @@ Scopes: skill-local (persists across fires of this skill, not visible to other s
2566
2538
 
2567
2539
  ## Time as first-class primitives
2568
2540
 
2569
- Currently `$(NOW)` (wall-clock). Planned relative-time primitives:
2541
+ Currently `${NOW}` (wall-clock). Planned relative-time primitives:
2570
2542
 
2571
2543
  ```
2572
- $(SECONDS_SINCE_LAST_USER_MESSAGE)
2573
- $(MINUTES_SINCE_SESSION_START)
2574
- $(SECONDS_SINCE_LAST_FIRE_OF.<skill-name>)
2544
+ ${SECONDS_SINCE_LAST_USER_MESSAGE}
2545
+ ${MINUTES_SINCE_SESSION_START}
2546
+ ${SECONDS_SINCE_LAST_FIRE_OF.<skill-name>}
2575
2547
  ```
2576
2548
 
2577
2549
  Most "right time" reasoning is relative, not wall-clock.
@@ -2585,17 +2557,13 @@ Most "right time" reasoning is relative, not wall-clock.
2585
2557
  - **Cross-skill pub-sub** — `# Publishes: signal.X` / `# Subscribes: signal.Y` decoupling
2586
2558
  - **Confidence/threshold gating** — `# RequiresConfidence: classifier >= 0.8` / `# RequiresThreshold:`
2587
2559
  - **Invocation-control axis** — `# Invocable-By: user | agent | trigger` (sensitive ops shouldn't leak across invocation boundaries)
2588
- - **Introspection primitives** — `$(PROMPT_CONTEXT.size)`, `$(SKILLS_FIRED_RECENTLY.last-1h)`, `$(SELF.confidence-trend)`
2560
+ - **Introspection primitives** — `${PROMPT_CONTEXT.size}`, `${SKILLS_FIRED_RECENTLY.last-1h}`, `${SELF.confidence-trend}`
2589
2561
 
2590
2562
  ## When the language extends, this section shrinks
2591
2563
 
2592
2564
  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.
2593
2565
 
2594
- ## Notation correction
2595
-
2596
- The planned time-primitive and introspection examples in this section are written with the `$(NAME)` paren sigil (`$(NOW)`, `$(SECONDS_SINCE_LAST_USER_MESSAGE)`, `$(PROMPT_CONTEXT.size)`, etc.). Skillscript's substitution sigil is `${...}` (brace) — the paren form is reserved to avoid bash `$(command)` collision (see Variable resolution). When these primitives ship they will use the brace form (`${NOW}`, `${SECONDS_SINCE_LAST_USER_MESSAGE}`), consistent with the Future-grammar section which already writes them correctly. Read the paren examples here as brace form.
2597
-
2598
2566
  ---
2599
2567
 
2600
- *Rendered from `skillscript/skillscript-language-reference` — 2026-07-09 10:21 EDT*
2568
+ *Rendered from `skillscript/skillscript-language-reference` — 2026-07-10 13:59 EDT*
2601
2569
  *Source of truth: AMP (`amp_render_document("skillscript/skillscript-language-reference")`)*
@@ -2,7 +2,7 @@
2
2
  "provenance_version": "1.0",
3
3
  "language_version": "1.0",
4
4
  "compiler_version": "0.1.0-dev",
5
- "compiled_at": "2026-07-09T14:37:33.651Z",
5
+ "compiled_at": "2026-07-10T18:06:49.305Z",
6
6
  "source_skill": {
7
7
  "name": "classify-support-ticket"
8
8
  },
@@ -2,7 +2,7 @@
2
2
  "provenance_version": "1.0",
3
3
  "language_version": "1.0",
4
4
  "compiler_version": "0.1.0-dev",
5
- "compiled_at": "2026-07-09T14:37:33.824Z",
5
+ "compiled_at": "2026-07-10T18:06:49.486Z",
6
6
  "source_skill": {
7
7
  "name": "data-store-roundtrip"
8
8
  },
@@ -2,7 +2,7 @@
2
2
  "provenance_version": "1.0",
3
3
  "language_version": "1.0",
4
4
  "compiler_version": "0.1.0-dev",
5
- "compiled_at": "2026-07-09T14:37:34.028Z",
5
+ "compiled_at": "2026-07-10T18:06:49.652Z",
6
6
  "source_skill": {
7
7
  "name": "doc-qa-with-citations"
8
8
  },
@@ -2,7 +2,7 @@
2
2
  "provenance_version": "1.0",
3
3
  "language_version": "1.0",
4
4
  "compiler_version": "0.1.0-dev",
5
- "compiled_at": "2026-07-09T14:37:34.201Z",
5
+ "compiled_at": "2026-07-10T18:06:49.855Z",
6
6
  "source_skill": {
7
7
  "name": "feedback-sentiment-scan"
8
8
  },
@@ -2,7 +2,7 @@
2
2
  "provenance_version": "1.0",
3
3
  "language_version": "1.0",
4
4
  "compiler_version": "0.1.0-dev",
5
- "compiled_at": "2026-07-09T14:37:34.363Z",
5
+ "compiled_at": "2026-07-10T18:06:50.004Z",
6
6
  "source_skill": {
7
7
  "name": "hello-world"
8
8
  },
@@ -2,7 +2,7 @@
2
2
  "provenance_version": "1.0",
3
3
  "language_version": "1.0",
4
4
  "compiler_version": "0.1.0-dev",
5
- "compiled_at": "2026-07-09T14:37:34.545Z",
5
+ "compiled_at": "2026-07-10T18:06:50.178Z",
6
6
  "source_skill": {
7
7
  "name": "morning-brief"
8
8
  },
@@ -2,7 +2,7 @@
2
2
  "provenance_version": "1.0",
3
3
  "language_version": "1.0",
4
4
  "compiler_version": "0.1.0-dev",
5
- "compiled_at": "2026-07-09T14:37:34.728Z",
5
+ "compiled_at": "2026-07-10T18:06:50.377Z",
6
6
  "source_skill": {
7
7
  "name": "queue-length-monitor"
8
8
  },
@@ -2,7 +2,7 @@
2
2
  "provenance_version": "1.0",
3
3
  "language_version": "1.0",
4
4
  "compiler_version": "0.1.0-dev",
5
- "compiled_at": "2026-07-09T14:37:34.940Z",
5
+ "compiled_at": "2026-07-10T18:06:50.550Z",
6
6
  "source_skill": {
7
7
  "name": "service-health-watch"
8
8
  },
@@ -2,7 +2,7 @@
2
2
  "provenance_version": "1.0",
3
3
  "language_version": "1.0",
4
4
  "compiler_version": "0.1.0-dev",
5
- "compiled_at": "2026-07-09T14:37:35.125Z",
5
+ "compiled_at": "2026-07-10T18:06:50.723Z",
6
6
  "source_skill": {
7
7
  "name": "skill-store-roundtrip"
8
8
  },
@@ -2,7 +2,7 @@
2
2
  "provenance_version": "1.0",
3
3
  "language_version": "1.0",
4
4
  "compiler_version": "0.1.0-dev",
5
- "compiled_at": "2026-07-09T14:37:35.325Z",
5
+ "compiled_at": "2026-07-10T18:06:50.897Z",
6
6
  "source_skill": {
7
7
  "name": "youtrack-morning-sweep"
8
8
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillscript-runtime",
3
- "version": "0.27.1",
3
+ "version": "0.27.3",
4
4
  "description": "Runtime, compiler, lint, CLI, and dashboard for Skillscript — a small declarative language for authoring agent workflows.",
5
5
  "license": "MIT",
6
6
  "author": "Scott Shwarts <scotts@pobox.com>",