skillscript-runtime 0.27.0 → 0.27.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +5 -1
- package/dist/cli.js +6 -2
- package/dist/cli.js.map +1 -1
- package/dist/errors.js +2 -2
- package/dist/errors.js.map +1 -1
- package/dist/help-content.js +5 -5
- package/docs/adopter-playbook.md +4 -6
- package/docs/configuration.md +1 -0
- package/docs/language-reference.md +127 -156
- package/examples/connectors/DataStoreTemplate/DataStoreTemplate.ts +14 -0
- package/examples/onboarding-scaffold/file-data-store.ts +13 -2
- package/examples/onboarding-scaffold/openai-local-model.ts +3 -1
- package/examples/onboarding-scaffold/tmux-shell-agent-connector.ts +41 -5
- package/examples/skillscripts/classify-support-ticket.skill.provenance.json +10 -0
- package/examples/skillscripts/data-store-roundtrip.skill.provenance.json +10 -0
- package/examples/skillscripts/doc-qa-with-citations.skill.provenance.json +10 -0
- package/examples/skillscripts/feedback-sentiment-scan.skill.provenance.json +10 -0
- package/examples/skillscripts/hello-world.skill.provenance.json +1 -1
- package/examples/skillscripts/morning-brief.skill.md +5 -6
- package/examples/skillscripts/morning-brief.skill.provenance.json +10 -0
- package/examples/skillscripts/queue-length-monitor.skill.provenance.json +10 -0
- package/examples/skillscripts/service-health-watch.skill.provenance.json +10 -0
- package/examples/skillscripts/skill-store-roundtrip.skill.provenance.json +10 -0
- package/examples/skillscripts/youtrack-morning-sweep.skill.provenance.json +10 -0
- package/package.json +1 -1
|
@@ -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(
|
|
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:`
|
|
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(
|
|
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** —
|
|
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:`
|
|
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,7 +669,7 @@ 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(
|
|
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 |
|
|
@@ -704,7 +704,7 @@ Injected automatically at runtime; never declared by the author.
|
|
|
704
704
|
| `$\{TRIGGER_TYPE}` | What event fired this skill |
|
|
705
705
|
| `$\{TRIGGER_PAYLOAD}` | Event-specific data |
|
|
706
706
|
| `$\{EVENT.*}` | Event-payload fields populated by the trigger source |
|
|
707
|
-
| `$\{ERROR_CONTEXT}` |
|
|
707
|
+
| `$\{ERROR_CONTEXT}` | Inside a target's `else:` error-handler: kind + message + target of the failure. |
|
|
708
708
|
|
|
709
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.
|
|
710
710
|
|
|
@@ -954,6 +954,8 @@ emit:
|
|
|
954
954
|
emit(text="nested: ${ISSUE.customFields.Assignee|fallback:\"unassigned\"}")
|
|
955
955
|
```
|
|
956
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
|
+
|
|
957
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.
|
|
958
960
|
|
|
959
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.
|
|
@@ -1018,7 +1020,7 @@ elif ${BODY|length} > 1000:
|
|
|
1018
1020
|
|
|
1019
1021
|
## Error handling
|
|
1020
1022
|
|
|
1021
|
-
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:`
|
|
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.
|
|
1022
1024
|
|
|
1023
1025
|
Bare `${NAME}` without a filter is unchanged.
|
|
1024
1026
|
|
|
@@ -1510,7 +1512,7 @@ The receiving agent reads `event_type` for routing ("911 — surface now" vs "ro
|
|
|
1510
1512
|
|
|
1511
1513
|
## Output routing failures
|
|
1512
1514
|
|
|
1513
|
-
If `# Output: agent: <name>` fires and the wired AgentConnector throws, delivery routes through `else:`
|
|
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.
|
|
1514
1516
|
|
|
1515
1517
|
## Connectors — substrate routing, the five connector types, agent_id resolution
|
|
1516
1518
|
|
|
@@ -1751,7 +1753,7 @@ interface PortableMemory {
|
|
|
1751
1753
|
score?: number;
|
|
1752
1754
|
|
|
1753
1755
|
// Curated substrate subset — concept-portable, value-substrate-specific.
|
|
1754
|
-
// Top-level access via $
|
|
1756
|
+
// Top-level access via ${MEMORY.field}. Connectors populate when the
|
|
1755
1757
|
// concept applies. MUST NOT also be duplicated into metadata.
|
|
1756
1758
|
thread_status?: string;
|
|
1757
1759
|
pinned?: boolean;
|
|
@@ -1765,7 +1767,7 @@ interface PortableMemory {
|
|
|
1765
1767
|
agent_id?: string;
|
|
1766
1768
|
vault?: string;
|
|
1767
1769
|
|
|
1768
|
-
// Substrate-specific bag. Accessed via $
|
|
1770
|
+
// Substrate-specific bag. Accessed via ${MEMORY.metadata.X}.
|
|
1769
1771
|
metadata?: Record<string, unknown>;
|
|
1770
1772
|
}
|
|
1771
1773
|
|
|
@@ -1784,13 +1786,13 @@ interface McpDispatchCtx {
|
|
|
1784
1786
|
|
|
1785
1787
|
## Field access semantics
|
|
1786
1788
|
|
|
1787
|
-
`$
|
|
1789
|
+
`${MEMORY.field}` resolves in tiers:
|
|
1788
1790
|
1. Core fields (id, summary, detail, score)
|
|
1789
1791
|
2. Curated substrate subset (thread_status, pinned, etc.)
|
|
1790
1792
|
3. `metadata.X` for everything else
|
|
1791
|
-
4. Ambient passthrough as literal `$
|
|
1793
|
+
4. Ambient passthrough as literal `${MEMORY.field}` if unresolved
|
|
1792
1794
|
|
|
1793
|
-
**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 `$
|
|
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.
|
|
1794
1796
|
|
|
1795
1797
|
## Why connector abstraction matters
|
|
1796
1798
|
|
|
@@ -1798,224 +1800,193 @@ Hard-coupling skills to specific substrates would make information-flow decision
|
|
|
1798
1800
|
|
|
1799
1801
|
## Lifecycle and status — # Status: header, three canonical states (Draft / Approved / Disabled), compile + runtime enforcement
|
|
1800
1802
|
|
|
1801
|
-
Skillscripts carry an explicit lifecycle state via the `# Status:` header. The compiler and runtime enforce
|
|
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.
|
|
1802
1804
|
|
|
1803
1805
|
## Header syntax
|
|
1804
1806
|
|
|
1805
1807
|
```
|
|
1806
1808
|
# Skill: support-response-draft
|
|
1807
|
-
# Status: Approved
|
|
1809
|
+
# Status: Approved v3:<signature>
|
|
1808
1810
|
# Description: ...
|
|
1809
1811
|
```
|
|
1810
1812
|
|
|
1811
|
-
If `# Status:` is omitted, the default
|
|
1813
|
+
If `# Status:` is omitted, the default is **Draft** — authors must explicitly promote through the lifecycle rather than treating "newly written" as "ready."
|
|
1812
1814
|
|
|
1813
|
-
**Case normalization:**
|
|
1815
|
+
**Case normalization:** the state word is accepted case-insensitively and stored canonical (`draft` / `Draft` / `DRAFT` → `Draft`).
|
|
1814
1816
|
|
|
1815
1817
|
## The three canonical states
|
|
1816
1818
|
|
|
1817
|
-
- **Draft** — being authored
|
|
1818
|
-
- **Approved** —
|
|
1819
|
-
- **Disabled** — explicitly off. Compile
|
|
1820
|
-
|
|
1821
|
-
These three states have crisp, universal operational meaning across every deployment. Every operator understands what each state means; no judgment calls about edge-case distinctions.
|
|
1822
|
-
|
|
1823
|
-
## Hash-token approval for Approved
|
|
1819
|
+
- **Draft** — being authored/revised; not production. Compile warns; the runtime refuses effectful execution unless explicitly force-run for the author's own testing; declared triggers are held non-firing.
|
|
1820
|
+
- **Approved** — reviewed and cleared to fire. In secured mode this requires a valid operator **signature** (below). Compile clean; runtime allows; declared triggers fire.
|
|
1821
|
+
- **Disabled** — explicitly off. Compile + runtime reject; triggers don't fire. Source and version history preserved, but the skill cannot execute under any path.
|
|
1824
1822
|
|
|
1825
|
-
|
|
1823
|
+
## Approval by operator signature (secured mode)
|
|
1826
1824
|
|
|
1827
|
-
The runtime verifies
|
|
1828
|
-
- Trigger-fired dispatch
|
|
1829
|
-
- MCP `execute_skill` invocation
|
|
1830
|
-
- In-skill `$ execute_skill` composition
|
|
1831
|
-
- Compile-time `inline(skill=...)` references
|
|
1825
|
+
In **secured mode**, `# Status: Approved` requires a valid **Ed25519 signature** over the skill body: `# Status: Approved v3:<signature>`. The signature is applied **operator-side** (the human's key); the runtime **verifies it on every execution path** — trigger dispatch, MCP `execute_skill`, in-skill `$ execute_skill` composition, compile-time `inline(skill=...)`. **The runtime never holds the signing key.** An unsigned, invalidly-signed, or tampered body is **inert** — it cannot perform effectful ops no matter how it's dispatched.
|
|
1832
1826
|
|
|
1833
|
-
|
|
1827
|
+
Two paths produce a signed Approved skill:
|
|
1828
|
+
1. **Dashboard approval** — a human reviews and approves; the dashboard signs.
|
|
1829
|
+
2. **`skillfile approve` CLI** — signs the body operator-side.
|
|
1834
1830
|
|
|
1835
|
-
|
|
1836
|
-
2. **`skill_write` MCP tool auto-stamp** — when an agent writes a skill body declaring `# Status: Approved`, the SkillStore auto-stamps the token on persist (headless-adopter convenience).
|
|
1837
|
-
3. **Manual stamp** — for unusual cases; details in the adopter playbook.
|
|
1831
|
+
**An agent cannot self-approve.** In secured mode, `skill_write` with an agent-declared `# Status: Approved` is **forced to Draft on persist** — a write can't grant approval; only the operator's signature can. Editing a signed body invalidates the signature (it no longer matches the changed body), so a tampered Approved skill refuses to execute; re-approval (re-sign) is required after any edit. (This is the closure of the two self-approval vectors: the token is a real signature the agent can't forge, and the write path can't stamp itself Approved. Verified by adversarial red-team — no forgery path.)
|
|
1838
1832
|
|
|
1839
|
-
|
|
1833
|
+
In **unsecured mode** (secured is a deliberate on/off choice made at onboarding), signature enforcement is off and Approved fires without a signature. Secured mode is the posture for any deployment where untrusted or fully-autonomous authoring happens.
|
|
1840
1834
|
|
|
1841
|
-
## Compile + runtime behavior
|
|
1835
|
+
## Compile + runtime behavior (secured mode)
|
|
1842
1836
|
|
|
1843
|
-
| State | Compile | Runtime
|
|
1844
|
-
|
|
1845
|
-
| Draft | warn | refuse (
|
|
1846
|
-
| Approved (
|
|
1847
|
-
| Approved (
|
|
1848
|
-
| Disabled | refuse | refuse | refuse |
|
|
1837
|
+
| State | Compile | Runtime (effectful) | Default trigger fire |
|
|
1838
|
+
|-------|---------|---------------------|----------------------|
|
|
1839
|
+
| Draft | warn | refuse (author force-run only) | refuse |
|
|
1840
|
+
| Approved (valid signature) | OK | allow | allow |
|
|
1841
|
+
| Approved (unsigned / invalid) | warn | refuse | refuse |
|
|
1842
|
+
| Disabled | refuse | refuse | refuse |
|
|
1849
1843
|
|
|
1850
1844
|
## Trigger registry interaction
|
|
1851
1845
|
|
|
1852
|
-
The trigger registry respects status
|
|
1853
|
-
|
|
1854
|
-
When a skillscript transitions to Approved (with valid stamp), its triggers activate. When it transitions to Disabled, its triggers deactivate.
|
|
1846
|
+
The trigger registry respects status: a Draft/Disabled skill's declared triggers are registered (visible via `list_triggers`) but the scheduler skips dispatch. On transition to a valid Approved, triggers activate; on Disabled, they deactivate. This lets authors register triggers while still in Draft without risking accidental production fires.
|
|
1855
1847
|
|
|
1856
1848
|
## State transitions
|
|
1857
1849
|
|
|
1858
|
-
|
|
1850
|
+
In **secured mode**, promotion to Approved is not a freeform header edit — it requires the operator signature (dashboard / `skillfile approve`). Demotion to Draft or Disabled by any author with write authority is fine. In unsecured mode, transitions are freeform header edits.
|
|
1859
1851
|
|
|
1860
1852
|
## Audit trail
|
|
1861
1853
|
|
|
1862
|
-
Status changes are visible via the storage substrate's versioning
|
|
1854
|
+
Status changes are visible via the storage substrate's versioning — a `skill_versions` row for SqliteSkillStore, git history for FilesystemSkillStore. The audit trail is part of the substrate, not the language.
|
|
1863
1855
|
|
|
1864
1856
|
## States considered but not implemented
|
|
1865
1857
|
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
- **Test** — distinct "passed compile but not production-ready" state. Today's Draft covers this case (same behavior — refuse to fire under default dispatch). If authors find Draft and Test are operationally distinct in practice, Test ships then.
|
|
1869
|
-
- **Deployed** — distinct "currently shipping" state separate from Approved. Today's Approved + active triggers IS deployed; no operational difference. If a deployment finds Approved-vs-Deployed meaningfully different (e.g., a release-gating workflow that distinguishes "ready" from "live"), Deployed ships then.
|
|
1870
|
-
- **Deprecated** — soft-warn state for "still works but new authoring should use a successor." Deprecation is currently carried in metadata (`deprecated: true` in frontmatter) + a lint warning at invocation sites. When deprecated skills accumulate enough that the metadata pattern is awkward, Deprecated promotes to a first-class state.
|
|
1871
|
-
|
|
1872
|
-
Adding states is additive — existing skills with the three-state model continue to work when new states are added.
|
|
1858
|
+
Test, Deployed, and Deprecated were considered and deferred — today's Draft/Approved cover their cases, and adding states later is additive. (Deprecation is currently carried as metadata + a lint warning at invocation sites.)
|
|
1873
1859
|
|
|
1874
1860
|
## Why this matters
|
|
1875
1861
|
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
## Open questions
|
|
1879
|
-
|
|
1880
|
-
- **Status + composition.** When a procedural skill references a data skill via `inline(skill=...)`, what happens if the data skill is Disabled? Probable answer: compile-time error if any referenced skill is Disabled.
|
|
1881
|
-
- **Bulk status operations.** "Disable all skills tagged with project:legacy" is a useful operational primitive. May add a `skillscript bulk-status <pattern> <state>` CLI affordance later.
|
|
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.
|
|
1882
1863
|
|
|
1883
1864
|
## Error handling — else: blocks, # OnError: fallback, op-level fallback values
|
|
1884
1865
|
|
|
1885
|
-
Skillscript
|
|
1866
|
+
Skillscript has no try/catch — error handling is authored. Two runtime mechanisms (local to global) plus a structural discipline that prevents failure outright.
|
|
1886
1867
|
|
|
1887
|
-
## Layer 1: Target-level `else:` block
|
|
1868
|
+
## Layer 1: Target-level `else:` block — the throw-container
|
|
1888
1869
|
|
|
1889
|
-
Runs if any op in the target's primary body
|
|
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).
|
|
1890
1871
|
|
|
1891
1872
|
```
|
|
1892
1873
|
fetch:
|
|
1893
1874
|
$ data_read mode=fts query=${TOPIC} limit=5 -> RESULT
|
|
1894
1875
|
else:
|
|
1895
|
-
emit(text="retrieval failed
|
|
1876
|
+
emit(text="retrieval failed: ${ERROR_CONTEXT.message}")
|
|
1896
1877
|
$set RESULT = ""
|
|
1897
1878
|
```
|
|
1898
1879
|
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
The keyword `else:` is shared between two purposes:
|
|
1902
|
-
- Conditional `else:` — appears after `if:` / `elif:` chain inside a target body
|
|
1903
|
-
- Target `else:` — appears as a sibling block after a target's primary body, as an error handler
|
|
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.
|
|
1904
1881
|
|
|
1905
|
-
|
|
1882
|
+
## Layer 2: Op-level `(fallback:)` value — uniform (v0.27.0+)
|
|
1906
1883
|
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
`else:` blocks may not declare their own error handlers (no nested catch). If an `else:` block fails, the whole target fails through `# OnError:` if present.
|
|
1910
|
-
|
|
1911
|
-
## Layer 2: Skill-level `# OnError:` header
|
|
1912
|
-
|
|
1913
|
-
Names a fallback skill to invoke if anything in the skill fails — including target-level errors that aren't caught by `else:`, compile errors, or the executing context running out of resources.
|
|
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.
|
|
1914
1885
|
|
|
1915
1886
|
```
|
|
1916
|
-
|
|
1917
|
-
|
|
1887
|
+
weather:
|
|
1888
|
+
shell(command="curl -s wttr.in/${LOC}?format=j1") -> RAW (fallback: "{}")
|
|
1889
|
+
$ json_parse ${RAW} -> W (fallback: "{}")
|
|
1890
|
+
$ llm prompt="Summarize: ${W}" -> SUMMARY (fallback: "summary unavailable")
|
|
1918
1891
|
```
|
|
1919
1892
|
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
The fallback skill receives:
|
|
1923
|
-
- The same inputs as the failing skill
|
|
1924
|
-
- An additional `${ERROR_CONTEXT}` ambient ref containing the error type and the target where it failed
|
|
1925
|
-
|
|
1926
|
-
### Constraint
|
|
1927
|
-
|
|
1928
|
-
Nested `# OnError:` is *not* supported. If `# OnError: degraded-skill` fires and `degraded-skill` itself errors, the runtime hard-exits with no further fallback. Spec is explicit on this.
|
|
1929
|
-
|
|
1930
|
-
## Layer 3: Op-level fallback values
|
|
1931
|
-
|
|
1932
|
-
Inline fallback declared on the op line. Used when the call fails or returns empty. Supported on `$` (MCP dispatch) ops and on `shell()` with coerce-on-bind semantics. For `shell()` the fallback fires on op throw OR empty stdout (e.g. `gh pr list` against a repo with no open PRs binds the fallback string).
|
|
1893
|
+
Fallback-value parsing is permissive — bare identifiers, quoted strings, and bracketed array literals all accepted:
|
|
1933
1894
|
|
|
1934
1895
|
```
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
$ slack.post channel=${CHANNEL} text=${SUMMARY} (fallback: "post failed silently") -> ACK
|
|
1939
|
-
shell(argv=["gh","pr","list","--repo","acme/foo"]) -> PRS (fallback: "No open PRs")
|
|
1896
|
+
$ data_read mode=fts query="..." -> RESULTS (fallback: []) # array literal
|
|
1897
|
+
$ llm prompt="..." -> VERDICT (fallback: unknown) # bare identifier
|
|
1898
|
+
$ slack.post text="..." -> ACK (fallback: "post failed") # quoted string
|
|
1940
1899
|
```
|
|
1941
1900
|
|
|
1942
|
-
|
|
1901
|
+
## Structural guard — prevent the failure
|
|
1943
1902
|
|
|
1944
|
-
|
|
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.
|
|
1945
1904
|
|
|
1946
|
-
|
|
1947
|
-
$ data_read mode=fts query="..." -> RESULTS (fallback: []) # array literal
|
|
1948
|
-
$ llm prompt="..." -> VERDICT (fallback: unknown) # bare identifier
|
|
1949
|
-
$ slack.post text="..." -> ACK (fallback: "post failed") # quoted string
|
|
1950
|
-
```
|
|
1905
|
+
## Choosing a mechanism
|
|
1951
1906
|
|
|
1952
|
-
|
|
1907
|
+
- **`(fallback:)`** — "any failure of this op → this default value." Uniform; the per-op opt-out. Reach for it first.
|
|
1908
|
+
- **`else:`** — target-level; when you need the error's details (`${ERROR_CONTEXT}`) to branch, or to contain a throw spanning several ops.
|
|
1909
|
+
- **Structural guard** — when you can cheaply validate the shape before the risky op; prevents the failure entirely.
|
|
1953
1910
|
|
|
1954
|
-
##
|
|
1911
|
+
## Deprecated: `# OnError:` — parsed but not wired
|
|
1955
1912
|
|
|
1956
|
-
-
|
|
1957
|
-
- Target error → caught by `# OnError:` if present, otherwise propagates to caller
|
|
1958
|
-
- Caller can still catch via standard exception handling on compile / runtime invocation APIs
|
|
1959
|
-
- `else:` blocks are not allowed to declare their own error handlers
|
|
1960
|
-
- If an `else:` block itself fails, the whole target fails through `# OnError:` (if present)
|
|
1913
|
+
A skill-level `# OnError: <fallback-skill>` header parses, but it is **not wired in the current runtime: the named fallback never fires.** A skill that relies on it has, in effect, no error handling — so don't use it. Use a target-level `else:` handler (or an op-level `(fallback:)`) instead. Removal of the header is planned for a future release (v0.28); until then it is inert, not a compile error. To recover at the whole-skill level, wrap the body in a target with an `else:` that dispatches your recovery skill:
|
|
1961
1914
|
|
|
1962
|
-
|
|
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
|
+
```
|
|
1963
1921
|
|
|
1964
|
-
|
|
1922
|
+
## Propagation
|
|
1965
1923
|
|
|
1966
|
-
|
|
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[]`).
|
|
1925
|
+
- A fired op-level `(fallback:)` is recorded in `result.fallbacks[]`, not `result.errors[]`.
|
|
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.)
|
|
1967
1928
|
|
|
1968
|
-
|
|
1969
|
-
- Compile-time: `# Requires: ... (fallback: value)`
|
|
1970
|
-
- Runtime op: `$ dispatch ... (fallback: value)` and `shell(...) ... (fallback: value)`
|
|
1971
|
-
- Runtime target: `else:` block
|
|
1972
|
-
- Whole skill: `# OnError:` header
|
|
1929
|
+
## Robustness & error containment — best practices (no try/catch by design)
|
|
1973
1930
|
|
|
1974
|
-
|
|
1931
|
+
**Skillscript has no try/catch — by design. Robustness is authored, not caught.** An unguarded fallible op that fails aborts the whole target — and in a fan-out, aborts every sibling that hadn't run yet. You contain failures explicitly.
|
|
1975
1932
|
|
|
1976
|
-
|
|
1933
|
+
**One uniform rule (v0.27.0+): `(fallback:)` contains ANY op failure.** A `-> VAR (fallback: "<default>")` trailer catches whatever goes wrong with that op — a `$`/`shell` dispatch error, a `shell` spawn-fail/timeout, an empty result, AND a raised throw (`$ json_parse` on off-shape input, `execute_skill` whose child throws). The op degrades to the fallback value and the target continues. (History: before v0.27.0, `execute_skill` and `$ json_parse` were runtime intercepts that threw before the fallback wiring, so `(fallback:)` did NOT catch their throws — you needed `else:` or a structural guard. v0.27.0 made the intercepts consult `op.fallback` like `file_read`, retiring that split. Legacy note for ≤0.26.x authors at the end.)
|
|
1977
1934
|
|
|
1978
|
-
|
|
1935
|
+
**The containment tools — pick by how much you want to handle:**
|
|
1936
|
+
| Tool | Catches | Scope |
|
|
1937
|
+
|---|---|---|
|
|
1938
|
+
| `(fallback: "…")` op trailer | ANY failure of that op — dispatch error / spawn-fail / timeout / empty result / raised throw | the single op |
|
|
1939
|
+
| `$\{ref\\|fallback:"x"}` filter | a missing / unresolved / empty value at use-time | the single reference |
|
|
1940
|
+
| `else:` block | a raised throw anywhere in the target body, WITH error context (`$\{ERROR_CONTEXT.kind/.message/.target}`) | the whole target |
|
|
1941
|
+
| structural guard (pre-bind defaults + a `contains`/shape check before the risky op) | PREVENTS the failure — the op isn't reached on bad input, so it can't fail | the risky op |
|
|
1942
|
+
| `# OnError: <skill>` | INERT in the current runtime (fallbackSkillExecutor never wired) — do NOT rely on it; prefer `else:` | — |
|
|
1979
1943
|
|
|
1980
|
-
|
|
1944
|
+
`(fallback:)` = "any failure → this default." `else:` = "I need the error's DETAILS to branch." Structural guard = "I'd rather this never fail" (most robust — the failure never reaches a handler).
|
|
1981
1945
|
|
|
1982
|
-
**
|
|
1946
|
+
**Rule 1 — Fallback every fallible op whose failure shouldn't be fatal.** `shell`, `$` dispatch, `file_read`, `execute_skill`, `$ json_parse` can all fail; give each `-> VAR (fallback: "<default>")` unless its failure SHOULD abort the skill (leaving it bare is how you mark it critical).
|
|
1983
1947
|
|
|
1984
|
-
**Rule
|
|
1948
|
+
**Rule 2 — In a fan-out, fallback EACH leg.** A gather running N independent legs (weather + mailbox + calendar + …) must fallback each, or the first leg to fail takes the rest down — later ops never run, every output empties. One non-critical leg must never sink its siblings. For a leg whose child can throw, ALSO throw-proof the child (below) — belt and suspenders, and it keeps the failure legible at the leg. (Observed 2026-07-05: an unguarded weather leg threw and aborted a 7-leg morning brief, six healthy legs lost. Fix that held: per-leg fallback + a throw-proof weather child.)
|
|
1985
1949
|
|
|
1986
|
-
|
|
1950
|
+
Throw-proof child pattern (the strongest per-op guard — prevents rather than catches):
|
|
1951
|
+
```
|
|
1952
|
+
fetch:
|
|
1953
|
+
$set AREA = "(weather unavailable)" # degraded default — always bound
|
|
1954
|
+
if ${RAW|contains:"current_condition"}: # guard BEFORE the risky op
|
|
1955
|
+
$ json_parse ${RAW} -> W
|
|
1956
|
+
$set AREA = "${W.nearest_area|fallback:\"(weather unavailable)\"}"
|
|
1957
|
+
default: fetch
|
|
1958
|
+
```
|
|
1959
|
+
Bad input skips the `if`, the default stands, the child never throws — so a parent gather can't be sunk by it (independent of the parent's own fallback).
|
|
1987
1960
|
|
|
1988
|
-
**Rule 3 — a
|
|
1989
|
-
- A chain with NO `|fallback` still throws on an unresolved ref (`${x|trim}` on undefined `x` throws) — that's your signal to add one.
|
|
1990
|
-
- `|fallback` only rescues the genuinely UNRESOLVED case. A present-but-empty value (e.g. `\" \"`) still flows through the filters, so `${x|length|fallback:\"0\"}` on `x=\" \"` returns `\"2\"`, not `\"0\"`.
|
|
1961
|
+
**Rule 3 — a `fallback` filter anywhere in a chain rescues an unresolved reference** (order-independent, v0.26.2+; also rescues a missing dotted/numeric path on a present object — `${W.a.0.b|fallback:"d"}` when `W` lacks `a` → `"d"`). A chain with NO `fallback` still throws on an unresolved ref — that's your signal to add one. It rescues only genuinely-unresolved refs; a present-but-empty value (`" "`) still flows through the other filters, so `${x|length|fallback:"0"}` on `" "` is `"2"`.
|
|
1991
1962
|
|
|
1992
|
-
**Rule 4 — A body-text output template must not reference a var a fallible step might leave unset.** The template renders after the target runs;
|
|
1963
|
+
**Rule 4 — A body-text output template must not reference a var a fallible step might leave unset.** The template renders after the target runs; an unset var hard-fails the render (`Unresolved variable reference: $(AREA)`) — and that is exactly how a child skill throws OUT to its `execute_skill` parent. Pre-bind every template var to a default before the fallible step (this doubles as the throw-prevention structure in Rule 2), or source each with a `|fallback`.
|
|
1993
1964
|
|
|
1994
|
-
**Rule 5 — Degrade LOUD, not silent.** A fallback value should be a visible marker (
|
|
1965
|
+
**Rule 5 — Degrade LOUD, not silent.** A fallback value should be a visible marker ("unavailable", "—", "n/a") — never empty, never a plausible-but-wrong value. A degraded run must be diagnosable: the throw's reason is preserved in `result.fallbacks[].reason`, so a degraded leg stays traceable. Don't silently ship a blank or a fake reading. Clean-degrade beats both a hard abort and a silent lie.
|
|
1995
1966
|
|
|
1996
|
-
**
|
|
1967
|
+
**Legacy note (≤0.26.x runtimes):** before v0.27.0, `(fallback:)` did NOT catch a raised throw from `execute_skill` or `$ json_parse` (they bypassed op.fallback). On an older runtime, contain those two with `else:` or a structural guard — a `(fallback:)` on them is a no-op against a throw. v0.27.0+ unifies it, so this only matters if you must support an old runtime.
|
|
1997
1968
|
|
|
1998
1969
|
## Composition — skills calling skills
|
|
1999
1970
|
|
|
2000
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.
|
|
2001
1972
|
|
|
2002
|
-
Composition is exposed as a runtime-intrinsic op: `execute_skill(
|
|
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.)
|
|
2003
1974
|
|
|
2004
1975
|
## Surface
|
|
2005
1976
|
|
|
2006
1977
|
```
|
|
2007
1978
|
parent:
|
|
2008
|
-
execute_skill(
|
|
1979
|
+
execute_skill(name="child") -> RESULT
|
|
2009
1980
|
emit(text="Child returned: ${RESULT}")
|
|
2010
1981
|
```
|
|
2011
1982
|
|
|
2012
|
-
The runtime resolves `
|
|
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.
|
|
2013
1984
|
|
|
2014
1985
|
## Tool signature
|
|
2015
1986
|
|
|
2016
1987
|
```
|
|
2017
1988
|
execute_skill({
|
|
2018
|
-
|
|
1989
|
+
name: string, // required — resolves via SkillStore (skill_name= accepted as back-compat alias)
|
|
2019
1990
|
inputs?: Record<string,string>, // optional — Vars override map
|
|
2020
1991
|
mechanical?: boolean // optional — dry-run mode (default false)
|
|
2021
1992
|
})
|
|
@@ -2062,7 +2033,7 @@ default: fetch
|
|
|
2062
2033
|
|
|
2063
2034
|
## Semantics
|
|
2064
2035
|
|
|
2065
|
-
**Skill resolution.** Missing skills produce a clean structured error (`MissingSkillReferenceError extends OpError`) — the parent's `(fallback: ...)` discipline applies if specified, otherwise
|
|
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.
|
|
2066
2037
|
|
|
2067
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.
|
|
2068
2039
|
|
|
@@ -2076,28 +2047,27 @@ default: fetch
|
|
|
2076
2047
|
|
|
2077
2048
|
Two non-obvious behaviors when reading a child's failures from the parent (both confirmed by the v1.0 runtime-semantics test battery):
|
|
2078
2049
|
|
|
2079
|
-
- **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: ...)` /
|
|
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`.
|
|
2080
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.
|
|
2081
2052
|
|
|
2082
2053
|
## Forward-reference resolution
|
|
2083
2054
|
|
|
2084
|
-
Skill references (`inline(skill=...)`, `execute_skill(
|
|
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).
|
|
2085
2056
|
|
|
2086
2057
|
**Lint behavior:**
|
|
2087
2058
|
- `unknown-skill-reference` (tier-2) — covers `inline()` and `execute_skill()` with missing targets
|
|
2088
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'."*
|
|
2089
2060
|
|
|
2090
|
-
**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
|
|
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.
|
|
2091
2062
|
|
|
2092
2063
|
**Stronger contracts kept tier-1:**
|
|
2093
|
-
- `# 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.
|
|
2094
2064
|
- `disabled-skill-reference` — pointing at a Disabled skill is a stronger contract than "missing yet to be authored"; explicit at compile.
|
|
2095
2065
|
|
|
2096
2066
|
## When to use composition vs other primitives
|
|
2097
2067
|
|
|
2098
2068
|
Three distinct cases that look similar but have different intents:
|
|
2099
2069
|
|
|
2100
|
-
1. **Get a value back from another skill.** Use `execute_skill(
|
|
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.
|
|
2101
2071
|
|
|
2102
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.
|
|
2103
2073
|
|
|
@@ -2125,7 +2095,7 @@ default: greet
|
|
|
2125
2095
|
# Status: Approved
|
|
2126
2096
|
|
|
2127
2097
|
call_greeting:
|
|
2128
|
-
execute_skill(
|
|
2098
|
+
execute_skill(name="greeting") -> GREETING_RESULT
|
|
2129
2099
|
emit(text="Greeting skill said: ${GREETING_RESULT.outputs.text}")
|
|
2130
2100
|
|
|
2131
2101
|
default: call_greeting
|
|
@@ -2154,7 +2124,7 @@ default: classify
|
|
|
2154
2124
|
# Vars: INPUT="some text"
|
|
2155
2125
|
|
|
2156
2126
|
call_with_inputs:
|
|
2157
|
-
execute_skill(
|
|
2127
|
+
execute_skill(name="classifier", inputs={"TEXT": "${INPUT}"}) -> R
|
|
2158
2128
|
emit(text="Classified as: ${R.VERDICT}")
|
|
2159
2129
|
|
|
2160
2130
|
default: call_with_inputs
|
|
@@ -2167,17 +2137,19 @@ default: call_with_inputs
|
|
|
2167
2137
|
# Status: Approved
|
|
2168
2138
|
|
|
2169
2139
|
call_maybe_missing:
|
|
2170
|
-
execute_skill(
|
|
2140
|
+
execute_skill(name="might-not-exist") -> RESULT (fallback: "child unavailable")
|
|
2171
2141
|
emit(text="Result: ${RESULT.outputs.text}")
|
|
2172
2142
|
|
|
2173
2143
|
default: call_maybe_missing
|
|
2174
2144
|
```
|
|
2175
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
|
+
|
|
2176
2148
|
**TestFlight preview (from the runtime caller, not from inside a skill):**
|
|
2177
2149
|
|
|
2178
2150
|
```
|
|
2179
2151
|
execute_skill({
|
|
2180
|
-
|
|
2152
|
+
name: "parent",
|
|
2181
2153
|
mechanical: true
|
|
2182
2154
|
})
|
|
2183
2155
|
```
|
|
@@ -2195,7 +2167,7 @@ For *data skills* (skills marked `# Type: data`), the compile-time inline primit
|
|
|
2195
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.
|
|
2196
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.
|
|
2197
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: []`.
|
|
2198
|
-
- 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.
|
|
2199
2171
|
- Use mechanical mode to TestFlight any multi-skill chain before shipping it as a Headless skill on a cron trigger.
|
|
2200
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.
|
|
2201
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.
|
|
@@ -2324,7 +2296,6 @@ Normal `prompt` / `prose` compilation ignores the `# Tests:` section entirely
|
|
|
2324
2296
|
### Runtime assertions (for `format: "test"` execution)
|
|
2325
2297
|
|
|
2326
2298
|
- `target_else_executed: "<target_name>"` — verifies the `else:` branch ran
|
|
2327
|
-
- `onerror_invoked: "<fallback_skill>"` — verifies the `# OnError:` skill was called
|
|
2328
2299
|
- `op_fallback_used: "<target.op_index>"` — verifies an op-level fallback value was substituted
|
|
2329
2300
|
- `result_value: "<expected_string>"` — the skill's final output value
|
|
2330
2301
|
|
|
@@ -2567,12 +2538,12 @@ Scopes: skill-local (persists across fires of this skill, not visible to other s
|
|
|
2567
2538
|
|
|
2568
2539
|
## Time as first-class primitives
|
|
2569
2540
|
|
|
2570
|
-
Currently `$
|
|
2541
|
+
Currently `${NOW}` (wall-clock). Planned relative-time primitives:
|
|
2571
2542
|
|
|
2572
2543
|
```
|
|
2573
|
-
$
|
|
2574
|
-
$
|
|
2575
|
-
$
|
|
2544
|
+
${SECONDS_SINCE_LAST_USER_MESSAGE}
|
|
2545
|
+
${MINUTES_SINCE_SESSION_START}
|
|
2546
|
+
${SECONDS_SINCE_LAST_FIRE_OF.<skill-name>}
|
|
2576
2547
|
```
|
|
2577
2548
|
|
|
2578
2549
|
Most "right time" reasoning is relative, not wall-clock.
|
|
@@ -2586,7 +2557,7 @@ Most "right time" reasoning is relative, not wall-clock.
|
|
|
2586
2557
|
- **Cross-skill pub-sub** — `# Publishes: signal.X` / `# Subscribes: signal.Y` decoupling
|
|
2587
2558
|
- **Confidence/threshold gating** — `# RequiresConfidence: classifier >= 0.8` / `# RequiresThreshold:`
|
|
2588
2559
|
- **Invocation-control axis** — `# Invocable-By: user | agent | trigger` (sensitive ops shouldn't leak across invocation boundaries)
|
|
2589
|
-
- **Introspection primitives** — `$
|
|
2560
|
+
- **Introspection primitives** — `${PROMPT_CONTEXT.size}`, `${SKILLS_FIRED_RECENTLY.last-1h}`, `${SELF.confidence-trend}`
|
|
2590
2561
|
|
|
2591
2562
|
## When the language extends, this section shrinks
|
|
2592
2563
|
|
|
@@ -2594,5 +2565,5 @@ When any of these primitives ship, the relevant grammar moves into its canonical
|
|
|
2594
2565
|
|
|
2595
2566
|
---
|
|
2596
2567
|
|
|
2597
|
-
*Rendered from `skillscript/skillscript-language-reference` — 2026-07-
|
|
2568
|
+
*Rendered from `skillscript/skillscript-language-reference` — 2026-07-09 18:31 EDT*
|
|
2598
2569
|
*Source of truth: AMP (`amp_render_document("skillscript/skillscript-language-reference")`)*
|