@skill-map/spec 0.52.0 → 0.54.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/plugin-kv-api.md CHANGED
@@ -5,7 +5,7 @@ Normative contract for plugin-accessible persistence. Two modes exist (see [`db-
5
5
  - **Mode A, KV**: plugin uses the kernel-provided `ctx.store.*` accessor. Backed by the shared `state_plugin_kvs` table.
6
6
  - **Mode B, Dedicated**: plugin owns its own tables with the `plugin_<normalizedId>_` prefix, migrated by the kernel.
7
7
 
8
- This document defines mode A in full and clarifies the boundary with mode B. Implementations MUST expose this API to every plugin that declares `"storage": { "mode": "kv" }` in its manifest.
8
+ This document defines mode A in full and the boundary with mode B. Implementations MUST expose this API to every plugin that declares `"storage": { "mode": "kv" }` in its manifest.
9
9
 
10
10
  ---
11
11
 
@@ -19,7 +19,7 @@ A plugin extension receives a `ctx` object at construction time. `ctx.store` is
19
19
  | `mode: "kv"` | `KvStore` (this document). |
20
20
  | `mode: "dedicated"` | `DedicatedStore` (scoped Database wrapper). See mode B below. |
21
21
 
22
- Plugins SHOULD pick the minimum mode they need. Mode A is simpler, deployed across every scope from day zero, and requires no migrations. Mode B is for plugins that need relational shape, indexes, or cross-row queries.
22
+ Plugins SHOULD pick the minimum mode they need. Mode A is simpler and requires no migrations. Mode B is for plugins that need relational shape, indexes, or cross-row queries.
23
23
 
24
24
  ---
25
25
 
@@ -47,25 +47,25 @@ Implementations in other languages MUST expose the same semantic surface.
47
47
 
48
48
  ### Scoping
49
49
 
50
- Every operation is scoped by the caller's `pluginId`. The plugin cannot specify, override, or observe another plugin's `pluginId`. This is enforced by the kernel when constructing the `ctx.store`, the `pluginId` is captured at registration time and is not an argument.
50
+ Every operation is scoped by the caller's `pluginId`. The plugin cannot specify, override, or observe another plugin's `pluginId`. The kernel enforces this when constructing `ctx.store`: the `pluginId` is captured at registration time and is not an argument.
51
51
 
52
52
  Operations MAY be additionally scoped by `nodePath`:
53
53
 
54
54
  - **Global KV (no `nodePath`)**: `{pluginId, nodePath: null, key}`. One row per plugin + key.
55
55
  - **Node-scoped KV (with `nodePath`)**: `{pluginId, nodePath: "<path>", key}`. One row per plugin + node + key.
56
56
 
57
- Both scopes share the same underlying `state_plugin_kvs` table (see [`db-schema.md`](./db-schema.md)). The `nodePath` column is nullable; implementations MUST use a sentinel empty string internally when the backing engine rejects NULL in composite primary keys.
57
+ Both scopes share the `state_plugin_kvs` table (see [`db-schema.md`](./db-schema.md)). The `nodePath` column is nullable; implementations MUST use a sentinel empty string internally when the backing engine rejects NULL in composite primary keys.
58
58
 
59
59
  ### Semantics
60
60
 
61
61
  | Operation | Behaviour |
62
62
  |---|---|
63
63
  | `get(key, { nodePath })` | Returns the stored value (JSON-decoded) or `null` if no row exists. Never throws for "missing". |
64
- | `set(key, value, { nodePath })` | Upsert. Replaces any existing value. Updates `updatedAt`. The value is JSON-encoded by the kernel; it MUST be JSON-serializable. Cyclic or non-serializable values MUST be rejected with a typed error. |
64
+ | `set(key, value, { nodePath })` | Upsert. Replaces any existing value, updates `updatedAt`. The kernel JSON-encodes the value; it MUST be JSON-serializable. Cyclic or non-serializable values MUST be rejected with a typed error. |
65
65
  | `delete(key, { nodePath })` | Deletes the row if present. Returns `true` if a row was deleted, `false` otherwise. Idempotent. |
66
66
  | `list({ nodePath, prefix })` | Returns all entries matching the scope. `nodePath` omitted: returns global entries (`nodePath IS NULL`). `nodePath: null` (explicit): same as omitted. `nodePath: "<path>"`: returns entries for that node. `prefix`: filters keys starting with the given string. |
67
67
 
68
- Return order of `list` is NOT specified by this spec; consumers MUST NOT rely on ordering. Implementations SHOULD order by `key ASC` for developer ergonomics.
68
+ Return order of `list` is NOT specified; consumers MUST NOT rely on ordering. Implementations SHOULD order by `key ASC` for developer ergonomics.
69
69
 
70
70
  ### Key constraints
71
71
 
@@ -83,7 +83,7 @@ Return order of `list` is NOT specified by this spec; consumers MUST NOT rely on
83
83
 
84
84
  The `KvStore` operations are individually atomic. There is NO multi-operation transaction in mode A, plugins that need transactional semantics across several rows MUST use mode B.
85
85
 
86
- Implementations MUST NOT expose a `transaction()` method on `KvStore` in mode A. The shape is intentionally minimal to keep the backing table simple.
86
+ Implementations MUST NOT expose a `transaction()` method on `KvStore` in mode A. The shape is minimal to keep the backing table simple.
87
87
 
88
88
  ### Errors
89
89
 
@@ -129,7 +129,7 @@ interface DedicatedStore {
129
129
  `DedicatedStore.db` is a wrapper, NOT a raw handle. Every query passes through a validator that rejects:
130
130
 
131
131
  - References to tables whose name doesn't start with this plugin's prefix.
132
- - DDL statements (`CREATE`, `ALTER`, `DROP`, `TRUNCATE`). Mode B DDL is runtime-immutable after migrations; plugins change shape via a new migration, not at runtime.
132
+ - DDL statements (`CREATE`, `ALTER`, `DROP`, `TRUNCATE`). Mode B DDL is runtime-immutable after migrations; plugins change shape via a new migration.
133
133
  - `ATTACH DATABASE` statements.
134
134
  - `PRAGMA` statements that aren't scoped to the plugin's own tables.
135
135
 
@@ -143,8 +143,7 @@ Mode B plugins MAY call `db.transaction(async (tx) => { ... })`. The kernel prov
143
143
 
144
144
  - Location: `<plugin-dir>/migrations/NNN_snake_case.sql`.
145
145
  - Applied in order after kernel migrations on boot.
146
- - Prefix injection: the kernel rewrites `CREATE TABLE <name>` into `CREATE TABLE plugin_<id>_<name>` if the prefix is missing.
147
- - Index and constraint prefixes are similarly injected.
146
+ - Prefix injection: the kernel rewrites `CREATE TABLE <name>` into `CREATE TABLE plugin_<id>_<name>` if the prefix is missing. Index and constraint prefixes are similarly injected.
148
147
  - A failing plugin migration disables only that plugin (`status: load-error`); other plugins and the kernel continue.
149
148
 
150
149
  See [`db-schema.md`](./db-schema.md) for the normative migration analyzers.
@@ -153,7 +152,7 @@ See [`db-schema.md`](./db-schema.md) for the normative migration analyzers.
153
152
 
154
153
  ## Mode selection guidance
155
154
 
156
- Non-normative; descriptive guidance for plugin authors.
155
+ Non-normative guidance for plugin authors.
157
156
 
158
157
  **Prefer mode A when**:
159
158
 
@@ -165,27 +164,27 @@ Non-normative; descriptive guidance for plugin authors.
165
164
 
166
165
  - You need indexes beyond `(pluginId, nodePath, key)`.
167
166
  - You need to `JOIN` rows, aggregate, or do relational queries.
168
- - Your data model is actually tabular (cache with TTL, observation log, provider registry).
167
+ - Your data model is tabular (cache with TTL, observation log, provider registry).
169
168
  - You are willing to own migrations forever.
170
169
 
171
- A plugin MUST declare **exactly one** storage mode. Mixing modes in the same plugin is forbidden. The [`plugins-registry.schema.json`](./schemas/plugins-registry.schema.json) enforces this at the manifest level (`storage` is a `oneOf` between `kv` and `dedicated`), and at runtime `ctx.store` exposes either the `KvStore` or the `DedicatedStore` shape, never both. A plugin that needs both KV-like and relational access MUST use mode B and implement KV-style rows as a dedicated table.
170
+ A plugin MUST declare **exactly one** storage mode; mixing modes is forbidden. [`plugins-registry.schema.json`](./schemas/plugins-registry.schema.json) enforces this at the manifest level (`storage` is a `oneOf` between `kv` and `dedicated`), and at runtime `ctx.store` exposes either the `KvStore` or the `DedicatedStore` shape, never both. A plugin that needs both KV-like and relational access MUST use mode B and implement KV-style rows as a dedicated table.
172
171
 
173
172
  ---
174
173
 
175
174
  ## Visibility analyzers
176
175
 
177
176
  - A plugin MUST NOT read or write rows outside its scope. Mode A: the accessor is scoped. Mode B: the validator enforces the prefix.
178
- - The kernel MAY expose read-only introspection for diagnostics (e.g., `sm plugins show <id> --storage` lists key counts). This is authoritative, not a plugin-level API.
179
- - `sm db shell` can read any table. This is an operator-level escape hatch; plugins MUST NOT rely on it.
177
+ - The kernel MAY expose read-only introspection for diagnostics (e.g., `sm plugins show <id> --storage` lists key counts). Authoritative, not a plugin-level API.
178
+ - `sm db shell` can read any table. Operator-level escape hatch; plugins MUST NOT rely on it.
180
179
 
181
180
  ---
182
181
 
183
182
  ## Backup and retention
184
183
 
185
- - Mode A rows are stored in `state_plugin_kvs` and are backed up with `sm db backup`.
186
- - Mode B rows live in the plugin's dedicated tables, prefixed `plugin_<id>_`, and are likewise backed up.
187
- - `sm plugins disable <id>` does NOT drop the plugin's data, disabled plugins keep their KV rows and dedicated tables. (`scan_contributions` rows ARE purged eagerly on disable, see `db-schema.md` § `scan_contributions`, because those are scan-derived and would otherwise keep rendering in the UI until the next scan. The KV / dedicated-table data is plugin-managed and survives toggle cycles so re-enabling restores state.) `sm plugins forget <id>` (deferred to post-`v1.0`) is the verb that wipes everything.
188
- - `sm db reset` (no modifier) drops only `scan_*`. Plugin KV rows (mode A) and plugin-dedicated tables (mode B) are **preserved**, the reset is non-destructive to plugin storage.
184
+ - Mode A rows live in `state_plugin_kvs` and are backed up with `sm db backup`.
185
+ - Mode B rows live in the plugin's dedicated tables (prefixed `plugin_<id>_`) and are likewise backed up.
186
+ - `sm plugins disable <id>` does NOT drop the plugin's data; disabled plugins keep their KV rows and dedicated tables. (`scan_contributions` rows ARE purged eagerly on disable, see `db-schema.md` § `scan_contributions`, because those are scan-derived and would otherwise keep rendering in the UI until the next scan. KV / dedicated-table data is plugin-managed and survives toggle cycles so re-enabling restores state.) `sm plugins forget <id>` (deferred to post-`v1.0`) wipes everything.
187
+ - `sm db reset` (no modifier) drops only `scan_*`. Plugin KV rows (mode A) and plugin-dedicated tables (mode B) are **preserved** (non-destructive to plugin storage).
189
188
  - `sm db reset --state` drops `state_*` AND every `plugin_<normalized_id>_*` table, which includes `state_plugin_kvs` (mode A) AND the plugin-dedicated tables (mode B). The CLI MUST require interactive confirmation unless `--yes` is passed.
190
189
  - `sm db reset --hard` deletes the DB file entirely, destroying all plugin storage regardless of mode.
191
190
 
@@ -193,11 +192,11 @@ A plugin MUST declare **exactly one** storage mode. Mixing modes in the same plu
193
192
 
194
193
  ## Honest note on isolation
195
194
 
196
- Mode A is perfectly isolated at the row level: the accessor physically cannot see another plugin's rows.
195
+ Mode A is isolated at the row level: the accessor physically cannot see another plugin's rows.
197
196
 
198
- Mode B is **isolated against accidents, not hostile code**. The scoped `Database` wrapper rejects cross-namespace queries at runtime. But a malicious plugin running in the same JavaScript process can bypass the wrapper by importing raw engine bindings directly. Plugins are user-placed code; the kernel trusts the user's judgement at install time.
197
+ Mode B is **isolated against accidents, not hostile code**. The scoped `Database` wrapper rejects cross-namespace queries at runtime, but a malicious plugin in the same JavaScript process can bypass it by importing raw engine bindings directly. Plugins are user-placed code; the kernel trusts the user's judgement at install time.
199
198
 
200
- Post-v1.0 work: signed manifest, sandboxed worker-thread isolation, per-plugin DB file. None of these land before `v0.5.0`.
199
+ Post-v1.0 work: signed manifest, sandboxed worker-thread isolation, per-plugin DB file. None land before `v0.5.0`.
201
200
 
202
201
  ---
203
202
 
@@ -211,7 +210,7 @@ Post-v1.0 work: signed manifest, sandboxed worker-thread isolation, per-plugin D
211
210
  ## Stability
212
211
 
213
212
  - The `KvStore` interface (method names, options, return shapes) is **stable** as of spec v1.0.0.
214
- - Adding a method to `KvStore` is a minor bump; removing or changing signature is a major bump.
213
+ - Adding a method to `KvStore` is a minor bump; removing or changing a signature is a major bump.
215
214
  - Mode names (`kv`, `dedicated`) are **stable**. Adding a third mode is a minor bump.
216
215
  - Key and value size limits are implementation-defined and MAY change without a spec bump; implementations MUST document their limits in their own changelog.
217
216
  - Error class names are **stable**; adding a new error class is a minor bump.
@@ -0,0 +1,96 @@
1
+ # Plugin quickstart
2
+
3
+ A working `skill-map` plugin in three steps, plus the map of where each kind fits. For the full contract (manifest, the six kinds, storage, view contributions, testing) see the [Plugin author guide](./plugin-author-guide.md); the schemas under [`schemas/`](./schemas/) are the source of truth.
4
+
5
+ ## Where each kind fits
6
+
7
+ A plugin is one or more of **six extension kinds**. Each plugs into one point of skill-map's lifecycle; pick the one that matches what you want to do.
8
+
9
+ ```text
10
+ THE DETERMINISTIC FLOW ( the scan: fast · reproducible · offline )
11
+ ═══════════════════════════════════════════════════════════════════
12
+
13
+ files on disk
14
+
15
+
16
+ ┌────────────┐
17
+ │ PROVIDER │ decides what counts as a node, and under which lens
18
+ └─────┬──────┘ e.g. .claude/skills/foo/SKILL.md → a Claude skill
19
+
20
+ ┌────────────┐
21
+ │ EXTRACTOR │ reads one node and pulls out its references and signals
22
+ └─────┬──────┘ e.g. an @architect mention → a link to that agent
23
+
24
+ ┌────────────┐
25
+ │ ANALYZER │ looks across the whole graph and flags problems
26
+ └─────┬──────┘ e.g. a link to a missing file → an Issue
27
+
28
+ ┌────────────┐
29
+ │ ACTION │ acts on a node (still on the deterministic flow); can
30
+ └─────┬──────┘ also run as an LLM job. e.g. Bump · Summarize (LLM)
31
+
32
+ ┌────────────┐
33
+ │ FORMATTER │ turns the finished graph into an output format
34
+ └────────────┘ e.g. the whole graph → an ASCII tree ( sm graph )
35
+
36
+
37
+ Off to the side, reacting to the whole lifecycle (never blocks it):
38
+
39
+ ┌────────────┐
40
+ │ HOOK │ watches events and reacts with a side effect
41
+ └────────────┘ e.g. after a scan finishes → notify Slack
42
+ fires on: boot · scan · extractor/analyzer/action · job · shutdown
43
+ ```
44
+
45
+ (Same diagram as the [author guide](./plugin-author-guide.md#plugin-lifecycle-at-a-glance), copied here so the quickstart stands alone.)
46
+
47
+ ## 1. Scaffold
48
+
49
+ ```bash
50
+ sm plugins create extractor my-plugin
51
+ ```
52
+
53
+ Writes a loader-clean plugin under `.skill-map/plugins/my-plugin/`: a `plugin.json` plus a working stub for the chosen kind. The first positional is the kind, the second the plugin id. (Details in [§Scaffolder](./plugin-author-guide.md#scaffolder).)
54
+
55
+ ## 2. Fill the stub
56
+
57
+ Open the generated `index.js` and write your logic. An extractor emits its findings through callbacks on `ctx`:
58
+
59
+ ```javascript
60
+ export default {
61
+ version: '1.0.0',
62
+ description: 'Link any node that mentions ROADMAP.md.',
63
+ scope: 'body',
64
+ extract(ctx) {
65
+ if (ctx.body.includes('ROADMAP.md')) {
66
+ ctx.emitLink({
67
+ source: ctx.node.path,
68
+ target: 'ROADMAP.md',
69
+ kind: 'references',
70
+ sources: ['my-plugin'],
71
+ });
72
+ }
73
+ },
74
+ };
75
+ ```
76
+
77
+ The method name and `ctx` shape differ per kind; each has an example in [§The six extension kinds](./plugin-author-guide.md#the-six-extension-kinds).
78
+
79
+ ## 3. Load and run
80
+
81
+ ```bash
82
+ sm plugins list # confirm it loaded (status should be green)
83
+ sm scan # run it over your project
84
+ ```
85
+
86
+ A non-green status? [§Diagnostics](./plugin-author-guide.md#diagnostics) lists every status and how to fix it.
87
+
88
+ ## Then go deeper
89
+
90
+ Whatever you need next is one section away in the [Plugin author guide](./plugin-author-guide.md):
91
+
92
+ - [Manifest fields](./plugin-author-guide.md#manifest) and the [`specCompat` strategy](./plugin-author-guide.md#speccompat-strategy)
93
+ - [The six extension kinds](./plugin-author-guide.md#the-six-extension-kinds), in full, with an example each
94
+ - [Storage](./plugin-author-guide.md#storage): a KV bag or a dedicated table
95
+ - [View contributions](./plugin-author-guide.md#view-contributions): chips, badges, and buttons in the UI
96
+ - [Testing your plugin](./plugin-author-guide.md#testing-your-plugin) against the kernel's public types
@@ -1,6 +1,6 @@
1
1
  # Prompt preamble
2
2
 
3
- Canonical text the kernel prepends to every rendered job content blob before the action-specific template. The preamble exists to mitigate prompt injection from user-authored node content. This document defines:
3
+ Canonical text the kernel prepends to every rendered job content blob, before the action-specific template, to mitigate prompt injection from user-authored node content. This document defines:
4
4
 
5
5
  1. The **delimiter contract** that wraps user content.
6
6
  2. The **verbatim preamble text** (the only normative text in the spec).
@@ -32,7 +32,7 @@ An action template that violates rule 4 (e.g., interpolates user text outside `<
32
32
 
33
33
  ## The preamble text
34
34
 
35
- The following text is **normative and verbatim**. Byte-for-byte reproducible. Included in the `contentHash` computation (via `promptTemplateHash`, which itself hashes the preamble + action template concatenation).
35
+ The following text is **normative and verbatim**, byte-for-byte reproducible. Included in the `contentHash` computation (via `promptTemplateHash`, which hashes the preamble + action template concatenation).
36
36
 
37
37
  ```
38
38
  You are operating inside skill-map, a deterministic tool that runs actions
@@ -102,7 +102,7 @@ The preamble establishes a promise from the model:
102
102
  - `safety` MUST conform to [`schemas/report-base.schema.json`](./schemas/report-base.schema.json)`#/properties/safety`.
103
103
  - `confidence` MUST be a number in `[0.0, 1.0]`.
104
104
 
105
- The kernel validates every report against the action's declared schema (which MUST extend [`report-base.schema.json`](./schemas/report-base.schema.json)). A report that lacks `safety` or `confidence`, or whose values are of the wrong shape, is rejected; the job transitions to `failed` with reason `report-invalid` (see [`job-lifecycle.md`](./job-lifecycle.md)).
105
+ The kernel validates every report against the action's declared schema (which MUST extend [`report-base.schema.json`](./schemas/report-base.schema.json)). A report lacking `safety` or `confidence`, or with wrong-shape values, is rejected; the job transitions to `failed` with reason `report-invalid` (see [`job-lifecycle.md`](./job-lifecycle.md)).
106
106
 
107
107
  Implementations MUST NOT tolerate the absence of `safety`. If a model returns a report without it, the failure is the runner's problem to surface, not the kernel's to tolerate.
108
108
 
@@ -116,10 +116,10 @@ On `sm job submit`:
116
116
  2. The kernel validates that the template does not interpolate user text outside of `<user-content>` blocks.
117
117
  3. The kernel prepends the verbatim preamble text above.
118
118
  4. The kernel renders the template by interpolating the node content, wrapping it in `<user-content>`.
119
- 5. The kernel stores the result in `state_job_contents` keyed by `contentHash` (content-addressed: multiple jobs that resolve to the same `contentHash` share one row). There is no canonical filesystem artifact, `sm job preview` and `sm job claim --json` both read directly from this table. Subprocess runners that need a file (e.g., `claude -p` reading stdin from a path) materialize a temporary file from the DB row and remove it after spawn; the temp file is operationally ephemeral, not part of the contract.
119
+ 5. The kernel stores the result in `state_job_contents` keyed by `contentHash` (content-addressed: jobs resolving to the same `contentHash` share one row). No canonical filesystem artifact: `sm job preview` and `sm job claim --json` read directly from this table. Subprocess runners that need a file (e.g., `claude -p` reading stdin from a path) materialize a temp file from the DB row and remove it after spawn; it is operationally ephemeral, not part of the contract.
120
120
  6. The kernel computes `contentHash` over (among other things) the concatenation of preamble + template. A changed preamble (e.g., spec bump) MUST produce a different hash and therefore MUST NOT collide with prior jobs.
121
121
 
122
- Implementations MUST NOT modify the preamble text at runtime (e.g., based on locale, model, or config). The text is universal and invariant.
122
+ Implementations MUST NOT modify the preamble text at runtime (e.g., based on locale, model, or config): it is universal and invariant.
123
123
 
124
124
  ---
125
125
 
@@ -143,7 +143,7 @@ This preamble is a **mitigation**, not a guarantee. A determined attacker can st
143
143
  2. It gives the model a structured place to report suspected injections, so consumers can act (flag the node, re-run with a different model, refuse to summarize).
144
144
  3. It makes injection attempts visible (via the `safety` field in reports) so that deterministic rules can surface patterns over the graph.
145
145
 
146
- Defense-in-depth: the deterministic analyzer `injection-pattern` (shipped as a built-in analyzer in the default plugin pack) scans node bodies for known injection patterns independently of the LLM. Neither layer is sufficient alone.
146
+ Defense-in-depth: the deterministic analyzer `injection-pattern` (a built-in in the default plugin pack) scans node bodies for known injection patterns independently of the LLM. Neither layer is sufficient alone.
147
147
 
148
148
  ---
149
149
 
@@ -18,6 +18,12 @@
18
18
  "minimum": 1,
19
19
  "description": "Best-effort estimate of wall-clock duration when `mode=probabilistic`. Drives TTL (`ttl = max(probExpectedDurationSeconds × graceMultiplier, minimumTtlSeconds)`). Required for `probabilistic`; ignored otherwise. Renamed from `expectedDurationSeconds` with the structure-as-truth refactor, the `prob` prefix makes it clear at a glance which mode the field belongs to."
20
20
  },
21
+ "writes": {
22
+ "type": "array",
23
+ "uniqueItems": true,
24
+ "items": { "type": "string", "enum": ["sidecar"] },
25
+ "description": "Declares the kinds of persistent writes this Action's `invoke()` may emit (mirrors `IActionResult.writes[].kind`). Today the only kind is `sidecar`: the Action creates or modifies a `.sm` annotation sidecar next to a source file. An Action whose `invoke()` returns a write of a given kind MUST declare that kind here; the declaration is the source of truth consumers gate on. When the `allowSidecarWriters: false` project policy is set, every Action that declares `sidecar` is dropped from the scan composer (its `inspector.action.button` never renders) and the sidecar store refuses the write. Absent = the Action performs no persistent writes (read-only / report-only)."
26
+ },
21
27
  "precondition": {
22
28
  "type": "object",
23
29
  "additionalProperties": false,
@@ -139,6 +139,10 @@
139
139
  "allowEditSmFiles": {
140
140
  "type": "boolean",
141
141
  "description": "**Project-local only** (per `core/config/helper:PROJECT_LOCAL_ONLY_KEYS`). Grants this project permission to create / modify `.sm` annotation sidecars next to source files. Default `false`. The first time a verb or BFF route attempts a `.sm` write while this is `false`, the kernel raises `EConsentRequiredError`. The CLI surfaces it as an interactive `confirm()` prompt (or `--yes` bypass); the BFF returns 412 `confirm-required` so the UI can open a `ConfirmationService` dialog. On accept the flag is persisted to `<cwd>/.skill-map/settings.local.json` (gitignored, per-checkout) and never asked again. On decline the operation aborts WITHOUT persisting the rejection, the next attempt re-asks. **Stripped with a warning when found in the committed `project` layer** (`<cwd>/.skill-map/settings.json`), each developer consents independently."
142
+ },
143
+ "allowSidecarWriters": {
144
+ "type": "boolean",
145
+ "description": "**Project policy, team-shared** (committed in `<cwd>/.skill-map/settings.json`, NOT project-local). Default `true`. When `false`, every extension whose manifest declares `writes: ['sidecar']` (the built-in `core/node-bump`, `core/node-set-tags`, `core/node-set-stability`, plus any external action) is dropped from the scan composer so its `inspector.action.button` never renders, and the sidecar store refuses the write with `ESidecarWritersForbiddenError`. This is a HARD gate that wins over the per-machine `allowEditSmFiles` consent: a developer cannot re-enable sidecar writes locally, and `--yes` does not bypass it. Reads of existing `.sm` sidecars (annotation orphan fields) are unaffected, the policy governs writes / generation only. Unlike `allowEditSmFiles` this key is meant to travel via the shared repo, so it is NOT stripped from the committed `project` layer."
142
146
  }
143
147
  }
144
148
  }