# Weaver user's guide This guide summarizes the behaviour exposed to operators by the current pre-0.1.0 CLI and daemon foundation. Configuration is currently the primary focus because both binaries share the same loading pipeline and rely on the `weaver-config` crate to merge settings from files, environment variables, and command-line arguments. ## 0.1.0 command-surface target Weaver is pre-0.1.0, so the current public command grammar is not a compatibility promise. [ADR 007](adr-007-agent-native-command-surface.md) and the [roadmap](roadmap.md) reset the future public interface around a human-friendly, agent-native command contract. The 0.1.0 target keeps the human terminal experience first-class while making agent usage reliable: - default output remains localized, readable, and accessible for humans; - `--json` is the canonical machine-readable output switch; - resource-first commands replace the prototype `observe`, `act`, and `verify` domain grammar; - Sempai one-liner queries are first-class selectors alongside file-position selectors; - observe-style commands can emit structured selector records that act-style commands consume directly or after ordinary UNIX filtering; - capabilities are public, while providers such as Rope and rust-analyzer are implementation details surfaced through provenance, diagnostics, policy, and expert overrides; and - reusable command metadata, renderer metadata, profile, delivery, feedback, skill, and execution-ledger contracts depend on OrthoConfig where the OrthoConfig roadmap already owns the generic machinery. The [OrthoConfig consumer boundary matrix](orthoconfig-consumer-boundary.md) tracks which target command-contract tasks already consume OrthoConfig, which ones use temporary Weaver wrappers, and which ones are pending upstream OrthoConfig contracts. While those pending and wrapper rows remain open, users may see Weaver-owned help, output, profile, delivery, feedback, or job-ledger behaviour that is expected to converge on the named OrthoConfig contract before the 0.1.0 compatibility promise is made. Representative target commands look like this: ```sh weaver definitions get --uri file:///src/main.rs --position 10:5 weaver references list --uri file:///src/main.rs --position 10:5 --json weaver symbols list --query 'fn $name(...)' --json \ | jq 'select(.name | startswith("old_"))' \ | weaver symbols rename --from-stdin --replace-prefix old_ --with-prefix new_ weaver symbols rename --query 'fn process_request(...)' --new-name run_request weaver patches apply --file changes.patch --dry-run weaver context --json weaver capabilities list --json ``` The command reference below records the current prototype implementation. It is useful for operating this branch today, but future roadmap work should follow the target contract above unless a later ADR explicitly changes the 0.1.0 surface. ## Configuration layering Configuration is layered using `ortho-config` with the following precedence order: 1. built-in defaults, 2. configuration files discovered via `--config-path` and the XDG search path, 3. environment variables, and 4. CLI flags. Each successive layer overrides earlier sources. This guarantees that a parameter passed through the CLI is honoured even when a configuration file or environment variable also supplies the same field. ### CLI flags The CLI exposes the following configuration flags today: - `--config-path ` — reads an explicit configuration file. - `--daemon-socket ` — overrides the daemon transport. Accepts values such as `unix:///run/user/1000/weaver.sock` or `tcp://127.0.0.1:9779`. - `--log-filter ` — sets the tracing filter (defaults to `info`). - `--log-format ` — selects the log output format (`json` or `compact` only). - `--capability-overrides ` — appends a directive of the form `language:capability=directive`. Directives may be repeated to accumulate overrides. Duplicate entries are resolved by keeping the last directive for each language and capability pair, and lookups ignore case and surrounding whitespace. - `--locale ` — selects the operator-facing locale (defaults to `en-US`). Locale values must be valid BCP 47 language identifiers. `weaver --help` and `weaver daemon start --help` both list these flags in their `Options:` section. The runtime behaviour remains strict, however: for a configuration flag to take effect, it must appear before the command domain or structured subcommand. ### Environment variables Most options are available through environment variables. They follow the `WEAVER_*` naming convention: - `WEAVER_CONFIG_PATH` - `WEAVER_DAEMON_SOCKET` - `WEAVER_LOG_FILTER` - `WEAVER_LOG_FORMAT` - `WEAVER_LOCALE` Environment variables override files, but remain lower priority than CLI flags. ### Configuration file example Configuration files are written in TOML. The following snippet demonstrates how to pin the daemon socket, switch to a compact log format, and force the call hierarchy capability for Python: ```toml daemon_socket = { transport = "tcp", host = "127.0.0.1", port = 9779 } log_filter = "info" log_format = "compact" locale = "en-GB" [[capability_overrides]] language = "python" capability = "observe.call-hierarchy" directive = "force" ``` ### Validation and error reporting Invalid configuration files are treated as fatal. When `--config-path` points at a broken file, or when discovery finds a malformed `weaver.toml`/ `.weaver.toml`, both the CLI and daemon abort with a `LoadConfiguration` error that lists every offending path. Remove or fix the reported files before retrying. If no configuration files exist at all, the loader still falls back to the built-in defaults described below. See the [developer's guide](developers-guide.md) for toolchain baseline and configuration framework internals. Operators will see aggregated errors enumerated in the order discovery encounters them. For example: ```text failed to load configuration: multiple configuration errors: 1: Configuration file error in '/etc/weaver/weaver.toml': expected `}` 2: Configuration file error in '/home/alex/.weaver.toml': invalid type: string "yes", expected a boolean ``` ## Defaults - **Daemon socket:** On Unix-like targets, the daemon listens on `$XDG_RUNTIME_DIR/weaver/weaverd.sock`. When the runtime directory is unavailable, the default falls back to a per-user namespace under the system temporary directory (for example `/tmp/weaver/uid-1000/weaverd.sock`). Other platforms default to `tcp://127.0.0.1:9779`. - **Logging:** The default filter is `info` and the default format is `json`. - **Capability overrides:** No overrides are applied unless provided via one of the mechanisms above. Each directive is treated independently, so multiple overrides may be supplied to tailor the capability matrix for different languages. When `weaverd` starts, it ensures the parent directory for the configured Unix socket exists, returning a descriptive error if the directory cannot be created. This prevents silent failures later when the daemon attempts to bind the socket. ## Daemon lifecycle `weaverd` backgrounds itself using `daemonize-me` and manages runtime artefacts under the same directory as the Unix socket (for example `$XDG_RUNTIME_DIR/weaver`). Launching the daemon creates a lock file (`weaverd.lock`), a PID file (`weaverd.pid`), and a health snapshot (`weaverd.health`). PID and health files are written atomically, so observers never see a partially written payload. Attempts to start a second copy while one is running fail fast with an "already running" error that reports the existing PID. When the original launch is still initializing and has not yet published a PID, the second invocation now reports "launch already in progress" instead of removing the lock. If the daemon exited uncleanly, the new instance removes the stale files before continuing. The daemon now binds a socket listener as part of startup. It binds to the configured `--daemon-socket` endpoint and accepts multiple client connections concurrently. On Unix targets, stale socket files are removed only after confirming no listener responds, while actively used sockets cause the daemon to fail fast with a clear error. The listener removes the Unix socket file on shutdown to avoid lingering bind failures. The daemon implements a JSONL request dispatch loop that reads `CommandRequest` messages from connected clients, routes them to the appropriate domain handler, and streams `DaemonMessage` responses back. Request parsing validates the JSONL structure and rejects malformed input with structured error messages. Domain routing supports `observe`, `act`, and `verify` commands. Unknown domains or operations return structured errors with exit status 1. The `observe get-definition`, `observe get-card`, and `observe graph-slice` operations are fully implemented. `get-definition` accepts `--uri` and `--position`, infers the language from the file extension, initializes the appropriate language server, and returns definition locations as JSON. `get-card` accepts the same location arguments plus `--detail`, reads the target file locally, and returns a Tree-sitter-backed symbol card for supported Rust, Python, and TypeScript files. `graph-slice` accepts the same location arguments plus traversal, detail, and budget options, and returns a stable same-file graph-slice envelope. Missing or malformed arguments return structured error messages with exit status 1. Operations outside the implemented `observe` subcommands, and outside the implemented `act` and `verify` flows, may return "not yet implemented" responses while backend wiring is being completed. The health snapshot is a single-line JSON document describing the current state, enabling operators and automation to poll readiness without speaking the daemon protocol. Example: ```json {"status":"ready","pid":12345,"timestamp":1713356400} ``` The `status` transitions through `starting`, `ready`, and `stopping` before the files are removed on shutdown. Sending `SIGTERM`, `SIGINT`, `SIGQUIT`, or `SIGHUP` prompts the daemon to log the request and complete its shutdown sequence within a ten-second budget. For interactive debugging or CI jobs, set `WEAVER_FOREGROUND=1` to keep the daemon attached to the terminal while preserving the same lock, PID, and health semantics. ## Sandbox defaults External tools launched by the daemon now run inside the `weaver-sandbox` wrapper around `birdcage` 0.8.1. Linux namespaces and `seccomp-bpf` filters are applied automatically; networking is disabled by default; and only a small set of standard library directories are readable to keep dynamically linked executables functioning. Commands must be provided as absolute paths and added to the sandbox allowlist before launch; requests made from multithreaded contexts return a `MultiThreaded` error rather than panicking the process. The sandbox strips the environment unless specific variables are explicitly whitelisted, so callers should pass configuration via the broker rather than relying on inherited host state. ### Lifecycle commands `weaver` now exposes explicit lifecycle commands so operators do not need to manage the daemon manually. All three commands share the same helper logic and therefore honour the configuration flags supplied to the CLI, including `--config-path` and `--daemon-socket`. - `weaver daemon start` verifies that the configured socket is free, spawns the `weaverd` binary (the path can be overridden via `WEAVERD_BIN`), and waits for the health snapshot to report `ready`. The command refuses to start when the socket already accepts connections and prints the runtime directory that now holds the lock, PID, and health files. - `weaver daemon stop` reads the PID file, sends `SIGTERM`, and waits for the runtime artefacts and socket to disappear. If the socket is reachable but the PID file is missing, the command surfaces an error rather than blindly killing a process. Successful stops report the PID that was terminated and confirm the runtime directory was cleaned up. - `weaver daemon status` inspects the JSON health snapshot when present, falling back to the PID file and socket reachability. When no runtime artefacts exist the command prints a short reminder that `daemon start` can be used to launch a new instance. Lifecycle commands never contact the daemon's JSONL transport. They operate on shared runtime files from `weaver-config`, so the CLI and daemon use the same directory layout even when the daemon socket is overridden. ### Automatic daemon startup When a domain command is issued and the daemon is not running, the CLI automatically attempts to start the daemon rather than failing immediately. The message `Waiting for daemon start...` appears on stderr while the CLI waits for the daemon to become ready. The timeout for automatic startup is 30 seconds; if the daemon fails to start within this period, the CLI reports the failure and exits. This behaviour allows operators to run commands without explicitly starting the daemon first: ```sh weaver observe get-definition --uri file:///src/main.rs --position 10:5 ``` If the daemon is not running, it will be started automatically before the command executes. The automatic startup uses the same configuration flags (`--config-path`, `--daemon-socket`, etc.) passed to the command. Errors that prevent connection but are not related to the daemon being offline (such as permission denied or network timeouts) bypass automatic startup and are reported immediately. When the daemon binary cannot be found, the CLI provides actionable guidance: ```text $ weaver observe get-definition --symbol main Waiting for daemon start... error: failed to spawn weaverd binary 'weaverd' Valid alternatives: - Verify weaverd is installed and in your PATH - Set WEAVERD_BIN to the full path to the weaverd binary - Inspect runtime artefacts under $XDG_RUNTIME_DIR/weaver Next command: command -v weaverd || echo 'weaverd not found in PATH' ``` For other startup failures, the CLI suggests checking daemon logs, shows the `weaverd.health` path when available, and suggests running in the foreground to see startup output: ```text error: daemon exited before reporting ready (status: Some(1)) Valid alternatives: - Check the daemon logs for errors - Check health snapshot at $XDG_RUNTIME_DIR/weaver/weaverd.health - Run with WEAVER_FOREGROUND=1 to see startup output Next command: WEAVER_FOREGROUND=1 weaver daemon start ``` ## Current prototype command reference `weaver` exposes three command families: the `--capabilities` probe, daemon lifecycle commands, and domain operations (`observe`, `act`, `verify`). Domain commands are sent to the daemon as JSONL; any arguments after the operation are forwarded verbatim without CLI validation. This section describes the current implementation only; the 0.1.0 target command surface is resource-first and is summarized at the start of this guide. ### Bare invocation Running `weaver` without any arguments prints a short help summary to standard error and exits with a non-zero status code: ```text error: command domain must be provided Usage: weaver [ARG]... Domains: observe Query code structure and relationships act Perform code modifications verify Validate code correctness Next command: weaver --help ``` This output follows the unified three-part error template: an error statement, an alternatives block, and a concrete next command. It does not require a configuration file or a running daemon. Use `weaver --help` for the full reference, including global options and the `daemon` subcommand. ### Version Running `weaver --version` or `weaver -V` prints the version string to standard output and exits with code 0: ```text weaver 0.1.0 ``` This output does not require a configuration file or a running daemon. ### Top-level help Running `weaver --help` displays the full command reference to standard output and exits with code 0. The output includes a purpose statement, quick-start examples, global options, the `daemon` subcommand, and a catalogue of all domains and operations. It also includes the shared configuration flags `--config-path`, `--daemon-socket`, `--log-filter`, `--log-format`, `--capability-overrides`, and `--locale` in the `Options:` section: ```text Domains and operations: observe — Query code structure and relationships get-definition find-references grep diagnostics call-hierarchy get-card graph-slice act — Perform code modifications rename-symbol apply-edits apply-patch apply-rewrite refactor verify — Validate code correctness diagnostics syntax ``` This catalogue is built into the binary and does not require a running daemon or configuration file. The graph-slice operation uses this syntax: ```sh weaver observe graph-slice --uri --position [OPTIONS] ``` `weaver daemon start --help` exposes the same six configuration flags in its own `Options:` section. As with the top-level command, the help surface is truthful about the shared config contract, but the flags still need to appear before `daemon start` at runtime in order to change behaviour. ### Domain-only guidance Running a domain without an operation fails fast on the client side. Known domains print the valid operations for that domain. Unknown domains also fail fast on the client side, even when an operation token is present. This happens before configuration loading, daemon startup, or socket access. Example: ```text $ weaver observe error: operation required for domain 'observe' Available operations: get-definition find-references grep diagnostics call-hierarchy get-card graph-slice Next command: weaver observe get-definition --help ``` The listed `graph-slice` operation accepts: ```sh weaver observe graph-slice --uri --position [OPTIONS] ``` Unknown domains list the canonical domains instead of printing the operation catalogue: ```text $ weaver obsrve get-definition --uri file:///tmp/main.rs --position 1:1 error: unknown domain 'obsrve' Valid domains: observe, act, verify Did you mean 'observe'? Next command: weaver observe get-definition --help ``` The suggestion line appears only when exactly one valid domain is within edit distance 2 of the supplied token. More distant values omit the suggestion but still provide a next command: ```text $ weaver bogus get-definition --uri file:///tmp/main.rs --position 1:1 error: unknown domain 'bogus' Valid domains: observe, act, verify Next command: weaver --help ``` All error messages follow the unified three-part template: error statement, alternatives block, and concrete next command. Unknown operations are handled differently. The request still reaches the daemon because the daemon router owns the canonical operation list for each domain. Human-readable output now includes the full alternatives returned by the daemon: ```text $ weaver --output human observe nonexistent error: unknown operation 'nonexistent' for domain 'observe' Available operations: get-definition find-references grep diagnostics call-hierarchy get-card graph-slice Next command: weaver observe get-definition --help ``` The `graph-slice` alternative in this list maps to: ```sh weaver observe graph-slice --uri --position [OPTIONS] ``` JSON output forwards the daemon payload unchanged: ```json { "status": "error", "type": "UnknownOperation", "details": { "domain": "observe", "operation": "nonexistent", "known_operations": [ "get-definition", "find-references", "grep", "diagnostics", "call-hierarchy", "get-card", "graph-slice" ] } } ``` ### Output formats Daemon responses are JSON objects with `kind` set to `stream` or `exit`. Stream messages include a `stream` field (`stdout` or `stderr`) plus a `data` payload; exit messages contain a numeric `status`. The CLI writes each `data` payload to the matching host stream and terminates using the exit status provided by the final exit message. The `data` payload can be plain text (human-readable) or a JSON document (machine-readable). The CLI accepts `--output` with `auto` (default), `human`, and `json` values. `auto` selects `human` when stdout is a TTY and `json` when output is redirected, so JSON pipelines remain stable. Place `--output` before the command domain and operation because arguments after the operation are passed directly to the daemon (for example, `weaver --output human observe get-definition ...`). When `--output human` is active, commands that return code locations or diagnostics render context blocks with file headers, line-numbered source context, and caret spans. If source content is unavailable, the CLI falls back to the path and range with an explanation of why context could not be shown. Example JSONL envelope: ```json {"kind":"stream","stream":"stdout","data":"definition: file:///path/main.rs:42:17\n"} {"kind":"exit","status":0} ``` Daemon connections time out after five seconds. The CLI aborts after ten consecutive blank lines and treats missing exit messages as failures. ### Capability probe Syntax: ```sh weaver --capabilities ``` Output is always JSON (pretty-printed for humans). Example: ```json { "languages": { "python": { "overrides": { "observe.call-hierarchy": "force" } } } } ``` ### Daemon lifecycle commands Syntax: ```sh weaver daemon start weaver daemon stop weaver daemon status ``` Example human-readable output (`daemon start`): ```text daemon ready (pid 12345) on unix:///tmp/weaver/uid-1000/weaverd.sock runtime artefacts stored under /tmp/weaver/uid-1000 ``` Example JSON output written by the daemon health snapshot file (`weaverd.health`): ```json {"status":"ready","pid":12345,"timestamp":1713356400} ``` ### Domain commands (`observe`, `act`, `verify`) Syntax: ```sh weaver [ARG ...] ``` Current capability keys used for Language Server Protocol (LSP)-backed operations: - `observe.get-definition` - `observe.get-card-hover` - `observe.graph-slice` - `observe.find-references` - `observe.call-hierarchy` - `verify.diagnostics` `observe.get-card-hover` controls whether `observe get-card --detail semantic` may route `textDocument/hover` requests for LSP enrichment. Syntactic operations provided by `weaver-syntax` use the same domain/operation shape (`observe grep` and `act apply-rewrite`) once they are wired into the daemon request loop. The examples below are illustrative; the daemon defines the exact payload schema. #### observe get-definition Syntax: ```sh weaver observe get-definition --uri --position ``` Both `--uri` and `--position` are required. The position uses 1-indexed line and column numbers (matching editor conventions). The language is inferred from the file extension: `.rs` for Rust, `.py` for Python, and `.ts`/`.tsx` for TypeScript. Unsupported extensions return an error. Human output: ```text --> : | | | ^ definition ``` JSON payload (written to stdout stream): ```json [{"uri":"file:///path/to/file.rs","line":42,"column":17}] ``` The response is an array of definition locations. Each location includes the target URI, line number, and column (all 1-indexed). The array may be empty if no definition is found, or contain multiple entries for overloaded symbols. #### observe find-references Syntax: ```sh weaver observe find-references --uri --position ``` Human output: ```text --> : | | | ^ reference ``` JSON payload: ```json {"references":[{"uri":"","line":12,"column":3}]} ``` #### observe call-hierarchy Syntax: ```sh weaver observe call-hierarchy --uri --position ``` Human output: ```text call hierarchy: (direction outgoing, depth 2) ``` JSON payload: Call hierarchy responses return a call graph. Each node includes its stable identifier, symbol name, kind, location, and optional container. Each edge captures the caller, callee, provenance, and optional call-site position. ```json { "nodes": [ { "id": "/src/lib.rs:10:0:main", "name": "main", "kind": "function", "uri": "file:///src/lib.rs", "line": 10, "column": 0, "container": null } ], "edges": [ { "caller": "/src/lib.rs:10:0:main", "callee": "/src/lib.rs:42:0:helper", "source": "lsp", "call_site": { "line": 12, "column": 4 } } ] } ``` #### observe get-card Syntax: ```sh weaver observe get-card --uri --position [--detail ] ``` Arguments: - `--uri` (required) — file URI of the source file containing the symbol. - `--position` (required) — 1-indexed `LINE:COL` position within the symbol. - `--detail` (optional) — progressive detail level, controlling how much information the card contains. One of `minimal`, `signature`, `structure` (default), `semantic`, or `full`. - `--format` (optional) — output format. Currently, only `json` (the default) is supported. Response: The response is a discriminated-union JSON envelope keyed on the `"status"` field. When `"status"` is `"refusal"`, the envelope carries a refusal payload indicating why a card could not be produced. When `"status"` is `"success"`, the envelope contains the card payload. The overall shape of the envelope therefore depends on the `"status"` value. `observe get-card` is Tree-sitter-first. Supported Rust, Python, and TypeScript files return a deterministic card. Requests for unsupported file types or positions that do not resolve to a symbol return a structured refusal. When `--detail semantic` (or higher) is requested, the handler attempts LSP enrichment via `textDocument/hover` to populate the card's `lsp` field with hover documentation, type information, and deprecation status. If the language server is unavailable, the card degrades gracefully to a Tree-sitter-only extraction with provenance `"tree_sitter_degraded_semantic"`. `observe get-card` responses are cached per daemon process by `(path, content hash, language, detail level, line, column)`. Repeating the same request against an unchanged file revision reuses the cached card instead of reparsing the file. When the file contents change, Weaver invalidates stale cached revisions for that path and records a fresh `provenance.extracted_at` timestamp. Cache hits preserve the original extraction timestamp. When the operation cannot produce a card, the status is `"refusal"`: ```json { "status": "refusal", "refusal": { "reason": "unsupported_language", "message": "observe get-card: unsupported language for path /tmp/example.txt", "requested_detail": "structure" } } ``` On success, the status is `"success"` and the payload wraps a `SymbolCard` object: ```json { "status": "success", "card": { "card_version": 1, "symbol": { "symbol_id": "sym_abc123", "ref": { "uri": "file:///src/main.rs", "range": { "start": { "line": 10, "column": 0 }, "end": { "line": 42, "column": 1 } }, "language": "rust", "kind": "function", "name": "process_request", "container": "handlers" } }, "signature": { "display": "fn process_request(req: &Request) -> Response", "params": [{ "name": "req", "type": "&Request" }], "returns": "Response" }, "doc": { "docstring": "Processes an incoming request.", "summary": "Processes an incoming request.", "source": "tree_sitter" }, "attachments": { "doc_comments": ["Processes an incoming request."], "decorators": [], "normalized": { "decorators": [] }, "bundle_rule": "leading_trivia" }, "structure": { "locals": [{ "name": "result", "kind": "variable", "decl_line": 15 }], "branches": [{ "kind": "if", "line": 18 }] }, "metrics": { "lines": 33, "cyclomatic": 5 }, "provenance": { "extracted_at": "2026-03-03T12:34:56Z", "sources": ["tree_sitter"] } } } ``` Note: the `card.symbol.ref.range` uses 0-based line and column numbers in a half-open interval — `start` is inclusive and `end` is exclusive (i.e. `[start, end)`). This differs from the `--position` request flag, which accepts 1-indexed `LINE:COL` values. Card fields beyond identity are progressively included based on the detail level: - `minimal` — returns only the `symbol` and `provenance` fields. - `signature` — adds the `signature` block (for callable symbols) exposing the callable display string, parameters, and return type. Non-callable symbols (classes, variables, constants) may omit `signature` or structure it differently. - `structure` (default) — further adds `doc`, `structure`, and basic `metrics`. May include `attachments`. - `semantic` — attempts LSP enrichment via `textDocument/hover`. When the language server is available and supports hover, the card's `lsp` field is populated with hover documentation, type information, and deprecation status, and provenance includes `"lsp_hover"`. When LSP is unavailable, the card degrades to a Tree-sitter-only extraction with provenance `"tree_sitter_degraded_semantic"`. - `full` — currently degrades to a Tree-sitter-only card with explicit provenance markers; dependency edges and fan-in/out metrics are not yet included. #### observe graph-slice Syntax: ```sh weaver observe graph-slice --uri --position [OPTIONS] ``` Arguments: - `--uri` (required) — file URI of the source file containing the root symbol. Must start with `file://`. - `--position` (required) — 1-indexed `LINE:COL` position within the root symbol. - `--depth` (optional) — maximum traversal depth. Default: `2`. - `--direction` (optional) — traversal direction. One of `in`, `out`, or `both` (default). - `--edge-types` (optional) — comma-separated list of edge types to follow. Any combination of `call`, `import`, `config`. Default: all three. - `--min-confidence` (optional) — minimum edge confidence threshold between `0.0` and `1.0`. Default: `0.5`. - `--max-cards` (optional) — maximum number of cards in the budget. Default: `30`. - `--max-edges` (optional) — maximum number of edges in the budget. Default: `200`. - `--max-estimated-tokens` (optional) — maximum estimated token count in the budget. Default: `4000`. - `--entry-detail` (optional) — detail level for the entry card. One of `minimal`, `signature`, `structure` (default), `semantic`, or `full`. - `--node-detail` (optional) — detail level for neighbouring node cards. One of `minimal` (default), `signature`, `structure`, `semantic`, or `full`. Response: The response is a discriminated-union JSON envelope keyed on the `"status"` field. When `"status"` is `"refusal"`, the envelope carries a structured refusal explaining why a slice could not be produced. When `"status"` is `"success"`, the envelope contains the graph slice. When the operation cannot produce a slice, the status is `"refusal"`: ```json { "status": "refusal", "schema_version": "graph_slice.v1", "refusal": { "reason": "unsupported_language", "message": "observe graph-slice: unsupported language for 'notes.txt'" } } ``` On success, the response wraps the slice with constraints, cards, edges, and spillover metadata: ```json { "status": "success", "schema_version": "graph_slice.v1", "slice_version": 1, "entry": { "symbol_id": "sym_abc123" }, "constraints": { "depth": 2, "direction": "both", "edge_types": ["call", "import", "config"], "min_confidence": 0.5, "budget": { "max_cards": 30, "max_edges": 200, "max_estimated_tokens": 4000 }, "entry_detail": "structure", "node_detail": "minimal" }, "cards": [ { "card_version": 1, "symbol": { "symbol_id": "sym_abc123", "ref": { "uri": "file:///src/main.rs", "range": { "start": { "line": 10, "column": 0 }, "end": { "line": 42, "column": 1 } }, "language": "rust", "kind": "function", "name": "process_request", "container": "handlers" } }, "provenance": { "extracted_at": "2026-03-03T12:34:56Z", "sources": ["tree_sitter"] } } ], "edges": [], "spillover": { "truncated": false, "frontier": [] } } ``` The `constraints` object reflects the applied request parameters after defaults are resolved. The `cards` array contains the extracted symbol cards within the budget. For prototype archive roadmap item 7.2.1, Weaver builds a deterministic same-file slice: the entry card plus additional same-file symbol cards that fit within `budget.max_cards`. The `edges` array is therefore currently empty in runtime responses, while the stable schema already reserves the typed edge shape for later milestones. The `--max-edges` CLI flag is accepted for forward compatibility but has no runtime effect in 7.2.1; only `budget.max_cards` limits the number of cards produced. When traversal exceeds the budget, `spillover.truncated` is `true` and `spillover.frontier` lists candidate same-file symbols that were discovered but excluded. `spillover.truncated` may also be `true` while `spillover.frontier` is empty when the discovery cap, rather than excluded cards, caused truncation. The `discovery_cap_marks_spillover_truncated_when_card_budget_remains` test is the canonical behaviour: `spillover.frontier` is populated only for discovered candidate symbols excluded from the response, not for symbols that discovery limits prevented Weaver from enumerating. The stable edge schema is already locked even though runtime edges are deferred to later milestones. When present, every edge will carry a `resolution_scope` of `full_symbol_table`, `partial_symbol_table`, or `lsp`. #### observe grep Syntax: ```sh weaver observe grep --pattern --path ``` Optional flags: ```text --language ``` Human output: ```text match: :: "$NAME" ``` JSON payload: ```json {"matches":[{"start":[1,1],"captures":{"NAME":"foo"}}]} ``` #### verify diagnostics Syntax: ```sh weaver verify diagnostics --uri ``` Human output: ```text --> : | | | ^ ``` JSON payload: ```json {"diagnostics":[{"line":12,"column":5,"message":"..."}]} ``` #### act apply-patch Syntax: ```sh weaver act apply-patch < patch.diff ``` `act apply-patch` reads a Git-style patch stream from STDIN. The patch may include SEARCH/REPLACE blocks for modifications, `new file mode` hunks for file creation, or `deleted file mode` entries for deletions. Binary patches are rejected, and an empty STDIN payload is treated as an error by the CLI. JSON payload: ```json {"status":"ok","files_written":1,"files_deleted":0} ``` Failures return structured error envelopes on stderr and a non-zero exit status. Verification failures are rendered with the same human-readable output as other `act` commands when `--output human` is selected. The daemon rejects JSONL request lines larger than 1 MiB, so large patch streams should be split into multiple `act apply-patch` invocations. #### act apply-rewrite Syntax: ```sh weaver act apply-rewrite --pattern --replacement --path ``` Human output: ```text rewrite: (replacements 2) ``` JSON payload: ```json {"path":"","replacements":2,"changed":true} ``` #### act refactor Delegates a refactoring operation to a registered plugin. The plugin runs in a sandboxed process and produces a unified diff that is validated by the Double-Lock safety harness before any filesystem change is committed. Syntax: ```sh weaver act refactor --provider --refactoring --file --position [KEY=VALUE...] ``` Arguments: Table: act refactor command-line flags | Flag | Description | | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `--provider` | Required provider name for the registered plugin. Built-in values are `rope` for Python rename flows and `rust-analyzer` for Rust rename flows. | | `--refactoring` | Refactoring operation to request (currently `rename`). The handler maps `rename` to the `rename-symbol` capability contract internally. | | `--file` | Path to the target file (relative to workspace root). | | `--position` | 1-indexed `LINE:COL` position of the symbol used as the rename anchor. | | `KEY=VALUE` | Extra key-value arguments forwarded to the plugin. | The plugin receives the file content in-band as part of the JSONL request and does not need filesystem access. The daemon validates the resulting diff through both the syntactic (Tree-sitter) and semantic (LSP) locks before writing to disk. A plugin response that claims success but does not carry diff output is refused as a failure: Weaver exits with status `1`, prints `act refactor failed: plugin succeeded but did not return diff output`, and leaves the filesystem unchanged. For the built-in actuators, `rename` requires `--position ` and `new_name=`. `weaverd` requires all four top-level flags in one request and rejects incomplete invocations before plugin resolution, file I/O, or backend startup. The legacy `offset=` form is accepted only as a deprecated compatibility path and will be removed in a future release. When `offset=` is supplied without `--position`, `weaverd` writes the following warning to stderr before processing the request: ```text Warning: 'offset=' is deprecated; use '--position LINE:COL' instead. ``` See the [rename position migration guide](weaver-act-refactor-rename-position-migration-guide.md) for upgrade examples. ### Parameter semantics and valid values The `act refactor` handler requires `--provider`, `--refactoring`, `--file`, and `--position`, then forwards any additional `KEY=VALUE` pairs to the selected plugin. Table: act refactor parameter semantics and validation | Parameter | Meaning | Valid values | Failure conditions | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `--provider` | Provider to use for the refactoring request. | Registered actuator name such as `rope` or `rust-analyzer`. | Missing flag, missing value, or unknown provider name causes failure. | | `--refactoring` | Refactoring operation requested from the plugin. The handler maps `rename` to the `rename-symbol` capability contract before forwarding to the plugin. | Currently only `rename` is implemented by built-in `rope` and `rust-analyzer` plugins. | Missing flag, missing value, or unsupported operation name (for example `extract_method`) causes failure. | | `--file` | Target file to load and refactor. | Workspace-relative path to an existing readable file (for example `src/main.py`). | Missing flag, missing value, absolute paths, parent traversal (`..`), or unreadable/missing files cause failure. | | `--position` | Symbol occurrence used as the rename anchor. | 1-indexed `LINE:COL` value, counting Unicode characters for the column. | Missing flag, malformed value, zero line or column, or a position outside the file causes failure. | | `new_name` | New symbol name used by `rename`. | Non-empty string value. | Missing key, non-string value, or empty/whitespace-only value causes failure. | | `offset` | Deprecated compatibility spelling for older rename invocations. | Non-negative UTF-8 byte offset. Prefer `--position`. | Cannot be combined with `--position`; malformed values are rejected by the daemon before plugin execution. | The daemon converts `--position` to any provider-specific offset required by the current built-in plugins. Byte offsets are an internal compatibility detail, not the canonical command interface. ### Expected behaviour of the worked examples Both examples follow the same execution pipeline: 1. `weaverd` parses `--provider`, `--refactoring`, `--file`, and `--position`. 2. It validates that `--provider` is a known actuator name and that `--refactoring` is a supported user-facing operation. 3. It maps `rename` to `rename-symbol`, infers the target language from the path, and validates the explicit provider against that capability request. 4. It emits a structured `CapabilityResolution` record describing that routing decision. 5. The file content is read from the workspace and sent to the plugin in-band. 6. The plugin executes `rename-symbol` using `position` and `new_name`. 7. The plugin returns a unified diff for the modified file. 8. Weaver validates the diff via the Double-Lock safety harness (syntax then semantic checks). 9. If validation passes, Weaver writes the file atomically and returns: `{"files_deleted":0,"files_written":1,"status":"ok"}`. When required flags are missing, `act refactor` returns one deterministic actionable error instead of failing one flag at a time: ```text invalid arguments: act refactor requires --provider , --refactoring , --file , and --position Valid alternatives: - Providers: rope, rust-analyzer - Refactorings: rename Next command: weaver act refactor --provider rope --refactoring rename --file path/to/file.py --position 1:1 new_name=renamed_symbol ``` When validation fails, parameters are invalid, or the plugin reports an error, the command exits non-zero and leaves the filesystem unchanged. A plugin response that reports success without a `Diff` payload is treated the same way: Weaver refuses the response, exits with status `1`, and does not touch the filesystem. Worked examples: - Python rename with explicit `rope` provider: ```sh weaver --output json act refactor \ --provider rope \ --refactoring rename \ --file src/main.py \ --position 1:5 \ new_name=renamed_symbol ``` Example routing rationale emitted before the final success payload: ```json { "status": "ok", "type": "CapabilityResolution", "details": { "capability": "rename-symbol", "language": "python", "requested_provider": "rope", "selected_provider": "rope", "selection_mode": "explicit_provider", "outcome": "selected", "candidates": [ { "provider": "rope", "accepted": true, "reason": "matched_language_and_capability" }, { "provider": "rust-analyzer", "accepted": false, "reason": "unsupported_language" } ] } } ``` Example final result: ```json {"files_deleted":0,"files_written":1,"status":"ok"} ``` - Rust rename with explicit `rust-analyzer` provider: ```sh weaver --output json act refactor \ --provider rust-analyzer \ --refactoring rename \ --file src/main.rs \ --position 1:4 \ new_name=renamed_name ``` Example final result: ```json {"files_deleted":0,"files_written":1,"status":"ok"} ``` The daemon ships with default actuator registrations: - `rope` for Python (`timeout_secs = 30`, `capabilities = ["rename-symbol"]`) - `rust-analyzer` for Rust (`timeout_secs = 60`, `capabilities = ["rename-symbol"]`) By default, it expects plugin executables at: - `/usr/bin/weaver-plugin-rope` - `/usr/bin/weaver-plugin-rust-analyzer` Override these paths with: ```sh WEAVER_ROPE_PLUGIN_PATH=/absolute/path/to/weaver-plugin-rope WEAVER_RUST_ANALYZER_PLUGIN_PATH=/absolute/path/to/weaver-plugin-rust-analyzer ``` The override path is resolved to an absolute path at daemon startup. If the plugin executable cannot be launched, `act refactor` returns a structured failure and does not modify the filesystem. The built-in rust-analyzer plugin now declares the same capability contract as rope for rename flows, even though the CLI continues to accept `--refactoring rename`. In human-readable output mode, Weaver renders the routing rationale as concise text instead of raw JSON, for example: ```text rename-symbol explicit_provider for python: selected rope (selected) requested provider: rope candidate accepted: rope (matched_language_and_capability) candidate rejected: rust-analyzer (unsupported_language) ``` ## Plugin system The `weaver-plugins` crate provides the plugin orchestration layer that enables `weaverd` to delegate specialist tasks to external tools running in sandboxed processes. ### Plugin categories Plugins are categorized as either **sensors** or **actuators**: - **Sensors** provide data to the intelligence engine (e.g. `jedi` for Python static analysis). They produce structured JSON output. - **Actuators** perform actions on the codebase (e.g. `rope` for Python refactoring, `srgn` for structural rewriting). They produce unified diffs. ### Plugin manifest Each plugin is described by a manifest containing: | Field | Description | | -------------- | ------------------------------------------------------ | | `name` | Unique plugin identifier (e.g. `rope`). | | `version` | Plugin version string. | | `kind` | `sensor` or `actuator`. | | `languages` | List of supported languages (case-insensitive). | | `executable` | Absolute path to the plugin binary. | | `args` | Default arguments passed to the executable (optional). | | `timeout_secs` | Maximum execution time in seconds (default: 30). | ### IPC protocol Plugins communicate with the broker via a single-line JSONL exchange over standard I/O: 1. The broker writes one JSONL request line to the plugin's stdin and closes stdin. 2. The plugin writes one JSONL response line to stdout and exits. 3. Plugin stderr is captured for diagnostic logging but is not part of the protocol. File content is passed in-band as part of the request body, so sandboxed plugins do not need filesystem access. ### Plugin registry The daemon maintains a `PluginRegistry` that stores validated plugin manifests keyed by name. Plugins can be looked up by name, kind, language, or a combination thereof (e.g. "find all actuator plugins for Python"). For the current actuator rollout, `weaverd` registers: - `rope` - kind: `actuator` - language: `python` - capabilities: `["rename-symbol"]` - executable: `/usr/bin/weaver-plugin-rope` (or `WEAVER_ROPE_PLUGIN_PATH`) - timeout: `30s` - `rust-analyzer` - kind: `actuator` - language: `rust` - capabilities: `["rename-symbol"]` - executable: `/usr/bin/weaver-plugin-rust-analyzer` (or `WEAVER_RUST_ANALYZER_PLUGIN_PATH`) - timeout: `60s` ### Plugin capabilities Actuator plugins declare the capabilities they support in their manifest. The daemon uses these declarations to route operations to the correct plugin based on both the requested capability and the target language. #### Capability identifiers The following capability identifiers are defined: Table: Code transformation capabilities. | Identifier | Description | | ------------------- | ---------------------------------------------------- | | `rename-symbol` | Rename a symbol across a codebase. | | `extricate-symbol` | Move a symbol to a different module or file. | | `extract-method` | Extract a code region into a new function or method. | | `replace-body` | Replace the body of a function or method. | | `extract-predicate` | Extract a conditional expression into a predicate. | #### The `rename-symbol` capability contract The `rename-symbol` capability is the first fully specified contract. Plugins that declare this capability must accept requests containing three required fields in the `arguments` map. The built-in `rope` and `rust-analyzer` rename plugins are validated against the same shared contract fixtures. Request and response checks therefore stay aligned across Python and Rust rename flows. Table: Required fields for `rename-symbol` requests. | Field | Type | Description | | ---------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `uri` | string | File URI of the symbol to rename. | | `position` | string | Internal position value used by the daemon-to-plugin request. The current built-in plugins receive a UTF-8 byte offset converted from CLI `--position`. | | `new_name` | string | The new name for the symbol (must be non-empty). | Successful responses must contain a `Diff` output with a unified diff patch. If a plugin reports success with any other output shape, Weaver refuses the response, exits with status `1`, and makes no filesystem changes. Failed responses may include diagnostics with an optional `reason_code` field. #### Contract versioning Each capability contract carries a version (`major.minor`). Contracts with the same major version are considered compatible. The current `rename-symbol` contract version is `1.0`. #### Refusal reason codes When a plugin cannot perform a requested operation, it returns a failure response with diagnostics. Each diagnostic may include a `reason_code` for programmatic matching: Table: Refusal reason codes for plugin diagnostics. | Reason code | Meaning | | ------------------------- | ---------------------------------------------------- | | `symbol_not_found` | The target symbol could not be located. | | `macro_generated` | The symbol is generated by a macro. | | `ambiguous_references` | Multiple candidate symbols match the position. | | `unsupported_language` | The plugin does not support the target language. | | `incomplete_payload` | Required fields are missing from the request. | | `name_conflict` | The new name conflicts with an existing symbol. | | `operation_not_supported` | The plugin does not support the requested operation. | Reason codes are stable identifiers intended for automation. They appear in the JSON diagnostic payload alongside the human-readable `message` field. #### Manifest capability declarations Actuator plugins declare capabilities in their manifest: ```toml name = "rope" version = "1.0.0" kind = "actuator" languages = ["python"] executable = "/usr/bin/weaver-plugin-rope" capabilities = ["rename-symbol"] ``` Sensor plugins must not declare any capabilities. The registry validates this constraint during registration and rejects manifests that violate it. The daemon uses capability declarations to select plugins. For example, when routing a `rename-symbol` request for Python, the daemon queries the registry for actuator plugins that declare `rename-symbol` and support the `python` language. For `act refactor`, operators must still pass `--provider` explicitly, using `rope` for Python rename flows or `rust-analyzer` for Rust rename flows. The daemon refuses deterministically for unsupported languages, unknown providers, and explicit provider/language mismatches. ### Safety harness integration Actuator plugin output (unified diffs) flows through the same Double-Lock safety harness used by `act apply-patch`. Changes are validated by both the syntactic (Tree-sitter) and semantic (LSP) locks before any filesystem write is committed. If verification fails, the filesystem is left untouched and a structured error is returned to the caller. ## Language server capability detection The `weaver-lsp-host` crate initializes the LSP servers for Rust, Python, and TypeScript and records which core requests each server advertises: `textDocument/definition`, `textDocument/references`, diagnostics, and call hierarchy (`textDocument/prepareCallHierarchy` plus incoming/outgoing calls). These advertised capabilities are merged with any overrides provided via `capability_overrides` in `weaver-config`. `force` directives allow a request even when the server claims not to support it, while `deny` directives block the request regardless of the server report. When a request is rejected, the error explains whether the feature was disabled by configuration or simply absent from the server so operators and agents can adjust their plans without guesswork. ### Process-based language server adapters The daemon spawns real language server processes for each language and communicates via JSON-RPC 2.0 over stdio. The following binaries must be available in `PATH`: | Language | Binary | Example invocation | | ---------- | --------------- | ------------------ | | Rust | `rust-analyzer` | `rust-analyzer` | | Python | `pyrefly` | `pyrefly lsp` | | TypeScript | `tsgo` | `tsgo --lsp` | When a language server binary is not found, the daemon returns a clear error message identifying the missing command. This allows operators to install the required tooling before retrying. Example: ```text failed to spawn rust language server: command 'rust-analyzer' not found ``` Language servers are initialized lazily when the first operation for that language is requested. The daemon sends the LSP `initialize` handshake followed by `initialized`, then routes subsequent requests through the established session. Graceful shutdown is performed when the daemon stops: a `shutdown` request is sent to each running language server, followed by an `exit` notification. If a server does not exit within five seconds, it is terminated forcefully. ## Double-Lock safety harness All `act` commands pass through a "Double-Lock" safety harness before any changes are committed to the filesystem. This verification layer ensures that agent-generated modifications do not corrupt the codebase by introducing syntax errors or type mismatches. ### Two-phase verification The harness validates proposed edits in two sequential phases: 1. **Syntactic Lock**: Each modified file is parsed to ensure it produces a valid syntax tree. Structural errors such as unbalanced braces, missing semicolons, or malformed declarations are caught at this stage. Files that fail parsing are rejected immediately, and the filesystem remains untouched. 2. **Semantic Lock**: If the syntactic lock passes, the modified content is submitted to the configured language server. The daemon requests fresh diagnostics and compares them against the pre-edit baseline. Any new errors or high-severity warnings cause the semantic lock to fail. Only when both locks pass are the changes atomically written to disk. ### In-memory application Edits are first applied to in-memory copies of the affected files. The original content is preserved until both verification phases succeed. This allows the harness to reject problematic changes without leaving partially written files on disk. ### Document sync notifications The semantic lock now opens in-memory documents on the language server using `textDocument/didOpen`, applies updates with `textDocument/didChange`, and closes them with `textDocument/didClose` once diagnostics are collected. This lets the server validate the modified content at the real file URI without writing temporary files, so cross-file imports resolve as usual. ### Atomic commits When both locks pass, the harness writes each modified file atomically by creating a temporary file and renaming it into place. This guarantees that a crash or power loss during the commit phase does not leave files in a corrupted intermediate state. ### Error reporting When verification fails, the harness returns a structured error describing: - **Lock phase**: Whether the failure occurred during syntactic or semantic validation. - **Affected files**: Paths to the files that triggered the failure. - **Locations**: Optional line and column numbers pinpointing each issue. - **Messages**: Human-readable descriptions of what went wrong. Agents can use this information to diagnose problems and regenerate corrected edits. The structured format also enables tooling to present failures in IDE integrations or CI pipelines. ### Tree-sitter syntactic lock The syntactic lock is powered by the `weaver-syntax` crate, which integrates Tree-sitter parsers for Rust, Python, and TypeScript. When validating a file, the lock parses the content and inspects the resulting syntax tree for ERROR nodes. Files containing structural errors—such as unbalanced braces, missing semicolons, or malformed declarations—are rejected before the semantic lock runs. Files with extensions not recognized by any configured parser are skipped (pass through) to avoid blocking edits to configuration files, documentation, or other non-code artefacts. The validation reports each failure with: - **Path**: The file that failed validation. - **Line and column**: The position of the first syntax error. - **Message**: A human-readable description (typically "syntax error"). This fast, local check catches many common agent mistakes without needing to contact a language server. ### Pattern matching and rewriting The `weaver-syntax` crate also provides a structural pattern matching engine inspired by ast-grep. Patterns use metavariables (`$VAR` for single captures, `$$$VAR` for multiple) to match and capture portions of the syntax tree. This enables the future `observe grep` and `act apply-rewrite` commands to perform precise, AST-aware search and transformation across the codebase. The engine currently supports Rust, Python, and TypeScript. ## Sempai query engine The `sempai` crate provides a Semgrep-compatible query engine backed by Tree-sitter for semantics-aware code pattern matching. It is organized as three workspace crates: - **`sempai_core`** — canonical data model including language identifiers (`Language`), source spans (`Span`, `LineCol`), match results (`Match`), capture bindings (`CaptureValue`, `CapturedNode`), structured diagnostics (`DiagnosticReport`, `Diagnostic`, `DiagnosticCode`), and engine configuration (`EngineConfig`). - **`sempai_yaml`** — Semgrep-compatible YAML parser built on `saphyr` and `serde-saphyr`, exposing schema-aligned rule models for legacy and v2 search principals plus parser-time handling for extract, join, and taint rules. - **`sempai`** — stable facade crate that re-exports all public types from `sempai_core` and provides the `Engine` entrypoint. The `Engine` struct exposes three methods for query compilation and execution: - `compile_yaml(yaml)` — compiles a YAML rule file into query plans. - `compile_dsl(rule_id, language, dsl)` — compiles a one-liner domain-specific language (DSL) expression. - `execute(plan, uri, source)` — executes a compiled plan against a source snapshot. `compile_yaml(yaml)` now performs real YAML parsing plus a mode-aware validation pass. Malformed YAML returns `E_SEMPAI_YAML_PARSE`, and schema-shape failures such as missing required rule keys return `E_SEMPAI_SCHEMA_INVALID`, both using the shared structured diagnostic payload with `primary_span` locations when available. After parsing succeeds, `compile_yaml(yaml)` normalizes search rules into canonical query plans: - Valid `search` rules are normalized into the canonical `Formula` model defined in `sempai_core::formula`. Both legacy (`pattern*`) and v2 (`match`) syntaxes are lowered into a shared representation. - Normalized formulas are validated for semantic correctness: `E_SEMPAI_INVALID_NOT_IN_OR` is emitted when negated terms appear in disjunction branches, and `E_SEMPAI_MISSING_POSITIVE_TERM_IN_AND` is emitted when conjunctions contain only constraint formulas. - For each valid search rule and declared language, a `QueryPlan` is returned containing the normalized formula and metadata. - Valid `extract`, `taint`, `join`, and unknown future mode strings fail deterministically with `E_SEMPAI_UNSUPPORTED_MODE`. - Compatibility-only `r2c-internal-project-depends-on` rules normalize to a degenerate formula that will never match real code. Unsupported-mode diagnostics point at the rule's `mode` field when that span is available. Semantic validation errors include accurate `primary_span` locations when available from the parser. ### Migration notes Upgrading from v0.1? See the [Sempai v0.1→v0.2 migration guide](sempai-v0.1-to-v0.2-migration-guide.md). `compile_dsl(...)` and `execute(...)` still return "not implemented" diagnostics. They will be wired to the DSL parser and Tree-sitter backend as those components are delivered in subsequent roadmap phases. All error conditions are reported through `DiagnosticReport`, which carries stable diagnostic codes suitable for programmatic consumption. Stub methods return the `NOT_IMPLEMENTED` code where implementation is still pending, while the YAML parser now emits real `E_SEMPAI_*` codes for malformed or invalid rule files. Diagnostics include a code, message, `primary_span` (or `null` when unavailable), and supplementary notes. Both parser-path and validator-path diagnostics use the same JSON schema, and snapshot tests lock this contract.