@karmaniverous/jeeves-watcher-openclaw 0.18.0-0 → 0.18.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/README.md CHANGED
@@ -11,15 +11,19 @@ A running [jeeves-watcher](https://www.npmjs.com/package/@karmaniverous/jeeves-w
11
11
  The plugin is a standard OpenClaw plugin with no installer of its own. On a Jeeves box, `jeeves install` (from [`@karmaniverous/jeeves`](https://www.npmjs.com/package/@karmaniverous/jeeves) 0.6+) installs it and writes its config:
12
12
 
13
13
  ```bash
14
- jeeves install watcher --config-root /srv/jeeves/config
14
+ jeeves install --workspace /srv/jeeves/workspace --config-root /srv/jeeves/config watcher
15
15
  ```
16
16
 
17
+ Without `--workspace`, `jeeves install` takes the current folder as the workspace and writes its platform files there.
18
+
17
19
  Or install it directly with the OpenClaw CLI and set the config yourself:
18
20
 
19
21
  ```bash
20
- openclaw plugins install npm:@karmaniverous/jeeves-watcher-openclaw@<version> --pin --accept-capabilities
22
+ openclaw plugins install npm:@karmaniverous/jeeves-watcher-openclaw@<version> --pin --accept-capabilities --force
21
23
  ```
22
24
 
25
+ `--force` confirms the npm source and replaces an installed version; without it a non-interactive install is cancelled.
26
+
23
27
  The plugin registers an always-in-context rule set through OpenClaw's `before_prompt_build` hook, so it needs `plugins.entries.jeeves-watcher-openclaw.hooks.allowConversationAccess: true`. `jeeves install` / `jeeves update` grant this automatically (the hook is declared in `package.json` under `jeeves.conversationHooks`).
24
28
 
25
29
  Restart the OpenClaw gateway to apply changes.
package/dist/index.js CHANGED
@@ -31098,7 +31098,7 @@ object$1({
31098
31098
  * The jeeves-watcher skill (descriptor `skill`), compiled from `skills/jeeves-watcher/SKILL.md` by `npm run skill`. Generated; do not edit.
31099
31099
  */
31100
31100
  /** Contents of the jeeves-watcher `SKILL.md`. */
31101
- const WATCHER_SKILL = '---\nname: jeeves-watcher\ndescription: >\n Semantic search of the indexed archive: recall prior context, find documents, scan, enrich metadata, watcher config, reindex, indexing issues, file history.\n---\n\n# jeeves-watcher — Search, Discovery & Administration\n\n## Service Architecture\n\nThe watcher is an HTTP API running as a background service (typically NSSM on Windows, systemd on Linux).\n\n**Default port:** 1936 (configurable via `api.port` in watcher config) **Non-default port:** If the watcher runs on a different port, the user must set `plugins.entries.jeeves-watcher-openclaw.config.apiUrl` in `openclaw.json`. The plugin cannot auto-discover a non-default port.\n\n**API key:** If the watcher sets `api.apiKey`, every route except `GET /status` and `GET /openapi.json` needs `Authorization: Bearer <key>`. The tools send it from `plugins.entries.jeeves-watcher-openclaw.config.apiKey` (or the `JEEVES_WATCHER_API_KEY` env var). From `watcher_config`, `watcher_config_apply`, `watcher_search`, `watcher_scan` and `watcher_show_vcs_file`, a missing or wrong key comes back as `HTTP 401: {"error":"Unauthorized","code":"unauthorized"}` (see [Error Handling](#error-handling) for the fix). From the other tools it reads `HTTP 401 unauthorized: the watcher API requires an API key. Provide it via plugins.entries.jeeves-watcher-openclaw.config.apiKey or the JEEVES_WATCHER_API_KEY env var.` (or, when a key was sent, `... rejected the API key. Check ... against the service\'s api.apiKey.`). When calling the API directly, add the header yourself. Never echo the key.\n\n**Tool errors:** Tool error text is prefixed `Error: `. Every `watcher_*` tool is built from the watcher\'s endpoint catalog and reports a non-2xx response as `HTTP <status>: <body>`, with the server\'s JSON body verbatim: read its `code` (and `details` or `issues`) there, e.g. `HTTP 400: {"error":"Validation failed","code":"invalid_request",...}` or `HTTP 503: {"error":"Scan in progress","code":"scan_in_progress"}`. Branch on the code, not the message. The tools reject parameters that don\'t match their schema before sending anything. Each call waits up to 120 s; a timeout does not cancel a state change on the service, so check the outcome (e.g. `watcher_check_vcs_exclusion` after `watcher_exclude_vcs_path`) before retrying. An unreachable service reads `Service not reachable at <url>.` with a hint to set `apiUrl`. (`watcher_service` comes from the shared Jeeves core toolset and words its own errors.)\n\n**Health check:** `GET /status` returns `name`, `version`, `uptime`, `status` (`healthy`/`degraded`/`unhealthy`), and a `health` object containing `collection` (`name`, `pointCount`, `dimensions`, `systemIndexes`), `reindex` progress, `initialScan` progress, `embedding` (provider health), and (when version tracking is on) `vcs` (`roots`, `pending`, `trippedRoots`, `oldestTrippedAt`). `status` is `degraded` (still HTTP 200) when embeddings are failing or a version-control root has tripped.\n\n**Mental model:** The `watcher_*` tools are thin HTTP wrappers. Each tool call translates to an HTTP request to the watcher API. When tools are available, use them. When they\'re not (e.g., different session, plugin not loaded), you can hit the API directly. Replace `<PORT>` below with the configured port (default 1936; check `plugins.entries.jeeves-watcher-openclaw.config.apiUrl` in `openclaw.json` if overridden), and add `-H "Authorization: Bearer <key>"` when the service sets `api.apiKey`:\n\n```\n# Health check\ncurl http://127.0.0.1:<PORT>/status\n\n# Search\ncurl -X POST http://127.0.0.1:<PORT>/search \\\n -H "Content-Type: application/json" \\\n -d \'{"query": "search text", "limit": 5}\'\n\n# Query config\ncurl http://127.0.0.1:<PORT>/config\n```\n\n**Key endpoints:**\n\n| Endpoint | Method | Purpose |\n| --- | --- | --- |\n| `/status` | GET | Health check, uptime, collection stats |\n| `/openapi.json` | GET | OpenAPI 3.1 document of the catalog routes (no key needed) |\n| `/search` | POST | Semantic search (main query interface) |\n| `/config` | GET | Public resolved config (allow-listed keys plus `issues`); optional `?path=<jsonpath>` filter |\n| `/config/validate` | POST | Validate candidate config |\n| `/config/apply` | POST | Apply config changes |\n| `/reindex` | POST | Trigger reindex |\n| `/metadata` | POST | Enrich document metadata (file path or source reference) |\n| `/scan` | POST | Filter-only point query (no embeddings) |\n| `/walk` | POST | Watched-file listing with glob intersection |\n| `/issues` | GET | Runtime processing failures (file paths and source references) |\n| `/documents` | POST | Notify changed source documents (`{ source, rule }`) |\n| `/documents/delete` | POST | Delete source documents by reference or filter |\n| `/rules/register` | POST | Register virtual inference rules (auto-triggers rules reindex) |\n| `/rules/unregister` | DELETE | Remove virtual rules by source (`{ source }` body) |\n| `/rules/unregister/:source` | DELETE | Remove virtual rules by source (parameterized) |\n| `/rules/reapply` | POST | Re-apply rules to indexed files matching `globs` |\n| `/points/delete` | POST | Delete points matching a Qdrant filter |\n| `/render` | POST | Render a document (file path or source reference) through its rule |\n| `/search/facets` | GET | Get search facet definitions and values (`?rules=a,b` scopes them to those rules) |\n| `/search/facets/counts` | POST | Count values of indexed fields over a filter, each document once; counts are approximate (`{ fields, filter?, rules?, limit? }`); `{ min, max }` for number/date fields; `503 not_available` while its system indexes build or past its 10 s budget |\n| `/rebuild-metadata` | POST | Rebuild the enrichment store from Qdrant file point payloads |\n| `/config/schema` | GET | Get config JSON schema |\n| `/config/match` | POST | Test file paths against inference rules and watch scope |\n| `/vcs/*` | GET/POST | Version tracking (`status`, `history`, `show`, `diff`, `check-exclusion`, `revert`, `exclude`) |\n\n**If the watcher is unreachable:** Check the service status (`nssm status jeeves-watcher` on Windows), check the configured port in the watcher config file, and check logs for startup errors.\n\n## Theory of Operation\n\nYou have access to a **semantic archive** of your human\'s working world. Documents, messages, tickets, notes, code, and other artifacts are indexed, chunked, embedded, and searchable. This is your long-term recall for anything beyond the current conversation.\n\n**When to reach for the watcher:**\n\n- **Someone asks about something that happened.** A meeting, a decision, a conversation, a ticket, a message. You weren\'t there, but the archive was. Search it.\n- **You need context you don\'t have.** Before asking the human "what\'s the status of X?", search for X. The answer is probably already indexed.\n- **You\'re working on something and need background.** Prior discussions, related records, relevant documents. Search by topic, filter by domain.\n- **You need to verify something.** Don\'t guess from stale memory. Search for the current state.\n- **You want to connect dots across domains.** The same topic might appear in multiple domains. A single semantic search surfaces all of them.\n\n**When NOT to use it:**\n\n- You already have the information in the current conversation\n- The question is about general knowledge, not the human\'s specific context\n- The watcher is unreachable (fall back to filesystem browsing)\n\n**The principle:** Memory-core is your curated highlights. The watcher archive is your perfect recall. Use memory first for speed and signal, but never let its narrow scope be the ceiling of what you can remember.\n\n## ⚠️ Embedding Cost — Hard Behavioral Gate\n\n**The watcher embeds every file it sees.** Embedding is the most expensive operation in the pipeline — each file triggers a Gemini API call. The index may contain hundreds of thousands of points. Any action that causes the watcher to process a large number of files has real, potentially significant cost.\n\n**STOP and ask for a human decision before:**\n\n- **Renaming, moving, or reorganizing watched directories.** The watcher has **move detection** enabled by default (`watch.moveDetection.enabled: true`). When a file is moved, the change source reports an unlink and an add; the `MoveCorrelator` buffers the unlink, matches it with the add via content hash, and processes it as a zero-embedding move (point ID remap, metadata re-inference, enrichment migration). Under the default parcel source, a renamed or moved directory is paired as a whole (at least 90% of its files at the same relative path and size) and each file becomes such a move. **No re-embedding cost for moves.** However, if a rename is blocked (e.g., by a file lock from the watcher or another service), **do NOT work around it by creating new directories and copying/moving files**. That creates duplicate embeddings at the new paths while the old paths still exist. Instead: stop the blocking service, perform the rename, restart.\n- **Changing `watch.paths` to add large directory trees.** Adding a new watch root triggers initial indexing of every matching file under it.\n- **Running `scope: "full"` reindex.** Every watched file is re-extracted (a file whose text to embed changed, or that was never indexed, is re-embedded) and every indexed source document is retrieved through its callback and always re-embedded. Use `scope: "rules"` for inference rule changes (no file re-embedding; only metadata reapplication).\n- **Any bulk file operation** (mass copy, mass move, template-based file generation) under watched paths.\n\n**The right instinct when blocked:** If a filesystem operation fails because a service has a lock, the correct response is to stop the service, do the operation, and restart — NOT to find a creative workaround. Creative workarounds in the presence of a live watcher are how you accidentally trigger re-embedding of tens of thousands of files.\n\n**Cost context:** A single file embedding costs a Gemini API call. 1,000 unnecessary embeddings is a noticeable cost. 100,000 unnecessary embeddings (e.g., copying an entire domain directory to a new path) is a billing event worth flagging.\n\n## Plugin Installation\n\nThe plugin is a standard OpenClaw plugin. On a Jeeves box, `jeeves install watcher --config-root <path>` installs it and writes its config; otherwise use `openclaw plugins install npm:@karmaniverous/jeeves-watcher-openclaw@<version> --pin --accept-capabilities` and set `plugins.entries.jeeves-watcher-openclaw.config` yourself. Restart the gateway to load it.\n\n`configRoot` has no default. Only `watcher_service` with `action: "install"` needs it; other `watcher_service` actions and all HTTP API tools work without it. If `watcher_service install` returns "configRoot not configured", set `configRoot` in the plugin config or the `JEEVES_CONFIG_ROOT` env var.\n\n## Quick Start (Existing Deployment)\n\nIf the watcher service is already running and healthy:\n\n1. **Search** — use `watcher_search` with a natural language query and optional metadata filters\n2. **Read source** — use `read` (standard file read) with `file_path` from search results for full document content. Source documents (points carrying `source`, such as `app://chat/thread/42`, and no `file_path`) are not files: their content comes from the source\'s callback (`POST /render` with `{ "source": "<ref>" }`).\n\n## Bootstrap (First-Time Setup)\n\nWhen the plugin loads and the watcher service is NOT yet set up, drive the entire setup proactively. The user should be able to install the plugin with nothing else in place and the bootstrap process gets them to a working system.\n\n**The agent drives this process.** Don\'t hand the user CLI commands and wait. Check each prerequisite, explain what\'s needed, execute what you can, and prompt the user only for decisions that require human judgment.\n\n### Step 1: Check Node.js\n\nVerify Node.js is installed and version ≥ 24.22.2:\n\n```bash\nnode --version\n```\n\nIf missing or too old, guide the user to install Node.js 24.22.2 or later from https://nodejs.org or via their package manager.\n\n### Step 2: Install Qdrant\n\nCheck if Qdrant is already running:\n\n```bash\ncurl -s http://localhost:6333/healthz\n```\n\nIf not running, install it. **Prefer native installation** (especially on cloud instances where Docker may not be available):\n\n**Linux (recommended for servers):**\n\n```bash\n# Download and install binary\ncurl -L https://github.com/qdrant/qdrant/releases/latest/download/qdrant-x86_64-unknown-linux-musl.tar.gz -o /tmp/qdrant.tar.gz\nsudo tar xzf /tmp/qdrant.tar.gz -C /usr/local/bin/\n\n# Create qdrant user and directories\nsudo useradd -r -s /bin/false qdrant\nsudo mkdir -p /var/lib/qdrant/storage /var/lib/qdrant/snapshots /etc/qdrant\nsudo chown -R qdrant:qdrant /var/lib/qdrant\n\n# Create config\nsudo tee /etc/qdrant/config.yaml > /dev/null <<EOF\nstorage:\n storage_path: /var/lib/qdrant/storage\n snapshots_path: /var/lib/qdrant/snapshots\nservice:\n host: 0.0.0.0\n http_port: 6333\n grpc_port: 6334\nEOF\n\n# Create systemd service\nsudo tee /etc/systemd/system/qdrant.service > /dev/null <<EOF\n[Unit]\nDescription=Qdrant Vector Database\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=/usr/local/bin/qdrant --config-path /etc/qdrant/config.yaml\nWorkingDirectory=/var/lib/qdrant\nRestart=always\nUser=qdrant\n\n[Install]\nWantedBy=multi-user.target\nEOF\nsudo systemctl daemon-reload\nsudo systemctl enable --now qdrant\n```\n\n**Windows:**\n\n```powershell\n# Download from GitHub releases page\n# https://github.com/qdrant/qdrant/releases\n# Extract and run, or register as NSSM service:\nnssm install Qdrant <path-to-qdrant.exe>\nnssm start Qdrant\n```\n\n**Docker (fallback, if available):**\n\n```bash\ndocker run -d -p 6333:6333 -v qdrant_data:/qdrant/storage qdrant/qdrant\n```\n\nAfter installation, verify:\n\n```bash\ncurl -s http://localhost:6333/healthz\n```\n\n### Step 3: Install Watcher Service\n\nInstall the watcher CLI globally:\n\n```bash\nnpm install -g @karmaniverous/jeeves-watcher\n```\n\nVerify:\n\n```bash\njeeves-watcher --version\n```\n\n### Step 4: Set Up Embedding Provider\n\nThe watcher uses Google Gemini for embeddings by default (`gemini-embedding-001`, 3072 dimensions).\n\nCheck for an existing API key:\n\n```bash\necho $GOOGLE_API_KEY # Linux/Mac\necho %GOOGLE_API_KEY% # Windows cmd\n$env:GOOGLE_API_KEY # PowerShell\n```\n\nIf not set, guide the user:\n\n1. Go to https://aistudio.google.com/apikey\n2. Create an API key (free tier supports 1,000 embedding requests/minute)\n3. Set it as a persistent environment variable:\n - **Linux:** Add `export GOOGLE_API_KEY=<key>` to `~/.bashrc` or `~/.profile`\n - **Windows:** `setx GOOGLE_API_KEY "<key>"` (new shell sessions only) or set via System Properties → Environment Variables\n - **macOS:** Add to `~/.zshrc` or use `launchctl setenv`\n\nVerify the key works by testing a Gemini API call:\n\n```bash\ncurl -s "https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent?key=$GOOGLE_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{"model":"models/gemini-embedding-001","content":{"parts":[{"text":"test"}]}}\'\n```\n\nA successful response contains an `embedding.values` array.\n\n### Step 5: Author Initial Config\n\nAsk the user these questions:\n\n- **What directories should the watcher index?** (e.g., `~/documents`, `~/projects`, a workspace path)\n- **What types of files matter?** (helps determine file extensions for watch globs)\n- **Are there directories to exclude?** (node_modules, .git, build outputs, etc.)\n\nThen generate a starter config file. Example minimal config:\n\n```json\n{\n "description": "Personal knowledge base indexing",\n "api": { "port": 1936 },\n "watch": {\n "paths": ["/home/user/documents/**/*.{md,txt,json,pdf,html,docx}"],\n "ignored": ["**/node_modules/**", "**/.git/**", "**/dist/**"]\n },\n "embedding": {\n "provider": "gemini",\n "model": "gemini-embedding-001",\n "dimensions": 3072,\n "apiKey": "${GOOGLE_API_KEY}",\n "chunkSize": 1000,\n "chunkOverlap": 200,\n "rateLimitPerMinute": 1000,\n "concurrency": 5\n },\n "vectorStore": {\n "url": "http://localhost:6333",\n "collectionName": "jeeves_archive"\n },\n "search": {\n "scoreThresholds": { "strong": 0.75, "relevant": 0.5, "noise": 0.25 },\n "hybrid": { "enabled": true }\n },\n "logging": { "level": "info" },\n "inferenceRules": []\n}\n```\n\nWrite the config to a sensible location (e.g., `~/.config/jeeves-watcher.config.json` on Linux, or alongside the user\'s workspace). Validate it against the config schema (offline; no running service needed) with:\n\n```bash\njeeves-watcher config validate -c <config-path>\n```\n\n### Step 6: Register and Start as a Service\n\n**The watcher should run as a persistent service, not a foreground process.**\n\n**Linux (systemd):**\n\n```bash\nsudo tee /etc/systemd/system/jeeves-watcher.service > /dev/null <<EOF\n[Unit]\nDescription=Jeeves Watcher - Filesystem Indexing Service\nAfter=network.target qdrant.service\n\n[Service]\nType=simple\nExecStart=$(which jeeves-watcher) start -c <config-path>\nWorkingDirectory=%h\nRestart=always\nEnvironment=GOOGLE_API_KEY=<key>\nUser=$USER\n\n[Install]\nWantedBy=multi-user.target\nEOF\nsudo systemctl enable --now jeeves-watcher\n```\n\n**Windows (NSSM):**\n\n```powershell\njeeves-watcher service install -c <config-path>\n# Or manually:\nnssm install jeeves-watcher (Get-Command jeeves-watcher).Source start -c <config-path>\nnssm set jeeves-watcher AppEnvironmentExtra GOOGLE_API_KEY=<key>\nnssm start jeeves-watcher\n```\n\nVerify the service started:\n\n```bash\ncurl -s http://127.0.0.1:1936/status\n```\n\n### Step 7: Verify Health\n\nCall `watcher_status` (or `curl http://127.0.0.1:1936/status`). Confirm:\n\n- Service is running\n- Qdrant collection exists with expected dimensions (3072)\n- Point count is increasing (initial indexing in progress)\n\nIf the point count is 0 after a minute, check `watcher_list_issues` for processing failures.\n\n### Step 8: Orientation\n\nOnce health is confirmed and initial indexing has started:\n\n1. Query `$.[\'description\',\'search\']` for the deployment\'s organizational strategy and score thresholds.\n2. Query `$.inferenceRules[*].[\'name\',\'description\']` for available record types.\n3. Report to the user: how many points indexed so far, which domains are available, estimated time to complete initial indexing (based on file count and embedding rate).\n\n### On Subsequent Sessions\n\nOn sessions after bootstrap is complete:\n\n1. Call `watcher_status` silently.\n2. Run the orientation queries silently.\n3. Only report if something changed (service down, point count dropped significantly, new domains appeared).\n\n**Key principle:** The agent drives discovery. The user shouldn\'t have to explain their archive to you — the archive explains itself through its config.\n\n## Tools\n\nThe plugin registers 18 tools: `watcher_status`, `watcher_config`, `watcher_config_apply`, `watcher_service`, `watcher_search`, `watcher_set_metadata`, `watcher_validate_config`, `watcher_reindex`, `watcher_scan`, `watcher_list_issues`, `watcher_walk`, and the seven version-control tools (see [Version Tracking](#version-tracking)).\n\n### `watcher_search`\n\nSemantic search over indexed documents (`POST /search`).\n\n- `query` (string, required) — natural language search query (non-empty)\n- `limit` (number, optional) — max results (positive integer), default 10\n- `offset` (number, optional) — skip N results for pagination (non-negative integer)\n- `filter` (object, optional) — Qdrant filter for metadata filtering\n\nReturns an array of `{ id, score, payload }` ordered by score (see [Search Result Shape](#search-result-shape)). A filter Qdrant rejects is `400 invalid_request`; an exhausted embedding budget is `503 embedding_unavailable`.\n\n### `watcher_set_metadata`\n\nSet or update metadata on a document (`POST /metadata`).\n\n- `path` (string, required) — file path of the document (absolute or watched path)\n- `metadata` (object, required) — key-value metadata to merge\n\nReturns `{ ok: true, matched_rules }`. If the document is busy (being indexed) for 10 s, the tool reports `503 not_available` with `Retry-After: 10` and changes nothing; retry after 10 s. The tool addresses files only; to enrich a source document, call `POST /metadata` directly with `{ "source": "<name>://<id>", "metadata": { ... } }` (exactly one of `path` or `source`).\n\n### `watcher_status`\n\nService health check (`GET /status`, never needs the API key). Returns `name`, `version`, `uptime`, `status`, and a `health` object: `collection` (`name`, `pointCount`, `dimensions`, and `systemIndexes`: the state of each index the service creates itself, e.g. `{ "chunk_index": "ready", "file_path": "ready", "matched_rules": "building" }`; `building`, `ready`, `conflict` or `failed`), `embedding` (`state` `ok` or `failing`, `lastSuccessAt`, `lastError` `{ at, code }`, `failingSince`, `backoffUntil`; codes only, never provider text), `reindex` (`active`; while active also `scope`, `startedAt`, `filesProcessed`, `totalFiles`), `initialScan`, and `vcs` when version tracking is on: `roots`, `pending` (changes not yet committed), `trippedRoots` (roots whose commits keep failing) and `oldestTrippedAt`.\n\nAfter a service restart, the `health.initialScan` field shows scan progress:\n\n- `active: true` — filesystem walk in progress; `filesMatched` and `filesEnqueued` grow until the initial walk (or chokidar\'s scan) completes\n- `active: false` with `completedAt`/`durationMs` — scan finished\n\nUse this to determine if the service is still initializing after a restart.\n\n### `watcher_config`\n\nQuery the effective runtime config via JSONPath (`GET /config`). Returns the full resolved merged document when no path is provided.\n\n- `path` (string, optional) — JSONPath expression\n\nThe document holds only the allow-listed keys `description`, `watch`, `configWatch`, `search`, `schemas`, `inferenceRules`, `mapHelpers`, `templateHelpers`, `maps`, `templates`, plus `issues` (the current issues). `api`, `sources`, `embedding`, `vectorStore` (and their keys) are never included. Each rule carries `source` (`config`, or the registering source of a virtual rule) and `values` (the runtime values index). File references (`map`, `maps`, `templates`) and named schema references are always resolved. An invalid JSONPath is `400 invalid_request`.\n\n### `watcher_validate_config`\n\nValidate config and optionally test file paths (`POST /config/validate`).\n\n- `config` (object, optional) — candidate config (partial or full). Omit to validate current config.\n- `testPaths` (string[], optional) — file paths to test against the config\n\nPartial configs are merged into the running config shallowly: each top-level key replaces the running value, except `inferenceRules`, which merge by rule name. This differs from `watcher_config_apply` (deep merge), so pass a whole top-level object (e.g. all of `watch`) when validating a change inside it. Returns `{ valid: false, errors: [{ path, message }] }`, or `{ valid: true, warnings?, testResults? }`, where each test result is `{ path, matchedRules, metadata, error? }` (`error: "File not found"` when the path cannot be read). Validation problems come back in a `200` body, so this is the place to see them in full.\n\n### `watcher_config_apply`\n\nApply config changes (`POST /config/apply`, sent as `{ patch: config }`).\n\n- `config` (object, required) — full or partial config to apply\n- `replace` (boolean, optional) — replace the whole config with `config` instead of merging\n\nPartial configs are deep-merged into the config file the service runs from (objects recursively, arrays replaced, `inferenceRules` by rule name), so `{}` or a single key is valid. The merged result is validated and written to disk, then the `configWatch.reindex` scope (default `issues`) runs; that reindex covers files only. With `configWatch.enabled` (default `true`) the service picks up the written file without a restart, including new/removed watch paths (the filesystem watcher is rebuilt), inference rule changes, move detection settings, and gitignore filter updates.\n\nReturns `{ applied: true, config }`. `config` is redacted through the same allow-list as `GET /config`: `api`, `sources` and provider keys are never echoed. When the file was written but the post-write apply step failed, the body also carries `warning` (a string describing the failure). An invalid merged config is a tool error reading `HTTP 400: {"error":"Config validation failed","code":"invalid_request","issues":[...]}`, so run `watcher_validate_config` first. A config file the service cannot read or write is `HTTP 500` with the generic `internal_error` body, the cause in the service log.\n\n### `watcher_reindex`\n\nTrigger a reindex operation (`POST /reindex`). Non-prune scopes return a `plan` object showing the blast area; the live run reuses the same targets. A live prune (non-dry-run) returns immediately without a plan.\n\n**Parameters:**\n\n- `scope` (string, optional) — Reindex scope. The tool sends `"rules"` when omitted (the HTTP API\'s own default is `"issues"`). One of:\n - `"rules"` — Re-apply inference rules to every watched file (no re-embedding; changed values written, keys the rules no longer project removed). With `path`, only files matching those globs. Without `path`, every indexed source document is also retrieved through its callback and its stored rule re-applied: unchanged content gets a metadata-only update, and a document is re-embedded only if its rendered text changed.\n - `"full"` — Re-extract and re-process every watched file (a file is re-embedded when the text to embed changed or it was never indexed), and retrieve and always re-embed every indexed source document. Clears the runtime values index first.\n - `"issues"` — Re-process the documents in `watcher_list_issues`: failed files, and failed source references (with their stored rule, or the `rule` recorded on the issue when the document was never indexed).\n - `"path"` — Re-process a specific file or all files under a directory. Requires `path`. Each target must be watched and not gitignored (`403 path_not_watched`) and must exist (`404 not_found`). Files only.\n - `"prune"` — Delete Qdrant points for files no longer in watch scope (removed paths, gitignored files, deleted files). No re-embedding. Pure cleanup. Never selects source points (`source` set, no `file_path`).\n- `path` (string | string[], optional) — Target file(s) or directories for `"path"` (required there), or globs narrowing `"rules"`. Ignored by other scopes.\n- `dryRun` (boolean, optional) — When `true`, compute and return the plan without executing.\n\nThe tool has no `source` parameter. To re-process specific source references, call `POST /reindex` directly with `{ "scope": "path", "source": ["<name>://<id>", ...] }` (at most 1000): they are enqueued on the notify queue, are not tracked by `GET /status`, and send no completion callback. A reindex triggered by a config change (`watcher_config_apply`, config-file reload, rule registration) covers files only.\n\n**Response (normal):**\n\n```json\n{\n "status": "started",\n "scope": "rules",\n "plan": {\n "total": 148000,\n "toProcess": 148000,\n "toDelete": 0,\n "byRoot": { "/data": 95000, "/srv/config": 3000 }\n }\n}\n```\n\n**Response (dryRun):**\n\n```json\n{\n "status": "dry_run",\n "scope": "prune",\n "plan": {\n "total": 562000,\n "toProcess": 0,\n "toDelete": 2300,\n "byRoot": { "/srv/app/node_modules": 1800, "/srv/app/.cache": 500 }\n }\n}\n```\n\n**Plan fields:**\n\n- `total` — Points scrolled (prune), or files plus source documents covered (other scopes).\n- `toProcess` — Files plus source documents to process (0 for prune).\n- `toDelete` — Points to delete (prune only, 0 for others).\n- `byRoot` — Files grouped by watch root (for prune: orphaned files), plus one entry per source name for source documents. Shows where the impact concentrates.\n- `incomplete` — `true` when the prune scroll failed part-way and the plan is partial.\n\n**Progress:** a live run (every scope except `path` with `source`) is tracked in `watcher_status` under `health.reindex` (`filesProcessed` / `totalFiles`, counting files plus source documents). A failing file or source document is logged and recorded; the rest of the run continues.\n\n**Guidance:**\n\n- Use `dryRun: true` before any large-blast operation to preview impact.\n- `prune` is safe — it only deletes orphaned file points, never re-embeds. Use after changing watch paths, fixing gitignore, or cleaning up stale data.\n- `prune` is NOT triggered by config-watch auto-reindex (too dangerous for auto-trigger).\n\n### `watcher_scan`\n\nFilter-only point query without vector search (`POST /scan`). Use for structural queries where the question has no semantic dimension.\n\n- `filter` (object, required) — Qdrant filter object. Required to prevent accidental full-collection scans.\n- `limit` (number, optional) — page size (integer 1 to 1000), default 100\n- `cursor` (string or number, optional) — opaque cursor from previous response for pagination\n- `fields` (string[], optional) — payload fields to return (projection)\n- `countOnly` (boolean, optional) — if true, return `{ count }` instead of points\n\n**Response (normal):**\n\n```json\n{\n "points": [\n {\n "id": "uuid",\n "payload": { "file_path": "/data/tickets/PROJ-123.json" }\n }\n ],\n "cursor": "opaque string, number or null"\n}\n```\n\n**Response (countOnly):**\n\n```json\n{ "count": 1234 }\n```\n\n**Key differences from `watcher_search`:**\n\n- No `query` parameter — does NOT use embeddings\n- No `score` field — results are unranked filter matches\n- Cursor-based pagination (not offset-based); `cursor` is `null` on the last page\n- Zero cost per call beyond Qdrant\'s filtered scroll\n\n**Pagination pattern:**\n\n```\nlet cursor = undefined;\ndo {\n const result = await watcher_scan({ filter, limit: 100, cursor });\n // process result.points\n cursor = result.cursor;\n} while (cursor);\n```\n\n### `watcher_service`\n\nManage the watcher background service (install, uninstall, start, stop, restart, check status). No HTTP call.\n\n- `action` (string, required) — one of: `install`, `uninstall`, `start`, `stop`, `restart`, `status`\n\nReturns the service manager\'s response. On Windows uses NSSM, on Linux systemd, on macOS launchd. Only `install` needs `configRoot` (see [Plugin Installation](#plugin-installation)).\n\n### `watcher_list_issues`\n\nGet runtime processing failures (`GET /issues`). Returns `{ count, issues: { [key]: IssueRecord[] } }`: `count` is the number of documents with issues, and each key is a file path or a source reference (`<name>://<id>`). Each `IssueRecord` has `type` (one of the [issue codes](#error-categories)), `message`, `timestamp`, and optionally `operation`, `property`, `rule`, `rules`, `types`, `details`.\n\n### `watcher_walk`\n\nList watched files matching globs (`POST /walk`). Returns matching file paths from the watcher\'s in-memory list of watched files across all configured watch roots.\n\n- `globs` (string[], required) — glob patterns to intersect with watch paths (non-empty; matched case-insensitively, dotfiles included)\n\n**Response:**\n\n```json\n{\n "paths": ["/data/foo/.meta/meta.json", "/data/bar/.meta/meta.json"],\n "matchedCount": 2,\n "scannedRoots": ["/data", "/srv/config"]\n}\n```\n\n**Use cases:**\n\n- Discover files matching a pattern across all watched directories (e.g., `["**/.meta/meta.json"]`)\n- Enumerate files before rule registration to understand scope\n- Find files that aren\'t yet indexed (no Qdrant dependency)\n\n**Key differences from `watcher_scan`:**\n\n- Lists the files the filesystem watcher tracks, not the Qdrant index\n- No embedding or indexing required, but the initial filesystem scan must be complete: until then the tool returns `HTTP 503 scan_in_progress` (check `health.initialScan` in `watcher_status`)\n- Returns file paths only (no metadata, no vectors)\n- Applies `watch.ignored` and gitignore filtering automatically\n\n## Query Planning: Scan vs Search\n\n**Decision rule:** If the query has no semantic/natural-language dimension, use `watcher_scan`. If you need meaning-based similarity, use `watcher_search`.\n\n| Use `watcher_scan` | Use `watcher_search` |\n| --- | --- |\n| "List all files in domain X" | "Find documents about authentication" |\n| "Files modified after timestamp T" | "What discusses rate limiting?" |\n| "Enumerate paths under prefix P" | "Prior conversations about deployment" |\n| "Count files matching a condition" | "Related tickets to this issue" |\n| "Staleness detection / delta computation" | "What happened in last week\'s meetings?" |\n\n**Scan-specific filter examples:**\n\n**Domain enumeration:**\n\n```json\n{ "must": [{ "key": "domains", "match": { "value": "email" } }] }\n```\n\n**Modified after timestamp:**\n\n```json\n{ "must": [{ "key": "modified_at", "range": { "gte": 1772800000 } }] }\n```\n\n**Path prefix matching:**\n\n```json\n{ "must": [{ "key": "file_path", "match": { "text": "/data/tickets" } }] }\n```\n\n**Count files in a domain (no point data transferred):**\n\n```\nwatcher_scan: filter={"must":[{"key":"domains","match":{"value":"github"}}]}, countOnly=true\n```\n\n---\n\n## Qdrant Filter Syntax\n\nFilters use Qdrant\'s native JSON filter format, passed as the `filter` parameter to `watcher_search`.\n\n### Basic Patterns\n\n**Match exact value:**\n\n```json\n{ "must": [{ "key": "domain", "match": { "value": "email" } }] }\n```\n\n**Match text (full-text search within field):**\n\n```json\n{ "must": [{ "key": "chunk_text", "match": { "text": "authentication" } }] }\n```\n\n**Combine conditions (AND):**\n\n```json\n{\n "must": [\n { "key": "domain", "match": { "value": "jira" } },\n { "key": "status", "match": { "value": "In Progress" } }\n ]\n}\n```\n\n**Exclude (NOT):**\n\n```json\n{\n "must_not": [{ "key": "domain", "match": { "value": "repos" } }]\n}\n```\n\n**Any of (OR):**\n\n```json\n{\n "should": [\n { "key": "domain", "match": { "value": "email" } },\n { "key": "domain", "match": { "value": "slack" } }\n ]\n}\n```\n\n**Nested (combine AND + NOT):**\n\n```json\n{\n "must": [{ "key": "domain", "match": { "value": "jira" } }],\n "must_not": [{ "key": "status", "match": { "value": "Done" } }]\n}\n```\n\n### Key Differences\n\n- `match.value` — exact match (case-sensitive, for keyword fields like `domain`, `status`)\n- `match.text` — full-text match (for text fields like `chunk_text`)\n\n## Search Result Shape\n\nEach result from `watcher_search` contains:\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `id` | string | Qdrant point ID |\n| `score` | number | Similarity (or hybrid fused) score; higher = more relevant |\n| `payload.file_path` | string | Normalized file path (file points only) |\n| `payload.source` | string | Source reference `<name>://<id>` (source points only) |\n| `payload.source_rule` | string | Designated rule of a source document (source points only) |\n| `payload.chunk_text` | string | The matched text chunk |\n| `payload.chunk_index` | number | Zero-based chunk position within the document |\n| `payload.total_chunks` | number | Total chunks for this document |\n| `payload.line_start` | number | 1-indexed line where the chunk starts |\n| `payload.line_end` | number | 1-indexed line where the chunk ends |\n| `payload.created_at` | number | Creation timestamp (unix seconds) |\n| `payload.modified_at` | number | Modification timestamp (unix seconds) |\n| `payload.content_hash` | string | SHA-256 hash of the embedded text |\n| `payload.matched_rules` | string[] | Names of inference rules that matched |\n\n`file_path` is not always present: a source point (a document indexed through a source callback) carries a non-empty `source` and no `file_path`. A file point may also carry a `source` written by its own rules; it is still a file point. Additional metadata fields depend on the deployment\'s inference rules (e.g., `domain`, `status`, `author`). Use `watcher_config` to discover available fields.\n\n## Query Planning (Per Search Task)\n\nIdentify relevant rule(s) from the orientation model, then retrieve their schemas:\n\n**Retrieve complete schema for a rule:**\n\n```\nwatcher_config: path="$.inferenceRules[?(@.name==\'jira-issue\')].schema"\n```\n\nReturns the rule\'s schema array with named schema references and file references already resolved: properties, types, `set` expressions, `uiHint`, `enum`, etc.\n\n**For select/multiselect fields without `enum` in schema:**\n\n```\nwatcher_config: path="$.inferenceRules[?(@.name==\'jira-issue\')].values.status"\n```\n\nRetrieves valid filter values from the runtime values index (distinct values accumulated during embedding).\n\n**When search results span multiple rules** (indicated by `matched_rules` on results): query each unique rule\'s schema separately and merge mentally. Most result sets share the same rule combination, so this is typically one or two queries, not one per result.\n\n---\n\n## uiHint → Filter Mapping\n\nUse `uiHint` to determine filter construction strategy. **This table is explicit, not intuited:**\n\n| `uiHint` | Qdrant filter | Notes |\n| --- | --- | --- |\n| `text` | `{ "key": "<field>", "match": { "text": "<value>" } }` | Substring/keyword match |\n| `select` | `{ "key": "<field>", "match": { "value": "<enum_value>" } }` | Exact match; use `enum` values from schema or runtime values index |\n| `multiselect` | `{ "key": "<field>", "match": { "value": "<enum_value>" } }` | Any-element match on array field; use `enum` or runtime values index |\n| `date` | `{ "key": "<field>", "range": { "gte": <unix_ts>, "lt": <unix_ts> } }` | Range filter against integer fields holding Unix timestamps (seconds). Source dates should be normalized in config via `{{toUnix ...}}` in `set` expressions. Either bound optional for open-ended ranges (e.g., "after January" → `gte` only) |\n| `number` | `{ "key": "<field>", "range": { "gte": <n>, "lte": <n> } }` | Either bound optional for open-ended ranges |\n| `check` | `{ "key": "<field>", "match": { "value": true } }` | Boolean match |\n| _(absent)_ | Do not use in filters | Internal bookkeeping field, not intended for search |\n\n**Fallback:** If a `select`/`multiselect` field has neither `enum` in schema nor values in the index, treat it as `text` (substring match instead of exact match).\n\n---\n\n## Qdrant Filter Combinators\n\nCompose individual field conditions into complex queries using three combinators:\n\n| Combinator | Semantics | Use case |\n| --- | --- | --- |\n| `must` | AND — all conditions required | Intersecting constraints (domain + date range + assignee) |\n| `should` | OR — at least one must match | Alternative values, fuzzy criteria ("assigned to X or Y") |\n| `must_not` | Exclusion — any match triggers exclude | Filtering out noise (exclude Done, exclude codebase domain) |\n\n**Combinators nest arbitrarily for complex boolean logic:**\n\n```json\n{\n "must": [\n { "key": "domain", "match": { "value": "jira" } },\n { "key": "created", "range": { "gte": 1735689600 } }\n ],\n "should": [\n { "key": "assignee", "match": { "value": "Jane Doe" } },\n { "key": "assignee", "match": { "value": null } }\n ],\n "must_not": [{ "key": "status", "match": { "value": "Done" } }]\n}\n```\n\nA consuming UI will necessarily compose simple single-field filters. The assistant can compose deeply complex queries combining multiple fields, nested boolean logic, and open-ended ranges to precisely target what it needs.\n\n---\n\n## Search Execution\n\n**Plain semantic search is valid and often sufficient.** Not every query needs metadata filters. When the user\'s question is broad or exploratory, a natural language query with no filter object is the right starting point. Add filters to narrow, not as a default.\n\n**Result limit guidance:**\n\n- Default: 10 results\n- Broad discovery / exploratory: 20–30, apply score threshold cutoff from config\n- Targeted retrieval with tight filters: 5\n- Cross-domain sweep: 15–20, no domain filter, use score to separate signal from noise\n\n---\n\n## Post-Processing Guidance\n\n### Score Interpretation\n\nUse `scoreThresholds` from config (queried during orientation). Values are deployment-specific, constrained to [-1, 1]:\n\n- `strong` — minimum score for a strong match. **Action:** High confidence. Use these results directly.\n- `relevant` — minimum score for relevance. **Action:** Likely useful but verify context before relying on them.\n- `noise` — maximum score below which results are noise. **Action:** Discard. If all results fall below this threshold, broaden your query or try different terms.\n\n### Chunk Grouping\n\nMultiple results with the same `file_path` (or, for source documents, the same `source`) are chunks of one document. Read the full file for complete context.\n\n### Schema Lookup\n\nUse `matched_rules` on results to look up applicable schemas for metadata interpretation:\n\n```\nwatcher_config: path="$.inferenceRules[?(@.name==\'jira-issue\')].schema"\n```\n\n### Full Context\n\nSearch gives you chunks; use `read` with `file_path` for the complete document. A source document has no file: render it with `POST /render` and `{ "source": "<ref>" }`, which retrieves it through its callback.\n\n---\n\n## Path Testing\n\nWhen uncertain whether a file would be indexed, test it against the rules. `watcher_validate_config` with `testPaths` (and no `config`) runs the current rules against each path and returns `{ path, matchedRules, metadata }` per path (`error: "File not found"` when the file cannot be read):\n\n```\nwatcher_validate_config: testPaths=["/data/tickets/PROJ-123.json"]\n```\n\nOr call `POST /config/match` directly with `{ "paths": [...] }`. It returns `{ matches: [{ rules, watched }] }`, one entry per path, without reading the files:\n\n- `rules` lists the matching rule names; an empty array means no inference rules match\n- `watched: false` means the path falls outside watch paths or is excluded by ignore patterns\n\n---\n\n## Config Authoring\n\n### Rule Structure\n\nEach inference rule has:\n\n- `name` (required) — unique identifier\n- `description` (required) — human-readable purpose\n- `match` (required) — JSON Schema matched against file attributes (e.g. a picomatch `glob` on `file.path`)\n- `schema` (optional) — array of named schema references and/or inline schema objects with `set` templates\n- `map` (optional) — JsonMap transform (inline, named map, or `.json` file path)\n- `template` (optional) — Handlebars template (inline, named template, or `.hbs`/`.handlebars` file path)\n- `render` (optional) — declarative frontmatter + Markdown renderer (mutually exclusive with `template`)\n- `renderAs` (optional) — output extension (1-10 lowercase alphanumerics); requires `template` or `render`\n\n### Authoring Notes\n\n- **`maps` entries are bare strings or inline maps.** A string value is a file path, loaded and parsed as a JsonMap at startup. A `{ path, description }` object is kept as-is and never loaded from the file, so it silently does nothing useful. (`mapHelpers` and `templateHelpers` are different: `{ path, description }` objects are their documented form.)\n- **Metadata is derived with `set`, not `attributes`.** Each schema property\'s `set` template produces its value. There is no `attributes` key in the config schema; older configs and examples that use one need rewriting.\n- **`render.frontmatter` accepts picomatch globs and `!` exclusions** alongside explicit keys, e.g. `["meta_id", "*", "!_*", "!chunk_*"]`. Explicit names keep their listed order; keys matched by a glob follow, sorted alphabetically; a key matching an exclusion is dropped.\n- **Virtual rules have a startup window.** Virtual rules are registered at runtime by other services (`POST /rules/register`, e.g. jeeves-meta), so they are absent during the initial file scan after a watcher start. Files indexed in that window lack virtual-rule metadata until the registration\'s automatic rules reindex runs, or until `POST /rules/reapply` covers them. If metadata from a virtual rule is missing right after a restart, check whether its service has registered yet.\n\n### Config Workflow\n\n1. Edit config (or build partial config object)\n2. Validate: `watcher_validate_config` with optional `testPaths` for dry-run preview\n3. Apply: `watcher_config_apply` — validates, writes, triggers the `configWatch.reindex` reindex\n4. Monitor: `watcher_list_issues` for runtime processing failures\n\n### Reindex Concurrency\n\nReindex operations process files and source documents concurrently using `reindex.concurrency` (default 50). This applies to every scope, to issue-source discovery, and to the stored-rule lookups of `POST /reindex` with `source`. The watcher\'s incremental processing (file events and source notifications) uses a separate `embedding.concurrency` setting.\n\nProgress is reported via `watcher_status` (`health.reindex.filesProcessed` / `health.reindex.totalFiles`, counting files plus source documents).\n\n### When to Reindex\n\n- **Rules scope** (`"rules"`): Changed rule matching patterns, set expressions, schema mappings. No file re-embedding needed.\n- **Full scope** (`"full"`): Added watch paths, broad schema restructuring, or source documents whose vectors must be rebuilt. Re-extracts every file (re-embedding the ones whose text to embed changed) and re-embeds every source document.\n- **Issues scope** (`"issues"`): After fixing the root cause of processing failures (permissions, encoding, file format, a source outage). Re-processes only the failed files and source documents.\n- **Path scope** (`"path"`): Edited files in a specific directory, or a single file looks stale, and you want them re-processed without a full reindex. A file whose text to embed is unchanged is not re-embedded.\n- **Prune scope** (`"prune"`): After changing `watch.paths`, adding gitignore rules, or discovering stale/orphaned points (e.g., indexed `node_modules`). Deletes points for out-of-scope or deleted files. Always `dryRun: true` first to preview.\n\n---\n\n## Diagnostics\n\n### Escalation Path\n\n1. `watcher_status` — is the service healthy? Is a reindex running? Is the initial scan still active?\n2. `watcher_list_issues` — which documents are failing and why?\n3. `watcher_config` with `$.issues` — the same issue records via JSONPath\n4. Check logs at the configured log path\n\n### Error Categories\n\nEvery issue record\'s `type` is one of these codes:\n\n- `type_collision` — conflicting metadata types across rules (includes `property`, `rules[]`, `types[]`)\n- `interpolation_error` — template or `set` expression rendering failed (includes `property`, `rule`; `details` holds per-step failures)\n- `extraction_error` — reading, rule application or chunking failed\n- `embedding_error` — the embedding provider failed\n- `upsert_error` — the vector store write failed\n- `match_failed` — a source document\'s designated rule did not match (`details` holds the AJV errors)\n- `rule_missing` — a source document\'s designated rule was not found\n- `no_content` — nothing to embed\n- `source_unavailable` — the source callback failed\n- `unsupported_content_type` — the source callback returned a non-JSON body\n- `too_large` — the document is over a `processing` ceiling (`maxFileBytes`, `maxChunksPerFile` or `maxTextBytes`); it is not embedded and its points are deleted. Raise the ceiling and run `reindex` with scope `issues` to restore it\n\nSource-document issues always carry `rule` (the designated rule of the failed run).\n\n**Issues are self-healing:** resolved on successful re-process. The issues file always represents the current set of unresolved problems: a live todo list.\n\n### Change Source\n\n`watcher_status` reports the change source under `health.changeSource` (`@parcel/watcher` by default, chokidar on request or when polling):\n\n- **Service refuses to start, log names `@parcel/watcher failed to load`:** the platform binary is missing or broken. Reinstall without `--omit=optional`, or set `watch.changeSource: chokidar` (the manual switch; a reload rebuilds the watcher with one full initial scan).\n- **Rising `reconcileEvents`:** the source is missing events and reconciliation is finding them. **Rising `resubscribes`:** subscriptions keep failing (Linux). **Rising `renewFailures`:** a Linux renewal could not unsubscribe; restart the service. **`unavailableRoots` above 0:** a watch root is missing or unreadable (an unmounted share); its files keep their points until it returns. **High `pendingStability`, or a long `lastReconcileDurationMs` / `lastRenewDurationMs`:** a large write burst or a slow disk; a long renewal pauses event handling for every root.\n- **Linux, a file in a moved-in or renamed folder is indexed late:** changes inside folders nested in a newly created or moved-in tree, or in a directory renamed within a root, are missed until the root\'s subscription is renewed at the next periodic reconcile, up to `watch.reconcileIntervalMs` (15 minutes by default) late. Expected; shorten the interval if the latency matters. The files moved in with the tree are indexed at once.\n- **Linux, events lost under a write burst with nothing in the log:** parcel cannot detect an inotify queue overflow. Check `sysctl fs.inotify.max_queued_events` (recommended 1048576) and `fs.inotify.max_user_watches` (the start log warns at 90%); the periodic reconcile finds what was lost (`reconcileEvents` rises).\n- **Windows, files in deep folders indexed only by reconciliation:** expected. Under parcel, creating or changing a file whose path is 260 characters or more produces no change event at either `LongPathsEnabled` setting, so it is indexed at the next periodic reconcile, up to `watch.reconcileIntervalMs` late. Nothing to fix; check that `watch.reconcileIntervalMs` is not 0. (`LongPathsEnabled = 1` is still recommended for other tools; it is a host change for the owner.)\n- **Windows, `lastRestartCause` set:** under parcel the service never unsubscribes on Windows (`@parcel/watcher` 2.6.0 crashes the process there), so a reload that rebuilds the watcher, a subscription error or a vanished watch root exits with code 75 and the service manager restarts it. `watch-config-changed`, `subscription-error` or `root-gone` says why, `lastRestartAt` when. If the service stays down after such an exit, its service manager is not restarting it on exit (NSSM: `AppExit Default Restart`).\n\n### Index Check at Start\n\nAfter the start walk the service deletes the indexed documents of files gone from disk (deleted or moved while it was down). `watcher_status` reports it under `health.indexCheck` (`state`, `at`, `durationMs`, `checked`, `removed`, `skippedRoots`, `capped`):\n\n- **`capped` above 0:** under some watch root more than 10% of the indexed documents (once more than 20 are missing) or more than 1,000 were missing from disk or filters, so nothing was deleted there; the log names the root. Run `watcher_reindex` with scope `prune` and `dryRun: true`, check the plan, then run it.\n- **`state: failed`:** the index could not be listed (Qdrant down), or the service stopped first; it runs again at the next start.\n- **`skippedRoots` above 0:** a root was unavailable, unwatched, gone, or its start walk failed; its deletions wait for the next start or a `prune`.\n- **Files that still exist but are now ignored** keep their points: that is `prune`\'s job. `watch.indexCheckOnStart: false` turns the check off.\n\n---\n\n## Helper Management\n\nHelpers use namespace prefixing: config key becomes prefix. A helper named `slack` exports `slack_extractParticipants`.\n\nEnumerate loaded helpers:\n\n```\n$.mapHelpers — JsonMap helper namespaces with exports\n$.templateHelpers — Handlebars helper namespaces with exports\n```\n\n### Built-in JsonMap Helpers\n\nThe following helpers are available in every JsonMap `lib` context without any helper config:\n\n#### `fetchSiblings(filePath, options?)`\n\nRetrieve extracted text from neighboring files in the same directory with the same extension. Useful for contextual embedding — e.g., injecting surrounding email messages into a thread member\'s embedding for better semantic search.\n\n**Parameters:**\n\n- `filePath` (string) — the current file path (in a rule\'s `map`, `$.input.file.path`)\n- `options` (object, optional; JsonMap `params` are path strings, so a call from a config `map` uses the defaults):\n - `before` (number, default 3) — number of preceding siblings to include\n - `after` (number, default 1) — number of following siblings to include\n - `sort` (`"name"` | `"mtime"`, default `"name"`) — sort order for determining neighbor position\n\n**Returns:** `string[]` — extracted text from sibling files, in sort order. Files that fail extraction are silently skipped. For a source document (a source rule\'s `map`) there is no file reader, so it returns `[]`.\n\n**Example** (in a rule\'s `map`):\n\n```json\n{\n "context": {\n "$": { "method": "$.lib.fetchSiblings", "params": "$.input.file.path" }\n }\n}\n```\n\n---\n\n## Enrichment\n\nUse `watcher_set_metadata` to tag documents after analysis (e.g., `reviewed: true`, project labels).\n\n**Enrichments are durable.** Stored in a SQLite database (`<stateDir>/enrichments.sqlite`, default `stateDir` `.jeeves-metadata`), enrichments survive full reindexes. Enriching a file that is not indexed yet stores the enrichment, which applies once the file is indexed. When the watcher re-processes a file, enrichments are merged with inference rule output using composable semantics: scalar fields overwrite, array fields union+deduplicate.\n\n**Metadata is validated against the file\'s matched rule schemas** (for a source document, its designated rule\'s schema). Only declared property types are checked; unknown properties and `null` values pass. The tool reports a failure as `HTTP 400 invalid_request: Validation failed`; the structured `details` are in the HTTP response body:\n\n```json\n{\n "code": "invalid_request",\n "error": "Validation failed",\n "details": [\n {\n "property": "priority",\n "expected": "string",\n "received": "number",\n "rule": "jira-issue",\n "message": "Property \'priority\' is declared as string in jira-issue schema, received number"\n }\n ]\n}\n```\n\n---\n\n## Version Tracking\n\nThe watcher includes built-in history and undo for your watched files. When enabled, every change to a watched file is automatically versioned. You can view the history of any file or folder, see exactly what changed between versions, retrieve old versions, and undo changes — all through the version-control tools below.\n\n### Tools\n\n| Tool | Purpose |\n| --- | --- |\n| `watcher_get_vcs_status` | Check version tracking health: enabled state, tracked roots, remote sync status |\n| `watcher_list_vcs_history` | View change history for a file or folder, with optional date range and limit |\n| `watcher_show_vcs_file` | Retrieve the content of a file at a specific past version |\n| `watcher_diff_vcs` | Show what changed between two versions, or between a past version and now |\n| `watcher_revert_vcs` | Undo changes by restoring files to a specific past version |\n| `watcher_exclude_vcs_path` | Exclude a path from version history (it stays in search), or remove the exclusion; untracks matching files |\n| `watcher_check_vcs_exclusion` | Check whether git ignores a path and why (rule, file, line) |\n\n### Usage Patterns\n\n**View recent changes to a file:**\n\n```\nwatcher_list_vcs_history: glob="/data/tickets/PROJ-123.json", limit=10\n```\n\n**See what changed between two versions:**\n\n```\nwatcher_diff_vcs: glob="/data/tickets/PROJ-123.json", commit="abc1234"\n```\n\n**Restore a file to a previous version:**\n\n```\nwatcher_revert_vcs: glob="/data/tickets/PROJ-123.json", commit="abc1234"\n```\n\n**View old content without restoring:**\n\n```\nwatcher_show_vcs_file: path="/data/tickets/PROJ-123.json", commit="abc1234"\n```\n\n**Exclude files that don\'t need versioning** (e.g., Jira issues that are read-only syncs):\n\n```\nwatcher_exclude_vcs_path: glob="/data/tickets/**/*.json"\n```\n\n**Check if a file is excluded and why:**\n\n```\nwatcher_check_vcs_exclusion: path="/data/tickets/PROJ-123.json"\n```\n\n### Enabling Version Tracking\n\nVersion tracking is controlled by the `vcs.enabled` config setting. When set to `true`, the watcher automatically versions all watched files. No additional setup is required — just enable it and history starts accumulating.\n\nSquash retention activates by default when VCS is enabled, even without explicit `retention` config. The defaults are: 30 days max age, 100 versions max, and a daily midnight cron schedule (`0 0 * * *`). These defaults prevent unbounded commit history growth.\n\n### Excluding Paths\n\nNot all watched files need version history. For example, if Jira issues are synced read-only, there\'s nothing to "undo." Use `watcher_exclude_vcs_path` to stop tracking specific paths.\n\nThe exclusion is VCS-only: the pattern goes to the root\'s `.git/info/exclude`, so the files stop being versioned but stay indexed and searchable.\n\n- **Destructive.** Adding a pattern untracks the tracked files it matches, in one commit: their history ends at their last committed version (the files stay on disk). `remove: true` deletes the pattern but does not restore history; the files are versioned again from their next change.\n- **One gitignore pattern, root-anchored.** The pattern is written as `/` plus the glob relative to the root, so `/data/tickets/*.json` (root `/data`) matches only directly under `tickets/`; use `**/` for any depth. Pass `root` to give a root-relative glob (`tickets/**/*.json`, without a leading `/`: with `root`, a glob starting with `/` is a filesystem path and must lie under the root).\n- **Brackets are a character class.** `Report [draft].docx` matches `Report d.docx`; escape them to name the file: `Report \\[draft\\].docx`.\n- **Refused (`400`):** a newline, a leading `!` or `#`, braces, a pattern matching the whole root (`*`, `**`, `**/*`, ...), or a glob outside the given `root`.\n- **More than 100 files** to untrack need `confirm: true`; without it the tool reports `409 confirm_required` with the count and changes nothing. Check the count with the user before confirming.\n- **Busy root.** If version control is busy on the root for 30 s the tool reports `503 not_available`; retry after 30 s.\n- `watcher_check_vcs_exclusion` reports `excluded: false` for a tracked file even when a human `.gitignore` matches it (git never reports tracked paths as ignored); exclude it to untrack it.\n\n### Score Interpretation\n\nVCS tools do not use vector search. There are no relevance scores to interpret — results are exact history lookups, diffs, and file retrievals.\n\n---\n\n## Error Handling\n\nIf the watcher is unreachable:\n\n- Inform the user that semantic search is temporarily unavailable\n- Fall back to direct `read` for known file paths\n- Do not retry silently in a loop\n\nIf a tool fails with `HTTP 401` (`unauthorized`):\n\n- The watcher sets `api.apiKey` and the plugin sent no key, or a different one.\n- Ask the user to set `plugins.entries.jeeves-watcher-openclaw.config.apiKey` in `openclaw.json` (or the `JEEVES_WATCHER_API_KEY` environment variable of the gateway) to the service\'s `api.apiKey`. The tools read it on every call, so no restart is needed.\n- Never ask for, print or guess the key. `GET /status` and `GET /openapi.json` never need it.\n\nIf a tool or call fails with `HTTP 503` (`not_available`) and a `Retry-After` header:\n\n- `POST /metadata` (`watcher_set_metadata`), `POST /rules/reapply` and `POST /documents/delete` wait at most 10 s for a document that is being indexed, then answer `503` with `Retry-After: 10`. A single-document request changed nothing; a multi-document request kept the documents it already changed. All three are idempotent: retry the same request after 10 s.\n- `watcher_exclude_vcs_path` waits 30 s for a busy version-control root.\n\nIf a tool fails with `HTTP 500: {"error":"Response validation failed","code":"internal_error"}`, the watcher produced a response that doesn\'t match its own schema: a server bug. Report it with the request you made; retrying won\'t help.\n\nIf tools are unavailable (plugin not loaded in this session):\n\n- The watcher API is still accessible via direct HTTP calls\n- Use `exec` to call the endpoints listed in Service Architecture\n- Default: `http://127.0.0.1:1936`\n\n**CLI Fallbacks:**\n\n- `jeeves-watcher status [-p <port>]` — check if the service is running (`GET /status`)\n- `jeeves-watcher config validate -c <path>` — validate a config file against the schema (offline)\n- Restart via NSSM (Windows) or systemctl (Linux)\n\n---\n\n## References\n\n- [JSONPath Plus documentation](https://www.npmjs.com/package/jsonpath-plus) for JSONPath syntax\n- [Qdrant filtering documentation](https://qdrant.tech/documentation/concepts/filtering/) for advanced query patterns and search response format\n';
31101
+ const WATCHER_SKILL = '---\nname: jeeves-watcher\ndescription: >\n Semantic search of the indexed archive: recall prior context, find documents, scan, enrich metadata, watcher config, reindex, indexing issues, file history.\n---\n\n# jeeves-watcher — Search, Discovery & Administration\n\n## Service Architecture\n\nThe watcher is an HTTP API running as a background service (typically NSSM on Windows, systemd on Linux).\n\n**Default port:** 1936 (configurable via `api.port` in watcher config) **Non-default port:** If the watcher runs on a different port, the user must set `plugins.entries.jeeves-watcher-openclaw.config.apiUrl` in `openclaw.json`. The plugin cannot auto-discover a non-default port.\n\n**API key:** If the watcher sets `api.apiKey`, every route except `GET /status` and `GET /openapi.json` needs `Authorization: Bearer <key>`. The tools send it from `plugins.entries.jeeves-watcher-openclaw.config.apiKey` (or the `JEEVES_WATCHER_API_KEY` env var). From `watcher_config`, `watcher_config_apply`, `watcher_search`, `watcher_scan` and `watcher_show_vcs_file`, a missing or wrong key comes back as `HTTP 401: {"error":"Unauthorized","code":"unauthorized"}` (see [Error Handling](#error-handling) for the fix). From the other tools it reads `HTTP 401 unauthorized: the watcher API requires an API key. Provide it via plugins.entries.jeeves-watcher-openclaw.config.apiKey or the JEEVES_WATCHER_API_KEY env var.` (or, when a key was sent, `... rejected the API key. Check ... against the service\'s api.apiKey.`). When calling the API directly, add the header yourself. Never echo the key.\n\n**Tool errors:** Tool error text is prefixed `Error: `. Every `watcher_*` tool is built from the watcher\'s endpoint catalog and reports a non-2xx response as `HTTP <status>: <body>`, with the server\'s JSON body verbatim: read its `code` (and `details` or `issues`) there, e.g. `HTTP 400: {"error":"Validation failed","code":"invalid_request",...}` or `HTTP 503: {"error":"Scan in progress","code":"scan_in_progress"}`. Branch on the code, not the message. The tools reject parameters that don\'t match their schema before sending anything. Each call waits up to 120 s; a timeout does not cancel a state change on the service, so check the outcome (e.g. `watcher_check_vcs_exclusion` after `watcher_exclude_vcs_path`) before retrying. An unreachable service reads `Service not reachable at <url>.` with a hint to set `apiUrl`. (`watcher_service` comes from the shared Jeeves core toolset and words its own errors.)\n\n**Health check:** `GET /status` returns `name`, `version`, `uptime`, `status` (`healthy`/`degraded`/`unhealthy`), and a `health` object containing `collection` (`name`, `pointCount`, `dimensions`, `systemIndexes`), `reindex` progress, `initialScan` progress, `embedding` (provider health), and (when version tracking is on) `vcs` (`roots`, `pending`, `trippedRoots`, `oldestTrippedAt`). `status` is `degraded` (still HTTP 200) when embeddings are failing or a version-control root has tripped.\n\n**Mental model:** The `watcher_*` tools are thin HTTP wrappers. Each tool call translates to an HTTP request to the watcher API. When tools are available, use them. When they\'re not (e.g., different session, plugin not loaded), you can hit the API directly. Replace `<PORT>` below with the configured port (default 1936; check `plugins.entries.jeeves-watcher-openclaw.config.apiUrl` in `openclaw.json` if overridden), and add `-H "Authorization: Bearer <key>"` when the service sets `api.apiKey`:\n\n```\n# Health check\ncurl http://127.0.0.1:<PORT>/status\n\n# Search\ncurl -X POST http://127.0.0.1:<PORT>/search \\\n -H "Content-Type: application/json" \\\n -d \'{"query": "search text", "limit": 5}\'\n\n# Query config\ncurl http://127.0.0.1:<PORT>/config\n```\n\n**Key endpoints:**\n\n| Endpoint | Method | Purpose |\n| --- | --- | --- |\n| `/status` | GET | Health check, uptime, collection stats |\n| `/openapi.json` | GET | OpenAPI 3.1 document of the catalog routes (no key needed) |\n| `/search` | POST | Semantic search (main query interface) |\n| `/config` | GET | Public resolved config (allow-listed keys plus `issues`); optional `?path=<jsonpath>` filter |\n| `/config/validate` | POST | Validate candidate config |\n| `/config/apply` | POST | Apply config changes |\n| `/reindex` | POST | Trigger reindex |\n| `/metadata` | POST | Enrich document metadata (file path or source reference) |\n| `/scan` | POST | Filter-only point query (no embeddings) |\n| `/walk` | POST | Watched-file listing with glob intersection |\n| `/issues` | GET | Runtime processing failures (file paths and source references) |\n| `/documents` | POST | Notify changed source documents (`{ source, rule }`) |\n| `/documents/delete` | POST | Delete source documents by reference or filter |\n| `/rules/register` | POST | Register virtual inference rules (auto-triggers rules reindex) |\n| `/rules/unregister` | DELETE | Remove virtual rules by source (`{ source }` body) |\n| `/rules/unregister/:source` | DELETE | Remove virtual rules by source (parameterized) |\n| `/rules/reapply` | POST | Re-apply rules to indexed files matching `globs` |\n| `/points/delete` | POST | Delete points matching a Qdrant filter |\n| `/render` | POST | Render a document (file path or source reference) through its rule |\n| `/search/facets` | GET | Get search facet definitions and values (`?rules=a,b` scopes them to those rules) |\n| `/search/facets/counts` | POST | Count values of indexed fields over a filter, each document once; counts are approximate (`{ fields, filter?, rules?, limit? }`); `{ min, max }` for number/date fields; `503 not_available` while its system indexes build or past its 10 s budget |\n| `/rebuild-metadata` | POST | Rebuild the enrichment store from Qdrant file point payloads |\n| `/config/schema` | GET | Get config JSON schema |\n| `/config/match` | POST | Test file paths against inference rules and watch scope |\n| `/vcs/*` | GET/POST | Version tracking (`status`, `history`, `show`, `diff`, `check-exclusion`, `revert`, `exclude`) |\n\n**If the watcher is unreachable:** Check the service status (`nssm status jeeves-watcher` on Windows), check the configured port in the watcher config file, and check logs for startup errors.\n\n## Theory of Operation\n\nYou have access to a **semantic archive** of your human\'s working world. Documents, messages, tickets, notes, code, and other artifacts are indexed, chunked, embedded, and searchable. This is your long-term recall for anything beyond the current conversation.\n\n**When to reach for the watcher:**\n\n- **Someone asks about something that happened.** A meeting, a decision, a conversation, a ticket, a message. You weren\'t there, but the archive was. Search it.\n- **You need context you don\'t have.** Before asking the human "what\'s the status of X?", search for X. The answer is probably already indexed.\n- **You\'re working on something and need background.** Prior discussions, related records, relevant documents. Search by topic, filter by domain.\n- **You need to verify something.** Don\'t guess from stale memory. Search for the current state.\n- **You want to connect dots across domains.** The same topic might appear in multiple domains. A single semantic search surfaces all of them.\n\n**When NOT to use it:**\n\n- You already have the information in the current conversation\n- The question is about general knowledge, not the human\'s specific context\n- The watcher is unreachable (fall back to filesystem browsing)\n\n**The principle:** Memory-core is your curated highlights. The watcher archive is your perfect recall. Use memory first for speed and signal, but never let its narrow scope be the ceiling of what you can remember.\n\n## ⚠️ Embedding Cost — Hard Behavioral Gate\n\n**The watcher embeds every file it sees.** Embedding is the most expensive operation in the pipeline — each file triggers a Gemini API call. The index may contain hundreds of thousands of points. Any action that causes the watcher to process a large number of files has real, potentially significant cost.\n\n**STOP and ask for a human decision before:**\n\n- **Renaming, moving, or reorganizing watched directories.** The watcher has **move detection** enabled by default (`watch.moveDetection.enabled: true`). When a file is moved, the change source reports an unlink and an add; the `MoveCorrelator` buffers the unlink, matches it with the add via content hash, and processes it as a zero-embedding move (point ID remap, metadata re-inference, enrichment migration). Under the default parcel source, a renamed or moved directory is paired as a whole (at least 90% of its files at the same relative path and size) and each file becomes such a move. **No re-embedding cost for moves.** However, if a rename is blocked (e.g., by a file lock from the watcher or another service), **do NOT work around it by creating new directories and copying/moving files**. That creates duplicate embeddings at the new paths while the old paths still exist. Instead: stop the blocking service, perform the rename, restart.\n- **Changing `watch.paths` to add large directory trees.** Adding a new watch root triggers initial indexing of every matching file under it.\n- **Running `scope: "full"` reindex.** Every watched file is re-extracted (a file whose text to embed changed, or that was never indexed, is re-embedded) and every indexed source document is retrieved through its callback and always re-embedded. Use `scope: "rules"` for inference rule changes (no file re-embedding; only metadata reapplication).\n- **Any bulk file operation** (mass copy, mass move, template-based file generation) under watched paths.\n\n**The right instinct when blocked:** If a filesystem operation fails because a service has a lock, the correct response is to stop the service, do the operation, and restart — NOT to find a creative workaround. Creative workarounds in the presence of a live watcher are how you accidentally trigger re-embedding of tens of thousands of files.\n\n**Cost context:** A single file embedding costs a Gemini API call. 1,000 unnecessary embeddings is a noticeable cost. 100,000 unnecessary embeddings (e.g., copying an entire domain directory to a new path) is a billing event worth flagging.\n\n## Plugin Installation\n\nThe plugin is a standard OpenClaw plugin. On a Jeeves box, `jeeves install --workspace <workspace> --config-root <path> watcher` installs it and writes its config (without `--workspace` it takes the current folder as the workspace and writes platform files there); otherwise use `openclaw plugins install npm:@karmaniverous/jeeves-watcher-openclaw@<version> --pin --accept-capabilities --force` and set `plugins.entries.jeeves-watcher-openclaw.config` yourself. `--force` confirms the npm source and replaces an installed version; without it a non-interactive install is cancelled. To upgrade or roll back, run the same command with the other version. Restart the gateway to load it.\n\n`configRoot` has no default. Only `watcher_service` with `action: "install"` needs it; other `watcher_service` actions and all HTTP API tools work without it. If `watcher_service install` returns "configRoot not configured", set `configRoot` in the plugin config or the `JEEVES_CONFIG_ROOT` env var.\n\n## Quick Start (Existing Deployment)\n\nIf the watcher service is already running and healthy:\n\n1. **Search** — use `watcher_search` with a natural language query and optional metadata filters\n2. **Read source** — use `read` (standard file read) with `file_path` from search results for full document content. Source documents (points carrying `source`, such as `app://chat/thread/42`, and no `file_path`) are not files: their content comes from the source\'s callback (`POST /render` with `{ "source": "<ref>" }`).\n\n## Bootstrap (First-Time Setup)\n\nWhen the plugin loads and the watcher service is NOT yet set up, drive the entire setup proactively. The user should be able to install the plugin with nothing else in place and the bootstrap process gets them to a working system.\n\n**The agent drives this process.** Don\'t hand the user CLI commands and wait. Check each prerequisite, explain what\'s needed, execute what you can, and prompt the user only for decisions that require human judgment.\n\n### Step 1: Check Node.js\n\nVerify Node.js is installed and version ≥ 24.22.2:\n\n```bash\nnode --version\n```\n\nIf missing or too old, guide the user to install Node.js 24.22.2 or later from https://nodejs.org or via their package manager.\n\n### Step 2: Install Qdrant\n\nCheck if Qdrant is already running:\n\n```bash\ncurl -s http://localhost:6333/healthz\n```\n\nIf not running, install it. **Prefer native installation** (especially on cloud instances where Docker may not be available):\n\n**Linux (recommended for servers):**\n\n```bash\n# Download and install binary\ncurl -L https://github.com/qdrant/qdrant/releases/latest/download/qdrant-x86_64-unknown-linux-musl.tar.gz -o /tmp/qdrant.tar.gz\nsudo tar xzf /tmp/qdrant.tar.gz -C /usr/local/bin/\n\n# Create qdrant user and directories\nsudo useradd -r -s /bin/false qdrant\nsudo mkdir -p /var/lib/qdrant/storage /var/lib/qdrant/snapshots /etc/qdrant\nsudo chown -R qdrant:qdrant /var/lib/qdrant\n\n# Create config\nsudo tee /etc/qdrant/config.yaml > /dev/null <<EOF\nstorage:\n storage_path: /var/lib/qdrant/storage\n snapshots_path: /var/lib/qdrant/snapshots\nservice:\n host: 0.0.0.0\n http_port: 6333\n grpc_port: 6334\nEOF\n\n# Create systemd service\nsudo tee /etc/systemd/system/qdrant.service > /dev/null <<EOF\n[Unit]\nDescription=Qdrant Vector Database\nAfter=network.target\n\n[Service]\nType=simple\nExecStart=/usr/local/bin/qdrant --config-path /etc/qdrant/config.yaml\nWorkingDirectory=/var/lib/qdrant\nRestart=always\nUser=qdrant\n\n[Install]\nWantedBy=multi-user.target\nEOF\nsudo systemctl daemon-reload\nsudo systemctl enable --now qdrant\n```\n\n**Windows:**\n\n```powershell\n# Download from GitHub releases page\n# https://github.com/qdrant/qdrant/releases\n# Extract and run, or register as NSSM service:\nnssm install Qdrant <path-to-qdrant.exe>\nnssm start Qdrant\n```\n\n**Docker (fallback, if available):**\n\n```bash\ndocker run -d -p 6333:6333 -v qdrant_data:/qdrant/storage qdrant/qdrant\n```\n\nAfter installation, verify:\n\n```bash\ncurl -s http://localhost:6333/healthz\n```\n\n### Step 3: Install Watcher Service\n\nInstall the watcher CLI globally:\n\n```bash\nnpm install -g @karmaniverous/jeeves-watcher\n```\n\nVerify:\n\n```bash\njeeves-watcher --version\n```\n\n### Step 4: Set Up Embedding Provider\n\nThe watcher uses Google Gemini for embeddings by default (`gemini-embedding-001`, 3072 dimensions).\n\nCheck for an existing API key:\n\n```bash\necho $GOOGLE_API_KEY # Linux/Mac\necho %GOOGLE_API_KEY% # Windows cmd\n$env:GOOGLE_API_KEY # PowerShell\n```\n\nIf not set, guide the user:\n\n1. Go to https://aistudio.google.com/apikey\n2. Create an API key (free tier supports 1,000 embedding requests/minute)\n3. Set it as a persistent environment variable:\n - **Linux:** Add `export GOOGLE_API_KEY=<key>` to `~/.bashrc` or `~/.profile`\n - **Windows:** `setx GOOGLE_API_KEY "<key>"` (new shell sessions only) or set via System Properties → Environment Variables\n - **macOS:** Add to `~/.zshrc` or use `launchctl setenv`\n\nVerify the key works by testing a Gemini API call:\n\n```bash\ncurl -s "https://generativelanguage.googleapis.com/v1beta/models/gemini-embedding-001:embedContent?key=$GOOGLE_API_KEY" \\\n -H "Content-Type: application/json" \\\n -d \'{"model":"models/gemini-embedding-001","content":{"parts":[{"text":"test"}]}}\'\n```\n\nA successful response contains an `embedding.values` array.\n\n### Step 5: Author Initial Config\n\nAsk the user these questions:\n\n- **What directories should the watcher index?** (e.g., `~/documents`, `~/projects`, a workspace path)\n- **What types of files matter?** (helps determine file extensions for watch globs)\n- **Are there directories to exclude?** (node_modules, .git, build outputs, etc.)\n\nThen generate a starter config file. Example minimal config:\n\n```json\n{\n "description": "Personal knowledge base indexing",\n "api": { "port": 1936 },\n "watch": {\n "paths": ["/home/user/documents/**/*.{md,txt,json,pdf,html,docx}"],\n "ignored": ["**/node_modules/**", "**/.git/**", "**/dist/**"]\n },\n "embedding": {\n "provider": "gemini",\n "model": "gemini-embedding-001",\n "dimensions": 3072,\n "apiKey": "${GOOGLE_API_KEY}",\n "chunkSize": 1000,\n "chunkOverlap": 200,\n "rateLimitPerMinute": 1000,\n "concurrency": 5\n },\n "vectorStore": {\n "url": "http://localhost:6333",\n "collectionName": "jeeves_archive"\n },\n "search": {\n "scoreThresholds": { "strong": 0.75, "relevant": 0.5, "noise": 0.25 },\n "hybrid": { "enabled": true }\n },\n "logging": { "level": "info" },\n "inferenceRules": []\n}\n```\n\nWrite the config to a sensible location (e.g., `~/.config/jeeves-watcher.config.json` on Linux, or alongside the user\'s workspace). Validate it against the config schema (offline; no running service needed) with:\n\n```bash\njeeves-watcher config validate -c <config-path>\n```\n\n### Step 6: Register and Start as a Service\n\n**The watcher should run as a persistent service, not a foreground process.**\n\n**Linux (systemd):**\n\n```bash\nsudo tee /etc/systemd/system/jeeves-watcher.service > /dev/null <<EOF\n[Unit]\nDescription=Jeeves Watcher - Filesystem Indexing Service\nAfter=network.target qdrant.service\n\n[Service]\nType=simple\nExecStart=$(which jeeves-watcher) start -c <config-path>\nWorkingDirectory=%h\nRestart=always\nEnvironment=GOOGLE_API_KEY=<key>\nUser=$USER\n\n[Install]\nWantedBy=multi-user.target\nEOF\nsudo systemctl enable --now jeeves-watcher\n```\n\n**Windows (NSSM):**\n\n```powershell\njeeves-watcher service install -c <config-path>\n# Or manually:\nnssm install jeeves-watcher (Get-Command jeeves-watcher).Source start -c <config-path>\nnssm set jeeves-watcher AppEnvironmentExtra GOOGLE_API_KEY=<key>\nnssm start jeeves-watcher\n```\n\nVerify the service started:\n\n```bash\ncurl -s http://127.0.0.1:1936/status\n```\n\n### Step 7: Verify Health\n\nCall `watcher_status` (or `curl http://127.0.0.1:1936/status`). Confirm:\n\n- Service is running\n- Qdrant collection exists with expected dimensions (3072)\n- Point count is increasing (initial indexing in progress)\n\nIf the point count is 0 after a minute, check `watcher_list_issues` for processing failures.\n\n### Step 8: Orientation\n\nOnce health is confirmed and initial indexing has started:\n\n1. Query `$.[\'description\',\'search\']` for the deployment\'s organizational strategy and score thresholds.\n2. Query `$.inferenceRules[*].[\'name\',\'description\']` for available record types.\n3. Report to the user: how many points indexed so far, which domains are available, estimated time to complete initial indexing (based on file count and embedding rate).\n\n### On Subsequent Sessions\n\nOn sessions after bootstrap is complete:\n\n1. Call `watcher_status` silently.\n2. Run the orientation queries silently.\n3. Only report if something changed (service down, point count dropped significantly, new domains appeared).\n\n**Key principle:** The agent drives discovery. The user shouldn\'t have to explain their archive to you — the archive explains itself through its config.\n\n## Tools\n\nThe plugin registers 18 tools: `watcher_status`, `watcher_config`, `watcher_config_apply`, `watcher_service`, `watcher_search`, `watcher_set_metadata`, `watcher_validate_config`, `watcher_reindex`, `watcher_scan`, `watcher_list_issues`, `watcher_walk`, and the seven version-control tools (see [Version Tracking](#version-tracking)).\n\n### `watcher_search`\n\nSemantic search over indexed documents (`POST /search`).\n\n- `query` (string, required) — natural language search query (non-empty)\n- `limit` (number, optional) — max results (positive integer), default 10\n- `offset` (number, optional) — skip N results for pagination (non-negative integer)\n- `filter` (object, optional) — Qdrant filter for metadata filtering\n\nReturns an array of `{ id, score, payload }` ordered by score (see [Search Result Shape](#search-result-shape)). A filter Qdrant rejects is `400 invalid_request`; an exhausted embedding budget is `503 embedding_unavailable`.\n\n### `watcher_set_metadata`\n\nSet or update metadata on a document (`POST /metadata`).\n\n- `path` (string, required) — file path of the document (absolute or watched path)\n- `metadata` (object, required) — key-value metadata to merge\n\nReturns `{ ok: true, matched_rules }`. If the document is busy (being indexed) for 10 s, the tool reports `503 not_available` with `Retry-After: 10` and changes nothing; retry after 10 s. The tool addresses files only; to enrich a source document, call `POST /metadata` directly with `{ "source": "<name>://<id>", "metadata": { ... } }` (exactly one of `path` or `source`).\n\n### `watcher_status`\n\nService health check (`GET /status`, never needs the API key). Returns `name`, `version`, `uptime`, `status`, and a `health` object: `collection` (`name`, `pointCount`, `dimensions`, and `systemIndexes`: the state of each index the service creates itself, e.g. `{ "chunk_index": "ready", "file_path": "ready", "matched_rules": "building" }`; `building`, `ready`, `conflict` or `failed`), `embedding` (`state` `ok` or `failing`, `lastSuccessAt`, `lastError` `{ at, code }`, `failingSince`, `backoffUntil`; codes only, never provider text), `reindex` (`active`; while active also `scope`, `startedAt`, `filesProcessed`, `totalFiles`), `initialScan`, and `vcs` when version tracking is on: `roots`, `pending` (changes not yet committed), `trippedRoots` (roots whose commits keep failing) and `oldestTrippedAt`.\n\nAfter a service restart, the `health.initialScan` field shows scan progress:\n\n- `active: true` — filesystem walk in progress; `filesMatched` and `filesEnqueued` grow until the initial walk (or chokidar\'s scan) completes\n- `active: false` with `completedAt`/`durationMs` — scan finished\n\nUse this to determine if the service is still initializing after a restart.\n\n### `watcher_config`\n\nQuery the effective runtime config via JSONPath (`GET /config`). Returns the full resolved merged document when no path is provided.\n\n- `path` (string, optional) — JSONPath expression\n\nThe document holds only the allow-listed keys `description`, `watch`, `configWatch`, `search`, `schemas`, `inferenceRules`, `mapHelpers`, `templateHelpers`, `maps`, `templates`, plus `issues` (the current issues). `api`, `sources`, `embedding`, `vectorStore` (and their keys) are never included. Each rule carries `source` (`config`, or the registering source of a virtual rule) and `values` (the runtime values index). File references (`map`, `maps`, `templates`) and named schema references are always resolved. An invalid JSONPath is `400 invalid_request`.\n\n### `watcher_validate_config`\n\nValidate config and optionally test file paths (`POST /config/validate`).\n\n- `config` (object, optional) — candidate config (partial or full). Omit to validate current config.\n- `testPaths` (string[], optional) — file paths to test against the config\n\nPartial configs are merged into the running config shallowly: each top-level key replaces the running value, except `inferenceRules`, which merge by rule name. This differs from `watcher_config_apply` (deep merge), so pass a whole top-level object (e.g. all of `watch`) when validating a change inside it. Returns `{ valid: false, errors: [{ path, message }] }`, or `{ valid: true, warnings?, testResults? }`, where each test result is `{ path, matchedRules, metadata, error? }` (`error: "File not found"` when the path cannot be read). Validation problems come back in a `200` body, so this is the place to see them in full.\n\n### `watcher_config_apply`\n\nApply config changes (`POST /config/apply`, sent as `{ patch: config }`).\n\n- `config` (object, required) — full or partial config to apply\n- `replace` (boolean, optional) — replace the whole config with `config` instead of merging\n\nPartial configs are deep-merged into the config file the service runs from (objects recursively, arrays replaced, `inferenceRules` by rule name), so `{}` or a single key is valid. The merged result is validated and written to disk, then the `configWatch.reindex` scope (default `issues`) runs; that reindex covers files only. With `configWatch.enabled` (default `true`) the service picks up the written file without a restart, including new/removed watch paths (the filesystem watcher is rebuilt), inference rule changes, move detection settings, and gitignore filter updates.\n\nReturns `{ applied: true, config }`. `config` is redacted through the same allow-list as `GET /config`: `api`, `sources` and provider keys are never echoed. When the file was written but the post-write apply step failed, the body also carries `warning` (a string describing the failure). An invalid merged config is a tool error reading `HTTP 400: {"error":"Config validation failed","code":"invalid_request","issues":[...]}`, so run `watcher_validate_config` first. A config file the service cannot read or write is `HTTP 500` with the generic `internal_error` body, the cause in the service log.\n\n### `watcher_reindex`\n\nTrigger a reindex operation (`POST /reindex`). Non-prune scopes return a `plan` object showing the blast area; the live run reuses the same targets. A live prune (non-dry-run) returns immediately without a plan.\n\n**Parameters:**\n\n- `scope` (string, optional) — Reindex scope. The tool sends `"rules"` when omitted (the HTTP API\'s own default is `"issues"`). One of:\n - `"rules"` — Re-apply inference rules to every watched file (no re-embedding; changed values written, keys the rules no longer project removed). With `path`, only files matching those globs. Without `path`, every indexed source document is also retrieved through its callback and its stored rule re-applied: unchanged content gets a metadata-only update, and a document is re-embedded only if its rendered text changed.\n - `"full"` — Re-extract and re-process every watched file (a file is re-embedded when the text to embed changed or it was never indexed), and retrieve and always re-embed every indexed source document. Clears the runtime values index first.\n - `"issues"` — Re-process the documents in `watcher_list_issues`: failed files, and failed source references (with their stored rule, or the `rule` recorded on the issue when the document was never indexed).\n - `"path"` — Re-process a specific file or all files under a directory. Requires `path`. Each target must be watched and not gitignored (`403 path_not_watched`) and must exist (`404 not_found`). Files only.\n - `"prune"` — Delete Qdrant points for files no longer in watch scope (removed paths, gitignored files, deleted files). No re-embedding. Pure cleanup. Never selects source points (`source` set, no `file_path`).\n- `path` (string | string[], optional) — Target file(s) or directories for `"path"` (required there), or globs narrowing `"rules"`. Ignored by other scopes.\n- `dryRun` (boolean, optional) — When `true`, compute and return the plan without executing.\n\nThe tool has no `source` parameter. To re-process specific source references, call `POST /reindex` directly with `{ "scope": "path", "source": ["<name>://<id>", ...] }` (at most 1000): they are enqueued on the notify queue, are not tracked by `GET /status`, and send no completion callback. A reindex triggered by a config change (`watcher_config_apply`, config-file reload, rule registration) covers files only.\n\n**Response (normal):**\n\n```json\n{\n "status": "started",\n "scope": "rules",\n "plan": {\n "total": 148000,\n "toProcess": 148000,\n "toDelete": 0,\n "byRoot": { "/data": 95000, "/srv/config": 3000 }\n }\n}\n```\n\n**Response (dryRun):**\n\n```json\n{\n "status": "dry_run",\n "scope": "prune",\n "plan": {\n "total": 562000,\n "toProcess": 0,\n "toDelete": 2300,\n "byRoot": { "/srv/app/node_modules": 1800, "/srv/app/.cache": 500 }\n }\n}\n```\n\n**Plan fields:**\n\n- `total` — Points scrolled (prune), or files plus source documents covered (other scopes).\n- `toProcess` — Files plus source documents to process (0 for prune).\n- `toDelete` — Points to delete (prune only, 0 for others).\n- `byRoot` — Files grouped by watch root (for prune: orphaned files), plus one entry per source name for source documents. Shows where the impact concentrates.\n- `incomplete` — `true` when the prune scroll failed part-way and the plan is partial.\n\n**Progress:** a live run (every scope except `path` with `source`) is tracked in `watcher_status` under `health.reindex` (`filesProcessed` / `totalFiles`, counting files plus source documents). A failing file or source document is logged and recorded; the rest of the run continues.\n\n**Guidance:**\n\n- Use `dryRun: true` before any large-blast operation to preview impact.\n- `prune` is safe — it only deletes orphaned file points, never re-embeds. Use after changing watch paths, fixing gitignore, or cleaning up stale data.\n- `prune` is NOT triggered by config-watch auto-reindex (too dangerous for auto-trigger).\n\n### `watcher_scan`\n\nFilter-only point query without vector search (`POST /scan`). Use for structural queries where the question has no semantic dimension.\n\n- `filter` (object, required) — Qdrant filter object. Required to prevent accidental full-collection scans.\n- `limit` (number, optional) — page size (integer 1 to 1000), default 100\n- `cursor` (string or number, optional) — opaque cursor from previous response for pagination\n- `fields` (string[], optional) — payload fields to return (projection)\n- `countOnly` (boolean, optional) — if true, return `{ count }` instead of points\n\n**Response (normal):**\n\n```json\n{\n "points": [\n {\n "id": "uuid",\n "payload": { "file_path": "/data/tickets/PROJ-123.json" }\n }\n ],\n "cursor": "opaque string, number or null"\n}\n```\n\n**Response (countOnly):**\n\n```json\n{ "count": 1234 }\n```\n\n**Key differences from `watcher_search`:**\n\n- No `query` parameter — does NOT use embeddings\n- No `score` field — results are unranked filter matches\n- Cursor-based pagination (not offset-based); `cursor` is `null` on the last page\n- Zero cost per call beyond Qdrant\'s filtered scroll\n\n**Pagination pattern:**\n\n```\nlet cursor = undefined;\ndo {\n const result = await watcher_scan({ filter, limit: 100, cursor });\n // process result.points\n cursor = result.cursor;\n} while (cursor);\n```\n\n### `watcher_service`\n\nManage the watcher background service (install, uninstall, start, stop, restart, check status). No HTTP call.\n\n- `action` (string, required) — one of: `install`, `uninstall`, `start`, `stop`, `restart`, `status`\n\nReturns the service manager\'s response. On Windows uses NSSM, on Linux systemd, on macOS launchd. Only `install` needs `configRoot` (see [Plugin Installation](#plugin-installation)).\n\n### `watcher_list_issues`\n\nGet runtime processing failures (`GET /issues`). Returns `{ count, issues: { [key]: IssueRecord[] } }`: `count` is the number of documents with issues, and each key is a file path or a source reference (`<name>://<id>`). Each `IssueRecord` has `type` (one of the [issue codes](#error-categories)), `message`, `timestamp`, and optionally `operation`, `property`, `rule`, `rules`, `types`, `details`.\n\n### `watcher_walk`\n\nList watched files matching globs (`POST /walk`). Returns matching file paths from the watcher\'s in-memory list of watched files across all configured watch roots.\n\n- `globs` (string[], required) — glob patterns to intersect with watch paths (non-empty; matched case-insensitively, dotfiles included)\n\n**Response:**\n\n```json\n{\n "paths": ["/data/foo/.meta/meta.json", "/data/bar/.meta/meta.json"],\n "matchedCount": 2,\n "scannedRoots": ["/data", "/srv/config"]\n}\n```\n\n**Use cases:**\n\n- Discover files matching a pattern across all watched directories (e.g., `["**/.meta/meta.json"]`)\n- Enumerate files before rule registration to understand scope\n- Find files that aren\'t yet indexed (no Qdrant dependency)\n\n**Key differences from `watcher_scan`:**\n\n- Lists the files the filesystem watcher tracks, not the Qdrant index\n- No embedding or indexing required, but the initial filesystem scan must be complete: until then the tool returns `HTTP 503 scan_in_progress` (check `health.initialScan` in `watcher_status`)\n- Returns file paths only (no metadata, no vectors)\n- Applies `watch.ignored` and gitignore filtering automatically\n\n## Query Planning: Scan vs Search\n\n**Decision rule:** If the query has no semantic/natural-language dimension, use `watcher_scan`. If you need meaning-based similarity, use `watcher_search`.\n\n| Use `watcher_scan` | Use `watcher_search` |\n| --- | --- |\n| "List all files in domain X" | "Find documents about authentication" |\n| "Files modified after timestamp T" | "What discusses rate limiting?" |\n| "Enumerate paths under prefix P" | "Prior conversations about deployment" |\n| "Count files matching a condition" | "Related tickets to this issue" |\n| "Staleness detection / delta computation" | "What happened in last week\'s meetings?" |\n\n**Scan-specific filter examples:**\n\n**Domain enumeration:**\n\n```json\n{ "must": [{ "key": "domains", "match": { "value": "email" } }] }\n```\n\n**Modified after timestamp:**\n\n```json\n{ "must": [{ "key": "modified_at", "range": { "gte": 1772800000 } }] }\n```\n\n**Path prefix matching:**\n\n```json\n{ "must": [{ "key": "file_path", "match": { "text": "/data/tickets" } }] }\n```\n\n**Count files in a domain (no point data transferred):**\n\n```\nwatcher_scan: filter={"must":[{"key":"domains","match":{"value":"github"}}]}, countOnly=true\n```\n\n---\n\n## Qdrant Filter Syntax\n\nFilters use Qdrant\'s native JSON filter format, passed as the `filter` parameter to `watcher_search`.\n\n### Basic Patterns\n\n**Match exact value:**\n\n```json\n{ "must": [{ "key": "domain", "match": { "value": "email" } }] }\n```\n\n**Match text (full-text search within field):**\n\n```json\n{ "must": [{ "key": "chunk_text", "match": { "text": "authentication" } }] }\n```\n\n**Combine conditions (AND):**\n\n```json\n{\n "must": [\n { "key": "domain", "match": { "value": "jira" } },\n { "key": "status", "match": { "value": "In Progress" } }\n ]\n}\n```\n\n**Exclude (NOT):**\n\n```json\n{\n "must_not": [{ "key": "domain", "match": { "value": "repos" } }]\n}\n```\n\n**Any of (OR):**\n\n```json\n{\n "should": [\n { "key": "domain", "match": { "value": "email" } },\n { "key": "domain", "match": { "value": "slack" } }\n ]\n}\n```\n\n**Nested (combine AND + NOT):**\n\n```json\n{\n "must": [{ "key": "domain", "match": { "value": "jira" } }],\n "must_not": [{ "key": "status", "match": { "value": "Done" } }]\n}\n```\n\n### Key Differences\n\n- `match.value` — exact match (case-sensitive, for keyword fields like `domain`, `status`)\n- `match.text` — full-text match (for text fields like `chunk_text`)\n\n## Search Result Shape\n\nEach result from `watcher_search` contains:\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `id` | string | Qdrant point ID |\n| `score` | number | Similarity (or hybrid fused) score; higher = more relevant |\n| `payload.file_path` | string | Normalized file path (file points only) |\n| `payload.source` | string | Source reference `<name>://<id>` (source points only) |\n| `payload.source_rule` | string | Designated rule of a source document (source points only) |\n| `payload.chunk_text` | string | The matched text chunk |\n| `payload.chunk_index` | number | Zero-based chunk position within the document |\n| `payload.total_chunks` | number | Total chunks for this document |\n| `payload.line_start` | number | 1-indexed line where the chunk starts |\n| `payload.line_end` | number | 1-indexed line where the chunk ends |\n| `payload.created_at` | number | Creation timestamp (unix seconds) |\n| `payload.modified_at` | number | Modification timestamp (unix seconds) |\n| `payload.content_hash` | string | SHA-256 hash of the embedded text |\n| `payload.matched_rules` | string[] | Names of inference rules that matched |\n\n`file_path` is not always present: a source point (a document indexed through a source callback) carries a non-empty `source` and no `file_path`. A file point may also carry a `source` written by its own rules; it is still a file point. Additional metadata fields depend on the deployment\'s inference rules (e.g., `domain`, `status`, `author`). Use `watcher_config` to discover available fields.\n\n## Query Planning (Per Search Task)\n\nIdentify relevant rule(s) from the orientation model, then retrieve their schemas:\n\n**Retrieve complete schema for a rule:**\n\n```\nwatcher_config: path="$.inferenceRules[?(@.name==\'jira-issue\')].schema"\n```\n\nReturns the rule\'s schema array with named schema references and file references already resolved: properties, types, `set` expressions, `uiHint`, `enum`, etc.\n\n**For select/multiselect fields without `enum` in schema:**\n\n```\nwatcher_config: path="$.inferenceRules[?(@.name==\'jira-issue\')].values.status"\n```\n\nRetrieves valid filter values from the runtime values index (distinct values accumulated during embedding).\n\n**When search results span multiple rules** (indicated by `matched_rules` on results): query each unique rule\'s schema separately and merge mentally. Most result sets share the same rule combination, so this is typically one or two queries, not one per result.\n\n---\n\n## uiHint → Filter Mapping\n\nUse `uiHint` to determine filter construction strategy. **This table is explicit, not intuited:**\n\n| `uiHint` | Qdrant filter | Notes |\n| --- | --- | --- |\n| `text` | `{ "key": "<field>", "match": { "text": "<value>" } }` | Substring/keyword match |\n| `select` | `{ "key": "<field>", "match": { "value": "<enum_value>" } }` | Exact match; use `enum` values from schema or runtime values index |\n| `multiselect` | `{ "key": "<field>", "match": { "value": "<enum_value>" } }` | Any-element match on array field; use `enum` or runtime values index |\n| `date` | `{ "key": "<field>", "range": { "gte": <unix_ts>, "lt": <unix_ts> } }` | Range filter against integer fields holding Unix timestamps (seconds). Source dates should be normalized in config via `{{toUnix ...}}` in `set` expressions. Either bound optional for open-ended ranges (e.g., "after January" → `gte` only) |\n| `number` | `{ "key": "<field>", "range": { "gte": <n>, "lte": <n> } }` | Either bound optional for open-ended ranges |\n| `check` | `{ "key": "<field>", "match": { "value": true } }` | Boolean match |\n| _(absent)_ | Do not use in filters | Internal bookkeeping field, not intended for search |\n\n**Fallback:** If a `select`/`multiselect` field has neither `enum` in schema nor values in the index, treat it as `text` (substring match instead of exact match).\n\n---\n\n## Qdrant Filter Combinators\n\nCompose individual field conditions into complex queries using three combinators:\n\n| Combinator | Semantics | Use case |\n| --- | --- | --- |\n| `must` | AND — all conditions required | Intersecting constraints (domain + date range + assignee) |\n| `should` | OR — at least one must match | Alternative values, fuzzy criteria ("assigned to X or Y") |\n| `must_not` | Exclusion — any match triggers exclude | Filtering out noise (exclude Done, exclude codebase domain) |\n\n**Combinators nest arbitrarily for complex boolean logic:**\n\n```json\n{\n "must": [\n { "key": "domain", "match": { "value": "jira" } },\n { "key": "created", "range": { "gte": 1735689600 } }\n ],\n "should": [\n { "key": "assignee", "match": { "value": "Jane Doe" } },\n { "key": "assignee", "match": { "value": null } }\n ],\n "must_not": [{ "key": "status", "match": { "value": "Done" } }]\n}\n```\n\nA consuming UI will necessarily compose simple single-field filters. The assistant can compose deeply complex queries combining multiple fields, nested boolean logic, and open-ended ranges to precisely target what it needs.\n\n---\n\n## Search Execution\n\n**Plain semantic search is valid and often sufficient.** Not every query needs metadata filters. When the user\'s question is broad or exploratory, a natural language query with no filter object is the right starting point. Add filters to narrow, not as a default.\n\n**Result limit guidance:**\n\n- Default: 10 results\n- Broad discovery / exploratory: 20–30, apply score threshold cutoff from config\n- Targeted retrieval with tight filters: 5\n- Cross-domain sweep: 15–20, no domain filter, use score to separate signal from noise\n\n---\n\n## Post-Processing Guidance\n\n### Score Interpretation\n\nUse `scoreThresholds` from config (queried during orientation). Values are deployment-specific, constrained to [-1, 1]:\n\n- `strong` — minimum score for a strong match. **Action:** High confidence. Use these results directly.\n- `relevant` — minimum score for relevance. **Action:** Likely useful but verify context before relying on them.\n- `noise` — maximum score below which results are noise. **Action:** Discard. If all results fall below this threshold, broaden your query or try different terms.\n\n### Chunk Grouping\n\nMultiple results with the same `file_path` (or, for source documents, the same `source`) are chunks of one document. Read the full file for complete context.\n\n### Schema Lookup\n\nUse `matched_rules` on results to look up applicable schemas for metadata interpretation:\n\n```\nwatcher_config: path="$.inferenceRules[?(@.name==\'jira-issue\')].schema"\n```\n\n### Full Context\n\nSearch gives you chunks; use `read` with `file_path` for the complete document. A source document has no file: render it with `POST /render` and `{ "source": "<ref>" }`, which retrieves it through its callback.\n\n---\n\n## Path Testing\n\nWhen uncertain whether a file would be indexed, test it against the rules. `watcher_validate_config` with `testPaths` (and no `config`) runs the current rules against each path and returns `{ path, matchedRules, metadata }` per path (`error: "File not found"` when the file cannot be read):\n\n```\nwatcher_validate_config: testPaths=["/data/tickets/PROJ-123.json"]\n```\n\nOr call `POST /config/match` directly with `{ "paths": [...] }`. It returns `{ matches: [{ rules, watched }] }`, one entry per path, without reading the files:\n\n- `rules` lists the matching rule names; an empty array means no inference rules match\n- `watched: false` means the path falls outside watch paths or is excluded by ignore patterns\n\n---\n\n## Config Authoring\n\n### Rule Structure\n\nEach inference rule has:\n\n- `name` (required) — unique identifier\n- `description` (required) — human-readable purpose\n- `match` (required) — JSON Schema matched against file attributes (e.g. a picomatch `glob` on `file.path`)\n- `schema` (optional) — array of named schema references and/or inline schema objects with `set` templates\n- `map` (optional) — JsonMap transform (inline, named map, or `.json` file path)\n- `template` (optional) — Handlebars template (inline, named template, or `.hbs`/`.handlebars` file path)\n- `render` (optional) — declarative frontmatter + Markdown renderer (mutually exclusive with `template`)\n- `renderAs` (optional) — output extension (1-10 lowercase alphanumerics); requires `template` or `render`\n\n### Authoring Notes\n\n- **`maps` entries are bare strings or inline maps.** A string value is a file path, loaded and parsed as a JsonMap at startup. A `{ path, description }` object is kept as-is and never loaded from the file, so it silently does nothing useful. (`mapHelpers` and `templateHelpers` are different: `{ path, description }` objects are their documented form.)\n- **Metadata is derived with `set`, not `attributes`.** Each schema property\'s `set` template produces its value. There is no `attributes` key in the config schema; older configs and examples that use one need rewriting.\n- **`render.frontmatter` accepts picomatch globs and `!` exclusions** alongside explicit keys, e.g. `["meta_id", "*", "!_*", "!chunk_*"]`. Explicit names keep their listed order; keys matched by a glob follow, sorted alphabetically; a key matching an exclusion is dropped.\n- **Virtual rules have a startup window.** Virtual rules are registered at runtime by other services (`POST /rules/register`, e.g. jeeves-meta), so they are absent during the initial file scan after a watcher start. Files indexed in that window lack virtual-rule metadata until the registration\'s automatic rules reindex runs, or until `POST /rules/reapply` covers them. If metadata from a virtual rule is missing right after a restart, check whether its service has registered yet.\n\n### Config Workflow\n\n1. Edit config (or build partial config object)\n2. Validate: `watcher_validate_config` with optional `testPaths` for dry-run preview\n3. Apply: `watcher_config_apply` — validates, writes, triggers the `configWatch.reindex` reindex\n4. Monitor: `watcher_list_issues` for runtime processing failures\n\n### Reindex Concurrency\n\nReindex operations process files and source documents concurrently using `reindex.concurrency` (default 50). This applies to every scope, to issue-source discovery, and to the stored-rule lookups of `POST /reindex` with `source`. The watcher\'s incremental processing (file events and source notifications) uses a separate `embedding.concurrency` setting.\n\nProgress is reported via `watcher_status` (`health.reindex.filesProcessed` / `health.reindex.totalFiles`, counting files plus source documents).\n\n### When to Reindex\n\n- **Rules scope** (`"rules"`): Changed rule matching patterns, set expressions, schema mappings. No file re-embedding needed.\n- **Full scope** (`"full"`): Added watch paths, broad schema restructuring, or source documents whose vectors must be rebuilt. Re-extracts every file (re-embedding the ones whose text to embed changed) and re-embeds every source document.\n- **Issues scope** (`"issues"`): After fixing the root cause of processing failures (permissions, encoding, file format, a source outage). Re-processes only the failed files and source documents.\n- **Path scope** (`"path"`): Edited files in a specific directory, or a single file looks stale, and you want them re-processed without a full reindex. A file whose text to embed is unchanged is not re-embedded.\n- **Prune scope** (`"prune"`): After changing `watch.paths`, adding gitignore rules, or discovering stale/orphaned points (e.g., indexed `node_modules`). Deletes points for out-of-scope or deleted files. Always `dryRun: true` first to preview.\n\n---\n\n## Diagnostics\n\n### Escalation Path\n\n1. `watcher_status` — is the service healthy? Is a reindex running? Is the initial scan still active?\n2. `watcher_list_issues` — which documents are failing and why?\n3. `watcher_config` with `$.issues` — the same issue records via JSONPath\n4. Check logs at the configured log path\n\n### Error Categories\n\nEvery issue record\'s `type` is one of these codes:\n\n- `type_collision` — conflicting metadata types across rules (includes `property`, `rules[]`, `types[]`)\n- `interpolation_error` — template or `set` expression rendering failed (includes `property`, `rule`; `details` holds per-step failures)\n- `extraction_error` — reading, rule application or chunking failed. A file that was indexed and whose content no longer extracts (e.g. front matter with `!!float 1`, refused since 0.21.0) has its points deleted, so it is out of search until fixed; its enrichment is kept. A system error reading it (a lock, a dropped share) or a Node error (an enrichment store or extractor that fails) keeps the points\n- `embedding_error` — the embedding provider failed\n- `upsert_error` — the vector store write failed\n- `match_failed` — a source document\'s designated rule did not match (`details` holds the AJV errors)\n- `rule_missing` — a source document\'s designated rule was not found\n- `no_content` — nothing to embed\n- `source_unavailable` — the source callback failed\n- `unsupported_content_type` — the source callback returned a non-JSON body\n- `too_large` — the document is over a `processing` ceiling (`maxFileBytes`, `maxChunksPerFile` or `maxTextBytes`); it is not embedded and its points are deleted. Raise the ceiling and run `reindex` with scope `issues` to restore it\n\nSource-document issues always carry `rule` (the designated rule of the failed run).\n\n**Issues are self-healing:** resolved on successful re-process. The issues file always represents the current set of unresolved problems: a live todo list.\n\n### Change Source\n\n`watcher_status` reports the change source under `health.changeSource` (`@parcel/watcher` by default, chokidar on request or when polling):\n\n- **Service refuses to start, log names `@parcel/watcher failed to load`:** the platform binary is missing or broken. Reinstall without `--omit=optional`, or set `watch.changeSource: chokidar` (the manual switch; a reload rebuilds the watcher with one full initial scan).\n- **Rising `reconcileEvents`:** the source is missing events and reconciliation is finding them. **Rising `resubscribes`:** subscriptions keep failing (Linux). **Rising `renewFailures`:** a Linux renewal could not unsubscribe; restart the service. **`unavailableRoots` above 0:** a watch root is missing or unreadable (an unmounted share); its files keep their points until it returns. **High `pendingStability`, or a long `lastReconcileDurationMs` / `lastRenewDurationMs`:** a large write burst or a slow disk; a long renewal pauses event handling for every root.\n- **Linux, a file in a moved-in or renamed folder is indexed late:** changes inside folders nested in a newly created or moved-in tree, or in a directory renamed within a root, are missed until the root\'s subscription is renewed at the next periodic reconcile, up to `watch.reconcileIntervalMs` (15 minutes by default) late. Expected; shorten the interval if the latency matters. The files moved in with the tree are indexed at once.\n- **Linux, events lost under a write burst with nothing in the log:** parcel cannot detect an inotify queue overflow. Check `sysctl fs.inotify.max_queued_events` (recommended 1048576) and `fs.inotify.max_user_watches` (the start log warns at 90%); the periodic reconcile finds what was lost (`reconcileEvents` rises).\n- **Windows, files in deep folders indexed only by reconciliation:** expected. Under parcel, creating or changing a file whose path is 260 characters or more produces no change event at either `LongPathsEnabled` setting, so it is indexed at the next periodic reconcile, up to `watch.reconcileIntervalMs` late. Nothing to fix; check that `watch.reconcileIntervalMs` is not 0. (`LongPathsEnabled = 1` is still recommended for other tools; it is a host change for the owner.)\n- **Windows, `lastRestartCause` set:** under parcel the service never unsubscribes on Windows (`@parcel/watcher` 2.6.0 crashes the process there), so a reload that rebuilds the watcher, a subscription error or a vanished watch root exits with code 75 and the service manager restarts it. `watch-config-changed`, `subscription-error` or `root-gone` says why, `lastRestartAt` when. If the service stays down after such an exit, its service manager is not restarting it on exit (NSSM: `AppExit Default Restart`).\n\n### Index Check at Start\n\nAfter the start walk the service deletes the indexed documents of files gone from disk (deleted or moved while it was down). `watcher_status` reports it under `health.indexCheck` (`state`, `at`, `durationMs`, `checked`, `removed`, `skippedRoots`, `capped`):\n\n- **`capped` above 0:** under some watch root more than 10% of the indexed documents (once more than 20 are missing) or more than 1,000 were missing from disk or filters, so nothing was deleted there; the log names the root. Run `watcher_reindex` with scope `prune` and `dryRun: true`, check the plan, then run it.\n- **`state: failed`:** the index could not be listed (Qdrant down), or the service stopped first; it runs again at the next start.\n- **`skippedRoots` above 0:** a root was unavailable, unwatched, gone, or its start walk failed; its deletions wait for the next start or a `prune`.\n- **Files that still exist but are now ignored** keep their points: that is `prune`\'s job. `watch.indexCheckOnStart: false` turns the check off.\n\n---\n\n## Helper Management\n\nHelpers use namespace prefixing: config key becomes prefix. A helper named `slack` exports `slack_extractParticipants`.\n\nEnumerate loaded helpers:\n\n```\n$.mapHelpers — JsonMap helper namespaces with exports\n$.templateHelpers — Handlebars helper namespaces with exports\n```\n\n### Built-in JsonMap Helpers\n\nThe following helpers are available in every JsonMap `lib` context without any helper config:\n\n#### `fetchSiblings(filePath, options?)`\n\nRetrieve extracted text from neighboring files in the same directory with the same extension. Useful for contextual embedding — e.g., injecting surrounding email messages into a thread member\'s embedding for better semantic search.\n\n**Parameters:**\n\n- `filePath` (string) — the current file path (in a rule\'s `map`, `$.input.file.path`)\n- `options` (object, optional; JsonMap `params` are path strings, so a call from a config `map` uses the defaults):\n - `before` (number, default 3) — number of preceding siblings to include\n - `after` (number, default 1) — number of following siblings to include\n - `sort` (`"name"` | `"mtime"`, default `"name"`) — sort order for determining neighbor position\n\n**Returns:** `string[]` — extracted text from sibling files, in sort order. Files that fail extraction are silently skipped. For a source document (a source rule\'s `map`) there is no file reader, so it returns `[]`.\n\n**Example** (in a rule\'s `map`):\n\n```json\n{\n "context": {\n "$": { "method": "$.lib.fetchSiblings", "params": "$.input.file.path" }\n }\n}\n```\n\n---\n\n## Enrichment\n\nUse `watcher_set_metadata` to tag documents after analysis (e.g., `reviewed: true`, project labels).\n\n**Enrichments are durable.** Stored in a SQLite database (`<stateDir>/enrichments.sqlite`, default `stateDir` `.jeeves-metadata`), enrichments survive full reindexes. Enriching a file that is not indexed yet stores the enrichment, which applies once the file is indexed. When the watcher re-processes a file, enrichments are merged with inference rule output using composable semantics: scalar fields overwrite, array fields union+deduplicate.\n\n**Metadata is validated against the file\'s matched rule schemas** (for a source document, its designated rule\'s schema). Only declared property types are checked; unknown properties and `null` values pass. The tool reports a failure as `HTTP 400 invalid_request: Validation failed`; the structured `details` are in the HTTP response body:\n\n```json\n{\n "code": "invalid_request",\n "error": "Validation failed",\n "details": [\n {\n "property": "priority",\n "expected": "string",\n "received": "number",\n "rule": "jira-issue",\n "message": "Property \'priority\' is declared as string in jira-issue schema, received number"\n }\n ]\n}\n```\n\n---\n\n## Version Tracking\n\nThe watcher includes built-in history and undo for your watched files. When enabled, every change to a watched file is automatically versioned. You can view the history of any file or folder, see exactly what changed between versions, retrieve old versions, and undo changes — all through the version-control tools below.\n\n### Tools\n\n| Tool | Purpose |\n| --- | --- |\n| `watcher_get_vcs_status` | Check version tracking health: enabled state, tracked roots, remote sync status |\n| `watcher_list_vcs_history` | View change history for a file or folder, with optional date range and limit |\n| `watcher_show_vcs_file` | Retrieve the content of a file at a specific past version |\n| `watcher_diff_vcs` | Show what changed between two versions, or between a past version and now |\n| `watcher_revert_vcs` | Undo changes by restoring files to a specific past version |\n| `watcher_exclude_vcs_path` | Exclude a path from version history (it stays in search), or remove the exclusion; untracks matching files |\n| `watcher_check_vcs_exclusion` | Check whether git ignores a path and why (rule, file, line) |\n\n### Usage Patterns\n\n**View recent changes to a file:**\n\n```\nwatcher_list_vcs_history: glob="/data/tickets/PROJ-123.json", limit=10\n```\n\n**See what changed between two versions:**\n\n```\nwatcher_diff_vcs: glob="/data/tickets/PROJ-123.json", commit="abc1234"\n```\n\n**Restore a file to a previous version:**\n\n```\nwatcher_revert_vcs: glob="/data/tickets/PROJ-123.json", commit="abc1234"\n```\n\n**View old content without restoring:**\n\n```\nwatcher_show_vcs_file: path="/data/tickets/PROJ-123.json", commit="abc1234"\n```\n\n**Exclude files that don\'t need versioning** (e.g., Jira issues that are read-only syncs):\n\n```\nwatcher_exclude_vcs_path: glob="/data/tickets/**/*.json"\n```\n\n**Check if a file is excluded and why:**\n\n```\nwatcher_check_vcs_exclusion: path="/data/tickets/PROJ-123.json"\n```\n\n### Enabling Version Tracking\n\nVersion tracking is controlled by the `vcs.enabled` config setting. When set to `true`, the watcher automatically versions all watched files. No additional setup is required — just enable it and history starts accumulating.\n\nSquash retention activates by default when VCS is enabled, even without explicit `retention` config. The defaults are: 30 days max age, 100 versions max, and a daily midnight cron schedule (`0 0 * * *`). These defaults prevent unbounded commit history growth.\n\n### Excluding Paths\n\nNot all watched files need version history. For example, if Jira issues are synced read-only, there\'s nothing to "undo." Use `watcher_exclude_vcs_path` to stop tracking specific paths.\n\nThe exclusion is VCS-only: the pattern goes to the root\'s `.git/info/exclude`, so the files stop being versioned but stay indexed and searchable.\n\n- **Destructive.** Adding a pattern untracks the tracked files it matches, in one commit: their history ends at their last committed version (the files stay on disk). `remove: true` deletes the pattern but does not restore history; the files are versioned again from their next change.\n- **One gitignore pattern, root-anchored.** The pattern is written as `/` plus the glob relative to the root, so `/data/tickets/*.json` (root `/data`) matches only directly under `tickets/`; use `**/` for any depth. Pass `root` to give a root-relative glob (`tickets/**/*.json`, without a leading `/`: with `root`, a glob starting with `/` is a filesystem path and must lie under the root).\n- **Brackets are a character class.** `Report [draft].docx` matches `Report d.docx`; escape them to name the file: `Report \\[draft\\].docx`.\n- **Refused (`400`):** a newline, a leading `!` or `#`, braces, a pattern matching the whole root (`*`, `**`, `**/*`, ...), or a glob outside the given `root`.\n- **More than 100 files** to untrack need `confirm: true`; without it the tool reports `409 confirm_required` with the count and changes nothing. Check the count with the user before confirming.\n- **Busy root.** If version control is busy on the root for 30 s the tool reports `503 not_available`; retry after 30 s.\n- `watcher_check_vcs_exclusion` reports `excluded: false` for a tracked file even when a human `.gitignore` matches it (git never reports tracked paths as ignored); exclude it to untrack it.\n\n### Score Interpretation\n\nVCS tools do not use vector search. There are no relevance scores to interpret — results are exact history lookups, diffs, and file retrievals.\n\n---\n\n## Error Handling\n\nIf the watcher is unreachable:\n\n- Inform the user that semantic search is temporarily unavailable\n- Fall back to direct `read` for known file paths\n- Do not retry silently in a loop\n\nIf a tool fails with `HTTP 401` (`unauthorized`):\n\n- The watcher sets `api.apiKey` and the plugin sent no key, or a different one.\n- Ask the user to set `plugins.entries.jeeves-watcher-openclaw.config.apiKey` in `openclaw.json` (or the `JEEVES_WATCHER_API_KEY` environment variable of the gateway) to the service\'s `api.apiKey`. The tools read it on every call, so no restart is needed.\n- Never ask for, print or guess the key. `GET /status` and `GET /openapi.json` never need it.\n\nIf a tool or call fails with `HTTP 503` (`not_available`) and a `Retry-After` header:\n\n- `POST /metadata` (`watcher_set_metadata`), `POST /rules/reapply` and `POST /documents/delete` wait at most 10 s for a document that is being indexed, then answer `503` with `Retry-After: 10`. A single-document request changed nothing; a multi-document request kept the documents it already changed. All three are idempotent: retry the same request after 10 s.\n- `watcher_exclude_vcs_path` waits 30 s for a busy version-control root.\n\nIf a tool fails with `HTTP 500: {"error":"Response validation failed","code":"internal_error"}`, the watcher produced a response that doesn\'t match its own schema: a server bug. Report it with the request you made; retrying won\'t help.\n\nIf tools are unavailable (plugin not loaded in this session):\n\n- The watcher API is still accessible via direct HTTP calls\n- Use `exec` to call the endpoints listed in Service Architecture\n- Default: `http://127.0.0.1:1936`\n\n**CLI Fallbacks:**\n\n- `jeeves-watcher status [-p <port>]` — check if the service is running (`GET /status`)\n- `jeeves-watcher config validate -c <path>` — validate a config file against the schema (offline)\n- Restart via NSSM (Windows) or systemctl (Linux)\n\n---\n\n## References\n\n- [JSONPath Plus documentation](https://www.npmjs.com/package/jsonpath-plus) for JSONPath syntax\n- [Qdrant filtering documentation](https://qdrant.tech/documentation/concepts/filtering/) for advanced query patterns and search response format\n';
31102
31102
 
31103
31103
  /**
31104
31104
  * @module plugin/constants
@@ -102,7 +102,7 @@ You have access to a **semantic archive** of your human's working world. Documen
102
102
 
103
103
  ## Plugin Installation
104
104
 
105
- The plugin is a standard OpenClaw plugin. On a Jeeves box, `jeeves install watcher --config-root <path>` installs it and writes its config; otherwise use `openclaw plugins install npm:@karmaniverous/jeeves-watcher-openclaw@<version> --pin --accept-capabilities` and set `plugins.entries.jeeves-watcher-openclaw.config` yourself. Restart the gateway to load it.
105
+ The plugin is a standard OpenClaw plugin. On a Jeeves box, `jeeves install --workspace <workspace> --config-root <path> watcher` installs it and writes its config (without `--workspace` it takes the current folder as the workspace and writes platform files there); otherwise use `openclaw plugins install npm:@karmaniverous/jeeves-watcher-openclaw@<version> --pin --accept-capabilities --force` and set `plugins.entries.jeeves-watcher-openclaw.config` yourself. `--force` confirms the npm source and replaces an installed version; without it a non-interactive install is cancelled. To upgrade or roll back, run the same command with the other version. Restart the gateway to load it.
106
106
 
107
107
  `configRoot` has no default. Only `watcher_service` with `action: "install"` needs it; other `watcher_service` actions and all HTTP API tools work without it. If `watcher_service install` returns "configRoot not configured", set `configRoot` in the plugin config or the `JEEVES_CONFIG_ROOT` env var.
108
108
 
@@ -894,7 +894,7 @@ Every issue record's `type` is one of these codes:
894
894
 
895
895
  - `type_collision` — conflicting metadata types across rules (includes `property`, `rules[]`, `types[]`)
896
896
  - `interpolation_error` — template or `set` expression rendering failed (includes `property`, `rule`; `details` holds per-step failures)
897
- - `extraction_error` — reading, rule application or chunking failed
897
+ - `extraction_error` — reading, rule application or chunking failed. A file that was indexed and whose content no longer extracts (e.g. front matter with `!!float 1`, refused since 0.21.0) has its points deleted, so it is out of search until fixed; its enrichment is kept. A system error reading it (a lock, a dropped share) or a Node error (an enrichment store or extractor that fails) keeps the points
898
898
  - `embedding_error` — the embedding provider failed
899
899
  - `upsert_error` — the vector store write failed
900
900
  - `match_failed` — a source document's designated rule did not match (`details` holds the AJV errors)
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * @module plugin/distSmoke.test
3
- * Smoke test of the built plugin (`dist/index.js`, as OpenClaw loads it): it registers the 18 tools in their 0.17.0 order (under their D77 names) from its bundled watcher-core and Zod, keeps `watcher_search` and `watcher_scan` `direct-only`, and a catalog-built tool sends the bearer key. Runs after `npm run build`; in CI (which builds before testing) a missing `dist` fails.
3
+ * Smoke test of the built plugin (`dist/index.js`, as OpenClaw loads it): it registers the 18 tools in their 0.17.0 order (under their D77 names) from its bundled watcher-core and Zod, keeps `watcher_search` and `watcher_scan` `direct-only`, and a catalog-built tool sends the bearer key. Runs after `npm run build`; in CI (which builds before testing) a missing `dist` fails. A `dist` older than its inputs (the plugin's and watcher-core's source, and watcher-core's build), or bundling a watcher-core build older than watcher-core's source, is skipped with a rebuild message instead of tested (spec D201); in CI it fails.
4
4
  */
5
5
  export {};
@@ -0,0 +1,20 @@
1
+ /**
2
+ * @module test/distFreshness
3
+ * Whether a build output is older than its inputs (spec D201): the newest modification time of the files under the input directories, test files and `test` folders excluded, against the output's. Reads the file system; test-only.
4
+ */
5
+ /**
6
+ * Newest modification time of the build inputs under a directory: every
7
+ * file at any depth except test files and anything under a `test` folder.
8
+ *
9
+ * @param dir - The directory (missing: `0`).
10
+ * @returns The newest `mtimeMs`, or `0` when there is no input.
11
+ */
12
+ export declare function newestInputMtimeMs(dir: string): number;
13
+ /**
14
+ * Whether a built file is older than any input under the given directories.
15
+ *
16
+ * @param builtFile - The build output (must exist).
17
+ * @param inputDirs - Directories holding its inputs.
18
+ * @returns `true` when some input changed after the build.
19
+ */
20
+ export declare function isOlderThanInputs(builtFile: string, inputDirs: readonly string[]): boolean;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @module test/distFreshness.test
3
+ * The stale-build check behind the dist smoke test (spec D201): a build is fresh until an input under any input directory changes after it; test files, `test` folders and missing directories do not count.
4
+ */
5
+ export {};
@@ -2,7 +2,7 @@
2
2
  "id": "jeeves-watcher-openclaw",
3
3
  "name": "Jeeves Watcher",
4
4
  "description": "Semantic search, metadata enrichment, and instance administration for a jeeves-watcher deployment.",
5
- "version": "0.18.0-0",
5
+ "version": "0.18.0",
6
6
  "skills": ["dist/skills/jeeves-watcher"],
7
7
  "configSchema": {
8
8
  "type": "object",
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  },
6
6
  "dependencies": {
7
7
  "@karmaniverous/jeeves": "0.9.0",
8
- "@karmaniverous/jeeves-watcher-core": "^0.5.0-3"
8
+ "@karmaniverous/jeeves-watcher-core": "^0.5.0"
9
9
  },
10
10
  "description": "OpenClaw plugin for jeeves-watcher — semantic search and metadata enrichment tools",
11
11
  "devDependencies": {
@@ -120,5 +120,5 @@
120
120
  },
121
121
  "type": "module",
122
122
  "types": "dist/index.d.ts",
123
- "version": "0.18.0-0"
123
+ "version": "0.18.0"
124
124
  }