skillscript-runtime 0.16.4 → 0.16.5

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.
@@ -14,6 +14,10 @@ These are features designed or anticipated but not yet implemented in the curren
14
14
  - **`while CONDITION:` loops** — today's iteration is `foreach IDENT in EXPR:` only. While loops are planned for ad-hoc orchestration patterns ("loop until response contains 'done'").
15
15
  - **Arithmetic in `$set`** — today accepts literals + `${VAR}` interpolation; no `+ - * /` operators. Planned alongside `while` for turn counters and orchestration bookkeeping.
16
16
 
17
+ ## Array aggregation primitives
18
+
19
+ `|max`, `|min`, `|sum`, `|reduce` pipe-filters over arrays. Today the language tops out at "shape one record" — aggregating across an array requires `foreach` accumulator ceremony. Warm-agent Phase 3 weather skill (2026-06-01) couldn't do `Math.max(hourly.chanceofrain)` without the workaround. Planned as a design question: is `foreach` the deliberate ceiling for aggregation, or do we add primitives?
20
+
17
21
  ## Strings
18
22
 
19
23
  - **Multi-line / heredoc string literals** — today's `emit(text="...")` accepts single-line strings or `\n`-escaped multi-line. Planned: Python-style triple-quote `emit(text="""...""")` for ad-hoc prose blocks in template-kind skills.
@@ -48,10 +52,6 @@ $set NAME = value scope=session
48
52
 
49
53
  Scopes: skill-local (persists across fires of this skill, not visible to other skills), agent-global (visible to all skills of the same agent), session (alive for the duration of the current session, cleared at session end). Backed by a configured data-records connector.
50
54
 
51
- ## Per-skill / per-op timeouts
52
-
53
- `# Timeout:` skill-level header + per-op `timeoutSeconds=N` kwarg + runtime defaults. Hung dispatches today have no timeout cap.
54
-
55
55
  ## Sensors as a language category
56
56
 
57
57
  Distinct from triggers. Sensors are continuous channels the agent reads but doesn't emit on. Planned syntax:
@@ -86,6 +86,7 @@ Most "right time" reasoning is relative, not wall-clock.
86
86
  - **Channel/locality awareness** — `$(CHANNEL_TYPE)`, `$(CHANNEL_PRIVACY)` ambient refs for routing decisions
87
87
  - **Introspection primitives** — `$(PROMPT_CONTEXT.size)`, `$(SKILLS_FIRED_RECENTLY.last-1h)`, `$(SELF.confidence-trend)`
88
88
  - **Capability declarations** — `# Requires-Capabilities: sensors=[mic, camera], tools=[...]` (audit surface for operators)
89
+ - **`unknown-llm-arg` lint** — typo-catch for `$ llm` kwargs (today, unknown kwargs pass through silently to the connector). Sibling to the older `unknown-retrieval-arg` rule. Sharpening pass on `$ llm` typed-contract validation.
89
90
 
90
91
  ## When the language extends, this section shrinks
91
92
 
@@ -214,7 +215,7 @@ The following identifiers are reserved and cannot be used as variable names, tar
214
215
 
215
216
  **Mutation statements:** `$set`, `$append`
216
217
 
217
- **Runtime-intrinsic op names:** `emit`, `notify`, `ask`, `inline`, `execute_skill`, `shell`, `file_read`, `file_write` (the closed function-call list; see Ops Reference)
218
+ **Runtime-intrinsic op names:** `emit`, `notify`, `inline`, `execute_skill`, `shell`, `file_read`, `file_write` (the closed function-call list; see Ops Reference)
218
219
 
219
220
  **Control flow:** `default`, `needs`, `if`, `elif`, `else`, `foreach`, `in`, `not`, `unsafe`
220
221
 
@@ -284,7 +285,7 @@ The op surface is three classes, each with its own grammar and resolution path.
284
285
  |---|---|---|
285
286
  | **Mutation statements** | `$set VAR = value`, `$append VAR <value>` | Reserved keywords (parser dispatches directly). |
286
287
  | **Runtime-intrinsic function-calls** | `verb(kwarg=value, ...) [-> BINDING]` | Closed built-in list (below). Unknown verb → tier-1 `unknown-runtime-op`. |
287
- | **External MCP dispatch** | `$ <connector>[.<tool>] kwarg=value, ... [-> BINDING]` | `connectors.json` resolution at compile. Unknown connector → tier-1 `unknown-connector`. See External MCP dispatch subsection below for flat vs dotted dispatch shape. |
288
+ | **External MCP dispatch** | `$ <connector>.<tool> kwarg=value, ... [-> BINDING]` (substrate-specific) or `$ <tool> kwarg=value, ... [-> BINDING]` (typed-contract only) | `connectors.json` resolution at compile. Unknown connector → tier-1 `unknown-connector`. See External MCP dispatch subsection below for canonical form rules. |
288
289
 
289
290
  The `$` prefix is information-bearing: it marks **state-affecting ops** (mutation OR external dispatch). Function-call shape marks **language-intrinsic ops the runtime knows directly**. Parse-time discrimination is unambiguous — three grammars, three resolution paths, zero overlap.
290
291
 
@@ -359,7 +360,7 @@ Closed list of language-intrinsic ops the runtime knows directly. Each is a func
359
360
  | `file_read` | `file_read(path="...") -> R` | required | Read a file at `path`; binds string contents. |
360
361
  | `file_write` | `file_write(path="...", content="...")` | none | Write `content` to `path`. `mkdir -p` semantics for parent directories. Mutation-classified. |
361
362
 
362
- **Unknown op name** → tier-1 lint `unknown-runtime-op` with remediation pointing at MCP dispatch: "if this is an external tool, use `$ tool_name args -> R`."
363
+ **Unknown op name** → tier-1 lint `unknown-runtime-op` with remediation pointing at MCP dispatch: "if this is an external tool, use `$ <connector>.<tool> args -> R`."
363
364
 
364
365
  ### `emit` — delivery-channel append
365
366
 
@@ -389,10 +390,6 @@ Synchronous alert to a named agent via wired AgentConnector(s). **Contrast with
389
390
 
390
391
  Returns ACK `{agent, dispatched: [{connector, ok, error?}]}` — fire-and-forget callers ignore the binding; check-delivery callers inspect ACK.
391
392
 
392
- ### Removed in v0.16.0: `ask` runtime intrinsic
393
-
394
- `ask(prompt="...")` was removed because it conflated two unrelated concerns: (1) surfacing a question to a user, which requires an interactive channel the runtime can't guarantee for cron/event-fired skills, and (2) acting as a mutation gate. The mutation-gate role is now covered by per-op `approved="reason"` kwarg + skill-level `# Autonomous: true`. For input-collection workflows, use `emit(text="...")` and have the caller handle the round-trip.
395
-
396
393
  ### `shell` — sandboxed or unsafe shell exec
397
394
 
398
395
  **Sandboxed default:**
@@ -424,7 +421,7 @@ file_read(path="/tmp/state.json") -> STATE
424
421
  file_write(path="/tmp/report.md", content="${REPORT}", approved="nightly sweep deliverable")
425
422
  ```
426
423
 
427
- `file_read` is read-only (always allowed). `file_write` is mutation-classified — requires `# Autonomous: true` declaration on the skill OR per-call `approved="..."` kwarg OR a preceding `ask` gate in the same target. `mkdir -p` semantics for the parent directory.
424
+ `file_read` is read-only (always allowed). `file_write` is mutation-classified — requires `# Autonomous: true` declaration on the skill OR per-call `approved="..."` kwarg. `mkdir -p` semantics for the parent directory.
428
425
 
429
426
  `unconfirmed-mutation` lint enforces the mutation-classification rule.
430
427
 
@@ -432,11 +429,11 @@ file_write(path="/tmp/report.md", content="${REPORT}", approved="nightly sweep d
432
429
 
433
430
  ```
434
431
  brief:
435
- $ llm prompt="${VOICE_RULES} Now write a one-line status:" model=qwen -> RESULT
432
+ $ llm prompt="${VOICE_RULES} Now write a one-line status:" -> RESULT
436
433
  inline(skill="voice-rules")
437
434
  ```
438
435
 
439
- Inlines an Approved `# Type: data` skill into the host skill's compiled artifact at the call site. Resolved at `compile()` time; the data skill's `content_hash` is recorded in the host's provenance. `skillfile audit` detects stale recompiles when a referenced data skill changes.
436
+ Inlines an Approved `# Type: data` skill into the host skill's compiled artifact at the call site. Resolved at `compile()` time; the data skill's `content_hash` is recorded in the host's provenance. `skillscript audit` detects stale recompiles when a referenced data skill changes.
440
437
 
441
438
  See Composition section for the distinction between `inline` (compile-time), `execute_skill` (in-skill runtime call), and dispatched skills.
442
439
 
@@ -455,43 +452,165 @@ Runtime-resolved against the SkillStore. Recursion-depth-guarded (default 10).
455
452
 
456
453
  Calls a tool through a configured connector. Connector name resolves against `connectors.json`. Output binds via optional `-> VAR`. Adopter-side contract details + connector wiring conventions live in the adopter playbook.
457
454
 
458
- ### Two dispatch forms — flat and dotted
455
+ ### Dispatch form
456
+
457
+ **Substrate-specific tools use named (dotted) form:**
459
458
 
460
- Both forms are first-class. Neither is canonical; choose by what makes the call site clearer.
459
+ ```
460
+ $ <connector>.<tool> kwarg=value, ... [-> R]
461
+ ```
461
462
 
462
- **Flat-name dispatch** is the common case. The tool name resolves against the wired connector (most often the `primary`/`default` connector's tool list, or a dedicated entry per tool):
463
+ The text before the dot is the connector name (must match an entry in `connectors.json`); the rest is the tool name + args. Named form is canonical for any tool whose semantics are substrate-specific adopter MCP servers (`amp_*`, `github_*`, `linear_*`, etc.).
463
464
 
464
465
  ```
465
- $ ticketing_search query="project:INFRA" -> R
466
- $ llm prompt="${INPUT}" -> V
467
- $ data_read mode=fts query="..." limit=5 -> M
466
+ $ amp.amp_check_mailbox limit=20 -> MAIL
467
+ $ amp.amp_write_memory summary="..." domain_tags=["x"] vault="private" -> ACK
468
+ $ github.search_issues query="repo:acme/foo state:open" -> ISSUES
469
+ $ linear.find_issues filter="project:INFRA" -> TICKETS
468
470
  ```
469
471
 
470
- **Dotted-prefix dispatch** is the explicit-routing escape hatchuseful when multiple connectors expose tools with overlapping names, or when an adopter wants the connector identity visible at the call site for audit clarity:
472
+ The skill body literally names its substrate edges reading the body tells you which MCP servers it depends on, with no `primary` config indirection.
473
+
474
+ **Typed-contract tools use bare form:**
471
475
 
472
476
  ```
473
- $ ticketing.search query="project:INFRA" -> R
474
- $ data_read.query query="..." -> M
477
+ $ <tool> kwarg=value, ... [-> R]
475
478
  ```
476
479
 
477
- Parser rule: the text before the dot is the connector name (must match an entry in `connectors.json`); the rest is the tool + args.
480
+ Bare form is reserved for ops with a typed runtime contract the substrate variance is invisible to the skill body because the runtime dispatches through the contract.
478
481
 
479
- ### Worked examples
482
+ ```
483
+ $ data_read mode=fts query="${TOPIC}" limit=5 -> RESULTS
484
+ $ data_write content="${SUMMARY}" tags=["nightly"] -> ACK
485
+ $ skill_read name="hello-world" -> S
486
+ $ skill_write name="child" source="..." overwrite=true -> W
487
+ $ json_parse ${RAW_JSON} -> PARSED
488
+ $ llm prompt="..." -> R
489
+ ```
490
+
491
+ Typed-contract ops (`data_*`, `skill_*`, `json_parse`, `llm`) auto-wire from the adopter's `substrate.local_model` / `substrate.data_store` / `substrate.skill_store` config — the skill body doesn't know whether it's running against SQLite or Postgres or Pinecone. That's the point of the typed contract.
492
+
493
+ **Lint rules:**
494
+ - `unknown-connector` (tier-1) — `$ x.y` where `x` isn't wired. Lists wired connector names.
495
+ - `bare-mcp-call` (tier-1) — `$ tool_x` where `tool_x` isn't a typed-contract op AND no `primary` connector is wired. Remediation: qualify as `$ <connector>.tool_x`.
496
+ - `unverified-qualified-tool` (advisory) — `$ x.y` where connector `x` doesn't declare its tool surface statically. Runtime will resolve at call time.
497
+
498
+ ### Universal op-level kwargs
499
+
500
+ Four surfaces apply to every `$` dispatch regardless of tool. The runtime intercepts these before forwarding the remaining kwargs to the connector.
501
+
502
+ **`timeout=N`** — Per-op timeout in **seconds**. Integer literal or `${VAR}` ref. Resolution chain (most-specific wins):
503
+
504
+ 1. Per-op `timeout=N` kwarg
505
+ 2. Skill-level `# Timeout: N` header
506
+ 3. Absolute context default (runtime config)
507
+ 4. Built-in default
508
+
509
+ ```
510
+ $ llm prompt="..." timeout=30 -> R
511
+ $ amp.amp_olsen_task task_type="scan" timeout=120 -> DISPATCH
512
+ ```
513
+
514
+ **`approved="<reason>"`** — Author-intent marker for the mutation-gate lint. Any non-empty string satisfies. Extracted at runtime; **not forwarded to the connector**. The value is required but not parsed semantically — presence is the signal.
515
+
516
+ ```
517
+ $ data_write content="${REPORT}" tags=["oncall"] approved="morning roundup" -> ACK
518
+ ```
519
+
520
+ **`(fallback: "value")` trailer** — Fires on dispatch throw OR empty bound value (empty string after trim, empty array, null/undefined). 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.
521
+
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
+
524
+ ```
525
+ $ llm prompt="Classify: ${INPUT}" -> VERDICT (fallback: "unknown")
526
+ $ amp.amp_query_memories query="${TOPIC}" -> RESULTS (fallback: [])
527
+ $ ticketing.search query="..." -> ISSUES (fallback: "search-unavailable")
528
+ ```
529
+
530
+ **`-> R` binding** — Optional output capture. Result string binds to `R` and to `${target}.output`. Omit when the dispatch is fire-and-forget.
531
+
532
+ **Kwarg validation for typed-contract ops.** Two sibling lints close the kwarg surface — provider-API kwargs (e.g., `temperature=0.7` on `$ llm`) silently dropped by the bridge get caught at parse time:
533
+
534
+ - `unknown-llm-arg` (tier-2) — `$ llm` carries a kwarg outside the canonical surface (`prompt`/`maxTokens`/`model`/`timeout`/`approved`/`fallback`). `LocalModel.run()` only consumes `{maxTokens, model}`; anything else is silently dropped at the bridge.
535
+ - `unknown-data-read-arg` (tier-2) — `$ data_read` carries a kwarg outside the canonical surface (`mode`/`query`/`limit`/`connector`/`fallback`/`domain_tags`/`filters`/`min_confidence`).
536
+
537
+ For named-form dispatch (`$ <connector>.<tool>`) the connector's `staticTools()` surface + per-tool kwarg validation is the discipline boundary — adopter MCP servers each define their own.
538
+
539
+ ### Typed-contract op surfaces
540
+
541
+ Canonical kwarg surfaces for each typed-contract bare-form op. These are the ops the runtime auto-wires from `substrate.local_model` / `substrate.data_store` / `substrate.skill_store` config.
542
+
543
+ #### `$ llm`
544
+
545
+ Routes through the wired LocalModel.
546
+
547
+ | Kwarg | Required | Notes |
548
+ |---|---|---|
549
+ | `prompt="..."` | yes | Non-empty string. Substitution-resolved at runtime. |
550
+ | `maxTokens=N` | no | Positive integer (number or numeric string). Forwarded as `runOpts.maxTokens`. |
551
+ | `model="X"` | no | Per-call model selection. Resolves against registered LocalModel aliases via `registry.getLocalModel(X)`; falls through to the default LocalModel with `model=X` passed as an upstream hint (e.g., Ollama tag) when X doesn't resolve. See `unknown-llm-model` lint below. |
552
+
553
+ Plus the universal op-level kwargs (`timeout`, `approved`, `(fallback:)`, `-> R`).
554
+
555
+ Canonical shape:
556
+
557
+ ```
558
+ $ llm prompt="..." [maxTokens=N] [model="<alias-or-tag>"] [timeout=N] [approved="..."] -> R [(fallback: "...")]
559
+ ```
560
+
561
+ The wired LocalModel determines which underlying model serves the request. Adopters with multiple registered LocalModels (e.g., `qwen` + `gemma`) can target a specific one via `model="qwen"` from the skill body. The `unknown-llm-model` lint (tier-2) validates the value against both registered alias names AND each LocalModel's `manifest().manifest.models_available` — substrate-aware typo-catch, since manifest data is exposed via `runtime_capabilities`.
562
+
563
+ #### `$ data_read` / `$ data_write`
564
+
565
+ Route through the wired DataStore. Surface depends on the connector contract — see the adopter playbook for the canonical DataStore contract reference. Common shape:
480
566
 
481
567
  ```
482
- $ ticketing_search query="project:INFRA state:Open" limit=20 -> ISSUES
483
- $ llm prompt="Classify: ${INPUT}" -> VERDICT
484
568
  $ data_read mode=fts query="${TOPIC}" limit=5 -> RESULTS
485
- $ data_write content="${SUMMARY}" recipients=["oncall"] approved="morning roundup, 2026-05-25" -> ACK
569
+ $ data_write content="${SUMMARY}" tags=["nightly"] approved="..." -> ACK
570
+ ```
571
+
572
+ #### `$ skill_read` / `$ skill_write`
573
+
574
+ Route through the wired SkillStore.
575
+
576
+ ```
577
+ $ skill_read name="hello-world" -> S
578
+ $ skill_write name="child" source="..." overwrite=true approved="..." -> W
579
+ ```
580
+
581
+ #### `$ json_parse`
582
+
583
+ Runtime-intrinsic parser. Single positional value (the raw JSON string).
584
+
585
+ ```
586
+ $ json_parse ${RAW_JSON} -> PARSED
587
+ ```
588
+
589
+ ### Worked examples
590
+
591
+ ```
592
+ $ amp.amp_olsen_task task_type="scan" -> DISPATCH
593
+ $ amp.amp_query_memories query="recent activity" limit=5 -> RECENT
594
+ $ llm prompt="Classify: ${INPUT}" timeout=30 -> VERDICT
595
+ $ llm prompt="Summarize: ${TEXT}" model="qwen" maxTokens=500 -> SUMMARY
596
+ $ data_read mode=fts query="${TOPIC}" limit=5 -> RESULTS (fallback: [])
597
+ $ data_write content="${SUMMARY}" tags=["oncall"] approved="morning roundup, 2026-05-25" -> ACK
598
+ $ ticketing.search query="project:INFRA state:Open" limit=20 -> ISSUES
486
599
  ```
487
600
 
488
601
  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[]`.
489
602
 
490
- **Substrate-neutrality.** Connector names like `$ llm`, `$ data_read`, `$ ticketing_search` are NOT reserved or built-in — they're whatever the adopter declares in `connectors.json` (substrate config). Bridges for `$ llm` and `$ data_read` / `$ data_write` auto-wire only when the adopter's substrate config sets `substrate.local_model` / `substrate.data_store` respectively. See the adopter playbook for the full substrate config reference.
603
+ **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:
604
+
605
+ - `HttpMcpConnector` — declarative wiring for any Streamable HTTP MCP server (JSON-RPC over HTTP + SSE). No subprocess. Adopters declare instances by class name in `connectors.json`.
606
+ - `RemoteMcpConnector` — stdio bridging for MCPs distributed as spawnable binaries (YouTrack, GitHub, Linear when run locally) or HTTP MCPs adapted via `npx mcp-remote ... --sse`.
607
+ - Custom fork (`examples/connectors/McpConnectorTemplate/`) — when the wire shape needs behavior the bundled connectors don't expose.
608
+
609
+ See the adopter playbook for the substrate config reference + the full Case 2 tradeoff.
491
610
 
492
- **Unknown connector** tier-1 `unknown-connector` lint with the list of wired connector names.
611
+ **Discovery surface.** `runtime_capabilities` (MCP tool) exposes the registered substrate state. Every entry across all four substrate slots (SkillStore / DataStore / LocalModel / McpConnector) carries its instance `manifest()` payload alongside the static features. Three observable states per entry: working `manifest:{...}`, runtime failure `manifest:null, manifest_error:"..."`, structural absence `manifest:null, manifest_unsupported:true` (AgentConnector only — the contract has no `manifest()` method). The bridge `wraps` convention re-exposes the underlying substrate's full manifest, so adopters reading the discovery surface see the full bound state without traversing multiple entries.
493
612
 
494
- **Unquoted-substitution lint** (`unquoted-substitution-in-kwarg-value`, tier-2): fires when `$ tool key=${VAR}` has unquoted `${VAR}` AND the var's binding origin is "suspect" (`# Vars:` default with whitespace, `$set` with whitespace, op output, foreach iterator). Closes the silent-arg-truncation footgun where the MCP arg parser whitespace-splits substituted values. Remediation: wrap as `key="${VAR}"`.
613
+ **Unquoted-substitution lint** (`unquoted-substitution-in-kwarg-value`, tier-2): fires when `$ x.y key=${VAR}` has unquoted `${VAR}` AND the var's binding origin is "suspect" (`# Vars:` default with whitespace, `$set` with whitespace, op output, foreach iterator). Closes the silent-arg-truncation footgun where the MCP arg parser whitespace-splits substituted values. Remediation: wrap as `key="${VAR}"`.
495
614
 
496
615
  ---
497
616
 
@@ -504,12 +623,12 @@ Mutation ops require an authorization signal. The signal is per-op, not a mode b
504
623
  - `$ data_write ...` and any MCP connector entry declared `"mutating": true` in `connectors.json`
505
624
  - `shell(command=..., unsafe=true)` (always mutation-classified)
506
625
  - `shell(command=...)` with destructive verb (rm, mv, dd, mkfs, etc. — heuristic list)
507
- - `$ <tool>` matching the mutating-verb regex
626
+ - `$ <connector>.<tool>` matching the mutating-verb regex
508
627
 
509
628
  **Read-only ops (always allowed, no authorization needed):**
510
- - `file_read`, `emit`, `notify`, `ask`, `inline`, `execute_skill`
629
+ - `file_read`, `emit`, `notify`, `inline`, `execute_skill`
511
630
  - `shell(command=...)` with read-only verb
512
- - `$ <connector> ...` against tools declared `mutating: false` (or unspecified, default false for query-shaped tools)
631
+ - `$ <connector>.<tool>` against tools declared `mutating: false` (or unspecified, default false for query-shaped tools)
513
632
  - `$set`, `$append`
514
633
 
515
634
  **Authorization signals (either suffices):**
@@ -524,7 +643,7 @@ Mutation ops require an authorization signal. The signal is per-op, not a mode b
524
643
 
525
644
  deliver:
526
645
  file_write(path="/tmp/sweep.md", content="${REPORT}") # no approved= needed
527
- $ data_write content="${REPORT}" recipients=["oncall"] # no approved= needed
646
+ $ data_write content="${REPORT}" tags=["oncall"] # no approved= needed
528
647
  ```
529
648
 
530
649
  ```
@@ -551,7 +670,8 @@ deliver:
551
670
  | Runtime-intrinsic | `shell` | `shell(command="...", [unsafe=true], [approved="..."]) -> R` | optional |
552
671
  | Runtime-intrinsic | `file_read` | `file_read(path="...") -> R` | required |
553
672
  | Runtime-intrinsic | `file_write` | `file_write(path="...", content="...", [approved="..."])` | none |
554
- | External MCP | `$ <connector>` | `$ <name>[.<tool>] kwarg=value, ... [-> R]` | optional |
673
+ | External MCP — substrate-specific | `$ <connector>.<tool>` | `$ <connector>.<tool> kwarg=value, ... [timeout=N] [approved="..."] [-> R] [(fallback: "...")]` | optional |
674
+ | External MCP — typed-contract | `$ <tool>` | `$ <tool> kwarg=value, ... [timeout=N] [approved="..."] [-> R] [(fallback: "...")]` (typed-contract ops only: `data_*`, `skill_*`, `json_parse`, `llm`) | optional |
555
675
 
556
676
  ## Variable resolution — ${VAR} canonical, substitution + ambient refs + # Requires: cascade
557
677
 
@@ -1576,7 +1696,7 @@ The Template-kind skill is the canonical static shape — its `# Output: templat
1576
1696
 
1577
1697
  ## Dynamic skill
1578
1698
 
1579
- A dynamic skill requires the Skillscript runtime to execute. The runtime walks the dispatch DAG, fires `$` ops against wired connectors, runs runtime-intrinsic ops (`emit`, `notify`, `ask`, `shell`, `file_read`, `file_write`, `execute_skill`), and threads outputs through variable bindings.
1699
+ A dynamic skill requires the Skillscript runtime to execute. The runtime walks the dispatch DAG, fires `$` ops against wired connectors, runs runtime-intrinsic ops (`emit`, `notify`, `shell`, `file_read`, `file_write`, `execute_skill`), and threads outputs through variable bindings.
1580
1700
 
1581
1701
  Dynamic skills are the default for:
1582
1702
  - **Autonomous workflows** — cron-fired Headless skills that fetch, reason, and emit
@@ -1598,7 +1718,7 @@ The axes are independent. A skill author can produce any combination.
1598
1718
 
1599
1719
  A `# Portability: static | dynamic` frontmatter header would declare the skill's intended execution model. The compiler would lint-check that the skill's op set is consistent with the declaration:
1600
1720
 
1601
- - `# Portability: static` → no `$` dispatch ops permitted; no side-effect runtime-intrinsics (`shell`, `file_write`, `ask`, `notify`, `execute_skill`); only the static-safe set (`emit`, `$set`, `$append`, `inline()`, conditionals, iteration)
1721
+ - `# Portability: static` → no `$` dispatch ops permitted; no side-effect runtime-intrinsics (`shell`, `file_write`, `notify`, `execute_skill`); only the static-safe set (`emit`, `$set`, `$append`, `inline()`, conditionals, iteration)
1602
1722
  - `# Portability: dynamic` (or unset, the default) → any op permitted
1603
1723
 
1604
1724
  A new compile mode `compile_skill({source, mode: "static"})` would render only the portable artifact, refusing skills that depend on runtime dispatch.
@@ -1922,5 +2042,5 @@ Hung dispatches hang the skill without explicit timeout configuration. Lean: ski
1922
2042
 
1923
2043
  ---
1924
2044
 
1925
- *Rendered from `skillscript/skillscript-language-reference` — 2026-05-30 09:52 EDT*
2045
+ *Rendered from `skillscript/skillscript-language-reference` — 2026-06-02 16:30 EDT*
1926
2046
  *Source of truth: AMP (`amp_render_document("skillscript/skillscript-language-reference")`)*
@@ -2,11 +2,12 @@
2
2
 
3
3
  A skeleton `McpConnector` implementation for adopters writing their own. Not runnable; every method throws a `TODO` error. Copy this directory, rename, fill in the substrate-specific work.
4
4
 
5
- **Most adopters don't need this template.** Four bundled impls already cover the common cases:
5
+ **Most adopters don't need this template.** Bundled impls already cover the common cases:
6
6
 
7
7
  | Bundled impl | What it covers |
8
8
  |---|---|
9
- | `RemoteMcpConnector` | Stdio bridging to remote MCP servers (`npx mcp-remote ...`). YouTrack, GitHub, Linear, most adopter MCP wiring goes through this. JSON-configurable via `connectors.json`. |
9
+ | `HttpMcpConnector` | Speaks Streamable HTTP MCP (JSON-RPC over HTTP + SSE) directly — no subprocess. Works against any compliant HTTP MCP server (Anthropic's hosted MCP, GitHub MCP, Linear MCP, etc.). JSON-configurable via `connectors.json`. |
10
+ | `RemoteMcpConnector` | Stdio bridging to remote MCP servers (`npx mcp-remote ...`). For MCPs distributed as binaries you spawn (YouTrack, GitHub, Linear when run locally) or HTTP MCPs that need an HTTPS-to-stdio adapter. JSON-configurable. |
10
11
  | `CallbackMcpConnector` | Wraps a JS function. Test rigs + embedder-wired transports where the dispatch is local code. |
11
12
  | `LocalModelMcpConnector` | Bridges a registered `LocalModel` as `$ llm prompt=...`. Auto-wired when `substrate.local_model` is set. |
12
13
  | `DataStoreMcpConnector` | Bridges a registered `DataStore` as `$ data_read mode=...` + `$ data_write content=...`. Auto-wired when `substrate.data_store` is set. |
@@ -14,15 +15,15 @@ A skeleton `McpConnector` implementation for adopters writing their own. Not run
14
15
 
15
16
  **Fork this template only when none of those fit** — e.g.:
16
17
 
17
- - Direct HTTP MCP (JSON-RPC over HTTP, no child process) — for now; check the bundled `McpConnector` classes before forking, as a bundled `HttpMcpConnector` is on the near-term roadmap
18
18
  - WebSocket MCP
19
19
  - In-process MCP (call methods directly without IPC)
20
- - Custom transport that doesn't match stdio framing
20
+ - Custom transport that doesn't match stdio framing or Streamable HTTP
21
21
  - Cross-thread / worker-pool dispatch
22
+ - An HTTP MCP server that diverges from the spec (custom auth handshake, non-SSE response framing, tool-name normalization, etc.) in ways `HttpMcpConnector` doesn't expose
22
23
 
23
- If you're trying to wire a remote stdio MCP server like YouTrack or GitHub, **you want `RemoteMcpConnector` in `connectors.json`**, not this template. See `connectors.json.example` for the wiring pattern.
24
+ If you're trying to wire a remote stdio MCP server like YouTrack or GitHub run locally, **you want `RemoteMcpConnector` in `connectors.json`**, not this template. See `connectors.json.example` for the wiring pattern.
24
25
 
25
- If you're trying to wire a Streamable HTTP MCP server (AMP, Anthropic's hosted MCP, etc.), the simplest path today is `RemoteMcpConnector` + `npx mcp-remote https://... --sse` (bridges HTTPS-SSE into stdio via a node subprocess). For lower overhead (no subprocess layer) check whether a bundled `HttpMcpConnector` is available in your installed runtime version if so, declare an instance in `connectors.json` rather than forking this template.
26
+ If you're trying to wire a Streamable HTTP MCP server (Anthropic's hosted MCP, GitHub MCP via HTTPS, etc.), **declare an `HttpMcpConnector` instance in `connectors.json`**no subprocess, no implementation. See `docs/adopter-playbook.md` §Case 2 path (b) for the full shape.
26
27
 
27
28
  ## Forking workflow
28
29
 
@@ -1,7 +1,19 @@
1
1
  {
2
2
  "_comment": "Onboarding scaffold connectors.json — wires example MCP servers an adopter might add alongside the bundled bridges. Most onboarding deployments don't need anything here; the file-backed data store + OpenAI LLM + tmux-shell wiring lives in bootstrap.ts. This file is the place for adopter-specific MCP servers (your in-house tools, a YouTrack instance, a custom agent API, etc.).",
3
3
 
4
- "_comment_transport": "RemoteMcpConnector bridges stdio MCP servers. The `mcp-remote` shape below also bridges Streamable HTTP MCP servers (the wire transport increasingly common for hosted MCPsAnthropic's hosted MCP, AMP, etc.): `mcp-remote https://endpoint/sse` runs a node subprocess that adapts HTTPS-SSE to stdio for the runtime. For lower overhead (no subprocess layer), wire a direct HTTP MCP connector class see docs/adopter-playbook.md §Case 2 and examples/connectors/McpConnectorTemplate/.",
4
+ "_comment_transport": "Two bundled paths for MCP transport: (1) `HttpMcpConnector` speaks Streamable HTTP MCP directly — no subprocess, lowest overhead, works against any compliant Streamable HTTP MCP server. (2) `RemoteMcpConnector` spawns a child process for stdio MCP servers also bridges Streamable HTTP via `mcp-remote https://endpoint/sse` when you need an HTTPS-to-stdio adapter. Pick (1) by default for HTTP MCP; (2) for native-stdio servers (community MCPs commonly distributed as binaries you spawn). See docs/adopter-playbook.md §Case 2 for the full tradeoff.",
5
+
6
+ "_example_http_mcp": {
7
+ "class": "HttpMcpConnector",
8
+ "config": {
9
+ "endpoint": "https://your-mcp-server.example.com/",
10
+ "headers": {
11
+ "Authorization": "Bearer ${MY_API_TOKEN}",
12
+ "X-Agent-ID": "${AGENT_ID}"
13
+ }
14
+ },
15
+ "allowed_tools": ["search", "fetch"]
16
+ },
5
17
 
6
18
  "_example_remote_mcp": {
7
19
  "class": "RemoteMcpConnector",
@@ -2,7 +2,7 @@
2
2
  "provenance_version": "1.0",
3
3
  "language_version": "1.0",
4
4
  "compiler_version": "0.1.0-dev",
5
- "compiled_at": "2026-06-02T19:36:50.914Z",
5
+ "compiled_at": "2026-06-02T20:33:13.321Z",
6
6
  "source_skill": {
7
7
  "name": "hello-world"
8
8
  },
@@ -1,6 +1,6 @@
1
1
  # Skill: morning-brief
2
- # Status: Approved v1:091ae2ae
3
- # Description: Compose a daily morning brief from calendar, mailbox, and overnight data writes when the cron trigger fires at 7am. Delivers via the agent: lifecycle hook to the receiving agent, who decides whether to surface to Slack / Discord / etc.
2
+ # Status: Approved v1:a08d0f21
3
+ # Description: Compose a daily morning brief from calendar, mailbox, and overnight data writes when the cron trigger fires at 7am. Delivers via the agent: lifecycle hook to the receiving agent, who decides whether to surface to Slack / Discord / etc. The `model=qwen` value below is a representative alias — adopters register a LocalModel under whatever name fits their setup (the bundled bootstrap registers one as `default`).
4
4
  # Vars: AGENT, BRIEF_HORIZON_HOURS=24
5
5
  # Triggers: cron: 0 7 * * *
6
6
  # OnError: morning-brief-degraded
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillscript-runtime",
3
- "version": "0.16.4",
3
+ "version": "0.16.5",
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>",