@skill-map/spec 0.2.0 → 0.3.0

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/cli-contract.md CHANGED
@@ -25,6 +25,7 @@ These flags apply to every verb unless marked otherwise.
25
25
  | `--no-color` | boolean | Disable ANSI color codes. Implementations MUST also auto-disable color when stdout is not a TTY. |
26
26
  | `-h` / `--help` | boolean | Print verb-specific or top-level help, exit 0. |
27
27
  | `--db <path>` | string | Override the database file location (escape hatch; primarily for debugging). |
28
+ | `--all` | boolean | Universal fan-out. Any verb that accepts a target identifier (node path, job id, plugin id, etc.) MUST accept `--all` as "apply to every eligible target matching the verb's preconditions". Mutually exclusive with a positional target or `-n <path>` on the same invocation. Verbs that inherently target everything (`sm scan` without `-n`, `sm list`, `sm check`, `sm doctor`) accept the flag for script-composition uniformity but treat it as a no-op. Verbs where fan-out is nonsensical (`sm record`, `sm init`, `sm version`, `sm help`, `sm config get/set/reset/show`, `sm db *`, `sm serve`) MUST reject `--all` with exit `2`. |
28
29
 
29
30
  Env-var equivalents are normative:
30
31
 
@@ -169,7 +170,8 @@ Exit: 0 on clean, 1 if error-severity issues exist, 2 on operational error.
169
170
  | `sm graph [--format ascii\|mermaid\|dot]` | Render the full graph via the named renderer. |
170
171
  | `sm export <query> --format json\|md\|mermaid` | Filtered export. Query syntax is implementation-defined pre-1.0. |
171
172
  | `sm orphans` | History rows whose target node is missing. |
172
- | `sm orphans reconcile <orphan.path> --to <new.path>` | Migrate history rows from the old path to the new one after a rename. |
173
+ | `sm orphans reconcile <orphan.path> --to <new.path>` | Migrate history rows from the old path to the new one after a rename. Use case: the scan's rename heuristic missed a match (semantic-only rename, body rewrite) and the user wants to stitch history manually. |
174
+ | `sm orphans undo-rename <new.path> [--from <old.path>] [--force]` | Reverse a medium- or ambiguous-confidence auto-rename. Requires an active `auto-rename-medium` or `auto-rename-ambiguous` issue on `<new.path>`. For `auto-rename-medium`, omit `--from` — the previous path is read from `issue.data_json`. For `auto-rename-ambiguous`, `--from <old.path>` is REQUIRED to pick one of the candidates listed in `data_json.candidates`. Migrates `state_*` FKs back and resolves the issue; the previous path becomes an `orphan` (its file no longer exists in FS). Destructive; prompts for confirmation unless `--force`. Exit `5` if no active auto-rename issue targets `<new.path>`, or if `--from` references a path not in `data_json.candidates`. |
173
175
 
174
176
  ---
175
177
 
@@ -195,15 +197,16 @@ See `job-lifecycle.md` for the state machine; this table is the CLI surface.
195
197
  | `sm job submit <action> --all` | Fan out to every node matching the action's preconditions. |
196
198
  | `sm job submit ... --force` | Bypass duplicate detection. |
197
199
  | `sm job submit ... --ttl <seconds>` | Override computed TTL. |
200
+ | `sm job submit ... --priority <n>` | Override job priority. Integer; higher runs first. Default `0`. Negative allowed (deprioritize). Frozen on `state_jobs.priority` at submit time. |
198
201
  | `sm job list [--status ...] [--action ...] [--node ...]` | List jobs. |
199
202
  | `sm job show <job.id>` | Detail: current state, claim timestamp, TTL remaining, runner, content hash. |
200
203
  | `sm job preview <job.id>` | Render the job MD file without executing. |
201
204
  | `sm job claim [--filter <action>]` | Atomic primitive: return next queued job id, mark it running. Exit 0 with id on stdout; exit 1 if queue empty. |
202
205
  | `sm job run` | Full CLI-runner loop: claim + spawn + record. Runs one job. |
203
- | `sm job run --all` | Drain the queue (MVP: sequential). |
206
+ | `sm job run --all` | Drain the queue (sequential through `v1.0`; in-runner parallelism deferred). |
204
207
  | `sm job run --max N` | Drain at most N jobs. |
205
208
  | `sm job status [<job.id>]` | Counts (per status) or single-job status. |
206
- | `sm job cancel <job.id>` | Force a running job to `failed` state with reason `user-cancelled`. |
209
+ | `sm job cancel <job.id> \| --all` | Force a running job to `failed` state with reason `user-cancelled`. `--all` cancels every `queued` and `running` job. |
207
210
  | `sm job prune` | Retention GC for completed/failed jobs (per config policy). |
208
211
  | `sm job prune --orphan-files` | Remove MD files with no matching DB row. |
209
212
 
@@ -238,8 +241,8 @@ Authentication: the nonce is the sole credential. An implementation MUST reject
238
241
 
239
242
  | Command | Purpose |
240
243
  |---|---|
241
- | `sm history [-n <node.path>] [--action <id>] [--status ...] [--since <date>]` | Filter execution records. `--json` emits an array of `execution-record.schema.json` objects. |
242
- | `sm history stats` | Aggregates: tokens per action, executions per month, top nodes by frequency, error rates. |
244
+ | `sm history [-n <node.path>] [--action <id>] [--status ...] [--since <date>] [--until <date>]` | Filter execution records. `--json` emits an array of `execution-record.schema.json` objects. |
245
+ | `sm history stats [--since <date>] [--until <date>] [--period day\|week\|month] [--top N]` | Aggregates over `state_executions` in the window. `--json` emits a document conforming to `history-stats.schema.json`: totals, tokens per action, executions per period (granularity from `--period`, default `month`), top N nodes by frequency (default 10), error rates (global + per-action + per failure reason). |
243
246
 
244
247
  ---
245
248
 
@@ -249,8 +252,8 @@ Authentication: the nonce is the sole credential. An implementation MUST reject
249
252
  |---|---|
250
253
  | `sm plugins list` | Auto-discovered plugins with status. `--json` emits an array of `DiscoveredPlugin`. |
251
254
  | `sm plugins show <id>` | Full manifest + compat detail. |
252
- | `sm plugins enable <id>` | Toggle on. Persists in `config_plugins`. |
253
- | `sm plugins disable <id>` | Toggle off; does not delete the plugin directory. |
255
+ | `sm plugins enable <id> \| --all` | Toggle on. Persists in `config_plugins`. `--all` applies to every discovered plugin. |
256
+ | `sm plugins disable <id> \| --all` | Toggle off; does not delete the plugin directory. `--all` applies to every discovered plugin. |
254
257
  | `sm plugins doctor` | Revalidate all plugins against current spec version; update `status` fields. |
255
258
 
256
259
  ---
@@ -272,15 +275,16 @@ See `db-schema.md` for the table catalog.
272
275
 
273
276
  | Command | Purpose |
274
277
  |---|---|
275
- | `sm db reset` | Drop `scan_*` + `state_*`, keep `config_*`. |
276
- | `sm db reset --hard` | Delete the DB file entirely. |
278
+ | `sm db reset` | Drop `scan_*` only. Keep `state_*` and `config_*`. Non-destructive — no confirmation required. |
279
+ | `sm db reset --state` | Drop `scan_*` AND `state_*` (including `state_plugin_kvs` and every `plugin_<id>_*` table). Keep `config_*`. Destructive. |
280
+ | `sm db reset --hard` | Delete the DB file entirely. Keep the plugins folder so the next boot re-discovers them. Destructive. |
277
281
  | `sm db backup [--out <path>]` | WAL checkpoint + file copy. |
278
282
  | `sm db restore <path>` | Swap the DB. |
279
283
  | `sm db shell` | Interactive SQL shell (implementations backed by SQLite use `sqlite3`; others use equivalent). |
280
284
  | `sm db dump [--tables ...]` | SQL dump. |
281
285
  | `sm db migrate [--dry-run \| --status \| --to <n> \| --kernel-only \| --plugin <id> \| --no-backup]` | Migration controls. |
282
286
 
283
- All destructive verbs (`reset`, `reset --hard`, `restore`) require interactive confirmation unless `--force`.
287
+ Destructive verbs (`reset --state`, `reset --hard`, `restore`) require interactive confirmation unless `--yes` (non-interactive mode for scripts) or `--force` (alias, kept for backward compatibility) is passed. `sm db reset` without a modifier is non-destructive and never prompts.
284
288
 
285
289
  ---
286
290
 
@@ -325,6 +329,46 @@ When `--json` is set:
325
329
 
326
330
  ---
327
331
 
332
+ ## Elapsed time
333
+
334
+ Every verb that does non-trivial work MUST report its own wall-clock duration. Coverage is broad on purpose — operators and agents need to notice regressions without instrumenting the host.
335
+
336
+ ### Scope
337
+
338
+ **In scope**: any verb that walks the filesystem, hits the DB, spawns a subprocess, or renders a report. Examples: `sm scan`, `sm check`, `sm list`, `sm show`, `sm findings`, `sm history`, `sm history stats`, `sm graph`, `sm export`, `sm audit run`, `sm job submit`, `sm job run`, `sm job claim`, `sm job preview`, `sm record`, `sm doctor`, `sm db backup`, `sm db restore`, `sm db dump`, `sm db migrate`, `sm plugins list`, `sm plugins doctor`, `sm init`.
339
+
340
+ **Exempt**: informational verbs that return in well under a millisecond and would clutter the output — `sm --version`, `sm --help`, `sm version`, `sm help`, `sm config get`, `sm config list`, `sm config show`.
341
+
342
+ ### Pretty output (TTY)
343
+
344
+ The last line written to stderr MUST be `done in <formatted>` where `<formatted>` is:
345
+
346
+ - `< 1000ms` → `<N>ms` (integer, no decimals).
347
+ - `≥ 1s` and `< 60s` → `<N.N>s` (one decimal).
348
+ - `≥ 60s` → `<M>m <S>s` (integer minutes + integer seconds).
349
+
350
+ Examples: `done in 34ms`, `done in 2.4s`, `done in 1m 42s`.
351
+
352
+ The line is suppressed by `--quiet`. It goes to stderr so it never pollutes stdout, including in `--json` mode.
353
+
354
+ ### JSON output (`--json`)
355
+
356
+ When the verb's `--json` output is a top-level **object**, the schema includes an `elapsedMs` top-level field (integer, milliseconds). Stdout then carries the timing inside the document. Stderr still emits the `done in …` line unless `--quiet`.
357
+
358
+ When the verb's `--json` output is a top-level **array** or an **ndjson stream**, the schema does NOT include `elapsedMs` (there is no object to attach it to). Stderr is the sole carrier of the timing line.
359
+
360
+ Schemas that already express the command's wall-clock under a nested field (e.g. `scan-result.schema.json` → `stats.durationMs`) MUST treat that field as the elapsed time of the scan command itself. Adding a top-level `elapsedMs` to those schemas for redundancy is a minor bump and MAY happen later for consistency; until then, consumers read the nested field.
361
+
362
+ ### Implementations
363
+
364
+ Implementations MUST measure from the moment the verb starts its own work (after Clipanion / arg-parsing overhead) to the moment before writing the terminal output. Sub-millisecond verbs exempt per §Scope MAY skip the measurement entirely.
365
+
366
+ ### Stability
367
+
368
+ The `done in …` stderr line, its format grammar, and the `elapsedMs` field contract are **stable** as of spec v1.0.0. Changing the grammar, the time units, or the location (stderr ↔ stdout) is a major bump. Adding `elapsedMs` to a schema that previously omitted it is a minor bump.
369
+
370
+ ---
371
+
328
372
  ## Stability
329
373
 
330
374
  The **verb list** is stable as of spec v1.0.0. Adding a verb is a minor bump. Removing a verb is a major bump.
@@ -0,0 +1,66 @@
1
+ # Conformance coverage
2
+
3
+ Authoritative map of JSON Schemas in `spec/schemas/` to the conformance cases that exercise them. Every schema MUST have at least one case before spec v1.0.0 ships — missing case → missing release (AGENTS.md §Rules for AI agents editing spec/).
4
+
5
+ This file is hand-maintained. A CI check at spec cut-time compares the schema inventory against this table and fails if any schema lacks a case.
6
+
7
+ ## Coverage matrix
8
+
9
+ | # | Schema | Case(s) | Status | Notes |
10
+ |---|---|---|---|---|
11
+ | 1 | `node.schema.json` | `basic-scan` | 🟢 covered | Exercised via ScanResult containment. |
12
+ | 2 | `link.schema.json` | — | 🔴 missing | Needs fixture with at least one `invokes` + `references` + `mentions` link, both `high`/`medium`/`low` confidence. |
13
+ | 3 | `issue.schema.json` | — | 🔴 missing | Needs fixture triggering `trigger-collision` + `broken-ref` + `superseded`. |
14
+ | 4 | `scan-result.schema.json` | `basic-scan`, `kernel-empty-boot` | 🟢 covered | Zero-filled (empty-boot) + populated (minimal-claude) both asserted. |
15
+ | 5 | `execution-record.schema.json` | — | 🔴 missing | Blocked by Step 4 (history). Needs a case that runs a `local` action and inspects `state_executions` via `sm history --json`. |
16
+ | 6 | `project-config.schema.json` | — | 🔴 missing | Case: init a scope, write a partial `.skill-map.json`, assert effective config after merge. |
17
+ | 7 | `plugins-registry.schema.json` | — | 🔴 missing | Two sub-cases required: (a) `PluginManifest` validation via `sm plugins show --json`; (b) aggregate `PluginsRegistry` via `sm plugins list --json`. |
18
+ | 8 | `job.schema.json` | — | 🔴 missing | Blocked by Step 9 (job system). Needs a case that submits a local action (no LLM), inspects `sm job show --json`. |
19
+ | 9 | `report-base.schema.json` | — | 🔴 missing | Indirect coverage once any summarizer case lands. Direct contract case: validate a handcrafted minimal report ({confidence, safety}) against the base schema. |
20
+ | 10 | `conformance-case.schema.json` | — | 🔴 missing | Self-referential: every `*.json` under `cases/` MUST validate against this schema. Add a meta-case that enumerates + validates all cases. |
21
+ | 11 | `frontmatter/base.schema.json` | `basic-scan` (indirect) | 🟡 partial | Covered via every kind schema's `allOf`. Direct case: fixture with min-required frontmatter only. |
22
+ | 12 | `frontmatter/skill.schema.json` | `basic-scan` | 🟢 covered | One skill in `minimal-claude`. |
23
+ | 13 | `frontmatter/agent.schema.json` | `basic-scan` | 🟢 covered | One agent in `minimal-claude`. |
24
+ | 14 | `frontmatter/command.schema.json` | `basic-scan` | 🟢 covered | One command in `minimal-claude`. |
25
+ | 15 | `frontmatter/hook.schema.json` | `basic-scan` | 🟢 covered | One hook in `minimal-claude`. |
26
+ | 16 | `frontmatter/note.schema.json` | `basic-scan` | 🟢 covered | One note in `minimal-claude`. |
27
+ | 17 | `summaries/skill.schema.json` | — | 🔴 missing | Blocked by Step 9 (`skill-summarizer`). Case: submit summarizer, validate report. |
28
+ | 18 | `summaries/agent.schema.json` | — | 🔴 missing | Blocked by Step 10. |
29
+ | 19 | `summaries/command.schema.json` | — | 🔴 missing | Blocked by Step 10. |
30
+ | 20 | `summaries/hook.schema.json` | — | 🔴 missing | Blocked by Step 10. |
31
+ | 21 | `summaries/note.schema.json` | — | 🔴 missing | Blocked by Step 10. |
32
+ | 22 | `extensions/base.schema.json` | — | 🔴 missing | Meta-case: every manifest under `src/extensions/` validates against the appropriate kind schema (which extends base via `allOf`). |
33
+ | 23 | `extensions/adapter.schema.json` | — | 🔴 missing | Case: the `claude` adapter manifest validates; a crafted invalid manifest (missing `defaultRefreshAction`) fails with `invalid-manifest`. |
34
+ | 24 | `extensions/detector.schema.json` | — | 🔴 missing | Case: `frontmatter` + `slash` + `at-directive` detector manifests validate; a detector emitting a disallowed `emitsLinkKinds` value fails. |
35
+ | 25 | `extensions/rule.schema.json` | — | 🔴 missing | Case: `trigger-collision`, `broken-ref`, `superseded` manifests validate. |
36
+ | 26 | `extensions/action.schema.json` | — | 🔴 missing | Case: a `local` action manifest validates; an `invocation-template` action WITHOUT `promptTemplateRef` fails. |
37
+ | 27 | `extensions/audit.schema.json` | — | 🔴 missing | Case: `validate-all` audit manifest validates; an audit referencing a non-existent rule id in `composes` fails at load with `invalid-manifest`. |
38
+ | 28 | `extensions/renderer.schema.json` | — | 🔴 missing | Case: `ascii` renderer manifest validates. |
39
+ | 29 | `history-stats.schema.json` | — | 🔴 missing | Blocked by Step 4 (history). Case: seed `state_executions` with a deterministic fixture, run `sm history stats --json --since <T0> --until <T1> --period month --top 5`, assert the document validates and that `totals.executionsCount == sum(perAction.executionsCount)` and `errorRates.global == totals.failedCount / totals.executionsCount`. Percentiles (`p95`/`p99`) intentionally omitted in v1 — add later as a minor bump without breaking consumers. |
40
+
41
+ Status legend: 🟢 covered (at least one case asserts the schema end-to-end) · 🟡 partial (covered only indirectly or via a sub-shape) · 🔴 missing.
42
+
43
+ ## Non-schema normative artifacts
44
+
45
+ These have their own conformance cases even though they are not JSON Schemas.
46
+
47
+ | # | Artifact | Case | Status | Notes |
48
+ |---|---|---|---|---|
49
+ | A | Preamble verbatim text | `preamble-bitwise-match` | 🟠 deferred | Deferred to Step 9 (needs `sm job preview` to render a job file). Fixture: `fixtures/preamble-v1.txt` (already present, byte-identical to `prompt-preamble.md` source). |
50
+ | B | Kernel empty-boot invariant | `kernel-empty-boot` | 🟢 covered | All extensions disabled → empty ScanResult. |
51
+ | C | Atomic-claim race safety | — | 🔴 missing | Blocked by Step 9. Two concurrent `sm job claim` invocations against a single queued row — exactly one MUST succeed. |
52
+ | D | Duplicate detection | — | 🔴 missing | Blocked by Step 9. Two `sm job submit` with same `(action, version, node, contentHash)` — second exits 3. |
53
+ | E | `--force` bypass | — | 🔴 missing | Blocked by Step 9. |
54
+ | F | Nonce mismatch | — | 🔴 missing | Blocked by Step 9. `sm record` with wrong nonce → exit 4. |
55
+ | G | Reap | — | 🔴 missing | Blocked by Step 9. Set TTL to 1s; claim; wait; next `sm job run` reaps with reason `abandoned`. |
56
+ | H | `run.*` event envelope for Skill agent | — | 🔴 missing | Blocked by Step 9. Skill-agent flow emits synthetic `r-ext-*` run envelope around one job. |
57
+ | I | Rename heuristic | — | 🔴 missing | Blocked by Step 4. Move a file; same-`body_hash` → high-confidence auto-rename; `state_*` FK rows migrated; no issue emitted. |
58
+ | J | Plugin DDL rejection | — | 🔴 missing | Blocked by Step 8. Plugin migration referencing `state_jobs` → disabled with `invalid-manifest`. |
59
+ | K | Plugin prefix injection | — | 🔴 missing | Blocked by Step 8. Plugin declares `CREATE TABLE foo` → kernel applies as `plugin_<id>_foo`. |
60
+ | L | Elapsed-time reporting | — | 🔴 missing | Blocked by Step 3 (first real verb work). Run any in-scope verb; stderr last line MUST match `/^done in (\d+ms\|\d+\.\d+s\|\d+m \d+s)$/`. In-scope verb with `--json` returning an object MUST carry `elapsedMs`. Exempt verb (`sm version`) MUST NOT emit the line. |
61
+
62
+ ## Release gates
63
+
64
+ - **spec v0.x**: partial coverage acceptable. Every case added as the reference impl lands the verb that makes it runnable.
65
+ - **spec v1.0.0 cut**: all rows above MUST be 🟢 covered or explicitly 🟠 deferred to v1.1 with a linked issue.
66
+ - **CI check**: [`scripts/check-coverage.mjs`](../../scripts/check-coverage.mjs) compares `spec/schemas/**/*.schema.json` against the matrix above on every PR. A schema without a row here, or a row pointing at a missing schema, fails CI (exit 1 with a `::error::` annotation). Wired into `ci.yml` §validate and into `npm run spec:check`.
package/db-schema.md CHANGED
@@ -35,7 +35,7 @@ Every kernel table belongs to exactly one zone, identified by a mandatory name p
35
35
  | State | `state_` | Persistent operational data: jobs, executions, summaries, enrichment, plugin KV. | No | Yes | `state_jobs` |
36
36
  | Config | `config_` | User-owned configuration: plugin enable/disable, preferences, migration ledger. | No | Yes | `config_plugins` |
37
37
 
38
- `sm db reset` drops `scan_*` + `state_*`, keeps `config_*`. `sm db backup` preserves `state_*` + `config_*`; `scan_*` is regenerated on demand.
38
+ `sm db reset` drops `scan_*` only (non-destructive — equivalent to forcing the next scan from a clean slate). `sm db reset --state` also drops `state_*` (destructive to operational history). `sm db reset --hard` deletes the DB file entirely. `sm db backup` preserves `state_*` + `config_*`; `scan_*` is always regenerated on demand and is never included in backups.
39
39
 
40
40
  ---
41
41
 
@@ -206,7 +206,7 @@ One row per `(node_id, summarizer_action_id)`. See `schemas/summaries/`.
206
206
 
207
207
  Primary key: `(node_id, summarizer_action_id)`. Indexes: `ix_state_summaries_generated_at`.
208
208
 
209
- ### `state_enrichment`
209
+ ### `state_enrichments`
210
210
 
211
211
  One row per `(node_id, provider_id)`.
212
212
 
@@ -219,9 +219,9 @@ One row per `(node_id, provider_id)`.
219
219
  | `fetched_at` | INTEGER | NOT NULL |
220
220
  | `stale_after` | INTEGER | NULL |
221
221
 
222
- Primary key: `(node_id, provider_id)`. Indexes: `ix_state_enrichment_stale_after`.
222
+ Primary key: `(node_id, provider_id)`. Indexes: `ix_state_enrichments_stale_after`.
223
223
 
224
- ### `state_plugin_kv`
224
+ ### `state_plugin_kvs`
225
225
 
226
226
  Shared key-value store for plugins that declared storage mode `kv`. See `plugin-kv-api.md` for the accessor contract.
227
227
 
@@ -233,7 +233,7 @@ Shared key-value store for plugins that declared storage mode `kv`. See `plugin-
233
233
  | `value_json` | TEXT | NOT NULL |
234
234
  | `updated_at` | INTEGER | NOT NULL |
235
235
 
236
- Primary key: `(plugin_id, node_id, key)` with `node_id` using a sentinel empty string when NULL to satisfy PK constraints on engines that reject NULL in PK columns. Indexes: `ix_state_plugin_kv_plugin_id`.
236
+ Primary key: `(plugin_id, node_id, key)` with `node_id` using a sentinel empty string when NULL to satisfy PK constraints on engines that reject NULL in PK columns. Indexes: `ix_state_plugin_kvs_plugin_id`.
237
237
 
238
238
  ---
239
239
 
@@ -285,7 +285,7 @@ The kernel ALSO maintains `PRAGMA user_version` (or the engine equivalent) as a
285
285
  - **Location**: kernel migrations in `src/migrations/` (reference impl); plugin migrations in `<plugin-dir>/migrations/`.
286
286
  - **Wrapping**: the kernel wraps each file in `BEGIN; ... ; COMMIT;`. Files contain DDL only.
287
287
  - **Strict versioning**: no idempotency is required. `CREATE TABLE IF NOT EXISTS` is DISCOURAGED in kernel migrations (but permitted in plugin migrations, at the plugin author's discretion).
288
- - **Auto-apply**: on startup, unless `auto_migrate: false` in config. A backup is written to `.skill-map/backups/skill-map-pre-migrate-v<N>.db` before applying.
288
+ - **Auto-apply**: on startup, unless `autoMigrate: false` in config. A backup is written to `.skill-map/backups/skill-map-pre-migrate-v<N>.db` before applying.
289
289
  - **Plugin migration order**: plugins are migrated after kernel migrations and in stable alphabetical order by plugin id. A failing plugin migration disables only that plugin; other plugins and the kernel continue.
290
290
 
291
291
  `sm db migrate` controls migration flow manually: `--dry-run`, `--status`, `--to <n>`, `--kernel-only`, `--plugin <id>`, `--no-backup`.
@@ -298,7 +298,7 @@ Two modes declared in `plugin.json` (see `schemas/plugins-registry.schema.json`)
298
298
 
299
299
  | Mode | Manifest | Backing |
300
300
  |---|---|---|
301
- | **KV** (mode A) | `"storage": { "mode": "kv" }` | Shared `state_plugin_kv`. See `plugin-kv-api.md`. |
301
+ | **KV** (mode A) | `"storage": { "mode": "kv" }` | Shared `state_plugin_kvs`. See `plugin-kv-api.md`. |
302
302
  | **Dedicated** (mode B) | `"storage": { "mode": "dedicated", "tables": [...], "migrations": [...] }` | Plugin-owned tables, prefixed `plugin_<normalized_id>_`. |
303
303
 
304
304
  Normalization of `plugin_id` for the prefix:
@@ -314,11 +314,19 @@ Collisions after normalization are a load-time error; both plugins are disabled
314
314
 
315
315
  ### Triple protection for mode B
316
316
 
317
- The kernel MUST enforce all three layers:
317
+ The kernel MUST enforce all three layers **in this exact order** for every plugin migration:
318
318
 
319
- 1. **Prefix injection**: the kernel rewrites the `CREATE TABLE` statements in the plugin migration to inject `plugin_<id>_` into every table name that doesn't already have it. A plugin CANNOT create un-prefixed tables.
320
- 2. **DDL validation**: plugin migrations are parsed before application. The kernel MUST reject: foreign keys to kernel tables, triggers on kernel tables, `DROP` / `ALTER` against kernel tables, `ATTACH` statements, global `PRAGMA` statements (except `PRAGMA <plugin>_*` if applicable to the backend).
321
- 3. **Scoped connection**: at runtime, the plugin receives a `Database` wrapper (not a raw handle). The wrapper rejects queries that touch tables outside the plugin's own prefix.
319
+ 1. **Parse** the kernel parses each plugin migration SQL file into an AST. Parse errors disable the plugin with status `load-error`.
320
+ 2. **DDL validation (pre-rewrite)** the AST is validated against the original table names authored by the plugin. Kernel MUST reject, before any rewrite:
321
+ - References (FK / trigger / view) to any kernel table (prefix `scan_`, `state_`, `config_`) or to another plugin's table (prefix `plugin_<other-id>_`).
322
+ - `DROP` / `ALTER` / `TRUNCATE` against anything outside the plugin's own logical table names.
323
+ - `ATTACH DATABASE` statements.
324
+ - Global `PRAGMA` statements (anything not scoped to a plugin-owned table).
325
+ Rejection here is intentional: validation runs **before** prefix injection so kernel tables are named as the plugin wrote them, making the reject test straightforward.
326
+ 3. **Prefix injection (rewrite)** — the kernel rewrites the AST so every table name the plugin authored becomes `plugin_<normalizedId>_<originalName>` if it doesn't already carry the prefix. Index and constraint names get the same treatment. A plugin CANNOT create un-prefixed tables.
327
+ 4. **Scoped connection (runtime)** — at runtime, the plugin receives a `Database` wrapper (not a raw handle). The wrapper rejects any query that touches tables whose name doesn't start with this plugin's prefix. This is the last-line defense: even if a migration-time layer were bypassed, runtime queries still cannot reach out-of-namespace data.
328
+
329
+ Step 4 is separate from 1–3 because it applies at query time, not migration time. Together the four steps form the "triple protection" referenced across the spec (the name predates the explicit parse step).
322
330
 
323
331
  Honest note: plugins are user-placed code. Protection guards against accidents (a plugin that mistakenly names a table `state_jobs`), not against hostile plugins. A malicious plugin running in the same process can bypass any JS-level guard. Post-v1.0 evaluates sandboxing (worker threads, VM contexts) and/or signing.
324
332
 
@@ -335,6 +343,34 @@ Backups include `state_*` + `config_*` only; `scan_*` is regenerated after resto
335
343
 
336
344
  ---
337
345
 
346
+ ## Rename detection (automatic)
347
+
348
+ `scan_nodes.path` is the canonical node identifier in v0. Moving a file therefore rewrites the primary key, which would orphan every `state_*` row referencing the old path (`state_executions.node_ids_json`, `state_jobs.node_id`, `state_summaries.node_id`, `state_enrichments.node_id`).
349
+
350
+ Implementations MUST apply a rename heuristic at scan time **before** committing the new scan transaction:
351
+
352
+ 1. Compute the set `deletedPaths` (rows present in the previous `scan_nodes` but absent from the new walk) and `newPaths` (rows present in the new walk but absent from the previous scan).
353
+ 2. For each pair `(deletedPath, newPath)` where `newPath.bodyHash == deletedPath.bodyHash` → classify as **high-confidence rename**. The kernel MUST:
354
+ - Update every `state_*` row whose `node_id` equals `deletedPath` to reference `newPath`.
355
+ - Emit no issue. Log at `info` level.
356
+ 3. Remaining pairs where `newPath.frontmatterHash == deletedPath.frontmatterHash` (body differs, frontmatter is a perfect match) → classify as **medium-confidence rename**. The kernel MUST:
357
+ - Apply the same FK migration.
358
+ - Emit an issue with `ruleId: auto-rename-medium` (severity `warn`) pointing to both paths. The issue's `data` MUST include `{ from: <old.path>, to: <new.path>, confidence: "medium" }` so `sm orphans undo-rename <new.path>` can read the prior path without user input.
359
+ 4. Any `deletedPath` left without a match after steps 2–3 becomes an **orphan**: the kernel emits an issue with `ruleId: orphan` (severity `info`) and keeps the `state_*` rows referencing the dead path untouched until the user runs `sm orphans reconcile <dead.path> --to <new.path>` or accepts the orphan.
360
+
361
+ Matching is 1-to-1: once a `newPath` is claimed as the rename target of some `deletedPath`, no other deletion can match it in the same scan. Ambiguity (two deletions share a body hash with the same new path) → fall back to the orphan path for all candidates, with issue `auto-rename-ambiguous` listing every conflict. `auto-rename-ambiguous` issues MUST populate `data` with `{ to: <new.path>, candidates: [<old.path.a>, <old.path.b>, ...] }`; in this case `sm orphans undo-rename` requires the user to pass `--from <old.path>` to disambiguate.
362
+
363
+ Note on casing: `bodyHash` / `frontmatterHash` / `ruleId` / `data` are the domain-object field names (per `node.schema.json` and `issue.schema.json`). The SQLite reference impl stores the same values in `body_hash` / `frontmatter_hash` / `rule_id` / `data_json` columns; the storage adapter bridges the two (see §Naming conventions above). The heuristic is specified against the domain types, not the columns.
364
+
365
+ The heuristic runs inside the scan transaction, so either all renames land or none do. `sm scan` is the only surface that triggers automatic rename detection. Two manual verbs exist for cases the heuristic missed or got wrong:
366
+
367
+ - `sm orphans reconcile <orphan.path> --to <new.path>` — forward direction. Attaches FKs of an orphan to a live node. Use when the heuristic could not match (semantic rename, body rewrite).
368
+ - `sm orphans undo-rename <new.path>` — reverse direction. Reads `issue.data.from` from the active `auto-rename-medium` (or `--from`-disambiguated `auto-rename-ambiguous`) issue on `<new.path>`, migrates `state_*` FKs back, and resolves the issue. The prior path becomes an `orphan`. Use when the heuristic matched two unrelated files that happened to share a frontmatter hash.
369
+
370
+ Both verbs operate on FK ownership only; neither edits files on disk.
371
+
372
+ ---
373
+
338
374
  ## Integrity
339
375
 
340
376
  `sm doctor` MUST check at least:
package/index.json CHANGED
@@ -156,42 +156,52 @@
156
156
  }
157
157
  ]
158
158
  },
159
+ "specPackageVersion": "0.3.0",
159
160
  "integrity": {
160
161
  "algorithm": "sha256",
161
162
  "files": {
162
- "CHANGELOG.md": "88908b684eabb5f508d6fe65a49e0786d1789ca9c8d2eb4195103c837ece1160",
163
- "README.md": "7737242efe28e82b7ba4e2324e0ff6917a42e84c5bde9b940e409c8282d573eb",
164
- "architecture.md": "6c25d25eabee7473fa25aa7884f1d3e3e8bed70eda3d688f7755ae9d5d4b0fd1",
165
- "cli-contract.md": "321f625a9fbfdccf16f226dc76ca158bfb3392da5f96fc7b8faab4aa952a284a",
163
+ "CHANGELOG.md": "a9777384c89405fd2ccf16758f8973fd3f55ce5a78ea76f6f859ff86d5d37f1f",
164
+ "README.md": "a233e6b5ab1c41c13cb4790485f0514f7c111880eb70165e77e1253999069120",
165
+ "architecture.md": "be4085b7bbb3476f8a9d6df940b5794839b936ac5dd1921203a91558b008c7bc",
166
+ "cli-contract.md": "4368f2cdf6884f792607a369b43ed6a8a9674cb2d2f790eb086fc961eb29689c",
166
167
  "conformance/README.md": "4e41ff823b55ce3c274a033c5152ae0b2759fc714a714d7815593d8be84c8a4c",
167
168
  "conformance/cases/basic-scan.json": "24623da0cad8c8c54b3ff9b09820ea1276fe8b8f0fc680bf6e8abeb4edb8e424",
168
169
  "conformance/cases/kernel-empty-boot.json": "175524674b14d993d29f10080d7697074b3a2eee25b359ff903344d73c6acc98",
170
+ "conformance/coverage.md": "f46b5b20649c169c4f15c721a2a9cd7f7be11566a64f8fbeb8a6b171eb420762",
169
171
  "conformance/fixtures/minimal-claude/agents/reviewer.md": "d0dd681ba63838301e480116aa09825329f01832b0116de5c5476fdd8a5dcf54",
170
172
  "conformance/fixtures/minimal-claude/commands/status.md": "3f36e053fd1c059ffd902f84a55be8a458c26072f97cb37dd7e97314ae2a9bf5",
171
173
  "conformance/fixtures/minimal-claude/hooks/pre-commit.md": "ec9cec8ac4ce34d40ec055ffd90e8f06ea3e5764d6ec3ee84e0d97de71b930c7",
172
174
  "conformance/fixtures/minimal-claude/notes/architecture.md": "5a7e6fdbb1556733dacebad63758057dc1e19090b5a983292c0c65e90b98bcf1",
173
175
  "conformance/fixtures/minimal-claude/skills/hello.md": "8598074020430f294ff1eac39876302448f004b6c48446d453092159319bcbee",
174
176
  "conformance/fixtures/preamble-v1.txt": "1e0aeef224b64477bdc13a949c3ad402e68249caf499ecdba1302371677c068b",
175
- "db-schema.md": "d2dfd52c2bc2a6b00ec6c89acc09240b409a6cfe29a06394351e6de5bc1de651",
177
+ "db-schema.md": "17de515904b8869ea0fe8d3cd608423376396bed6281b784ee1670f184d29f2c",
176
178
  "interfaces/security-scanner.md": "468f8ee412594b14b389c36cefa9ca75a5dec652adbf03ab8bbc7f57ca980253",
177
- "job-events.md": "947d2124acc29f78df8f493549f0d1bdff8159caf621fe8307423c90d7d05f58",
178
- "job-lifecycle.md": "ef3e96357784088e253c5b47ea4e1d4b04a5fdd1b3efb7ab55d70d006ed7e09f",
179
- "plugin-kv-api.md": "fb7cbaea6e18ad696e108fd6ec7a118dc9be6ffef4cf08cc92e511feafa9be30",
179
+ "job-events.md": "2641ef72863a24f590e2ffdb94a352e5ed5ab7123a9f04f42696f96da793bb91",
180
+ "job-lifecycle.md": "793d34de0793596be17d2ed9042eeb4d532e805bceee25d9f9da0c3cb0b34b1c",
181
+ "plugin-kv-api.md": "8079d8b4304ba3d112b5daa40c49e38e8186949b5d4166dadf5a225087b88ce7",
180
182
  "prompt-preamble.md": "9b7478ddce0a77043983f35932677d702319348048fe5d6b1c60257bd1c9f605",
181
183
  "schemas/conformance-case.schema.json": "2740874e00269de6d8121300339401d0283197b6d97dcd77538ec5d108b14de2",
182
184
  "schemas/execution-record.schema.json": "ec0f3acf1d0ce099c059d73eb434936bfd1bcf12023693bd572efb2a7352faa6",
183
- "schemas/frontmatter/agent.schema.json": "3c7ff5e485c1edd40d643ba0e0669fb920a511dfc174b91caff63473585b993d",
184
- "schemas/frontmatter/base.schema.json": "d5cb9460e855a9a1e5e6afb509e0d44165e2fbe02cd2cfc8f4a437906260f980",
185
+ "schemas/extensions/action.schema.json": "d22a98aca9ad3d7bee475b370135372913e5756ad5987084c77e91e04c8472e8",
186
+ "schemas/extensions/adapter.schema.json": "882c23b0c914eb37af5d50704268e028eb94e1601ceb5fc61aaf932f1ab82eb2",
187
+ "schemas/extensions/audit.schema.json": "a871f96d340a9b72fc4cd3ce6f56a553bb6b56636e342501dcb5d3b724475502",
188
+ "schemas/extensions/base.schema.json": "845f0abe723f1916354f05f6c1cd134c4b8da25fd612976f5ca1e205bf0d0fdb",
189
+ "schemas/extensions/detector.schema.json": "58957bf96ca58218eadd1fceecfb0203bcc11d93eb8cef6bba90e89deca3e195",
190
+ "schemas/extensions/renderer.schema.json": "a8cfc40c8be525fb69e62d53bd98f622b82599bb95e3fac66ec324d4298e46f5",
191
+ "schemas/extensions/rule.schema.json": "eca5a877f3eb6d52cbe62e97bbd2ca3ccd2724bbc760e16e63d7473dfb4fa61a",
192
+ "schemas/frontmatter/agent.schema.json": "0e63d7692efb29facccc69472fff48a25f44934618346bfc09738864c6917787",
193
+ "schemas/frontmatter/base.schema.json": "e68fbb85d3e873c4897af776eaf873860bd6e86b5abc1799e801d35c4f7937cf",
185
194
  "schemas/frontmatter/command.schema.json": "7b8463ce9c83edd2e3073dd4cd1bbeec4b42e53b03b48bc9a59e540136c2de89",
186
195
  "schemas/frontmatter/hook.schema.json": "4f935bb2a94c6b08795e7e10a473d5185e5abaa8c215152d41036291835b7aad",
187
196
  "schemas/frontmatter/note.schema.json": "9806b371193c802803638682f9a625f8277152ad3ef68939eb5f05fff2ef65f4",
188
197
  "schemas/frontmatter/skill.schema.json": "b99b8ab23bee01333b4a04946cd9fc13d373d827ef6ddfc7d058daf637f2f80b",
198
+ "schemas/history-stats.schema.json": "23f472d1de06d23fc775aabba821f8375f347af4dc8d89ba567980d61a11f9de",
189
199
  "schemas/issue.schema.json": "40f6f8abadcce0fd8eac9df27ffcc20b2fc9fda6970142ddb8e7e56b1760b9b1",
190
200
  "schemas/job.schema.json": "582999899f8846f70c4d745d2813e53b97a4f5ccd3d8d163eeb68b201e7124e4",
191
201
  "schemas/link.schema.json": "3e92f5c9def61a857a2c7b22846d82b988157de083463615144ddc92403a489e",
192
202
  "schemas/node.schema.json": "14f345fac450f5728c895d1b878e0015eabb9d72ba9da4a8d2236c82933d3fcf",
193
- "schemas/plugins-registry.schema.json": "69388548a51496f2597145154911a208bef12d5c805ed71901e236525dccadf9",
194
- "schemas/project-config.schema.json": "a2aa639f123f303b086683d41f350ccf3929eb23d7f7c9de296da5fbe96d3e0e",
203
+ "schemas/plugins-registry.schema.json": "92b2052bd06e366709dd6e1449d99408999e33707c4007afc7662980e73c3ef1",
204
+ "schemas/project-config.schema.json": "286895996eb8cfb8054eb53a9042ea89fbaa36c1eb9e38fdcfbe08440ae692e3",
195
205
  "schemas/report-base.schema.json": "a1021e9a59b4df9f99cd92454d797e88469766e7d49f52d231c4645ffdfdad8f",
196
206
  "schemas/scan-result.schema.json": "5efe9b1954c5e729c4b55dbc4dd51263d97967d16c0b3cea398877ace74d37b7",
197
207
  "schemas/summaries/agent.schema.json": "3d22558eeb170e00c4fc32018a810d27333cc632c9e528ff386100cfdfded087",
package/job-events.md CHANGED
@@ -50,7 +50,7 @@ Unknown fields in `data` MUST be ignored by consumers (forward compatibility).
50
50
 
51
51
  ## Event catalog
52
52
 
53
- Emitted in roughly this order during a `sm job run --all` invocation. The exact sequence may interleave for parallel runs (post-MVP).
53
+ Emitted in roughly this order during a `sm job run --all` invocation. The exact sequence may interleave for parallel runs (deferred to post-`v1.0`).
54
54
 
55
55
  ### `run.started`
56
56
 
@@ -200,7 +200,20 @@ Emitted inside `sm record` when the callback arrives and passes nonce validation
200
200
  }
201
201
  ```
202
202
 
203
- `runId` on this event is the run that originally claimed the job. If the record is called from outside a run (interactive skill), `runId` is a synthetic id prefixed `r-ext-`.
203
+ `runId` on this event is the run that originally claimed the job. If the record is called from outside a CLI run the canonical case being a Skill agent that called `sm job claim` + `sm record` without ever entering `sm job run` — the kernel MUST synthesize a `runId` of the form `r-ext-YYYYMMDD-HHMMSS-XXXX` (same timestamp + 4-hex shape as real run ids, with the `r-ext-` prefix reserved for externally-driven claims).
204
+
205
+ Synthetic-run envelope: when a Skill agent claims a job, the kernel MUST emit — on the server's WebSocket and in the `--json` ndjson stream if active — a full envelope covering that claim:
206
+
207
+ ```
208
+ run.started (mode="external")
209
+ → job.claimed
210
+ → (no job.spawning — the claim itself is the spawn signal for external runs)
211
+ → job.callback.received
212
+ → (job.completed | job.failed)
213
+ → run.summary
214
+ ```
215
+
216
+ The `run.started.data.mode` carries the literal string `external` so UI consumers can render skill-driven work differently from CLI-driven work. `run.summary` closes the synthetic run as soon as the callback is processed; one synthetic run always wraps exactly one job. This keeps the WebSocket broadcaster's contract ("every job event lives inside a run envelope") intact across both runner paths.
204
217
 
205
218
  ### `job.completed`
206
219
 
@@ -291,6 +304,111 @@ A parallel implementation MAY interleave per-job sequences across different `job
291
304
 
292
305
  ---
293
306
 
307
+ ## Non-job events (Stability: experimental)
308
+
309
+ These event families cover kernel activity other than job execution. They share the common envelope (`type`, `timestamp`, `runId`, `jobId`, `data`). For non-job events `jobId` is always `null`; `runId` identifies the invocation that produced the event — a scan gets an `r-scan-YYYYMMDD-HHMMSS-XXXX` id, an issue recomputation outside a scan gets an `r-check-...` id, following the same `r-<mode>-...` shape as the external-Skill synthetic envelope (`r-ext-...`).
310
+
311
+ The **shapes below are experimental through spec v0.x**. The reference impl starts emitting them at Step 12 alongside the WebSocket broadcaster; once real consumers exercise the stream, the fields lock. Bumping them to `stable` is a minor spec bump; changes to field shapes before `stable` are allowed without a major bump (per `versioning.md` §Pre-1.0).
312
+
313
+ ### Scan events
314
+
315
+ #### `scan.started`
316
+
317
+ Emitted once when a scan begins (full, `--changed`, or `-n <node.path>`).
318
+
319
+ ```json
320
+ {
321
+ "type": "scan.started",
322
+ "timestamp": 1745159455123,
323
+ "runId": "r-scan-20260420-143055-a3f2",
324
+ "jobId": null,
325
+ "data": {
326
+ "mode": "full | changed | single",
327
+ "target": "<node.path> | null",
328
+ "rootsCount": 1
329
+ }
330
+ }
331
+ ```
332
+
333
+ #### `scan.progress`
334
+
335
+ Emitted periodically during a scan (implementation-defined cadence; SHOULD throttle to ≥250 ms apart to keep WS traffic cheap).
336
+
337
+ ```json
338
+ {
339
+ "type": "scan.progress",
340
+ "timestamp": 1745159455500,
341
+ "runId": "...",
342
+ "jobId": null,
343
+ "data": {
344
+ "filesSeen": 128,
345
+ "filesProcessed": 64,
346
+ "filesSkipped": 3
347
+ }
348
+ }
349
+ ```
350
+
351
+ #### `scan.completed`
352
+
353
+ Emitted once at scan end.
354
+
355
+ ```json
356
+ {
357
+ "type": "scan.completed",
358
+ "timestamp": 1745159456000,
359
+ "runId": "...",
360
+ "jobId": null,
361
+ "data": {
362
+ "nodes": 187,
363
+ "links": 421,
364
+ "issues": 12,
365
+ "durationMs": 877
366
+ }
367
+ }
368
+ ```
369
+
370
+ ### Issue events
371
+
372
+ Emitted by the scan after `scan.completed` when the new scan's issue set differs from the previous one. Enables a UI "issue inbox" to update incrementally without re-fetching the full list.
373
+
374
+ #### `issue.added`
375
+
376
+ ```json
377
+ {
378
+ "type": "issue.added",
379
+ "timestamp": 1745159456100,
380
+ "runId": "...",
381
+ "jobId": null,
382
+ "data": {
383
+ "ruleId": "trigger-collision",
384
+ "severity": "warn",
385
+ "nodeIds": ["skills/a.md", "skills/b.md"],
386
+ "message": "..."
387
+ }
388
+ }
389
+ ```
390
+
391
+ #### `issue.resolved`
392
+
393
+ Emitted when an issue present in the previous scan is absent from the new one.
394
+
395
+ ```json
396
+ {
397
+ "type": "issue.resolved",
398
+ "timestamp": 1745159456101,
399
+ "runId": "...",
400
+ "jobId": null,
401
+ "data": {
402
+ "ruleId": "broken-ref",
403
+ "nodeIds": ["skills/c.md"]
404
+ }
405
+ }
406
+ ```
407
+
408
+ Issue diffing is keyed on `(ruleId, nodeIds sorted, message)` — same key → same issue. A payload change on the same key emits no event; consumers re-read full issue detail from `sm check` when needed.
409
+
410
+ ---
411
+
294
412
  ## Error handling
295
413
 
296
414
  If an event payload cannot be serialized (internal bug), the implementation MUST emit a synthetic event:
@@ -313,10 +431,12 @@ Consumers MAY treat `emitter.error` as a soft failure (log and continue). Implem
313
431
 
314
432
  ## Stability
315
433
 
316
- The **event type list** above is stable as of spec v1.0.0. Adding a new event type is a minor bump. Removing or renaming one is a major bump.
434
+ The **job event type list** (`run.*`, `job.*`, `model.delta`, `emitter.error`) is stable as of spec v1.0.0. Adding a new event type is a minor bump. Removing or renaming one is a major bump.
317
435
 
318
436
  **Adding** fields to `data` is a minor bump. Changing a field's type or removing a field is a major bump.
319
437
 
320
438
  Consumers MUST ignore unknown fields (forward compatibility).
321
439
 
322
440
  The envelope (`type`, `timestamp`, `runId`, `jobId`, `data`) is stable. Adding an envelope field is a major bump because every consumer would need to handle it.
441
+
442
+ The **non-job event families** (`scan.*`, `issue.*`) are marked **experimental** across spec v0.x. They ship alongside the WebSocket broadcaster at Step 12 of the reference impl; shapes may tighten before a stable tag lands. Once promoted to `stable` (a minor spec bump), the same add/remove/rename semantics as the job events apply.