This release adds 3 notable features for engineering teams evaluating rollout.
✓ No known CVEs patched in this version
Topics
Affected surfaces
Summary
AI summaryUpdates Files changed, NEW, and OR across a mixed release.
Full changelog
Three Phase-4 findings + one substrate-side bug uncovered along the way,
plus two Phase-3-era findings Perry surfaced after the v0.15.4 ship.
None are runtime correctness bugs in the Phase 4 set — the warm adopter
completed Phase 4 with full conformance and all probes green. The two
carry-overs ARE bugs (one runtime, one DX). Bundling them avoids a
release cycle.
The Phase 4 attribution skews substrate-ward — typed contract held, the
documented bootstrap({dataStore}) path worked, conformance passed
unmodified. Phase 4 mostly stress-tested where the contract is silent
rather than where it's wrong; v0.15.5 fills the silences. The Phase 3
carry-overs are the discipline-only-contract pattern recurring (5th
instance of that class — see enableUnsafeShell below).
DataWrite.expires_at: number | null — portable durable-forever opt-in
Pre-v0.15.5, DataWrite.expires_at: number forced every write to specify
a timestamp. Substrates with default TTL (AMP memory vaults, Redis with
default expiry, hosted memory APIs) had no portable contract verb to
express "durable forever" — each adopter had to invent a substrate-
specific durability path. The warm adopter papered over it via AMP's
decay_model: "none" + pinned: true flags inside their AmpDataStore,
but that's AMP-specific knowledge that doesn't transfer.
Type is now number | null:
- Unix timestamp → finite expiry (substrate-specific honor / sweep)
null→ "durable forever" — substrates with default sweep map this to
their pin / no-decay flag; substrates that are already durable-by-default
(the bundledSqliteDataStore) treat it as a no-op.- Omitted → defer to the substrate's default lifecycle policy.
Back-compat preserved: existing skill bodies that set numeric timestamps
or omit the field continue to work unchanged. The null opt-in is new.
data-store-roundtrip softens to N ≥ 1 (substrate-portable across FTS variance)
The v0.15.3 + v0.15.4 demo's description asserted "read back 1 items"
based on an FTS query against a per-run unique marker embedded in
content. Worked for SqliteDataStore's strict FTS; broke for AMP's
token-OR FTS where the marker's tokens (probe, year, month, etc.)
matched prior runs' records too. The contract is silent on FTS matching
strictness — both substrate behaviors are conformant — so the demo's
exact-count claim baked in a hidden capability assumption.
Fix: the demo body keeps the v0.15.3 shape (marker in content + FTS
query) but the assertion softens to N ≥ 1 items returned. Works
across any FTS-supporting substrate without imposing a hidden strictness
requirement. The portable strict-count pattern (domain_tags=[...]
filter) is documented in the playbook for adopters who need deterministic
exact-count reads — but not required by the bundled smoke test.
The demo body re-stamped (v1:fb106073); scaffold/skills copy synced.
Description rewritten to make the FTS-variance reality explicit and
point to the playbook's strict-count pattern.
Per Perry's cf5ad0d7 thread: "the substrate-portability claim is
better served by demos that work uniformly than by demos that demand a
specific FTS semantic that varies across implementations." Bundled demos
are install-validation; the canonical pattern guides live in docs.
SqliteDataStore.query() honors domain_tags: string[] (was silently dropped)
While fixing the demo, surfaced a real substrate-side bug. The bundled
SqliteDataStore.query() only handled domain_tags when the filter
value was a single string; passing an array silently dropped the filter
and returned unfiltered results. The contract docs
(DataStoreTemplate/README.md) document arrays as "any-of (OR) match" —
discipline-only-contract that bit the demo and would have bitten any
adopter passing arrays.
Fix: domain_tags accepts both shapes now:
domain_tags: "tag"— single-tag exact match (existing behavior)domain_tags: ["a", "b", ...]— any-of (OR) match across the listed tagsdomain_tags: []— empty array = no filter applied (avoids the trap
where a generated empty list silently zeroes results)
Implemented via dynamically-bound IN ($tag_0, $tag_1, ...) placeholders
so SQLite-prepared-statement caching stays effective per-arity. 9 new
test cases cover the matrix (string, array, empty-array, single-element-
array, non-matching, FTS+tag combined narrowing).
Adopter-playbook gains FTS-strictness + durable-forever notes
Two new bullets in the §"Notable things..." section, both timeless:
- "FTS matching strictness varies by substrate" — articulates that the
contract names the modes but doesn't pin down semantics within each.
Portable read patterns usedomain_tagsfiltering for deterministic
count assertions; FTS is the mode wrapper with substrate-loose
semantics. - "Durable-forever opt-in via
expires_at: null" — documents the new
portable verb + the substrate impl responsibility (pin / no-decay map
on TTL-default substrates; no-op on durable-default substrates).
Files changed
src/connectors/types.ts—DataWrite.expires_at: number | null.src/connectors/data-store.ts—expires_at: nullno-op for SqliteDataStore;domain_tags: string[]honored via dynamic-IN placeholders.src/mcp-server.ts—enableUnsafeShellthreaded into execute_skill ExecuteContext.src/lint.ts— newvars-space-separatedtier-2 rule.examples/skillscripts/data-store-roundtrip.skill.md— description rewritten to soften the count assertion (N ≥ 1) and point at the playbook's tag-filter pattern for adopters needing strict counts; body keeps the v0.15.3 marker-in-content FTS-query shape. Re-stamped.scaffold/skills/data-store-roundtrip.skill.md— synced.tests/v0.15.5-domain-tags-array.test.ts(NEW) — 9 cases covering substrate fix + newnullpath.tests/v0.15.5-enable-unsafe-shell-ctx.test.ts(NEW) — 3-case regression for the ctx-threading bug.tests/v0.15.5-vars-space-separated-lint.test.ts(NEW) — 10 cases covering positive + negative shapes for the lint rule.docs/adopter-playbook.md— FTS-strictness + durable-forever bullets.package.json— version 0.15.4 → 0.15.5.tests/dogfood-t7.test.ts— version assertion.CHANGELOG.md— this entry.
Phase 3 carry-over (1): enableUnsafeShell threaded into execute_skill ctx
Perry's thread adf47c0b — runtime bug surfaced by the warm adopter
during Phase 3 weather-skill authoring. mcp-server.ts built the
execute_skill ExecuteContext with {registry, mechanical, recursionDepth}
but never read enableUnsafeShell from McpServerDeps. Net effect: an
adopter who configured the runtime with enableUnsafeShell: true had:
- lint passes ✓
- compile passes ✓
runtime_capabilitiesreportedunsafeShellEnabled: true✓- but
execute_skillexecution path refusedshell(..., unsafe=true)✗
The contract surface declared discipline; the dispatch path didn't honor
it. 5th instance of the discipline-only-contracts class — sibling to
v0.13.7 skill_status, v0.14.0 strict_filters, v0.14.x mutation gate,
v0.15.0 skill_write declared-but-unwired. One-line ctx fix in
mcp-server.ts:executeSkill (now spreads enableUnsafeShell when set in
deps), plus a three-case regression test
(tests/v0.15.5-enable-unsafe-shell-ctx.test.ts) covering:
enableUnsafeShell: true→shell(unsafe=true)via execute_skill succeedsenableUnsafeShell: false→ refuses withUnsafeShellDisabledErrorenableUnsafeShellunset → refuses (default-false posture preserved)
Per Perry's thread, a v0.16.x structural follow-on is worth considering:
a contract-test fixture that forces every config flag in
runtime_capabilities to have a matching execution-path probe test.
Would catch this whole class pre-merge. Filed but not landed here —
v0.15.5 closes the specific surface; the fixture pattern is its own scope.
Phase 3 carry-over (2): vars-space-separated lint
Perry's thread 1b9d83a7 — warm adopter space-separated their
# Vars: declarations (# Vars: A=1 B=2), parser silently dropped B
(parser only splits on commas), downstream $(B) references fired
undeclared-var pointing at the symptom not the cause. One debugging
cycle wasted to discover the rule.
New tier-2 lint vars-space-separated: detects when a var's default
value contains whitespace followed by an IDENT= shape (in non-quoted
regions), emits a warning naming the suspect missing comma. Heuristic
respects quote-boundaries — # Vars: NAME="Bob Jones" doesn't false-fire
because the whitespace is inside the quoted region.
Ten test cases in tests/v0.15.5-vars-space-separated-lint.test.ts cover
the positive cases (A=1 B=2, three-space-separated, quoted-whitespace
mixed with comma-separated) and the negative cases (canonical comma form,
quoted internal whitespace, value with space but no follow-on =, URL
with ://, etc.).
Cold-adopter dogfood arc maturity progression
Phase 1 found bugs in the install path (broken examples, missing exports,
demo non-runnability). Phase 2 found seams in the typed-contract fork
path (conformance namespacing, ESM setup). Phase 3 found friction in
update / repeat-run paths (idempotency, warning suppression). Phase 4
found silences in the contract itself (expires_at type didn't have a
durability verb; FTS strictness was undefined). Different class of
finding, different remediation cost. The framework is approaching "what's
missing from the spec" rather than "what's wrong with the spec" —
maturity signal worth banking.
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]