# description
Manifest shape for an `Action` extension. An action operates on one or more nodes in one of two modes: `deterministic` (code runs in-process, returns a report JSON directly) or `probabilistic` (kernel renders a prompt, a runner executes it against an LLM, the callback closes the job). **Structure-as-truth files**: every Action carries `<action-dir>/report.schema.json` (the JSON Schema for the report, MUST extend `report-base.schema.json`); probabilistic Actions additionally carry `<action-dir>/prompt.md` (the prompt template). An Action whose report schema extends the canonical node-summary schema (under `summaries/`, via `$ref`; today the single universal `summaries/markdown.schema.json`) is a summarizer: recording a `completed` job for it also upserts the validated report into `state_summaries` (see `job-lifecycle.md` §Record). The summarizer signal is the report schema itself; there is no manifest flag. The kernel resolves both by convention; missing or mis-placed files surface as `load-error`. A deterministic Action with a `prompt.md` in its folder is also `load-error` (config inconsistent). **`prob*` prefix convention**: manifest fields that only apply when `mode=probabilistic` start with `prob`; if a deterministic-only field ever appears, it starts with `det`.

# properties.mode.description
`deterministic` (default): the plugin's code computes the report synchronously, no job, no agent. `probabilistic`: the kernel renders `prompt.md` + preamble into a queued job; an external agent processes it via `sm jobs claim` and `sm record` closes it.

# properties.probNodeless.description
Declares that this probabilistic Action operates on NO node: its prompt asks nothing about project content, so a job for it carries no user content and needs no target. Submitting one skips target resolution entirely and enqueues against the synthetic node id `sm://<qualified-extension-id>`, which never resolves to a file; the on-disk read + drift verification (`job-lifecycle.md` §Submit step 8) is skipped along with it, since there is nothing on disk to read or to drift. The `{{userContent}}` seam still marks where the kernel injects its prelude (the report contract), but no `<user-content>` block is emitted. Today the single declarer is the system liveness probe `core/ai-ping-action`, which measures whether an agent is draining the queue, a fact about the AGENT, not about any file: aiming it at a real node made the probe fail whenever that node had been deleted since the last scan, and made it impossible to run at all on an empty corpus. Ignored unless `mode=probabilistic` (a deterministic Action runs in-process against a node it is given, and has no prompt to render).

# properties.probExpectedDurationSeconds.description
Best-effort ADVISORY estimate of wall-clock duration when `mode=probabilistic`. It never arms an expiry (jobs carry no TTL unless the operator sets one, Decision #139); it feeds the `jobs-overdue` doctor check and display surfaces. 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.

# properties.io.description
Declared IO capability. `invoke()` is pure by contract (no IO); an Action that MUST reach the network (e.g. the provenance verifier `github/enrichment`) declares `['network']` here, which (a) relaxes the purity rule for exactly that capability, (b) injects `ctx.fetch` into its invocation context (implementations MUST route remote calls through it, never a global), and (c) subjects execution to the project policy `allowNetworkActions` (default false: declared-network Actions load and list but are refused at execution with a directed advisory). Network Actions execute via `sm enrich`, never inside `sm scan` and never as queued jobs (the queue carries prompts for agents).

# properties.writes.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).

# properties.precondition.description
Declarative filter that nodes must satisfy for this action to be applicable. Consumed by `--all` fan-out, UI button gating, `sm actions show`. Also drives the inverse analyzer↔action relationship (Modelo B): `analyzerIds` declares which analyzers' findings this action is intended to resolve, replacing the deprecated `Analyzer.recommendedActions` map.

# properties.precondition.properties.provider.description
Provider ids whose nodes this action accepts.

# properties.precondition.properties.analyzerIds.description
Qualified analyzer ids whose findings this action is intended to resolve (Modelo B, replaces the deprecated `Analyzer.recommendedActions`). The UI surfaces matching actions in the node inspector under 'Resolve this issue' when the analyzer's id matches an entry here. Format `<plugin>/<analyzer>` or `<plugin>/<analyzer>:<sub-id>` when the analyzer emits sub-typed issues. Dangling references warn via `recommended-action-missing` in `sm plugins doctor` but do NOT block load.

# properties.precondition.properties.frontmatterMissing.description
Frontmatter-gap gate: the action applies ONLY to nodes whose frontmatter is missing at least one of the listed fields (no frontmatter block, absent field, or empty-string value; a non-string value counts as present). Evaluated by the same node matcher as `kind` / `provider` (BFF launcher classification, `--all` fan-out), so a node already carrying every listed field never lists the action nor receives a fan-out job. E.g. `core/ai-frontmatter-action` declares `["name", "description"]`: its standalone launcher renders only while the file is missing one of them.

# properties.prompt.properties.inputType.description
Input-type id from the closed catalog. The UI renders the matching control before dispatch.

# properties.prompt.properties.paramKey.description
Key under which the UI-collected value is placed in the dispatch `input` body.

# properties.prompt.properties.options.description
Choice list for `enum-pick` / `enum-multipick` input types.

# properties.prompt.description
Reserved (Steps 3+). When set, a parametrized Action declares the single user input it needs; the UI renders the matching input-type control before dispatch and places the value under `paramKey` in the dispatch body. Deterministic no-prompt actions (e.g. `node-bump`) omit it. Mirrors `view-slots.schema.json#/$defs/payloads/_ActionPrompt`.

# properties.ui.description
Plugin-contributed view contributions. Same contract as Extractor.ui / Analyzer.ui (slot-driven, payload-validated). An Action emits these during its scan-time `project()` self-projection (deterministic, side-effect-free, runs in the contribution phase), supplying the target node path explicitly per emission via `ctx.emitContribution(<nodePath>, <contributionId>, payload)`, the same signature Analyzer uses because the Action sees the full graph, not a single node. The canonical use is an Action declaring its OWN `inspector.action.button` so the button lives with the Action that dispatches it (no separate projector analyzer). `extractor`, `analyzer`, and `action` kinds may declare this field.