skillscript-runtime 0.13.7 → 0.13.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/ERD.md DELETED
@@ -1,1139 +0,0 @@
1
- # Skillscript ERD — engineering requirements (placeholder, in flight)
2
-
3
- Engineering requirements document for the Skillscript runtime + compiler. Audience: engineers building skillscript-runtime. Threads scope to ERD slices when work begins.
4
-
5
- Status: placeholder. Requirements being shaped in conversation between Scott + Perry; sections will fill in as decisions land. See companion docs under project anchor `cc2d7cfb`:
6
- - `skillscript-prd` — product positioning, value prop
7
- - `skillscript-language-reference` — language syntax and semantics
8
-
9
- Initial requirements sketched in conversation 2026-05-19 (Scott's starter list + Perry's adds): codebase shape, modifiability, connector contracts, security, compiler architecture, runtime architecture, observability, testing strategy, distribution shape. To be expanded as the design crystallizes.
10
-
11
- ## §1 Codebase shape — small enough to understand, explicit LOC ceiling
12
-
13
- **Requirement.** The skillscript-runtime codebase must be small enough that one person (or one agent) can read it end-to-end in a sitting.
14
-
15
- **Concrete commitments:**
16
- - One process. No internal microservice boundaries.
17
- - Parser + compiler + executor + connector registry + lint together ≤ ~5K LOC for the core. Tests and conformance suite are separate budget.
18
- - Fewer than 20 source files in the core. (Tests + adversarial library count separately.)
19
- - No internal RPC; in-process function calls only. External integration goes through connectors.
20
-
21
- **Why explicit numbers:** "small" drifts upward over feature releases. A PR that pushes past the LOC ceiling is a deliberate decision, not a slide. The ceiling enforces architectural pressure toward fewer, more orthogonal abstractions.
22
-
23
- **Affordance for understanding:** the codebase ships with a one-page architecture map (which file does what), kept current with each release. New contributors (human or agent) read the map first, the code second.
24
-
25
- **Out of scope for this section:** specific file naming, module boundaries (those live in the compiler/runtime architecture sections). This section only sets the size ceiling.
26
-
27
- ## §2 Modifiability — three concrete properties for agent-modifiable codebase
28
-
29
- **Requirement.** Agents (Claude, CC, or future) must be able to modify the codebase and bring in upstream changes without losing local work. "Easy to modify" is operationalized via three concrete properties.
30
-
31
- **1. Tests give clear signals.**
32
- - Every behavioral rule has a test that names the rule and fails predictably when the rule breaks.
33
- - A passing test suite is sufficient evidence that the change ships.
34
- - Test names are diagnostic, not just descriptive — "throws when `?` has no prompt" rather than "test 47."
35
- - No tests gated on environment-specific state (network, time-of-day, host config).
36
-
37
- **2. Code structure maps to language features.**
38
- - Adding a new op kind = adding a file in `ops/` and registering it. Predictable, mechanical.
39
- - Adding a new filter = adding a function in `filters/` and registering it.
40
- - Adding a new connector instance = config change, no code change.
41
- - New contributors (agent or human) can predict where a feature lives without reading the whole codebase.
42
-
43
- **3. Design rationale lives near the code.**
44
- - Non-obvious decisions get a comment explaining the reason, not just the rule. ("Why this works this way" not "what this does.")
45
- - Major design decisions reference the ERD section that justifies them.
46
- - Avoid relying on git blame archaeology for "why is this here?" — the answer is in the source or one click away.
47
-
48
- **Upstream-merge property:** local plugin extensions (filters, connector implementations) live in `~/.skillscript/plugins/` or `node_modules/skillscript-plugin-*`, never in the core source tree. Upstream merges don't touch user extensions.
49
-
50
- **Validation:** does an agent reading the codebase for the first time know where to add a new filter without being told? If yes, this requirement is met. If no, the structure is wrong.
51
-
52
- ## §3 Connectors and the AMP proof case — contracts as the integration boundary
53
-
54
- **Requirement.** All external integration happens through typed connector contracts. The compiler and runtime are connector-agnostic; deployments wire concrete implementations behind the contract interfaces. Skillscript's storage-agnostic claim in the PRD (Adoption flexibility, Substrate independence) depends on this separation: choosing one connector backend doesn't determine the others.
55
-
56
- Satisfies PRD requirements FR-4 (pluggable backends), FR-12 (MCP server contract), NFR-7 (substrate independence), NFR-12 (multi-instance), NFR-13 (containerizable).
57
-
58
- ## The four connector contracts
59
-
60
- The runtime defines four typed connector contracts. Each is a distinct integration boundary; implementations live behind the contract.
61
-
62
- ### MemoryStore — backs `>` retrieval
63
-
64
- ```typescript
65
- interface MemoryStore {
66
- query(filters: QueryFilters): Promise<PortableMemory[]>;
67
- capabilities(): Capabilities;
68
- }
69
- ```
70
-
71
- Returns `PortableMemory[]` to the runtime. Substrate-specific extensions in `metadata`. Curated subset (thread_status, pinned, confidence, etc.) populated where the concept applies.
72
-
73
- ### LocalModel — backs `~` invocations
74
-
75
- ```typescript
76
- interface LocalModel {
77
- run(prompt: string, opts?: { maxTokens?: number; timeoutMs?: number }): Promise<string>;
78
- capabilities(): Capabilities;
79
- }
80
- ```
81
-
82
- Per-instance timeout config (per decision 5: `timeout_ms` field, default 60000ms). Multi-instance by design — multiple named LocalModel instances per deployment.
83
-
84
- ### McpConnector — backs `$` MCP-tool ops
85
-
86
- ```typescript
87
- interface McpConnector {
88
- call(toolName: string, args: object, ctxOverrides?: McpDispatchCtx): Promise<unknown>;
89
- capabilities(): Capabilities;
90
- }
91
- ```
92
-
93
- Per-call identity propagation via `ctxOverrides`. Registry-configured per-connector identity merges with per-call ctx (registry top, ctx fallback).
94
-
95
- ### SkillStore — backs the compiler's source loading (per decision 3)
96
-
97
- ```typescript
98
- interface SkillStore {
99
- // Read
100
- load(name: string, version?: string): Promise<SkillSource>;
101
- query(filter?: SkillFilter): Promise<SkillMeta[]>;
102
- metadata(name: string): Promise<SkillMeta>;
103
- versions(name: string): Promise<VersionInfo[]>;
104
-
105
- // Write
106
- store(name: string, source: string, metadata?: Partial<SkillMeta>): Promise<VersionInfo>;
107
- delete(name: string): Promise<void>;
108
- update_status(name: string, status: SkillStatus): Promise<VersionInfo>;
109
-
110
- // Introspection
111
- capabilities(): Capabilities;
112
- }
113
- ```
114
-
115
- Eight methods. SkillStore is separate from MemoryStore because access patterns and lifecycle differ fundamentally — random read by name vs query by predicate, lifecycle-state-aware operations vs continuous accumulation. The compiler reads from SkillStore; the dashboard (FR-8) reads from SkillStore for the management UI; lint tools read from SkillStore for discovery.
116
-
117
- ## Common types referenced above
118
-
119
- ```typescript
120
- // Lifecycle states (per decision 6 — three states, additive expansion deferred)
121
- type SkillStatus = "draft" | "approved" | "disabled";
122
-
123
- // Source as loaded from the store
124
- interface SkillSource {
125
- name: string;
126
- version: string; // opaque substrate-declared label (see below)
127
- content_hash: string; // substrate-independent identity (see below)
128
- source: string; // raw skillscript text
129
- metadata: SkillMeta; // parsed header data + storage metadata
130
- }
131
-
132
- // Metadata derived from headers + storage layer
133
- interface SkillMeta {
134
- name: string;
135
- version: string;
136
- content_hash: string;
137
- status: SkillStatus;
138
- description?: string;
139
-
140
- // Headers extracted from source
141
- vars?: string[]; // declared input variables
142
- requires?: string[]; // referenced skills (composition deps)
143
- triggers?: TriggerDecl[]; // declared triggers
144
- outputs?: string[]; // declared output channels
145
- type?: "procedural" | "data"; // per decision 1; defaults to procedural
146
-
147
- // Storage metadata
148
- created_at: number; // unix seconds
149
- updated_at: number;
150
- status_changed_at?: number;
151
- author?: string; // agent_id of last writer
152
-
153
- // Substrate-specific bag (matches PortableMemory pattern)
154
- metadata_bag?: Record<string, unknown>;
155
- }
156
-
157
- // Returned by every write op
158
- interface VersionInfo {
159
- name: string;
160
- version: string;
161
- content_hash: string;
162
- status: SkillStatus;
163
- changed_at: number;
164
- changed_by?: string;
165
- }
166
-
167
- // Predicate for `query()`
168
- interface SkillFilter {
169
- status?: SkillStatus | SkillStatus[];
170
- type?: "procedural" | "data";
171
- tag?: string | string[]; // substrate-defined tag mechanism
172
- author?: string;
173
- since?: number; // updated since this unix-seconds timestamp
174
- name_pattern?: string; // glob or regex
175
- limit?: number;
176
- offset?: number;
177
- [key: string]: unknown; // substrate-specific pass-through
178
- }
179
-
180
- interface TriggerDecl {
181
- source: "cron" | "session" | "event" | "agent-event" | "file-watch" | "sensor";
182
- name: string;
183
- agent_id?: string;
184
- }
185
-
186
- interface PortableMemory {
187
- // Core fields — mandatory on every connector return.
188
- id: string;
189
- summary: string;
190
- detail?: string;
191
- score?: number;
192
-
193
- // Curated substrate subset — concept-portable, value-substrate-specific.
194
- thread_status?: string;
195
- pinned?: boolean;
196
- confidence?: number;
197
- domain_tags?: string[];
198
- payload_type?: string;
199
- knowledge_type?: string;
200
- recipients?: string[];
201
- expires_at?: number;
202
- created_at?: number;
203
- agent_id?: string;
204
- vault?: string;
205
-
206
- // Substrate-specific bag. Accessed via $(MEMORY.metadata.X).
207
- metadata?: Record<string, unknown>;
208
- }
209
-
210
- interface QueryFilters {
211
- query: string;
212
- limit: number;
213
- mode: "fts" | "semantic" | "rerank" | string;
214
- [key: string]: unknown;
215
- }
216
-
217
- interface McpDispatchCtx {
218
- agentId?: string;
219
- isAdmin?: boolean;
220
- }
221
- ```
222
-
223
- ## Version vs content_hash semantics
224
-
225
- Every skill carries two version-like fields. They serve different roles and must not be conflated:
226
-
227
- - **`version`** — the **opaque substrate-declared label**. Semver if the substrate maintains semver versioning, content-hash if filesystem-backed and no separate versioning concept exists, monotonic counter, build identifier, whatever the substrate uses internally. **Equality-comparison-only as discipline.** Consumers MUST NOT parse this string, sort it, or attempt arithmetic on it. The runtime treats it as an opaque token; what it means depends on the substrate.
228
-
229
- - **`content_hash`** — the **substrate-independent identity**. Always a SHA-256 of the canonicalized source bytes at load time. The compiler computes this if the substrate doesn't provide it. This is what consumers reason about when they need cross-substrate equality.
230
-
231
- ### Which consumer uses which
232
-
233
- - **Provenance recording (§5)** — stores both. `version` for human-readable display in compiled-artifact headers and the dashboard; `content_hash` for identity comparison during recompile-staleness detection.
234
- - **Staleness detection** — compares `content_hash` between the stored compiled-artifact's recorded inputs and the current source. Substrate-independent; works identically for filesystem-backed and versioned-DB substrates.
235
- - **Version pinning** (e.g., `# Requires: voice-guide@v7`) — substrate-specific. The substrate either understands semver pinning and resolves to the matching `version`, or treats the pin as opaque equality. Contract doesn't constrain.
236
- - **Dependency walking** — uses `content_hash` for equality comparison. "Did this dependency change since I last compiled?" is a content-hash question.
237
-
238
- The discipline: if you find yourself wanting to know whether a version string is "newer," you're reaching into substrate-specific territory. Either use `content_hash` (which is identity, not order) or ask the substrate via `versions(name)` and let it return its ordered history.
239
-
240
- ## Error contract
241
-
242
- All four contracts throw structured errors. Implementations subclass the base `ConnectorError` from `skillscript-runtime/errors`:
243
-
244
- ```typescript
245
- class ConnectorError extends Error {
246
- connector_type: ConnectorType;
247
- implementation: string;
248
- // ...
249
- }
250
-
251
- // SkillStore errors
252
- class SkillNotFoundError extends ConnectorError { skill_name: string; }
253
- class VersionNotFoundError extends ConnectorError { skill_name: string; version: string; }
254
- class LintFailureError extends ConnectorError { diagnostics: LintDiagnostic[]; }
255
- class StorageConflictError extends ConnectorError { skill_name: string; reason: string; }
256
-
257
- // MemoryStore / LocalModel / McpConnector errors
258
- class QueryError extends ConnectorError { mode?: string; }
259
- class DispatchError extends ConnectorError { tool?: string; }
260
- class ModelError extends ConnectorError { model?: string; }
261
- class TimeoutError extends ConnectorError { timeout_ms: number; }
262
- ```
263
-
264
- Per-method contract:
265
-
266
- **MemoryStore:**
267
- - `query(filters)` returns `[]` on empty result (never throws). Throws `QueryError` on malformed filter; `TimeoutError` if substrate times out.
268
-
269
- **LocalModel:**
270
- - `run(prompt, opts)` returns string on success. Throws `ModelError` on model failure; `TimeoutError` if `timeoutMs` elapses.
271
-
272
- **McpConnector:**
273
- - `call(toolName, args, ctxOverrides?)` returns whatever the tool returns. Throws `DispatchError` on tool failure; preserves inner cause via `error.cause`.
274
-
275
- **SkillStore:**
276
- - `load(name, version?)` throws `SkillNotFoundError` if name missing; `VersionNotFoundError` if version specified and missing.
277
- - `query(filter?)` returns `[]` if no matches (never throws). Throws `QueryError` on malformed filter.
278
- - `metadata(name)` throws `SkillNotFoundError` if missing.
279
- - `versions(name)` throws `SkillNotFoundError` if missing.
280
- - `store(name, source, meta?)` throws `LintFailureError` if tier-1 lint rejects; `StorageConflictError` if substrate refuses overwrite.
281
- - `delete(name)` throws `SkillNotFoundError` if missing. Implementations decide referential-integrity behavior (does deleting `voice-guide` orphan dependent skills, or refuse with `StorageConflictError`? Substrate-specific).
282
- - `update_status(name, status)` throws `SkillNotFoundError` if missing; returns new `VersionInfo`.
283
- - `capabilities()` never throws.
284
-
285
- **Error propagation through the runtime:** the executor catches these structured errors and routes them through the language's `else:` / `# OnError:` machinery (per §6). Authors see the error class via filter helpers (`$(ERR|class)` returns `"SkillNotFoundError"` etc.); inner cause preserved for nested error reasoning.
286
-
287
- ## Connector boundary properties
288
-
289
- - **The contract IS the boundary.** Anything beyond the contract is implementation detail.
290
- - **Multi-instance by design.** Each contract supports multiple named instances per deployment. `local-model.default` and `local-model.qwen` and `local-model.fast` registered simultaneously; per-skill resolution selects via `~ model=<name>`.
291
- - **Capability declarations.** Each connector exposes `capabilities()` for runtime + compile-time validation. The compiler can refuse to compile a skillscript that requires capabilities a configured connector doesn't provide.
292
- - **Three-layer config resolution** (env var → working-dir config file → bundled default). All deployment-specific config (paths, endpoints, credentials) is overridable; nothing is host-hardcoded (NFR-13).
293
-
294
- ## Capabilities specification
295
-
296
- The `capabilities()` method on every contract returns a `Capabilities` object that the compiler and runtime use for feature matching, dynamic dispatch, and authoring-tool discovery.
297
-
298
- ```typescript
299
- interface Capabilities {
300
- // Identity
301
- connector_type: ConnectorType; // "memory_store" | "local_model" | "mcp_connector" | "skill_store"
302
- implementation: string; // e.g., "AmpMemoryStore", "OllamaLocalModel", "FilesystemSkillStore"
303
- contract_version: string; // semver of the contract this impl satisfies
304
-
305
- // Feature flags — true means the impl supports the named feature
306
- features: Record<string, boolean>;
307
-
308
- // Optional structured manifest for substrate-specific details
309
- manifest?: Record<string, unknown>;
310
- }
311
- ```
312
-
313
- ### Per-contract feature flag namespaces
314
-
315
- The compiler matches `# Requires:` headers against these flag names.
316
-
317
- **MemoryStore (`memory_store.*`):**
318
- - `supports_semantic` — `mode: "semantic"` queries work
319
- - `supports_rerank` — `mode: "rerank"` queries work
320
- - `supports_tag_filter` — `domain_tags`-style filtering
321
- - `supports_thread_status_filter`
322
- - `supports_pinning` — `pinned` populated on returns
323
- - `supports_decay_model` — decay-aware ranking
324
- - `supports_writes` — substrate has a write surface (read-only stores omit)
325
-
326
- **LocalModel (`local_model.*`):**
327
- - `supports_streaming` — `runStream()` method present (decision 5 v2 path; v1 impls omit)
328
- - `supports_max_tokens` — `maxTokens` opt respected
329
- - `supports_timeout` — `timeoutMs` opt respected (assumed true in v1)
330
- - `supports_embedding` — generates embeddings (future surface)
331
-
332
- Plus `manifest.models_available: string[]` — the list of model identifiers this LocalModel instance can serve.
333
-
334
- **McpConnector (`mcp_connector.*`):**
335
- - `supports_identity_propagation` — accepts `ctxOverrides`
336
- - `supports_streaming_responses` — for long-running tool calls
337
- - `supports_batch` — multi-call batching surface (future)
338
-
339
- Plus `manifest.tools_available: string[]` — the list of tool names exposed by the underlying MCP server.
340
-
341
- **SkillStore (`skill_store.*`):**
342
- - `supports_versioning` — `versions()` returns history > 1 entry
343
- - `supports_tag_filter`
344
- - `supports_audit_trail` — status changes produce auditable history
345
- - `supports_writes` — `store()` + `delete()` + `update_status()` all work
346
- - `supports_atomic_status_transitions` — `update_status()` is transactional
347
-
348
- ### How the compiler uses Capabilities
349
-
350
- Skill author declares required features via `# Requires:`:
351
-
352
- ```
353
- # Requires: memory_store.supports_semantic local_model.supports_streaming
354
- ```
355
-
356
- The compiler matches against the configured connector's `capabilities().features`. Missing feature → compile-time error with a clear diagnostic ("skill requires `memory_store.supports_semantic` but connector `primary` reports `supports_semantic: false`"). Skills without `# Requires:` compile against any configured connector.
357
-
358
- This is Phase 5 of the connector work; the spec is locked now so v1 impls populate the flag namespace correctly.
359
-
360
- ### How the runtime uses Capabilities
361
-
362
- `listMemoryStores()` / `listLocalModels()` / `listMcpConnectors()` return arrays of `{ name, capabilities }` pairs. Authoring tools surface the registered set; agents can query mid-execution to pick a connector based on the moment's needs.
363
-
364
- ## ContractConformance test suite
365
-
366
- To verify that any implementation actually honors a contract, `skillscript-runtime/testing` exports a conformance suite per contract:
367
-
368
- ```typescript
369
- // External package author runs this against their custom MemoryStore
370
- import { MemoryStoreConformance } from "skillscript-runtime/testing";
371
- import { MyCustomMemoryStore } from "./my-store";
372
-
373
- describe("MyCustomMemoryStore conformance", () => {
374
- const store = new MyCustomMemoryStore({ /* test config */ });
375
- MemoryStoreConformance.runAll(store);
376
- });
377
- ```
378
-
379
- `MemoryStoreConformance.runAll(store)` executes every test in the suite. Tests are keyed by rule ID so failures diagnose precisely.
380
-
381
- ### Test categories per contract
382
-
383
- Every conformance suite covers:
384
-
385
- 1. **Method existence + signatures.** Every method on the interface is callable with documented param types and returns the documented shape.
386
- 2. **Return type conformance.** Returns parse-validate against the type schema (uses Zod or equivalent for runtime check).
387
- 3. **Error contract conformance.** Methods throw the documented error class on the documented trigger condition. Inner `cause` chain preserved where applicable.
388
- 4. **Capability declaration conformance.** `capabilities()` returns a valid Capabilities object; `connector_type` matches; `contract_version` is recognized.
389
- 5. **Substrate-specific behavior.** For every feature flag the impl declares true, the corresponding behavior actually works (e.g., if `supports_semantic: true`, semantic queries return results).
390
-
391
- ### Fixture pattern
392
-
393
- Conformance tests need substrate state to exercise. Each impl supplies a fixture builder:
394
-
395
- ```typescript
396
- interface ConformanceFixture<T> {
397
- reset(): Promise<void>; // clear test state
398
- createSkill(name: string, source: string, meta?: Partial<SkillMeta>): Promise<void>;
399
- createMemory(memory: Partial<PortableMemory>): Promise<void>;
400
- // ... per-contract setup helpers
401
- }
402
- ```
403
-
404
- The conformance suite calls the fixture to set up state, then exercises the contract. Each substrate handles its own fixture setup (filesystem-backed: write files; SQLite: insert rows; AMP-backed: write memories).
405
-
406
- ### What conformance does NOT cover
407
-
408
- - **Performance.** Conformance verifies correctness, not speed. Performance benchmarks are a separate `skillscript-runtime/benchmarks` suite (per §9).
409
- - **Cross-impl interop.** Conformance is per-impl. Cross-impl consistency (does Filesystem + AMP produce equivalent results for the same logical query?) is a separate concern; out of scope for v1.
410
-
411
- ### Bundled-default + AMP-backed conformance gate
412
-
413
- The Thread 2 acceptance criteria require both bundled-default impls AND AMP-backed impls (from Thread 8) to pass `*Conformance.runAll()`. If AMP-backed impls fail conformance, either AMP or the contract is wrong; we fix whichever's incorrect before declaring T2 done.
414
-
415
- ## Bundled default implementations
416
-
417
- Out-of-the-box runnable on first install — `skillfile init` + `skillfile run examples/skillscripts/hello.skill.md` works without any deployment configuration.
418
-
419
- - **MemoryStore default:** SQLite-backed local store with FTS + tag filters. Lives at `~/.skillscript/memory.db` (overridable). Single-process, single-user. Capabilities: `supports_tag_filter: true`, `supports_writes: true`, `supports_semantic: false` (v1 — semantic via SQLite extensions is a v1.x add).
420
- - **LocalModel defaults:** three Ollama-backed instances — `default` (gemma2:9b), `gemma2` (alias of default), `qwen` (qwen2.5:7b). All at `localhost:11434` (per-instance overridable). Capabilities: `supports_timeout: true`, `supports_max_tokens: true`, `supports_streaming: false` (v1).
421
- - **McpConnector default:** none wired. Deployments configure their own MCP servers per `connectors.json`. `skillfile init` includes a commented example.
422
- - **SkillStore default:** filesystem-backed at `~/.skillscript/skills/` (overridable). Skills live as `.skill` files; status changes recorded via in-file `# Status:` header edit + git commit (if git initialized) or in a sidecar `.versions.jsonl`. Capabilities: `supports_versioning: true`, `supports_tag_filter: true`, `supports_audit_trail: true` (via git when present, sidecar otherwise), `supports_writes: true`, `supports_atomic_status_transitions: true`.
423
-
424
- ## AMP connector as proof case for the contract
425
-
426
- AMP-backed implementations of all four contracts ship as the primary integration test (Thread 8). The architectural value isn't AMP specifically — it's that AMP exercises each contract at its full surface area. If AMP fits cleanly, simpler substrates (filesystem, SQLite, vector stores) fit trivially. If something doesn't fit, the contract is incomplete and we learn what's missing.
427
-
428
- Concrete:
429
-
430
- - **AmpMemoryStore** (`memory_store.query` → `amp_query_memories`). Translates AMP results into `PortableMemory` shape. AMP-specific fields (vault, confidence_basis, decay_model, knowledge_type) populate the curated subset; everything else goes into `metadata`. Capabilities: `supports_semantic: true`, `supports_rerank: true`, `supports_tag_filter: true`, `supports_thread_status_filter: true`, `supports_pinning: true`, `supports_decay_model: true`, `supports_writes: false` (read-only adapter for v1; writes go through `amp_write_memory` directly, not the connector).
431
- - **AmpSkillStore** (`skill_store.load` → AMP `payload_type:skill` memory by name; `update_status` → memory revision with new `# Status:` header; `versions` → memory version history; `query` → AMP tag/status filter). Lifecycle operations produce an auditable memory revision per call. Capabilities: `supports_versioning: true`, `supports_tag_filter: true`, `supports_audit_trail: true`, `supports_writes: true`, `supports_atomic_status_transitions: true`.
432
- - **AmpMcpConnector** (`call` → amp-mcp's MCP server). Most generic of the four; uses AMP's MCP surface as one of N possible MCP backends. Capabilities: `supports_identity_propagation: true`, `manifest.tools_available` lists the amp_* tool surface.
433
- - **AmpLocalModel** — not applicable. LocalModel is Ollama-shaped; AMP doesn't host models.
434
-
435
- The AMP proof case is Thread 8, parallelizable with T3-T7 once Thread 2 contracts pin.
436
-
437
- ## Model selection convention (from olsen-nightly diagnosis 2026-05-20)
438
-
439
- LocalModel instances should be allocated by use-case-tier, not picked arbitrarily. Convention:
440
-
441
- - **gemma2 (or equivalent classification-class model) for batch/scan work** — Olsen scan, atomization, large-batch classification, anything async or background-scheduled.
442
- - **qwen (or equivalent dispatch-class model) for interactive verdicts in skills** — single-shot decisions inside an active skill execution where latency matters and queue contention with batch work would block forward progress.
443
-
444
- Skill authors use `~ model="qwen"` for latency-sensitive calls. When in doubt: gemma2 if the call is asynchronous from a user/agent's perspective, qwen if a downstream op depends on the response. This convention also lives in the Language Reference's Connectors section.
445
-
446
- ## Contention property (the structural lesson behind the convention)
447
-
448
- Any skill that calls `~` shares the underlying model runner with every other process on the deployment that calls the same model. Ollama serializes per-model dispatch. A skill that dispatches async gemma2 work AND then uses gemma2 directly will race itself — the canonical example being olsen-nightly's `$ amp_olsen_task task_type="scan"` (which fires N gemma2 classification calls) followed by `~ prompt="..."` (which queues behind them).
449
-
450
- **The runtime does not promise concurrency-safe model dispatch.** Skill authors and operators own model-tier allocation. The lint rule for v1.x will flag the in-skill case (a skill body with both `$` ops known to dispatch model X and `~ model=X` ops gets a tier-2 warning). Cross-skill contention isn't compile-detectable; that's a deployment-coordination concern, addressed by the model-tier convention above.
451
-
452
- ## Open questions remaining for Thread 2
453
-
454
- - **Referential integrity on `delete()`.** When `voice-guide` is deleted and three skills `& voice-guide` reference it, what happens? Options: (a) substrate refuses with `StorageConflictError`, (b) substrate orphans the references and the next compile of a dependent skill errors, (c) substrate cascades. Lean: (a) by default, with substrate-specific override possible. Worth confirming before T2 implementations begin.
455
- - **Status transition validity.** Are `draft → approved → disabled` and `approved → disabled` valid? What about `disabled → approved` (revive a previously disabled skill)? Lean: all transitions valid in v1; the dashboard / CLI can enforce policy by refusing certain UI paths. Validity at the contract level is permissive.
456
- - **Capability registration mechanism.** Does the runtime auto-discover capabilities by introspecting registered connectors, or does the connector declare them at registration time? Lean: connectors declare via `capabilities()`; runtime caches the declaration on first call. Worth confirming.
457
-
458
- ## Resolved
459
-
460
- - **`SkillSource.version` shape (2026-05-20):** opaque substrate-declared label + always-populated `content_hash` for substrate-independent identity. Both fields on `SkillSource`, `SkillMeta`, and `VersionInfo`. `version` is equality-comparison-only; `content_hash` is what consumers reach for when they need cross-substrate identity. See "Version vs content_hash semantics" section above.
461
-
462
- ## §4 Security — credential isolation, identity propagation, sandbox guarantees, audit trail
463
-
464
- **Requirement.** Security is enforced at multiple layers, with each layer providing distinct guarantees. No single point of failure.
465
-
466
- Satisfies PRD requirements FR-7 (lifecycle states enforce status), FR-14 (structured error handling), NFR-8 (security boundaries), NFR-13 (containerizable).
467
-
468
- The security story rests on a specific architectural commitment from the PRD: **the constraint IS the safety story, enforced at the language level, not as an aspiration.** Every layer below operates downstream of that commitment.
469
-
470
- ## Credential isolation
471
-
472
- Credentials are per-connector instance, not global. A skillscript dispatching through `mcp.personal` does not have access to credentials for `mcp.production`. Concrete:
473
-
474
- - Per-connector credential storage (env vars or `connectors.json`), scoped to the connector instance name.
475
- - Credentials never appear in compiled skillscript artifacts. Connector identity is referenced by name; the runtime resolves credentials at dispatch time.
476
- - The compiler refuses to compile a skillscript that hard-codes credentials in `$ ... apikey="..."` arguments. Lint rule enforced as tier-1.
477
- - Containerized deployments (NFR-13) mount credentials via environment variables or secret-mount paths; both are first-class resolution sources.
478
-
479
- ## Identity propagation
480
-
481
- When a skillscript fires, whose authority does it act under? Merge order (top wins):
482
- 1. Registry-configured per-connector identity (`connectors.json` field `identity: { agentId: "scotts", isAdmin: false }`).
483
- 2. Per-call `ctxOverrides` threaded by the runtime.
484
- 3. (No intrinsic identity — adapter forwards whatever the merge produces.)
485
-
486
- A skill running as Perry can dispatch against a personal MCP server under a different identity without needing connector-internal state. The runtime's admin-privilege-drop discipline (from CC's `ecb6e1b` security boundary work) applies by default — skills don't inherit scheduler authority.
487
-
488
- ## Language-level sandbox: the `@` op (per decision 2)
489
-
490
- The `@` op is **structurally bounded by the language grammar**, not by runtime sandboxing. Per decision 2 (restricted no-control-flow subset):
491
-
492
- - *One binary per `@` invocation.* `@ curl -s "wttr.in/..." -> RAW` is valid; `@ curl ... | jq ...` is a parse error (pipes are not in the grammar).
493
- - *No shell control flow.* No `if`/`for`/`while` inside `@`. No subshells (`$(...)`). No backticks. No `&&` / `||`.
494
- - *Args parsed structurally.* The compiler sees every arg the binary will receive; static analysis can validate them.
495
- - *Pipe-like composition via `@` op chains.* `@ curl ... -> RAW` followed by `@ jq ... stdin=$(RAW) -> PARSED` is the idiomatic pattern. Each `@` is one binary; the runtime feeds previous output as stdin where declared.
496
-
497
- This delivers the PRD's safety promise: the language grammar can't express `curl | sh` or `cat | base64 -d | bash`. There's no syntactic path to those constructions, regardless of runtime sandbox configuration.
498
-
499
- ### Opt-in escape hatch: `@@`
500
-
501
- For truly irreducible shell-pipeline cases, the language provides an explicit unsafe form: `@@ <full-shell-command>` invokes a full bash shell with the command as-is. Properties:
502
-
503
- - **Lint-flagged tier-2 every time it appears.** Lint warning says "tier-2: this skillscript uses unsafe shell exec; review required for production admission."
504
- - **Runtime-refuse by default.** `runtime.enable_unsafe_shell = false` (default) means `@@` ops fail at runtime with a clean error. Operators must explicitly enable to use.
505
- - **Visible in audit.** `skillfile audit <path>` enumerates every `@@` op, the command shape, and the line number. Reviewers see exactly what's being asked.
506
-
507
- The escape hatch exists because some legitimate pipeline patterns truly can't decompose. The high cost of using it (lint warning + runtime config + audit visibility) ensures it's reserved for the rare case where the architectural commitment yields to pragmatic necessity.
508
-
509
- ## Lint enforcement at admission
510
-
511
- Per FR-6 + §7 (Validation and testing):
512
- - **Disabled skills** rejected at compile (status enforcement, per Lang Ref Lifecycle section). Cannot enter the library.
513
- - **Draft skills** compile-warn. Cannot fire under default trigger dispatch.
514
- - **`@@` ops** flagged tier-2 (requires human review before storage).
515
- - **Mutating ops without `??` confirmation gates** flagged tier-2.
516
- - **Model-contention pattern** (in-skill `$` dispatching gemma2 + downstream `~ model=gemma2`) flagged tier-2 per §3.
517
- - **Plugin name collisions** (filesystem + npm package with same name) flagged tier-3 per §10.
518
- - **Credential hard-coding in `$` args** rejected at compile (tier-1).
519
-
520
- ## Audit trail
521
-
522
- Every dispatch logged:
523
- - Skill ID + version
524
- - Trigger origin (manual / cron / event / agent-invocation)
525
- - Identity used (post-merge per identity propagation rules above)
526
- - Ops fired (kind + connector + result/error)
527
- - Outputs produced (channel + content hash)
528
-
529
- The trail is part of the storage substrate (AMP versioning, file-based + git, or runtime trace memory depending on deployment per §8). Audit tooling reads the trail via `skillfile audit <skill> --history` or equivalent.
530
-
531
- ## Status enforcement
532
-
533
- Skillscripts in Draft or Disabled states cannot fire under default trigger dispatch (per Lang Ref Lifecycle section + decision 6). This is a security property as much as a lifecycle one — operational mistakes (forgot to flip Draft → Approved) don't become production fires.
534
-
535
- The trigger registry respects status; a Draft skill's cron trigger is registered but the scheduler skips dispatch.
536
-
537
- ## Plugin loader security (per decision 4)
538
-
539
- Plugin loading from `~/.skillscript/plugins/` is a real attack surface for security-conscious deployments. Decision 4's resolution config supports:
540
-
541
- - *Default* (`["filesystem", "packages"]`): filesystem wins, then npm packages.
542
- - *Packages-only* (`["packages"]`): filesystem plugins entirely disabled. No local-override surface. All plugins come from signed, versioned npm packages.
543
- - *Filesystem-only* (`["filesystem"]`): less restrictive but predictable. No npm-package surface to compromise.
544
-
545
- Security-conscious deployments set `plugins.resolution_order = ["packages"]` and require all plugins to come from signed, versioned npm packages. The runtime refuses to load anything from local filesystem. Reproducibility holds.
546
-
547
- ## Containerization (per NFR-13)
548
-
549
- The security boundaries above all work inside container isolation. Specifically:
550
-
551
- - *Credential isolation:* container-mounted secrets work the same as host-mounted; resolution chain unchanged.
552
- - *Language sandbox:* the `@` op's grammar constraint operates at the parse level, not the OS level. No nested-isolation concern — the language doesn't permit the dangerous primitives regardless of host.
553
- - *Network endpoints:* per-connector URL config covers all in-container vs out-of-container reachability questions.
554
-
555
- The nested-isolation question that came up in NFR-13 discussion ("sandboxed bash inside a container") is moot in the decision-2 model — there is no sandboxed bash. The language is the sandbox. Containers add a second isolation layer; they don't conflict with the language layer.
556
-
557
- ## Open questions for the security thread
558
-
559
- - **Capability declaration shape** — what does `Capabilities` actually expose, and how does the compiler validate a skillscript's `# Connectors:` declaration against the configured connector's capabilities? Specify before §3 contracts pin.
560
- - **Status state quarantine** — should there be an explicit `Quarantined` state separate from Disabled, for skills under suspicion (security review pending)? Or is "Disabled with a `memory_subtype:quarantine` tag" sufficient? V1 leans: Disabled-with-tag is enough.
561
- - **Cross-organizational trust** — if skillscripts are shared via a registry / marketplace, what verifies the source? Signature-based attestation? Out of v1 scope.
562
- - **`@@` enable/disable as deployment policy** — should there be a way for a deployment to enable `@@` for specific skills only (allowlist by skill name)? V1: binary enable/disable. V2: per-skill if needed.
563
-
564
- ## §5 Compiler architecture — parser, semantic analysis, render
565
-
566
- **Requirement.** The compiler is the canonical deliverable. It must be pure (no side effects beyond reading source + emitting artifact), deterministic (same source + same referenced data-skill versions → byte-identical artifact), and bounded (compile time sub-second for typical skills per NFR-1).
567
-
568
- Satisfies PRD requirements FR-1 (compile to procedural artifact), FR-9 (composition), FR-10 (data skills), FR-13 (audit trail provenance), NFR-1 (compile-time performance), NFR-9 (compile-time determinism).
569
-
570
- ## Three subsystems
571
-
572
- The compiler has three sequential phases. Each is its own module; each can be tested independently against fixture inputs.
573
-
574
- ### 1. Parser
575
-
576
- Source text → AST. Recognizes the full v1 grammar:
577
- - Header lines (`# Skill:`, `# Status:`, `# Description:`, `# Vars:`, `# Requires:`, `# Triggers:`, `# Output:`, `# Connectors:`, `# OnError:`, `# Timeout:`, `# Tests:`)
578
- - Targets with optional `needs:` dependencies
579
- - Op lines (`$`, `~`, `>`, `@`, `@@`, `!`, `??`, `$set`, `?`, `&`)
580
- - Variable interpolation (`$(NAME)`, `$(NAME|filter)`, `$(target.output)`, `$(M.field)`)
581
- - Conditional structures (`if`/`elif`/`else`, with the v1 narrow grammar — truthy / `==` / `!=` / `in` / `not in`)
582
- - `foreach IDENT in EXPR:` blocks with indent-based dedent
583
- - Target-trailing `else:` (error handler) vs conditional `else:` (parser scope-stack discriminates)
584
-
585
- Parser errors are clean: syntax errors name the offending line + column + expected token. Semantic errors are caught in the next phase, not at parse.
586
-
587
- ### 2. Semantic analysis
588
-
589
- AST → resolved skill model. Three sub-phases:
590
-
591
- **a. Variable resolution.** Walks the `# Requires:` cascade, calls SkillStore.metadata() for each referenced skill, populates variable bindings. Caller-supplied inputs > `# Requires:` > `# Vars:` defaults > unresolved error.
592
-
593
- **b. Data-skill compile-time inlining** (per decision 1). When the source references another skillscript via `&`:
594
- - The compiler calls `SkillStore.load(name)` for the referenced skill.
595
- - If the referenced skill has `# Type: data`, its content is **inlined** into the compiled artifact at every reference site. The data is baked into the output; the runtime never fetches it.
596
- - If the referenced skill is procedural (no `# Type: data`), the reference compiles to a runtime invocation — `& other-skill` becomes a runtime call through the executor's skill-invocation machinery.
597
- - The compiler tracks which data-skill versions were inlined: the compiled artifact's provenance records "compiled against `voice-guide@v7`, `taxonomy@v3`." Recompile detection works by hash comparison against current data-skill versions.
598
-
599
- **c. Topological sort.** Resolves the target dependency DAG. `default:` names the goal; the compiler walks dependencies backward to produce the leaves-first execution order. Cycle detection at compile time; orphan-target warnings (targets unreachable from the goal) surface in `warnings[]`.
600
-
601
- **d. Lint passes.** Runs alongside semantic analysis. Validates structural rules (FR-6): undeclared variables, missing dependencies, malformed op grammar, conditional-syntax violations, lifecycle-status enforcement (Disabled skills error at compile; Draft warns), `@@` opt-in shell flagged tier-2, model-contention warnings from §3, plugin-collision warnings from §10. Lint output is structured diagnostic format (rule + block + line).
602
-
603
- ### 3. Render
604
-
605
- Resolved skill model → output artifact. Three output formats:
606
-
607
- - **`prompt`** (canonical) — procedural artifact for agent execution. Anthropic-Skill-shaped markdown that bundles description + resolved variables + topo-sorted target list with translated ops. Data skills appear inlined as their content. The artifact is self-contained — an agent reading it doesn't need to fetch anything else.
608
- - **`prose`** — narrative format for human reading. Per-target paragraphs with heading + flowing prose. Used for documentation, never for execution.
609
- - **`test`** (v1.x) — test harness runner. Executes `# Tests:` block assertions against the compiled artifact.
610
-
611
- ## Provenance tracking (NFR-9, FR-13)
612
-
613
- Every compiled artifact records its full provenance:
614
- ```
615
- {
616
- source_skill: { name: "support-response-draft", version: "v12", hash: "..." },
617
- data_skills_inlined: [
618
- { name: "support-voice-guide", version: "v7", hash: "..." },
619
- { name: "support-response-examples", version: "v4", hash: "..." }
620
- ],
621
- compiled_at: 1779290000,
622
- compiler_version: "1.0.0",
623
- language_version: "1.0"
624
- }
625
- ```
626
-
627
- The provenance is recorded as a structured block in the compiled artifact (or in a sidecar `.provenance.json`). The `skillfile audit <path>` command reads the provenance + compares against current source state, surfacing "data-skill X has been updated since this artifact was compiled" warnings.
628
-
629
- Cross-references FR-13 (audit-trail outputs) and the lifecycle-staleness story from the PRD's "What good looks like" section.
630
-
631
- ## Compile-time vs runtime distinction
632
-
633
- Compile-time:
634
- - Header parsing, target resolution
635
- - `# Requires:` cascade resolution
636
- - Variable substitution for static values (caller inputs, defaults)
637
- - Data-skill inlining (decision 1)
638
- - Lint validation
639
- - Output rendering
640
-
641
- Runtime:
642
- - Ambient ref substitution (`$(NOW)`, iterator vars, output bindings from `>`/`~`)
643
- - `~`/`>`/`@` dispatch
644
- - `$` MCP calls
645
- - foreach iteration
646
- - Conditional evaluation
647
- - Output routing
648
-
649
- The line matters because compile-time is bounded + deterministic; runtime is not. Anything that can be pushed to compile-time (data skill content, variable defaults, dependency resolution) should be.
650
-
651
- ## Timeout configuration (per decision 7)
652
-
653
- The compiler reads `# Timeout: N` headers and bakes them into the compiled artifact. The runtime's timeout resolution chain becomes:
654
-
655
- 1. Per-op override (`~ ... timeoutSeconds=N`) — highest precedence
656
- 2. Skill-level `# Timeout: N` header — applies to all ops in the skill
657
- 3. Connector instance default (`local_model.<name>.timeout_ms`, `mcp.<name>.timeout_ms`)
658
- 4. Built-in language fallback — 300000ms (5 minutes), absolute backstop
659
-
660
- The compiler validates that per-op overrides don't exceed the built-in fallback (5 minutes is the absolute ceiling). If a skill explicitly needs more time than that, the operator overrides the built-in fallback at runtime config; the language doesn't permit unbounded execution.
661
-
662
- ## Reuse from current amp-mcp
663
-
664
- Per the May 17 design notes: parser ~80% reusable, semantic analysis ~60% reusable (the data-skill inlining is new), render ~70% reusable (extending for provenance is straightforward). The reusable code lives in `amp-mcp/src/skills/parser.ts` + `compile_skill.ts` + `render.ts` — porting to standalone runtime is mostly module-shape work, not algorithmic.
665
-
666
- ## Open questions for the compiler thread
667
-
668
- - **Compile output format pin.** The PRD says "Anthropic-Skill-shaped." Concretely: what's the format spec? Markdown with declared frontmatter, declared section structure, declared field set. Worth committing to the exact shape so consumers (agents reading the artifact) can rely on it.
669
- - **Provenance block: inline or sidecar?** Inline keeps the compiled artifact self-describing (auditor reads one file). Sidecar keeps the artifact uncluttered (consumer agents don't see provenance unless they look). Lean: sidecar by default with `--inline-provenance` flag for audit-priority compiles.
670
- - **Lint rule versioning surface.** Per §7 testing strategy — lint rules need to be versioned independently from the compiler. Specify how rules are loaded + how rule version is tracked in compile output.
671
- - **Compile error format.** Error messages need to be structured (for agent consumption) and human-readable (for humans). Same format for both? Different? Specify.
672
-
673
- ## §6 Runtime architecture — executor, dispatcher, trigger scheduler
674
-
675
- **Requirement.** The runtime is the secondary deliverable (compiler is canonical). It interprets parsed skillscripts directly, dispatching through configured connectors. Used for autonomous fires (cron, event-triggered, scheduled) where no agent is in the loop.
676
-
677
- Satisfies PRD requirements FR-2 (runtime-mediated execution), FR-3 (agent-mediated execution), FR-5 (autonomous triggers), FR-14 (structured error propagation), NFR-2 (runtime overhead), NFR-11 (observability), NFR-13 (containerizable).
678
-
679
- ## Three subsystems
680
-
681
- The runtime has three subsystems. Each owns a distinct concern; failures isolate per subsystem.
682
-
683
- ### Executor
684
-
685
- Walks the parsed AST, evaluating ops in topological order from `default:` target's dependencies up to the goal. Manages:
686
- - Variable binding scope (per-target locals, foreach iterator vars, ambient refs)
687
- - Conditional branch evaluation (`if`/`elif`/`else` with v1 grammar)
688
- - foreach iteration (one execution per item, scoped binding cleared between iterations)
689
- - Error propagation through `else:` blocks + `# OnError:` fallbacks + op-level fallback values
690
-
691
- Per-op error contract: every op returns either a result (bound to `-> VAR`) or throws via `makeOpError`. Thrown errors land in `result.errors[]` with origin (target + op kind + inner message preserved). The executor never silently swallows.
692
-
693
- This is CC's surface 1+2 fix from 2026-05-17 (`f292532d`), portable verbatim into the standalone runtime. Inner-tool `isError` results propagate as op errors; `else:` and `# OnError:` machinery catches them; the scheduler reads `result.errors[]` and logs to stderr.
694
-
695
- ### Dispatcher
696
-
697
- Per-op routing through connector contracts (per §3). Resolution chain for each op:
698
-
699
- - `$ <connector>.<tool>` → `McpConnector.call()` against the named connector instance; bare `$ <tool>` → `primary` McpConnector
700
- - `~ prompt=... [model=name]` → `LocalModel.run()` against the named instance; bare → `default`
701
- - `>` → `MemoryStore.query()` against the named instance (via `connector=name` kwarg); bare → `primary`
702
- - `@ <command>` → restricted sandbox shell exec, per decision 2 — one binary per `@`, no control flow, no pipes
703
- - `@@ <command>` → opt-in unsafe full-shell exec, lint-flagged tier-2 every time; runtime refuses if `runtime.enable_unsafe_shell = false` (default)
704
- - `! <text>` → emission via configured output router
705
- - `?? <prompt>` → user-input prompt via interactive surface (refuse in autonomous mode per decision 6)
706
- - `&` → SkillStore.load() + recursive compilation + executor invocation (for procedural skill composition; data skills are inlined at compile time per §5)
707
-
708
- ### Trigger scheduler
709
-
710
- Polls for due triggers, dispatches matching skillscripts. Sources (per Lang Ref Triggers section):
711
- - `cron` — time-based via standard 5-field expression
712
- - `session` — lifecycle hooks (start/end)
713
- - `event` / `agent-event` / `file-watch` / `sensor` — parse-only in v1; dispatch in later phases
714
-
715
- **Status state respect.** The scheduler does not fire skills in Draft or Disabled status (per decision 6). The trigger is registered (visible via `list_triggers`) but the scheduler skips dispatch. When the skill transitions to Approved, its triggers activate.
716
-
717
- **Concurrency.** Each skillscript executes single-threaded. The runtime can execute multiple skillscripts concurrently in independent execution contexts; no shared mutable state between executions. Trigger fires that overlap in time run in parallel; the runtime doesn't serialize.
718
-
719
- ## Per-op timeout enforcement (per decision 7)
720
-
721
- Every op carries a configurable timeout. The resolution chain (top wins):
722
- 1. Per-op override (`~ ... timeoutSeconds=30 ...`) — author's explicit per-call ceiling
723
- 2. Skill-level `# Timeout: N` header — applies to all ops in the skill
724
- 3. Connector instance default (`local_model.qwen.timeout_ms = 60000`, etc.)
725
- 4. Built-in language fallback — 300000ms (5 minutes), absolute backstop
726
-
727
- When the timeout fires, the runtime aborts the in-flight op via AbortController (or equivalent for non-fetch ops), propagates a structured op-error with the timeout context (which timeout fired, at what level), and routes through the `else:` / `# OnError:` machinery.
728
-
729
- The 5-minute built-in fallback is configurable via `runtime.absolute_timeout_ms` for the rare deployment that needs to override either way, but 5 minutes is the canonical default per decision 7.
730
-
731
- ## Contention property (the lesson the runtime carries)
732
-
733
- **The runtime does not promise concurrency-safe model dispatch.** Skill authors and operators own model-tier allocation. When skillscript A's `~ model=gemma2` fires while skillscript B's `$ amp_olsen_task` (which dispatches N gemma2 classification calls) is in flight, the calls serialize at the Ollama runner level. The runtime can't prevent this — it's a shared-resource property.
734
-
735
- The runtime's contribution to mitigating this is:
736
- - Per-instance LocalModel config (decision 5) lets operators provision separate model tiers (gemma2 for batch, qwen for interactive).
737
- - The lint pass flags in-skill self-contention (per §3 contention rule).
738
- - The trigger scheduler does not currently rate-limit or queue based on shared-resource awareness — that's a v2 concern.
739
-
740
- ## Streaming `~` timeout migration (per decision 5)
741
-
742
- V1 ships with single full-completion timeout. The `~` op against `LocalModel.run()` waits for the full response or aborts. This catches "model hung entirely" but not "model trickles tokens forever."
743
-
744
- V2 migration path:
745
- - LocalModel interface grows a `runStream(prompt, opts)` method returning an async iterator of tokens.
746
- - The runtime gains decomposed timeout config: `# FirstTokenTimeout: N` + `# IdleTimeout: N` headers.
747
- - Existing `# Timeout: N` headers stay valid (semantics: full-completion ceiling).
748
- - Backward compatibility: v1-era skillscripts continue to work; v2 features are additive.
749
-
750
- When v2 ships, the lint rule for `# Timeout:` without `# IdleTimeout:` may warn for cron-fired skills (where idle detection matters more), but stays advisory.
751
-
752
- ## Out of scope for runtime
753
-
754
- - Storage governance (vault, supersession, versioning) — that's the connector's job per §3.
755
- - Memory consolidation, decay, freshness inference — substrate-specific, lives behind the MemoryStore connector.
756
- - Agent-style reasoning. The runtime executes deterministic dispatch; reasoning happens at the agent-mediated path (compile to prompt, agent reads + reasons + dispatches via own tools).
757
- - Shared-resource queueing / priority dispatch — per §3 contention property, this is operator-coordination, not runtime concern.
758
-
759
- ## Containerization considerations (per NFR-13)
760
-
761
- The runtime runs cleanly in a container. All filesystem paths (scratch dir for `@` ops, SkillStore default location, MemoryStore default DB path) are configurable via env vars or config file. The `@` sandbox (restricted no-control-flow subset, decision 2) operates inside container isolation — nested isolation isn't a concern because the language constraint is the boundary, not the host sandbox.
762
-
763
- Network endpoints (Ollama URL, MCP server URLs, etc.) are configurable per-connector. The bundled defaults assume `localhost:11434` for Ollama, which a containerized deployment overrides via `local_model.default.url`.
764
-
765
- ## Reuse from current amp-mcp
766
-
767
- Per the May 17 design notes: executor + dispatcher ~60% reusable. Trigger scheduler ~30% reusable (broker-coupled, needs rewrite for standalone). CC's error-visibility surface fix (commit `c580de5`) is verbatim-portable. The skill-execution code in `amp-mcp/src/skills/runtime/executor.ts` is the starting point.
768
-
769
- ## Open questions for the runtime thread
770
-
771
- - **Trigger scheduler de-dup.** When `cron: 0 8 * * *` and `event: scott.present` both fire within seconds, does the skill run twice (independent) or get deduped? V1 lean: independent (per Lang Ref Open Q #9).
772
- - **Concurrent skill execution limits.** A runtime under heavy trigger load (many skills firing simultaneously) could saturate. Should the runtime cap concurrent executions? Default unlimited; configurable per-deployment. v1.x.
773
- - **`@@` runtime enable/disable.** The opt-in unsafe shell op needs a runtime config toggle (`runtime.enable_unsafe_shell = false` by default). Operators enable explicitly per deployment; lint flags skills using `@@` regardless.
774
- - **AbortController equivalent for non-fetch ops.** `~` aborts via fetch's AbortController. `$` MCP calls go through HTTP — same mechanism. `@` shell exec needs `SIGKILL` or equivalent. The runtime needs a unified timeout-abort mechanism that handles all op kinds.
775
-
776
- ## §7 Validation and testing — lint, conformance suite, adversarial library
777
-
778
- **Requirement.** Skillscripts pass static validation before entering the library. Validation is enforced by a lint tool that shares the parser with the compiler. The lint vocabulary is designed for agent consumption (structured diagnostics naming the rule + block + line). The lint and parser are validated by a conformance test suite plus an adversarial library.
779
-
780
- Satisfies PRD requirements FR-6 (lint validation), FR-13 (audit trail), NFR-9 (compile-time determinism), NFR-11 (observability).
781
-
782
- ## Lint architecture
783
-
784
- - *Shared parser, independent rule engine.* The lint tool parses with the same parser as the compiler (single source of truth, no parser-drift risk). Lint rules are a separate engine that walks the parsed AST.
785
- - *Three severity tiers* (per yesterday's CC review):
786
- - *Tier 1 — won't execute correctly.* Undeclared variables, missing dependencies, calls to skills that don't exist, syntactically valid but semantically broken, credential hard-coding in `$` args. **Hard-blocks storage.**
787
- - *Tier 2 — might be unsafe.* `@@` unsafe shell ops, mutating ops without `??` confirmation gates, in-skill model contention pattern, disabled-skill references, agent-authored skills with path-1 requirements that contradict declared `# Paths:`. **Requires human review before admission.**
788
- - *Tier 3 — structurally suspicious.* No `default:`, unreachable blocks (orphan-target warnings), duplicate skill names, plugin-name collisions across resolution locations. **Advisory only.**
789
- - *Agent-consumable output.* Diagnostics name the block + line + rule + remediation suggestion, not just "warning." Format designed for agent re-authoring loops.
790
- - *Lint rules versioned independently.* New attack patterns get new rules; existing library re-linted on rule updates.
791
-
792
- ## V1 lint rule set
793
-
794
- Concrete rules the lint tool enforces:
795
-
796
- **Tier 1 (hard-block):**
797
- - `undeclared-var` — `$(NAME)` references an undefined variable
798
- - `missing-dependency` — `needs:` references a target that doesn't exist
799
- - `unknown-filter` — `$(VAR|filter)` with a filter not in the filter registry
800
- - `malformed-op-grammar` — op line doesn't parse per the op kind's expected shape
801
- - `invalid-conditional-syntax` — conditional outside v1 narrow grammar (truthy / `==` / `!=` / `in` / `not in`)
802
- - `unknown-skill-reference` — `&` references a skill not in the SkillStore
803
- - `disabled-skill-reference` — `&` references a Disabled skill
804
- - `credential-in-args` — `$` op has args matching credential patterns (`apikey=`, `token=`, `password=`)
805
- - `status-disabled` — the skillscript being compiled has `# Status: Disabled`
806
- - `circular-dependency` — target dependency DAG has a cycle
807
-
808
- **Tier 2 (requires review):**
809
- - `unsafe-shell-op` — `@@` op present in skillscript body (always)
810
- - `unconfirmed-mutation` — `$` op invoking known-mutating tool (write, delete, update) without preceding `??` confirmation
811
- - `model-contention` — skill body has both `$` op known to dispatch model X async (e.g., `amp_olsen_task`) AND downstream `~ model=X` op
812
- - `path-declaration-mismatch` — `# Paths:` header declares `runtime` but skill body contains `@` ops requiring agent-mediated path
813
- - `draft-with-trigger` — skill has `# Status: Draft` but declares triggers (the triggers won't fire, but the author should know)
814
-
815
- **Tier 3 (advisory):**
816
- - `no-default-target` — multi-target skill without explicit `default:` declaration
817
- - `unreachable-target` — target declared but not reached from `default:` via dependency walk
818
- - `duplicate-skill-name` — multiple skills in the library share a name
819
- - `plugin-collision` — same plugin name resolves in both `~/.skillscript/plugins/` and `node_modules/`
820
- - `data-skill-staleness` — compiled artifact references a data skill version that has been updated since compile
821
-
822
- ## Conformance test suite
823
-
824
- Behavioral contracts of the language captured as test cases. Every language feature has at least one test that names the feature and fails predictably when broken. Each new feature ships with conformance tests; new tests are additive (don't modify existing).
825
-
826
- Test categories:
827
- - *Parser:* every op kind, every header, every conditional shape, every filter chain
828
- - *Compiler:* variable resolution, data-skill inlining, target topo sort, lint pass integration
829
- - *Runtime:* op dispatch via mock connectors, error propagation through `else:` / `# OnError:`, foreach scoping, conditional evaluation
830
- - *Connectors:* contract conformance — bundled implementations satisfy the contract; AMP-backed implementations satisfy the same contract
831
- - *Lint:* every rule has at least one positive test (lint catches the violation) and one negative test (lint doesn't false-positive on the boundary case)
832
-
833
- Conformance suite runs on every PR. Regressions fail the build.
834
-
835
- ## Adversarial library
836
-
837
- Companion to the conformance suite. Each entry is a skillscript designed to *look reasonable but trigger a specific lint rule or break a specific assumption*.
838
-
839
- Sources:
840
- - *Production failures* — every skill that broke at execute-time becomes a new adversarial example with metadata explaining the trap.
841
- - *Lint-flagged unexpected* — when a skill triggers a lint rule the author didn't anticipate, that's a candidate adversarial example for the rule's edge cases.
842
- - *Deliberate fuzzing* — randomized op generation against the parser; surviving cases that parse but should have been rejected go into the library.
843
- - *Agent-generated adversarial* — cron-fired skill that asks a model "produce adversarial skillscripts for lint rule X." Tireless source.
844
-
845
- Stored as procedural memory (per the framing from yesterday's CC review). Each example carries metadata: what rule it tests, what trap it sets, discovery context, expected behavior. Re-runs on lint version updates; pruned when underlying issues are rendered impossible by architectural changes.
846
-
847
- ## Validation properties
848
-
849
- - *Differential testing.* Renderer + interpreter agree on a skillscript's shape — if they disagree, the disagreement itself is the diagnostic. The conformance suite includes "renderer/interpreter agreement" as an explicit property.
850
- - *Lint as preflight.* A skillscript that fails lint cannot enter the library. The authoring loop is "author → lint → revise → store," not "author → store → break at 3am."
851
- - *No silent passes.* Lint output is structured; consumers (agents or humans) can act on it programmatically.
852
-
853
- ## Open questions for the validation/testing thread
854
-
855
- - *Adversarial example bootstrap.* How do we generate the initial seed library? Nightly cron-fired adversarial generation skill that asks a model "produce adversarial skillscripts for lint rule X"? Lean: ship with ~50 hand-authored adversarial examples per rule, grow via agent-generated adversarial after launch.
856
- - *Lint rule versioning surface.* Config-as-data (lint rules expressible as a typed data skill, letting non-engineers contribute new rules) vs code-shipped (rules are TypeScript modules). Lean: config-as-data for tier-3 advisory rules (community-contributable); code-shipped for tier-1/tier-2 (security-critical, requires review).
857
- - *Test infrastructure for skillscript runtime tests.* The `# Tests:` block (FR-15 visualization adjacent) needs sandboxed runtime execution that doesn't pollute production memory or fire production triggers. Specify the test-sandbox shape.
858
- - *Conformance suite as part of distribution.* Should the adversarial library + conformance suite ship with the npm package? Probably yes (operators can re-run conformance against their deployment). Size matters — ~10K test cases × a few KB each = ~100MB. Lean: ship core conformance with package; full adversarial library is a separate downloadable.
859
-
860
- ## §8 Observability — error propagation, dispatch traces, lifecycle visibility
861
-
862
- **Requirement.** Failures must be visible. The runtime never silently fails — every op error propagates to the dispatching agent and into the persistent trace. Status changes are visible. Trigger fire outcomes are tracked. The browser dashboard (FR-8) is the operator-facing control plane on top of these surfaces.
863
-
864
- Satisfies PRD requirements FR-8 (browser dashboard), FR-13 (audit trail), FR-14 (structured error propagation), NFR-11 (observability).
865
-
866
- ## Error propagation contract
867
-
868
- The foundation: op errors throw via `makeOpError` (or equivalent). The throw routes through the language's error-handling machinery (`else:` blocks, `# OnError:` fallbacks, op-level fallback values per Lang Ref Error Handling section). Unhandled errors accumulate in `result.errors[]`.
869
-
870
- The scheduler reads `result.errors[]` and logs each entry to stderr with origin (skill + target + op kind + inner cause preserved). No silent swallowing. This is CC's surface 1+2 fix from `f292532d` (commit `c580de5`), portable verbatim into the standalone runtime.
871
-
872
- The pattern applies to every op kind, not just `$`. Per-op error contract:
873
- - `$` op returns `isError: true` → executor throws, error propagates
874
- - `~` op times out or model errors → executor throws, error propagates
875
- - `>` op fails → executor throws, error propagates
876
- - `@` / `@@` op exit code non-zero or sandbox refuses → executor throws, error propagates
877
- - `&` op invoked skill itself errors → executor throws with sub-error preserved as nested context
878
-
879
- ## Dispatch trace
880
-
881
- Per-fire trace recording. Three modes (configurable per-deployment):
882
-
883
- - **Off** — no per-fire trace recorded. Error-only trace still required (NFR-11). Production-default for high-volume deployments.
884
- - **On** — every op + every output recorded with timestamp, args, result, duration. Stored per-deployment substrate (AMP-backed: memory with `domain_tags: ["trace:skillscript:<name>"]`; file-backed: JSON log per fire).
885
- - **Sample** — N% of fires recorded for telemetry without full volume.
886
-
887
- When enabled, trace memory follows the "procedural memory" framing from CC's review — traces become re-runnable for debugging, not just historical record. `skillfile replay <trace_id>` replays the trace against current connectors.
888
-
889
- ## Lifecycle visibility
890
-
891
- The dashboard (FR-8) reads from SkillStore via the §3 contract operations:
892
-
893
- - *List view:* `skill_store.list({filter?})` returns SkillMeta[] for every skill in the library — name, description, status, last-fired, fire success rate, ops dispatch count.
894
- - *Detail view:* `skill_store.metadata(name)` + `skill_store.versions(name)` shows skill source, version history, status transition log.
895
- - *Status toggle:* `skill_store.update_status(name, new_state)` is the dashboard's primary write operation — operators flip Draft → Approved or Approved → Disabled via UI without CLI access.
896
- - *Trigger inspection:* `list_triggers({skill?})` shows what's registered, when last fired, error rate, next scheduled fire.
897
- - *Connector inspection:* `list_connectors()` shows what's wired up — which connector backs which contract, capability flags, recent error rate.
898
-
899
- ## Health metrics surface
900
-
901
- Aggregate observability beyond per-fire detail:
902
-
903
- - *Fire rate per skill* — N fires/day broken down by trigger source
904
- - *Success rate per skill* — % of fires that completed without errors in `result.errors[]`
905
- - *Error category breakdown* — for skills with non-zero error rate, distribution by op kind and error type
906
- - *Connector health* — per-connector latency, error rate, last-successful-call timestamp
907
- - *Lifecycle queue* — count of skills in each status state across the library
908
-
909
- These power the dashboard's overview view. Operators see at a glance: which skills are misbehaving, which connectors are degraded, what's queued in Draft awaiting promotion.
910
-
911
- ## Sentinel diagnostics (CLI-facing audit tools)
912
-
913
- - `skillfile diagram <skill>` — mermaid graph of control flow + dispatch. Audit artifact for human review.
914
- - `skillfile audit <skill>` — dispatch shape report (every tool called, every memory write template, every model prompt template, declared inputs, declared output channels). Static analysis, no LLM.
915
- - `skillfile sign <skill>` / `skillfile verify <skill>` — content-hash signing so reviewers confirm what's running matches what was reviewed.
916
- - `skillfile audit <skill> --history` — fire history + error log for the skill. Reads the dispatch trace store.
917
-
918
- ## Trigger fire visibility
919
-
920
- Every trigger fire records:
921
- - skill_id + version compiled against
922
- - trigger source + name + fire timestamp
923
- - success/failure outcome
924
- - error categories (if any)
925
- - output channels emitted to (per skill's `# Output:` declaration)
926
-
927
- Per-skill aggregate visible via dashboard or `skillfile fires <skill>`. Bounded retention — fires older than configurable threshold (default 30 days) auto-pruned.
928
-
929
- The trigger fire log is what catches the kind of issue olsen-nightly experienced — a trigger that's registered, that fires nightly, but whose skill body errors silently. Without per-fire visibility, that pattern persists undetected. With it, the error rate per fire is visible at the dashboard level on day one.
930
-
931
- ## Dashboard architecture (FR-8 detail)
932
-
933
- The dashboard is a separate process from skillscript-runtime. Communicates via the runtime's MCP server contract (§10):
934
-
935
- - *Stack:* lightweight HTTP server + browser UI. Lean: small SPA bundled with the runtime, served at `localhost:<port>` by default. Configurable port + bind address for containerized deployments.
936
- - *Auth:* v1 ships with no auth (single-user / localhost-only assumption). Operators deploying in shared environments configure reverse proxy with auth.
937
- - *Read path:* dashboard polls runtime MCP server every N seconds for state. Calls `skill_list`, `list_triggers`, health-metrics endpoint.
938
- - *Write path:* dashboard calls `skill_status({name, new_state})` for status transitions. Calls `skill_register_trigger` / `skill_unregister_trigger` for trigger management.
939
- - *Containerization (NFR-13):* dashboard image published alongside runtime; can be run separately or co-deployed via `docker-compose.yml`.
940
-
941
- The dashboard is the "operational control plane for non-engineers" per FR-8. Engineers can do everything via CLI; the dashboard exists for operators who need a one-click way to disable a misbehaving skill.
942
-
943
- ## Open questions for the observability thread
944
-
945
- - *Trace memory volume.* Production deployments may produce thousands of fires/day. Default-on trace is probably wrong (storage + cost); default-off-with-error-only-trace is the safer floor. Configurable per-skill or per-trigger.
946
- - *Centralized observability vs per-skill.* Does a deployment have ONE observability dashboard, or per-skill query surfaces? Probably both at maturity; per-skill query surface is the foundational layer.
947
- - *Dashboard auth.* V1 no-auth is suitable for localhost; multi-user deployments need something. JWT? OAuth? Reverse-proxy assumption?
948
- - *Real-time vs polling.* Dashboard polls runtime every N seconds today; should it use WebSockets or SSE for real-time updates? Probably v1.x — polling is simpler and works fine for "dashboard refreshing every 30s."
949
-
950
- ## §9 Performance bounds — what the runtime commits to
951
-
952
- **Requirement.** The runtime commits to specific performance properties. These aren't aspirational targets — they're tested invariants the conformance suite verifies.
953
-
954
- **Compile-time bounds:**
955
- - *Sub-second for typical skillscripts.* "Typical" defined as: ≤ 100 ops total across all targets, ≤ 5 levels of nested conditionals/foreach, ≤ 20 referenced data skills.
956
- - *Linear in skill size.* No exponential blowups from composition or variable resolution. A skillscript twice as large compiles in roughly twice the time.
957
- - *Bounded memory footprint.* Compile-time memory usage stays under ~50MB for typical skills (no compiler-state leak across compiles).
958
-
959
- **Runtime bounds:**
960
- - *Sub-millisecond op dispatch overhead.* The runtime's per-op cost (variable substitution, dispatch routing, output binding) is under 1ms. Actual op execution time depends on the underlying call (LocalModel ~150ms warm, MCP tool call variable).
961
- - *Bounded foreach iteration.* No unbounded loops; iteration count is the iterable's length, evaluated once. A skill iterating over 1000 items is bounded at 1000 iterations.
962
- - *Per-op timeout enforcement.* Every op respects its configured timeout (skill-level header + per-op override + runtime default + built-in fallback). No op can hang the runtime indefinitely.
963
-
964
- **Concurrency:**
965
- - One skillscript executes single-threaded. No parallel op dispatch within a skill in v1. (v2 may add explicit `parallel:` blocks.)
966
- - Multiple skillscripts can execute concurrently in independent runtime instances. The runtime is stateless across skill executions; concurrency is a deployment-level concern.
967
-
968
- **Memory footprint:**
969
- - Runtime base memory under ~100MB excluding connector implementations.
970
- - Connector implementations (LocalModel especially — Ollama keeps models loaded) own their own memory budget, not the runtime's.
971
-
972
- **Throughput targets:**
973
- - Single-process runtime handles ≥ 100 concurrent skillscript executions before saturation. Higher throughput is a horizontal scale problem (multiple runtime instances), not a single-process tuning problem.
974
-
975
- **Validation:**
976
- - Performance benchmarks run as part of the conformance suite. Regressions fail the build.
977
- - Real-fire benchmarks (production-shaped skill bodies, not synthetic) run nightly; results tracked over time.
978
-
979
- **Open questions:**
980
- - v1 single-process is fine; v2 needs to decide on the parallel `&` invocation model (skills calling skills — can they parallelize?). Probably yes, but with explicit syntax.
981
- - Connector latency budgets — should the runtime enforce per-connector budgets (e.g., "no MCP call may exceed 30s")? Or leave to the connector? Probably per-connector config.
982
-
983
- ## §10 Distribution and integration — npm package, CLI, MCP server contract
984
-
985
- **Requirement.** Skillscript-runtime ships as a single, installable package with three integration surfaces: CLI binary, library exports, MCP server. Distribution is friction-free; first-run installations work without elaborate configuration. Containerized deployment is first-class.
986
-
987
- Satisfies PRD requirements FR-11 (CLI), FR-12 (MCP server contract), NFR-3 (memory footprint), NFR-4 (installability), NFR-13 (containerizable).
988
-
989
- ## Package shape
990
-
991
- - *Single npm package* — `skillscript-runtime`. Installable via `npm install -g skillscript-runtime` (or `pnpm`, etc.). Cross-platform Node.js binary (Linux, macOS, Windows).
992
- - *Single binary distribution* recommended for v1.x via `pkg` or similar — for non-Node environments and friction-free installs that don't require Node setup.
993
- - *Container image* — published alongside npm. `docker pull skillscript-runtime:latest` works for containerized deployments without npm dependency.
994
-
995
- ## CLI binary (`skillfile`)
996
-
997
- Author-facing commands:
998
- - `skillfile init` — scaffolds a working config + sample skillscript in `~/.skillscript/` (overridable). First-run friendly.
999
- - `skillfile run <path|name> --input KEY=value` — execute a skillscript (compile + run)
1000
- - `skillfile compile <path|name>` — render the compiled artifact (Skill-shaped output)
1001
- - `skillfile lint <path>` — run static validation
1002
- - `skillfile diagram <path>` — render mermaid graph
1003
- - `skillfile audit <path>` — dispatch shape report (per §8 observability)
1004
- - `skillfile sign <path>` / `skillfile verify <path>` — content-hash signing for audit
1005
-
1006
- Operator-facing commands:
1007
- - `skillfile list` — show available skillscripts (per configured SkillStore)
1008
- - `skillfile status <skill> <new-state>` — transition lifecycle state (Draft / Approved / Disabled per decision 6)
1009
- - `skillfile register-trigger <skill> <source> <name>` — imperative trigger registration
1010
- - `skillfile list-triggers` — show active trigger registry
1011
-
1012
- ## Library exports
1013
-
1014
- `import { compile, execute, lint, render, audit } from 'skillscript-runtime'` for embedding in larger TypeScript/Node applications. Exports follow semver per §11. Breaking changes documented per the versioning policy.
1015
-
1016
- ## MCP server contract (FR-12)
1017
-
1018
- `skillscript-runtime` itself exposes an MCP server (`skillfile-mcp` or similar). Tools surfaced:
1019
-
1020
- - `skill_run({skill_id|path, inputs})` → result
1021
- - `skill_compile({skill_id|path, inputs, mechanical?})` → rendered artifact
1022
- - `skill_lint({skill_id|path})` → validation report
1023
- - `skill_list({filter?})` / `skill_get({skill_id|path})` — discovery
1024
- - `skill_status({skill_id|path, new_state})` — lifecycle transitions
1025
- - `skill_register_trigger`, `skill_list_triggers`, `skill_unregister_trigger`
1026
-
1027
- Compatibility: works as MCP server for Claude Desktop, Claude Code, Cursor, any MCP-aware client. The MCP server is how agents in those clients invoke skills without direct CLI access.
1028
-
1029
- ## Plugin loader (per decision 4)
1030
-
1031
- Plugins (filter implementations, connector implementations beyond bundled defaults) load from two locations:
1032
-
1033
- - *Filesystem:* `~/.skillscript/plugins/` (overridable via env var `SKILLSCRIPT_PLUGINS_DIR` or `plugins.local_dir` in config). For experimentation, local dev loop, deployment-specific extensions.
1034
- - *npm packages:* discovered via standard Node resolution as `node_modules/skillscript-plugin-*`. For distribution, team-shared extensions, signed/versioned plugin libraries.
1035
-
1036
- **Resolution order** (per decision 4): default `["filesystem", "packages"]` — filesystem wins when a plugin name resolves in both locations. Operators invert via config:
1037
-
1038
- ```toml
1039
- [plugins]
1040
- resolution_order = ["filesystem", "packages"] # default — filesystem wins
1041
- # or:
1042
- resolution_order = ["packages", "filesystem"] # packages win
1043
- # or:
1044
- resolution_order = ["packages"] # filesystem disabled (no local overrides)
1045
- ```
1046
-
1047
- Security-conscious deployments set `["packages"]` and reject filesystem plugins entirely. Operators see this is a deployment policy choice, not a language constraint.
1048
-
1049
- **Collision detection:** when both locations have the same plugin name, lint warns ("plugin `my-hash-filter` resolves to filesystem version 0.3.1 but packages also provide 0.4.0; filesystem wins per config"). Author can decide which to keep.
1050
-
1051
- ## Configuration
1052
-
1053
- Config discovery via three-layer resolution (per NFR-13 containerization requirement):
1054
-
1055
- 1. *Env vars* — `SKILLFILE_CONFIG`, `SKILLSCRIPT_PLUGINS_DIR`, per-connector overrides. Ad-hoc / per-process override.
1056
- 2. *Working-dir / agent-scoped `skillfile.config.toml`* — persistent per-deployment config.
1057
- 3. *Bundled defaults* — shipping with the package. Sensible for first-run.
1058
-
1059
- **Bundled defaults** (per §3):
1060
- - SkillStore: filesystem at `~/.skillscript/skills/`
1061
- - MemoryStore: SQLite at `~/.skillscript/memory.db`
1062
- - LocalModel: Ollama at `localhost:11434` with `gemma2:9b` and `qwen2.5:7b` registered as `default` and `qwen` respectively
1063
- - McpConnector: no servers wired by default; commented example in init scaffold
1064
-
1065
- All defaults are file paths or `localhost` references — explicitly overridable when running in a container.
1066
-
1067
- ## Friction-free first run (NFR-4)
1068
-
1069
- ```
1070
- npm install -g skillscript-runtime
1071
- skillfile init
1072
- skillfile run examples/skillscripts/hello.skill.md
1073
- ```
1074
-
1075
- Three commands. Working installation. Bundled examples demonstrate each form (autonomous, inline, compile-to-skill — mirroring the PRD's Use Cases section).
1076
-
1077
- ## Containerization (NFR-13)
1078
-
1079
- The runtime ships with first-class containerization support:
1080
-
1081
- - **Published container image** — `skillscript-runtime:latest` on Docker Hub (or org-equivalent). Multi-arch (linux/amd64, linux/arm64).
1082
- - **Sample `docker-compose.yml`** in the package — wires `skillscript-runtime` alongside `ollama` (for LocalModel) and a SQLite volume (for MemoryStore and SkillStore filesystem backend). Demonstrates a working deployment.
1083
- - **Configuration via env vars** — every config field has a corresponding env var (`SKILLSCRIPT_MEMORY_DB_PATH`, `SKILLSCRIPT_OLLAMA_URL`, etc.). Containers can be configured entirely via env without mounting a config file.
1084
- - **Secrets handling** — supports container-mounted secrets at `/run/secrets/<name>` (Docker/Kubernetes convention). Credentials referenced by path in `connectors.json`.
1085
- - **Plugin loading in containers** — `~/.skillscript/plugins/` is mountable via volume; `node_modules/skillscript-plugin-*` works as in any Node app. Security-conscious deployments set `plugins.resolution_order = ["packages"]` and don't mount the filesystem plugins dir at all.
1086
-
1087
- ## Open questions for the distribution thread
1088
-
1089
- - **Container image base.** `node:alpine` (small) vs `node:slim` (more compatible) vs distroless. Lean toward distroless for security-conscious deployments; `node:alpine` for the default image.
1090
- - **IDE integration.** VS Code extension for syntax highlighting + lint? Probably v1.x deferral. Workable for v1: tree-sitter grammar published as a separate package so any IDE with tree-sitter support gets highlighting.
1091
- - **Distribution beyond npm.** Homebrew, apt, Chocolatey. Probably v2; v1 sticks to npm + container image + manual install.
1092
- - **Versioning + auto-update.** Does the CLI check for updates? Lean: no auto-update; explicit `skillfile self-update` command in v1.x.
1093
-
1094
- ## §11 Versioning and backward compatibility — language version, breaking-change discipline
1095
-
1096
- **Requirement.** Skillscript follows semantic versioning at the language level. Breaking changes to language grammar or runtime contract are clearly demarcated; deprecation periods give authors time to migrate.
1097
-
1098
- **Language version:**
1099
- - *Major.* Breaking changes to grammar or semantics. New major version = old skillscripts may not compile or execute correctly. Mandatory migration tooling ships alongside.
1100
- - *Minor.* New ops, new filters, new triggers, new output kinds. Existing skillscripts unaffected. Compiler is forward-compatible (older sources compile under newer minor versions).
1101
- - *Patch.* Bug fixes, performance improvements, lint rule additions. No language surface changes.
1102
-
1103
- **Skillscript-source version declaration:**
1104
- - Skillscripts may declare `# Language: 1.x` to lock to a specific language version. Compiler reads the declaration and applies version-appropriate semantics.
1105
- - Default version (if declaration omitted): latest stable.
1106
-
1107
- **Connector contract version:**
1108
- - `MemoryStore` / `LocalModel` / `McpConnector` / `SkillStore` contracts versioned independently from the language. Connector implementations declare which contract version they implement.
1109
- - Runtime checks compatibility at startup; refuses to wire a connector that implements an incompatible contract version.
1110
-
1111
- **Deprecation discipline:**
1112
- - Deprecated language features compile-warn for at least one minor version before becoming compile errors in the next major.
1113
- - Migration tooling: `skillfile migrate <path>` walks a skillscript, identifies deprecated patterns, proposes replacements. Authoring assistance, not automated rewrite.
1114
-
1115
- **Breaking-change criteria** (what counts as a major-version bump):
1116
- - Removing or renaming an op kind (e.g., killing `?` op).
1117
- - Changing the semantics of an existing op (e.g., changing `$` op error propagation).
1118
- - Changing the connector contract surface in non-additive ways.
1119
- - Removing or renaming a filter.
1120
-
1121
- **Non-breaking criteria** (minor or patch):
1122
- - Adding a new op, filter, header, trigger source, or output kind.
1123
- - Adding a new contract method (additive).
1124
- - Adding a new lint rule (tier 1/2/3 advisory; existing skills re-linted).
1125
- - Bug fixes that align behavior with documented spec.
1126
-
1127
- **Compatibility commitment:**
1128
- - Once a major version ships, no breaking changes within that major for at least 12 months.
1129
- - Major versions supported with patches for at least 12 months after the next major ships (overlap period).
1130
-
1131
- **Open questions:**
1132
- - Language version vs runtime version — should they be tied or independent? Probably tied (runtime implements one language version at a time, with explicit compatibility matrix).
1133
- - Per-skill version pinning for executed-by-old-runtime case — if a skillscript declares `# Language: 1.x` but the runtime is `2.x`, runtime should compile under 1.x semantics. Specify the runtime's multi-version compile capability.
1134
- - Connector contract evolution — how do existing skillscripts behave when a connector contract grows? Skillscripts don't see the connector contract directly (they see the ops); contract evolution should be invisible to source.
1135
-
1136
- ---
1137
-
1138
- *Rendered from `skillscript/skillscript-erd` — 2026-05-20 13:56 EDT*
1139
- *Source of truth: AMP (`amp_render_document("skillscript/skillscript-erd")`)*