@karmaniverous/jeeves-watcher-openclaw 0.16.1 → 0.18.0-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.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: jeeves-watcher
3
3
  description: >
4
- Semantic search across a structured document archive. Use when you need to recall prior context, find documents, answer questions that require searching across indexed domains, enrich document metadata, manage watcher config, or diagnose indexing issues.
4
+ Semantic search of the indexed archive: recall prior context, find documents, scan, enrich metadata, watcher config, reindex, indexing issues, file history.
5
5
  ---
6
6
 
7
7
  # jeeves-watcher — Search, Discovery & Administration
@@ -12,9 +12,13 @@ The watcher is an HTTP API running as a background service (typically NSSM on Wi
12
12
 
13
13
  **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.
14
14
 
15
- **Health check:** `GET /status` returns `name`, `version`, `uptime`, `status` (`healthy`/`degraded`/`unhealthy`), and a `health` object containing `collection` (point count, dimensions), `reindex` status, and `initialScan` progress.
15
+ **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.
16
16
 
17
- **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.config.apiUrl` in `openclaw.json` if overridden):
17
+ **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.)
18
+
19
+ **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.
20
+
21
+ **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`:
18
22
 
19
23
  ```
20
24
  # Health check
@@ -34,25 +38,30 @@ curl http://127.0.0.1:<PORT>/config
34
38
  | Endpoint | Method | Purpose |
35
39
  | --- | --- | --- |
36
40
  | `/status` | GET | Health check, uptime, collection stats |
41
+ | `/openapi.json` | GET | OpenAPI 3.1 document of the catalog routes (no key needed) |
37
42
  | `/search` | POST | Semantic search (main query interface) |
38
- | `/config` | GET | Full resolved config; optional `?path=<jsonpath>` filter |
43
+ | `/config` | GET | Public resolved config (allow-listed keys plus `issues`); optional `?path=<jsonpath>` filter |
39
44
  | `/config/validate` | POST | Validate candidate config |
40
45
  | `/config/apply` | POST | Apply config changes |
41
46
  | `/reindex` | POST | Trigger reindex |
42
- | `/metadata` | POST | Enrich document metadata |
47
+ | `/metadata` | POST | Enrich document metadata (file path or source reference) |
43
48
  | `/scan` | POST | Filter-only point query (no embeddings) |
44
- | `/walk` | POST | Filesystem walk with glob intersection |
45
- | `/issues` | GET | Runtime embedding failures |
49
+ | `/walk` | POST | Watched-file listing with glob intersection |
50
+ | `/issues` | GET | Runtime processing failures (file paths and source references) |
51
+ | `/documents` | POST | Notify changed source documents (`{ source, rule }`) |
52
+ | `/documents/delete` | POST | Delete source documents by reference or filter |
46
53
  | `/rules/register` | POST | Register virtual inference rules (auto-triggers rules reindex) |
47
- | `/rules/unregister` | DELETE | Remove virtual rules by source |
54
+ | `/rules/unregister` | DELETE | Remove virtual rules by source (`{ source }` body) |
48
55
  | `/rules/unregister/:source` | DELETE | Remove virtual rules by source (parameterized) |
49
- | `/rules/reapply` | POST | Re-apply rules to points |
56
+ | `/rules/reapply` | POST | Re-apply rules to indexed files matching `globs` |
50
57
  | `/points/delete` | POST | Delete points matching a Qdrant filter |
51
- | `/render` | POST | Render a template for a document |
52
- | `/search/facets` | GET | Get search facet values |
53
- | `/rebuild-metadata` | POST | Rebuild enrichment metadata from Qdrant |
58
+ | `/render` | POST | Render a document (file path or source reference) through its rule |
59
+ | `/search/facets` | GET | Get search facet definitions and values (`?rules=a,b` scopes them to those rules) |
60
+ | `/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 |
61
+ | `/rebuild-metadata` | POST | Rebuild the enrichment store from Qdrant file point payloads |
54
62
  | `/config/schema` | GET | Get config JSON schema |
55
- | `/config/match` | POST | Match config paths against rules |
63
+ | `/config/match` | POST | Test file paths against inference rules and watch scope |
64
+ | `/vcs/*` | GET/POST | Version tracking (`status`, `history`, `show`, `diff`, `check-exclusion`, `revert`, `exclude`) |
56
65
 
57
66
  **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.
58
67
 
@@ -82,9 +91,9 @@ You have access to a **semantic archive** of your human's working world. Documen
82
91
 
83
92
  **STOP and ask for a human decision before:**
84
93
 
85
- - **Renaming, moving, or reorganizing watched directories.** The watcher has **move detection** enabled by default (`watch.moveDetection.enabled: true`). When a file is moved, chokidar emits unlink + add events; 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). **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.
94
+ - **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.
86
95
  - **Changing `watch.paths` to add large directory trees.** Adding a new watch root triggers initial indexing of every matching file under it.
87
- - **Running `scope: "full"` reindex.** This re-embeds the entire index. Use `scope: "rules"` for inference rule changes (zero embedding cost — only metadata reapplication).
96
+ - **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).
88
97
  - **Any bulk file operation** (mass copy, mass move, template-based file generation) under watched paths.
89
98
 
90
99
  **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.
@@ -102,7 +111,7 @@ The plugin is a standard OpenClaw plugin. On a Jeeves box, `jeeves install watch
102
111
  If the watcher service is already running and healthy:
103
112
 
104
113
  1. **Search** — use `watcher_search` with a natural language query and optional metadata filters
105
- 2. **Read source** — use `read` (standard file read) with `file_path` from search results for full document content
114
+ 2. **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>" }`).
106
115
 
107
116
  ## Bootstrap (First-Time Setup)
108
117
 
@@ -112,13 +121,13 @@ When the plugin loads and the watcher service is NOT yet set up, drive the entir
112
121
 
113
122
  ### Step 1: Check Node.js
114
123
 
115
- Verify Node.js is installed and version ≥ 22.13:
124
+ Verify Node.js is installed and version ≥ 24.22.2:
116
125
 
117
126
  ```bash
118
127
  node --version
119
128
  ```
120
129
 
121
- If missing or too old, guide the user to install Node.js 22.13+ from https://nodejs.org or via their package manager.
130
+ If 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.
122
131
 
123
132
  ### Step 2: Install Qdrant
124
133
 
@@ -281,10 +290,10 @@ Then generate a starter config file. Example minimal config:
281
290
  }
282
291
  ```
283
292
 
284
- Write the config to a sensible location (e.g., `~/.config/jeeves-watcher.config.json` on Linux, or alongside the user's workspace). Validate with:
293
+ Write 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:
285
294
 
286
295
  ```bash
287
- jeeves-watcher validate -c <config-path>
296
+ jeeves-watcher config validate -c <config-path>
288
297
  ```
289
298
 
290
299
  ### Step 6: Register and Start as a Service
@@ -316,9 +325,9 @@ sudo systemctl enable --now jeeves-watcher
316
325
  **Windows (NSSM):**
317
326
 
318
327
  ```powershell
319
- jeeves-watcher service install
328
+ jeeves-watcher service install -c <config-path>
320
329
  # Or manually:
321
- nssm install jeeves-watcher "$(which jeeves-watcher)" start -c <config-path>
330
+ nssm install jeeves-watcher (Get-Command jeeves-watcher).Source start -c <config-path>
322
331
  nssm set jeeves-watcher AppEnvironmentExtra GOOGLE_API_KEY=<key>
323
332
  nssm start jeeves-watcher
324
333
  ```
@@ -337,7 +346,7 @@ Call `watcher_status` (or `curl http://127.0.0.1:1936/status`). Confirm:
337
346
  - Qdrant collection exists with expected dimensions (3072)
338
347
  - Point count is increasing (initial indexing in progress)
339
348
 
340
- If the point count is 0 after a minute, check `watcher_issues` for embedding failures.
349
+ If the point count is 0 after a minute, check `watcher_list_issues` for processing failures.
341
350
 
342
351
  ### Step 8: Orientation
343
352
 
@@ -359,70 +368,83 @@ On sessions after bootstrap is complete:
359
368
 
360
369
  ## Tools
361
370
 
371
+ The 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)).
372
+
362
373
  ### `watcher_search`
363
374
 
364
- Semantic search over indexed documents.
375
+ Semantic search over indexed documents (`POST /search`).
365
376
 
366
- - `query` (string, required) — natural language search query
367
- - `limit` (number, optional) — max results, default 10
368
- - `offset` (number, optional) — skip N results for pagination
377
+ - `query` (string, required) — natural language search query (non-empty)
378
+ - `limit` (number, optional) — max results (positive integer), default 10
379
+ - `offset` (number, optional) — skip N results for pagination (non-negative integer)
369
380
  - `filter` (object, optional) — Qdrant filter for metadata filtering
370
381
 
371
- ### `watcher_enrich`
382
+ Returns 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`.
383
+
384
+ ### `watcher_set_metadata`
372
385
 
373
- Set or update metadata on a document.
386
+ Set or update metadata on a document (`POST /metadata`).
374
387
 
375
- - `path` (string, required) — file path of the document
388
+ - `path` (string, required) — file path of the document (absolute or watched path)
376
389
  - `metadata` (object, required) — key-value metadata to merge
377
390
 
391
+ Returns `{ 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`).
392
+
378
393
  ### `watcher_status`
379
394
 
380
- Service health check. Returns `name`, `version`, `uptime`, `status`, and `health` object with collection stats, reindex status, and initial scan progress.
395
+ Service 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`.
381
396
 
382
397
  After a service restart, the `health.initialScan` field shows scan progress:
383
398
 
384
- - `active: true` — filesystem walk in progress; `filesMatched` and `filesEnqueued` grow until chokidar completes
399
+ - `active: true` — filesystem walk in progress; `filesMatched` and `filesEnqueued` grow until the initial walk (or chokidar's scan) completes
385
400
  - `active: false` with `completedAt`/`durationMs` — scan finished
386
401
 
387
402
  Use this to determine if the service is still initializing after a restart.
388
403
 
389
404
  ### `watcher_config`
390
405
 
391
- Query the effective runtime config via JSONPath. Returns the full resolved merged document when no path is provided.
406
+ Query the effective runtime config via JSONPath (`GET /config`). Returns the full resolved merged document when no path is provided.
392
407
 
393
408
  - `path` (string, optional) — JSONPath expression
394
409
 
395
- ### `watcher_validate`
410
+ The 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`.
411
+
412
+ ### `watcher_validate_config`
396
413
 
397
- Validate config and optionally test file paths.
414
+ Validate config and optionally test file paths (`POST /config/validate`).
398
415
 
399
416
  - `config` (object, optional) — candidate config (partial or full). Omit to validate current config.
400
417
  - `testPaths` (string[], optional) — file paths to test against the config
401
418
 
402
- Partial configs merge with current config by rule name. If `config` is omitted, tests against the running config.
419
+ Partial 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.
403
420
 
404
421
  ### `watcher_config_apply`
405
422
 
406
- Apply config changes atomically.
423
+ Apply config changes (`POST /config/apply`, sent as `{ patch: config }`).
407
424
 
408
425
  - `config` (object, required) — full or partial config to apply
426
+ - `replace` (boolean, optional) — replace the whole config with `config` instead of merging
427
+
428
+ Partial 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.
409
429
 
410
- Partial configs are deep-merged into the running config (objects recursively, arrays replaced, `inferenceRules` by rule name), so `{}` or a single key is valid. The merged result is validated, written to disk, and triggers configured reindex behavior. Returns validation errors if invalid. Config changes take full effect without service restart — including new/removed watch paths (filesystem watcher is rebuilt), inference rule changes, move detection settings, and gitignore filter updates.
430
+ Returns `{ 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.
411
431
 
412
432
  ### `watcher_reindex`
413
433
 
414
- Trigger a reindex operation. Non-prune scopes return a `plan` object showing blast area before execution begins. Live prune (non-dry-run) returns immediately without a plan.
434
+ Trigger 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.
415
435
 
416
436
  **Parameters:**
417
437
 
418
- - `scope` (string, optional) — Reindex scope. Default: `"rules"`. One of:
419
- - `"rules"` — Re-apply inference rules to all watched files. No re-embedding. Lightweight.
420
- - `"full"` — Re-extract text, re-embed, and re-apply rules for all watched files. Expensive.
421
- - `"issues"` — Re-process only files that previously failed embedding (from `watcher_issues`).
422
- - `"path"` — Re-embed a specific file or all files under a directory. Requires `path` parameter.
423
- - `"prune"` — Delete Qdrant points for files no longer in watch scope (removed paths, gitignored files, stale data). No re-embedding. Pure cleanup.
424
- - `path` (string, required when scope is `"path"`) — Target file or directory path.
425
- - `dryRun` (boolean, optional) — When `true`, compute and return the blast area plan without executing. Returns synchronously.
438
+ - `scope` (string, optional) — Reindex scope. The tool sends `"rules"` when omitted (the HTTP API's own default is `"issues"`). One of:
439
+ - `"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.
440
+ - `"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.
441
+ - `"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).
442
+ - `"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.
443
+ - `"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`).
444
+ - `path` (string | string[], optional) — Target file(s) or directories for `"path"` (required there), or globs narrowing `"rules"`. Ignored by other scopes.
445
+ - `dryRun` (boolean, optional) — When `true`, compute and return the plan without executing.
446
+
447
+ The 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.
426
448
 
427
449
  **Response (normal):**
428
450
 
@@ -434,7 +456,7 @@ Trigger a reindex operation. Non-prune scopes return a `plan` object showing bla
434
456
  "total": 148000,
435
457
  "toProcess": 148000,
436
458
  "toDelete": 0,
437
- "byRoot": { "j:/domains": 95000, "j:/config": 3000 }
459
+ "byRoot": { "/data": 95000, "/srv/config": 3000 }
438
460
  }
439
461
  }
440
462
  ```
@@ -449,31 +471,34 @@ Trigger a reindex operation. Non-prune scopes return a `plan` object showing bla
449
471
  "total": 562000,
450
472
  "toProcess": 0,
451
473
  "toDelete": 2300,
452
- "byRoot": { "j:/jeeves/node_modules": 1800, "j:/jeeves/.bridge": 500 }
474
+ "byRoot": { "/srv/app/node_modules": 1800, "/srv/app/.cache": 500 }
453
475
  }
454
476
  }
455
477
  ```
456
478
 
457
479
  **Plan fields:**
458
480
 
459
- - `total` — Total points (prune) or files (other scopes) examined.
460
- - `toProcess` — Items to embed/re-apply rules (0 for prune).
481
+ - `total` — Points scrolled (prune), or files plus source documents covered (other scopes).
482
+ - `toProcess` — Files plus source documents to process (0 for prune).
461
483
  - `toDelete` — Points to delete (prune only, 0 for others).
462
- - `byRoot` — Counts grouped by watch root prefix. Shows where the impact concentrates.
484
+ - `byRoot` — Files grouped by watch root (for prune: orphaned files), plus one entry per source name for source documents. Shows where the impact concentrates.
485
+ - `incomplete` — `true` when the prune scroll failed part-way and the plan is partial.
486
+
487
+ **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.
463
488
 
464
489
  **Guidance:**
465
490
 
466
491
  - Use `dryRun: true` before any large-blast operation to preview impact.
467
- - `prune` is safe — it only deletes orphaned points, never re-embeds. Use after changing watch paths, fixing gitignore, or cleaning up stale data.
492
+ - `prune` is safe — it only deletes orphaned file points, never re-embeds. Use after changing watch paths, fixing gitignore, or cleaning up stale data.
468
493
  - `prune` is NOT triggered by config-watch auto-reindex (too dangerous for auto-trigger).
469
494
 
470
495
  ### `watcher_scan`
471
496
 
472
- Filter-only point query without vector search. Use for structural queries where the question has no semantic dimension.
497
+ Filter-only point query without vector search (`POST /scan`). Use for structural queries where the question has no semantic dimension.
473
498
 
474
499
  - `filter` (object, required) — Qdrant filter object. Required to prevent accidental full-collection scans.
475
- - `limit` (number, optional) — page size, default 100, max 1000
476
- - `cursor` (string, optional) — opaque cursor from previous response for pagination
500
+ - `limit` (number, optional) — page size (integer 1 to 1000), default 100
501
+ - `cursor` (string or number, optional) — opaque cursor from previous response for pagination
477
502
  - `fields` (string[], optional) — payload fields to return (projection)
478
503
  - `countOnly` (boolean, optional) — if true, return `{ count }` instead of points
479
504
 
@@ -481,8 +506,13 @@ Filter-only point query without vector search. Use for structural queries where
481
506
 
482
507
  ```json
483
508
  {
484
- "points": [{ "id": "uuid", "payload": { ... } }],
485
- "cursor": "opaque-string-or-null"
509
+ "points": [
510
+ {
511
+ "id": "uuid",
512
+ "payload": { "file_path": "/data/tickets/PROJ-123.json" }
513
+ }
514
+ ],
515
+ "cursor": "opaque string, number or null"
486
516
  }
487
517
  ```
488
518
 
@@ -496,7 +526,7 @@ Filter-only point query without vector search. Use for structural queries where
496
526
 
497
527
  - No `query` parameter — does NOT use embeddings
498
528
  - No `score` field — results are unranked filter matches
499
- - Cursor-based pagination (not offset-based)
529
+ - Cursor-based pagination (not offset-based); `cursor` is `null` on the last page
500
530
  - Zero cost per call beyond Qdrant's filtered scroll
501
531
 
502
532
  **Pagination pattern:**
@@ -512,29 +542,29 @@ do {
512
542
 
513
543
  ### `watcher_service`
514
544
 
515
- Manage the watcher background service (install, uninstall, start, stop, restart, check status).
545
+ Manage the watcher background service (install, uninstall, start, stop, restart, check status). No HTTP call.
516
546
 
517
547
  - `action` (string, required) — one of: `install`, `uninstall`, `start`, `stop`, `restart`, `status`
518
548
 
519
- Returns the service manager's response. On Windows uses NSSM, on Linux uses systemd.
549
+ Returns the service manager's response. On Windows uses NSSM, on Linux systemd, on macOS launchd. Only `install` needs `configRoot` (see [Plugin Installation](#plugin-installation)).
520
550
 
521
- ### `watcher_issues`
551
+ ### `watcher_list_issues`
522
552
 
523
- Get runtime embedding failures. Returns `{ filePath: IssueRecord }` showing files that failed and why.
553
+ Get 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`.
524
554
 
525
555
  ### `watcher_walk`
526
556
 
527
- Walk watched filesystem paths with glob intersection. Returns matching file paths from all configured watch roots.
557
+ List 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.
528
558
 
529
- - `globs` (string[], required) — glob patterns to intersect with watch paths
559
+ - `globs` (string[], required) — glob patterns to intersect with watch paths (non-empty; matched case-insensitively, dotfiles included)
530
560
 
531
561
  **Response:**
532
562
 
533
563
  ```json
534
564
  {
535
- "paths": ["j:/domains/foo/.meta/meta.json", "j:/domains/bar/.meta/meta.json"],
565
+ "paths": ["/data/foo/.meta/meta.json", "/data/bar/.meta/meta.json"],
536
566
  "matchedCount": 2,
537
- "scannedRoots": ["j:/domains", "j:/config"]
567
+ "scannedRoots": ["/data", "/srv/config"]
538
568
  }
539
569
  ```
540
570
 
@@ -542,12 +572,12 @@ Walk watched filesystem paths with glob intersection. Returns matching file path
542
572
 
543
573
  - Discover files matching a pattern across all watched directories (e.g., `["**/.meta/meta.json"]`)
544
574
  - Enumerate files before rule registration to understand scope
545
- - Find files that aren't yet indexed (no Qdrant dependency — works even before first embedding)
575
+ - Find files that aren't yet indexed (no Qdrant dependency)
546
576
 
547
577
  **Key differences from `watcher_scan`:**
548
578
 
549
- - Walks the actual filesystem, not the Qdrant index
550
- - No embedding or indexing required — works immediately after service start
579
+ - Lists the files the filesystem watcher tracks, not the Qdrant index
580
+ - 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`)
551
581
  - Returns file paths only (no metadata, no vectors)
552
582
  - Applies `watch.ignored` and gitignore filtering automatically
553
583
 
@@ -580,7 +610,7 @@ Walk watched filesystem paths with glob intersection. Returns matching file path
580
610
  **Path prefix matching:**
581
611
 
582
612
  ```json
583
- { "must": [{ "key": "file_path", "match": { "text": "j:/domains/jira" } }] }
613
+ { "must": [{ "key": "file_path", "match": { "text": "/data/tickets" } }] }
584
614
  ```
585
615
 
586
616
  **Count files in a domain (no point data transferred):**
@@ -660,15 +690,21 @@ Each result from `watcher_search` contains:
660
690
  | Field | Type | Description |
661
691
  | --- | --- | --- |
662
692
  | `id` | string | Qdrant point ID |
663
- | `score` | number | Similarity score (0-1, higher = more relevant) |
664
- | `payload.file_path` | string | Source file path |
693
+ | `score` | number | Similarity (or hybrid fused) score; higher = more relevant |
694
+ | `payload.file_path` | string | Normalized file path (file points only) |
695
+ | `payload.source` | string | Source reference `<name>://<id>` (source points only) |
696
+ | `payload.source_rule` | string | Designated rule of a source document (source points only) |
665
697
  | `payload.chunk_text` | string | The matched text chunk |
666
- | `payload.chunk_index` | number | Chunk position within the file |
667
- | `payload.total_chunks` | number | Total chunks for this file |
668
- | `payload.content_hash` | string | Hash of the full document content |
698
+ | `payload.chunk_index` | number | Zero-based chunk position within the document |
699
+ | `payload.total_chunks` | number | Total chunks for this document |
700
+ | `payload.line_start` | number | 1-indexed line where the chunk starts |
701
+ | `payload.line_end` | number | 1-indexed line where the chunk ends |
702
+ | `payload.created_at` | number | Creation timestamp (unix seconds) |
703
+ | `payload.modified_at` | number | Modification timestamp (unix seconds) |
704
+ | `payload.content_hash` | string | SHA-256 hash of the embedded text |
669
705
  | `payload.matched_rules` | string[] | Names of inference rules that matched |
670
706
 
671
- Additional metadata fields depend on the deployment's inference rules (e.g., `domain`, `status`, `author`). Use `watcher_config` to discover available fields.
707
+ `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.
672
708
 
673
709
  ## Query Planning (Per Search Task)
674
710
 
@@ -678,10 +714,9 @@ Identify relevant rule(s) from the orientation model, then retrieve their schema
678
714
 
679
715
  ```
680
716
  watcher_config: path="$.inferenceRules[?(@.name=='jira-issue')].schema"
681
- resolve=["files","globals"]
682
717
  ```
683
718
 
684
- Returns the fully merged schema with properties, types, `set` provenance, `uiHint`, `enum`, etc.
719
+ Returns the rule's schema array with named schema references and file references already resolved: properties, types, `set` expressions, `uiHint`, `enum`, etc.
685
720
 
686
721
  **For select/multiselect fields without `enum` in schema:**
687
722
 
@@ -704,7 +739,7 @@ Use `uiHint` to determine filter construction strategy. **This table is explicit
704
739
  | `text` | `{ "key": "<field>", "match": { "text": "<value>" } }` | Substring/keyword match |
705
740
  | `select` | `{ "key": "<field>", "match": { "value": "<enum_value>" } }` | Exact match; use `enum` values from schema or runtime values index |
706
741
  | `multiselect` | `{ "key": "<field>", "match": { "value": "<enum_value>" } }` | Any-element match on array field; use `enum` or runtime values index |
707
- | `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. | for open-ended ranges (e.g., "after January" → `gte` only) |
742
+ | `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) |
708
743
  | `number` | `{ "key": "<field>", "range": { "gte": <n>, "lte": <n> } }` | Either bound optional for open-ended ranges |
709
744
  | `check` | `{ "key": "<field>", "match": { "value": true } }` | Boolean match |
710
745
  | _(absent)_ | Do not use in filters | Internal bookkeeping field, not intended for search |
@@ -768,7 +803,7 @@ Use `scoreThresholds` from config (queried during orientation). Values are deplo
768
803
 
769
804
  ### Chunk Grouping
770
805
 
771
- Multiple results with the same `file_path` are chunks of one document. Read the full file for complete context.
806
+ Multiple 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.
772
807
 
773
808
  ### Schema Lookup
774
809
 
@@ -776,27 +811,25 @@ Use `matched_rules` on results to look up applicable schemas for metadata interp
776
811
 
777
812
  ```
778
813
  watcher_config: path="$.inferenceRules[?(@.name=='jira-issue')].schema"
779
- resolve=["files","globals"]
780
814
  ```
781
815
 
782
816
  ### Full Context
783
817
 
784
- Search gives you chunks; use `read` with `file_path` for the complete document.
818
+ Search 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.
785
819
 
786
820
  ---
787
821
 
788
822
  ## Path Testing
789
823
 
790
- When uncertain whether a file is indexed, use the path test endpoint:
824
+ When 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):
791
825
 
792
826
  ```
793
- watcher_config: path="$.inferenceRules[?(@.name=='<rule>')].match"
827
+ watcher_validate_config: testPaths=["/data/tickets/PROJ-123.json"]
794
828
  ```
795
829
 
796
- Or check if a specific path would match:
830
+ Or call `POST /config/match` directly with `{ "paths": [...] }`. It returns `{ matches: [{ rules, watched }] }`, one entry per path, without reading the files:
797
831
 
798
- - Returns matching rule names and watch scope status
799
- - Empty `rules` array means no inference rules match
832
+ - `rules` lists the matching rule names; an empty array means no inference rules match
800
833
  - `watched: false` means the path falls outside watch paths or is excluded by ignore patterns
801
834
 
802
835
  ---
@@ -809,31 +842,40 @@ Each inference rule has:
809
842
 
810
843
  - `name` (required) — unique identifier
811
844
  - `description` (required) — human-readable purpose
812
- - `match` — JSON Schema with picomatch glob for path matching
813
- - `schema` — array of named schema references and/or inline schema objects with `set` templates
814
- - `map` (optional) — named JsonMap transform
815
- - `template` (optional) — named Handlebars template
845
+ - `match` (required) — JSON Schema matched against file attributes (e.g. a picomatch `glob` on `file.path`)
846
+ - `schema` (optional) — array of named schema references and/or inline schema objects with `set` templates
847
+ - `map` (optional) — JsonMap transform (inline, named map, or `.json` file path)
848
+ - `template` (optional) — Handlebars template (inline, named template, or `.hbs`/`.handlebars` file path)
849
+ - `render` (optional) — declarative frontmatter + Markdown renderer (mutually exclusive with `template`)
850
+ - `renderAs` (optional) — output extension (1-10 lowercase alphanumerics); requires `template` or `render`
851
+
852
+ ### Authoring Notes
853
+
854
+ - **`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.)
855
+ - **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.
856
+ - **`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.
857
+ - **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.
816
858
 
817
859
  ### Config Workflow
818
860
 
819
861
  1. Edit config (or build partial config object)
820
- 2. Validate: `watcher_validate` with optional `testPaths` for dry-run preview
821
- 3. Apply: `watcher_config_apply` — validates, writes, triggers reindex
822
- 4. Monitor: `watcher_issues` for runtime embedding failures
862
+ 2. Validate: `watcher_validate_config` with optional `testPaths` for dry-run preview
863
+ 3. Apply: `watcher_config_apply` — validates, writes, triggers the `configWatch.reindex` reindex
864
+ 4. Monitor: `watcher_list_issues` for runtime processing failures
823
865
 
824
866
  ### Reindex Concurrency
825
867
 
826
- Reindex operations process files concurrently using `reindex.concurrency` (default 50). This applies to full reindex, rules reindex, issues reindex, and the `POST /reindex` endpoint. The watcher's incremental file processing (chokidar) uses a separate `embedding.concurrency` setting.
868
+ Reindex 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.
827
869
 
828
- Progress is reported via `watcher_status` (`reindex.filesProcessed` / `reindex.totalFiles`).
870
+ Progress is reported via `watcher_status` (`health.reindex.filesProcessed` / `health.reindex.totalFiles`, counting files plus source documents).
829
871
 
830
872
  ### When to Reindex
831
873
 
832
- - **Rules scope** (`"rules"`): Changed rule matching patterns, set expressions, schema mappings. No re-embedding needed.
833
- - **Full scope** (`"full"`): Changed embedding config, added watch paths, broad schema restructuring. Re-embeds everything.
834
- - **Issues scope** (`"issues"`): After fixing the root cause of embedding failures (permissions, encoding, file format). Re-processes only failed files.
835
- - **Path scope** (`"path"`): Edited files in a specific directory and want to force re-embedding without a full reindex. Or a single file's embedding looks wrong.
836
- - **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 files. Always `dryRun: true` first to preview.
874
+ - **Rules scope** (`"rules"`): Changed rule matching patterns, set expressions, schema mappings. No file re-embedding needed.
875
+ - **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.
876
+ - **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.
877
+ - **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.
878
+ - **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.
837
879
 
838
880
  ---
839
881
 
@@ -842,19 +884,50 @@ Progress is reported via `watcher_status` (`reindex.filesProcessed` / `reindex.t
842
884
  ### Escalation Path
843
885
 
844
886
  1. `watcher_status` — is the service healthy? Is a reindex running? Is the initial scan still active?
845
- 2. `watcher_issues` — what files are failing and why?
846
- 3. `watcher_config` with `$.issues` — same data via JSONPath
887
+ 2. `watcher_list_issues` — which documents are failing and why?
888
+ 3. `watcher_config` with `$.issues` — the same issue records via JSONPath
847
889
  4. Check logs at the configured log path
848
890
 
849
891
  ### Error Categories
850
892
 
851
- - `type_collision` — metadata field type mismatch during extraction (includes `property`, `rules[]`, `types[]`)
852
- - `interpolation` / `interpolation_error` — template/set expression failed to resolve (includes `property`, `rule`)
853
- - `read_failure` — file couldn't be read (permissions, encoding)
854
- - `embedding` — embedding API error
893
+ Every issue record's `type` is one of these codes:
894
+
895
+ - `type_collision` — conflicting metadata types across rules (includes `property`, `rules[]`, `types[]`)
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
898
+ - `embedding_error` — the embedding provider failed
899
+ - `upsert_error` — the vector store write failed
900
+ - `match_failed` — a source document's designated rule did not match (`details` holds the AJV errors)
901
+ - `rule_missing` — a source document's designated rule was not found
902
+ - `no_content` — nothing to embed
903
+ - `source_unavailable` — the source callback failed
904
+ - `unsupported_content_type` — the source callback returned a non-JSON body
905
+ - `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
906
+
907
+ Source-document issues always carry `rule` (the designated rule of the failed run).
855
908
 
856
909
  **Issues are self-healing:** resolved on successful re-process. The issues file always represents the current set of unresolved problems: a live todo list.
857
910
 
911
+ ### Change Source
912
+
913
+ `watcher_status` reports the change source under `health.changeSource` (`@parcel/watcher` by default, chokidar on request or when polling):
914
+
915
+ - **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).
916
+ - **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.
917
+ - **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.
918
+ - **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).
919
+ - **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.)
920
+ - **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`).
921
+
922
+ ### Index Check at Start
923
+
924
+ After 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`):
925
+
926
+ - **`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.
927
+ - **`state: failed`:** the index could not be listed (Qdrant down), or the service stopped first; it runs again at the next start.
928
+ - **`skippedRoots` above 0:** a root was unavailable, unwatched, gone, or its start walk failed; its deletions wait for the next start or a `prune`.
929
+ - **Files that still exist but are now ignored** keep their points: that is `prune`'s job. `watch.indexCheckOnStart: false` turns the check off.
930
+
858
931
  ---
859
932
 
860
933
  ## Helper Management
@@ -874,25 +947,24 @@ The following helpers are available in every JsonMap `lib` context without any h
874
947
 
875
948
  #### `fetchSiblings(filePath, options?)`
876
949
 
877
- Retrieve extracted text from neighboring files in the same directory. Useful for contextual embedding — e.g., injecting surrounding email messages into a thread member's embedding for better semantic search.
950
+ Retrieve 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.
878
951
 
879
952
  **Parameters:**
880
953
 
881
- - `filePath` (string) — the current file path (typically `$file_path`)
882
- - `options` (object, optional):
954
+ - `filePath` (string) — the current file path (in a rule's `map`, `$.input.file.path`)
955
+ - `options` (object, optional; JsonMap `params` are path strings, so a call from a config `map` uses the defaults):
883
956
  - `before` (number, default 3) — number of preceding siblings to include
884
957
  - `after` (number, default 1) — number of following siblings to include
885
958
  - `sort` (`"name"` | `"mtime"`, default `"name"`) — sort order for determining neighbor position
886
959
 
887
- **Returns:** `string[]` — extracted text from sibling files, in sort order. Files that fail extraction are silently skipped.
960
+ **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 `[]`.
888
961
 
889
- **Example** (in a JsonMap `$set` expression):
962
+ **Example** (in a rule's `map`):
890
963
 
891
964
  ```json
892
965
  {
893
966
  "context": {
894
- "$fn": "fetchSiblings",
895
- "$args": ["$file_path", { "before": 2, "after": 1 }]
967
+ "$": { "method": "$.lib.fetchSiblings", "params": "$.input.file.path" }
896
968
  }
897
969
  }
898
970
  ```
@@ -901,14 +973,15 @@ Retrieve extracted text from neighboring files in the same directory. Useful for
901
973
 
902
974
  ## Enrichment
903
975
 
904
- Use `watcher_enrich` to tag documents after analysis (e.g., `reviewed: true`, project labels).
976
+ Use `watcher_set_metadata` to tag documents after analysis (e.g., `reviewed: true`, project labels).
905
977
 
906
- **Enrichments are durable.** Stored in a SQLite database (`<stateDir>/enrichments.sqlite`), enrichments survive full reindexes. When the watcher re-processes a file, enrichments are merged with inference rule output using composable semantics: scalar fields overwrite, array fields union+deduplicate.
978
+ **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.
907
979
 
908
- **Metadata is validated against the file's matched rule schemas.** Validation errors return structured messages:
980
+ **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:
909
981
 
910
982
  ```json
911
983
  {
984
+ "code": "invalid_request",
912
985
  "error": "Validation failed",
913
986
  "details": [
914
987
  {
@@ -926,56 +999,56 @@ Use `watcher_enrich` to tag documents after analysis (e.g., `reviewed: true`, pr
926
999
 
927
1000
  ## Version Tracking
928
1001
 
929
- The 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 `watcher_vcs_*` tools.
1002
+ The 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.
930
1003
 
931
1004
  ### Tools
932
1005
 
933
1006
  | Tool | Purpose |
934
1007
  | --- | --- |
935
- | `watcher_vcs_status` | Check version tracking health: enabled state, tracked roots, remote sync status |
936
- | `watcher_vcs_history` | View change history for a file or folder, with optional date range and limit |
937
- | `watcher_vcs_show` | Retrieve the content of a file at a specific past version |
938
- | `watcher_vcs_diff` | Show what changed between two versions, or between a past version and now |
939
- | `watcher_vcs_revert` | Undo changes by restoring files to a specific past version |
940
- | `watcher_vcs_exclude` | Exclude (or re-include) paths from version tracking |
941
- | `watcher_vcs_check` | Check whether a path is excluded from version tracking and why |
1008
+ | `watcher_get_vcs_status` | Check version tracking health: enabled state, tracked roots, remote sync status |
1009
+ | `watcher_list_vcs_history` | View change history for a file or folder, with optional date range and limit |
1010
+ | `watcher_show_vcs_file` | Retrieve the content of a file at a specific past version |
1011
+ | `watcher_diff_vcs` | Show what changed between two versions, or between a past version and now |
1012
+ | `watcher_revert_vcs` | Undo changes by restoring files to a specific past version |
1013
+ | `watcher_exclude_vcs_path` | Exclude a path from version history (it stays in search), or remove the exclusion; untracks matching files |
1014
+ | `watcher_check_vcs_exclusion` | Check whether git ignores a path and why (rule, file, line) |
942
1015
 
943
1016
  ### Usage Patterns
944
1017
 
945
1018
  **View recent changes to a file:**
946
1019
 
947
1020
  ```
948
- watcher_vcs_history: glob="J:/domains/jira/PROJ-123.json", limit=10
1021
+ watcher_list_vcs_history: glob="/data/tickets/PROJ-123.json", limit=10
949
1022
  ```
950
1023
 
951
1024
  **See what changed between two versions:**
952
1025
 
953
1026
  ```
954
- watcher_vcs_diff: glob="J:/domains/jira/PROJ-123.json", commit="abc1234"
1027
+ watcher_diff_vcs: glob="/data/tickets/PROJ-123.json", commit="abc1234"
955
1028
  ```
956
1029
 
957
1030
  **Restore a file to a previous version:**
958
1031
 
959
1032
  ```
960
- watcher_vcs_revert: glob="J:/domains/jira/PROJ-123.json", commit="abc1234"
1033
+ watcher_revert_vcs: glob="/data/tickets/PROJ-123.json", commit="abc1234"
961
1034
  ```
962
1035
 
963
1036
  **View old content without restoring:**
964
1037
 
965
1038
  ```
966
- watcher_vcs_show: path="J:/domains/jira/PROJ-123.json", commit="abc1234"
1039
+ watcher_show_vcs_file: path="/data/tickets/PROJ-123.json", commit="abc1234"
967
1040
  ```
968
1041
 
969
1042
  **Exclude files that don't need versioning** (e.g., Jira issues that are read-only syncs):
970
1043
 
971
1044
  ```
972
- watcher_vcs_exclude: glob="J:/domains/jira/**/*.json"
1045
+ watcher_exclude_vcs_path: glob="/data/tickets/**/*.json"
973
1046
  ```
974
1047
 
975
1048
  **Check if a file is excluded and why:**
976
1049
 
977
1050
  ```
978
- watcher_vcs_check: path="J:/domains/jira/PROJ-123.json"
1051
+ watcher_check_vcs_exclusion: path="/data/tickets/PROJ-123.json"
979
1052
  ```
980
1053
 
981
1054
  ### Enabling Version Tracking
@@ -986,9 +1059,17 @@ Squash retention activates by default when VCS is enabled, even without explicit
986
1059
 
987
1060
  ### Excluding Paths
988
1061
 
989
- Not all watched files need version history. For example, if Jira issues are synced read-only, there's nothing to "undo." Use `watcher_vcs_exclude` to stop tracking specific paths.
1062
+ Not 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.
1063
+
1064
+ The 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.
990
1065
 
991
- Exclusions are placed as close to the target as possible: if you exclude `J:/domains/jira/**/*.json`, the exclusion rule is written inside the `J:/domains/jira/` directory, not at the root. This keeps exclusions local and easy to reason about. Re-include a path by calling `watcher_vcs_exclude` with `remove: true`.
1066
+ - **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.
1067
+ - **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).
1068
+ - **Brackets are a character class.** `Report [draft].docx` matches `Report d.docx`; escape them to name the file: `Report \[draft\].docx`.
1069
+ - **Refused (`400`):** a newline, a leading `!` or `#`, braces, a pattern matching the whole root (`*`, `**`, `**/*`, ...), or a glob outside the given `root`.
1070
+ - **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.
1071
+ - **Busy root.** If version control is busy on the root for 30 s the tool reports `503 not_available`; retry after 30 s.
1072
+ - `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.
992
1073
 
993
1074
  ### Score Interpretation
994
1075
 
@@ -1004,6 +1085,19 @@ If the watcher is unreachable:
1004
1085
  - Fall back to direct `read` for known file paths
1005
1086
  - Do not retry silently in a loop
1006
1087
 
1088
+ If a tool fails with `HTTP 401` (`unauthorized`):
1089
+
1090
+ - The watcher sets `api.apiKey` and the plugin sent no key, or a different one.
1091
+ - 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.
1092
+ - Never ask for, print or guess the key. `GET /status` and `GET /openapi.json` never need it.
1093
+
1094
+ If a tool or call fails with `HTTP 503` (`not_available`) and a `Retry-After` header:
1095
+
1096
+ - `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.
1097
+ - `watcher_exclude_vcs_path` waits 30 s for a busy version-control root.
1098
+
1099
+ If 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.
1100
+
1007
1101
  If tools are unavailable (plugin not loaded in this session):
1008
1102
 
1009
1103
  - The watcher API is still accessible via direct HTTP calls
@@ -1012,8 +1106,8 @@ If tools are unavailable (plugin not loaded in this session):
1012
1106
 
1013
1107
  **CLI Fallbacks:**
1014
1108
 
1015
- - `jeeves-watcher status` — check if the service is running
1016
- - `jeeves-watcher validate` — validate config from CLI
1109
+ - `jeeves-watcher status [-p <port>]` — check if the service is running (`GET /status`)
1110
+ - `jeeves-watcher config validate -c <path>` — validate a config file against the schema (offline)
1017
1111
  - Restart via NSSM (Windows) or systemctl (Linux)
1018
1112
 
1019
1113
  ---