@karmaniverous/jeeves-watcher-openclaw 0.16.1 → 0.17.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.
@@ -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` needs `Authorization: Bearer <key>`. The tools send it from `plugins.entries.jeeves-watcher-openclaw.config.apiKey` (or the `JEEVES_WATCHER_API_KEY` env var). A missing or wrong key comes back as `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: `. Any other non-2xx response reads `HTTP <status> <code>: <message>`, where `<code>` is the server's machine-readable error code (e.g. `HTTP 400 invalid_request: Validation failed`, `HTTP 503 scan_in_progress: Scan in progress: ...`). Branch on the code, not the message. Structured `details` (Zod issues, per-property validation failures) are in the HTTP response body only; call the API directly when you need them. An unreachable service reads `Service not reachable at <url>.` with a hint to set `apiUrl`. (`watcher_status` and `watcher_service` come from the shared Jeeves core toolset and word their 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`), `reindex` progress, and `initialScan` progress.
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
@@ -35,24 +39,27 @@ curl http://127.0.0.1:<PORT>/config
35
39
  | --- | --- | --- |
36
40
  | `/status` | GET | Health check, uptime, collection stats |
37
41
  | `/search` | POST | Semantic search (main query interface) |
38
- | `/config` | GET | Full resolved config; optional `?path=<jsonpath>` filter |
42
+ | `/config` | GET | Public resolved config (allow-listed keys plus `issues`); optional `?path=<jsonpath>` filter |
39
43
  | `/config/validate` | POST | Validate candidate config |
40
44
  | `/config/apply` | POST | Apply config changes |
41
45
  | `/reindex` | POST | Trigger reindex |
42
- | `/metadata` | POST | Enrich document metadata |
46
+ | `/metadata` | POST | Enrich document metadata (file path or source reference) |
43
47
  | `/scan` | POST | Filter-only point query (no embeddings) |
44
- | `/walk` | POST | Filesystem walk with glob intersection |
45
- | `/issues` | GET | Runtime embedding failures |
48
+ | `/walk` | POST | Watched-file listing with glob intersection |
49
+ | `/issues` | GET | Runtime processing failures (file paths and source references) |
50
+ | `/documents` | POST | Notify changed source documents (`{ source, rule }`) |
51
+ | `/documents/delete` | POST | Delete source documents by reference or filter |
46
52
  | `/rules/register` | POST | Register virtual inference rules (auto-triggers rules reindex) |
47
- | `/rules/unregister` | DELETE | Remove virtual rules by source |
53
+ | `/rules/unregister` | DELETE | Remove virtual rules by source (`{ source }` body) |
48
54
  | `/rules/unregister/:source` | DELETE | Remove virtual rules by source (parameterized) |
49
- | `/rules/reapply` | POST | Re-apply rules to points |
55
+ | `/rules/reapply` | POST | Re-apply rules to indexed files matching `globs` |
50
56
  | `/points/delete` | POST | Delete points matching a Qdrant filter |
51
- | `/render` | POST | Render a template for a document |
57
+ | `/render` | POST | Render a document (file path or source reference) through its rule |
52
58
  | `/search/facets` | GET | Get search facet values |
53
- | `/rebuild-metadata` | POST | Rebuild enrichment metadata from Qdrant |
59
+ | `/rebuild-metadata` | POST | Rebuild the enrichment store from Qdrant file point payloads |
54
60
  | `/config/schema` | GET | Get config JSON schema |
55
- | `/config/match` | POST | Match config paths against rules |
61
+ | `/config/match` | POST | Test file paths against inference rules and watch scope |
62
+ | `/vcs/*` | GET/POST | Version tracking (`status`, `history`, `show`, `diff`, `check-exclusion`, `revert`, `exclude`) |
56
63
 
57
64
  **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
65
 
@@ -84,7 +91,7 @@ You have access to a **semantic archive** of your human's working world. Documen
84
91
 
85
92
  - **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.
86
93
  - **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).
94
+ - **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
95
  - **Any bulk file operation** (mass copy, mass move, template-based file generation) under watched paths.
89
96
 
90
97
  **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 +109,7 @@ The plugin is a standard OpenClaw plugin. On a Jeeves box, `jeeves install watch
102
109
  If the watcher service is already running and healthy:
103
110
 
104
111
  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
112
+ 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
113
 
107
114
  ## Bootstrap (First-Time Setup)
108
115
 
@@ -281,10 +288,10 @@ Then generate a starter config file. Example minimal config:
281
288
  }
282
289
  ```
283
290
 
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:
291
+ 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
292
 
286
293
  ```bash
287
- jeeves-watcher validate -c <config-path>
294
+ jeeves-watcher config validate -c <config-path>
288
295
  ```
289
296
 
290
297
  ### Step 6: Register and Start as a Service
@@ -316,9 +323,9 @@ sudo systemctl enable --now jeeves-watcher
316
323
  **Windows (NSSM):**
317
324
 
318
325
  ```powershell
319
- jeeves-watcher service install
326
+ jeeves-watcher service install -c <config-path>
320
327
  # Or manually:
321
- nssm install jeeves-watcher "$(which jeeves-watcher)" start -c <config-path>
328
+ nssm install jeeves-watcher (Get-Command jeeves-watcher).Source start -c <config-path>
322
329
  nssm set jeeves-watcher AppEnvironmentExtra GOOGLE_API_KEY=<key>
323
330
  nssm start jeeves-watcher
324
331
  ```
@@ -337,7 +344,7 @@ Call `watcher_status` (or `curl http://127.0.0.1:1936/status`). Confirm:
337
344
  - Qdrant collection exists with expected dimensions (3072)
338
345
  - Point count is increasing (initial indexing in progress)
339
346
 
340
- If the point count is 0 after a minute, check `watcher_issues` for embedding failures.
347
+ If the point count is 0 after a minute, check `watcher_issues` for processing failures.
341
348
 
342
349
  ### Step 8: Orientation
343
350
 
@@ -359,25 +366,31 @@ On sessions after bootstrap is complete:
359
366
 
360
367
  ## Tools
361
368
 
369
+ The plugin registers 18 tools: `watcher_status`, `watcher_config`, `watcher_config_apply`, `watcher_service`, `watcher_search`, `watcher_enrich`, `watcher_validate`, `watcher_reindex`, `watcher_scan`, `watcher_issues`, `watcher_walk`, and the seven `watcher_vcs_*` tools (see [Version Tracking](#version-tracking)).
370
+
362
371
  ### `watcher_search`
363
372
 
364
- Semantic search over indexed documents.
373
+ Semantic search over indexed documents (`POST /search`).
365
374
 
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
375
+ - `query` (string, required) — natural language search query (non-empty)
376
+ - `limit` (number, optional) — max results (positive integer), default 10
377
+ - `offset` (number, optional) — skip N results for pagination (non-negative integer)
369
378
  - `filter` (object, optional) — Qdrant filter for metadata filtering
370
379
 
380
+ 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`.
381
+
371
382
  ### `watcher_enrich`
372
383
 
373
- Set or update metadata on a document.
384
+ Set or update metadata on a document (`POST /metadata`).
374
385
 
375
- - `path` (string, required) — file path of the document
386
+ - `path` (string, required) — file path of the document (absolute or watched path)
376
387
  - `metadata` (object, required) — key-value metadata to merge
377
388
 
389
+ Returns `{ ok: true, matched_rules }`. 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`).
390
+
378
391
  ### `watcher_status`
379
392
 
380
- Service health check. Returns `name`, `version`, `uptime`, `status`, and `health` object with collection stats, reindex status, and initial scan progress.
393
+ Service health check (`GET /status`, never needs the API key). Returns `name`, `version`, `uptime`, `status`, and a `health` object: `collection` (`name`, `pointCount`, `dimensions`), `reindex` (`active`; while active also `scope`, `startedAt`, `filesProcessed`, `totalFiles`), and `initialScan`.
381
394
 
382
395
  After a service restart, the `health.initialScan` field shows scan progress:
383
396
 
@@ -388,41 +401,47 @@ Use this to determine if the service is still initializing after a restart.
388
401
 
389
402
  ### `watcher_config`
390
403
 
391
- Query the effective runtime config via JSONPath. Returns the full resolved merged document when no path is provided.
404
+ Query the effective runtime config via JSONPath (`GET /config`). Returns the full resolved merged document when no path is provided.
392
405
 
393
406
  - `path` (string, optional) — JSONPath expression
394
407
 
408
+ 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`.
409
+
395
410
  ### `watcher_validate`
396
411
 
397
- Validate config and optionally test file paths.
412
+ Validate config and optionally test file paths (`POST /config/validate`).
398
413
 
399
414
  - `config` (object, optional) — candidate config (partial or full). Omit to validate current config.
400
415
  - `testPaths` (string[], optional) — file paths to test against the config
401
416
 
402
- Partial configs merge with current config by rule name. If `config` is omitted, tests against the running config.
417
+ 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
418
 
404
419
  ### `watcher_config_apply`
405
420
 
406
- Apply config changes atomically.
421
+ Apply config changes (`POST /config/apply`, sent as `{ patch: config }`).
407
422
 
408
423
  - `config` (object, required) — full or partial config to apply
409
424
 
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.
425
+ 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.
426
+
427
+ 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 invalid_request: Config validation failed` (the Zod `issues` are only in the HTTP body), so run `watcher_validate` first. A config file the service cannot read or write is a generic `HTTP 500 internal_error`, with the cause in the service log.
411
428
 
412
429
  ### `watcher_reindex`
413
430
 
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.
431
+ 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
432
 
416
433
  **Parameters:**
417
434
 
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.
435
+ - `scope` (string, optional) — Reindex scope. The tool sends `"rules"` when omitted (the HTTP API's own default is `"issues"`). One of:
436
+ - `"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.
437
+ - `"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.
438
+ - `"issues"` — Re-process the documents in `watcher_issues`: failed files, and failed source references (with their stored rule, or the `rule` recorded on the issue when the document was never indexed).
439
+ - `"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.
440
+ - `"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`).
441
+ - `path` (string | string[], optional) — Target file(s) or directories for `"path"` (required there), or globs narrowing `"rules"`. Ignored by other scopes.
442
+ - `dryRun` (boolean, optional) — When `true`, compute and return the plan without executing.
443
+
444
+ 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
445
 
427
446
  **Response (normal):**
428
447
 
@@ -434,7 +453,7 @@ Trigger a reindex operation. Non-prune scopes return a `plan` object showing bla
434
453
  "total": 148000,
435
454
  "toProcess": 148000,
436
455
  "toDelete": 0,
437
- "byRoot": { "j:/domains": 95000, "j:/config": 3000 }
456
+ "byRoot": { "/data": 95000, "/srv/config": 3000 }
438
457
  }
439
458
  }
440
459
  ```
@@ -449,30 +468,33 @@ Trigger a reindex operation. Non-prune scopes return a `plan` object showing bla
449
468
  "total": 562000,
450
469
  "toProcess": 0,
451
470
  "toDelete": 2300,
452
- "byRoot": { "j:/jeeves/node_modules": 1800, "j:/jeeves/.bridge": 500 }
471
+ "byRoot": { "/srv/app/node_modules": 1800, "/srv/app/.cache": 500 }
453
472
  }
454
473
  }
455
474
  ```
456
475
 
457
476
  **Plan fields:**
458
477
 
459
- - `total` — Total points (prune) or files (other scopes) examined.
460
- - `toProcess` — Items to embed/re-apply rules (0 for prune).
478
+ - `total` — Points scrolled (prune), or files plus source documents covered (other scopes).
479
+ - `toProcess` — Files plus source documents to process (0 for prune).
461
480
  - `toDelete` — Points to delete (prune only, 0 for others).
462
- - `byRoot` — Counts grouped by watch root prefix. Shows where the impact concentrates.
481
+ - `byRoot` — Files grouped by watch root (for prune: orphaned files), plus one entry per source name for source documents. Shows where the impact concentrates.
482
+ - `incomplete` — `true` when the prune scroll failed part-way and the plan is partial.
483
+
484
+ **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
485
 
464
486
  **Guidance:**
465
487
 
466
488
  - 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.
489
+ - `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
490
  - `prune` is NOT triggered by config-watch auto-reindex (too dangerous for auto-trigger).
469
491
 
470
492
  ### `watcher_scan`
471
493
 
472
- Filter-only point query without vector search. Use for structural queries where the question has no semantic dimension.
494
+ Filter-only point query without vector search (`POST /scan`). Use for structural queries where the question has no semantic dimension.
473
495
 
474
496
  - `filter` (object, required) — Qdrant filter object. Required to prevent accidental full-collection scans.
475
- - `limit` (number, optional) — page size, default 100, max 1000
497
+ - `limit` (number, optional) — page size (integer 1 to 1000), default 100
476
498
  - `cursor` (string, optional) — opaque cursor from previous response for pagination
477
499
  - `fields` (string[], optional) — payload fields to return (projection)
478
500
  - `countOnly` (boolean, optional) — if true, return `{ count }` instead of points
@@ -481,7 +503,12 @@ Filter-only point query without vector search. Use for structural queries where
481
503
 
482
504
  ```json
483
505
  {
484
- "points": [{ "id": "uuid", "payload": { ... } }],
506
+ "points": [
507
+ {
508
+ "id": "uuid",
509
+ "payload": { "file_path": "/data/tickets/PROJ-123.json" }
510
+ }
511
+ ],
485
512
  "cursor": "opaque-string-or-null"
486
513
  }
487
514
  ```
@@ -496,7 +523,7 @@ Filter-only point query without vector search. Use for structural queries where
496
523
 
497
524
  - No `query` parameter — does NOT use embeddings
498
525
  - No `score` field — results are unranked filter matches
499
- - Cursor-based pagination (not offset-based)
526
+ - Cursor-based pagination (not offset-based); `cursor` is `null` on the last page
500
527
  - Zero cost per call beyond Qdrant's filtered scroll
501
528
 
502
529
  **Pagination pattern:**
@@ -512,29 +539,29 @@ do {
512
539
 
513
540
  ### `watcher_service`
514
541
 
515
- Manage the watcher background service (install, uninstall, start, stop, restart, check status).
542
+ Manage the watcher background service (install, uninstall, start, stop, restart, check status). No HTTP call.
516
543
 
517
544
  - `action` (string, required) — one of: `install`, `uninstall`, `start`, `stop`, `restart`, `status`
518
545
 
519
- Returns the service manager's response. On Windows uses NSSM, on Linux uses systemd.
546
+ 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
547
 
521
548
  ### `watcher_issues`
522
549
 
523
- Get runtime embedding failures. Returns `{ filePath: IssueRecord }` showing files that failed and why.
550
+ 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
551
 
525
552
  ### `watcher_walk`
526
553
 
527
- Walk watched filesystem paths with glob intersection. Returns matching file paths from all configured watch roots.
554
+ 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
555
 
529
- - `globs` (string[], required) — glob patterns to intersect with watch paths
556
+ - `globs` (string[], required) — glob patterns to intersect with watch paths (non-empty; matched case-insensitively, dotfiles included)
530
557
 
531
558
  **Response:**
532
559
 
533
560
  ```json
534
561
  {
535
- "paths": ["j:/domains/foo/.meta/meta.json", "j:/domains/bar/.meta/meta.json"],
562
+ "paths": ["/data/foo/.meta/meta.json", "/data/bar/.meta/meta.json"],
536
563
  "matchedCount": 2,
537
- "scannedRoots": ["j:/domains", "j:/config"]
564
+ "scannedRoots": ["/data", "/srv/config"]
538
565
  }
539
566
  ```
540
567
 
@@ -542,12 +569,12 @@ Walk watched filesystem paths with glob intersection. Returns matching file path
542
569
 
543
570
  - Discover files matching a pattern across all watched directories (e.g., `["**/.meta/meta.json"]`)
544
571
  - Enumerate files before rule registration to understand scope
545
- - Find files that aren't yet indexed (no Qdrant dependency — works even before first embedding)
572
+ - Find files that aren't yet indexed (no Qdrant dependency)
546
573
 
547
574
  **Key differences from `watcher_scan`:**
548
575
 
549
- - Walks the actual filesystem, not the Qdrant index
550
- - No embedding or indexing required — works immediately after service start
576
+ - Lists the files the filesystem watcher tracks, not the Qdrant index
577
+ - 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
578
  - Returns file paths only (no metadata, no vectors)
552
579
  - Applies `watch.ignored` and gitignore filtering automatically
553
580
 
@@ -580,7 +607,7 @@ Walk watched filesystem paths with glob intersection. Returns matching file path
580
607
  **Path prefix matching:**
581
608
 
582
609
  ```json
583
- { "must": [{ "key": "file_path", "match": { "text": "j:/domains/jira" } }] }
610
+ { "must": [{ "key": "file_path", "match": { "text": "/data/tickets" } }] }
584
611
  ```
585
612
 
586
613
  **Count files in a domain (no point data transferred):**
@@ -660,15 +687,21 @@ Each result from `watcher_search` contains:
660
687
  | Field | Type | Description |
661
688
  | --- | --- | --- |
662
689
  | `id` | string | Qdrant point ID |
663
- | `score` | number | Similarity score (0-1, higher = more relevant) |
664
- | `payload.file_path` | string | Source file path |
690
+ | `score` | number | Similarity (or hybrid fused) score; higher = more relevant |
691
+ | `payload.file_path` | string | Normalized file path (file points only) |
692
+ | `payload.source` | string | Source reference `<name>://<id>` (source points only) |
693
+ | `payload.source_rule` | string | Designated rule of a source document (source points only) |
665
694
  | `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 |
695
+ | `payload.chunk_index` | number | Zero-based chunk position within the document |
696
+ | `payload.total_chunks` | number | Total chunks for this document |
697
+ | `payload.line_start` | number | 1-indexed line where the chunk starts |
698
+ | `payload.line_end` | number | 1-indexed line where the chunk ends |
699
+ | `payload.created_at` | number | Creation timestamp (unix seconds) |
700
+ | `payload.modified_at` | number | Modification timestamp (unix seconds) |
701
+ | `payload.content_hash` | string | SHA-256 hash of the embedded text |
669
702
  | `payload.matched_rules` | string[] | Names of inference rules that matched |
670
703
 
671
- Additional metadata fields depend on the deployment's inference rules (e.g., `domain`, `status`, `author`). Use `watcher_config` to discover available fields.
704
+ `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
705
 
673
706
  ## Query Planning (Per Search Task)
674
707
 
@@ -678,10 +711,9 @@ Identify relevant rule(s) from the orientation model, then retrieve their schema
678
711
 
679
712
  ```
680
713
  watcher_config: path="$.inferenceRules[?(@.name=='jira-issue')].schema"
681
- resolve=["files","globals"]
682
714
  ```
683
715
 
684
- Returns the fully merged schema with properties, types, `set` provenance, `uiHint`, `enum`, etc.
716
+ Returns the rule's schema array with named schema references and file references already resolved: properties, types, `set` expressions, `uiHint`, `enum`, etc.
685
717
 
686
718
  **For select/multiselect fields without `enum` in schema:**
687
719
 
@@ -704,7 +736,7 @@ Use `uiHint` to determine filter construction strategy. **This table is explicit
704
736
  | `text` | `{ "key": "<field>", "match": { "text": "<value>" } }` | Substring/keyword match |
705
737
  | `select` | `{ "key": "<field>", "match": { "value": "<enum_value>" } }` | Exact match; use `enum` values from schema or runtime values index |
706
738
  | `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) |
739
+ | `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
740
  | `number` | `{ "key": "<field>", "range": { "gte": <n>, "lte": <n> } }` | Either bound optional for open-ended ranges |
709
741
  | `check` | `{ "key": "<field>", "match": { "value": true } }` | Boolean match |
710
742
  | _(absent)_ | Do not use in filters | Internal bookkeeping field, not intended for search |
@@ -768,7 +800,7 @@ Use `scoreThresholds` from config (queried during orientation). Values are deplo
768
800
 
769
801
  ### Chunk Grouping
770
802
 
771
- Multiple results with the same `file_path` are chunks of one document. Read the full file for complete context.
803
+ 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
804
 
773
805
  ### Schema Lookup
774
806
 
@@ -776,27 +808,25 @@ Use `matched_rules` on results to look up applicable schemas for metadata interp
776
808
 
777
809
  ```
778
810
  watcher_config: path="$.inferenceRules[?(@.name=='jira-issue')].schema"
779
- resolve=["files","globals"]
780
811
  ```
781
812
 
782
813
  ### Full Context
783
814
 
784
- Search gives you chunks; use `read` with `file_path` for the complete document.
815
+ 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
816
 
786
817
  ---
787
818
 
788
819
  ## Path Testing
789
820
 
790
- When uncertain whether a file is indexed, use the path test endpoint:
821
+ When uncertain whether a file would be indexed, test it against the rules. `watcher_validate` 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
822
 
792
823
  ```
793
- watcher_config: path="$.inferenceRules[?(@.name=='<rule>')].match"
824
+ watcher_validate: testPaths=["/data/tickets/PROJ-123.json"]
794
825
  ```
795
826
 
796
- Or check if a specific path would match:
827
+ Or call `POST /config/match` directly with `{ "paths": [...] }`. It returns `{ matches: [{ rules, watched }] }`, one entry per path, without reading the files:
797
828
 
798
- - Returns matching rule names and watch scope status
799
- - Empty `rules` array means no inference rules match
829
+ - `rules` lists the matching rule names; an empty array means no inference rules match
800
830
  - `watched: false` means the path falls outside watch paths or is excluded by ignore patterns
801
831
 
802
832
  ---
@@ -809,31 +839,33 @@ Each inference rule has:
809
839
 
810
840
  - `name` (required) — unique identifier
811
841
  - `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
842
+ - `match` (required) — JSON Schema matched against file attributes (e.g. a picomatch `glob` on `file.path`)
843
+ - `schema` (optional) — array of named schema references and/or inline schema objects with `set` templates
844
+ - `map` (optional) — JsonMap transform (inline, named map, or `.json` file path)
845
+ - `template` (optional) — Handlebars template (inline, named template, or `.hbs`/`.handlebars` file path)
846
+ - `render` (optional) — declarative frontmatter + Markdown renderer (mutually exclusive with `template`)
847
+ - `renderAs` (optional) — output extension (1-10 lowercase alphanumerics); requires `template` or `render`
816
848
 
817
849
  ### Config Workflow
818
850
 
819
851
  1. Edit config (or build partial config object)
820
852
  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
853
+ 3. Apply: `watcher_config_apply` — validates, writes, triggers the `configWatch.reindex` reindex
854
+ 4. Monitor: `watcher_issues` for runtime processing failures
823
855
 
824
856
  ### Reindex Concurrency
825
857
 
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.
858
+ 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
859
 
828
- Progress is reported via `watcher_status` (`reindex.filesProcessed` / `reindex.totalFiles`).
860
+ Progress is reported via `watcher_status` (`health.reindex.filesProcessed` / `health.reindex.totalFiles`, counting files plus source documents).
829
861
 
830
862
  ### When to Reindex
831
863
 
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.
864
+ - **Rules scope** (`"rules"`): Changed rule matching patterns, set expressions, schema mappings. No file re-embedding needed.
865
+ - **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.
866
+ - **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.
867
+ - **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.
868
+ - **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
869
 
838
870
  ---
839
871
 
@@ -842,16 +874,26 @@ Progress is reported via `watcher_status` (`reindex.filesProcessed` / `reindex.t
842
874
  ### Escalation Path
843
875
 
844
876
  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
877
+ 2. `watcher_issues` — which documents are failing and why?
878
+ 3. `watcher_config` with `$.issues` — the same issue records via JSONPath
847
879
  4. Check logs at the configured log path
848
880
 
849
881
  ### Error Categories
850
882
 
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
883
+ Every issue record's `type` is one of these codes:
884
+
885
+ - `type_collision` — conflicting metadata types across rules (includes `property`, `rules[]`, `types[]`)
886
+ - `interpolation_error` — template or `set` expression rendering failed (includes `property`, `rule`; `details` holds per-step failures)
887
+ - `extraction_error` — reading, rule application or chunking failed
888
+ - `embedding_error` — the embedding provider failed
889
+ - `upsert_error` — the vector store write failed
890
+ - `match_failed` — a source document's designated rule did not match (`details` holds the AJV errors)
891
+ - `rule_missing` — a source document's designated rule was not found
892
+ - `no_content` — nothing to embed
893
+ - `source_unavailable` — the source callback failed
894
+ - `unsupported_content_type` — the source callback returned a non-JSON body
895
+
896
+ Source-document issues always carry `rule` (the designated rule of the failed run).
855
897
 
856
898
  **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
899
 
@@ -874,25 +916,24 @@ The following helpers are available in every JsonMap `lib` context without any h
874
916
 
875
917
  #### `fetchSiblings(filePath, options?)`
876
918
 
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.
919
+ 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
920
 
879
921
  **Parameters:**
880
922
 
881
- - `filePath` (string) — the current file path (typically `$file_path`)
882
- - `options` (object, optional):
923
+ - `filePath` (string) — the current file path (in a rule's `map`, `$.input.file.path`)
924
+ - `options` (object, optional; JsonMap `params` are path strings, so a call from a config `map` uses the defaults):
883
925
  - `before` (number, default 3) — number of preceding siblings to include
884
926
  - `after` (number, default 1) — number of following siblings to include
885
927
  - `sort` (`"name"` | `"mtime"`, default `"name"`) — sort order for determining neighbor position
886
928
 
887
- **Returns:** `string[]` — extracted text from sibling files, in sort order. Files that fail extraction are silently skipped.
929
+ **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
930
 
889
- **Example** (in a JsonMap `$set` expression):
931
+ **Example** (in a rule's `map`):
890
932
 
891
933
  ```json
892
934
  {
893
935
  "context": {
894
- "$fn": "fetchSiblings",
895
- "$args": ["$file_path", { "before": 2, "after": 1 }]
936
+ "$": { "method": "$.lib.fetchSiblings", "params": "$.input.file.path" }
896
937
  }
897
938
  }
898
939
  ```
@@ -903,12 +944,13 @@ Retrieve extracted text from neighboring files in the same directory. Useful for
903
944
 
904
945
  Use `watcher_enrich` to tag documents after analysis (e.g., `reviewed: true`, project labels).
905
946
 
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.
947
+ **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
948
 
908
- **Metadata is validated against the file's matched rule schemas.** Validation errors return structured messages:
949
+ **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
950
 
910
951
  ```json
911
952
  {
953
+ "code": "invalid_request",
912
954
  "error": "Validation failed",
913
955
  "details": [
914
956
  {
@@ -945,37 +987,37 @@ The watcher includes built-in history and undo for your watched files. When enab
945
987
  **View recent changes to a file:**
946
988
 
947
989
  ```
948
- watcher_vcs_history: glob="J:/domains/jira/PROJ-123.json", limit=10
990
+ watcher_vcs_history: glob="/data/tickets/PROJ-123.json", limit=10
949
991
  ```
950
992
 
951
993
  **See what changed between two versions:**
952
994
 
953
995
  ```
954
- watcher_vcs_diff: glob="J:/domains/jira/PROJ-123.json", commit="abc1234"
996
+ watcher_vcs_diff: glob="/data/tickets/PROJ-123.json", commit="abc1234"
955
997
  ```
956
998
 
957
999
  **Restore a file to a previous version:**
958
1000
 
959
1001
  ```
960
- watcher_vcs_revert: glob="J:/domains/jira/PROJ-123.json", commit="abc1234"
1002
+ watcher_vcs_revert: glob="/data/tickets/PROJ-123.json", commit="abc1234"
961
1003
  ```
962
1004
 
963
1005
  **View old content without restoring:**
964
1006
 
965
1007
  ```
966
- watcher_vcs_show: path="J:/domains/jira/PROJ-123.json", commit="abc1234"
1008
+ watcher_vcs_show: path="/data/tickets/PROJ-123.json", commit="abc1234"
967
1009
  ```
968
1010
 
969
1011
  **Exclude files that don't need versioning** (e.g., Jira issues that are read-only syncs):
970
1012
 
971
1013
  ```
972
- watcher_vcs_exclude: glob="J:/domains/jira/**/*.json"
1014
+ watcher_vcs_exclude: glob="/data/tickets/**/*.json"
973
1015
  ```
974
1016
 
975
1017
  **Check if a file is excluded and why:**
976
1018
 
977
1019
  ```
978
- watcher_vcs_check: path="J:/domains/jira/PROJ-123.json"
1020
+ watcher_vcs_check: path="/data/tickets/PROJ-123.json"
979
1021
  ```
980
1022
 
981
1023
  ### Enabling Version Tracking
@@ -988,7 +1030,7 @@ Squash retention activates by default when VCS is enabled, even without explicit
988
1030
 
989
1031
  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.
990
1032
 
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`.
1033
+ Exclusions are placed as close to the target as possible: if you exclude `/data/tickets/**/*.json`, the exclusion rule is written inside the `/data/tickets/` 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`.
992
1034
 
993
1035
  ### Score Interpretation
994
1036
 
@@ -1012,8 +1054,8 @@ If tools are unavailable (plugin not loaded in this session):
1012
1054
 
1013
1055
  **CLI Fallbacks:**
1014
1056
 
1015
- - `jeeves-watcher status` — check if the service is running
1016
- - `jeeves-watcher validate` — validate config from CLI
1057
+ - `jeeves-watcher status [-p <port>]` — check if the service is running (`GET /status`)
1058
+ - `jeeves-watcher config validate -c <path>` — validate a config file against the schema (offline)
1017
1059
  - Restart via NSSM (Windows) or systemctl (Linux)
1018
1060
 
1019
1061
  ---