woods 2.0.0.beta4 → 2.0.1

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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +495 -471
  3. data/CONTRIBUTING.md +12 -2
  4. data/README.md +11 -26
  5. data/docs/AGENT_GUIDE.md +31 -12
  6. data/docs/AGENT_SETUP.md +17 -10
  7. data/docs/AUTOMATIC_MAINTENANCE.md +222 -0
  8. data/docs/BACKEND_MATRIX.md +13 -7
  9. data/docs/CLIENT_HOOKS.md +1 -1
  10. data/docs/CONFIGURATION_REFERENCE.md +44 -27
  11. data/docs/CONSOLE_MCP_SETUP.md +95 -16
  12. data/docs/DOCKER_SETUP.md +15 -0
  13. data/docs/EVALUATION.md +10 -4
  14. data/docs/EXTRACTOR_REFERENCE.md +14 -2
  15. data/docs/FAQ.md +14 -3
  16. data/docs/GETTING_STARTED.md +18 -17
  17. data/docs/INCREMENTAL_EXTRACTION.md +8 -3
  18. data/docs/INDEX_LAYOUT.md +2 -2
  19. data/docs/MCP_HTTP_TRANSPORT.md +54 -2
  20. data/docs/MCP_SERVERS.md +28 -11
  21. data/docs/MCP_TOOL_COOKBOOK.md +1 -1
  22. data/docs/MCP_WORKTREE_SETUP.md +13 -1
  23. data/docs/PUBLISHED_INDEX.md +1 -1
  24. data/docs/README.md +2 -1
  25. data/docs/RETRIEVAL_GUIDE.md +57 -8
  26. data/docs/SOURCE_FRESHNESS.md +1 -1
  27. data/docs/TOKEN_BENCHMARK.md +16 -10
  28. data/docs/TROUBLESHOOTING.md +133 -37
  29. data/docs/UPGRADING_TO_2.md +69 -7
  30. data/docs/WATCH_DAEMON.md +172 -17
  31. data/docs/WHY_WOODS.md +9 -5
  32. data/exe/woods-console +13 -11
  33. data/exe/woods-mcp-http +16 -9
  34. data/exe/woods-watch +5 -0
  35. data/lib/generators/woods/watch_generator.rb +53 -0
  36. data/lib/puma/plugin/woods.rb +10 -0
  37. data/lib/tasks/woods.rake +14 -0
  38. data/lib/woods/cache/cache_middleware.rb +18 -11
  39. data/lib/woods/console/adapter_family.rb +39 -0
  40. data/lib/woods/console/credential_index.rb +33 -3
  41. data/lib/woods/console/embedded_executor.rb +401 -43
  42. data/lib/woods/console/model_validator.rb +8 -0
  43. data/lib/woods/console/rack_middleware.rb +39 -10
  44. data/lib/woods/console/redactor.rb +24 -10
  45. data/lib/woods/console/safe_context.rb +44 -7
  46. data/lib/woods/console/sql_noise_stripper.rb +41 -12
  47. data/lib/woods/console/sql_table_scanner.rb +45 -34
  48. data/lib/woods/console/sql_validator.rb +37 -2
  49. data/lib/woods/console/stdio_transport.rb +27 -0
  50. data/lib/woods/extractor.rb +25 -7
  51. data/lib/woods/git_command.rb +6 -7
  52. data/lib/woods/git_provenance.rb +4 -6
  53. data/lib/woods/mcp/bearer_auth.rb +1 -1
  54. data/lib/woods/mcp/bootstrapper.rb +3 -1
  55. data/lib/woods/mcp/initialization_guidance.rb +1 -1
  56. data/lib/woods/mcp/origin_guard.rb +24 -77
  57. data/lib/woods/mcp/origin_policy.rb +124 -0
  58. data/lib/woods/mcp/server.rb +41 -9
  59. data/lib/woods/railtie_support.rb +8 -0
  60. data/lib/woods/retrieval/corpus_status.rb +46 -0
  61. data/lib/woods/retriever.rb +19 -7
  62. data/lib/woods/storage/local_corpus_stats.rb +32 -0
  63. data/lib/woods/storage/metadata_store.rb +20 -0
  64. data/lib/woods/storage/vector_store.rb +10 -0
  65. data/lib/woods/version.rb +1 -1
  66. data/lib/woods/watch/child_environment.rb +30 -0
  67. data/lib/woods/watch/cli.rb +91 -0
  68. data/lib/woods/watch/daemon.rb +55 -7
  69. data/lib/woods/watch/event_stream.rb +70 -0
  70. data/lib/woods/watch/guardian.rb +142 -0
  71. data/lib/woods/watch/installation/layout.rb +70 -0
  72. data/lib/woods/watch/installation/options.rb +128 -0
  73. data/lib/woods/watch/installation/planner.rb +128 -0
  74. data/lib/woods/watch/installation/probe.rb +101 -0
  75. data/lib/woods/watch/installation/receipt.rb +77 -0
  76. data/lib/woods/watch/installation/recovery.rb +64 -0
  77. data/lib/woods/watch/installation/templates.rb +58 -0
  78. data/lib/woods/watch/installation.rb +56 -0
  79. data/lib/woods/watch/lifecycle.rb +182 -0
  80. data/lib/woods/watch/managed_child.rb +113 -0
  81. data/lib/woods/watch/managed_cleanup.rb +48 -0
  82. data/lib/woods/watch/managed_process.rb +144 -0
  83. data/lib/woods/watch/puma_adapter.rb +87 -0
  84. data/lib/woods/watch/puma_child.rb +66 -0
  85. data/lib/woods/watch/supervision_records.rb +95 -0
  86. data/lib/woods/watch/supervision_status.rb +104 -0
  87. data/lib/woods/watch/supervisor.rb +161 -0
  88. data/lib/woods/watch/supervisor_reporting.rb +46 -0
  89. data/plugin/.claude-plugin/plugin.json +1 -1
  90. data/plugin/skills/woods-agent-enable/SKILL.md +1 -1
  91. data/plugin/skills/woods-diagnose/SKILL.md +88 -8
  92. data/plugin/skills/woods-investigate/SKILL.md +6 -6
  93. data/plugin/skills/woods-mcp-config/SKILL.md +43 -1
  94. data/plugin/skills/woods-setup/SKILL.md +66 -4
  95. metadata +37 -5
@@ -210,7 +210,7 @@ config.vector_store_options = {
210
210
 
211
211
  Woods uses an HNSW index over pgvector's `vector` representation, which supports
212
212
  **1–2,000 dimensions** ([pgvector's HNSW limits](https://github.com/pgvector/pgvector#hnsw)).
213
- Unreleased after `2.0.0.beta3`: the adapter rejects wider dimensions before any
213
+ Included in Woods `2.0.0`: the adapter rejects wider dimensions before any
214
214
  SQL, and `woods:pgvector` rejects invalid widths before writing a migration.
215
215
  There is no automatic vector truncation or half-precision conversion.
216
216
 
@@ -282,7 +282,7 @@ in the promoted dump; the index MCP server loads that snapshot at startup or
282
282
  reload. Incremental embedding publishes changes to paths, dependencies, and
283
283
  other unit metadata even when unchanged source needs no new embedding. A run
284
284
  with no content or metadata changes keeps the existing dump and retention
285
- window. Unreleased after `2.0.0.beta3`: a full `Indexer#index_all` run replaces
285
+ window. Included in Woods `2.0.0`: a full `Indexer#index_all` run replaces
286
286
  the published corpus even when a custom caller reuses in-memory vector and
287
287
  metadata stores. Deleted units, including metadata-only records, are removed;
288
288
  an empty full rebuild publishes an empty dump. Failed embedding leaves the
@@ -291,6 +291,13 @@ unchanged. This is a reasonable default for hosts that don't bundle `sqlite3`.
291
291
 
292
292
  ## Retrieval cache options
293
293
 
294
+ The 2.0.1 maintenance patch scopes retrieval contexts to one retriever instance.
295
+ Reload retires only that instance's namespace, including results still in flight;
296
+ already-running requests may finish against the previous corpus. Restarting starts
297
+ a fresh context namespace. Retired entries expire under their configured TTL or
298
+ backend eviction; disabling both can retain unused entries indefinitely. Embedding
299
+ caches remain separate and are not cleared by context invalidation.
300
+
294
301
  The optional cache wraps both embedding-provider calls and assembled retrieval
295
302
  contexts. It is disabled by default and is separate from the Index Server's
296
303
  tool-result `_meta` cache hint.
@@ -451,7 +458,7 @@ A malformed record invalidates the entire snapshot, rather than exposing partial
451
458
  history. Legacy bare identifier keys, omitted/null unit collections, and optional per-unit hash
452
459
  fields remain supported; timestamp strings are not restricted to a new format.
453
460
  Unit history limits count matching unit records, not the most recent snapshots
454
- searched (JSON fallback correction unreleased after `2.0.0.beta3`). A unit
461
+ searched (JSON fallback correction included in Woods `2.0.0`). A unit
455
462
  missing from newer snapshots can still have retained history.
456
463
  Snapshot lists and unit history omit unusable files; direct lookup returns no
457
464
  snapshot, and a diff with an unavailable snapshot returns empty added, modified,
@@ -682,7 +689,7 @@ deployment guide including defense layers.
682
689
  | `console_mcp_enabled` | Boolean | `false` | Master switch. When `false`, stdio exits and the mounted Console middleware passes requests through to Rails. |
683
690
  | `console_mcp_http_enabled` | Boolean | `true` | HTTP transport switch; effective only while the master switch is on. Set `false` for stdio-only use without HTTP token validation or an active HTTP endpoint. Read at request time. |
684
691
  | `console_mcp_token` | String | `ENV['WOODS_CONSOLE_MCP_TOKEN']` or `nil` | Bearer token required on every enabled Console HTTP request. With both Console flags enabled, production boot raises on a missing token; other environments warn and requests fail closed with 401. A configured token shorter than 32 characters raises at boot while HTTP is enabled. Explicit stdio-only configurations skip HTTP token validation. Generate with `SecureRandom.hex(32)`. |
685
- | `console_mcp_allowed_origins` | Array\<String\> | `%w[http://localhost http://127.0.0.1 http://[::1]]` | `OriginGuard` allowlist. Port is stripped before comparison, so `http://localhost` matches any localhost port. Override for tunneled / internal-dashboard access. |
692
+ | `console_mcp_allowed_origins` | Array\<String\> | `%w[http://localhost http://127.0.0.1 http://[::1]]` | `OriginGuard` allowlist shared with SDK dispatch. Portless entries permit same-authority requests; cross-origin ports must be listed explicitly. Default HTTP(S) ports normalize to their omitted form. Invalid entries fail at boot. |
686
693
  | `console_mcp_path` | String | `/mcp/console` | URL path the Rack middleware responds on. |
687
694
  | `console_embedded_read_tools` | Boolean | `false` | Register `console_sql` and `console_query` in supported stdio and Rack modes. |
688
695
  | `console_blocked_tables` | Array\<String\> | `Woods::DEFAULT_CONSOLE_BLOCKED_TABLES` | TableGate denylist (case-insensitive). Bare names match every schema; qualified names (`schema.table`) match exactly. |
@@ -704,7 +711,7 @@ These variables are read by the gem and its MCP servers at runtime. They complem
704
711
  |----------|---------|---------|
705
712
  | `WOODS_RETRIEVAL_MODE` | `semantic` | Explicit packaged MCP retrieval mode: `semantic` or `lexical`. Lexical reads extraction unit JSON without provider autodetection, credentials or vector artifacts. |
706
713
  | `WOODS_DIR` | unset | MCP extraction-index path, after a positional argument and before `WOODS_OUTPUT`. See precedence below. |
707
- | `WOODS_OUTPUT` | unset | MCP index-path fallback when neither a positional path nor `WOODS_DIR` is set; unreleased after `2.0.0.beta3`. |
714
+ | `WOODS_OUTPUT` | unset | MCP index-path fallback when neither a positional path nor `WOODS_DIR` is set; included in Woods `2.0.0`. |
708
715
  | `WOODS_REQUIRE_INDEX` | unset | Set to `"1"` to fail closed: the server refuses to boot (raises `MissingArtifact`) unless a real index (`woods.json`) is present. By default an extract-only host boots in pattern/structural mode without it. Explicit lexical mode requires a valid published extraction index, not `woods.json`. |
709
716
  | `WOODS_ALLOW_AUTODETECT` | unset | **Deprecated no-op.** Auto-detect is now the default; accepted for backward compatibility only. |
710
717
  | `WOODS_SEARCH_MAX_SCAN` | `500` | Cap on unit files loaded during a phase-2 (metadata/source_code) `search`. Hitting the cap sets `partial: true` in the response. |
@@ -720,7 +727,7 @@ These variables are read by the gem and its MCP servers at runtime. They complem
720
727
  | `WOODS_QDRANT_URL`, `WOODS_QDRANT_COLLECTION`, `WOODS_QDRANT_API_KEY` | n/a | Override/require Qdrant connection settings when a pgvector/Qdrant-backed index is served outside its host application (no `Woods.configuration` available). |
721
728
  | `WOODS_PG_URL` | n/a | Required when a pgvector-backed index is served outside its host application. |
722
729
 
723
- **MCP index path precedence (unreleased after `2.0.0.beta3`):** positional
730
+ **MCP index path precedence (included in Woods `2.0.0`):** positional
724
731
  argument → `WOODS_DIR` → `WOODS_OUTPUT` → current directory for `woods-mcp`
725
732
  and `woods-mcp-http`. `woods-mcp-start` still requires one of the first three;
726
733
  it never silently selects the current directory. An explicitly empty
@@ -771,10 +778,24 @@ existing index before deciding another extraction is needed.
771
778
  | `WOODS_WATCH_CATCH_UP` | `1` (enabled) | Set to `"0"` to skip generation-watermark catch-up on daemon start. |
772
779
  | `WOODS_WATCH_TRUST_FOREIGN_HOST` | unset (disabled) | Set to `"1"` in each task/MCP reader to trust a foreign daemon's heartbeat for up to 15 minutes, without a local pid check. See [cross-host liveness](WATCH_DAEMON.md#cross-host-liveness) for clock bounds, degraded coverage, and startup limitations. |
773
780
 
781
+ ### Managed watcher startup
782
+
783
+ **Included in Woods `2.0.0`.** `woods-watch` wraps the raw task for native
784
+ Foreman/Puma lifecycle management. `--root PATH` selects the application working
785
+ directory; `--boot-timeout SECONDS` defaults to `300` and bounds boot/handshake,
786
+ not extraction. Explicit child argv follows `--`. Output-directory precedence
787
+ remains `WOODS_OUTPUT`, then `Woods.configuration.output_dir`.
788
+
789
+ Managed mode requires `WOODS_WATCH_IDLE_TIMEOUT` to be unset, preserves one owner,
790
+ and never takes over a conflicting daemon. The optional Puma adapter only starts
791
+ in its finalized development environment. See [startup and installation](WATCH_DAEMON.md#managed-development-startup)
792
+ for the generator's explicit modes, portable receipt, update/removal, and the
793
+ separate supervision status. Raw task settings above remain compatible.
794
+
774
795
  ### Opt-in plugin refresh hooks
775
796
 
776
797
  These settings control the plugin shell worker. Check installed
777
- `woods:hook_refresh` support first; the task is unreleased after 2.0.0.beta2.
798
+ `woods:hook_refresh` support first; the task is included in Woods `2.0.0`.
778
799
  See [hook coverage and retry](WATCH_DAEMON.md#hooks-for-agent-sessions) and
779
800
  [optional context limits](WATCH_DAEMON.md#optional-bounded-context-hints).
780
801
 
@@ -807,26 +828,23 @@ tasks for manual refreshes; hook transport is not a general shell execution API.
807
828
  | `GITHUB_BASE_REF` | unset (GitHub Actions) | Build the diff range `origin/<ref>...HEAD` for `woods:incremental`; an unfetched ref makes the range unresolvable, same exit behavior. |
808
829
  | `RAILS_ENV` | `development` | Rails environment the rake tasks boot in. |
809
830
  | `WOODS_PROFILE` | unset | Set to `"1"` to log disjoint `[Woods] [profile] <phase> in N.NNs` durations, including git enrichment, reconciliation, payload sync, pointer publication (`publish`) and retention (`payload prune`). Separate `[profile total]` lines report whole extraction wall time, including unprofiled setup and failed runs; never add these totals to phase durations. Excludes process/Rails boot before extraction. Off by default. |
810
- | `WOODS_GIT_DIR` | unset | Absolute path to the canonical git directory. Wins over the repository Woods would otherwise find, at all three of its git call sites: per-unit `commit_count`/`change_frequency` (enrichment), `manifest.json`'s `git_branch`/`git_sha` (provenance), and the `woods:incremental` diff range. All three build their command line with `Woods::GitCommand.argv`. |
831
+ | `WOODS_GIT_DIR` | unset | Absolute path passed directly to Git's `--git-dir`, selecting that directory's `HEAD`. For a linked worktree use its `worktrees/<id>` directory inside the complete shared Git layout. Wins over the repository Woods would otherwise find, at all three of its git call sites: per-unit `commit_count`/`change_frequency` (enrichment), `manifest.json`'s `git_branch`/`git_sha` (provenance), and the `woods:incremental` diff range. All three build their command line with `Woods::GitCommand.argv`. |
811
832
  | `GIT_BRANCH`, `GIT_SHA` | unset | Provenance for a checkout with no `.git` at all (a source tarball, a Docker `COPY` that excludes it). Ignored when a `.git` is present but unresolvable, so a stale build arg cannot mask a worktree. |
812
833
 
813
- **`GIT_DIR` alone is not enough for a linked git worktree.** Woods runs git as a
814
- subprocess, so git's own `GIT_DIR` and `GIT_COMMON_DIR` are honored wherever git
815
- honors them. But pointing `GIT_DIR` at a worktree's *private* git directory only
816
- moves the failure: that directory reaches the shared object store through a
817
- relative `commondir` pointer, which still resolves outside a container mount,
818
- and `GIT_COMMON_DIR` does not override it. `git rev-parse --git-dir` then
819
- succeeds while no ref resolves.
820
-
821
- Woods refuses to enrich in that state rather than writing `commit_count: 0` and
822
- `change_frequency: "new"` on every unit: the git keys are omitted, provenance is
823
- `"unknown"`, and one warning names the cause. Point `WOODS_GIT_DIR` at the
824
- canonical git directory (the one a worktree's `gitdir:` pointer ultimately leads
825
- to) and mount it:
826
-
827
- ```bash
828
- WOODS_GIT_DIR=/canonical-git bundle exec rake woods:extract
829
- ```
834
+ For linked worktrees in containers, preserve access to the complete shared
835
+ Git directory and the worktree-specific HEAD. A same-path mount that resolves
836
+ the existing `.git` pointer needs no override. For a relocated complete layout,
837
+ set `WOODS_GIT_DIR=/mounted-common/worktrees/<id>`, deriving `<id>` from Git's
838
+ worktree metadata rather than the branch name. Selecting `/mounted-common`
839
+ itself selects the primary checkout's HEAD and can produce incorrect history,
840
+ provenance, and incremental paths. See the canonical
841
+ [worktree mount and verification steps](TROUBLESHOOTING.md#git-directory-mounts-for-linked-worktrees).
842
+
843
+ Git subprocesses inherit Git's own environment variables, so inspect existing
844
+ `GIT_DIR` and `GIT_COMMON_DIR` settings when resolving layout problems.
845
+ If HEAD cannot be resolved, enrichment omits the Git keys and provenance is
846
+ `"unknown"` for a present but unresolvable `.git`; one warning names the cause.
847
+ After correcting the layout, run a full extraction to refresh retained metadata.
830
848
 
831
849
  ### Exporters
832
850
 
@@ -846,11 +864,10 @@ The `woods-mcp` bootstrapper emits a one-line STDERR banner at startup indicatin
846
864
 
847
865
  ## Git enrichment history
848
866
 
849
- Current source requires **Git 2.31 or newer** for optional per-unit git
867
+ Woods 2.0 requires **Git 2.31 or newer** for optional per-unit git
850
868
  metadata. Extraction still succeeds when git is unavailable or history cannot
851
869
  be read completely. Git enrichment is omitted in either case; a failed or
852
870
  incomplete streamed history read logs a warning.
853
- This requirement and the history policy below are unreleased after 2.0.0.beta2.
854
871
 
855
872
  Per-unit enrichment also requires a non-shallow repository. A shallow checkout
856
873
  or a failed repository-depth probe omits enrichment with one warning per
@@ -48,7 +48,7 @@ its token, allowed origins and TLS as described in [Option C](#option-c-http-rac
48
48
 
49
49
  The rake task does two things before starting the MCP server:
50
50
 
51
- 1. **Captures stdout before Rails boots.** Rails boot emits OpenTelemetry warnings, gem notices, and other output to stdout. An MCP client cannot parse these as JSON-RPC, they break the protocol. The rake task redirects stdout → stderr immediately, saves the real stdout fd, and restores it after boot completes.
51
+ 1. **Captures stdout before Rails boots.** Rails boot emits OpenTelemetry warnings, gem notices, and other output to stdout. An MCP client cannot parse these as JSON-RPC. The rake task saves the protocol output and redirects application stdout to stderr. In the runtime isolation fix (included in Woods `2.0.0`), that redirection remains active throughout the server's lifetime; only the MCP transport writes to the saved protocol pipe. Rails loggers, `puts`, and writes to standard output during queries stay on stderr.
52
52
  2. **Calls `Rails.application.eager_load!`** to load all application models. Without eager loading, only the models that happen to be autoloaded before the first query appear in the registry.
53
53
 
54
54
  ### MCP client configuration
@@ -83,7 +83,7 @@ rake woods:console
83
83
  │ ├─ Rails.application.eager_load!
84
84
  │ ├─ build model registry from ActiveRecord::Base.descendants
85
85
  │ ├─ Server.build_embedded(model_validator:, safe_context:, ...)
86
- │ └─ MCP::Server::Transports::StdioTransport.new(server).open
86
+ │ └─ Woods::Console::StdioTransport.new(server, output: protocol_out).open
87
87
  │
88
88
  └─ MCP server responds to tool calls via stdin/stdout
89
89
  ```
@@ -201,7 +201,7 @@ request. Missing or incorrect tokens receive `401 Unauthorized`.
201
201
  The HTTP authentication scheme is ASCII case-insensitive (`Bearer`, `bearer`,
202
202
  or `BEARER`); the token remains case-sensitive and must match exactly after one
203
203
  space. This applies to both Console HTTP and `woods-mcp-http`. Case-insensitive
204
- scheme support is unreleased after `2.0.0.beta3`; use the canonical `Bearer`
204
+ scheme support is included in Woods `2.0.0`; use the canonical `Bearer`
205
205
  spelling in client configuration for compatibility with earlier releases.
206
206
 
207
207
  For non-loopback access, `console_mcp_allowed_origins` must include the public
@@ -209,7 +209,10 @@ Rails/MCP host. If a browser-based client sends an `Origin` header from a
209
209
  different host, include that exact origin too. This allow-list controls both
210
210
  DNS-rebinding Host checks and browser CORS; keep Rails' own `config.hosts`, TLS,
211
211
  and proxy rules aligned with it. Server-to-server clients normally omit
212
- `Origin`, but their request `Host` must still be allowed.
212
+ `Origin`, but a present request `Host` must still be allowed. Supporting security
213
+ revisions deliberately allow configured non-loopback Hosts through the SDK too;
214
+ 2.0.0 could refuse them at that inner layer despite the Woods allowlist. This
215
+ widening is limited to the configured authorities and retains bearer auth.
213
216
 
214
217
  Do not mount `Woods::Console::RackMiddleware` by itself. The Railtie composes
215
218
  `OriginGuard`, `BearerAuth`, and the Console middleware in the supported order.
@@ -636,6 +639,9 @@ Redaction is defense-in-depth, prefer not storing plaintext secrets in database
636
639
 
637
640
  ### `console_redacted_key_values`
638
641
 
642
+ See [read policy compatibility](#read-policy-compatibility) for SQL column-list
643
+ restrictions, conservative typed masking, exact key spelling and binary columns.
644
+
639
645
  Column-name redaction falls short when credentials are stored in a **key-value (EAV)** table, e.g. a Stripe Connect `authorizations` row of `{key: "stripe_access_token", value: "sk_live_..."}`. The column holding the secret is called `value`, which is generic: adding `value` to `console_redacted_columns` would over-redact every unrelated row in the table.
640
646
 
641
647
  `console_redacted_key_values` takes one or more patterns that describe "when a row has `key_column` set to one of these names, redact its `value_column`":
@@ -700,7 +706,7 @@ these controls, in order:
700
706
 
701
707
  1. `SqlValidator` rejects DML/DDL (`INSERT`/`UPDATE`/`DELETE`/`MERGE`/`DROP`/`TRUNCATE`/`ALTER`/`CREATE`/`REPLACE`), row-lock clauses (`FOR UPDATE`, `FOR SHARE`, `LOCK IN SHARE MODE`), writable CTEs (every `AS (...)` body, not just the first), `UNION`/`INTO`/`COPY`, multi-statement and comment-hidden injections, and most administrative keywords (`DO`, `SET`, `LISTEN`, `NOTIFY`, `CALL`, `LOAD`, `VACUUM`, `PREPARE`, transaction control, `EXPLAIN ANALYZE`) at the string level. Enforces a read-only **function allowlist** (`ALLOWED_FUNCTIONS`), anything not on it is rejected by name, quoted forms (`"pg_terminate_backend"(…)`) included. Only `SELECT`, `WITH…SELECT`, and plain `EXPLAIN` pass.
702
708
  2. `TableGate` refuses any SQL, model, or join that touches a `console_blocked_tables` entry.
703
- 3. `SafeContext` wraps every request in a rolled-back transaction with a short statement timeout. **It does NOT cover async side effects**: ActiveJob `perform_later`, ActionMailer `deliver_later`, direct HTTP egress, `Thread.new`-spawned work, `after_rollback` callbacks, and writes through a different shard all execute as live. Treat the Console MCP as an admin-trust boundary, not a sandbox.
709
+ 3. `SafeContext` wraps every request in a rolled-back transaction with an adapter-dependent statement timeout. **It does NOT cover async side effects**: ActiveJob `perform_later`, ActionMailer `deliver_later`, direct HTTP egress, `Thread.new`-spawned work, `after_rollback` callbacks, and writes through a different shard all execute as live. Treat the Console MCP as an admin-trust boundary, not a sandbox.
704
710
  4. `CredentialScanner` + column/EAV redaction scrub results.
705
711
 
706
712
  Keep the flag off when the host requires a narrower database capability.
@@ -718,7 +724,7 @@ supported transport (stdio, Docker/SSH launcher, and HTTP).
718
724
  | 1 | Blocked tables | `console_blocked_tables` | Tool dispatch, before executor | Reject any tool call that touches a named table (model, table, or sql arg) |
719
725
  | 2 | Credential scanner | `console_disabled_scanner_patterns` (`[:all]` to disable entirely) | After executor, before render | Content-shape redaction of credential-shaped strings anywhere in the response tree |
720
726
  | 3 | Column + EAV redaction | `console_redacted_columns`, `console_redacted_key_values` | After executor, before Layer 2 | Identity-based redaction by column name and by key/value row shape |
721
- | 4 | SqlValidator + SafeContext | built-in | Inside executor | SQL deny-list for `console_sql`; transaction-rollback for every request |
727
+ | 4 | SqlValidator + SafeContext | built-in | Inside executor | SQL validation and function allowlist for `console_sql`; transaction rollback for every request |
722
728
 
723
729
  Layers 0–3 are configured via `Woods.configure`. Layer 4 is always on and has no knobs. Observability hooks, `console.table_gate.rejected` for Layer 1, `console.credential_scan.hits` for Layer 2, emit structured log lines via `Woods::Observability::StructuredLogger` so operators can audit enforcement without scraping MCP wire traffic.
724
730
 
@@ -754,13 +760,20 @@ boundary remain necessary.
754
760
 
755
761
  ### Statement timeout
756
762
 
757
- Each transaction sets a statement timeout before any query runs. The default is **5000ms** (5 seconds). Timeout enforcement is adapter-specific:
763
+ SafeContext attempts a **5000ms** (5-second) timeout. Support depends on the
764
+ adapter and server; an unsupported setting is skipped and logged when a Rails
765
+ logger is available.
758
766
 
759
767
  | Adapter | Mechanism | Scope |
760
768
  |---------|-----------|-------|
761
- | PostgreSQL | `SET statement_timeout = '5000ms'` | All statement types |
762
- | MySQL | `SET max_execution_time = 5000` (session scope; the prior value is restored after the transaction) | SELECT only (MySQL limitation) |
763
- | Other | Best-effort (skipped gracefully) | n/a |
769
+ | PostgreSQL | `SET LOCAL statement_timeout = '5000ms'` | Transaction-local; discarded on rollback |
770
+ | MySQL | `SET max_execution_time = 5000` | SELECT only; previous session value restored in `ensure` |
771
+ | MariaDB | `SET max_statement_time = 5.0` | Seconds; previous session value restored in `ensure` |
772
+ | SQLite / unrecognized family | No supported per-statement timeout | Do not rely on a query time limit |
773
+
774
+ Rollback remains active when a timeout setting is unsupported. Recognition of a
775
+ MySQL-family adapter alone does not establish that its server supports the
776
+ corresponding timeout variable.
764
777
 
765
778
  ### SQL validation (tier 4 `console_sql`)
766
779
 
@@ -771,7 +784,7 @@ Validation runs **once**, inside the executor, with the dialect of the live adap
771
784
 
772
785
  - **Allowed prefixes:** `SELECT`, `WITH...SELECT`, and plain `EXPLAIN`. `EXPLAIN ANALYZE` is rejected, it executes the query rather than just planning it (both the whitespace and `EXPLAIN (ANALYZE, …)` option-list spellings).
773
786
  - **Rejected prefixes:** `INSERT`, `UPDATE`, `DELETE`, `MERGE`, `DROP`, `ALTER`, `TRUNCATE`, `CREATE`, `GRANT`, `REVOKE`
774
- - **Rejected anywhere in query:** `UNION`, `INTO`, `COPY`; row-lock clauses (`FOR UPDATE`, `FOR NO KEY UPDATE`, `FOR SHARE`, `FOR KEY SHARE`, `FOR UPDATE NOWAIT`/`SKIP LOCKED`, MySQL `LOCK IN SHARE MODE`) — these take live row locks even inside the rolled-back transaction. The lock check is adapter-aware: `console_sql` validates with the active adapter's dialect, including MySQL double-quoted strings/backtick identifiers and PostgreSQL quoted identifiers/E-strings. Unknown adapters conservatively scan all supported normalizations. Every view is scanned under both MySQL executable-comment (`/*!...*/`) semantics, so `#` comments and version-guarded comments cannot split a clause apart.
787
+ - **Rejected anywhere in query:** `UNION`, `INTO`, `COPY`; row-lock clauses (`FOR UPDATE`, `FOR NO KEY UPDATE`, `FOR SHARE`, `FOR KEY SHARE`, `FOR UPDATE NOWAIT`/`SKIP LOCKED`, MySQL `LOCK IN SHARE MODE`) — these take live row locks even inside the rolled-back transaction. The lock check is adapter-aware: `console_sql` validates with the active adapter's dialect, including MySQL double-quoted strings/backtick identifiers and PostgreSQL quoted identifiers/E-strings. Direct validator calls without a dialect conservatively scan all supported normalizations; packaged `console_sql` refuses unrecognized adapter families. Every view is scanned under both MySQL executable-comment (`/*!...*/`) semantics, so `#` comments and version-guarded comments cannot split a clause apart.
775
788
  - **Function allowlist (the authoritative function control):** every function-call-shaped identifier must appear in `ALLOWED_FUNCTIONS`, a conservative set of pure read-only functions (aggregates, window functions, string/number/date/JSON readers) kept portable across MySQL, PostgreSQL, and SQLite. Anything else is rejected by name, quoted forms (`"pg_terminate_backend"(…)`) included. This is an allowlist because a denylist cannot enumerate every side-effecting function (`nextval`, `pg_advisory_lock`, `pg_terminate_backend`, …). A legacy `DANGEROUS_FUNCTIONS` denylist (`pg_sleep`, `lo_import`, `lo_export`, `pg_read_file`, `pg_write_file`, `load_file`, `sleep`, `benchmark`) still runs first as belt-and-suspenders.
776
789
  - **Rejected patterns:** multiple statements (semicolons), writable CTEs (every `AS (...)` body is checked, so a writable CTE in any WITH position is refused — `WITH a AS (SELECT 1), b AS (DELETE FROM users RETURNING *) SELECT * FROM b`), a CTE list attached to top-level DML (`WITH a AS (SELECT 1) DELETE FROM users RETURNING *`), comment-hidden injections
777
790
 
@@ -799,13 +812,15 @@ to enforce their narrower parameterized scope grammar.
799
812
  - **HTTP:** Check that the Rails server is running and listening on the expected port. An unauthenticated `curl http://localhost:3000/mcp/console` should return `401` when the enabled middleware and bearer-auth guard are mounted. A request with the configured bearer token proceeds to MCP protocol handling.
800
813
  - **All modes:** Run `bundle exec rake woods:console` directly in a terminal. It should hang (waiting for MCP protocol input) rather than exit immediately. If it exits, check the error output.
801
814
 
802
- ### Rails boot noise breaks MCP protocol
815
+ <a id="rails-boot-noise-breaks-mcp-protocol"></a>
816
+
817
+ ### Rails logs break MCP protocol
803
818
 
804
- The rake task redirects stdout to stderr before Rails boots specifically to prevent this. If you see JSON parse errors from the MCP client, check:
819
+ Prefer `bundle exec rake woods:console`: it captures output before the Rails environment boots. Direct `rails runner` invocation can redirect output only after Rails has booted; it cannot recover an already contaminated protocol stream.
805
820
 
806
- 1. You are using `bundle exec rake woods:console`, not `rails runner exe/woods-console` directly (the runner path handles this too, but via a different mechanism).
807
- 2. No `puts` or `print` calls run at boot in your initializers before the task can capture stdout.
808
- 3. Try running `bundle exec rake woods:console 2>/dev/null` to isolate, the MCP protocol output goes to stdout, Rails noise goes to stderr.
821
+ The runtime isolation fix is included in Woods `2.0.0`. Earlier builds restore stdout after boot, so a logger that writes there can interleave SQL or application logs with MCP responses. On those builds, configure the Console process's application logger to write to stderr or a file. Discarding stderr does not fix logs written to stdout.
822
+
823
+ With a build containing the fix, application output remains on stderr during tool calls. If protocol contamination persists, inspect wrapper scripts and output emitted before the rake task starts; reserve stdout for MCP and retain stderr for diagnosis. Do not disable Console redaction or credential scanning to troubleshoot transport logging.
809
824
 
810
825
  ### Models not visible to `console_status`
811
826
 
@@ -900,3 +915,67 @@ These corrections require `2.0.0.beta4` or a reviewed development revision that
900
915
  contains them. Confirm that a patched release is available before selecting it.
901
916
  On affected versions, disable Console where these policies are required; Index MCP
902
917
  can stay enabled because it reads the published code index separately.
918
+
919
+
920
+ ## Maintenance policy corrections
921
+
922
+ The 2.0.1 maintenance patch applies authentication and origin policy to each
923
+ Console HTTP mount. Keep the normal Railtie setup: these additional checks do not
924
+ make an unreviewed manual mount the recommended installation path. Configure
925
+ allowed origins before boot and restart after changes; malformed entries fail
926
+ once at boot with the offending entry identified.
927
+
928
+ Protected collection values are masked as complete cells. EAV key policy checks
929
+ both stored and cast keys, including in predicates and ordering. Raw SQL refuses
930
+ ambiguous protected source identity, protected whole-row projections, positional
931
+ ordering that could expose protected values, and unsupported result types.
932
+ Explicit unaliased scalar columns and the structured tools are the recovery path.
933
+
934
+ SQL dialect detection follows adapter ancestry before adapter names. PostgreSQL
935
+ subclasses, Mysql2, MariaDB, Trilogy and SQLite use their applicable policies.
936
+ Genuinely unknown families are refused only by raw `console_sql`; this restriction
937
+ does not disable structured Tier-1 tools. Existing tool opt-ins remain unchanged.
938
+
939
+ PostgreSQL Unicode-escaped identifiers are refused by `console_sql` before
940
+ execution. Use ordinary identifiers or standard quoted identifiers instead;
941
+ structured query tools are unaffected by this syntax restriction.
942
+
943
+ ### Read policy compatibility
944
+
945
+ These rules describe the security-patch source; verify the installed revision
946
+ and loaded gem path until its release is published.
947
+
948
+ - **Column alias lists:** when either `console_redacted_columns` or
949
+ `console_redacted_key_values` is nonempty, `console_sql` refuses relation
950
+ and CTE column alias lists before execution, including lists on base tables,
951
+ derived tables, parenthesized `VALUES` sources and table functions. This applies
952
+ even when the selected names are not protected and independently of function
953
+ validation. Ordinary relation
954
+ aliases, CTEs without column lists and allowed scalar functions remain subject
955
+ to the normal SQL policy. Use explicit, unaliased protected columns or a
956
+ structured Console tool.
957
+ - **Conservative typed masking:** EAV type lookup matches the final source-table
958
+ name case-insensitively and includes every matching registered model, even
959
+ across schemas. Any matching type can cause masking. This deliberately may
960
+ mask extra values when table names differ only by case or share that final
961
+ name; qualifying the table does not narrow that type set.
962
+ - **Exact sensitive values:** `sensitive_keys` compares the stored and cast key
963
+ values with exact case after string conversion. Configure their actual raw or
964
+ cast spelling. `CredentialIndex` also matches credential substrings with exact
965
+ case; it does not decode hexadecimal binary output such as PostgreSQL `bytea`.
966
+ Binary cells have no general text-scanning guarantee: non-UTF-8 data can fail
967
+ JSON normalization before scanning. Put binary secret columns in
968
+ `console_redacted_columns` so they are masked before serialization.
969
+ - **Adapter boundary:** raw `console_sql` requires PostgreSQL, MySQL-family
970
+ (including Mysql2, MariaDB and Trilogy), or SQLite classification. Compatible
971
+ subclasses are recognized by ancestry. Unknown families receive a validation
972
+ refusal for raw SQL; structured tools remain available under their normal
973
+ gates. See [statement timeouts](#statement-timeout) for adapter limits.
974
+ - **SQL functions and select entries:** 2.x enforces a read-only function
975
+ allowlist; 1.6.x retains a function denylist. For `console_query`, provide one
976
+ expression per `select` array entry. 2.x refuses comma-combined entries that
977
+ 1.6.4 splits before validation. Execution and typed redaction use the same
978
+ validated projection on all patched lines.
979
+ - **Association counts:** polymorphic `belongs_to` counts remain unsupported
980
+ on this maintenance line and fail closed with a generic execution error.
981
+ The 2.1 functional correction is not included in this backport.
data/docs/DOCKER_SETUP.md CHANGED
@@ -87,9 +87,24 @@ docker compose exec app bundle exec rake woods:extract_framework
87
87
 
88
88
  Run the watcher as its own development service or process-manager entry, not as a one-off terminal command. Docker Desktop bind mounts may not deliver reliable native filesystem events; set `WOODS_WATCH_POLL=1` for polling when needed. The watcher updates structural generations automatically, while semantic vectors still require `woods:embed_incremental`.
89
89
 
90
+ Use the application's actual Rails task entrypoint. If its root `Rakefile` wraps
91
+ Compose, the container may need `bundle exec rails woods:watch`. Keep the raw
92
+ task under one external restart owner: `restart: unless-stopped` recovers after
93
+ Docker restarts while respecting an intentional stop; `on-failure` covers failed
94
+ process exits but not Docker restart. Leave idle TTL unset for continuous work.
95
+
96
+ Validate `docker compose config`: explicit YAML anchor keys can replace inherited
97
+ mounts/environment, and short `depends_on` does not establish database readiness.
98
+ Preserve the existing source/bundle mounts and use the application's healthcheck
99
+ convention. With Grove, include the watcher in the applicable shared or isolated
100
+ services list and align source/index mounts to the selected worktree. Follow
101
+ [Docker and Grove verification](AUTOMATIC_MAINTENANCE.md#docker-verify-the-resolved-service)
102
+ instead of copying a generic service that loses required settings.
103
+
90
104
  When host-side tasks or one-off containers read the daemon's shared index,
91
105
  `WOODS_WATCH_TRUST_FOREIGN_HOST=1` lets those readers trust its recent heartbeat.
92
106
  Set it in each reader process; Docker does not forward host variables by default.
107
+ Ordinary Index MCP reads do not need this trust or the extraction writer lock.
93
108
  See [cross-host liveness](WATCH_DAEMON.md#cross-host-liveness) for the 15-minute
94
109
  crash-detection bound and single-supervisor requirement.
95
110
 
data/docs/EVALUATION.md CHANGED
@@ -90,8 +90,8 @@ bundle exec ruby -Ilib bench/evaluation/runner.rb
90
90
  strategy selection also fail. The baseline format is developer-only and is
91
91
  **not** the `EVAL_BASELINE_FILE` aggregate-threshold format.
92
92
 
93
- B-190/B-191 recapture on Ruby 4.0.6 (five warmed pipeline repetitions per query; Ruby 3.3.1
94
- and 3.4.10 replay the same answers):
93
+ B-190/B-191 baseline, with the #549 output-label refresh captured on Ruby 4.0.6
94
+ (five warmed pipeline repetitions per query):
95
95
 
96
96
  | Strategy | Queries | Precision@5 | Recall | MRR | Mean actual context tokens |
97
97
  |---|---:|---:|---:|---:|---:|
@@ -99,8 +99,14 @@ and 3.4.10 replay the same answers):
99
99
  | Vector | 4 | 0.313 | 0.375 | 0.625 | 1,041.2 |
100
100
  | Graph | 8 | 0.813 | 0.519 | 1.000 | 1,041.1 |
101
101
  | Hybrid | 4 | 0.750 | 0.396 | 1.000 | 1,058.8 |
102
- | Direct with type filtering | 4 | 0.375 | 0.750 | 0.625 | 551.0 |
103
- | Within-type vector fallback | 4 | 0.400 | 1.000 | 1.000 | 1,250.8 |
102
+ | Direct with type filtering | 4 | 0.375 | 0.750 | 0.625 | 552.0 |
103
+ | Within-type vector fallback | 4 | 0.400 | 1.000 | 1.000 | 1,251.8 |
104
+
105
+ The clearer `Retrieval metadata records` heading adds one exact `cl100k_base`
106
+ token to each of the eight direct/fallback contexts. The other 20 contexts,
107
+ retrieved identifiers and order, annotations, and quality metrics are unchanged.
108
+ The refresh reuses captured vectors and the pinned `tiktoken 0.11.0` tokenizer;
109
+ it performs no new model inference.
104
110
 
105
111
  Precision@5 divides relevant hits by the actual returned slice size (up to five),
106
112
  not always by five. Recall divides retrieved relevant units by all annotated
@@ -32,6 +32,14 @@ Extractors discover code one of two ways:
32
32
 
33
33
  Some extractors combine both (e.g., `JobExtractor` scans directories first, then supplements with `ApplicationJob.descendants`).
34
34
 
35
+ Discovery is not exhaustive. A standalone module under `app/models` that is
36
+ called through singleton methods is not discovered by the model/PORO paths;
37
+ conventional concerns and app modules included by live models have separate
38
+ concern discovery. This gap is tracked in [#552](https://github.com/lost-in-the/woods/issues/552).
39
+ Separately, dependency scanning does not capture every method-body constant
40
+ reference ([#475](https://github.com/lost-in-the/woods/issues/475)). A missing unit
41
+ or edge is not proof of unused code; cross-check the application source.
42
+
35
43
  ### Identifier naming (source-derived units)
36
44
 
37
45
  File-based extractors derive an identifier in three steps, first match wins:
@@ -746,11 +754,15 @@ History is limited to commits reachable from `HEAD` in the past 365 days,
746
754
  including merged branch history. Unmerged branches, remote refs, and tool
747
755
  checkpoint refs do not contribute. Commands run against the application root;
748
756
  when `WOODS_GIT_DIR` is set, `HEAD` belongs to that explicitly selected git
749
- directory, which may differ from a linked worktree's HEAD.
757
+ directory. For a linked worktree, select its `worktrees/<id>` directory within
758
+ the complete shared Git layout; selecting the shared root instead uses the
759
+ primary checkout's HEAD. See the [worktree mount guide](TROUBLESHOOTING.md#git-directory-mounts-for-linked-worktrees).
750
760
 
751
761
  After upgrading from a version that included all refs, run a full
752
762
  `woods:extract` to replace previously published git metadata. Incremental
753
- extraction refreshes only the units it rewrites.
763
+ extraction refreshes only the units it rewrites. A commit alone does not
764
+ necessarily trigger the source-file watcher; run full extraction when current
765
+ Git history is required.
754
766
 
755
767
  | Field | Description |
756
768
  |-------|-------------|
data/docs/FAQ.md CHANGED
@@ -134,7 +134,7 @@ When a model includes a concern, the behavior defined in that concern is part of
134
134
 
135
135
  ### How do I update the index after code changes?
136
136
 
137
- Use incremental mode, which re-extracts only files that have changed since the last run:
137
+ Use incremental mode to dispatch a selected batch of changed paths:
138
138
 
139
139
  ```bash
140
140
  bundle exec rake woods:incremental
@@ -143,7 +143,12 @@ bundle exec rake woods:incremental
143
143
  docker compose exec app bundle exec rake woods:incremental
144
144
  ```
145
145
 
146
- Incremental mode is ideal for CI pipelines and local development workflows. It is typically 5-10× faster than a full extraction. Nine unit types, `route`, `middleware`, `engine`, `scheduled_job`, `state_machine`, `factory`, `event`, `database_view`, and `rails_source`, don't map to individual files, so incremental mode re-runs their extractor **wholesale** whenever the relevant trigger path changes (e.g. `config/routes.rb` for routes, `Gemfile.lock` for middleware/engines/rails_source; `rails_source` participates only when `include_framework_sources` is enabled). You never need to run a full extraction just because one of these changed, see [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for details.
146
+ The default Git range is `HEAD~1`; CI variables, an explicit range, or
147
+ `CHANGED_FILES` can select another batch. Incremental extraction also refreshes
148
+ affected concern consumers and re-runs whole-app extractors when their trigger
149
+ paths change. It can reduce work, but has no universal speedup: Rails boot, graph
150
+ work, and publication still take time. See the [incremental contract](INCREMENTAL_EXTRACTION.md)
151
+ for scope, whole-app triggers, and cases that require a full extraction.
147
152
 
148
153
  ---
149
154
 
@@ -411,7 +416,13 @@ When you run `rake woods:embed`, Woods generates embedding vectors for each extr
411
416
 
412
417
  ### What is the `codebase_retrieve` tool for?
413
418
 
414
- `codebase_retrieve` is the primary semantic search tool on the Index Server. It accepts a natural-language description of what you're looking for ("find where user email validation happens", "which services send Stripe API calls") and returns the most relevant extracted units as formatted context. It requires embedding configuration, without an embedding provider the tool responds with an error (`isError`, code `:not_configured`) and a remediation hint covering provider setup and the `search` tool for pattern-based matching in the meantime. Token budget is controlled by `config.max_context_tokens` (default: 8000).
419
+ `codebase_retrieve` ranks extracted units for a natural-language query, such as
420
+ "find where user email validation happens". Default semantic mode requires an
421
+ embedding provider and vector store. Explicit lexical mode ranks published
422
+ extraction text without either: set `WOODS_RETRIEVAL_MODE=lexical` in the MCP
423
+ process environment and restart it. There is no automatic fallback between
424
+ modes. See the [retrieval guide](RETRIEVAL_GUIDE.md#embedding-free-lexical-retrieval)
425
+ for setup, ranking evidence, and the estimated text-budget contract.
415
426
 
416
427
  ---
417
428
 
@@ -12,15 +12,15 @@ If an agent will perform the installation, use the safety and handoff checklist
12
12
 
13
13
  ## 1. Install the gem
14
14
 
15
- Use the [README release table](../README.md) to choose a version, then confirm
16
- that **exact version is published** on the [RubyGems versions page](https://rubygems.org/gems/woods/versions)
17
- before editing the Gemfile. A prepared release checkout can update the README
15
+ Choose a version from the [RubyGems versions page](https://rubygems.org/gems/woods/versions)
16
+ and confirm that **exact version is published** before editing the Gemfile.
17
+ A prepared release checkout can update the documentation
18
18
  before its gem is published; if the version is absent, choose an available
19
19
  version or wait for publication.
20
20
 
21
21
  If the published 2.x line has only beta or release-candidate versions, use an
22
- exact pin to the published prerelease, following the README's prerelease
23
- instructions; `~> 2.0` does not select prereleases. Follow the selected version's
22
+ exact pin to the published prerelease; `~> 2.0` does not select prereleases.
23
+ Follow the selected version's
24
24
  tag documentation. The `main` guides may describe features absent from the
25
25
  published gem.
26
26
 
@@ -148,21 +148,22 @@ Reconnect the MCP server and check `woods_status`. For OpenAI, pgvector, Qdrant,
148
148
 
149
149
  ### Keep the index current
150
150
 
151
- For automatic maintenance, keep a watcher running beside the Rails development process:
152
-
153
- ```bash
154
- bin/rails woods:watch
155
- ```
156
-
157
- ```text
158
- # Procfile.dev
159
- web: bin/rails server
160
- woods: bundle exec rake woods:watch
161
- ```
151
+ Enable one watcher through the application's normal development startup. The
152
+ [managed startup guide](WATCH_DAEMON.md#managed-development-startup) covers
153
+ opt-in Puma integration for a simple Rails application, an owned Foreman entry
154
+ for existing Procfile workflows, and external supervision for Docker/Grove.
155
+ The managed launcher and generator are **included in Woods `2.0.0`**;
156
+ check the installed commands before using them. Older packages can run the raw
157
+ `bin/rails woods:watch` task under an external restart-capable supervisor.
162
158
 
163
159
  On startup it reconciles changes made since the last successful generation. While running it batches file events, reloads Rails code when safe, extracts affected units, and publishes atomically. The Index Server detects the new generation on its next call and reloads automatically. After the initial extraction, ordinary code edits need no manual extraction or MCP restart.
164
160
 
165
- When dependencies, initializers, database configuration, credentials, or schema change, Rails cannot safely reload all captured state. The watcher records a degraded reason and exits with status 75 so the process manager can restart it. Docker bind mounts may require polling; follow [Watch daemon](WATCH_DAEMON.md).
161
+ When dependencies, initializers, database configuration, credentials, or schema
162
+ change, the raw task exits 75 to request a fresh Rails boot. The managed launcher
163
+ handles that restart internally; an external supervisor must handle it for the
164
+ raw task. Do not add the bare task to Foreman. Docker bind mounts may require
165
+ polling. See the [low-interaction workflow](AUTOMATIC_MAINTENANCE.md) for ownership,
166
+ hooks, and the checks that establish automatic maintenance is active.
166
167
 
167
168
  The watcher maintains the structural index. If semantic retrieval is enabled, also run `bin/rails woods:embed_incremental` to update vectors. Without a resident watcher, run `bin/rails woods:incremental` after changes. Use a full `woods:extract` after major upgrades or when validation reports drift. CI and shared-artifact patterns are covered in [Incremental extraction](INCREMENTAL_EXTRACTION.md).
168
169
 
@@ -89,8 +89,13 @@ Recovery choices, in the order they are worth trying:
89
89
  3. Run a full `woods:extract` when the range cannot be repaired this run.
90
90
 
91
91
  The diff itself is rooted at the extracted application (`git -C Rails.root`),
92
- so it cannot read whatever checkout the process happened to start in — the
93
- same rooting rule the manifest's git provenance follows.
92
+ independently of the process working directory. An explicit `WOODS_GIT_DIR`
93
+ selects that Git directory's HEAD for both the diff and manifest provenance.
94
+ For a linked worktree, use its worktree-specific directory within the complete
95
+ shared layout; selecting the shared root instead reads the primary checkout's
96
+ HEAD. See [worktree mount verification](TROUBLESHOOTING.md#git-directory-mounts-for-linked-worktrees).
97
+ Git-only changes may not trigger the source-file watcher; see the
98
+ [watcher limitation](WATCH_DAEMON.md#watcher-backends).
94
99
 
95
100
  Named, source-defined app modules included by runtime models are tracked as concern units even
96
101
  outside `concerns/` directories. Changing their source refreshes their includers,
@@ -100,7 +105,7 @@ to populate these previously missing source mappings.
100
105
 
101
106
  ## Handled source errors and retry
102
107
 
103
- Unreleased after `2.0.0.beta3`: when an incremental extraction or named refresh
108
+ Included in Woods `2.0.0`: when an incremental extraction or named refresh
104
109
  records a handled consumer error (for example malformed locale or schedule
105
110
  YAML), it raises `Woods::ExtractionError` before publishing. Empty output from
106
111
  that failed consumer does not authorize replacing or deleting its last-good
data/docs/INDEX_LAYOUT.md CHANGED
@@ -94,8 +94,8 @@ families and `manifest.provenance.mode`; it is not Rails runtime evidence.
94
94
  ### Semantic graph validation
95
95
 
96
96
  `woods:validate` checks raw graph data against the unit indexes and artifacts in
97
- one pinned published generation. This validation is unreleased after
98
- `2.0.0.beta2`. It checks the shapes of `nodes`, `edges`, `reverse`, `file_map`,
97
+ one pinned published generation. This validation is included in Woods `2.0.0`.
98
+ It checks the shapes of `nodes`, `edges`, `reverse`, `file_map`,
99
99
  `type_index`, and optional `variants`; typed identities must be unique and agree
100
100
  with the actual indexed units. Forward sources must exist. Reverse, file, and
101
101
  type memberships must match the union of primary and variant contributions.