skillscript-runtime 0.2.4
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/ARCHITECTURE.md +70 -0
- package/LICENSE +21 -0
- package/README.md +346 -0
- package/dist/audit.d.ts +33 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +76 -0
- package/dist/audit.js.map +1 -0
- package/dist/bootstrap.d.ts +69 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +117 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +805 -0
- package/dist/cli.js.map +1 -0
- package/dist/compile.d.ts +88 -0
- package/dist/compile.d.ts.map +1 -0
- package/dist/compile.js +544 -0
- package/dist/compile.js.map +1 -0
- package/dist/connectors/agent-noop.d.ts +23 -0
- package/dist/connectors/agent-noop.d.ts.map +1 -0
- package/dist/connectors/agent-noop.js +43 -0
- package/dist/connectors/agent-noop.js.map +1 -0
- package/dist/connectors/agent.d.ts +54 -0
- package/dist/connectors/agent.d.ts.map +1 -0
- package/dist/connectors/agent.js +21 -0
- package/dist/connectors/agent.js.map +1 -0
- package/dist/connectors/index.d.ts +13 -0
- package/dist/connectors/index.d.ts.map +1 -0
- package/dist/connectors/index.js +17 -0
- package/dist/connectors/index.js.map +1 -0
- package/dist/connectors/local-model.d.ts +41 -0
- package/dist/connectors/local-model.d.ts.map +1 -0
- package/dist/connectors/local-model.js +106 -0
- package/dist/connectors/local-model.js.map +1 -0
- package/dist/connectors/mcp.d.ts +22 -0
- package/dist/connectors/mcp.d.ts.map +1 -0
- package/dist/connectors/mcp.js +31 -0
- package/dist/connectors/mcp.js.map +1 -0
- package/dist/connectors/memory-store.d.ts +53 -0
- package/dist/connectors/memory-store.d.ts.map +1 -0
- package/dist/connectors/memory-store.js +169 -0
- package/dist/connectors/memory-store.js.map +1 -0
- package/dist/connectors/registry.d.ts +74 -0
- package/dist/connectors/registry.d.ts.map +1 -0
- package/dist/connectors/registry.js +127 -0
- package/dist/connectors/registry.js.map +1 -0
- package/dist/connectors/skill-store.d.ts +38 -0
- package/dist/connectors/skill-store.d.ts.map +1 -0
- package/dist/connectors/skill-store.js +314 -0
- package/dist/connectors/skill-store.js.map +1 -0
- package/dist/connectors/types.d.ts +188 -0
- package/dist/connectors/types.d.ts.map +1 -0
- package/dist/connectors/types.js +35 -0
- package/dist/connectors/types.js.map +1 -0
- package/dist/dashboard/server.d.ts +40 -0
- package/dist/dashboard/server.d.ts.map +1 -0
- package/dist/dashboard/server.js +122 -0
- package/dist/dashboard/server.js.map +1 -0
- package/dist/dashboard/spa/app.js +375 -0
- package/dist/dashboard/spa/index.html +26 -0
- package/dist/dashboard/spa/styles.css +99 -0
- package/dist/errors.d.ts +111 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +187 -0
- package/dist/errors.js.map +1 -0
- package/dist/filters.d.ts +17 -0
- package/dist/filters.d.ts.map +1 -0
- package/dist/filters.js +40 -0
- package/dist/filters.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/lint.d.ts +97 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +990 -0
- package/dist/lint.js.map +1 -0
- package/dist/mcp-server.d.ts +93 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +505 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/metrics.d.ts +51 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +107 -0
- package/dist/metrics.js.map +1 -0
- package/dist/parser.d.ts +160 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +991 -0
- package/dist/parser.js.map +1 -0
- package/dist/provenance.d.ts +43 -0
- package/dist/provenance.d.ts.map +1 -0
- package/dist/provenance.js +58 -0
- package/dist/provenance.js.map +1 -0
- package/dist/runtime.d.ts +145 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +1071 -0
- package/dist/runtime.js.map +1 -0
- package/dist/scheduler.d.ts +121 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +271 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/skill-manager.d.ts +121 -0
- package/dist/skill-manager.d.ts.map +1 -0
- package/dist/skill-manager.js +251 -0
- package/dist/skill-manager.js.map +1 -0
- package/dist/testing/conformance.d.ts +57 -0
- package/dist/testing/conformance.d.ts.map +1 -0
- package/dist/testing/conformance.js +365 -0
- package/dist/testing/conformance.js.map +1 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/trace.d.ts +141 -0
- package/dist/trace.d.ts.map +1 -0
- package/dist/trace.js +226 -0
- package/dist/trace.js.map +1 -0
- package/examples/README.md +56 -0
- package/examples/classify-support-ticket.skill.md +30 -0
- package/examples/cut-release-tag.skill.md +40 -0
- package/examples/doc-qa-with-citations.skill.md +12 -0
- package/examples/feedback-sentiment-scan.skill.md +29 -0
- package/examples/hello.skill.md +9 -0
- package/examples/hello.skill.provenance.json +10 -0
- package/examples/morning-brief.skill.md +24 -0
- package/examples/programmatic-trace-demo.mjs +89 -0
- package/examples/service-health-watch.skill.md +18 -0
- package/package.json +100 -0
- package/scaffold/config.toml +35 -0
- package/scaffold/connectors.json +19 -0
- package/scaffold/examples/hello.skill.md +9 -0
package/dist/trace.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, readdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join, basename, dirname } from "node:path";
|
|
4
|
+
const DEFAULT_SAMPLE_PCT = 10;
|
|
5
|
+
const DEFAULT_RETENTION_MS = 30 * 24 * 60 * 60 * 1000;
|
|
6
|
+
// ─── FilesystemTraceStore (bundled default) ─────────────────────────────────
|
|
7
|
+
/**
|
|
8
|
+
* Writes JSON records under `<rootDir>/<skill_name>/<trace_id>.json`.
|
|
9
|
+
* Zero external dependency; suitable for the standalone runtime out of
|
|
10
|
+
* the box. Operators with a substrate wired can swap for a
|
|
11
|
+
* substrate-backed store via config.
|
|
12
|
+
*/
|
|
13
|
+
export class FilesystemTraceStore {
|
|
14
|
+
rootDir;
|
|
15
|
+
constructor(rootDir) {
|
|
16
|
+
this.rootDir = rootDir;
|
|
17
|
+
}
|
|
18
|
+
async write(record) {
|
|
19
|
+
const dir = join(this.rootDir, sanitize(record.skill_name));
|
|
20
|
+
await mkdir(dir, { recursive: true });
|
|
21
|
+
const path = join(dir, `${record.trace_id}.json`);
|
|
22
|
+
await writeFile(path, JSON.stringify(record, null, 2), "utf8");
|
|
23
|
+
}
|
|
24
|
+
async query(filter) {
|
|
25
|
+
const results = [];
|
|
26
|
+
let skillDirs;
|
|
27
|
+
if (filter.skill_name !== undefined) {
|
|
28
|
+
skillDirs = [sanitize(filter.skill_name)];
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
try {
|
|
32
|
+
skillDirs = await readdir(this.rootDir);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
if (err.code === "ENOENT")
|
|
36
|
+
return [];
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
for (const skillDir of skillDirs) {
|
|
41
|
+
const full = join(this.rootDir, skillDir);
|
|
42
|
+
let entries;
|
|
43
|
+
try {
|
|
44
|
+
entries = await readdir(full);
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
if (err.code === "ENOENT")
|
|
48
|
+
continue;
|
|
49
|
+
throw err;
|
|
50
|
+
}
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (!entry.endsWith(".json"))
|
|
53
|
+
continue;
|
|
54
|
+
try {
|
|
55
|
+
const text = await readFile(join(full, entry), "utf8");
|
|
56
|
+
const rec = JSON.parse(text);
|
|
57
|
+
if (filter.since_ms !== undefined && rec.fired_at_ms < filter.since_ms)
|
|
58
|
+
continue;
|
|
59
|
+
if (filter.until_ms !== undefined && rec.fired_at_ms > filter.until_ms)
|
|
60
|
+
continue;
|
|
61
|
+
results.push(rec);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* unreadable / unparseable — skip */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
results.sort((a, b) => b.fired_at_ms - a.fired_at_ms);
|
|
69
|
+
if (filter.limit !== undefined)
|
|
70
|
+
return results.slice(0, filter.limit);
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
73
|
+
async get(traceId) {
|
|
74
|
+
let skillDirs;
|
|
75
|
+
try {
|
|
76
|
+
skillDirs = await readdir(this.rootDir);
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
if (err.code === "ENOENT")
|
|
80
|
+
return null;
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
for (const skillDir of skillDirs) {
|
|
84
|
+
const path = join(this.rootDir, skillDir, `${traceId}.json`);
|
|
85
|
+
try {
|
|
86
|
+
const text = await readFile(path, "utf8");
|
|
87
|
+
return JSON.parse(text);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
if (err.code === "ENOENT")
|
|
91
|
+
continue;
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
async prune(retentionMs) {
|
|
98
|
+
const cutoff = Date.now() - retentionMs;
|
|
99
|
+
let count = 0;
|
|
100
|
+
let skillDirs;
|
|
101
|
+
try {
|
|
102
|
+
skillDirs = await readdir(this.rootDir);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
if (err.code === "ENOENT")
|
|
106
|
+
return 0;
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
for (const skillDir of skillDirs) {
|
|
110
|
+
const full = join(this.rootDir, skillDir);
|
|
111
|
+
let entries;
|
|
112
|
+
try {
|
|
113
|
+
entries = await readdir(full);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
if (!entry.endsWith(".json"))
|
|
120
|
+
continue;
|
|
121
|
+
const path = join(full, entry);
|
|
122
|
+
try {
|
|
123
|
+
const text = await readFile(path, "utf8");
|
|
124
|
+
const rec = JSON.parse(text);
|
|
125
|
+
if (rec.fired_at_ms < cutoff) {
|
|
126
|
+
await unlink(path);
|
|
127
|
+
count++;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* skip unreadable */
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return count;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// ─── Sampling decision ─────────────────────────────────────────────────────
|
|
139
|
+
/**
|
|
140
|
+
* Deterministic sample decision per Open Q #2: SHA-256 of `trigger_id +
|
|
141
|
+
* ":" + skill_name`, take the first byte mod 100, fire trace if under
|
|
142
|
+
* `pct`. Same inputs always produce the same sampling decision — useful
|
|
143
|
+
* for reproducible testing + dashboard drill-down.
|
|
144
|
+
*/
|
|
145
|
+
export function shouldSample(triggerId, skillName, pct) {
|
|
146
|
+
if (pct >= 100)
|
|
147
|
+
return true;
|
|
148
|
+
if (pct <= 0)
|
|
149
|
+
return false;
|
|
150
|
+
const hash = createHash("sha256").update(`${triggerId}:${skillName}`).digest();
|
|
151
|
+
return hash[0] % 100 < pct;
|
|
152
|
+
}
|
|
153
|
+
// ─── TraceBuilder (mutable, used during execution) ──────────────────────────
|
|
154
|
+
/**
|
|
155
|
+
* Accumulator used by the runtime during a single fire. Records per-op
|
|
156
|
+
* timing + body; finalize() builds the immutable TraceRecord.
|
|
157
|
+
*/
|
|
158
|
+
export class TraceBuilder {
|
|
159
|
+
skill_name;
|
|
160
|
+
skill_version;
|
|
161
|
+
trigger;
|
|
162
|
+
identity;
|
|
163
|
+
ops = [];
|
|
164
|
+
firedAtMs;
|
|
165
|
+
trace_id;
|
|
166
|
+
constructor(skill_name, skill_version, trigger, identity) {
|
|
167
|
+
this.skill_name = skill_name;
|
|
168
|
+
this.skill_version = skill_version;
|
|
169
|
+
this.trigger = trigger;
|
|
170
|
+
this.identity = identity;
|
|
171
|
+
this.firedAtMs = trigger.fired_at_ms;
|
|
172
|
+
this.trace_id = randomUUID();
|
|
173
|
+
}
|
|
174
|
+
recordOp(record) {
|
|
175
|
+
this.ops.push(record);
|
|
176
|
+
}
|
|
177
|
+
finalize(emissions, outputs, errors) {
|
|
178
|
+
const completedAtMs = Date.now();
|
|
179
|
+
return {
|
|
180
|
+
version: 1,
|
|
181
|
+
trace_id: this.trace_id,
|
|
182
|
+
skill_name: this.skill_name,
|
|
183
|
+
skill_version: this.skill_version,
|
|
184
|
+
trigger: this.trigger,
|
|
185
|
+
identity: this.identity,
|
|
186
|
+
ops: this.ops,
|
|
187
|
+
emissions: [...emissions],
|
|
188
|
+
outputs: { ...outputs },
|
|
189
|
+
errors: [...errors],
|
|
190
|
+
fired_at_ms: this.firedAtMs,
|
|
191
|
+
completed_at_ms: completedAtMs,
|
|
192
|
+
duration_ms: completedAtMs - this.firedAtMs,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Decide whether a fire should be traced given config + trigger context.
|
|
198
|
+
* Used by the runtime / scheduler at fire start. `mode === "off"` always
|
|
199
|
+
* returns false (errors are still surfaced via `result.errors[]` — the
|
|
200
|
+
* NFR-11 floor); the trace store is bypassed entirely.
|
|
201
|
+
*/
|
|
202
|
+
export function shouldTraceFire(config, triggerId, skillName) {
|
|
203
|
+
if (config === undefined || config.mode === "off")
|
|
204
|
+
return false;
|
|
205
|
+
if (config.mode === "on")
|
|
206
|
+
return true;
|
|
207
|
+
const pct = config.samplePct ?? DEFAULT_SAMPLE_PCT;
|
|
208
|
+
return shouldSample(triggerId, skillName, pct);
|
|
209
|
+
}
|
|
210
|
+
export const TRACE_DEFAULTS = {
|
|
211
|
+
SAMPLE_PCT: DEFAULT_SAMPLE_PCT,
|
|
212
|
+
RETENTION_MS: DEFAULT_RETENTION_MS,
|
|
213
|
+
};
|
|
214
|
+
// ─── Internals ──────────────────────────────────────────────────────────────
|
|
215
|
+
function sanitize(name) {
|
|
216
|
+
// Filesystem-safe per the FilesystemSkillStore convention. Same charset
|
|
217
|
+
// — alphanumeric, hyphen, underscore, dot.
|
|
218
|
+
return name.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
219
|
+
}
|
|
220
|
+
/** Convenience for tests: derive the on-disk path FilesystemTraceStore writes to. */
|
|
221
|
+
export function _traceFilePathFor(rootDir, record) {
|
|
222
|
+
void basename;
|
|
223
|
+
void dirname;
|
|
224
|
+
return join(rootDir, sanitize(record.skill_name), `${record.trace_id}.json`);
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=trace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace.js","sourceRoot":"","sources":["../src/trace.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
Curated production-quality skills demonstrating distinct language patterns. Each compiles + lints clean and can be run directly via `skillfile run examples/<name>.skill.md` (some require connectors — see the per-skill notes below).
|
|
4
|
+
|
|
5
|
+
For full language semantics, see [`../docs/language-reference.md`](../docs/language-reference.md).
|
|
6
|
+
|
|
7
|
+
| Example | Patterns demonstrated |
|
|
8
|
+
|---|---|
|
|
9
|
+
| [`hello.skill.md`](./hello.skill.md) | Three-command first run; `! emit`; default target; `# Vars:` declared inputs |
|
|
10
|
+
| [`morning-brief.skill.md`](./morning-brief.skill.md) | Multi-target with `needs:`; cron-fired with `EVENT.fired_at_*`; `# OnError:` fallback; dual `# Output:` (slack + prompt-context); `# Requires:` user-var cascade with fallback; LocalModel + retrieval composition |
|
|
11
|
+
| [`doc-qa-with-citations.skill.md`](./doc-qa-with-citations.skill.md) | Single-target retrieval; `(fallback: [])` op-level fallback; LLM-with-citation pattern; pipe filter `\|json` for prompt embedding |
|
|
12
|
+
| [`classify-support-ticket.skill.md`](./classify-support-ticket.skill.md) | `if`/`elif`/`else` multi-branch routing; classifier-cascade pattern; MemoryStore `$` writes with structured `domain_tags`; `$set` literal binding |
|
|
13
|
+
| [`cut-release-tag.skill.md`](./cut-release-tag.skill.md) | `??` interactive ask-for-input; `else:` short-circuit on multiple targets; `@` shell ops with per-op error fallback; rollback-on-failure flow |
|
|
14
|
+
| [`service-health-watch.skill.md`](./service-health-watch.skill.md) | `foreach` iteration; cron-fired with `expires_at=$(EVENT.fired_at_plus_1d_unix)` TTL math; classifier-gated MemoryStore writes; pipe filter `\|url` for path-safe interpolation |
|
|
15
|
+
| [`feedback-sentiment-scan.skill.md`](./feedback-sentiment-scan.skill.md) | `in` / `not in` set-membership ops; dedupe-via-seen-markers idiom; nested `if`/`elif` classification cascade; TTL on bookkeeping writes |
|
|
16
|
+
|
|
17
|
+
## Connector requirements
|
|
18
|
+
|
|
19
|
+
Most examples assume the bundled-default connectors:
|
|
20
|
+
|
|
21
|
+
- `hello.skill.md` — zero deps (runs cold)
|
|
22
|
+
- `doc-qa-with-citations.skill.md` — Ollama (LocalModel) + MemoryStore for the retrieval cache
|
|
23
|
+
- `morning-brief.skill.md`, `feedback-sentiment-scan.skill.md` — Ollama + MemoryStore + a calendar MCP connector (the `$ calendar.list_events` op)
|
|
24
|
+
- `classify-support-ticket.skill.md` — Ollama + MemoryStore
|
|
25
|
+
- `cut-release-tag.skill.md` — git CLI on `PATH`; expects to run inside a git repo
|
|
26
|
+
- `service-health-watch.skill.md` — `curl` on `PATH`; network egress to `status.internal/*`
|
|
27
|
+
|
|
28
|
+
Skills that talk to MCP tools (like `calendar.list_events`) need that tool wired into the `Registry` via `registerMcpConnector()`. The `CallbackMcpConnector` in `skillscript-runtime/connectors` lets you stub one inline for development.
|
|
29
|
+
|
|
30
|
+
## Running an example
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
# Cold install — no Ollama, no MCP, no external state
|
|
34
|
+
skillfile run examples/hello.skill.md
|
|
35
|
+
|
|
36
|
+
# With overrides
|
|
37
|
+
skillfile run examples/hello.skill.md --input WHO=Scott
|
|
38
|
+
|
|
39
|
+
# Preview the compiled artifact without execution
|
|
40
|
+
skillfile compile examples/hello.skill.md
|
|
41
|
+
|
|
42
|
+
# Mechanical preview — $/~/> ops short-circuit (useful for examining flow
|
|
43
|
+
# without firing real LLM calls)
|
|
44
|
+
skillfile run examples/morning-brief.skill.md --mechanical
|
|
45
|
+
|
|
46
|
+
# Render the control-flow graph
|
|
47
|
+
skillfile diagram examples/cut-release-tag.skill.md
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Programmatic example
|
|
51
|
+
|
|
52
|
+
`programmatic-trace-demo.mjs` shows the library API path — wiring a `FilesystemSkillStore` + `FilesystemTraceStore` + `Scheduler`, dispatching a skill, and reading back the recorded trace + per-skill health metrics.
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
node examples/programmatic-trace-demo.mjs
|
|
56
|
+
```
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Skill: classify-support-ticket
|
|
2
|
+
# Status: Approved
|
|
3
|
+
# Description: Read an incoming support ticket and route it: severity-1 tickets get paged to ops-channel, billing tickets get tagged for finance review, everything else gets a draft reply queued for human review
|
|
4
|
+
# Vars: TICKET_TEXT, TICKET_ID
|
|
5
|
+
# Output: slack: ops-pages
|
|
6
|
+
|
|
7
|
+
classify:
|
|
8
|
+
~ prompt="Categorize this support ticket. Reply with EXACTLY one of: 'sev-1', 'billing', 'general'. No other text.\n\nTicket: $(TICKET_TEXT)" model=qwen maxTokens=10 -> CATEGORY
|
|
9
|
+
|
|
10
|
+
severity_check:
|
|
11
|
+
needs: classify
|
|
12
|
+
~ prompt="Does this support ticket describe a service outage, data loss, security incident, or other production-severity-1 issue? Reply ONLY 'yes' or 'no'.\n\nTicket: $(TICKET_TEXT)" model=qwen maxTokens=10 -> IS_SEV1
|
|
13
|
+
|
|
14
|
+
route:
|
|
15
|
+
needs: severity_check
|
|
16
|
+
if $(CATEGORY|trim) == "sev-1":
|
|
17
|
+
! PAGE: sev-1 ticket $(TICKET_ID) - $(TICKET_TEXT)
|
|
18
|
+
$ memorystore.write summary="sev-1 ticket $(TICKET_ID)" detail="$(TICKET_TEXT)" knowledge_type=hard_won vault=private domain_tags=["support","sev-1","page"]
|
|
19
|
+
elif $(IS_SEV1|trim) == "yes":
|
|
20
|
+
! PAGE: classifier said $(CATEGORY|trim) but severity-check flagged this as sev-1: $(TICKET_ID)
|
|
21
|
+
$ memorystore.write summary="sev-1 escalation $(TICKET_ID)" detail="category=$(CATEGORY|trim) but severity-check=yes" knowledge_type=hard_won vault=private domain_tags=["support","sev-1","disagreement"]
|
|
22
|
+
elif $(CATEGORY|trim) == "billing":
|
|
23
|
+
$set TAG_FOR = "finance"
|
|
24
|
+
! Tagged for $(TAG_FOR) review: $(TICKET_ID)
|
|
25
|
+
$ memorystore.write summary="billing ticket $(TICKET_ID)" detail="$(TICKET_TEXT)" knowledge_type=common vault=private domain_tags=["support","billing"]
|
|
26
|
+
else:
|
|
27
|
+
~ prompt="Draft a polite acknowledgment reply for this support ticket. Two short sentences. No greeting, no sign-off.\n\n$(TICKET_TEXT)" model=qwen maxTokens=150 -> DRAFT
|
|
28
|
+
$ memorystore.write summary="support draft $(TICKET_ID)" detail="$(DRAFT|trim)" knowledge_type=common vault=private domain_tags=["support","draft"]
|
|
29
|
+
|
|
30
|
+
default: route
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Skill: cut-release-tag
|
|
2
|
+
# Status: Approved
|
|
3
|
+
# Description: Run when the user says "ship a release" or "cut a tag" on the current branch; collects commit-list since last tag, asks the user to confirm the version bump, applies the tag, and pushes it
|
|
4
|
+
# Vars: BUMP_KIND=patch
|
|
5
|
+
# Output: text
|
|
6
|
+
|
|
7
|
+
last_tag:
|
|
8
|
+
@ git describe --tags --abbrev=0 -> PREV_TAG
|
|
9
|
+
else:
|
|
10
|
+
$set PREV_TAG = "(no prior tags)"
|
|
11
|
+
! no previous tag found; treating this as the first release
|
|
12
|
+
|
|
13
|
+
commits_since:
|
|
14
|
+
needs: last_tag
|
|
15
|
+
@ git log --oneline $(PREV_TAG)..HEAD -> COMMITS
|
|
16
|
+
else:
|
|
17
|
+
$set COMMITS = "(unable to read commit log; proceeding without preview)"
|
|
18
|
+
|
|
19
|
+
propose:
|
|
20
|
+
needs: commits_since
|
|
21
|
+
~ prompt="Propose the next semver tag given prev tag '$(PREV_TAG)', bump kind '$(BUMP_KIND)', and commits below. Return ONLY the new tag string (e.g. v1.4.3). No commentary.\n\nCommits:\n$(COMMITS)" model=qwen maxTokens=30 -> PROPOSED_TAG
|
|
22
|
+
|
|
23
|
+
confirm:
|
|
24
|
+
needs: propose
|
|
25
|
+
! Previous tag: $(PREV_TAG)
|
|
26
|
+
! Commits since:
|
|
27
|
+
! $(COMMITS)
|
|
28
|
+
! Proposed new tag: $(PROPOSED_TAG|trim)
|
|
29
|
+
?? "Cut tag $(PROPOSED_TAG|trim)?" -> APPROVED
|
|
30
|
+
|
|
31
|
+
apply:
|
|
32
|
+
needs: confirm
|
|
33
|
+
@ git tag $(PROPOSED_TAG|trim) -> TAG_OUT
|
|
34
|
+
@ git push origin $(PROPOSED_TAG|trim) -> PUSH_OUT
|
|
35
|
+
! Tagged and pushed $(PROPOSED_TAG|trim)
|
|
36
|
+
else:
|
|
37
|
+
! Tag/push failed; rolling back local tag
|
|
38
|
+
@ git tag -d $(PROPOSED_TAG|trim) -> ROLLBACK
|
|
39
|
+
|
|
40
|
+
default: apply
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Skill: doc-qa-with-citations
|
|
2
|
+
# Status: Approved
|
|
3
|
+
# Description: When the user asks a question that requires retrieval over the doc set, answer with inline citations to memory IDs
|
|
4
|
+
# Vars: QUESTION, K=6
|
|
5
|
+
# Output: text
|
|
6
|
+
|
|
7
|
+
answer:
|
|
8
|
+
> mode=rerank query="$(QUESTION)" limit=$(K) -> HITS (fallback: [])
|
|
9
|
+
~ prompt="Answer the question using ONLY the supplied passages. Cite each claim inline as [id:<memory-id>]. Question: $(QUESTION). Passages: $(HITS|json)" maxTokens=900 -> RESPONSE
|
|
10
|
+
! $(RESPONSE)
|
|
11
|
+
|
|
12
|
+
default: answer
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Skill: feedback-sentiment-scan
|
|
2
|
+
# Status: Approved
|
|
3
|
+
# Description: Each night, scan the previous 24h of customer feedback records, classify sentiment via local model, surface entries where sentiment is "frustrated" or "blocking" so the team sees them at start-of-day; skip entries already seen on prior nights
|
|
4
|
+
# Triggers: cron: 0 3 * * *
|
|
5
|
+
# Vars: SCAN_LIMIT=50
|
|
6
|
+
# Output: slack: customer-pulse
|
|
7
|
+
# Output: prompt-context: support-lead
|
|
8
|
+
|
|
9
|
+
fetch_new:
|
|
10
|
+
> mode=fts query="customer feedback" limit=$(SCAN_LIMIT) created_after=$(EVENT.fired_at_unix) -> FEEDBACK
|
|
11
|
+
|
|
12
|
+
fetch_seen:
|
|
13
|
+
> mode=fts query="sentiment-scan seen marker" limit=200 domain_tags=["sentiment-scan-seen"] -> SEEN_MARKERS
|
|
14
|
+
|
|
15
|
+
classify_and_emit:
|
|
16
|
+
needs: fetch_new, fetch_seen
|
|
17
|
+
! Sentiment scan results for $(NOW):
|
|
18
|
+
foreach F in $(FEEDBACK):
|
|
19
|
+
if $(F.id|trim) in $(SEEN_MARKERS):
|
|
20
|
+
! - skipped (already classified): $(F.id|trim)
|
|
21
|
+
elif $(F.id|trim) not in $(SEEN_MARKERS):
|
|
22
|
+
~ prompt="Classify the sentiment of this customer feedback. Respond with ONE word: 'frustrated', 'blocking', 'satisfied', 'neutral'. No explanation.\n\nFeedback: $(F.summary)\nDetail: $(F.detail)" model=gemma2 maxTokens=10 -> VERDICT
|
|
23
|
+
if $(VERDICT|trim) == "frustrated":
|
|
24
|
+
! - FRUSTRATED [$(F.id|trim)] $(F.summary)
|
|
25
|
+
elif $(VERDICT|trim) == "blocking":
|
|
26
|
+
! - BLOCKING [$(F.id|trim)] $(F.summary)
|
|
27
|
+
$ memorystore.write summary="sentiment-scan seen $(F.id|trim)" detail="verdict=$(VERDICT|trim) on $(EVENT.fired_at_unix)" knowledge_type=common vault=private domain_tags=["sentiment-scan-seen"] expires_at=$(EVENT.fired_at_plus_7d_unix)
|
|
28
|
+
|
|
29
|
+
default: classify_and_emit
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Skill: morning-brief
|
|
2
|
+
# Status: Approved
|
|
3
|
+
# Description: Compose a daily morning brief from calendar, mailbox, and overnight memory writes when the cron trigger fires at 7am.
|
|
4
|
+
# Vars: AGENT, BRIEF_HORIZON_HOURS=24
|
|
5
|
+
# Requires: user-var:morning-brief-channel -> CHANNEL (fallback: dev-null)
|
|
6
|
+
# Triggers: cron: 0 7 * * *
|
|
7
|
+
# OnError: morning-brief-degraded
|
|
8
|
+
# Output: slack: $(CHANNEL)
|
|
9
|
+
# Output: prompt-context: perry
|
|
10
|
+
|
|
11
|
+
calendar:
|
|
12
|
+
$ calendar.list_events horizon_hours=$(BRIEF_HORIZON_HOURS) -> EVENTS
|
|
13
|
+
|
|
14
|
+
mailbox:
|
|
15
|
+
> mode=fts query="addressed:$(AGENT) created_after:$(EVENT.fired_at_unix)" limit=10 -> MAIL
|
|
16
|
+
|
|
17
|
+
overnight:
|
|
18
|
+
> mode=rerank query="overnight writes since:$(EVENT.fired_at_plus_1d_unix)" limit=15 -> NOTES
|
|
19
|
+
|
|
20
|
+
compose: needs: calendar, mailbox, overnight
|
|
21
|
+
~ prompt="Compose a concise morning brief. Calendar: $(EVENTS|json). Mailbox: $(MAIL|json). Overnight notes: $(NOTES|json). Three sections, six bullets max each." model=qwen maxTokens=1200 -> BRIEF
|
|
22
|
+
! $(BRIEF)
|
|
23
|
+
|
|
24
|
+
default: compose
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Programmatic Scheduler demo — exercises the library API surface that
|
|
3
|
+
// embedders (not just CLI users) would use. Wires:
|
|
4
|
+
// - FilesystemSkillStore for skill sources
|
|
5
|
+
// - FilesystemTraceStore for dispatch traces
|
|
6
|
+
// - Scheduler with trace recording enabled
|
|
7
|
+
// Then registers a cron trigger, dispatches a few times, and queries
|
|
8
|
+
// traces + metrics back. ~60 lines of operator-shaped code.
|
|
9
|
+
//
|
|
10
|
+
// Run: node examples/programmatic-trace-demo.mjs
|
|
11
|
+
// Uses: /tmp/skillscript-prog-demo as a sandbox.
|
|
12
|
+
|
|
13
|
+
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import {
|
|
16
|
+
Scheduler,
|
|
17
|
+
FilesystemSkillStore,
|
|
18
|
+
FilesystemTraceStore,
|
|
19
|
+
Registry,
|
|
20
|
+
healthMetrics,
|
|
21
|
+
} from "../dist/index.js";
|
|
22
|
+
|
|
23
|
+
const HOME = "/tmp/skillscript-prog-demo";
|
|
24
|
+
rmSync(HOME, { recursive: true, force: true });
|
|
25
|
+
mkdirSync(join(HOME, "skills"), { recursive: true });
|
|
26
|
+
mkdirSync(join(HOME, "traces"), { recursive: true });
|
|
27
|
+
|
|
28
|
+
const skillStore = new FilesystemSkillStore(join(HOME, "skills"));
|
|
29
|
+
const traceStore = new FilesystemTraceStore(join(HOME, "traces"));
|
|
30
|
+
|
|
31
|
+
const SKILL_SRC = `# Skill: heartbeat
|
|
32
|
+
# Description: Per-fire heartbeat exercising the observability surface.
|
|
33
|
+
# Status: Approved
|
|
34
|
+
# Triggers: cron: */1 * * * *
|
|
35
|
+
|
|
36
|
+
emit:
|
|
37
|
+
! heartbeat at $(EVENT.fired_at_unix)
|
|
38
|
+
|
|
39
|
+
default: emit
|
|
40
|
+
`;
|
|
41
|
+
await skillStore.store("heartbeat", SKILL_SRC);
|
|
42
|
+
console.log(`stored skill at ${HOME}/skills/heartbeat.skill.md\n`);
|
|
43
|
+
|
|
44
|
+
const sched = new Scheduler({
|
|
45
|
+
registry: new Registry(),
|
|
46
|
+
skillStore,
|
|
47
|
+
traceStore,
|
|
48
|
+
trace: { mode: "on" },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
sched.registerTrigger({
|
|
52
|
+
skillName: "heartbeat",
|
|
53
|
+
source: "cron",
|
|
54
|
+
name: "*/1 * * * *",
|
|
55
|
+
declarative: true,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Fire 5 times via direct dispatch (the scheduler would also fire these
|
|
59
|
+
// via the poll loop; direct dispatch is faster for the demo).
|
|
60
|
+
console.log("dispatching 5 fires...");
|
|
61
|
+
for (let i = 0; i < 5; i++) {
|
|
62
|
+
const firedAtMs = Date.now();
|
|
63
|
+
const result = await sched.dispatchSkill("heartbeat", undefined, {
|
|
64
|
+
source: "cron",
|
|
65
|
+
name: "*/1 * * * *",
|
|
66
|
+
fired_at_ms: firedAtMs,
|
|
67
|
+
trigger_id: `demo-${i}`,
|
|
68
|
+
});
|
|
69
|
+
const status = result.errors.length === 0 ? "ok" : `err:${result.errors[0].class}`;
|
|
70
|
+
console.log(` fire ${i + 1}: ${status} (${result.emissions[0]})`);
|
|
71
|
+
// Brief sleep so fired_at_ms timestamps don't collide.
|
|
72
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log("\nquerying traces back:");
|
|
76
|
+
const traces = await traceStore.query({ skill_name: "heartbeat", limit: 10 });
|
|
77
|
+
for (const t of traces) {
|
|
78
|
+
const ts = new Date(t.fired_at_ms).toISOString();
|
|
79
|
+
console.log(` ${ts} ${t.trace_id} ops=${t.ops.length} errors=${t.errors.length}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log("\nhealth metrics:");
|
|
83
|
+
const metrics = await healthMetrics(traceStore, { since_ms: Date.now() - 60_000 });
|
|
84
|
+
console.log(` total fires: ${metrics.totalFires}`);
|
|
85
|
+
for (const [name, m] of Object.entries(metrics.perSkill)) {
|
|
86
|
+
console.log(` ${name}: ${m.fireCount} fires, successRate=${(m.successRate * 100).toFixed(0)}%`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(`\ntrace files on disk: ${HOME}/traces/heartbeat/`);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Skill: service-health-watch
|
|
2
|
+
# Status: Approved
|
|
3
|
+
# Description: Every 5 minutes check named service endpoints — if latency or status degrades, write a signal memory and alert
|
|
4
|
+
# Vars: SERVICES=[auth-api, ledger-api, search-api], LATENCY_BUDGET_MS=400
|
|
5
|
+
# Triggers: cron: */5 * * * *
|
|
6
|
+
# Output: none
|
|
7
|
+
|
|
8
|
+
probe:
|
|
9
|
+
foreach SVC in $(SERVICES):
|
|
10
|
+
@ curl -s -o /dev/null -w "%{http_code} %{time_total}" https://status.internal/$(SVC|url) -> RAW
|
|
11
|
+
~ prompt="From the line '$(RAW|trim)' (http_code time_seconds), and budget $(LATENCY_BUDGET_MS) ms, answer ok or degraded only." -> STATUS
|
|
12
|
+
if $(STATUS|trim) == "degraded":
|
|
13
|
+
$ memorystore.write summary="service degradation: $(SVC)" detail="probe at $(NOW): $(RAW|trim)" domain_tags=[ops, service-health, degraded:$(SVC)] vault=private knowledge_type=common expires_at=$(EVENT.fired_at_plus_1d_unix) -> ACK
|
|
14
|
+
! $(SVC) degraded — wrote signal
|
|
15
|
+
else:
|
|
16
|
+
! $(SVC) ok
|
|
17
|
+
|
|
18
|
+
default: probe
|
package/package.json
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "skillscript-runtime",
|
|
3
|
+
"version": "0.2.4",
|
|
4
|
+
"description": "Runtime, compiler, lint, CLI, and dashboard for Skillscript — a small declarative language for authoring agent workflows.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Scott Shwarts <scotts@pobox.com>",
|
|
7
|
+
"homepage": "https://github.com/sshwarts/skillscript-runtime",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/sshwarts/skillscript-runtime.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/sshwarts/skillscript-runtime/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./connectors": {
|
|
24
|
+
"types": "./dist/connectors/index.d.ts",
|
|
25
|
+
"import": "./dist/connectors/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./errors": {
|
|
28
|
+
"types": "./dist/errors.d.ts",
|
|
29
|
+
"import": "./dist/errors.js"
|
|
30
|
+
},
|
|
31
|
+
"./runtime": {
|
|
32
|
+
"types": "./dist/runtime.d.ts",
|
|
33
|
+
"import": "./dist/runtime.js"
|
|
34
|
+
},
|
|
35
|
+
"./trace": {
|
|
36
|
+
"types": "./dist/trace.d.ts",
|
|
37
|
+
"import": "./dist/trace.js"
|
|
38
|
+
},
|
|
39
|
+
"./metrics": {
|
|
40
|
+
"types": "./dist/metrics.d.ts",
|
|
41
|
+
"import": "./dist/metrics.js"
|
|
42
|
+
},
|
|
43
|
+
"./scheduler": {
|
|
44
|
+
"types": "./dist/scheduler.d.ts",
|
|
45
|
+
"import": "./dist/scheduler.js"
|
|
46
|
+
},
|
|
47
|
+
"./mcp-server": {
|
|
48
|
+
"types": "./dist/mcp-server.d.ts",
|
|
49
|
+
"import": "./dist/mcp-server.js"
|
|
50
|
+
},
|
|
51
|
+
"./testing": {
|
|
52
|
+
"types": "./dist/testing/index.d.ts",
|
|
53
|
+
"import": "./dist/testing/index.js"
|
|
54
|
+
},
|
|
55
|
+
"./package.json": "./package.json"
|
|
56
|
+
},
|
|
57
|
+
"bin": {
|
|
58
|
+
"skillfile": "./dist/cli.js"
|
|
59
|
+
},
|
|
60
|
+
"files": [
|
|
61
|
+
"dist",
|
|
62
|
+
"scaffold",
|
|
63
|
+
"examples",
|
|
64
|
+
"ARCHITECTURE.md",
|
|
65
|
+
"README.md",
|
|
66
|
+
"LICENSE"
|
|
67
|
+
],
|
|
68
|
+
"engines": {
|
|
69
|
+
"node": ">=22.5"
|
|
70
|
+
},
|
|
71
|
+
"keywords": [
|
|
72
|
+
"skillscript",
|
|
73
|
+
"agent",
|
|
74
|
+
"agent-workflow",
|
|
75
|
+
"dsl",
|
|
76
|
+
"declarative",
|
|
77
|
+
"workflow",
|
|
78
|
+
"llm",
|
|
79
|
+
"mcp",
|
|
80
|
+
"model-context-protocol"
|
|
81
|
+
],
|
|
82
|
+
"dependencies": {
|
|
83
|
+
"cron-parser": "^4.9.0"
|
|
84
|
+
},
|
|
85
|
+
"devDependencies": {
|
|
86
|
+
"@types/node": "^22.10.0",
|
|
87
|
+
"tsx": "^4.19.0",
|
|
88
|
+
"typescript": "^5.7.0",
|
|
89
|
+
"vitest": "^2.1.0"
|
|
90
|
+
},
|
|
91
|
+
"scripts": {
|
|
92
|
+
"build": "tsc -p tsconfig.build.json && node scripts/copy-dashboard-assets.mjs",
|
|
93
|
+
"dev": "tsc -p tsconfig.build.json --watch",
|
|
94
|
+
"test": "vitest run",
|
|
95
|
+
"test:watch": "vitest",
|
|
96
|
+
"typecheck": "tsc --noEmit",
|
|
97
|
+
"loc-check": "node scripts/loc-ceiling.mjs",
|
|
98
|
+
"lint": "tsc --noEmit && vitest run"
|
|
99
|
+
}
|
|
100
|
+
}
|