This release includes 2 breaking changes for platform teams planning a safe upgrade.
✓ No known CVEs patched in this version
Topics
Affected surfaces
Summary
AI summaryUpdates What changed, Migration, and HIGH across a mixed release.
Full changelog
Three real authorability frictions Perry hit while wiring project-brief
end-to-end against youtrack (RemoteMcpConnector) + amp
(HttpMcpConnector). All cold-author-relevant. Closed in severity order.
Finding 1 (HIGH) — $ <connector> <tool> foot-gun + misleading error
$ youtrack find_projects query="..." (space-separated, CLI muscle
memory shape) parses as a bare-form dispatch where the FIRST token is
the tool name. Skillscript sent name: "youtrack" to the MCP server,
which replied "Tool 'youtrack' not found" — the error sounds like the
connector is broken, sending the author to debug connectors.json
instead of their syntax.
Fix: new tier-1 lint connector-as-tool catches the pattern at
authoring time. When a $ op's body's first token matches a known
connector name AND a second bare identifier follows (not a kwarg key
ending in =), the lint fires with explicit remediation:
$ youtrack find_projects ...—youtrackis a connector name, not
a tool. Bare-form dispatch treats the first token as the tool name,
so this sendsname: "youtrack"to the server (which replies
"Tool 'youtrack' not found" — a misdirecting error). Two correct
forms:$ youtrack.find_projects ...(dotted; explicit
connector.tool) OR$ find_projects ...(bare tool; runtime
resolves to owning connector).
The regex uses \b to prevent greedy backtracking onto partial matches
of the legit same-name kwarg shape ($ data_write content="..." where
content= is a kwarg, not a tool token).
Finding 2 (MED) — ${R|length} silent-wrong char-count
Per-MCP-server result shape varies. When content[0].text JSON-parses
cleanly, the runtime returns a structured value (HttpMcpConnector +
RemoteMcpConnector both use the same unwrapToolResult path). When the
parse fails (prose-wrapped JSON, multi-content responses), the bound
var lands as a string. ${R|length} then silently returns the string's
char-count instead of the intended element-count — Perry's "1394 open
issues" surprise (real: 5).
Fix: tier-3 advisory remote-result-needs-parse. Narrow scope:
fires ONLY on ${R|length} for R bound by $ (the silent-wrong
path), NOT on ${R.field} (which loudly raises
UnresolvedVariableError). Suppressed when the skill already does
$ json_parse ${R} -> P somewhere — author has defended.
Did NOT auto-parse aggressively: the root cause is server-specific
(some servers return JSON-as-text, some prose-wrap, some multi-content);
the runtime's existing JSON.parse path is correct for spec-clean
servers. Auto-strip / multi-content extraction would be risky semantic
changes pending more data on which servers do what.
Finding 3 (LOW-MED) — emissions over lastBoundVar (silent masking closed)
Pre-v0.19.10, outputs.text for a no-template skill with both emit()
AND -> R (or $set R) returned lastBoundVar (the internal scratch),
NOT joined emissions. Perry hit this on project-brief: the emitted
brief lived only in transcript; outputs.text returned the last -> R
value (internal scratch from a mid-skill json_parse step).
Fix: prefer emissions over lastBoundVar in src/runtime.ts. When
the author writes emit(), emissions ARE the canonical output; -> R
is internal scratch. Behavior change for text/file/none output kinds:
} else if (emissions.length > 0) {
outputs[key] = emissions.join("\n"); // v0.19.10 — emit-first
} else if (lastBoundVar !== null && vars.has(lastBoundVar)) {
outputs[key] = vars.get(lastBoundVar); // compute-and-return pattern
}
Compute-and-return pattern (no emit, just -> R) preserved — falls to
lastBoundVar when emissions is empty.
Migration
- Skills calling
$ <connector> <tool>space-separated: tier-1 lint
now blocks compile. Fix: rewrite as$ connector.toolor$ tool. - Skills with
emit()+-> Rrelying on outputs.text = R: behavior
change.outputs.textis now joined emissions. If you need R, use
# Returns: Rto export R asfinal_vars.R. Two pre-v0.19.10 tests
inv0.5.0-outputs-shape.test.tscodified the old (incorrect)
semantic and have been updated to assert the new behavior.
What changed
src/lint.ts—connector-as-tool(tier-1) +remote-result-needs-parse(tier-3)src/runtime.ts— emissions over lastBoundVar in outputs.text pathscripts/loc-ceiling.mjs— narrow ceiling 11250 → 11400tests/v0.19.10-dogfood-findings.test.ts— 10 regression teststests/v0.5.0-outputs-shape.test.ts— updated to assert emit-first semantictests/v0.19.4-output-template.test.ts— legacy-shape test updated to
v0.19.10 string shapetests/v0.2.8.test.ts— recursion-depth-guard test now uses
# Returns: Rfor proper error propagation (was relying on the
closed lastBoundVar leak)
1898 total tests (+10 net new).
Breaking Changes
- The `$` dispatch pattern where the first token matches a known connector name (e.g., `$ youtrack find_projects …`) now triggers tier‑1 lint `connector-as-tool` and is blocked at compile time. Authors must rewrite as `$ connector.tool` or `$ tool`.
- In `src/runtime.ts`, when both `emit()` and a bound variable (`-> R`) are present, `outputs.text` now returns the joined emissions instead of the lastBoundVar value.
Weekly OSS security release digest.
The CVE patches and breaking changes that affected production tools this week. One email, every Sunday.
No spam, unsubscribe anytime.
Share this release
About Skillscript
All releases →Related context
Related tools
Earlier breaking changes
- v0.35.0 Editing `# Deadline:` on an approved skill drops it to Draft (signature invalidates).
Beta — feedback welcome: [email protected]