woods 2.0.0.beta2 → 2.0.0.beta3

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 (218) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +262 -1
  3. data/CONTRIBUTING.md +173 -9
  4. data/README.md +7 -3
  5. data/SECURITY.md +9 -6
  6. data/docs/AGENT_GUIDE.md +83 -4
  7. data/docs/AGENT_SETUP.md +82 -1
  8. data/docs/BACKEND_MATRIX.md +20 -0
  9. data/docs/CLIENT_HOOKS.md +111 -0
  10. data/docs/CONFIGURATION_REFERENCE.md +199 -14
  11. data/docs/CONSOLE_MCP_SETUP.md +35 -5
  12. data/docs/DOCKER_SETUP.md +21 -2
  13. data/docs/EVALUATION.md +464 -1
  14. data/docs/EXTRACTOR_REFERENCE.md +36 -5
  15. data/docs/FAQ.md +11 -12
  16. data/docs/GETTING_STARTED.md +17 -5
  17. data/docs/INCREMENTAL_EXTRACTION.md +117 -1
  18. data/docs/INDEX_LAYOUT.md +382 -0
  19. data/docs/INTERNALS.md +7 -2
  20. data/docs/MCP_SERVERS.md +221 -5
  21. data/docs/MCP_TOOL_COOKBOOK.md +33 -18
  22. data/docs/NOTION_INTEGRATION.md +13 -0
  23. data/docs/OBSIDIAN_INTEGRATION.md +57 -9
  24. data/docs/PUBLISHED_INDEX.md +55 -0
  25. data/docs/README.md +7 -0
  26. data/docs/RETRIEVAL_GUIDE.md +253 -11
  27. data/docs/RUNTIME_TRACING.md +71 -0
  28. data/docs/SOURCE_FRESHNESS.md +143 -0
  29. data/docs/TROUBLESHOOTING.md +117 -5
  30. data/docs/UNBLOCKED_INTEGRATION.md +25 -0
  31. data/docs/UPGRADING_TO_2.md +44 -22
  32. data/docs/WATCH_DAEMON.md +259 -59
  33. data/exe/woods-agent-config +6 -0
  34. data/exe/woods-extract +5 -0
  35. data/exe/woods-hook-context +6 -0
  36. data/lib/generators/woods/templates/woods.rb.tt +1 -3
  37. data/lib/tasks/woods.rake +47 -397
  38. data/lib/woods/agent_configuration/applier.rb +133 -0
  39. data/lib/woods/agent_configuration/cli.rb +101 -0
  40. data/lib/woods/agent_configuration/cli_options.rb +29 -0
  41. data/lib/woods/agent_configuration/document.rb +105 -0
  42. data/lib/woods/agent_configuration/error.rb +7 -0
  43. data/lib/woods/agent_configuration/launcher.rb +75 -0
  44. data/lib/woods/agent_configuration/layout.rb +59 -0
  45. data/lib/woods/agent_configuration/managed_section.rb +62 -0
  46. data/lib/woods/agent_configuration/plan.rb +98 -0
  47. data/lib/woods/agent_configuration/plan_diff.rb +38 -0
  48. data/lib/woods/agent_configuration/planned_files.rb +61 -0
  49. data/lib/woods/agent_configuration/planner.rb +63 -0
  50. data/lib/woods/agent_configuration/planner_validation.rb +77 -0
  51. data/lib/woods/agent_configuration/preflight.rb +100 -0
  52. data/lib/woods/agent_configuration/recovery.rb +49 -0
  53. data/lib/woods/ast/node.rb +2 -0
  54. data/lib/woods/ast/parser.rb +38 -5
  55. data/lib/woods/builder.rb +21 -5
  56. data/lib/woods/cache/cache_middleware.rb +28 -7
  57. data/lib/woods/cache/cache_store.rb +4 -5
  58. data/lib/woods/change_set.rb +5 -4
  59. data/lib/woods/console/credential_index.rb +20 -2
  60. data/lib/woods/console/credential_scanner.rb +14 -14
  61. data/lib/woods/console/credential_scanner_registry.rb +36 -0
  62. data/lib/woods/console/embedded_executor.rb +1 -1
  63. data/lib/woods/console/encrypted_credential_snapshot.rb +16 -0
  64. data/lib/woods/console/rack_middleware.rb +22 -13
  65. data/lib/woods/console/server.rb +18 -16
  66. data/lib/woods/dependency_graph.rb +65 -13
  67. data/lib/woods/embedding/corpus.rb +94 -0
  68. data/lib/woods/embedding/indexer.rb +90 -46
  69. data/lib/woods/embedding/openai.rb +17 -6
  70. data/lib/woods/evaluation/ablation_executor.rb +6 -1
  71. data/lib/woods/evaluation/ablation_timed_executor.rb +22 -4
  72. data/lib/woods/export/typed_reader.rb +56 -0
  73. data/lib/woods/extractor.rb +232 -137
  74. data/lib/woods/extractors/action_cable_extractor.rb +3 -1
  75. data/lib/woods/extractors/behavioral_profile.rb +9 -7
  76. data/lib/woods/extractors/caching_extractor.rb +3 -1
  77. data/lib/woods/extractors/concern_extractor.rb +64 -6
  78. data/lib/woods/extractors/configuration_extractor.rb +7 -3
  79. data/lib/woods/extractors/controller_extractor.rb +13 -4
  80. data/lib/woods/extractors/database_view_extractor.rb +3 -1
  81. data/lib/woods/extractors/decorator_extractor.rb +3 -1
  82. data/lib/woods/extractors/engine_extractor.rb +3 -1
  83. data/lib/woods/extractors/event_extractor.rb +4 -2
  84. data/lib/woods/extractors/factory_extractor.rb +3 -1
  85. data/lib/woods/extractors/graphql_extractor.rb +8 -2
  86. data/lib/woods/extractors/i18n_extractor.rb +3 -1
  87. data/lib/woods/extractors/job_extractor.rb +6 -19
  88. data/lib/woods/extractors/lib_extractor.rb +3 -1
  89. data/lib/woods/extractors/mailer_extractor.rb +20 -5
  90. data/lib/woods/extractors/manager_extractor.rb +3 -1
  91. data/lib/woods/extractors/method_parameters.rb +53 -0
  92. data/lib/woods/extractors/middleware_argument.rb +65 -0
  93. data/lib/woods/extractors/middleware_extractor.rb +9 -3
  94. data/lib/woods/extractors/migration_extractor.rb +3 -1
  95. data/lib/woods/extractors/model_extractor.rb +39 -33
  96. data/lib/woods/extractors/package_extractor.rb +24 -4
  97. data/lib/woods/extractors/phlex_extractor.rb +3 -1
  98. data/lib/woods/extractors/policy_extractor.rb +3 -1
  99. data/lib/woods/extractors/poro_extractor.rb +3 -1
  100. data/lib/woods/extractors/pundit_extractor.rb +3 -1
  101. data/lib/woods/extractors/rails_source_extractor.rb +4 -2
  102. data/lib/woods/extractors/rake_task_extractor.rb +4 -2
  103. data/lib/woods/extractors/route_extractor.rb +3 -1
  104. data/lib/woods/extractors/route_helper_resolver.rb +10 -33
  105. data/lib/woods/extractors/scheduled_job_extractor.rb +41 -15
  106. data/lib/woods/extractors/serializer_extractor.rb +4 -2
  107. data/lib/woods/extractors/service_extractor.rb +3 -1
  108. data/lib/woods/extractors/shared_dependency_scanner.rb +2 -2
  109. data/lib/woods/extractors/shared_utility_methods.rb +27 -15
  110. data/lib/woods/extractors/source_nesting.rb +1 -1
  111. data/lib/woods/extractors/state_machine_extractor.rb +3 -1
  112. data/lib/woods/extractors/test_mapping_extractor.rb +3 -1
  113. data/lib/woods/extractors/validator_extractor.rb +3 -1
  114. data/lib/woods/extractors/view_component_extractor.rb +3 -1
  115. data/lib/woods/extractors/view_template_extractor.rb +3 -1
  116. data/lib/woods/gem_mapper.rb +2 -0
  117. data/lib/woods/git_history.rb +116 -0
  118. data/lib/woods/graph_analyzer.rb +35 -6
  119. data/lib/woods/hooks/context_cli.rb +54 -0
  120. data/lib/woods/hooks/context_event.rb +88 -0
  121. data/lib/woods/hooks/context_hint.rb +73 -0
  122. data/lib/woods/hooks/context_impact.rb +77 -0
  123. data/lib/woods/hooks/context_output.rb +47 -0
  124. data/lib/woods/hooks/context_state.rb +102 -0
  125. data/lib/woods/hooks/refresh.rb +79 -0
  126. data/lib/woods/hooks/rule_projection.rb +78 -0
  127. data/lib/woods/input_rules.rb +19 -0
  128. data/lib/woods/mcp/bearer_auth.rb +20 -12
  129. data/lib/woods/mcp/bootstrapper.rb +62 -0
  130. data/lib/woods/mcp/index_reader.rb +323 -160
  131. data/lib/woods/mcp/initialization_guidance.rb +27 -0
  132. data/lib/woods/mcp/origin_guard.rb +17 -9
  133. data/lib/woods/mcp/published_lexical_retriever.rb +115 -0
  134. data/lib/woods/mcp/renderers/markdown_renderer.rb +8 -1
  135. data/lib/woods/mcp/renderers/plain_renderer.rb +7 -1
  136. data/lib/woods/mcp/search_results.rb +74 -0
  137. data/lib/woods/mcp/server.rb +158 -37
  138. data/lib/woods/mcp/tool_contract.rb +2 -0
  139. data/lib/woods/mcp/tool_response_renderer.rb +25 -0
  140. data/lib/woods/mcp/traversal_evidence.rb +113 -0
  141. data/lib/woods/mcp/traversal_evidence_index.rb +100 -0
  142. data/lib/woods/mcp/traversal_evidence_page.rb +41 -0
  143. data/lib/woods/mcp/traversal_evidence_text.rb +52 -0
  144. data/lib/woods/notion/exporter.rb +56 -17
  145. data/lib/woods/obsidian/destination_plan.rb +98 -0
  146. data/lib/woods/obsidian/name_mapper.rb +19 -3
  147. data/lib/woods/obsidian/note_builder.rb +19 -10
  148. data/lib/woods/obsidian/vault_exporter.rb +88 -32
  149. data/lib/woods/operator/pipeline_guard.rb +18 -13
  150. data/lib/woods/path_dispatcher.rb +7 -1
  151. data/lib/woods/payload_store.rb +27 -26
  152. data/lib/woods/railtie.rb +3 -3
  153. data/lib/woods/railtie_support.rb +12 -12
  154. data/lib/woods/rake_helpers.rb +392 -0
  155. data/lib/woods/resilience/graph_invariant_validator/membership_checks.rb +71 -0
  156. data/lib/woods/resilience/graph_invariant_validator/node_checks.rb +61 -0
  157. data/lib/woods/resilience/graph_invariant_validator/reverse_relationship_checks.rb +46 -0
  158. data/lib/woods/resilience/graph_invariant_validator.rb +119 -0
  159. data/lib/woods/resilience/index_validator/graph_checks.rb +80 -0
  160. data/lib/woods/resilience/index_validator.rb +112 -23
  161. data/lib/woods/retrieval/context_assembler.rb +50 -15
  162. data/lib/woods/retrieval/lexical_assembler.rb +73 -0
  163. data/lib/woods/retrieval/lexical_index.rb +119 -0
  164. data/lib/woods/retrieval/ranker.rb +4 -2
  165. data/lib/woods/retrieval/scope.rb +108 -0
  166. data/lib/woods/retrieval/scoped_graph_store.rb +32 -0
  167. data/lib/woods/retrieval/scoped_vector_store.rb +55 -0
  168. data/lib/woods/retrieval/search_executor.rb +86 -27
  169. data/lib/woods/retrieval/source_evidence.rb +200 -0
  170. data/lib/woods/retriever.rb +98 -22
  171. data/lib/woods/ruby_analyzer/trace_enricher.rb +77 -38
  172. data/lib/woods/session_tracer/middleware.rb +10 -12
  173. data/lib/woods/session_tracer/redis_store.rb +22 -6
  174. data/lib/woods/session_tracer/session_flow_assembler.rb +23 -17
  175. data/lib/woods/session_tracer/solid_cache_coordination.rb +6 -4
  176. data/lib/woods/session_tracer/unit_resolver.rb +63 -0
  177. data/lib/woods/source_inputs/consumer_errors.rb +27 -0
  178. data/lib/woods/source_inputs/handoff.rb +102 -0
  179. data/lib/woods/source_inputs/launcher.rb +157 -0
  180. data/lib/woods/source_inputs/manifest.rb +124 -0
  181. data/lib/woods/source_inputs/private_key.rb +55 -0
  182. data/lib/woods/source_inputs/scanner.rb +171 -0
  183. data/lib/woods/source_inputs/scopes.rb +71 -0
  184. data/lib/woods/source_inputs/session.rb +214 -0
  185. data/lib/woods/source_inputs/status.rb +84 -0
  186. data/lib/woods/source_inputs/verifier.rb +107 -0
  187. data/lib/woods/storage/metadata_store.rb +25 -25
  188. data/lib/woods/storage/pgvector.rb +29 -8
  189. data/lib/woods/storage/qdrant.rb +17 -7
  190. data/lib/woods/storage/vector_store.rb +18 -6
  191. data/lib/woods/tasks.rb +3 -2
  192. data/lib/woods/temporal/json_snapshot_store.rb +29 -8
  193. data/lib/woods/unblocked/exporter.rb +59 -70
  194. data/lib/woods/version.rb +1 -1
  195. data/lib/woods/watch/boot_snapshot.rb +52 -0
  196. data/lib/woods/watch/daemon.rb +136 -28
  197. data/lib/woods/watch/listen_watcher.rb +4 -0
  198. data/lib/woods/watch/polling_watcher.rb +5 -1
  199. data/lib/woods/watch/status.rb +20 -15
  200. data/lib/woods/watch/tree_scan.rb +21 -13
  201. data/lib/woods/watch/watcher.rb +4 -1
  202. data/lib/woods.rb +50 -11
  203. data/plugin/.claude-plugin/plugin.json +1 -1
  204. data/plugin/hooks/adapters/normalize.jq +15 -0
  205. data/plugin/hooks/adapters/normalize.rb +63 -0
  206. data/plugin/hooks/hooks.json +20 -0
  207. data/plugin/hooks/woods-context.sh +50 -0
  208. data/plugin/hooks/woods-input-rules.sh +159 -0
  209. data/plugin/hooks/woods-opencode.mjs +65 -0
  210. data/plugin/hooks/woods-post-edit.sh +2 -225
  211. data/plugin/hooks/woods-refresh.sh +260 -0
  212. data/plugin/hooks/woods-session-start.sh +47 -55
  213. data/plugin/skills/woods-agent-enable/SKILL.md +13 -0
  214. data/plugin/skills/woods-diagnose/SKILL.md +288 -1
  215. data/plugin/skills/woods-investigate/SKILL.md +106 -0
  216. data/plugin/skills/woods-mcp-config/SKILL.md +89 -1
  217. data/plugin/skills/woods-setup/SKILL.md +107 -6
  218. metadata +84 -5
@@ -108,8 +108,9 @@ Columns:
108
108
  | `output_dir` | Pathname/String | `Rails.root.join('tmp/woods')` | user-settable | Directory where extracted data is written |
109
109
  | `extractors` | Array<Symbol> | `[:models, :controllers, :services, ...]` | accepted, not implemented | Does not select which extractors run. See [Extractors](#extractors) below. |
110
110
  | `pretty_json` | Boolean | `true` | user-settable | Format extracted JSON with indentation |
111
- | `max_context_tokens` | Integer | `8000` | user-settable | Maximum tokens for retrieval context windows |
112
- | `similarity_threshold` | Float | `0.7` | user-settable | Minimum similarity score (0.0-1.0) for retrieval results |
111
+ | `retrieval_mode` | Symbol | `:semantic` | user-settable | `:semantic` uses configured embeddings; explicit `:lexical` ranks published text without a provider/vector store. See [retrieval modes](RETRIEVAL_GUIDE.md#embedding-free-lexical-retrieval). |
112
+ | `max_context_tokens` | Integer | `8000` | user-settable | Default context-assembly token budget captured when a retriever is built; per-call budget overrides it |
113
+ | `similarity_threshold` | Float | `0.7` | deprecated, inert | Accepted for compatibility; setting it warns. It does not filter or change retrieval ranking |
113
114
  | `context_format` | Symbol | `:markdown` | user-settable | Output format for retrieval: `:claude`, `:markdown`, `:plain`, `:json` |
114
115
  | `include_framework_sources` | Boolean | `true` | user-settable | Extract Rails and gem source code |
115
116
  | `concurrent_extraction` | Boolean | `false` | user-settable | Enable parallel extraction (experimental) |
@@ -135,6 +136,17 @@ config.embedding_options = {
135
136
  }
136
137
  ```
137
138
 
139
+ OpenAI embedding batches are sent in slices of at most 36 texts, preserving
140
+ input order. For inputs within the API's 8,192-token per-text limit, this stays
141
+ below both the 2,048-input limit and the 300,000-token total request limit.
142
+ See the [OpenAI embedding request contract](https://developers.openai.com/api/reference/resources/embeddings/methods/create).
143
+ Woods retains its conservative 8,191-token chunking ceiling; slicing does not
144
+ make an individually oversized text valid. This bound avoids relying on token
145
+ estimates and adds HTTP requests for batches containing many short chunks.
146
+ All slices must validate, including consistent vector dimensions, before the
147
+ provider returns any vectors for the batch. Ollama and custom providers keep
148
+ their existing batching behavior.
149
+
138
150
  ### Ollama embeddings
139
151
 
140
152
  ```ruby
@@ -224,6 +236,15 @@ config.metadata_store_options = {
224
236
  }
225
237
  ```
226
238
 
239
+ Without an explicit `database` option, SQLite uses
240
+ `<output_dir>/metadata.sqlite3`. For `woods:embed` and
241
+ `woods:embed_incremental`, `WOODS_OUTPUT` overrides that directory together
242
+ with the index and embedding dumps. An explicit `database` path (including
243
+ `:memory:`) still takes precedence, so configure a separate path for each
244
+ worktree when overriding it. Existing databases at the old configured output
245
+ path are not moved or deleted; run `woods:embed` for the selected index after
246
+ upgrading to populate its default metadata database.
247
+
227
248
  Requires the `sqlite3` gem in your host bundle. Rails apps backed by
228
249
  MySQL or PostgreSQL won't have it by default, selecting `:sqlite`
229
250
  without it raises `Woods::ConfigurationError` with install
@@ -236,10 +257,13 @@ unless cross-process metadata persistence matters.
236
257
  config.metadata_store = :in_memory
237
258
  ```
238
259
 
239
- Pure-Ruby hash-backed store. No external dependencies, no persistence, vectors and metadata both live in the building process and die with
240
- it. The `_index.json` manifest under `output_dir` is the durable
241
- metadata for the index MCP server, so this is a reasonable default
242
- for hosts that don't bundle `sqlite3`.
260
+ Pure-Ruby hash-backed store with no external dependencies. For local vector
261
+ presets, embedding runs persist it as `metadata.msgpack` alongside `vectors.bin`
262
+ in the promoted dump; the index MCP server loads that snapshot at startup or
263
+ reload. Incremental embedding publishes changes to paths, dependencies, and
264
+ other unit metadata even when unchanged source needs no new embedding. A run
265
+ with no content or metadata changes keeps the existing dump and retention
266
+ window. This is a reasonable default for hosts that don't bundle `sqlite3`.
243
267
 
244
268
  ## Retrieval cache options
245
269
 
@@ -278,6 +302,13 @@ overrides the wrapper defaults for `:embeddings` (24 hours) and `:context`
278
302
  (15 minutes). `:memory` accepts `max_entries` (default 500); it ignores
279
303
  `default_ttl` because each wrapper write supplies its domain TTL.
280
304
 
305
+ `Woods::Cache.cache_key` length-prefixes every component, including a single
306
+ component, so different argument counts cannot share a response. Existing
307
+ multi-component keys used by Woods' wrappers remain unchanged. Custom callers
308
+ using single-component keys must clear their affected persistent cache domain
309
+ when upgrading, since older unprefixed entries can alias the new encoding;
310
+ subsequent calls refill it normally. Namespace clearing still covers both formats.
311
+
281
312
  ## Deployment shapes
282
313
 
283
314
  Woods supports three deployment shapes, pick the preset that matches yours.
@@ -358,12 +389,41 @@ end
358
389
  | `extract_navigation_edges` | Boolean | `true` | Extract `link_to`, `redirect_to`, and `form_action` navigation edges from views and controllers |
359
390
  | `enable_snapshots` | Boolean | `false` | Enable temporal snapshots. Woods automatically migrates its internal output-directory SQLite store; if SQLite is unavailable, it uses the JSON snapshot store. No Rails migration is required. |
360
391
  | `volatile_dependency_ratio` | Float | `3.0` | A dependency whose commit count (last 365 days) exceeds the dependent's by this ratio appears in the `volatile_dependencies` report (top 20, ranked by PageRank). Must be greater than 1. Report only, never a gate. |
392
+ | `volatile_dependency_limit_per_target` | Integer or `nil` | `nil` | Optional maximum report edges per dependency (type and identifier), applied after ranking and before the global top 20. Positive integers only; `nil` leaves the default report unchanged. Distinct relationships consume separate slots. |
361
393
  | `graph_cycle_limit` | Integer or `nil` | `500` | How many distinct cycles `GraphAnalyzer` enumerates before it stops. Cycle detection finds one cycle per DFS back-edge, so a dense graph has tens of thousands of them and enumerating every one is the largest single cost of the analysis that runs on every extraction. Set to `nil` for exhaustive enumeration. |
362
394
  | `graph_cycle_max_length` | Integer or `nil` | `50` | The longest cycle recorded, in distinct nodes. A back-edge deep in the DFS closes a cycle as long as the path, which on a large graph is thousands of nodes: unreadable as a report and expensive to canonicalize. Set to `nil` to record a cycle of any length. |
363
395
 
364
396
  | `incremental_blast_radius_depth` | Integer or `nil` | `nil` | How many reverse hops an incremental run walks from a changed file before it stops re-extracting dependents. `nil` keeps the unbounded transitive closure. See the note below before setting it. |
365
397
  | `durable_payload_writes` | Boolean | `false` | Force an `fsync` on every payload file as it is written, on top of the single flush every publish already performs. See the note below before setting it. |
366
398
 
399
+ **Tuning volatile dependency reports.** Start with
400
+ `graph_analysis.json`'s `stats.volatile_dependency_count`: it counts every
401
+ qualifying edge before either cap, while the array normally keeps only 20.
402
+ Raise `volatile_dependency_ratio` until the remaining candidates are useful
403
+ for your application. A measured 7k-unit app had 544 qualifying edges at 3.0;
404
+ 8–10 was a useful ratio there, not a universal recommendation. Commit counts
405
+ cover the last 365 days and need complete git history; missing git data is not
406
+ proof of stability.
407
+
408
+ If one hot dependency still fills the list, set
409
+ `volatile_dependency_limit_per_target` to a small positive integer such as 3.
410
+ Each dependency keeps its highest-ranked edges before the global limit applies,
411
+ allowing other dependencies into the report. Different relationship labels
412
+ remain separate edges and consume separate slots. With this option enabled,
413
+ `stats.volatile_dependencies_limit_per_target` records the setting and
414
+ `stats.volatile_dependency_reported_count` counts the final persisted array;
415
+ `stats.volatile_dependency_count` still counts all qualifying edges. The extra
416
+ stats are absent at the default `nil`. This limits the report, not the work of
417
+ finding qualifying edges. Run extraction again after changing either setting;
418
+ MCP reads the published report. These findings remain informational, never a
419
+ release or architecture gate.
420
+
421
+ When the JSON snapshot fallback is in use, malformed JSON, top-level values
422
+ other than objects, and files that cannot be read (including concurrent retention
423
+ removals) are warned about and treated as absent. Snapshot lists and unit history
424
+ omit them; direct lookup returns no snapshot, and a diff with an unavailable
425
+ snapshot returns empty added, modified, and deleted lists.
426
+
367
427
  `incremental_blast_radius_depth` is unbounded by default because a unit two hops
368
428
  out really can have content that depends on the changed file. An STI grandchild
369
429
  (`SportsCar < Car < Vehicle`) inherits its grandparent's associations,
@@ -408,11 +468,54 @@ require 'woods/session_tracer/file_store' # the stores are not autoloaded
408
468
 
409
469
  config.session_tracer_enabled = true
410
470
  config.session_store = Woods::SessionTracer::FileStore.new(
411
- Rails.root.join('tmp/session_traces')
471
+ base_dir: Rails.root.join('tmp/session_traces')
412
472
  )
413
473
  config.session_exclude_paths = ['/health', '/metrics', '/assets']
414
474
  ```
415
475
 
476
+ ### Redis session index compatibility
477
+
478
+ `RedisStore` lists and clears both legacy SET indexes and recency ZSET indexes.
479
+ Listing removes expired members and orders summaries by their last request.
480
+ Reads and clears preserve a legacy SET, so upgrading only readers does not
481
+ break older writers. Type checks and index operations run atomically in Redis
482
+ and tolerate a concurrent writer converting the index.
483
+
484
+ The first `record` from a newer writer converts the SET to a ZSET atomically.
485
+ Upgrade writers together: older SET writers cannot write after that conversion.
486
+ Migrated members receive score zero, so they evict in lexical order at the
487
+ retention limit until recorded again; new records use their request timestamp.
488
+ Session list contents and TTLs are preserved by the index conversion.
489
+
490
+ ### Solid Cache session retention and compatibility
491
+
492
+ `Woods::SessionTracer::SolidCacheStore` accepts a direct `SolidCache::Store`.
493
+ Its coordination layer uses private Solid Cache APIs for uncached, per-key
494
+ routing and atomic ownership. Missing APIs raise
495
+ `Woods::SessionTracer::SolidCacheCoordination::BackendError`; a gem's version
496
+ constraint alone does not establish compatibility. The live contract suite
497
+ validates SQLite, PostgreSQL, and MySQL (including MySQL's insert path without
498
+ `RETURNING`). See [the version-validation procedure](../CONTRIBUTING.md#solid-cache-session-compatibility)
499
+ before upgrading Solid Cache.
500
+
501
+ Session traces are best-effort diagnostic data. The slot directory and record
502
+ rings are bounded by `max_sessions` and `max_requests_per_session`, but the
503
+ following crash/eviction limits remain:
504
+
505
+ - A crashed admission can strand an active-session mapping outside the directory.
506
+ These mappings can accumulate across distinct session IDs, with a bounded
507
+ amount per ID. Recording that session again reclaims its mapping; the normal
508
+ directory bound is not a hard bound on these stranded keys.
509
+ - If the backend evicts a slot counter while deep crash-orphan records remain
510
+ in an unoccupied slot, new records can be silently dropped until the counter
511
+ advances past them. This loss is bounded and self-healing, but the affected
512
+ requests are not recovered.
513
+ - Losing the epoch key fences every live session. Old traces become unreadable,
514
+ and each session is admitted again on its next record. This deliberately
515
+ favors privacy over retention so previously cleared data stays cleared.
516
+
517
+ Do not use this store as an audit log or the only record of a request.
518
+
416
519
  ## Gem indexing
417
520
 
418
521
  `config.add_gem` is accepted for forward compatibility but **not implemented**: nothing in the
@@ -525,8 +628,9 @@ deployment guide including defense layers.
525
628
 
526
629
  | Key | Type | Default | Description |
527
630
  |---|---|---|---|
528
- | `console_mcp_enabled` | Boolean | `false` | Master switch. When `false`, the Railtie does not mount the Console MCP middleware. |
529
- | `console_mcp_token` | String | `ENV['WOODS_CONSOLE_MCP_TOKEN']` or `nil` | Bearer token required on every Console HTTP request. **Required in production**: the Railtie raises `Woods::ConfigurationError` when `console_mcp_enabled` is true but no token is set. In non-production a missing token warns at boot and every Console request fails closed with `401 Unauthorized`. A configured token shorter than 32 characters raises `Woods::ConfigurationError` at boot in every environment. Generate with `SecureRandom.hex(32)`. |
631
+ | `console_mcp_enabled` | Boolean | `false` | Master switch. When `false`, stdio exits and the mounted Console middleware passes requests through to Rails. |
632
+ | `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. |
633
+ | `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)`. |
530
634
  | `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. |
531
635
  | `console_mcp_path` | String | `/mcp/console` | URL path the Rack middleware responds on. |
532
636
  | `console_embedded_read_tools` | Boolean | `false` | Register `console_sql` and `console_query` in supported stdio and Rack modes. |
@@ -547,13 +651,14 @@ These variables are read by the gem and its MCP servers at runtime. They complem
547
651
 
548
652
  | Variable | Default | Purpose |
549
653
  |----------|---------|---------|
654
+ | `WOODS_RETRIEVAL_MODE` | `semantic` | Explicit packaged MCP retrieval mode: `semantic` or `lexical`. Lexical reads extraction unit JSON without provider autodetection, credentials or vector artifacts. |
550
655
  | `WOODS_DIR` | `Dir.pwd` | Path to the extraction output directory. |
551
- | `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. |
656
+ | `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`. |
552
657
  | `WOODS_ALLOW_AUTODETECT` | unset | **Deprecated no-op.** Auto-detect is now the default; accepted for backward compatibility only. |
553
658
  | `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. |
554
659
  | `WOODS_SNAPSHOTS` | unset | Set to `"true"` to force-enable temporal snapshot storage, even without a pre-existing SQLite database. |
555
660
  | `WOODS_ALLOW_PURGE` | unset | Set to `"1"` to override the 30%-deletion purge guard in `woods:embed`/`woods:embed_incremental`. |
556
- | `WOODS_PAYLOAD_RETENTION` | `3` | How many past generations' payload directories (`payloads/gen-N/`) to retain, and — when the JSON snapshot store is in use — how many temporal snapshots (`snapshots/`) to keep. A payload pinned by an active reader process is kept temporarily beyond this bound and reconsidered after the pin is released. |
661
+ | `WOODS_PAYLOAD_RETENTION` | `3` | How many past generations' payload directories (`payloads/gen-N/`) to retain, and — when the JSON snapshot store is in use — how many temporal snapshots (`snapshots/`) to keep. JSON snapshot retention counts corrupt or unreadable SHA-named files and prunes them first; the just-captured snapshot is protected. Cleanup failures are non-fatal. A payload pinned by an active reader process is kept temporarily beyond this bound and reconsidered after the pin is released. |
557
662
  | `WOODS_MCP_CACHE_TTL_MS` | `10000` | Cache TTL advertised in tool result `_meta`. `0` disables caching. |
558
663
  | `WOODS_NO_UPDATE_CHECK` | unset | Set to `"1"` to skip the `woods_status` RubyGems version check. |
559
664
  | `XDG_CACHE_HOME` | `~/.cache` | Base directory for the best-effort update-check cache (`$XDG_CACHE_HOME/woods/update_check.json`). An unset or empty value uses `~/.cache`; if the home directory cannot be resolved, Woods falls back to the system temporary directory. |
@@ -590,24 +695,52 @@ These variables are read by the gem and its MCP servers at runtime. They complem
590
695
 
591
696
  | Variable | Default | Purpose |
592
697
  |----------|---------|---------|
593
- | `WOODS_IGNORE_WATCH` | unset | Set to `"1"` to make `woods:incremental`/`woods:clean` proceed even when a daemon is (or claims to be) running. For `woods:incremental` this removes daemon coverage: a git range that fails to resolve then exits 1 instead of standing down (see [Incremental Extraction](./INCREMENTAL_EXTRACTION.md#exit-behavior-in-ci-chains)). |
698
+ | `WOODS_IGNORE_WATCH` | unset | Set to `"1"` to make `woods:incremental`/`woods:clean`/`woods:hook_refresh` proceed even when a daemon is (or claims to be) running. For `woods:incremental` this removes daemon coverage: a git range that fails to resolve then exits 1 instead of standing down (see [Incremental Extraction](./INCREMENTAL_EXTRACTION.md#exit-behavior-in-ci-chains)). |
594
699
  | `WOODS_LOCK_WAIT` | `Watch::Daemon::LOCK_STALE_TIMEOUT` (600s) | How long a rake writer waits for `PipelineLock` before exiting non-zero. |
595
700
  | `WOODS_WATCH_POLL` | auto-detected | Set to `"1"`/`"0"` to force/disable polling mode (vs. `listen` gem, e.g. in a container without inotify). |
701
+ | `WOODS_WATCH_POLL_INTERVAL` | `1.0` (seconds) | Positive, finite delay between polling scans; also used on native-watcher fallback. Does not force polling. Larger values reduce scan frequency and can delay detection and shutdown. |
596
702
  | `WOODS_WATCH_DEBOUNCE` | `0.4` (seconds) | Delay before processing a batch of file-change events. |
597
703
  | `WOODS_WATCH_FULL_THRESHOLD` | `50` | Number of changed paths in one batch that triggers a full extraction instead of incremental. |
598
704
  | `WOODS_WATCH_IDLE_TIMEOUT` | unset (no timeout) | Seconds of inactivity before the daemon exits. |
599
705
  | `WOODS_WATCH_CATCH_UP` | `1` (enabled) | Set to `"0"` to skip generation-watermark catch-up on daemon start. |
706
+ | `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. |
707
+
708
+ ### Opt-in plugin refresh hooks
709
+
710
+ These settings control the plugin shell worker. Check installed
711
+ `woods:hook_refresh` support first; the task is unreleased after 2.0.0.beta2.
712
+ See [hook coverage and retry](WATCH_DAEMON.md#hooks-for-agent-sessions) and
713
+ [optional context limits](WATCH_DAEMON.md#optional-bounded-context-hints).
714
+
715
+ | Variable | Default | Purpose |
716
+ |----------|---------|---------|
717
+ | `WOODS_HOOKS_ENABLED` | unset (disabled) | Exact `1` enables the refresh/session hooks when an index exists. |
718
+ | `WOODS_HOOKS_DISABLED` | unset | Exact `1` disables both refresh and optional context hooks. |
719
+ | `WOODS_HOOK_CONTEXT_ENABLED` | unset (disabled) | Exact `1` enables separate bounded Claude orientation/impact hints; independent of refresh enablement. |
720
+ | `WOODS_HOOK_CONTEXT_COMMAND` | `bundle exec woods-hook-context` | Installed helper argv prefix; startup counts toward the fixed context deadline. Use a wrapper for quoting/container environment. |
721
+ | `WOODS_HOOK_CONTEXT_ROOT` | payload cwd | Explicit runtime-visible application root for context path mapping; set inside a container when host paths differ. |
722
+ | `WOODS_HOOK_RAKE` | `bundle exec rake` | Application command prefix; supports `docker compose exec -T app bundle exec rake`. Use a wrapper for shell quoting or explicit container environment. |
723
+ | `WOODS_SOURCE_CAPTURE` | internal | Private, one-use `woods-extract` child handoff. Do not set or persist this variable manually; see [source freshness](SOURCE_FRESHNESS.md). |
724
+ | `WOODS_HOOK_TIMEOUT_SECONDS` | `600` | PostToolUse worker deadline (SessionStart uses a fixed ten seconds), integer 1–3600 seconds; failed/deferred batches remain queued. Docker-side cancellation requires separate verification. |
725
+ | `WOODS_HOOK_LOCK_STALE_SECONDS` | `1800` | Age used only to reclaim legacy empty mkdir locks; live PID owners are never reclaimed merely by age. |
726
+
727
+ `woods:hook_refresh[<base64 JSON>]` is the internal plugin transport. Version 1
728
+ contains `output` and an `events` array of `{path, operation}` records; paths are
729
+ application-relative and operations are `add`, `update`, `delete`, or `move`.
730
+ The task validates inputs, defers active daemons with exit 75 before Rails boot,
731
+ and checks publication failure before acknowledging work. Use ordinary extraction
732
+ tasks for manual refreshes; hook transport is not a general shell execution API.
600
733
 
601
734
  ### Extraction rake tasks
602
735
 
603
736
  | Variable | Default | Purpose |
604
737
  |----------|---------|---------|
605
- | `WOODS_OUTPUT` | `Woods.configuration.output_dir` | Overrides the output directory for `woods:extract`/`woods:incremental`/`woods:watch` without editing the initializer. |
738
+ | `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. |
606
739
  | `CHANGED_FILES` | unset | Comma-separated explicit changed-path list for `woods:incremental`; when set, git range resolution is skipped entirely. |
607
740
  | `CI_COMMIT_BEFORE_SHA`, `CI_COMMIT_SHA` | unset (GitLab) | Build the diff range `<before>..<after>` for `woods:incremental`. A zero before-SHA (new branch) makes the range unresolvable, which exits 1 unless a running daemon covers the index. |
608
741
  | `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. |
609
742
  | `RAILS_ENV` | `development` | Rails environment the rake tasks boot in. |
610
- | `WOODS_PROFILE` | unset | Set to `"1"` to log one `[Woods] [profile] <phase> in N.NNs` line per run phase (payload seed, previous graph load, eager load, extraction or blast radius and re-extraction, type index, graph analysis, flows, manifest and summary, publish). Complements the per-extractor timing lines, which cover extraction only. Off by default and free when off. |
743
+ | `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. |
611
744
  | `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`. |
612
745
  | `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. |
613
746
 
@@ -645,6 +778,49 @@ WOODS_GIT_DIR=/canonical-git bundle exec rake woods:extract
645
778
 
646
779
  The `woods-mcp` bootstrapper emits a one-line STDERR banner at startup indicating whether semantic search is enabled and which provider is active. If no key/instance is found, pattern search still works and `codebase_retrieve` surfaces an actionable fix message.
647
780
 
781
+ ## Git enrichment history
782
+
783
+ Current source requires **Git 2.31 or newer** for optional per-unit git
784
+ metadata. Extraction still succeeds when git is unavailable or history cannot
785
+ be read completely. Git enrichment is omitted in either case; a failed or
786
+ incomplete streamed history read logs a warning.
787
+ This requirement and the history policy below are unreleased after 2.0.0.beta2.
788
+
789
+ Per-unit enrichment also requires a non-shallow repository. A shallow checkout
790
+ or a failed repository-depth probe omits enrichment with one warning per
791
+ extractor instance. Fetch complete history (`git fetch --unshallow`, or
792
+ `actions/checkout` with `fetch-depth: 0`) and run full extraction to refresh
793
+ retained metadata. If depth cannot be verified, check git access and version.
794
+ A source archive without a repository remains quiet.
795
+
796
+ Full and incremental extraction use one streamed `HEAD` history walk, restricted
797
+ to the last 365 days by git's `--since` traversal. Only requested app-owned paths
798
+ are retained. Commit counts and contributors describe **HEAD-reachable touched-path
799
+ events**, independent of which other paths are requested:
800
+
801
+ - A root commit compares with an empty tree; a normal commit compares with its parent.
802
+ - A merge compares with its first parent, while traversal still visits all parents.
803
+ A normal merge can therefore count both a side commit and the merge that introduces
804
+ its change. An `ours` merge touches no paths itself, but its side commits remain
805
+ reachable and count. A conflict-resolution merge counts when its result differs
806
+ from the first parent.
807
+ - Renames are deletion/addition events at exact current names; Woods never follows
808
+ previous names. Unmerged branches, remote-only refs, and checkpoint refs are excluded.
809
+ - `change_frequency` uses total events and events newer than 90 days. Contributors
810
+ and recent commits retain their existing top-five limit; recent commits and
811
+ `last_modified` follow git's traversal order, not a separate global timestamp sort.
812
+
813
+ **Compatibility:** merge-related counts, authors, recent commits, and derived
814
+ churn analysis can differ from the old 500-path batches, which used git's
815
+ pathspec history simplification. This is an intentional semantics change.
816
+ Run a full extraction after upgrading to replace retained incremental metadata
817
+ consistently. No configuration key enables the new policy; rollback uses the
818
+ previous gem plus a full extraction. Shallow checkouts still provide truncated
819
+ history; fetch the complete history for complete 365-day evidence.
820
+
821
+ The single walk removes repeated pathspec matching. Its end-to-end speedup on
822
+ the original #305 host has not yet been measured.
823
+
648
824
  ## Database compatibility
649
825
 
650
826
  All storage options work with both MySQL and PostgreSQL, except:
@@ -653,3 +829,12 @@ All storage options work with both MySQL and PostgreSQL, except:
653
829
  - **SQLite metadata store**: uses a standalone SQLite database file, independent of your app's database
654
830
 
655
831
  See [BACKEND_MATRIX.md](BACKEND_MATRIX.md) for the full compatibility matrix.
832
+
833
+ ### Explicit retrieval and discovery scope
834
+
835
+ On a server whose tool schema advertises them, `packages` and `source_paths` narrow
836
+ `search` and `codebase_retrieve` before candidate limits. These are per-call
837
+ arguments, not configuration settings. Inspect applied scope and completeness;
838
+ a narrow graph query can omit relevant cross-boundary dependencies. See the
839
+ [scope contract](RETRIEVAL_GUIDE.md#explicit-package-and-source-path-scopes) for
840
+ root/nested ownership, path normalization, errors, storage support, and cost.
@@ -26,13 +26,23 @@ The simplest setup. The `woods:console` rake task boots Rails, then starts the e
26
26
  ```ruby
27
27
  Woods.configure do |config|
28
28
  config.console_mcp_enabled = true
29
- config.console_mcp_token = ENV["WOODS_CONSOLE_MCP_TOKEN"]
29
+ config.console_mcp_http_enabled = false # stdio-only; no HTTP endpoint
30
30
  end
31
31
  ```
32
32
 
33
- The stdio and Docker entry points exit with status 1 while this setting is false. Enabling it grants the MCP process live read access under the blocked-table, redaction, and credential-scanning controls described below.
33
+ The stdio and Docker entry points exit with status 1 while `console_mcp_enabled` is false. Enabling it grants the MCP process live read access under the blocked-table, redaction, and credential-scanning controls described below.
34
34
 
35
- The token authenticates HTTP requests; a stdio client does not send it. Production Rails boot nevertheless requires a token of at least 32 characters whenever Console MCP is enabled, even for a stdio-only setup. Store `WOODS_CONSOLE_MCP_TOKEN` in the application's normal secret store. Outside production a missing token warns and leaves Console HTTP guarded with 401 — the boot warning names both transports, so a stdio-only setup can tell that the 401 is not its symptom and that its own session still works.
35
+ `console_mcp_http_enabled` defaults to `true` to preserve existing HTTP
36
+ setups. Set it to `false` for stdio-only use: HTTP guards and the Console
37
+ middleware pass requests through to Rails, and boot skips HTTP token checks,
38
+ including in production. Stdio does not send or consume a bearer token.
39
+
40
+ If both Console flags are enabled, HTTP still requires a token of at least
41
+ 32 characters. Missing tokens warn outside production and every guarded
42
+ request returns 401; production refuses to boot. A configured short token
43
+ raises at boot in every environment while HTTP is enabled. Store the token
44
+ in the application's normal secret store. Before enabling HTTP later, set
45
+ its token, allowed origins and TLS as described in [Option C](#option-c-http-rack-middleware).
36
46
 
37
47
  ### How it works
38
48
 
@@ -172,6 +182,7 @@ In an initializer (`config/initializers/woods.rb`):
172
182
  ```ruby
173
183
  Woods.configure do |config|
174
184
  config.console_mcp_enabled = true
185
+ config.console_mcp_http_enabled = true
175
186
  config.console_mcp_token = ENV.fetch('WOODS_CONSOLE_MCP_TOKEN')
176
187
  config.console_mcp_allowed_origins = [
177
188
  'https://rails.internal.example', # public Rails/MCP Host
@@ -483,6 +494,14 @@ Until this flag is `true`, none of the transports route traffic:
483
494
 
484
495
  Keep the flag off in environments where the Console isn't needed (production web tier, CI). Flip it on per-environment, e.g. in `config/environments/development.rb` or a staging-only initializer, once the layers below are configured for that environment's threat model.
485
496
 
497
+ ### `console_mcp_http_enabled` (HTTP transport gate)
498
+
499
+ Defaults to `true` for compatibility. Set to `false` alongside
500
+ `console_mcp_enabled = true` to retain stdio access without enabling HTTP or
501
+ requiring an HTTP token. Both flags are read at request time, so settings in
502
+ `config/initializers/woods.rb` take effect. Disabling HTTP does not disable
503
+ blocked-table, redaction, credential-scanning or rollback controls on stdio.
504
+
486
505
  ### `console_blocked_tables` (layer 1: table gate)
487
506
 
488
507
  Entries are lowercased table names. A tool call is rejected at dispatch time when:
@@ -536,7 +555,11 @@ registered in a supported mode, so this setting does not enable eval.
536
555
  Woods::Console::Server.rebuild_credential_index(rails_app: Rails.application)
537
556
  ```
538
557
 
539
- This rebuilds the index from the current credentials and hot-swaps it into the active scanner. The swap is atomic on MRI, in-flight scans see either the old or the new index, never a partial one. The method is a no-op (returns `nil`) when no server has been built yet or when `console_credential_defense_enabled` is `false`.
558
+ This reads a fresh encrypted-file and key snapshot, without changing Rails' cached application credentials, and replaces the index in every live embedded Console server in this process. Each response scan keeps one complete index; a rotation cannot change its index halfway through a response. Servers released by their transports are not retained by this registry. A later server construction also reads fresh credentials.
559
+
560
+ A refresh failure (including missing files or keys, failed decryption, and invalid YAML) raises and leaves every existing index intact. Treat a failed rebuild as an operational failure: resolve the credentials deployment and retry, or restart after verification. A valid empty credential mapping intentionally clears the index. A successful rebuild replaces the old set rather than retaining removed secret values. Custom `rails_app:` collaborators that expose `credentials.config` remain supported; those collaborators own freshness of their returned config.
561
+
562
+ The method is a no-op (returns `nil`) when no live scanner remains or when `console_credential_defense_enabled` is `false`. Normal server boot still permits unavailable credentials and falls back to the other configured defenses; this permissive boot behavior does not apply to an explicit rebuild.
540
563
 
541
564
  **Rotation warning.** At boot time, Woods checks whether any credentials file (`config/credentials.yml.enc`, `config/credentials/<env>.yml.enc`) was modified *after* the process started. When it detects this, it emits a `console.credential_index.stale` warn-level structured log line with the file path, mtime, and a hint to restart or call `rebuild_credential_index`. This check is on by default; disable it with:
542
565
 
@@ -658,7 +681,8 @@ touching ActiveRecord, and neither is registered in `tools/list`.
658
681
  ```ruby
659
682
  # config/initializers/woods.rb
660
683
  Woods.configure do |config|
661
- config.console_mcp_enabled = true # mount the Rack middleware via Railtie
684
+ config.console_mcp_enabled = true # master switch
685
+ config.console_mcp_http_enabled = true # false for stdio-only use
662
686
  config.console_mcp_token = ENV.fetch('WOODS_CONSOLE_MCP_TOKEN')
663
687
  config.console_embedded_read_tools = true # unlock console_sql / console_query
664
688
  config.console_redacted_columns = Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS
@@ -753,6 +777,12 @@ For `console_query`, a schema-qualified column reference such as `orders.total`
753
777
 
754
778
  Scope hashes accept Ransack-style predicate suffixes (`_eq`, `_not_eq`, `_gt`, `_gteq`, `_lt`, `_lteq`, `_in`, `_not_in`, `_null`, `_not_null`, `_present`, `_blank`, `_matches`), see the [cookbook](MCP_TOOL_COOKBOOK.md#scope-predicates) for the full table. Every column name in a suffixed key is validated before an Arel predicate is built, so SQL injection via column names is not possible.
755
779
 
780
+ The internal scope-array defense also uses the connected adapter's dialect and
781
+ MySQL session quote modes when rejecting subqueries and forbidden keywords.
782
+ This protects direct/legacy executor callers; supported tool schemas continue
783
+ to enforce their narrower parameterized scope grammar.
784
+
785
+
756
786
  ---
757
787
 
758
788
  ## Troubleshooting
data/docs/DOCKER_SETUP.md CHANGED
@@ -87,6 +87,12 @@ 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
+ When host-side tasks or one-off containers read the daemon's shared index,
91
+ `WOODS_WATCH_TRUST_FOREIGN_HOST=1` lets those readers trust its recent heartbeat.
92
+ Set it in each reader process; Docker does not forward host variables by default.
93
+ See [cross-host liveness](WATCH_DAEMON.md#cross-host-liveness) for the 15-minute
94
+ crash-detection bound and single-supervisor requirement.
95
+
90
96
  ### Index persistence
91
97
 
92
98
  Persist `tmp/woods/` if the index should survive container replacement. A bind mount also makes it available to optional host-side tools:
@@ -185,6 +191,16 @@ Use this only when the application bundle, a supported Ruby, and the Woods execu
185
191
  | **Survives container replacement** | Only with a bind/named volume | Yes, on host disk |
186
192
  | **Needs Ruby/Woods bundle on host** | No | Yes |
187
193
 
194
+ ### Index filesystem performance
195
+
196
+ Bind mounts backed by virtiofs or FUSE can make each hardlink, rename and
197
+ metadata lookup costly. Profile `payload seed`, writes and `payload prune`
198
+ before tuning extraction. A container volume can reduce these costs, but it
199
+ changes host visibility: use a distinct index location per worktree, run MCP
200
+ where that path is visible, and update export/archive paths together. A single
201
+ shared volume for every worktree would mix their indexes. See
202
+ [incremental profiling](INCREMENTAL_EXTRACTION.md#profiling-fixed-costs).
203
+
188
204
  ## Console Server Setup
189
205
 
190
206
  The Console Server queries live Rails state. There are two launch paths for the same embedded server.
@@ -194,13 +210,16 @@ Before either path can start, deliberately enable live-data access in the Rails
194
210
  ```ruby
195
211
  Woods.configure do |config|
196
212
  config.console_mcp_enabled = true
197
- config.console_mcp_token = ENV["WOODS_CONSOLE_MCP_TOKEN"]
213
+ config.console_mcp_http_enabled = false # stdio-only
198
214
  end
199
215
  ```
200
216
 
201
217
  The process exits with status 1 while this master switch is false. Review [Console MCP setup and security](CONSOLE_MCP_SETUP.md) before enabling it.
202
218
 
203
- Stdio does not send the bearer token, but production Rails boot still requires `WOODS_CONSOLE_MCP_TOKEN` to contain at least 32 characters whenever Console is enabled. Provide it to the container through the application's normal secret mechanism. Outside production, omitting it warns and leaves the Console HTTP endpoint guarded with 401.
219
+ This explicitly disables HTTP Console while retaining stdio access; no HTTP
220
+ token is needed at boot. Existing configurations default to HTTP enabled.
221
+ For HTTP deployment, enable the HTTP flag and configure its token, origins
222
+ and TLS using the [Console setup guide](CONSOLE_MCP_SETUP.md#option-c-http-rack-middleware).
204
223
 
205
224
  ### Comparison
206
225