woods 2.0.1 → 2.1.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.
Files changed (154) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +94 -7
  3. data/CONTRIBUTING.md +134 -19
  4. data/README.md +1 -1
  5. data/docs/AGENT_GUIDE.md +19 -0
  6. data/docs/AGENT_SETUP.md +22 -2
  7. data/docs/BACKEND_MATRIX.md +7 -0
  8. data/docs/CLIENT_HOOKS.md +6 -0
  9. data/docs/CONFIGURATION_REFERENCE.md +133 -25
  10. data/docs/CONSOLE_MCP_SETUP.md +82 -30
  11. data/docs/EMBEDDING_MODELS.md +16 -19
  12. data/docs/EXTRACTOR_REFERENCE.md +219 -21
  13. data/docs/FAQ.md +11 -25
  14. data/docs/GETTING_STARTED.md +7 -1
  15. data/docs/INCREMENTAL_EXTRACTION.md +261 -19
  16. data/docs/INDEX_LAYOUT.md +5 -0
  17. data/docs/INTERNALS.md +9 -0
  18. data/docs/MCP_HTTP_TRANSPORT.md +20 -15
  19. data/docs/MCP_SERVERS.md +87 -8
  20. data/docs/MCP_TOOL_COOKBOOK.md +13 -55
  21. data/docs/NOTION_INTEGRATION.md +7 -1
  22. data/docs/PUBLISHED_INDEX.md +6 -0
  23. data/docs/README.md +6 -1
  24. data/docs/RETRIEVAL_GUIDE.md +17 -0
  25. data/docs/SOURCE_FRESHNESS.md +157 -5
  26. data/docs/TOKEN_BENCHMARK.md +10 -18
  27. data/docs/TROUBLESHOOTING.md +70 -14
  28. data/docs/UNBLOCKED_INTEGRATION.md +60 -8
  29. data/docs/UPGRADING_TO_2.md +153 -38
  30. data/docs/WATCH_DAEMON.md +97 -14
  31. data/exe/woods-console-mcp +2 -2
  32. data/lib/generators/woods/templates/woods.rb.tt +2 -1
  33. data/lib/tasks/woods.rake +23 -7
  34. data/lib/tasks/woods_checks.rake +2 -2
  35. data/lib/woods/agent_configuration/cli.rb +1 -1
  36. data/lib/woods/agent_configuration/layout.rb +16 -2
  37. data/lib/woods/agent_configuration/plan.rb +13 -3
  38. data/lib/woods/agent_configuration/planner_validation.rb +4 -2
  39. data/lib/woods/agent_configuration/preflight.rb +5 -3
  40. data/lib/woods/builder.rb +17 -57
  41. data/lib/woods/cache/cache_middleware.rb +56 -30
  42. data/lib/woods/chunking/contributor_chunks.rb +119 -0
  43. data/lib/woods/chunking/semantic_chunker.rb +44 -21
  44. data/lib/woods/console/connection_manager.rb +56 -3
  45. data/lib/woods/console/embedded_executor.rb +30 -5
  46. data/lib/woods/console/rack_middleware.rb +29 -1
  47. data/lib/woods/dependency_graph.rb +34 -10
  48. data/lib/woods/embedding/fake.rb +12 -0
  49. data/lib/woods/embedding/indexer.rb +195 -98
  50. data/lib/woods/embedding/input_budget.rb +67 -0
  51. data/lib/woods/embedding/openai.rb +70 -20
  52. data/lib/woods/embedding/provider.rb +37 -25
  53. data/lib/woods/embedding/text_preparer.rb +76 -32
  54. data/lib/woods/embedding/token_counter.rb +18 -81
  55. data/lib/woods/embedding/vector_configuration.rb +48 -0
  56. data/lib/woods/extraction_identities.rb +175 -0
  57. data/lib/woods/extractor.rb +304 -107
  58. data/lib/woods/extractors/action_cable_extractor.rb +8 -3
  59. data/lib/woods/extractors/assigned_value_discovery.rb +74 -0
  60. data/lib/woods/extractors/class_declarations.rb +121 -0
  61. data/lib/woods/extractors/configuration_extractor.rb +11 -3
  62. data/lib/woods/extractors/declaration_ancestry.rb +92 -0
  63. data/lib/woods/extractors/event_extractor.rb +8 -0
  64. data/lib/woods/extractors/graphql_extractor.rb +134 -77
  65. data/lib/woods/extractors/job_extractor.rb +5 -1
  66. data/lib/woods/extractors/lib_extractor.rb +132 -15
  67. data/lib/woods/extractors/mailer_extractor.rb +3 -5
  68. data/lib/woods/extractors/manager_extractor.rb +7 -21
  69. data/lib/woods/extractors/migration_declaration.rb +87 -0
  70. data/lib/woods/extractors/migration_extractor.rb +5 -39
  71. data/lib/woods/extractors/phlex_extractor.rb +6 -2
  72. data/lib/woods/extractors/policy_extractor.rb +9 -5
  73. data/lib/woods/extractors/poro_extractor.rb +112 -53
  74. data/lib/woods/extractors/pundit_extractor.rb +11 -6
  75. data/lib/woods/extractors/scheduled_job_extractor.rb +45 -4
  76. data/lib/woods/extractors/serializer_extractor.rb +34 -22
  77. data/lib/woods/extractors/shared_utility_methods.rb +18 -1
  78. data/lib/woods/extractors/source_nesting.rb +142 -106
  79. data/lib/woods/extractors/standalone_module_discovery.rb +123 -0
  80. data/lib/woods/extractors/state_machine_extractor.rb +46 -40
  81. data/lib/woods/extractors/view_component_extractor.rb +9 -7
  82. data/lib/woods/flow_assembler.rb +4 -1
  83. data/lib/woods/generation.rb +25 -0
  84. data/lib/woods/hooks/context_hint.rb +7 -2
  85. data/lib/woods/mcp/bootstrapper.rb +33 -7
  86. data/lib/woods/mcp/config_resolver.rb +26 -7
  87. data/lib/woods/mcp/index_reader.rb +125 -24
  88. data/lib/woods/mcp/index_reader_pinning.rb +16 -0
  89. data/lib/woods/mcp/renderers/markdown_renderer.rb +7 -1
  90. data/lib/woods/mcp/renderers/plain_renderer.rb +3 -1
  91. data/lib/woods/mcp/search_results.rb +7 -1
  92. data/lib/woods/mcp/server.rb +24 -4
  93. data/lib/woods/module_reconciliation.rb +151 -0
  94. data/lib/woods/path_dispatcher.rb +7 -2
  95. data/lib/woods/rake_helpers.rb +43 -11
  96. data/lib/woods/release.rb +1 -1
  97. data/lib/woods/resilience/index_validator.rb +8 -3
  98. data/lib/woods/resilience/retryable_provider.rb +18 -1
  99. data/lib/woods/resolved_config.rb +68 -8
  100. data/lib/woods/retrieval/context_assembler.rb +3 -3
  101. data/lib/woods/retrieval/lexical_assembler.rb +3 -2
  102. data/lib/woods/retrieval/scope.rb +18 -2
  103. data/lib/woods/retrieval/source_evidence.rb +14 -2
  104. data/lib/woods/source_contributor_validation.rb +78 -0
  105. data/lib/woods/source_contributors.rb +116 -0
  106. data/lib/woods/source_inputs/handoff.rb +37 -0
  107. data/lib/woods/source_inputs/launcher.rb +53 -13
  108. data/lib/woods/source_inputs/manifest.rb +84 -3
  109. data/lib/woods/source_inputs/private_key.rb +44 -12
  110. data/lib/woods/source_inputs/scanner.rb +98 -27
  111. data/lib/woods/source_inputs/scopes.rb +1 -1
  112. data/lib/woods/source_inputs/session.rb +147 -15
  113. data/lib/woods/source_inputs/stable_reader.rb +127 -0
  114. data/lib/woods/source_inputs/status.rb +40 -8
  115. data/lib/woods/source_inputs/verifier.rb +28 -5
  116. data/lib/woods/source_path_encoding.rb +33 -0
  117. data/lib/woods/source_references/cache.rb +284 -0
  118. data/lib/woods/source_references/collector.rb +120 -0
  119. data/lib/woods/source_references/extraction.rb +185 -0
  120. data/lib/woods/source_references/inputs.rb +134 -0
  121. data/lib/woods/source_references/parser_adapter.rb +134 -0
  122. data/lib/woods/source_references/pass.rb +152 -0
  123. data/lib/woods/source_references/prism_adapter.rb +116 -0
  124. data/lib/woods/source_references/registry.rb +178 -0
  125. data/lib/woods/source_references/runtime_lookup.rb +127 -0
  126. data/lib/woods/source_references/value_class.rb +82 -0
  127. data/lib/woods/storage/metadata_store.rb +4 -1
  128. data/lib/woods/storage/qdrant.rb +2 -2
  129. data/lib/woods/unblocked/client.rb +12 -7
  130. data/lib/woods/unblocked/document_builder.rb +4 -1
  131. data/lib/woods/unblocked/exporter.rb +127 -37
  132. data/lib/woods/unblocked/sync_manifest.rb +137 -21
  133. data/lib/woods/unblocked/uri_migration.rb +105 -0
  134. data/lib/woods/util/host_guard.rb +3 -2
  135. data/lib/woods/version.rb +1 -1
  136. data/lib/woods/watch/catch_up.rb +138 -0
  137. data/lib/woods/watch/claim_lease.rb +150 -0
  138. data/lib/woods/watch/cli.rb +26 -2
  139. data/lib/woods/watch/daemon.rb +80 -59
  140. data/lib/woods/watch/installation/options.rb +1 -1
  141. data/lib/woods/watch/installation/receipt.rb +6 -1
  142. data/lib/woods/watch/managed_child.rb +1 -1
  143. data/lib/woods/watch/supervisor.rb +1 -1
  144. data/lib/woods/watch/tree_scan.rb +14 -2
  145. data/plugin/.claude-plugin/plugin.json +1 -1
  146. data/plugin/hooks/adapters/normalize.rb +3 -2
  147. data/plugin/hooks/woods-input-rules.sh +4 -0
  148. data/plugin/hooks/woods-refresh.sh +15 -7
  149. data/plugin/hooks/woods-session-start.sh +60 -3
  150. data/plugin/skills/woods-diagnose/SKILL.md +334 -11
  151. data/plugin/skills/woods-investigate/SKILL.md +11 -0
  152. data/plugin/skills/woods-mcp-config/SKILL.md +79 -8
  153. data/plugin/skills/woods-setup/SKILL.md +53 -6
  154. metadata +32 -5
@@ -115,7 +115,7 @@ Columns:
115
115
  | `include_framework_sources` | Boolean | `true` | user-settable | Extract Rails and gem source code |
116
116
  | `concurrent_extraction` | Boolean | `false` | user-settable | Enable parallel extraction (experimental) |
117
117
  | `vector_store` / `metadata_store` / `graph_store` / `embedding_provider` | Symbol | n/a | preset-derived | Adapter types. Set by presets; override individually to mix stacks. |
118
- | chars-per-token ratio (used by ContextAssembler, TextPreparer, Builder, cost_model) | Float | `4.0` (OpenAI) / `1.5` (Ollama) | computed | Derived from the active embedding provider via `Woods::TokenUtils.chars_per_token_for(...)`. Not directly user-settable; change `embedding_provider` to change the ratio. |
118
+ | chars-per-token ratio (used by ContextAssembler, TextPreparer, Builder, cost_model) | Float | `4.0` (OpenAI) / `1.5` (Ollama) | computed | A sizing estimate derived from the active provider via `Woods::TokenUtils.chars_per_token_for(...)`, not a proof of token count. Complete embedding admission uses the provider-specific bound described below. |
119
119
 
120
120
  ## Embedding options
121
121
 
@@ -136,6 +136,26 @@ config.embedding_options = {
136
136
  }
137
137
  ```
138
138
 
139
+ `dimensions:` explicitly requests an output width; use it to reduce a
140
+ `text-embedding-3-small` or `text-embedding-3-large` vector. The legacy singular
141
+ `dimension:` remains an alias for this explicit request; if both are supplied,
142
+ they must agree. `expected_dimensions:` records an expected output width
143
+ without requesting a reduction. Standalone snapshot restoration uses that
144
+ separate channel. Provider responses must match the expected width before
145
+ Woods accepts them.
146
+
147
+ `text-embedding-ada-002` has a fixed width of 1,536. A matching declared width
148
+ is accepted without sending the API's unsupported `dimensions` parameter;
149
+ other widths are refused before a request.
150
+
151
+ Embedding snapshots store observed `dimension` separately from
152
+ `requested_dimensions`. Standalone readers restore both. For legacy snapshots
153
+ without the latter field, Woods restores a reduction only when the model is
154
+ exactly `text-embedding-3-small` or `text-embedding-3-large` and the recorded
155
+ width is below that model's native width. It does not infer support for
156
+ reductions from arbitrary model names. Store and snapshot width checks still
157
+ apply; changing models or widths requires rebuilding a compatible vector store.
158
+
139
159
  OpenAI embedding batches are sent in slices of at most 36 texts, preserving
140
160
  input order. For inputs within the API's 8,192-token per-text limit, this stays
141
161
  below both the 2,048-input limit and the 300,000-token total request limit.
@@ -160,14 +180,20 @@ config.embedding_options = {
160
180
 
161
181
  The provider reads `model:`, `host:`, and `num_ctx:` from `embedding_options`. `num_ctx` is auto-selected from a per-model registry (`nomic-embed-text` → 2048, `bge-m3` → 8192, `mxbai-embed-large` → 512, `snowflake-arctic-embed` → 512, `snowflake-arctic-embed2` → 8192, `all-minilm` → 512). Unknown models fall back to 2048, matching Ollama's conservative embedding default. Set `num_ctx:` explicitly only when running a model with a known-larger native context that isn't in the registry yet.
162
182
 
163
- **Why `num_ctx` is capped at the native context.** Ollama has an open regression ([ollama/ollama#14186](https://github.com/ollama/ollama/issues/14186)) where `options.num_ctx` does not lift the effective ceiling on `/api/embed` for models whose native context is smaller than the override. Woods advertises the native ceiling so the chunker sizes inputs to what Ollama will actually accept.
183
+ An explicit `dimensions:` option (or its legacy `dimension:` alias) is sent to
184
+ Ollama and requires server/model support. A stored width is restored through
185
+ `expected_dimensions:` instead.
186
+ Discovering a width through a probe never adds `dimensions` to later requests.
164
187
 
165
- **Optional exact tokenization.** Install the [`tokenizers`](https://github.com/ankane/tokenizers-ruby) gem alongside Woods to get BERT WordPiece token counting. Without it, Woods falls back to a chars/token ratio, which under-counts dense Ruby source (CamelCase constants, callback DSLs) and can silently over-pack chunks. Recommended for any Ollama setup.
188
+ **Why `num_ctx` is capped at the native context.** Ollama has an open regression ([ollama/ollama#14186](https://github.com/ollama/ollama/issues/14186)) where `options.num_ctx` does not lift the effective ceiling on `/api/embed` for models whose native context is smaller than the override. Woods advertises the native ceiling so the chunker sizes inputs to what Ollama will actually accept.
166
189
 
167
- ```ruby
168
- # Gemfile (optional)
169
- gem 'tokenizers', '~> 0.5'
170
- ```
190
+ **Input counting (included in Woods 2.1).** Complete inputs include metadata
191
+ prefixes. Known OpenAI embedding models use a conservative byte upper bound;
192
+ Ollama/custom model counts remain estimates. Woods requests `truncate: false`
193
+ from Ollama and refuses/splits oversized inputs without dropping source.
194
+ Installing `tokenizers` no longer implicitly downloads BERT or establishes an
195
+ exact count for an unrelated model. A caller-supplied local tokenizer must match
196
+ the selected model. See the model guide for limitations.
171
197
 
172
198
  See [EMBEDDING_MODELS.md](EMBEDDING_MODELS.md) for the full model comparison and the procedure for adding a new model to the registry.
173
199
 
@@ -188,6 +214,39 @@ Anything responding to `#embed` and `#embed_batch` can be assigned directly, it
188
214
  config.embedding_provider = MyCompany::CustomEmbedder.new(endpoint: internal_url)
189
215
  ```
190
216
 
217
+ Included in Woods 2.1: snapshots capture the effective settings of injected
218
+ Ollama, OpenAI, and Fake instances, including through Woods' retry and embedding
219
+ cache wrappers. An injected instance takes precedence over unused
220
+ `embedding_options`. Ollama retains its model, host, context size, read timeout,
221
+ and vector settings; OpenAI and Fake retain their model and vector settings.
222
+ Capturing constructor settings makes no requests. When a live `provider:` is
223
+ passed to `ResolvedConfig.from_configuration`, its existing dimension-discovery
224
+ behavior remains available. The explicit request width remains separate from
225
+ the observed width.
226
+
227
+ API keys are never stored; standalone OpenAI restoration still requires
228
+ `OPENAI_API_KEY`. Only plain HTTP(S) origins can be persisted as Ollama hosts
229
+ (an optional trailing `/` is allowed). URLs containing user information, a path
230
+ prefix, query, or fragment are omitted and marked `requires_host_provider`.
231
+ Those components may be legitimate routing settings, but can also carry
232
+ credentials; Woods refuses to reconstruct a different endpoint by dropping them.
233
+ The same restriction applies when reading older snapshot endpoints.
234
+
235
+ **Reader compatibility:** snapshots marked `requires_host_provider` need a
236
+ supporting reader. Older schema-1 readers cannot enforce that marker and may
237
+ silently select defaults; do not use them for implicit restoration of these
238
+ snapshots. Ordinary built-in snapshots with non-secret settings remain
239
+ schema-1 compatible.
240
+
241
+ Custom providers and custom wrappers are also marked `requires_host_provider`;
242
+ Woods does not serialize arbitrary client state or infer a built-in from a custom
243
+ class name. Embedding in the configured host continues to work. To serve such a
244
+ snapshot semantically, provide an explicitly configured compatible provider in
245
+ the reader's host initializer. Implicit standalone restoration reports a
246
+ configuration error instead of choosing defaults. Explicit lexical retrieval
247
+ does not restore or call an embedding provider. Record the loaded revision
248
+ before relying on these snapshot improvements.
249
+
191
250
  ## Storage options
192
251
 
193
252
  | Option | Type | Default | Description |
@@ -264,6 +323,15 @@ worktree when overriding it. Existing databases at the old configured output
264
323
  path are not moved or deleted; run `woods:embed` for the selected index after
265
324
  upgrading to populate its default metadata database.
266
325
 
326
+ **Included in Woods 2.1:** embedding with dump-backed vectors reconciles the
327
+ complete SQLite identity inventory, including records without vectors and
328
+ same-name units with distinct types. A full rebuild removes vanished records,
329
+ including when the new corpus is empty. Incremental runs retain the existing
330
+ empty-input and 30% purge guards. Both modes still refuse incomplete extraction
331
+ input before changing stores. Standalone MCP opens the index's SQLite metadata
332
+ before restoring type filters on hydrated vectors, using that same metadata
333
+ store for retrieval. No database migration or embedding format change is required.
334
+
267
335
  Requires the `sqlite3` gem in your host bundle. Rails apps backed by
268
336
  MySQL or PostgreSQL won't have it by default, selecting `:sqlite`
269
337
  without it raises `Woods::ConfigurationError` with install
@@ -291,13 +359,6 @@ unchanged. This is a reasonable default for hosts that don't bundle `sqlite3`.
291
359
 
292
360
  ## Retrieval cache options
293
361
 
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
-
301
362
  The optional cache wraps both embedding-provider calls and assembled retrieval
302
363
  contexts. It is disabled by default and is separate from the Index Server's
303
364
  tool-result `_meta` cache hint.
@@ -333,9 +394,33 @@ overrides the wrapper defaults for `:embeddings` (24 hours) and `:context`
333
394
  (15 minutes). `:memory` accepts `max_entries` (default 500); it ignores
334
395
  `default_ttl` because each wrapper write supplies its domain TTL.
335
396
 
397
+ **Included in Woods 2.1:** retrieval contexts are scoped to one retriever
398
+ instance, including when Redis or Solid Cache is shared by applications or
399
+ worktrees. Restarting the retriever starts a fresh context namespace. Reload
400
+ retires only that instance's namespace, including results still in flight;
401
+ already running requests may finish against the previous corpus. Retired entries
402
+ expire according to their configured TTL or backend eviction; disabling both
403
+ can retain unused entries indefinitely. Embedding-vector caches are separate and survive context invalidation.
404
+
405
+ Embedding entries are scoped to the provider family, endpoint, model, requested
406
+ and declared widths, and relevant input options. Cache lookups never call a
407
+ provider's probing `dimensions` method. Keys contain hashes rather than raw
408
+ endpoint credentials or API keys. Malformed cached vectors and known-width
409
+ mismatches are refused. This configuration key format starts a fresh embedding
410
+ cache after upgrading; old entries expire under their existing TTLs.
411
+
412
+ Custom provider objects may expose a pure, JSON-compatible `cache_identity`
413
+ containing every setting that affects their vectors, plus a non-probing
414
+ `configured_dimensions` reader for width validation. Otherwise each cache
415
+ wrapper has its own namespace; cached vectors are still checked for valid
416
+ numeric content and consistent batch widths. Configuration changes require a
417
+ new provider and wrapper. A remotely replaced model under an unchanged name
418
+ still requires an explicit cache/model-version change and re-embedding.
419
+
336
420
  `Woods::Cache.cache_key` length-prefixes every component, including a single
337
- component, so different argument counts cannot share a response. Existing
338
- multi-component keys used by Woods' wrappers remain unchanged. Custom callers
421
+ component, so different argument counts cannot share a response. This component
422
+ encoding is unchanged and independent of the retriever's context namespace;
423
+ the embedding wrapper uses the new identity above. Custom callers
339
424
  using single-component keys must clear their affected persistent cache domain
340
425
  when upgrading, since older unprefixed entries can alias the new encoding;
341
426
  subsequent calls refill it normally. Namespace clearing still covers both formats.
@@ -503,22 +588,35 @@ there is no full population count to publish. Both caps must be a positive Integ
503
588
 
504
589
  | Option | Type | Default | Description |
505
590
  |--------|------|---------|-------------|
506
- | `session_tracer_enabled` | Boolean | `false` | Enable session tracing middleware |
591
+ | `session_tracer_enabled` | Boolean | `false` | Enable session tracing middleware; set with its store in `config/application.rb` before Railtie initialization |
507
592
  | `session_tracer_allow_production` | Boolean | `false` | Explicitly allow session tracing in `Rails.env.production?`. Without this opt-in, the Railtie warns and leaves the tracer disabled even when `session_tracer_enabled` is true. Review trace contents, retention, and access controls before enabling it. |
508
593
  | `session_store` | Object | `nil` | Store backend: `FileStore`, `RedisStore`, or `SolidCacheStore` |
509
594
  | `session_id_proc` | Proc | `nil` | Custom proc to extract session ID from requests |
510
595
  | `session_exclude_paths` | Array<String> | `[]` | Path patterns to exclude from tracing |
511
596
 
597
+ Configure the complete tracer setup in `config/application.rb`, after Woods is
598
+ required and before Rails initialization. A `config/initializers/woods.rb`
599
+ assignment is too late to mount this middleware; Woods warns and tracing stays
600
+ disabled. Restart the server after changing it.
601
+
512
602
  ```ruby
513
- require 'woods/session_tracer/file_store' # the stores are not autoloaded
603
+ # config/application.rb, after Bundler.require(*Rails.groups)
604
+ require 'woods/session_tracer/file_store'
514
605
 
515
- config.session_tracer_enabled = true
516
- config.session_store = Woods::SessionTracer::FileStore.new(
517
- base_dir: Rails.root.join('tmp/session_traces')
518
- )
519
- config.session_exclude_paths = ['/health', '/metrics', '/assets']
606
+ Woods.configure do |config|
607
+ config.session_tracer_enabled = true
608
+ config.session_store = Woods::SessionTracer::FileStore.new(
609
+ base_dir: File.expand_path('../tmp/session_traces', __dir__)
610
+ )
611
+ config.session_exclude_paths = ['/health', '/metrics', '/assets']
612
+ end
520
613
  ```
521
614
 
615
+ The Rails middleware records traces. Reading them with `session_trace` additionally
616
+ requires a custom Index Server process configured with the same store; the
617
+ packaged Index executable does not load application initializers. See
618
+ [conditional capabilities](MCP_SERVERS.md#conditional-index-capabilities).
619
+
522
620
  ### File session retention
523
621
 
524
622
  `FileStore` accepts `ttl:` in seconds (default `nil`, expiration disabled),
@@ -689,8 +787,8 @@ deployment guide including defense layers.
689
787
  | `console_mcp_enabled` | Boolean | `false` | Master switch. When `false`, stdio exits and the mounted Console middleware passes requests through to Rails. |
690
788
  | `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. |
691
789
  | `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)`. |
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. |
693
- | `console_mcp_path` | String | `/mcp/console` | URL path the Rack middleware responds on. |
790
+ | `console_mcp_allowed_origins` | Array\<String\> | `%w[http://localhost http://127.0.0.1 http://[::1]]` | Shared guard/SDK allowlist (included in Woods 2.1). Exact cross-origin entries include the port; portless entries also permit same-authority traffic. Include the actual browser origin and non-loopback endpoint authority. Restart after changes. |
791
+ | `console_mcp_path` | String | `/mcp/console` | URL path captured when Rack middleware mounts. Set a custom path in `config/application.rb` before Railtie initialization, then restart. |
694
792
  | `console_embedded_read_tools` | Boolean | `false` | Register `console_sql` and `console_query` in supported stdio and Rack modes. |
695
793
  | `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. |
696
794
  | `console_redacted_columns` | Array\<String\> | `Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS` | Column names whose values are replaced with `[REDACTED]` in responses, and which are refused as aggregate, scope, find, and order inputs. |
@@ -792,6 +890,11 @@ in its finalized development environment. See [startup and installation](WATCH_D
792
890
  for the generator's explicit modes, portable receipt, update/removal, and the
793
891
  separate supervision status. Raw task settings above remain compatible.
794
892
 
893
+ **Included in Woods 2.1 (#591):** blank/whitespace-only idle timeouts count as
894
+ unset consistently. `woods-watch --recover-claim INDEX --claim-token TOKEN`
895
+ provides explicit recovery only for a matching abandoned managed lifetime lease;
896
+ see [ownership recovery](WATCH_DAEMON.md#recovering-an-abandoned-managed-claim).
897
+
795
898
  ### Opt-in plugin refresh hooks
796
899
 
797
900
  These settings control the plugin shell worker. Check installed
@@ -820,6 +923,11 @@ tasks for manual refreshes; hook transport is not a general shell execution API.
820
923
 
821
924
  ### Extraction rake tasks
822
925
 
926
+ The separate `woods-extract` launcher captures before Rails configuration runs:
927
+ custom `output_dir` applications must supply matching `--output` or `WOODS_OUTPUT`.
928
+ Its implicit default is `tmp/woods`; the Woods 2.1 #591 guard refuses a finalized
929
+ configuration mismatch before publication. See [launcher output selection](SOURCE_FRESHNESS.md#establish-a-fresh-baseline).
930
+
823
931
  | Variable | Default | Purpose |
824
932
  |----------|---------|---------|
825
933
  | `WOODS_OUTPUT` | `Woods.configuration.output_dir` | Overrides the output directory for extraction/watch and embedding tasks without editing the initializer; embedding also places its default SQLite metadata database there. Explicit database options take precedence. |
@@ -212,7 +212,15 @@ and proxy rules aligned with it. Server-to-server clients normally omit
212
212
  `Origin`, but a present request `Host` must still be allowed. Supporting security
213
213
  revisions deliberately allow configured non-loopback Hosts through the SDK too;
214
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.
215
+ widening is limited to the configured authorities and retains bearer auth. The
216
+ same immutable policy makes preflight acceptance agree with SDK dispatch. List exact browser ports for
217
+ cross-port traffic, including loopback; a portless origin additionally permits
218
+ same-authority traffic, not arbitrary cross-port access. Console defaults remain
219
+ HTTP loopback, while the Index Server defaults include HTTP and HTTPS loopback.
220
+ Default HTTP(S) ports are equivalent to their omitted form. An explicit list
221
+ replaces default browser origins, including loopback origins; add loopback entries
222
+ if those clients are needed. Invalid entries fail at boot naming the entry.
223
+ Restart after changing the list. See [HTTP origin matching](MCP_HTTP_TRANSPORT.md#browser-origins-dns-rebinding-defense).
216
224
 
217
225
  Do not mount `Woods::Console::RackMiddleware` by itself. The Railtie composes
218
226
  `OriginGuard`, `BearerAuth`, and the Console middleware in the supported order.
@@ -332,6 +340,21 @@ user: deploy
332
340
  command: cd /app && bundle exec rake woods:console
333
341
  ```
334
342
 
343
+ **Included in Woods 2.1:** direct mode prefers an existing executable `bin/rake`
344
+ in the application's selected directory, falling back to `bundle exec rake`.
345
+ Relative `directory` values resolve from the launcher's initial working directory.
346
+ Explicit `command` settings win. Docker and SSH modes retain their remote default;
347
+ set `command` explicitly when that application uses another task entry point.
348
+
349
+ **Included in Woods 2.1:** the launcher rejects unknown keys, nested
350
+ `connection:`/`console:` sections, blank or non-string values, and options that
351
+ do not apply to the selected mode. Keep `mode` and `command` at the top level;
352
+ use `directory` for direct mode, `container` for Docker, and `host`/`user` for SSH.
353
+ Configure access controls and redaction in the Rails initializer, not this
354
+ process-launch file. Move legacy nested launch settings to the matching example
355
+ above. An empty file or mapping still selects the default direct command;
356
+ supported `command`/`directory` overrides can omit `mode` for direct execution.
357
+
335
358
  Override config path with environment variable:
336
359
 
337
360
  ```bash
@@ -379,6 +402,13 @@ servers register only executable tools: the 9 Tier 1 tools by default, plus
379
402
  | `console_association_count` | Count associated records for a specific record |
380
403
  | `console_recent` | Recently created/updated records (max 50) |
381
404
 
405
+ **Included in Woods 2.1:** `console_association_count` returns `0` or `1` for
406
+ `belongs_to` and `has_one`, including missing targets. It counts the association's
407
+ relation, applying `scope` to the target model; collection associations retain
408
+ their ordinary count. Target-table and scope-column checks still apply before
409
+ association reads. The tool does not call a custom association getter to count
410
+ an already loaded model object.
411
+
382
412
  ### Tier 2: Domain-aware (9 tools): inventory only, not executable
383
413
 
384
414
  | Tool | Description |
@@ -423,9 +453,21 @@ servers register only executable tools: the 9 Tier 1 tools by default, plus
423
453
 
424
454
  ## Configuration options
425
455
 
426
- Set these in your Rails initializer:
456
+ Set the request-time flags and protection settings below in your Rails initializer.
457
+ A **custom endpoint path** must be set earlier, after Woods is required in
458
+ `config/application.rb`, because the Railtie captures it while mounting middleware:
459
+
460
+ ```ruby
461
+ # config/application.rb, after Bundler.require(*Rails.groups)
462
+ Woods.configure { |config| config.console_mcp_path = '/internal/woods-console' }
463
+ ```
464
+
465
+ Restart after changing the path. Leaving the default `/mcp/console` requires no
466
+ path assignment. Session tracing also needs early configuration; follow
467
+ [session tracer options](CONFIGURATION_REFERENCE.md#session-tracer-options).
427
468
 
428
469
  ```ruby
470
+ # config/initializers/woods.rb
429
471
  Woods.configure do |config|
430
472
  # Master on/off switch for the Console MCP feature (Layer 0). Default: false.
431
473
  # Applies to every transport: stdio, launcher wrapper, and Rack.
@@ -435,9 +477,6 @@ Woods.configure do |config|
435
477
  # after configuring the layers below that match your threat model.
436
478
  config.console_mcp_enabled = true
437
479
 
438
- # URL path for the Rack middleware endpoint. Default: '/mcp/console'.
439
- config.console_mcp_path = '/mcp/console'
440
-
441
480
  # HTTP Origin + Host allow-list. Defaults to loopback only. Non-loopback
442
481
  # Rack deployments must include their public MCP host; browser clients from
443
482
  # another origin need that exact origin listed too.
@@ -599,6 +638,10 @@ Direct, unaliased selection of a redacted column stays allowed: the output heade
599
638
 
600
639
  `console_sql` applies a stricter form of the same rule because arbitrary SQL can rename output headers. A protected identifier is accepted only as a direct, unaliased outer `SELECT` column; aliases, aggregates, predicates, CTE shapes, ordering/grouping uses, and an EAV value without its paired key are refused before adapter execution. Use `console_query` when a protected column must participate in a more complex structured query.
601
640
 
641
+ The security correction refuses PostgreSQL Unicode-escaped identifiers in
642
+ `console_sql` before execution. Use ordinary identifiers or standard quoted
643
+ identifiers instead. This restriction does not change the structured query tools.
644
+
602
645
  `console_query`'s `having` is covered by the same oracle rule: an aggregate over a protected column (`MAX(amount) > ?`), a bare predicate on a redacted column (`salary > ?`), or a predicate on an EAV value column is refused, since repeated guesses reveal the protected value from whether a row is returned. EAV key predicates stay allowed so callers can select the rows whose paired values need redaction.
603
646
 
604
647
  **Ships with a curated credential default list** (`Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS`, 31 columns) covering Devise, Doorkeeper, Rodauth, has_secure_password, devise-two-factor, and common hand-rolled auth shapes: `password`, `password_digest`, `password_salt`, `encrypted_password`, `crypted_password`, `salt`, `otp_secret`, `encrypted_otp_secret`, `two_factor_secret`, `backup_codes`, `consumed_timestep`, `reset_password_token`, `confirmation_token`, `unlock_token`, `remember_token`, `invitation_token`, `access_token`, `refresh_token`, `auth_token`, `api_token`, `api_key`, `bearer_token`, `client_secret`, `webhook_secret`, `signing_secret`, `session_secret`, `private_key`, `encrypted_private_key`, `key_hash`, `token`, `secret`.
@@ -856,6 +899,23 @@ fail closed with `Woods::ConfigurationError`.
856
899
 
857
900
  The middleware lazy-initializes the MCP server on the first request, which includes `Rails.application.eager_load!`. This can take several seconds on large apps. Subsequent requests are fast. If you want to pre-warm, call a health check endpoint that touches the middleware path at app startup.
858
901
 
902
+ **Included in Woods 2.1:** if that eager load raises `NameError` (including a
903
+ Zeitwerk naming error), HTTP Console returns a stable `503` and records one
904
+ `console.eager_load.failed` diagnostic. That worker remains unavailable until
905
+ restart; it does not serve a partially populated model registry or repeatedly
906
+ retry loading on incoming requests. The response and diagnostic omit the raw
907
+ application exception. Reproduce it in the same application environment:
908
+
909
+ ```bash
910
+ bundle exec rails runner 'Rails.application.eager_load!'
911
+ ```
912
+
913
+ Fix the application error, then restart the Rails workers. Authentication and
914
+ requests outside the Console mount retain their normal behavior. Other exception
915
+ types retain Rails' usual failure handling. The existing stdio entry point warns
916
+ on eager-load `NameError` and continues; its model list may be incomplete, so fix
917
+ the same application error there too.
918
+
859
919
  ### Timeout errors on large models
860
920
 
861
921
  The default statement timeout is 5000ms (5 seconds). If you are hitting timeouts on models with millions of rows, use `scope` to narrow the query:
@@ -916,34 +976,29 @@ contains them. Confirm that a patched release is available before selecting it.
916
976
  On affected versions, disable Console where these policies are required; Index MCP
917
977
  can stay enabled because it reads the published code index separately.
918
978
 
979
+ ## Console corrections included in 2.1
919
980
 
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.
981
+ Woods 2.1 includes the reviewed Console corrections also released in the
982
+ 2.0.1 and 1.6.4 security patches. For Git/path installations, verify the loaded
983
+ revision and gem path. Plugin updates alone do not update the Console server.
984
+ See the published [security advisory](https://github.com/lost-in-the/woods/security/advisories/GHSA-wxxx-6hqc-qm8g).
933
985
 
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.
986
+ The patch tightens SQL policy for adapter-specific comment and quoting forms,
987
+ whole-row and multi-source redaction, typed key-value records, and per-mount HTTP
988
+ guards. Supported structured reads remain available. Raw `console_sql` requires
989
+ a recognized PostgreSQL, MySQL-family, or SQLite adapter; compatible subclasses
990
+ are recognized by ancestry. Unsupported raw SQL receives a clear refusal without
991
+ disabling structured tools.
938
992
 
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.
993
+ Malformed HTTP origin configuration now refuses at boot with the offending entry.
994
+ Fix the allowlist and restart; do not relax authentication or redaction to make a
995
+ refused request succeed. For stdio-only use, keep HTTP disabled.
942
996
 
943
997
  ### Read policy compatibility
944
998
 
945
- These rules describe the security-patch source; verify the installed revision
946
- and loaded gem path until its release is published.
999
+ These rules describe Woods 2.1 and the 2.0.1 security patch. Differences from
1000
+ the 1.6.4 backport are called out below. Verify the installed version and, for
1001
+ Git/path installations, the revision and loaded gem path.
947
1002
 
948
1003
  - **Column alias lists:** when either `console_redacted_columns` or
949
1004
  `console_redacted_key_values` is nonempty, `console_sql` refuses relation
@@ -976,6 +1031,3 @@ and loaded gem path until its release is published.
976
1031
  expression per `select` array entry. 2.x refuses comma-combined entries that
977
1032
  1.6.4 splits before validation. Execution and typed redaction use the same
978
1033
  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.
@@ -37,8 +37,8 @@ Woods.configure do |config|
37
37
  model: 'bge-m3',
38
38
  host: 'http://localhost:11434'
39
39
  }
40
- # Everything else, chunker sizing, token counting, num_ctx, is picked
41
- # up automatically from the model name.
40
+ # Model context and initial chunk sizing follow the registry.
41
+ # Local counts are estimates; the server enforces its actual limit.
42
42
  end
43
43
  ```
44
44
 
@@ -64,21 +64,20 @@ override. For `nomic-embed-text` (native 2048) the server rejects inputs above
64
64
  that with a `400 "the input length exceeds the context length"` regardless of
65
65
  `num_ctx`.
66
66
 
67
- Woods works around this two ways:
68
-
69
- 1. **Advertise the native ceiling, not the override.** `Provider::Ollama` keeps
70
- a model → native-context registry (`MODEL_CONTEXT_LENGTHS`) so the chunker
71
- sizes inputs to what Ollama will actually accept. `num_ctx` is still passed
72
- through the request body in case the regression is fixed upstream, but
73
- nothing relies on it.
74
- 2. **Verify client-side with the real tokenizer.** When the
75
- [`tokenizers`](https://github.com/ankane/tokenizers-ruby) gem is installed,
76
- `Embedding::TokenCounter` loads the `bert-base-uncased` WordPiece tokenizer
77
- (the one every BERT-family embedding model is built on) and the chunker
78
- re-verifies every slice. WordPiece fragments CamelCase and `::` separators
79
- differently than character-based estimation suggests, this verification is
80
- what catches the 10–20 % gap between our estimate and Ollama's internal
81
- count.
67
+ **Included in Woods 2.1:** Woods checks complete inputs, including their
68
+ metadata prefixes, and splits source without discarding characters. OpenAI's
69
+ known byte-BPE embedding models use a conservative UTF-8 byte upper bound at
70
+ 8191 tokens; this may create more chunks than an exact tokenizer would.
71
+ Ollama and custom models use explicitly labelled estimates. Ollama requests
72
+ set `truncate: false`, so an actual overflow fails instead of losing source.
73
+ A prefix that cannot fit or one unsplittable source character refuses before
74
+ vector/checkpoint writes; inspect the reported unit, model and limit.
75
+
76
+ Installing `tokenizers` alone no longer downloads or selects BERT for every
77
+ model. Advanced callers can inject a local model-matched tokenizer into
78
+ `Embedding::TokenCounter`; the caller must establish that it matches the server.
79
+ No automatic tokenizer download occurs. These checks do not prove that estimated
80
+ inputs fit an arbitrary tokenizer: the provider remains authoritative.
82
81
 
83
82
  ## Adding a new model to the registry
84
83
 
@@ -112,8 +111,6 @@ If you want Woods to auto-pick `num_ctx` for a model we don't ship support for:
112
111
 
113
112
  - Indexing a real-world Rails app where concern-inlined models or long service
114
113
  objects routinely exceed 2048 tokens.
115
- - You already pay the `tokenizers` gem install cost and want the tighter
116
- coupling between client-side verification and server-side enforcement.
117
114
  - Multilingual content (Arctic Embed 2 is BGE M3 with a stronger non-English
118
115
  story).
119
116