skillscript-runtime 0.5.0 → 0.7.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.
Files changed (43) hide show
  1. package/README.md +105 -35
  2. package/dist/bootstrap.d.ts.map +1 -1
  3. package/dist/bootstrap.js +15 -0
  4. package/dist/bootstrap.js.map +1 -1
  5. package/dist/compile.d.ts.map +1 -1
  6. package/dist/compile.js +42 -2
  7. package/dist/compile.js.map +1 -1
  8. package/dist/connectors/config.d.ts.map +1 -1
  9. package/dist/connectors/config.js +10 -0
  10. package/dist/connectors/config.js.map +1 -1
  11. package/dist/connectors/local-model-mcp.d.ts +9 -0
  12. package/dist/connectors/local-model-mcp.d.ts.map +1 -0
  13. package/dist/connectors/local-model-mcp.js +73 -0
  14. package/dist/connectors/local-model-mcp.js.map +1 -0
  15. package/dist/connectors/memory-store-mcp.d.ts +9 -0
  16. package/dist/connectors/memory-store-mcp.d.ts.map +1 -0
  17. package/dist/connectors/memory-store-mcp.js +94 -0
  18. package/dist/connectors/memory-store-mcp.js.map +1 -0
  19. package/dist/help-content.d.ts.map +1 -1
  20. package/dist/help-content.js +316 -238
  21. package/dist/help-content.js.map +1 -1
  22. package/dist/lint.d.ts.map +1 -1
  23. package/dist/lint.js +296 -53
  24. package/dist/lint.js.map +1 -1
  25. package/dist/parser.d.ts +48 -3
  26. package/dist/parser.d.ts.map +1 -1
  27. package/dist/parser.js +377 -31
  28. package/dist/parser.js.map +1 -1
  29. package/dist/runtime.d.ts.map +1 -1
  30. package/dist/runtime.js +93 -16
  31. package/dist/runtime.js.map +1 -1
  32. package/examples/classify-support-ticket.skill.md +13 -13
  33. package/examples/cut-release-tag.skill.md +14 -14
  34. package/examples/doc-qa-with-citations.skill.md +3 -3
  35. package/examples/feedback-sentiment-scan.skill.md +13 -13
  36. package/examples/hello.skill.md +2 -2
  37. package/examples/hello.skill.provenance.json +1 -1
  38. package/examples/morning-brief.skill.md +6 -6
  39. package/examples/queue-length-monitor.skill.md +4 -4
  40. package/examples/service-health-watch.skill.md +7 -7
  41. package/examples/youtrack-morning-sweep.skill.md +4 -4
  42. package/package.json +1 -1
  43. package/scaffold/connectors.json +15 -13
@@ -13,14 +13,40 @@
13
13
  //
14
14
  // Token estimates per topic are approximate; help() output is intended
15
15
  // for an agent's working context, not for human reading.
16
- const QUICKSTART = `# Skillscript — quickstart
16
+ const QUICKSTART = `# Skillscript — quickstart (v0.7.0+ canonical surface)
17
17
 
18
18
  Skillscript is a declarative language for authoring agent workflows. A
19
19
  skill is a small program with named targets composed of typed ops. The
20
20
  runtime walks dependencies backward from the goal target (declared via
21
21
  \`default:\`) and dispatches each op in topological order.
22
22
 
23
- ## 1. Shape of a skill file
23
+ ## 1. The skill model trigger → process → deliver
24
+
25
+ Every skill follows the same shape:
26
+
27
+ 1. **Trigger fires** — cron, command, event, session-start, or programmatic invocation
28
+ 2. **Process** — pull data (MCP / memory / file), classify / compose via sub-LLM + iteration, build the deliverable
29
+ 3. **Deliver** — via one or more of three channels
30
+
31
+ The three delivery channels are all first-class:
32
+
33
+ | Channel | Op | When you'd use it |
34
+ |---|---|---|
35
+ | **Embedded prompt** | \`emit(text="...")\` | Skill output is injected into the agent's turn as prompt-context |
36
+ | **Memory handoff** | \`$ memory_write content="..." addressed_to="<agent>" -> R\` | Skill writes a memory the target agent picks up via mailbox |
37
+ | **File handoff** | \`file_write(path="...", content="...")\` | Skill writes a file at a known location for the agent to read |
38
+
39
+ ## 2. The three op classes
40
+
41
+ | Class | Shape | Examples |
42
+ |---|---|---|
43
+ | **Mutation statements** | \`$verb VAR = value\` / \`$verb VAR <value>\` | \`$set NAME = "Scott"\`, \`$append LIST <item>\` |
44
+ | **Runtime-intrinsic function-calls** | \`verb(kwarg=value, ...) [-> BINDING]\` | \`emit(text="...")\`, \`ask(prompt="...") -> R\`, \`inline(skill="...")\`, \`execute_skill(skill_name="...") -> R\`, \`shell(command="...") -> R\`, \`file_read(path="...") -> R\`, \`file_write(path="...", content="...")\` |
45
+ | **External MCP dispatch** | \`$ <connector> kwarg=value, ... [-> BINDING]\` | \`$ youtrack_search query="..." -> R\`, \`$ llm prompt="..." -> R\`, \`$ memory mode=fts query="..." -> R\` |
46
+
47
+ The \`$\` prefix marks **state-affecting ops** (mutation OR external dispatch). Function-call shape marks **language-intrinsic ops the runtime knows directly**.
48
+
49
+ ## 3. Shape of a skill file
24
50
 
25
51
  \`\`\`
26
52
  # Skill: my-skill ← required: skill name
@@ -30,57 +56,46 @@ runtime walks dependencies backward from the goal target (declared via
30
56
  # Triggers: cron: 0 9 * * * ← optional: autonomous-dispatch sources
31
57
 
32
58
  target_a: ← a named block of ops
33
- @ curl -s https://example.com -> RAW
34
- ~ prompt="Summarize: $(RAW)" -> SUMMARY
59
+ $ ticketing_search query="state:open" -> ISSUES
60
+ $ llm prompt="Summarize: \${ISSUES}" -> SUMMARY
35
61
 
36
62
  target_b: target_a ← Make-style: target_b depends on target_a
37
- ! $(SUMMARY)
63
+ emit(text="\${SUMMARY}")
38
64
 
39
65
  default: target_b ← goal target the runtime walks toward
40
66
  \`\`\`
41
67
 
42
- ## 2. Op symbol legend
68
+ ## 4. Variable substitution
43
69
 
44
- | Op | Meaning |
45
- |---|---|
46
- | \`$ tool args -> VAR\` | MCP tool invocation; binds result to VAR |
47
- | \`~ prompt="..." -> VAR\` | LocalModel call; binds output |
48
- | \`> mode=... query=... limit=N -> VAR\` | Memory retrieval |
49
- | \`@ command args -> VAR\` | Shell exec (structural sandbox; \`@ unsafe\` for full bash) |
50
- | \`! text\` | Emit text to the user / output channel |
51
- | \`?? prompt -> VAR\` | Ask user (interactive mode only) |
52
- | \`$set NAME=value\` | Explicit variable binding |
53
- | \`$append VAR <value>\` | Accumulate a value into a list-typed VAR (v0.3.0) |
54
- | \`& skill-name args -> VAR\` | Inline a data-skill |
70
+ Use \`\${VAR}\` (canonical) inside any kwarg value or emit body. Field access works: \`\${ISSUE.title}\`. Filter chains: \`\${VAR|trim|length}\`. Missing-value fallback: \`\${VAR|fallback:"-"}\`.
71
+
72
+ The legacy \`$(VAR)\` form still compiles during the v0.7.x grace period (tier-2 \`deprecated-substitution-shape\` warning); tier-1 promotion in v0.8/v0.9.
55
73
 
56
- ## 3. Result binding
74
+ ## 5. Result binding + fallback
57
75
 
58
- Most ops accept \`-> VAR\` to bind their output. Reference later via \`$(VAR)\`.
59
- Optional \`(fallback: "default")\` after \`-> VAR\` binds the fallback on dispatch
60
- error instead of propagating.
76
+ Most dispatch ops accept \`-> VAR\` to bind their output. Reference later via \`\${VAR}\`. Optional \`(fallback: "default")\` after \`-> VAR\` binds the fallback on dispatch error instead of propagating.
61
77
 
62
- ## 4. Branching
78
+ ## 6. Branching
63
79
 
64
80
  \`\`\`
65
- if $(VERDICT) == "urgent":
66
- ! sound the alarm
67
- elif $(COUNT) > "10":
68
- ! threshold breached: $(COUNT) items
81
+ if \${VERDICT} == "urgent":
82
+ emit(text="sound the alarm")
83
+ elif \${COUNT} > "10":
84
+ emit(text="threshold breached: \${COUNT} items")
69
85
  else:
70
- ! all clear
86
+ emit(text="all clear")
71
87
  \`\`\`
72
88
 
73
- Numeric comparison (\`<\` / \`>\` / \`<=\` / \`>=\`) coerces both sides via Number();
74
- non-numeric operands raise TypeMismatchError.
89
+ Numeric comparison (\`<\` / \`>\` / \`<=\` / \`>=\`) coerces both sides via Number(); non-numeric operands raise TypeMismatchError.
75
90
 
76
- ## 5. Iteration
91
+ ## 7. Iteration
77
92
 
78
93
  \`\`\`
79
- foreach M in $(MEMORIES):
80
- ! Processing $(M.id): $(M.summary)
94
+ foreach M in \${MEMORIES}:
95
+ emit(text="Processing \${M.id}: \${M.summary}")
81
96
  \`\`\`
82
97
 
83
- ## 6. How to see what's broken
98
+ ## 8. How to see what's broken
84
99
 
85
100
  - \`lint_skill({source})\` — diagnostics across tier-1 (errors), tier-2 (warnings), tier-3 (advisories)
86
101
  - \`compile_skill({source, inputs?})\` — render the compiled artifact + surface compile errors
@@ -89,173 +104,201 @@ foreach M in $(MEMORIES):
89
104
  ## Worked end-to-end example
90
105
 
91
106
  \`\`\`
92
- # Skill: morning-temperature-alert
93
- # Description: Cron-fired check; alerts when overnight temp drops below threshold
107
+ # Skill: morning-showstopper-sweep
108
+ # Description: Cron-fired pre-triage; delivers triaged showstoppers to oncall agent via prompt-context channel
94
109
  # Status: Approved
95
- # Vars: LOCATION=Asheville,NC, THRESHOLD=40
96
- # Triggers: cron: 0 7 * * *
97
-
98
- fetch:
99
- @ curl -s "wttr.in/$(LOCATION|url)?format=j1" -> RAW (fallback: "")
110
+ # Autonomous: true
111
+ # Vars: PROJECT=INFRA
112
+ # Triggers: cron: 0 8 * * MON-FRI
113
+ # Output: prompt-context: oncall
100
114
 
101
- evaluate: fetch
102
- ~ prompt="Extract overnight low temperature in F from this JSON. Reply with only the number: $(RAW)" model=qwen -> TEMP
115
+ run:
116
+ $ ticketing_search query="project:\${PROJECT} severity:showstopper state:Open" limit=20 -> ISSUES
103
117
 
104
- alert: evaluate
105
- if $(TEMP|trim) < $(THRESHOLD):
106
- ! Cold morning: overnight low $(TEMP|trim)°F (threshold $(THRESHOLD)°F)
107
- else:
108
- ! Temp ok: $(TEMP|trim)°F
118
+ emit(text="Morning showstoppers for \${PROJECT} — \${ISSUES.totalCount} open:")
119
+ emit(text="")
120
+ foreach ISSUE in \${ISSUES.items}:
121
+ $ llm prompt="Two-line triage hypothesis for: \${ISSUE.summary}" -> ANALYSIS
122
+ emit(text="## \${ISSUE.id}: \${ISSUE.summary}")
123
+ emit(text="\${ANALYSIS}")
124
+ emit(text="")
109
125
 
110
- default: alert
126
+ default: run
111
127
  \`\`\`
112
128
 
129
+ What this example demonstrates:
130
+ - **Trigger** — cron at 8am weekdays
131
+ - **Process** — \`$ ticketing_search\` MCP dispatch (substrate-portable: adopters wire whatever ticketing connector they have), \`foreach\` iteration with per-item \`$ llm\` sub-classification
132
+ - **Deliver** — \`emit(text=...)\` per line accumulates as prompt-context, routed to the on-call agent via the \`# Output: prompt-context: oncall\` channel declaration
133
+ - **Authorization** — \`# Autonomous: true\` declares this skill cron-fired and unattended; mutation ops within are silenced from the user-confirmation lint
134
+
135
+ **Pattern note:** prefer \`emit(text="...")\` per line over building a multi-line accumulator string with \`$append\`. The runtime threads emissions into prompt-context naturally, and the per-line shape is what cold authors reach for. Multi-line string accumulators are a real pattern for file-writing scenarios; emit is the natural choice for prompt-context delivery.
136
+
113
137
  Use \`help({topic: "ops"})\`, \`help({topic: "frontmatter"})\`, \`help({topic: "examples"})\`,
114
138
  \`help({topic: "connectors"})\`, or \`help({topic: "lint-codes"})\` for deeper sections.
139
+
140
+ **Note on legacy syntax.** Legacy symbol-form ops (\`~\`, \`>\`, \`@\`, \`!\`, \`??\`, \`&\`) and \`$(VAR)\` substitution continue to compile during the v0.7.x grace period with tier-2 deprecation warnings. CHANGELOG.md \`## 0.7.0 — Migration\` documents the rewrite rules.
115
141
  `;
116
- const OPS = `# Op symbolsfull reference
142
+ const OPS = `# Ops referencev0.7.0 canonical surface
143
+
144
+ Three op classes, two grammars:
145
+
146
+ | Class | Shape | When you reach for it |
147
+ |---|---|---|
148
+ | **Mutation statements** | \`$verb VAR = value\` / \`$verb VAR <value>\` | Bind / mutate a named variable in scope |
149
+ | **Runtime-intrinsic function-calls** | \`verb(kwarg=value, ...) [-> BINDING]\` | Language-intrinsic side-effects: emit, ask, file I/O, shell, composition |
150
+ | **External MCP dispatch** | \`$ <connector> kwarg=value, ... [-> BINDING]\` | Any tool resolved through \`connectors.json\` (LLM calls, memory queries, business tools) |
151
+
152
+ The \`$\` prefix marks **state-affecting** ops (mutation OR external dispatch). Function-call shape marks **language-intrinsic** ops the runtime knows directly. Legacy symbol forms (\`~\` / \`>\` / \`@\` / \`!\` / \`??\` / \`&\`) compile during the v0.7.x grace period with tier-2 \`deprecated-symbol-op\` warnings.
117
153
 
118
- ## Dispatch ops (bind a result)
154
+ ## Class 1: Mutation statements
119
155
 
120
- ### \`$\` MCP tool invocation
156
+ ### \`$set VAR = value\`
157
+
158
+ Bind a variable; runtime resolves \`\${REF}\` substitutions in the RHS at bind time (v0.5.0). Value can be a literal, a \`\${REF}\` interpolation, or a JSON literal (object / array / bool / null).
121
159
 
122
160
  \`\`\`
123
- $ tool_name arg1=value1 arg2=value2 -> VAR [(fallback: "default")]
124
- $ connector.tool_name args -> VAR
161
+ $set GREETING = "Hello, \${USER}!"
162
+ $set ITEMS = []
163
+ $set CONFIG = {"timeout": 30, "retries": 3}
125
164
  \`\`\`
126
165
 
127
- Calls an MCP tool. Without an explicit connector prefix, routes to the
128
- \`primary\` MCP connector. Fallback binds when dispatch errors.
166
+ ### \`$append VAR <value>\`
129
167
 
130
- **Kwarg value grammar.** Each \`key=value\` token follows a small literal
131
- grammar:
168
+ Append to a binding. v0.5.0 type-dispatches on the existing target:
169
+ - **List-typed target** → push (\`$set FOUND = []\` then \`$append FOUND \${ID}\`)
170
+ - **String-typed target** → concatenate (\`$set REPORT = ""\` then \`$append REPORT "more text"\`)
132
171
 
133
- | Form | Example | Type |
134
- |------|---------|------|
135
- | Bare string | \`status=open\` | string \`"open"\` |
136
- | Quoted string | \`query="hello world"\` | string \`"hello world"\` (use when value contains whitespace) |
137
- | Integer | \`limit=10\` | number \`10\` |
138
- | Boolean | \`urgent=true\` | boolean \`true\` |
139
- | Null | \`assignee=null\` | null |
140
- | JSON array | \`tags=["a","b"]\` | array \`["a","b"]\` |
141
- | JSON object | \`payload={"k":"v"}\` | object \`{"k":"v"}\` |
142
- | Substitution | \`id=$(BUG_ID)\` | resolved at dispatch time |
143
- | Quoted substitution | \`query="$(QUERY)"\` | quoted resolution (recommended when value may contain whitespace) |
172
+ Lint guards: \`uninitialized-append\` (no \`$set\` / \`# Vars:\` init); \`foreach-local-accumulator-target\` (init inside the same foreach as the append — silently loses data each iteration); \`append-to-non-list\` (numeric/boolean/null init).
144
173
 
145
- **v0.5.0 lint warning** \`unquoted-substitution-in-kwarg-value\` fires when
146
- an unquoted \`$(VAR)\` substitution sits in kwarg-value position and VAR's
147
- binding origin suggests whitespace (\`# Vars:\` default with whitespace,
148
- \`$set\` literal with whitespace, \`~\`/\`$\`/\`>\` op output, or foreach
149
- iterator). Wrap as \`key="$(VAR)"\` to prevent silent arg truncation if
150
- the resolved value contains spaces — the MCP arg tokenizer respects
151
- quoted regions.
174
+ \`\`\`
175
+ walk:
176
+ $set SEEN = []
177
+ $set REPORT = ""
178
+ foreach M in \${MESSAGES}:
179
+ if \${M.id} not in \${SEEN}:
180
+ $append SEEN \${M.id}
181
+ $append REPORT "\\n - \${M.id}: \${M.summary}"
182
+ \`\`\`
152
183
 
153
- **Built-in (v0.2.8):** \`$ execute_skill skill_name=child -> RESULT\` invokes
154
- another stored skill end-to-end without requiring an MCP connector — pass
155
- input vars as additional kwargs.
184
+ The append mutates the outer-scope binding (unlike \`$set\`, which is loop-local inside \`foreach\`).
156
185
 
157
- **Built-in (v0.3.3):** \`$ json_parse $(VAR) -> P\` parses the input as JSON
158
- and binds the structured value to \`P\`. Dotted descent via \`$(P.field)\`
159
- works in conditions and emit — no filter+field grammar gymnastics.
186
+ ## Class 2: Runtime-intrinsic function-calls
160
187
 
161
- \`\`\`
162
- # Vars: PAYLOAD={"status":"ok","count":3}
188
+ Closed set: \`emit\`, \`ask\`, \`inline\`, \`execute_skill\`, \`shell\`, \`file_read\`, \`file_write\`. Unknown function-call names fire \`unknown-runtime-op\` tier-1 with remediation "if this is an MCP tool, use \`$ tool args -> R\` shape instead."
163
189
 
164
- read:
165
- $ json_parse $(PAYLOAD) -> P
166
- if $(P.status) == "ok" and $(P.count) > "0":
167
- ! processing $(P.count) items
190
+ ### \`emit(text="...")\` — output to skill consumer
191
+
192
+ One-line emission. \`\${VAR}\` substitutes. No result binding by default.
193
+
194
+ \`\`\`
195
+ emit(text="Hello, \${NAME}!")
196
+ emit(text="\${ISSUES.totalCount} open showstoppers in \${PROJECT}")
168
197
  \`\`\`
169
198
 
170
- Throws on malformed JSON (caught by \`else:\` / \`# OnError:\`). Replaces
171
- the v0.3.2 \`|json_parse\` filter, which couldn't propagate parsed
172
- structure through \`.field\` access.
199
+ ### \`ask(prompt="...") -> R\` prompt the user
173
200
 
174
- ### \`~\` LocalModel call
201
+ Interactive only. Autonomous-mode dispatch fails with a clean error (use \`# Autonomous: true\` skill flag to disable the gate).
175
202
 
176
203
  \`\`\`
177
- ~ prompt="..." model=qwen maxTokens=400 -> VAR [(fallback: "...")]
204
+ ask(prompt="Proceed with auto-assignment for P0/P1?") -> APPROVAL
178
205
  \`\`\`
179
206
 
180
- LLM call against a configured LocalModel. \`model\` selects the named model;
181
- default is \`default\`. Multi-line prompts via \`"..."\` are supported (parser
182
- folds quoted-string continuations).
207
+ ### \`shell(command="...", unsafe=true) [-> R]\` local subprocess
183
208
 
184
- ### \`>\`Memory retrieval
209
+ Default mode: structural spawn one binary, no shell metacharacters, no pipes/redirects. \`unsafe=true\` opts into full bash; tier-2 lint warns.
185
210
 
186
211
  \`\`\`
187
- > mode=fts query="..." limit=20 -> VAR [(fallback: "...")]
212
+ shell(command="git status --porcelain") -> STATUS
213
+ shell(command="echo hi && date +%Y", unsafe=true) -> OUT
188
214
  \`\`\`
189
215
 
190
- Queries the configured MemoryStore. \`mode\` is substrate-specific
191
- (\`fts\` / \`semantic\` / \`rerank\` are common). \`limit\` is required.
216
+ ### \`file_read(path="...") -> R\` read file contents
192
217
 
193
- ### \`@\`Shell exec
218
+ Reads via Node \`fs.readFile\`. Substitutes \`\${VAR}\` in the path. Optional \`(fallback: "...")\` trailer binds when read fails. **Container note:** when the runtime is sandboxed (Docker, container deployment), the runtime's filesystem is namespace-isolated from the author's host \`/tmp/x\` in the skill maps to the runtime's \`/tmp/x\`, not the host's. Use absolute paths under a known shared volume for cross-namespace work.
194
219
 
195
220
  \`\`\`
196
- @ command arg1 arg2 -> VAR [(fallback: "...")]
197
- @ unsafe full-shell-command -> VAR
221
+ file_read(path="/var/reports/today.md") -> REPORT (fallback: "no report")
198
222
  \`\`\`
199
223
 
200
- Default mode: structural spawnone binary, no shell metacharacters,
201
- no pipes/redirects. \`@ unsafe\` opts into full bash; lint-flags tier-2.
224
+ ### \`file_write(path="...", content="...", approved="...")\`write file contents
202
225
 
203
- ## Emission + control ops
204
-
205
- ### \`!\` — Emit text
226
+ Writes via Node \`fs.writeFile\`. Auto-creates parent directories. Substitutes \`\${VAR}\` in path + content. The \`approved="reason"\` kwarg authorizes the mutation per-op (any non-empty string; presence is what matters); skip when \`# Autonomous: true\` skill flag is declared. Same container FS-isolation caveat as \`file_read\` — the runtime's filesystem ≠ the author's.
206
227
 
207
228
  \`\`\`
208
- ! Hello, $(NAME)!
229
+ file_write(path="/var/reports/sweep-\${DATE}.md", content="\${REPORT}", approved="nightly cron deliverable")
209
230
  \`\`\`
210
231
 
211
- One-line literal emission. Variables substitute. No result binding.
232
+ ### \`inline(skill="...")\` compile-time skill composition
212
233
 
213
- ### \`??\` Ask user
234
+ References a \`# Type: data\` skill; the compiler inlines its emitted text at compile time so the compiled artifact is a single resolved document.
214
235
 
215
236
  \`\`\`
216
- ?? Are you sure? -> CONFIRM
237
+ inline(skill="common-prelude")
217
238
  \`\`\`
218
239
 
219
- Interactive only. Autonomous-mode dispatch fails with a clean error.
240
+ ### \`execute_skill(skill_name="...", ...kwargs) -> R\` runtime skill composition
220
241
 
221
- ### \`$set\` Explicit binding
242
+ Invokes another stored skill end-to-end against the runtime's connectors. Returns the full execution record (final vars, transcript, outputs). Access via \`\${R.final_vars.FIELD}\`, \`\${R.transcript}\`, etc.
222
243
 
223
244
  \`\`\`
224
- $set NAME=value
225
- $set NAME=$(OTHER_VAR|trim)
245
+ execute_skill(skill_name="extract-json-number", JSON_BLOB="\${RAW}", FIELD_PATH="total_count") -> RESULT
246
+ emit(text="Extracted: \${RESULT.final_vars.VALUE|trim}")
226
247
  \`\`\`
227
248
 
228
- ### \`$append\` Accumulator (v0.3.0)
249
+ ## Class 3: External MCP dispatch
229
250
 
230
251
  \`\`\`
231
- $append VAR <value>
252
+ $ tool_name arg1=value1 arg2=value2 -> VAR [(fallback: "default")]
253
+ $ connector.tool_name args -> VAR
232
254
  \`\`\`
233
255
 
234
- Single-value append to a list-typed VAR. The target must be initialized in an enclosing scope before the append fires:
256
+ Resolves the tool name against the adopter's \`connectors.json\`. Flat form (\`$ youtrack_search ...\`) uses the connector that owns the tool; dotted form (\`$ youtrack.search ...\`) routes explicitly. Fallback binds when dispatch errors. The substrate-specific shapes — LLM calls (\`$ llm\`), memory queries (\`$ memory\`), memory writes (\`$ memory_write\`), business tools — all use this dispatch.
257
+
258
+ **Kwarg value grammar.** Each \`key=value\` token follows a small literal grammar:
259
+
260
+ | Form | Example | Type |
261
+ |------|---------|------|
262
+ | Bare string | \`status=open\` | string \`"open"\` |
263
+ | Quoted string | \`query="hello world"\` | string \`"hello world"\` (use when value contains whitespace) |
264
+ | Integer | \`limit=10\` | number \`10\` |
265
+ | Boolean | \`urgent=true\` | boolean \`true\` |
266
+ | Null | \`assignee=null\` | null |
267
+ | JSON array | \`tags=["a","b"]\` | array \`["a","b"]\` |
268
+ | JSON object | \`payload={"k":"v"}\` | object \`{"k":"v"}\` |
269
+ | Substitution | \`id=\${BUG_ID}\` | resolved at dispatch time |
270
+ | Quoted substitution | \`query="\${QUERY}"\` | quoted resolution (recommended when value may contain whitespace) |
271
+
272
+ **v0.5.0 lint warning** \`unquoted-substitution-in-kwarg-value\` fires when an unquoted \`\${VAR}\` sits in kwarg-value position and VAR's binding origin suggests whitespace. Wrap as \`key="\${VAR}"\` to prevent silent arg truncation if the resolved value contains spaces.
273
+
274
+ **\`$ json_parse \${VAR} -> P\`** (v0.3.3) parses input as JSON and binds the structured value to \`P\`. Dotted descent via \`\${P.field}\` works in conditions and emit. Throws on malformed JSON (caught by \`else:\` / \`# OnError:\`).
235
275
 
236
276
  \`\`\`
237
- walk:
238
- $set FOUND = []
239
- foreach M in $(MESSAGES):
240
- if $(M.id) not in $(FOUND):
241
- $append FOUND $(M.id)
242
- ! NEW: $(M.id)
277
+ # Vars: PAYLOAD={"status":"ok","count":3}
278
+
279
+ read:
280
+ $ json_parse \${PAYLOAD} -> P
281
+ if \${P.status} == "ok" and \${P.count} > "0":
282
+ emit(text="processing \${P.count} items")
243
283
  \`\`\`
244
284
 
245
- The append mutates the outer-scope binding (unlike \`$set\`, which is loop-local inside \`foreach\`). Lint catches: missing init (\`uninitialized-append\`), init inside the same foreach as the append (\`foreach-local-accumulator-target\` — would silently lose data each iteration), init pointing at a non-list value (\`append-to-non-list\`). List-only in v0.3.0 — string concat and map-shaped accumulation deferred.
285
+ ## Substrate-portable LLM + memory dispatch
246
286
 
247
- ### \`&\`Inline data-skill
287
+ The canonical paths for LLM calls and memory queries are MCP dispatch through adopter-wired connectors. Connector names are convention \`llm\` / \`memory\` / \`memory_write\` are descriptive, but adopters wire whatever names match their substrate.
248
288
 
249
289
  \`\`\`
250
- & source-skill-name arg=value -> VAR
290
+ $ llm prompt="Classify priority: \${ISSUE.summary}" -> VERDICT
291
+ $ memory mode=fts query="recent incidents" limit=10 -> CONTEXT
292
+ $ memory_write content="\${REPORT}" addressed_to="oncall" tags="morning-sweep" approved="cron deliverable" -> R
251
293
  \`\`\`
252
294
 
253
- References a \`# Type: data\` skill; the compiler inlines its emitted text
254
- at compile time so the compiled artifact is a single resolved document.
295
+ **Today's reality (v0.7.2).** Default deployments auto-wire \`llm\` + \`memory\` MCP connectors via bundled bridges (\`LocalModelMcpConnector\` over \`LocalModel\`; \`MemoryStoreMcpConnector\` over \`MemoryStore\`), so \`$ llm\` and \`$ memory\` work zero-config against the bundled Ollama + SQLite contracts. Adopters override by re-registering the same connector names against their own substrate. The legacy \`~ prompt=...\` and \`> mode=... query=...\` ops continue to dispatch through the bundled typed contracts during the v0.7.x grace period with tier-2 \`deprecated-symbol-op\` warnings. \`$ memory_write\` is deferred past v0.7.2 (requires \`MemoryStore.write\` interface, not yet shipped).
296
+
297
+ **One canonical call surface per concern.** \`$ memory\` is **the** memory-retrieval call surface — one contract (\`mode=... query=... limit=N -> R\` returning \`{items: [...]}\` envelope), one connector name. Both bare-form (\`$ memory ...\`) and dotted-form (\`$ memory.query ...\`) dispatch through the same registered connector. Same shape for \`$ llm\` (one \`prompt=... [maxTokens=N] [model="..."] -> R\` contract returning the response string). Author against the canonical \`$ llm\` / \`$ memory\` surfaces today; legacy \`~\` / \`>\` removal lands in v0.8/v0.9.
255
298
 
256
299
  ## Pipe filters
257
300
 
258
- Apply on \`$(VAR|filter)\` references; chain left-to-right.
301
+ Apply on \`\${VAR|filter}\` references; chain left-to-right.
259
302
 
260
303
  | Filter | Effect |
261
304
  |---|---|
@@ -264,33 +307,29 @@ Apply on \`$(VAR|filter)\` references; chain left-to-right.
264
307
  | \`json\` | JSON.stringify |
265
308
  | \`trim\` | Whitespace trim |
266
309
  | \`length\` | Array element count or string char count (v0.2.5) |
267
- | \`fallback:"X"\` | (v0.5.0) Coalesce-on-missing: when the upstream ref is unresolved, substitute literal \`X\` and continue the chain. Positional — \`$(VAR|fallback:"-"|upper)\` defaults-then-uppercases. Named to align with op-level \`(fallback: ...)\` vocabulary. |
268
- | \`isodate\` | (v0.5.0) Format an epoch timestamp (ms or sec, auto-detected by magnitude) as ISO-8601. Passes already-ISO strings through unchanged. \`$(EVENT.fired_at_unix|isodate)\`. |
310
+ | \`fallback:"X"\` | (v0.5.0) Coalesce-on-missing: when the upstream ref is unresolved, substitute literal \`X\` and continue the chain. Positional — \`\${VAR|fallback:"-"|upper}\` defaults-then-uppercases. |
311
+ | \`isodate\` | (v0.5.0) Format an epoch timestamp (ms or sec, auto-detected by magnitude) as ISO-8601. Passes already-ISO strings through unchanged. \`\${EVENT.fired_at_unix|isodate}\`. |
269
312
 
270
- **v0.5.0 $(NOW) note.** \`$(NOW)\` now substitutes as an ISO-8601 string per
271
- the documented spec (was: raw epoch ms pre-v0.5.0 — a docs/runtime drift
272
- identified by R3 minion 2). Numeric epoch values remain available as
273
- \`$(EVENT.fired_at)\` (ms) and \`$(EVENT.fired_at_unix)\` (sec).
313
+ **\`\${NOW}\` ambient ref** substitutes as an ISO-8601 string per v0.5.0 spec. Numeric epoch values remain available as \`\${EVENT.fired_at}\` (ms) and \`\${EVENT.fired_at_unix}\` (sec).
274
314
 
275
315
  ## Conditional grammar
276
316
 
277
317
  \`\`\`
278
- if $(VAR): ← truthy check
279
- if not $(VAR): ← falsy check (v0.3.2)
280
- if $(VAR) == "literal": ← equality vs literal
281
- if $(VAR) == $(OTHER): ← equality vs ref
282
- if $(VAR) != "literal": ← inequality
283
- if $(N) < "10": ← numeric comparison (v0.2.5)
284
- if $(N) >= $(THRESHOLD): ← numeric vs ref
285
- if $(M.id) in $(SEEN): ← set membership
286
- if $(M.id) not in $(SEEN):
287
- if $(A) == "ok" and $(B) == "ok": ← logical AND (v0.3.2)
288
- if $(A) == "urgent" or $(B) > "5": ← logical OR (v0.3.2)
289
- if not $(A) and ($(B) or $(C)): ← compound with parens + not (v0.3.2)
318
+ if \${VAR}: ← truthy check
319
+ if not \${VAR}: ← falsy check (v0.3.2)
320
+ if \${VAR} == "literal": ← equality vs literal
321
+ if \${VAR} == \${OTHER}: ← equality vs ref
322
+ if \${VAR} != "literal": ← inequality
323
+ if \${N} < "10": ← numeric comparison (v0.2.5)
324
+ if \${N} >= \${THRESHOLD}: ← numeric vs ref
325
+ if \${M.id} in \${SEEN}: ← set membership
326
+ if \${M.id} not in \${SEEN}:
327
+ if \${A} == "ok" and \${B} == "ok": ← logical AND (v0.3.2)
328
+ if \${A} == "urgent" or \${B} > "5": ← logical OR (v0.3.2)
329
+ if not \${A} and (\${B} or \${C}): ← compound with parens + not (v0.3.2)
290
330
  \`\`\`
291
331
 
292
- Branches via \`if:\` / \`elif COND:\` / \`else:\`. The \`else:\` after a target
293
- body is a separate error-handler block (distinguished by indentation scope).
332
+ Branches via \`if:\` / \`elif COND:\` / \`else:\`. The \`else:\` after a target body is a separate error-handler block (distinguished by indentation scope).
294
333
 
295
334
  ### Compound conditions (v0.3.2)
296
335
 
@@ -298,7 +337,23 @@ body is a separate error-handler block (distinguished by indentation scope).
298
337
 
299
338
  - **Precedence** (tight → loose): comparison ops (\`==\`/\`<\`/etc.) > \`not\` > \`and\` > \`or\`
300
339
  - **Parentheses** override precedence: \`(a or b) and c\`
301
- - **Short-circuit evaluation**: AND skips RHS if LHS is false; OR skips RHS if LHS is true. Useful for the validate-then-access pattern — \`if $(X) == "ok" and $(X.field) ...\` won't error on the field access when \`$(X) == "ok"\` is false.
340
+ - **Short-circuit evaluation**: AND skips RHS if LHS is false; OR skips RHS if LHS is true. Useful for the validate-then-access pattern — \`if \${X} == "ok" and \${X.field} ...\` won't error on the field access when \`\${X} == "ok"\` is false.
341
+
342
+ ## Legacy syntax (grace period — tier-2 deprecated)
343
+
344
+ | Legacy | Canonical |
345
+ |---|---|
346
+ | \`! text\` | \`emit(text="text")\` |
347
+ | \`?? "prompt" -> R\` | \`ask(prompt="prompt") -> R\` |
348
+ | \`@ cmd args [-> R]\` | \`shell(command="cmd args") [-> R]\` |
349
+ | \`@ unsafe cmd\` | \`shell(command="cmd", unsafe=true)\` |
350
+ | \`& skill-name\` | \`inline(skill="skill-name")\` |
351
+ | \`~ prompt="..." -> R\` | \`$ llm prompt="..." -> R\` (auto-wired via \`LocalModelMcpConnector\` bridge in default deployments) |
352
+ | \`> mode=... query=... -> R\` | \`$ memory mode=... query=... -> R\` (auto-wired via \`MemoryStoreMcpConnector\` bridge in default deployments) |
353
+ | \`$(VAR)\` | \`\${VAR}\` |
354
+ | \`(approved: "reason")\` trailer | \`approved="reason"\` kwarg |
355
+
356
+ All legacy forms compile during v0.7.x with tier-2 \`deprecated-symbol-op\` / \`deprecated-substitution-shape\` warnings. Tier-1 promotion (refuse-to-compile) lands in v0.8/v0.9.
302
357
  `;
303
358
  const FRONTMATTER = `# Frontmatter headers — full reference
304
359
 
@@ -312,7 +367,7 @@ Skill files open with \`# Key: value\` headers. Order isn't significant.
312
367
  ## Common
313
368
 
314
369
  - \`# Description: <prose>\` — human-readable explanation; surfaces in dashboards.
315
- - \`# Type: procedural | data\` — \`procedural\` (default) for runtime-fired skills; \`data\` for compile-time-inlined fragments referenced by \`&\` ops.
370
+ - \`# Type: procedural | data\` — \`procedural\` (default) for runtime-fired skills; \`data\` for compile-time-inlined fragments referenced by \`inline(skill="...")\` (canonical) or legacy \`& <skill-name>\` ops.
316
371
  - \`# Vars: NAME=default, OTHER\` — declared variables. \`NAME=default\` provides a default; bare \`NAME\` is required at invocation.
317
372
  - \`# Triggers: cron: 0 9 * * *, session: start\` — autonomous-dispatch sources. Comma-separated entries split by source-keyword boundary; cron expressions with commas (\`30,45 9 * * 1-5\`) parse correctly.
318
373
  - \`# Output: text | slack: chan | prompt-context: agent | template: agent | file: path | card: id | none\` — output routing. **Value shape per kind (v0.5.0 clarification):** \`prompt-context:\` / \`template:\` / \`slack:\` / \`card:\` default to **joined emissions string** (the \`!\` lines concatenated with newlines) — these are human-readable delivery surfaces. \`text\` / \`file:\` default to the **last-bound variable value** (structured), falling back to the emissions array when no var was bound. If your skill emits multiple \`!\` lines and a downstream consumer only sees the final tool output via \`outputs.text\`, that's the structured-default behavior — use \`# Output: prompt-context: <agent>\` (or another text-coerced kind) to publish the joined emissions instead.
@@ -383,7 +438,7 @@ $(VAR.field|filter) field-access then filter
383
438
 
384
439
  Unresolved refs: tier-1 \`undeclared-var\` at compile, \`UnresolvedVariableError\` at runtime.
385
440
  `;
386
- const EXAMPLES = `# Three canonical worked skills
441
+ const EXAMPLES = `# Five canonical worked skills (v0.7.0+ canonical surface)
387
442
 
388
443
  ## 1. Minimal (single target, no dependencies)
389
444
 
@@ -394,13 +449,13 @@ const EXAMPLES = `# Three canonical worked skills
394
449
  # Vars: WHO=world
395
450
 
396
451
  greet:
397
- ! Hello, $(WHO)!
398
- ! Welcome to Skillscript.
452
+ emit(text="Hello, \${WHO}!")
453
+ emit(text="Welcome to Skillscript.")
399
454
 
400
455
  default: greet
401
456
  \`\`\`
402
457
 
403
- Demonstrates: required headers, variable defaults, \`!\` emission with substitution.
458
+ Demonstrates: required headers, variable defaults, \`emit(text="...")\` with \`\${VAR}\` substitution.
404
459
 
405
460
  ## 2. Cron-fired numeric threshold + count
406
461
 
@@ -408,25 +463,27 @@ Demonstrates: required headers, variable defaults, \`!\` emission with substitut
408
463
  # Skill: queue-length-monitor
409
464
  # Description: Count pending items in a queue and alert when the count exceeds threshold
410
465
  # Status: Approved
466
+ # Autonomous: true
411
467
  # Vars: QUEUE_PATH=/var/queue/pending.json, THRESHOLD=10
412
468
  # Triggers: cron: */5 * * * *
413
469
 
414
470
  fetch:
415
- @ cat $(QUEUE_PATH) -> ITEMS (fallback: "[]")
471
+ file_read(path="\${QUEUE_PATH}") -> ITEMS_JSON (fallback: "[]")
472
+ $ json_parse \${ITEMS_JSON} -> ITEMS
416
473
 
417
474
  evaluate:
418
475
  needs: fetch
419
- if $(ITEMS|length) > $(THRESHOLD):
420
- ! Queue backlog: $(ITEMS|length) items pending (threshold $(THRESHOLD)). Action required.
476
+ if \${ITEMS|length} > \${THRESHOLD}:
477
+ emit(text="Queue backlog: \${ITEMS|length} items pending (threshold \${THRESHOLD}). Action required.")
421
478
  else:
422
- ! Queue healthy: $(ITEMS|length) items pending (under $(THRESHOLD)).
479
+ emit(text="Queue healthy: \${ITEMS|length} items pending (under \${THRESHOLD}).")
423
480
 
424
481
  default: evaluate
425
482
  \`\`\`
426
483
 
427
- Demonstrates: \`# Triggers:\` cron, shell \`@\` op with fallback, \`needs:\` body-line dep, numeric comparison, \`|length\` filter, \`if\` / \`else\`.
484
+ Demonstrates: \`# Triggers:\` cron, \`# Autonomous: true\` for unattended skills, \`file_read\` with fallback, \`$ json_parse\` for structured parsing, \`needs:\` body-line dep, numeric comparison, \`|length\` filter, \`if\` / \`else\`.
428
485
 
429
- ## 3. LocalModel branching with agent delivery
486
+ ## 3. LLM branching with agent delivery
430
487
 
431
488
  \`\`\`
432
489
  # Skill: classify-support-ticket
@@ -438,21 +495,21 @@ Demonstrates: \`# Triggers:\` cron, shell \`@\` op with fallback, \`needs:\` bod
438
495
  # Output: prompt-context: oncall
439
496
 
440
497
  classify:
441
- ~ prompt="Classify this support ticket as one of: 'critical', 'normal', 'low'. Reply with only the label. Ticket: $(TICKET_BODY)" model=qwen -> VERDICT
498
+ $ llm prompt="Classify this support ticket as one of: 'critical', 'normal', 'low'. Reply with only the label. Ticket: \${TICKET_BODY}" -> VERDICT
442
499
 
443
500
  route: classify
444
- if $(VERDICT|trim) == "critical":
445
- ! CRITICAL ticket needs immediate attention:
446
- ! $(TICKET_BODY)
447
- elif $(VERDICT|trim) == "normal":
448
- ! Normal-priority ticket queued.
501
+ if \${VERDICT|trim} == "critical":
502
+ emit(text="CRITICAL ticket needs immediate attention:")
503
+ emit(text="\${TICKET_BODY}")
504
+ elif \${VERDICT|trim} == "normal":
505
+ emit(text="Normal-priority ticket queued.")
449
506
  else:
450
- ! Low-priority ticket logged.
507
+ emit(text="Low-priority ticket logged.")
451
508
 
452
509
  default: route
453
510
  \`\`\`
454
511
 
455
- Demonstrates: \`~\` LocalModel op, named model selection, \`|trim\` filter on LLM output, ref-vs-literal comparison, agent delivery via \`prompt-context:\`, augmenting headers (\`# Delivery-context:\` + \`# Templates:\`).
512
+ Demonstrates: \`$ llm\` MCP dispatch (substrate-portable — adopter wires their LLM substrate under the \`llm\` connector name), \`|trim\` filter on LLM output, ref-vs-literal comparison, agent delivery via \`prompt-context:\`, augmenting headers (\`# Delivery-context:\` + \`# Templates:\`).
456
513
 
457
514
  ## 4. Composition — orchestrator invoking child skills
458
515
 
@@ -463,133 +520,151 @@ Demonstrates: \`~\` LocalModel op, named model selection, \`|trim\` filter on LL
463
520
  # Vars: USER_NAME=Scott
464
521
 
465
522
  gather:
466
- $ execute_skill skill_name=calendar-today USER=$(USER_NAME) -> CAL (fallback: "(no calendar data)")
467
- $ execute_skill skill_name=mailbox-triage USER=$(USER_NAME) -> MAIL (fallback: "(mailbox empty)")
468
- $ execute_skill skill_name=weather-summary -> WX (fallback: "(weather unavailable)")
523
+ execute_skill(skill_name="calendar-today", USER="\${USER_NAME}") -> CAL (fallback: "(no calendar data)")
524
+ execute_skill(skill_name="mailbox-triage", USER="\${USER_NAME}") -> MAIL (fallback: "(mailbox empty)")
525
+ execute_skill(skill_name="weather-summary") -> WX (fallback: "(weather unavailable)")
469
526
 
470
527
  render: gather
471
- ! Good morning, $(USER_NAME). Today:
472
- ! • Calendar: $(CAL)
473
- ! • Mailbox: $(MAIL)
474
- ! • Weather: $(WX)
528
+ emit(text="Good morning, \${USER_NAME}. Today:")
529
+ emit(text="• Calendar: \${CAL}")
530
+ emit(text="• Mailbox: \${MAIL}")
531
+ emit(text="• Weather: \${WX}")
475
532
 
476
533
  default: render
477
534
  \`\`\`
478
535
 
479
- Demonstrates: in-skill \`$ execute_skill\` composition (each child runs through the runtime under a depth-counted chain), per-call \`(fallback: ...)\` for resilience, kwarg forwarding (\`USER=$(USER_NAME)\`), \`->\` binding child output for downstream reference.
536
+ Demonstrates: \`execute_skill(...)\` runtime composition (each child runs through the runtime under a depth-counted chain), per-call \`(fallback: ...)\` for resilience, kwarg forwarding, \`->\` binding child output for downstream reference.
480
537
 
481
- ## 5. Dedup-by-id with the accumulator (v0.3.0)
538
+ ## 5. Dedup-by-id with the accumulator (v0.3.0+)
482
539
 
483
540
  \`\`\`
484
541
  # Skill: dedup-walk
485
542
  # Description: Walk a result list, skip items whose id was already seen.
486
543
  # Status: Approved
544
+ # Vars: TOPIC=infrastructure
487
545
 
488
546
  walk:
489
- > mode=topical query="$(TOPIC)" limit=50 -> CANDIDATES
547
+ $ memory mode=topical query="\${TOPIC}" limit=50 -> CANDIDATES
490
548
  $set SEEN = []
491
- foreach C in $(CANDIDATES):
492
- if $(C.id) not in $(SEEN):
493
- $append SEEN $(C.id)
494
- ! NEW: $(C.id)$(C.summary)
549
+ foreach C in \${CANDIDATES.items}:
550
+ if \${C.id} not in \${SEEN}:
551
+ $append SEEN \${C.id}
552
+ emit(text="NEW: \${C.id}\${C.summary}")
495
553
  else:
496
- ! dup: $(C.id)
497
- ! Total novel items: $(SEEN|length)
554
+ emit(text="dup: \${C.id}")
555
+ emit(text="Total novel items: \${SEEN|length}")
498
556
 
499
557
  default: walk
500
558
  \`\`\`
501
559
 
502
- Demonstrates: \`$append\` accumulator pattern, \`$set SEEN = []\` init at the target body (before the foreach) so mutations persist across iterations, \`not in\` membership check against the accumulating list, \`|length\` filter on the final collected list. Pre-v0.3.0 this pattern was structurally unimplementable \`$set\` inside foreach is loop-local, so the SEEN list reset every iteration.
503
- `;
504
- const COMPOSITION = `# Composition
560
+ Demonstrates: \`$ memory\` MCP dispatch (substrate-portable memory query), \`$append\` accumulator pattern, \`$set SEEN = []\` init at the target body (before the foreach) so mutations persist across iterations, \`not in\` membership check against the accumulating list, \`|length\` filter on the final collected list. **Note** most MCP memory tools wrap the array in an envelope object (e.g., \`{items: [...], hasNextPage}\`); the example assumes \`.items\` is the array field. Check your tool's response shape; tier-3 \`object-iteration-advisory\` lint helps when you forget the field accessor.
505
561
 
506
- Skillscript has three composition primitives all let one skill draw on another's output, with different semantics around when, where, and how the child runs.
562
+ ## Triggered cron deliverablememory handoff
507
563
 
508
- ## 1. \`& <skill-name>\` — data-skill inline (compile-time)
564
+ \`\`\`
565
+ # Skill: morning-showstopper-sweep
566
+ # Description: Cron pre-triage; delivers triaged showstoppers to oncall via prompt-context
567
+ # Status: Approved
568
+ # Autonomous: true
569
+ # Vars: PROJECT=INFRA
570
+ # Triggers: cron: 0 8 * * MON-FRI
571
+ # Output: prompt-context: oncall
509
572
 
510
- Inlines an *Approved data skill* into the host skill's compiled artifact at the call site. The data skill's body becomes part of the rendered prompt. Use for *static* knowledge or templated content (style guides, voice rules, runbooks).
573
+ run:
574
+ $ ticketing_search query="project:\${PROJECT} severity:showstopper state:Open" limit=20 -> ISSUES
511
575
 
512
- \`\`\`
513
- brief:
514
- ~ prompt="$(VOICE_RULES) Now write a one-line status:" model=qwen -> RESULT
515
- & voice-rules
576
+ emit(text="Morning showstoppers for \${PROJECT} — \${ISSUES.totalCount} open:")
577
+ foreach ISSUE in \${ISSUES.items}:
578
+ $ llm prompt="Two-line triage hypothesis for: \${ISSUE.summary}" -> ANALYSIS
579
+ emit(text="")
580
+ emit(text="## \${ISSUE.id}: \${ISSUE.summary}")
581
+ emit(text="\${ANALYSIS}")
582
+
583
+ default: run
516
584
  \`\`\`
517
585
 
518
- - Resolved at \`compile()\` time the data skill's \`content_hash\` is recorded in the host's provenance block.
519
- - Provenance lets \`skillfile audit\` detect stale recompiles when a referenced data skill changes.
520
- - The data skill must be marked \`# Skill-kind: data\` (or live in a path the SkillStore recognizes as data); otherwise it's treated as procedural and won't inline.
586
+ Demonstrates: end-to-end trigger process deliver pattern. Trigger fires cron; process pulls data + sub-classifies each issue with \`$ llm\`; delivers via prompt-context channel (each \`emit(text=...)\` becomes a line in the agent's session-start context).
587
+ `;
588
+ const COMPOSITION = `# Composition composing skills from other skills
589
+
590
+ Skillscript has two composition primitives in v0.7.0+ canonical form. Both let one skill draw on another's output, with different semantics around when the child runs.
521
591
 
522
- ## 2. \`& invoke <skill-name>\`runtime call (per-fire)
592
+ ## 1. \`inline(skill="<name>")\`compile-time data-skill inline
523
593
 
524
- Calls a procedural skill at runtime. Each call goes through the runtime under a depth-counted chain (default limit 5) same recursion guard as Style 3 below.
594
+ Inlines an *Approved data skill* into the host skill's compiled artifact at the call site. The data skill's body becomes part of the rendered prompt. Use for *static* knowledge or templated content (style guides, voice rules, runbooks).
525
595
 
526
596
  \`\`\`
527
- escalate:
528
- & invoke notify-oncall
597
+ brief:
598
+ $ llm prompt="\${VOICE_RULES} Now write a one-line status:" -> RESULT
599
+ inline(skill="voice-rules")
529
600
  \`\`\`
530
601
 
531
- - Child skill's outputs flow into the parent's variable scope.
532
- - Failures propagate as \`OpError\`s.
602
+ - Resolved at \`compile()\` time — the data skill's \`content_hash\` is recorded in the host's provenance block.
603
+ - Provenance lets \`skillfile audit\` detect stale recompiles when a referenced data skill changes.
604
+ - The data skill must be marked \`# Type: data\` (or live in a path the SkillStore recognizes as data); otherwise it's treated as procedural and won't inline.
533
605
 
534
- ## 3. \`$ execute_skill skill_name="<child>" ...kwargs -> VAR\` — in-skill execute (per-fire)
606
+ ## 2. \`execute_skill(skill_name="<child>", ...kwargs) -> R\` — runtime invocation
535
607
 
536
- The most general form: the host \`$\`-dispatches the literal tool name \`execute_skill\` (intercepted by the runtime, not sent to any MCP server). Same depth-counted chain as Style 2, plus full kwarg forwarding and \`-> VAR\` binding for downstream use.
608
+ The general composition form: the host calls another skill at runtime, capturing its full execution record. Same depth-counted chain (default 5) as the recursion guard.
537
609
 
538
610
  \`\`\`
539
611
  gather:
540
- $ execute_skill skill_name="calendar-today" USER=$(USER_NAME) -> CAL (fallback: "(no calendar data)")
541
- $ execute_skill skill_name="mailbox-triage" inputs={"USER": "$(USER_NAME)"} -> MAIL
612
+ execute_skill(skill_name="calendar-today", USER="\${USER_NAME}") -> CAL (fallback: "(no calendar data)")
613
+ execute_skill(skill_name="mailbox-triage", inputs={"USER": "\${USER_NAME}"}) -> MAIL
542
614
  \`\`\`
543
615
 
544
- Two kwarg styles, both supported (v0.2.9 fix):
545
- - **Bare kwargs** — \`USER=$(USER_NAME)\` natural skill grammar
546
- - **\`inputs={...}\` JSON** — MCP-call parity, useful when forwarding many fields verbatim
616
+ Two kwarg-forwarding styles, both supported (v0.2.9):
617
+ - **Bare kwargs** — \`USER="\${USER_NAME}"\` natural skill grammar
618
+ - **\`inputs={...}\` JSON** — useful when forwarding many fields verbatim
547
619
 
548
- The bound \`-> VAR\` carries the child's final emit through to the host's scope.
620
+ The bound \`-> R\` carries the child's full execution record (final_vars, transcript, outputs) into the host's scope. Access via \`\${R.final_vars.FIELD}\`, \`\${R.transcript}\`, \`\${R.outputs.text}\`, etc.
549
621
 
550
622
  ## Limits & lint signals
551
623
 
552
- - **Recursion**: depth-5 chain by default (\`ExecuteSkillRecursionError\` if exceeded). Both \`& invoke\` and \`$ execute_skill\` share the counter.
553
- - **Lint** (\`unknown-skill-reference\`, tier-2 as of v0.3.1): \`& <name>\`, \`& invoke <name>\`, and \`$ execute_skill skill_name=<name>\` all validate the child skill exists in the SkillStore at compile time. Forward references are allowed: missing skills lint as a warning (not error), and the runtime throws \`MissingSkillReferenceError\` if still unresolved at execute time. Tier-3 \`deferred-skill-reference\` advisory confirms when the deferred-resolution path is engaged.
624
+ - **Recursion**: depth-5 chain by default (\`ExecuteSkillRecursionError\` if exceeded).
625
+ - **Lint** (\`unknown-skill-reference\`, tier-2 as of v0.3.1): both \`inline(skill="<name>")\` and \`execute_skill(skill_name="<name>", ...)\` validate the child exists in the SkillStore at compile time. Forward references are allowed: missing skills lint as warning (not error), runtime throws \`MissingSkillReferenceError\` if still unresolved at execute. Tier-3 \`deferred-skill-reference\` advisory confirms when the deferred-resolution path is engaged.
554
626
  - **Lint** (\`disabled-skill-reference\`, tier-1): any composition primitive pointing at a \`# Status: Disabled\` skill blocks compile.
555
627
 
556
628
  ## When to use which
557
629
 
558
630
  | Use case | Primitive |
559
631
  |---|---|
560
- | Static knowledge in a prompt | \`& <data-skill>\` |
561
- | Fire-and-forget child call | \`& invoke <skill>\` |
562
- | Child output bound into parent scope | \`$ execute_skill ... -> VAR\` |
563
- | Parallel orchestrators (v0.3.0 candidate — not yet shipped) | parked |
632
+ | Static knowledge in a prompt | \`inline(skill="<data-skill>")\` |
633
+ | Child output bound into parent scope | \`execute_skill(skill_name="<skill>", ...) -> R\` |
634
+
635
+ ## Legacy forms (grace period)
636
+
637
+ - \`& <skill-name>\` → \`inline(skill="<skill-name>")\` (compile-time inline)
638
+ - \`& invoke <skill-name>\` (removed concept) → \`execute_skill(skill_name="<skill-name>")\`
639
+ - \`$ execute_skill skill_name="<child>" ... -> R\` → \`execute_skill(skill_name="<child>", ...) -> R\` (legacy MCP-dispatch shape still compiles during grace; canonical is the function-call shape)
564
640
 
565
641
  See \`help({topic: "examples"})\` example 4 for a worked orchestrator skill.
566
642
  `;
567
643
  const CONNECTORS_PROLOGUE = `# Connectors
568
644
 
569
- The runtime resolves \`$\` / \`~\` / \`>\` / \`# Output:\` dispatches through a
570
- typed registry of five contracts:
645
+ Skillscript skills don't import packages they invoke connectors. The runtime resolves dispatches through a typed registry of five contracts:
571
646
 
572
- | Contract | Purpose | Op |
647
+ | Contract | Purpose | Op surface |
573
648
  |---|---|---|
574
- | SkillStore | Skill source persistence + status lifecycle | (implicit) |
575
- | MemoryStore | Knowledge retrieval | \`>\` |
576
- | LocalModel | LLM inference | \`~\` |
577
- | McpConnector | MCP tool dispatch | \`$\` |
578
- | AgentConnector | Deliver augment/template payloads | \`# Output: prompt-context:\` / \`template:\` |
649
+ | \`SkillStore\` | Skill source persistence + status lifecycle | implicit (\`inline\` / \`execute_skill\` reference) |
650
+ | \`LocalModel\` | LLM inference (Ollama by default) | \`$ llm\` MCP dispatch via auto-wired \`LocalModelMcpConnector\` bridge (v0.7.2); legacy \`~\` op during grace period |
651
+ | \`MemoryStore\` | Knowledge retrieval (SQLite-FTS by default) | \`$ memory\` MCP dispatch via auto-wired \`MemoryStoreMcpConnector\` bridge (v0.7.2); legacy \`>\` op during grace period |
652
+ | \`McpConnector\` | MCP tool dispatch — all external tools | \`$ <connector_name> args\` |
653
+ | \`AgentConnector\` | Deliver augment/template payloads | \`# Output: prompt-context:\` / \`template:\` |
654
+
655
+ **v0.7.2 substrate framing.** Canonical syntax routes substrate-specific dispatch through MCP (\`$ llm\` / \`$ memory\` rather than legacy \`~\` / \`>\`). Default deployments auto-wire the \`llm\` and \`memory\` connector names via bundled bridges (\`LocalModelMcpConnector\` over \`LocalModel\`; \`MemoryStoreMcpConnector\` over \`MemoryStore\`), so \`$ llm\` and \`$ memory\` work zero-config. Adopters override by re-registering those same connector names against their own substrate (e.g., a hosted-model MCP server in place of the local Ollama bridge); the canonical call sites don't change.
656
+
657
+ **One canonical call surface per concern.** \`$ memory\` is **the** memory-retrieval call surface — one contract (\`mode=... query=... limit=N -> R\` returning \`{items: [...]}\` envelope), one connector name. Both bare-form (\`$ memory ...\`) and dotted-form (\`$ memory.query ...\`) dispatch through the same registered connector. Same shape for \`$ llm\` (one \`prompt=... [maxTokens=N] [model="..."] -> R\` contract returning the response string). The legacy \`~\` / \`>\` removal lands in v0.8/v0.9 — author against the canonical \`$ llm\` / \`$ memory\` surfaces today.
658
+
659
+ ## Discovery
579
660
 
580
- Skills don't import packages they invoke connectors. The set wired into
581
- this runtime, plus their feature flags, is discoverable via:
661
+ \`runtime_capabilities()\` reports the live picture: which connectors are registered, which feature flags they advertise, and which named instances exist (e.g., \`default\` / \`qwen\` LocalModels, \`youtrack\` McpConnector).
582
662
 
583
- \`runtime_capabilities()\`
663
+ For shell execution (\`shell(...)\` op), \`runtime_capabilities\` also reports \`shellExecution.mode\` (\`"structural-spawn"\`) and \`shellExecution.unsafe_enabled\` (whether \`shell(command=..., unsafe=true)\` / legacy \`@ unsafe\` is permitted in this deployment).
584
664
 
585
- Call that tool for the live picture of which connectors are registered,
586
- which feature flags they advertise, and which named instances exist
587
- (e.g., \`default\` / \`qwen\` LocalModels).
665
+ ## Container filesystem isolation
588
666
 
589
- For shell execution (\`@\` op), \`runtime_capabilities\` also reports
590
- \`shellExecution.mode\` (\`"structural-spawn"\`) and
591
- \`shellExecution.unsafe_enabled\` (whether \`@ unsafe\` is permitted in
592
- this deployment).
667
+ When the runtime is sandboxed (Docker container, deployed VM, etc.), the runtime's filesystem is namespace-isolated from the author's host. \`file_read("/tmp/x")\` and \`file_write(path="/tmp/x", ...)\` operate on the *runtime's* \`/tmp\`, not the host's. For cross-namespace work, use a known shared volume path or expose the file via a mount point both sides see. \`runtime_capabilities()\` (planned v0.8+) will report writable base paths to make this discoverable from cold-author position.
593
668
  `;
594
669
  const LINT_CODES = `# Lint rule index
595
670
 
@@ -627,12 +702,14 @@ Three tiers per ERD §3:
627
702
  ## Tier-2 (warning)
628
703
 
629
704
  - \`deprecated-question\` — bare \`?\` op (deprecated v1; compile-error in v1.x)
705
+ - \`deprecated-symbol-op\` (v0.7.1) — legacy symbol-form op (\`~\`, \`>\`, \`@\`, \`!\`, \`??\`, \`&\`) compiles but warns with canonical replacement. Tier-1 promotion (refuse-to-compile) lands in v0.8/v0.9.
706
+ - \`deprecated-substitution-shape\` (v0.7.1) — \`$(VAR)\` substitution form compiles but warns; rewrite to \`\${VAR}\`. Tier-1 promotion in v0.8/v0.9.
630
707
  - \`unsafe-shell-ambiguous-subst\` — \`$(NAME)\` inside \`@ unsafe\` body that isn't a declared variable; collides with bash command-sub syntax
631
708
  - \`unsafe-shell-op\` — \`@ unsafe\` op present; requires human review every time
632
709
  - \`unknown-retrieval-arg\` — \`>\` op carries kwargs outside mode/query/limit/connector/fallback (v0.2.12 Bug 26)
633
710
  - \`unknown-skill-reference\` — \`&\` or \`$ execute_skill\` references a skill not in the store (demoted from tier-1 in v0.3.1; runtime throws \`MissingSkillReferenceError\` if still unresolved at execute)
634
711
  - \`unknown-template-reference\` — \`# Templates: <name>\` references a skill not in the store (demoted from tier-1 in v0.3.1)
635
- - \`unconfirmed-mutation\` — \`$\` op invokes a tool whose name suggests mutation (write/update/delete) without a preceding \`??\` confirmation. Silent when the skill declares \`# Autonomous: true\` (v0.4.2 — the autonomous-skill category exempts the rule since the user-confirmation pattern doesn't apply to unattended-execution skills)
712
+ - \`unconfirmed-mutation\` — mutation-class op (\`$\` tool with mutating-name shape, \`$ memory_write\`, \`file_write(...)\`) runs without authorization. v0.7.0+ accepts the captured \`approved="reason"\` per-op kwarg as authorization (any non-empty string; presence is what matters). Silent when the skill declares \`# Autonomous: true\` (v0.4.2 — the autonomous-skill category exempts the rule since the user-confirmation pattern doesn't apply to unattended-execution skills) or when a preceding \`??\` / \`ask(...)\` op gates the mutation in the same target.
636
713
  - \`model-contention\` — async + sync ops on the same model serialize on a single runtime worker
637
714
  - \`draft-with-trigger\` — \`# Status: Draft\` skill has \`# Triggers:\` declared; triggers won't fire until Approved
638
715
  - \`reference-to-disabled-skill\` — \`&\` op references a Disabled skill (also tier-1 in some contexts)
@@ -645,6 +722,7 @@ Three tiers per ERD §3:
645
722
  - \`plugin-collision\` — placeholder for v1.x plugin-loader name conflicts
646
723
  - \`deferred-skill-reference\` — composition ref (\`&\` / \`$ execute_skill\` / \`# Templates:\`) targets a skill not currently in the SkillStore; resolution deferred to execute time (v0.3.1+). Confirms the forward-reference path is engaged; clears once the target is stored.
647
724
  - \`unparsed-json-field-access\` — op text contains \`$(VAR|json_parse).field\`; the \`|json_parse\` filter was removed in v0.3.3. Replace with \`$ json_parse $(VAR) -> P\` then \`$(P.field)\`.
725
+ - \`object-iteration-advisory\` (v0.7.2) — \`foreach IT in \${VAR}\` iterates a bound variable whose origin is a \`$\` MCP tool output, without a \`.field\` accessor. MCP tools commonly wrap arrays in an envelope object (\`.items\`, \`.results\`, \`.issuesPage\`, \`.data\`, \`.records\`). Check the tool's response shape; rewrite as \`foreach IT in \${VAR.items}\` (or the correct field). Placeholder for v0.8 tool-schema introspection that catches this precisely.
648
726
  - \`disallowed-tool\` (tier-1, v0.4.1) — \`$ name.tool\` references a tool not in the connector's \`allowed_tools\` allowlist. Either rewrite the skill to use a permitted tool or update \`connectors.json\` to grant access. Runtime defense-in-depth refuses disallowed dispatch even if lint is bypassed.
649
727
 
650
728
  \`compile_skill({source})\` runs the full lint preflight and reports