woods 2.0.0.beta2 → 2.0.0.beta4
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +339 -1
- data/CONTRIBUTING.md +188 -12
- data/README.md +93 -174
- data/SECURITY.md +9 -6
- data/docs/AGENT_GUIDE.md +109 -8
- data/docs/AGENT_SETUP.md +98 -7
- data/docs/BACKEND_MATRIX.md +25 -0
- data/docs/CLIENT_HOOKS.md +111 -0
- data/docs/CONFIGURATION_REFERENCE.md +267 -16
- data/docs/CONSOLE_MCP_SETUP.md +80 -7
- data/docs/DOCKER_SETUP.md +22 -3
- data/docs/EVALUATION.md +464 -1
- data/docs/EXTRACTOR_REFERENCE.md +45 -6
- data/docs/FAQ.md +11 -12
- data/docs/GETTING_STARTED.md +17 -5
- data/docs/INCREMENTAL_EXTRACTION.md +147 -7
- data/docs/INDEX_LAYOUT.md +382 -0
- data/docs/INTERNALS.md +7 -2
- data/docs/MCP_SERVERS.md +276 -5
- data/docs/MCP_TOOL_COOKBOOK.md +37 -22
- data/docs/MCP_WORKTREE_SETUP.md +43 -83
- data/docs/NOTION_INTEGRATION.md +13 -0
- data/docs/OBSIDIAN_INTEGRATION.md +57 -9
- data/docs/PUBLISHED_INDEX.md +72 -0
- data/docs/README.md +7 -0
- data/docs/RETRIEVAL_GUIDE.md +273 -12
- data/docs/RUNTIME_TRACING.md +71 -0
- data/docs/SOURCE_FRESHNESS.md +143 -0
- data/docs/TROUBLESHOOTING.md +129 -18
- data/docs/UNBLOCKED_INTEGRATION.md +25 -0
- data/docs/UPGRADING_TO_2.md +48 -22
- data/docs/WATCH_DAEMON.md +277 -67
- data/exe/woods-agent-config +6 -0
- data/exe/woods-extract +5 -0
- data/exe/woods-hook-context +6 -0
- data/exe/woods-mcp-start +14 -9
- data/lib/generators/woods/pgvector_generator.rb +8 -2
- data/lib/generators/woods/templates/woods.rb.tt +1 -3
- data/lib/tasks/woods.rake +47 -397
- data/lib/woods/agent_configuration/applier.rb +135 -0
- data/lib/woods/agent_configuration/cli.rb +101 -0
- data/lib/woods/agent_configuration/cli_options.rb +29 -0
- data/lib/woods/agent_configuration/document.rb +105 -0
- data/lib/woods/agent_configuration/error.rb +7 -0
- data/lib/woods/agent_configuration/launcher.rb +75 -0
- data/lib/woods/agent_configuration/layout.rb +72 -0
- data/lib/woods/agent_configuration/managed_section.rb +62 -0
- data/lib/woods/agent_configuration/plan.rb +98 -0
- data/lib/woods/agent_configuration/plan_diff.rb +38 -0
- data/lib/woods/agent_configuration/planned_files.rb +61 -0
- data/lib/woods/agent_configuration/planner.rb +63 -0
- data/lib/woods/agent_configuration/planner_validation.rb +77 -0
- data/lib/woods/agent_configuration/preflight.rb +100 -0
- data/lib/woods/agent_configuration/recovery.rb +49 -0
- data/lib/woods/ast/node.rb +2 -0
- data/lib/woods/ast/parser.rb +38 -5
- data/lib/woods/builder.rb +21 -5
- data/lib/woods/cache/cache_middleware.rb +28 -7
- data/lib/woods/cache/cache_store.rb +4 -5
- data/lib/woods/change_set.rb +5 -4
- data/lib/woods/console/credential_index.rb +20 -2
- data/lib/woods/console/credential_scanner.rb +18 -17
- data/lib/woods/console/credential_scanner_registry.rb +36 -0
- data/lib/woods/console/dispatch_pipeline.rb +7 -0
- data/lib/woods/console/embedded_executor.rb +32 -10
- data/lib/woods/console/encrypted_credential_snapshot.rb +16 -0
- data/lib/woods/console/rack_middleware.rb +22 -13
- data/lib/woods/console/server.rb +18 -16
- data/lib/woods/console/sql_noise_stripper.rb +9 -7
- data/lib/woods/console/sql_table_scanner.rb +47 -7
- data/lib/woods/console/sql_validator.rb +49 -9
- data/lib/woods/console/sqlite_read_guard.rb +46 -0
- data/lib/woods/coordination/pipeline_lock.rb +3 -2
- data/lib/woods/dependency_graph.rb +65 -13
- data/lib/woods/embedding/corpus.rb +94 -0
- data/lib/woods/embedding/indexer.rb +114 -60
- data/lib/woods/embedding/openai.rb +17 -6
- data/lib/woods/evaluation/ablation_executor.rb +6 -1
- data/lib/woods/evaluation/ablation_timed_executor.rb +22 -4
- data/lib/woods/export/typed_reader.rb +56 -0
- data/lib/woods/extractor.rb +277 -149
- data/lib/woods/extractors/action_cable_extractor.rb +3 -1
- data/lib/woods/extractors/behavioral_profile.rb +9 -7
- data/lib/woods/extractors/caching_extractor.rb +3 -1
- data/lib/woods/extractors/concern_extractor.rb +64 -6
- data/lib/woods/extractors/configuration_extractor.rb +7 -3
- data/lib/woods/extractors/controller_extractor.rb +13 -4
- data/lib/woods/extractors/database_view_extractor.rb +3 -1
- data/lib/woods/extractors/declared_parent.rb +55 -0
- data/lib/woods/extractors/decorator_extractor.rb +3 -1
- data/lib/woods/extractors/engine_extractor.rb +3 -1
- data/lib/woods/extractors/event_extractor.rb +4 -2
- data/lib/woods/extractors/factory_extractor.rb +3 -1
- data/lib/woods/extractors/graphql_extractor.rb +10 -13
- data/lib/woods/extractors/i18n_extractor.rb +3 -1
- data/lib/woods/extractors/job_extractor.rb +6 -19
- data/lib/woods/extractors/lib_extractor.rb +13 -9
- data/lib/woods/extractors/mailer_extractor.rb +26 -15
- data/lib/woods/extractors/manager_extractor.rb +3 -1
- data/lib/woods/extractors/method_parameters.rb +53 -0
- data/lib/woods/extractors/middleware_argument.rb +65 -0
- data/lib/woods/extractors/middleware_extractor.rb +9 -3
- data/lib/woods/extractors/migration_extractor.rb +3 -1
- data/lib/woods/extractors/model_extractor.rb +26 -34
- data/lib/woods/extractors/package_extractor.rb +24 -4
- data/lib/woods/extractors/phlex_extractor.rb +3 -1
- data/lib/woods/extractors/policy_extractor.rb +3 -1
- data/lib/woods/extractors/poro_extractor.rb +13 -9
- data/lib/woods/extractors/pundit_extractor.rb +3 -1
- data/lib/woods/extractors/rails_source_extractor.rb +4 -2
- data/lib/woods/extractors/rake_task_extractor.rb +4 -2
- data/lib/woods/extractors/route_extractor.rb +3 -1
- data/lib/woods/extractors/route_helper_resolver.rb +10 -33
- data/lib/woods/extractors/scheduled_job_extractor.rb +41 -15
- data/lib/woods/extractors/serializer_extractor.rb +4 -2
- data/lib/woods/extractors/service_extractor.rb +3 -1
- data/lib/woods/extractors/shared_dependency_scanner.rb +2 -2
- data/lib/woods/extractors/shared_utility_methods.rb +48 -19
- data/lib/woods/extractors/source_nesting.rb +1 -1
- data/lib/woods/extractors/state_machine_extractor.rb +3 -1
- data/lib/woods/extractors/test_mapping_extractor.rb +3 -1
- data/lib/woods/extractors/validator_extractor.rb +3 -1
- data/lib/woods/extractors/view_component_extractor.rb +3 -1
- data/lib/woods/extractors/view_template_extractor.rb +3 -1
- data/lib/woods/gem_mapper.rb +2 -0
- data/lib/woods/git_history.rb +116 -0
- data/lib/woods/graph_analyzer.rb +35 -6
- data/lib/woods/hooks/context_cli.rb +54 -0
- data/lib/woods/hooks/context_event.rb +88 -0
- data/lib/woods/hooks/context_hint.rb +73 -0
- data/lib/woods/hooks/context_impact.rb +77 -0
- data/lib/woods/hooks/context_output.rb +47 -0
- data/lib/woods/hooks/context_state.rb +102 -0
- data/lib/woods/hooks/refresh.rb +79 -0
- data/lib/woods/hooks/rule_projection.rb +78 -0
- data/lib/woods/input_rules.rb +19 -0
- data/lib/woods/mcp/bearer_auth.rb +22 -13
- data/lib/woods/mcp/bootstrapper.rb +79 -4
- data/lib/woods/mcp/config_resolver.rb +2 -1
- data/lib/woods/mcp/index_reader.rb +334 -162
- data/lib/woods/mcp/initialization_guidance.rb +27 -0
- data/lib/woods/mcp/origin_guard.rb +17 -9
- data/lib/woods/mcp/published_lexical_retriever.rb +115 -0
- data/lib/woods/mcp/renderers/markdown_renderer.rb +22 -9
- data/lib/woods/mcp/renderers/plain_renderer.rb +18 -8
- data/lib/woods/mcp/search_results.rb +74 -0
- data/lib/woods/mcp/server.rb +178 -63
- data/lib/woods/mcp/tool_contract.rb +3 -1
- data/lib/woods/mcp/tool_response_renderer.rb +41 -0
- data/lib/woods/mcp/traversal_evidence.rb +113 -0
- data/lib/woods/mcp/traversal_evidence_index.rb +100 -0
- data/lib/woods/mcp/traversal_evidence_page.rb +41 -0
- data/lib/woods/mcp/traversal_evidence_text.rb +52 -0
- data/lib/woods/mcp/traversal_response.rb +22 -0
- data/lib/woods/notion/exporter.rb +56 -17
- data/lib/woods/obsidian/destination_plan.rb +98 -0
- data/lib/woods/obsidian/name_mapper.rb +19 -3
- data/lib/woods/obsidian/note_builder.rb +19 -10
- data/lib/woods/obsidian/vault_exporter.rb +88 -32
- data/lib/woods/operator/pipeline_guard.rb +18 -13
- data/lib/woods/path_dispatcher.rb +13 -6
- data/lib/woods/payload_store.rb +27 -26
- data/lib/woods/published_index/typed_unit_reader.rb +40 -3
- data/lib/woods/published_index.rb +2 -2
- data/lib/woods/railtie.rb +3 -3
- data/lib/woods/railtie_support.rb +12 -12
- data/lib/woods/rake_helpers.rb +382 -0
- data/lib/woods/resilience/graph_invariant_validator/membership_checks.rb +71 -0
- data/lib/woods/resilience/graph_invariant_validator/node_checks.rb +61 -0
- data/lib/woods/resilience/graph_invariant_validator/reverse_relationship_checks.rb +46 -0
- data/lib/woods/resilience/graph_invariant_validator.rb +119 -0
- data/lib/woods/resilience/index_validator/graph_checks.rb +80 -0
- data/lib/woods/resilience/index_validator.rb +112 -23
- data/lib/woods/retrieval/context_assembler.rb +50 -15
- data/lib/woods/retrieval/lexical_assembler.rb +84 -0
- data/lib/woods/retrieval/lexical_index.rb +120 -0
- data/lib/woods/retrieval/ranker.rb +4 -2
- data/lib/woods/retrieval/scope.rb +108 -0
- data/lib/woods/retrieval/scoped_graph_store.rb +32 -0
- data/lib/woods/retrieval/scoped_vector_store.rb +55 -0
- data/lib/woods/retrieval/search_executor.rb +86 -27
- data/lib/woods/retrieval/source_evidence.rb +200 -0
- data/lib/woods/retriever.rb +98 -22
- data/lib/woods/ruby_analyzer/trace_enricher.rb +77 -38
- data/lib/woods/session_tracer/file_store.rb +6 -1
- data/lib/woods/session_tracer/middleware.rb +10 -12
- data/lib/woods/session_tracer/redis_store.rb +22 -6
- data/lib/woods/session_tracer/session_flow_assembler.rb +23 -17
- data/lib/woods/session_tracer/solid_cache_coordination.rb +6 -4
- data/lib/woods/session_tracer/unit_resolver.rb +63 -0
- data/lib/woods/source_inputs/consumer_errors.rb +31 -0
- data/lib/woods/source_inputs/handoff.rb +102 -0
- data/lib/woods/source_inputs/launcher.rb +157 -0
- data/lib/woods/source_inputs/manifest.rb +124 -0
- data/lib/woods/source_inputs/private_key.rb +55 -0
- data/lib/woods/source_inputs/scanner.rb +171 -0
- data/lib/woods/source_inputs/scopes.rb +71 -0
- data/lib/woods/source_inputs/session.rb +214 -0
- data/lib/woods/source_inputs/status.rb +84 -0
- data/lib/woods/source_inputs/verifier.rb +107 -0
- data/lib/woods/storage/metadata_store.rb +25 -25
- data/lib/woods/storage/pgvector.rb +35 -10
- data/lib/woods/storage/qdrant.rb +17 -7
- data/lib/woods/storage/vector_store.rb +18 -6
- data/lib/woods/tasks.rb +3 -2
- data/lib/woods/temporal/json_snapshot_store.rb +58 -9
- data/lib/woods/unblocked/exporter.rb +59 -70
- data/lib/woods/version.rb +1 -1
- data/lib/woods/watch/boot_snapshot.rb +52 -0
- data/lib/woods/watch/daemon.rb +154 -32
- data/lib/woods/watch/listen_watcher.rb +4 -0
- data/lib/woods/watch/polling_watcher.rb +5 -1
- data/lib/woods/watch/status.rb +20 -15
- data/lib/woods/watch/tree_scan.rb +21 -13
- data/lib/woods/watch/watcher.rb +4 -1
- data/lib/woods.rb +50 -11
- data/plugin/.claude-plugin/plugin.json +1 -1
- data/plugin/hooks/adapters/normalize.jq +15 -0
- data/plugin/hooks/adapters/normalize.rb +63 -0
- data/plugin/hooks/hooks.json +20 -0
- data/plugin/hooks/woods-context.sh +50 -0
- data/plugin/hooks/woods-input-rules.sh +159 -0
- data/plugin/hooks/woods-opencode.mjs +65 -0
- data/plugin/hooks/woods-post-edit.sh +2 -225
- data/plugin/hooks/woods-refresh.sh +260 -0
- data/plugin/hooks/woods-session-start.sh +47 -55
- data/plugin/skills/woods-agent-enable/SKILL.md +19 -0
- data/plugin/skills/woods-diagnose/SKILL.md +319 -1
- data/plugin/skills/woods-investigate/SKILL.md +145 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +90 -2
- data/plugin/skills/woods-setup/SKILL.md +110 -6
- metadata +87 -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
|
-
| `
|
|
112
|
-
| `
|
|
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
|
|
@@ -196,6 +208,25 @@ config.vector_store_options = {
|
|
|
196
208
|
}
|
|
197
209
|
```
|
|
198
210
|
|
|
211
|
+
Woods uses an HNSW index over pgvector's `vector` representation, which supports
|
|
212
|
+
**1–2,000 dimensions** ([pgvector's HNSW limits](https://github.com/pgvector/pgvector#hnsw)).
|
|
213
|
+
Unreleased after `2.0.0.beta3`: the adapter rejects wider dimensions before any
|
|
214
|
+
SQL, and `woods:pgvector` rejects invalid widths before writing a migration.
|
|
215
|
+
There is no automatic vector truncation or half-precision conversion.
|
|
216
|
+
|
|
217
|
+
The default `text-embedding-3-large` output is 3,072 dimensions. With pgvector,
|
|
218
|
+
explicitly request a supported provider output width, for example:
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
config.embedding_model = 'text-embedding-3-large'
|
|
222
|
+
config.embedding_options = { dimensions: 1536 }
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Keep the provider, `vector_store_options[:dimensions]` (when set), and generated
|
|
226
|
+
migration width equal. Changing a stored width requires a compatible new table
|
|
227
|
+
or an intentional index rebuild; changing the setting does not resize old data.
|
|
228
|
+
Use another backend if you need the full 3,072-dimensional output.
|
|
229
|
+
|
|
199
230
|
Requires the pgvector extension. Run the generator to create migrations:
|
|
200
231
|
|
|
201
232
|
```bash
|
|
@@ -224,6 +255,15 @@ config.metadata_store_options = {
|
|
|
224
255
|
}
|
|
225
256
|
```
|
|
226
257
|
|
|
258
|
+
Without an explicit `database` option, SQLite uses
|
|
259
|
+
`<output_dir>/metadata.sqlite3`. For `woods:embed` and
|
|
260
|
+
`woods:embed_incremental`, `WOODS_OUTPUT` overrides that directory together
|
|
261
|
+
with the index and embedding dumps. An explicit `database` path (including
|
|
262
|
+
`:memory:`) still takes precedence, so configure a separate path for each
|
|
263
|
+
worktree when overriding it. Existing databases at the old configured output
|
|
264
|
+
path are not moved or deleted; run `woods:embed` for the selected index after
|
|
265
|
+
upgrading to populate its default metadata database.
|
|
266
|
+
|
|
227
267
|
Requires the `sqlite3` gem in your host bundle. Rails apps backed by
|
|
228
268
|
MySQL or PostgreSQL won't have it by default, selecting `:sqlite`
|
|
229
269
|
without it raises `Woods::ConfigurationError` with install
|
|
@@ -236,10 +276,18 @@ unless cross-process metadata persistence matters.
|
|
|
236
276
|
config.metadata_store = :in_memory
|
|
237
277
|
```
|
|
238
278
|
|
|
239
|
-
Pure-Ruby hash-backed store
|
|
240
|
-
it
|
|
241
|
-
|
|
242
|
-
|
|
279
|
+
Pure-Ruby hash-backed store with no external dependencies. For local vector
|
|
280
|
+
presets, embedding runs persist it as `metadata.msgpack` alongside `vectors.bin`
|
|
281
|
+
in the promoted dump; the index MCP server loads that snapshot at startup or
|
|
282
|
+
reload. Incremental embedding publishes changes to paths, dependencies, and
|
|
283
|
+
other unit metadata even when unchanged source needs no new embedding. A run
|
|
284
|
+
with no content or metadata changes keeps the existing dump and retention
|
|
285
|
+
window. Unreleased after `2.0.0.beta3`: a full `Indexer#index_all` run replaces
|
|
286
|
+
the published corpus even when a custom caller reuses in-memory vector and
|
|
287
|
+
metadata stores. Deleted units, including metadata-only records, are removed;
|
|
288
|
+
an empty full rebuild publishes an empty dump. Failed embedding leaves the
|
|
289
|
+
previous promoted dump and checkpoint intact. Incremental purge guards remain
|
|
290
|
+
unchanged. This is a reasonable default for hosts that don't bundle `sqlite3`.
|
|
243
291
|
|
|
244
292
|
## Retrieval cache options
|
|
245
293
|
|
|
@@ -278,6 +326,13 @@ overrides the wrapper defaults for `:embeddings` (24 hours) and `:context`
|
|
|
278
326
|
(15 minutes). `:memory` accepts `max_entries` (default 500); it ignores
|
|
279
327
|
`default_ttl` because each wrapper write supplies its domain TTL.
|
|
280
328
|
|
|
329
|
+
`Woods::Cache.cache_key` length-prefixes every component, including a single
|
|
330
|
+
component, so different argument counts cannot share a response. Existing
|
|
331
|
+
multi-component keys used by Woods' wrappers remain unchanged. Custom callers
|
|
332
|
+
using single-component keys must clear their affected persistent cache domain
|
|
333
|
+
when upgrading, since older unprefixed entries can alias the new encoding;
|
|
334
|
+
subsequent calls refill it normally. Namespace clearing still covers both formats.
|
|
335
|
+
|
|
281
336
|
## Deployment shapes
|
|
282
337
|
|
|
283
338
|
Woods supports three deployment shapes, pick the preset that matches yours.
|
|
@@ -304,7 +359,7 @@ The embed run writes `woods.json` + `dumps/<ISO8601>/vectors.bin` + `metadata.ms
|
|
|
304
359
|
|
|
305
360
|
Requirements:
|
|
306
361
|
- `output_dir` must be set and readable by both the embed process and the MCP server.
|
|
307
|
-
- The MCP server must know the same `output_dir` (pass via `woods-mcp <DIR>` or set `WOODS_DIR
|
|
362
|
+
- The MCP server must know the same `output_dir` (pass via `woods-mcp <DIR>` or set `WOODS_DIR`; see MCP path precedence below).
|
|
308
363
|
|
|
309
364
|
## Presets
|
|
310
365
|
|
|
@@ -358,12 +413,56 @@ end
|
|
|
358
413
|
| `extract_navigation_edges` | Boolean | `true` | Extract `link_to`, `redirect_to`, and `form_action` navigation edges from views and controllers |
|
|
359
414
|
| `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
415
|
| `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. |
|
|
416
|
+
| `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
417
|
| `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
418
|
| `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
419
|
|
|
364
420
|
| `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
421
|
| `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
422
|
|
|
423
|
+
**Tuning volatile dependency reports.** Start with
|
|
424
|
+
`graph_analysis.json`'s `stats.volatile_dependency_count`: it counts every
|
|
425
|
+
qualifying edge before either cap, while the array normally keeps only 20.
|
|
426
|
+
Raise `volatile_dependency_ratio` until the remaining candidates are useful
|
|
427
|
+
for your application. A measured 7k-unit app had 544 qualifying edges at 3.0;
|
|
428
|
+
8–10 was a useful ratio there, not a universal recommendation. Commit counts
|
|
429
|
+
cover the last 365 days and need complete git history; missing git data is not
|
|
430
|
+
proof of stability.
|
|
431
|
+
|
|
432
|
+
If one hot dependency still fills the list, set
|
|
433
|
+
`volatile_dependency_limit_per_target` to a small positive integer such as 3.
|
|
434
|
+
Each dependency keeps its highest-ranked edges before the global limit applies,
|
|
435
|
+
allowing other dependencies into the report. Different relationship labels
|
|
436
|
+
remain separate edges and consume separate slots. With this option enabled,
|
|
437
|
+
`stats.volatile_dependencies_limit_per_target` records the setting and
|
|
438
|
+
`stats.volatile_dependency_reported_count` counts the final persisted array;
|
|
439
|
+
`stats.volatile_dependency_count` still counts all qualifying edges. The extra
|
|
440
|
+
stats are absent at the default `nil`. This limits the report, not the work of
|
|
441
|
+
finding qualifying edges. Run extraction again after changing either setting;
|
|
442
|
+
MCP reads the published report. These findings remain informational, never a
|
|
443
|
+
release or architecture gate.
|
|
444
|
+
|
|
445
|
+
When the JSON snapshot fallback is in use, malformed JSON, invalid snapshot
|
|
446
|
+
shapes, and files that cannot be read (including concurrent retention removals)
|
|
447
|
+
are warned about and treated as absent. A snapshot needs a hexadecimal string
|
|
448
|
+
`git_sha` matching its filename; `extracted_at` may be a string, null, or omitted.
|
|
449
|
+
When present and non-null, `units` must be an object whose records are objects.
|
|
450
|
+
A malformed record invalidates the entire snapshot, rather than exposing partial
|
|
451
|
+
history. Legacy bare identifier keys, omitted/null unit collections, and optional per-unit hash
|
|
452
|
+
fields remain supported; timestamp strings are not restricted to a new format.
|
|
453
|
+
Unit history limits count matching unit records, not the most recent snapshots
|
|
454
|
+
searched (JSON fallback correction unreleased after `2.0.0.beta3`). A unit
|
|
455
|
+
missing from newer snapshots can still have retained history.
|
|
456
|
+
Snapshot lists and unit history omit unusable files; direct lookup returns no
|
|
457
|
+
snapshot, and a diff with an unavailable snapshot returns empty added, modified,
|
|
458
|
+
and deleted lists. An empty diff in this case is not proof that nothing changed.
|
|
459
|
+
New captures compare against the latest usable snapshot. Unusable SHA-named files
|
|
460
|
+
still count toward retention and are pruned first when the limit is exceeded;
|
|
461
|
+
reading alone does not delete them. Valid legacy snapshots with null or omitted
|
|
462
|
+
timestamps are retained ahead of corrupt files, then treated as oldest among
|
|
463
|
+
usable snapshots. Direct lookup and diff still reject invalid
|
|
464
|
+
caller-supplied SHA paths with an argument error.
|
|
465
|
+
|
|
367
466
|
`incremental_blast_radius_depth` is unbounded by default because a unit two hops
|
|
368
467
|
out really can have content that depends on the changed file. An STI grandchild
|
|
369
468
|
(`SportsCar < Car < Vehicle`) inherits its grandparent's associations,
|
|
@@ -408,11 +507,66 @@ require 'woods/session_tracer/file_store' # the stores are not autoloaded
|
|
|
408
507
|
|
|
409
508
|
config.session_tracer_enabled = true
|
|
410
509
|
config.session_store = Woods::SessionTracer::FileStore.new(
|
|
411
|
-
Rails.root.join('tmp/session_traces')
|
|
510
|
+
base_dir: Rails.root.join('tmp/session_traces')
|
|
412
511
|
)
|
|
413
512
|
config.session_exclude_paths = ['/health', '/metrics', '/assets']
|
|
414
513
|
```
|
|
415
514
|
|
|
515
|
+
### File session retention
|
|
516
|
+
|
|
517
|
+
`FileStore` accepts `ttl:` in seconds (default `nil`, expiration disabled),
|
|
518
|
+
`max_sessions:` (default `1000`), and `max_requests_per_session:` (default `1000`).
|
|
519
|
+
TTL expires a file when the store clock reaches its modification time plus the
|
|
520
|
+
TTL. Recording after expiry starts a fresh history; expired events are discarded
|
|
521
|
+
before appending or migrating legacy filenames, under the same store lock.
|
|
522
|
+
When legacy and encoded files coexist, each expires independently before any
|
|
523
|
+
surviving histories are merged. Clearing a session is idempotent for supported
|
|
524
|
+
IDs, including Unicode and punctuation, and removes both filename formats when
|
|
525
|
+
applicable.
|
|
526
|
+
|
|
527
|
+
### Redis session index compatibility
|
|
528
|
+
|
|
529
|
+
`RedisStore` lists and clears both legacy SET indexes and recency ZSET indexes.
|
|
530
|
+
Listing removes expired members and orders summaries by their last request.
|
|
531
|
+
Reads and clears preserve a legacy SET, so upgrading only readers does not
|
|
532
|
+
break older writers. Type checks and index operations run atomically in Redis
|
|
533
|
+
and tolerate a concurrent writer converting the index.
|
|
534
|
+
|
|
535
|
+
The first `record` from a newer writer converts the SET to a ZSET atomically.
|
|
536
|
+
Upgrade writers together: older SET writers cannot write after that conversion.
|
|
537
|
+
Migrated members receive score zero, so they evict in lexical order at the
|
|
538
|
+
retention limit until recorded again; new records use their request timestamp.
|
|
539
|
+
Session list contents and TTLs are preserved by the index conversion.
|
|
540
|
+
|
|
541
|
+
### Solid Cache session retention and compatibility
|
|
542
|
+
|
|
543
|
+
`Woods::SessionTracer::SolidCacheStore` accepts a direct `SolidCache::Store`.
|
|
544
|
+
Its coordination layer uses private Solid Cache APIs for uncached, per-key
|
|
545
|
+
routing and atomic ownership. Missing APIs raise
|
|
546
|
+
`Woods::SessionTracer::SolidCacheCoordination::BackendError`; a gem's version
|
|
547
|
+
constraint alone does not establish compatibility. The live contract suite
|
|
548
|
+
validates SQLite, PostgreSQL, and MySQL (including MySQL's insert path without
|
|
549
|
+
`RETURNING`). See [the version-validation procedure](../CONTRIBUTING.md#solid-cache-session-compatibility)
|
|
550
|
+
before upgrading Solid Cache.
|
|
551
|
+
|
|
552
|
+
Session traces are best-effort diagnostic data. The slot directory and record
|
|
553
|
+
rings are bounded by `max_sessions` and `max_requests_per_session`, but the
|
|
554
|
+
following crash/eviction limits remain:
|
|
555
|
+
|
|
556
|
+
- A crashed admission can strand an active-session mapping outside the directory.
|
|
557
|
+
These mappings can accumulate across distinct session IDs, with a bounded
|
|
558
|
+
amount per ID. Recording that session again reclaims its mapping; the normal
|
|
559
|
+
directory bound is not a hard bound on these stranded keys.
|
|
560
|
+
- If the backend evicts a slot counter while deep crash-orphan records remain
|
|
561
|
+
in an unoccupied slot, new records can be silently dropped until the counter
|
|
562
|
+
advances past them. This loss is bounded and self-healing, but the affected
|
|
563
|
+
requests are not recovered.
|
|
564
|
+
- Losing the epoch key fences every live session. Old traces become unreadable,
|
|
565
|
+
and each session is admitted again on its next record. This deliberately
|
|
566
|
+
favors privacy over retention so previously cleared data stays cleared.
|
|
567
|
+
|
|
568
|
+
Do not use this store as an audit log or the only record of a request.
|
|
569
|
+
|
|
416
570
|
## Gem indexing
|
|
417
571
|
|
|
418
572
|
`config.add_gem` is accepted for forward compatibility but **not implemented**: nothing in the
|
|
@@ -525,8 +679,9 @@ deployment guide including defense layers.
|
|
|
525
679
|
|
|
526
680
|
| Key | Type | Default | Description |
|
|
527
681
|
|---|---|---|---|
|
|
528
|
-
| `console_mcp_enabled` | Boolean | `false` | Master switch. When `false`, the
|
|
529
|
-
| `
|
|
682
|
+
| `console_mcp_enabled` | Boolean | `false` | Master switch. When `false`, stdio exits and the mounted Console middleware passes requests through to Rails. |
|
|
683
|
+
| `console_mcp_http_enabled` | Boolean | `true` | HTTP transport switch; effective only while the master switch is on. Set `false` for stdio-only use without HTTP token validation or an active HTTP endpoint. Read at request time. |
|
|
684
|
+
| `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
685
|
| `console_mcp_allowed_origins` | Array\<String\> | `%w[http://localhost http://127.0.0.1 http://[::1]]` | `OriginGuard` allowlist. Port is stripped before comparison, so `http://localhost` matches any localhost port. Override for tunneled / internal-dashboard access. |
|
|
531
686
|
| `console_mcp_path` | String | `/mcp/console` | URL path the Rack middleware responds on. |
|
|
532
687
|
| `console_embedded_read_tools` | Boolean | `false` | Register `console_sql` and `console_query` in supported stdio and Rack modes. |
|
|
@@ -547,13 +702,15 @@ These variables are read by the gem and its MCP servers at runtime. They complem
|
|
|
547
702
|
|
|
548
703
|
| Variable | Default | Purpose |
|
|
549
704
|
|----------|---------|---------|
|
|
550
|
-
| `
|
|
551
|
-
| `
|
|
705
|
+
| `WOODS_RETRIEVAL_MODE` | `semantic` | Explicit packaged MCP retrieval mode: `semantic` or `lexical`. Lexical reads extraction unit JSON without provider autodetection, credentials or vector artifacts. |
|
|
706
|
+
| `WOODS_DIR` | unset | MCP extraction-index path, after a positional argument and before `WOODS_OUTPUT`. See precedence below. |
|
|
707
|
+
| `WOODS_OUTPUT` | unset | MCP index-path fallback when neither a positional path nor `WOODS_DIR` is set; unreleased after `2.0.0.beta3`. |
|
|
708
|
+
| `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
709
|
| `WOODS_ALLOW_AUTODETECT` | unset | **Deprecated no-op.** Auto-detect is now the default; accepted for backward compatibility only. |
|
|
553
710
|
| `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
711
|
| `WOODS_SNAPSHOTS` | unset | Set to `"true"` to force-enable temporal snapshot storage, even without a pre-existing SQLite database. |
|
|
555
712
|
| `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. |
|
|
713
|
+
| `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
714
|
| `WOODS_MCP_CACHE_TTL_MS` | `10000` | Cache TTL advertised in tool result `_meta`. `0` disables caching. |
|
|
558
715
|
| `WOODS_NO_UPDATE_CHECK` | unset | Set to `"1"` to skip the `woods_status` RubyGems version check. |
|
|
559
716
|
| `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. |
|
|
@@ -563,6 +720,20 @@ These variables are read by the gem and its MCP servers at runtime. They complem
|
|
|
563
720
|
| `WOODS_QDRANT_URL`, `WOODS_QDRANT_COLLECTION`, `WOODS_QDRANT_API_KEY` | n/a | Override/require Qdrant connection settings when a pgvector/Qdrant-backed index is served outside its host application (no `Woods.configuration` available). |
|
|
564
721
|
| `WOODS_PG_URL` | n/a | Required when a pgvector-backed index is served outside its host application. |
|
|
565
722
|
|
|
723
|
+
**MCP index path precedence (unreleased after `2.0.0.beta3`):** positional
|
|
724
|
+
argument → `WOODS_DIR` → `WOODS_OUTPUT` → current directory for `woods-mcp`
|
|
725
|
+
and `woods-mcp-http`. `woods-mcp-start` still requires one of the first three;
|
|
726
|
+
it never silently selects the current directory. An explicitly empty
|
|
727
|
+
`WOODS_DIR` remains an invalid override rather than falling through. Earlier
|
|
728
|
+
versions accept the positional path or `WOODS_DIR`; use an explicit path for
|
|
729
|
+
portable client configuration.
|
|
730
|
+
|
|
731
|
+
Paths are resolved in the MCP process's working directory and filesystem.
|
|
732
|
+
A published index can have `generation.json` pointing to a payload's
|
|
733
|
+
`manifest.json`; a root `manifest.json` is only the legacy flat layout. If
|
|
734
|
+
startup cannot find a manifest, check the reported directory and point at the
|
|
735
|
+
existing index before deciding another extraction is needed.
|
|
736
|
+
|
|
566
737
|
### Rake tasks
|
|
567
738
|
|
|
568
739
|
| Variable | Default | Purpose |
|
|
@@ -590,24 +761,52 @@ These variables are read by the gem and its MCP servers at runtime. They complem
|
|
|
590
761
|
|
|
591
762
|
| Variable | Default | Purpose |
|
|
592
763
|
|----------|---------|---------|
|
|
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)). |
|
|
764
|
+
| `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
765
|
| `WOODS_LOCK_WAIT` | `Watch::Daemon::LOCK_STALE_TIMEOUT` (600s) | How long a rake writer waits for `PipelineLock` before exiting non-zero. |
|
|
595
766
|
| `WOODS_WATCH_POLL` | auto-detected | Set to `"1"`/`"0"` to force/disable polling mode (vs. `listen` gem, e.g. in a container without inotify). |
|
|
767
|
+
| `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
768
|
| `WOODS_WATCH_DEBOUNCE` | `0.4` (seconds) | Delay before processing a batch of file-change events. |
|
|
597
769
|
| `WOODS_WATCH_FULL_THRESHOLD` | `50` | Number of changed paths in one batch that triggers a full extraction instead of incremental. |
|
|
598
770
|
| `WOODS_WATCH_IDLE_TIMEOUT` | unset (no timeout) | Seconds of inactivity before the daemon exits. |
|
|
599
771
|
| `WOODS_WATCH_CATCH_UP` | `1` (enabled) | Set to `"0"` to skip generation-watermark catch-up on daemon start. |
|
|
772
|
+
| `WOODS_WATCH_TRUST_FOREIGN_HOST` | unset (disabled) | Set to `"1"` in each task/MCP reader to trust a foreign daemon's heartbeat for up to 15 minutes, without a local pid check. See [cross-host liveness](WATCH_DAEMON.md#cross-host-liveness) for clock bounds, degraded coverage, and startup limitations. |
|
|
773
|
+
|
|
774
|
+
### Opt-in plugin refresh hooks
|
|
775
|
+
|
|
776
|
+
These settings control the plugin shell worker. Check installed
|
|
777
|
+
`woods:hook_refresh` support first; the task is unreleased after 2.0.0.beta2.
|
|
778
|
+
See [hook coverage and retry](WATCH_DAEMON.md#hooks-for-agent-sessions) and
|
|
779
|
+
[optional context limits](WATCH_DAEMON.md#optional-bounded-context-hints).
|
|
780
|
+
|
|
781
|
+
| Variable | Default | Purpose |
|
|
782
|
+
|----------|---------|---------|
|
|
783
|
+
| `WOODS_HOOKS_ENABLED` | unset (disabled) | Exact `1` enables the refresh/session hooks when an index exists. |
|
|
784
|
+
| `WOODS_HOOKS_DISABLED` | unset | Exact `1` disables both refresh and optional context hooks. |
|
|
785
|
+
| `WOODS_HOOK_CONTEXT_ENABLED` | unset (disabled) | Exact `1` enables separate bounded Claude orientation/impact hints; independent of refresh enablement. |
|
|
786
|
+
| `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. |
|
|
787
|
+
| `WOODS_HOOK_CONTEXT_ROOT` | payload cwd | Explicit runtime-visible application root for context path mapping; set inside a container when host paths differ. |
|
|
788
|
+
| `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. |
|
|
789
|
+
| `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). |
|
|
790
|
+
| `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. |
|
|
791
|
+
| `WOODS_HOOK_LOCK_STALE_SECONDS` | `1800` | Age used only to reclaim legacy empty mkdir locks; live PID owners are never reclaimed merely by age. |
|
|
792
|
+
|
|
793
|
+
`woods:hook_refresh[<base64 JSON>]` is the internal plugin transport. Version 1
|
|
794
|
+
contains `output` and an `events` array of `{path, operation}` records; paths are
|
|
795
|
+
application-relative and operations are `add`, `update`, `delete`, or `move`.
|
|
796
|
+
The task validates inputs, defers active daemons with exit 75 before Rails boot,
|
|
797
|
+
and checks publication failure before acknowledging work. Use ordinary extraction
|
|
798
|
+
tasks for manual refreshes; hook transport is not a general shell execution API.
|
|
600
799
|
|
|
601
800
|
### Extraction rake tasks
|
|
602
801
|
|
|
603
802
|
| Variable | Default | Purpose |
|
|
604
803
|
|----------|---------|---------|
|
|
605
|
-
| `WOODS_OUTPUT` | `Woods.configuration.output_dir` | Overrides the output directory for
|
|
804
|
+
| `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
805
|
| `CHANGED_FILES` | unset | Comma-separated explicit changed-path list for `woods:incremental`; when set, git range resolution is skipped entirely. |
|
|
607
806
|
| `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
807
|
| `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
808
|
| `RAILS_ENV` | `development` | Rails environment the rake tasks boot in. |
|
|
610
|
-
| `WOODS_PROFILE` | unset | Set to `"1"` to log
|
|
809
|
+
| `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
810
|
| `WOODS_GIT_DIR` | unset | Absolute path to the canonical git directory. Wins over the repository Woods would otherwise find, at all three of its git call sites: per-unit `commit_count`/`change_frequency` (enrichment), `manifest.json`'s `git_branch`/`git_sha` (provenance), and the `woods:incremental` diff range. All three build their command line with `Woods::GitCommand.argv`. |
|
|
612
811
|
| `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
812
|
|
|
@@ -645,6 +844,49 @@ WOODS_GIT_DIR=/canonical-git bundle exec rake woods:extract
|
|
|
645
844
|
|
|
646
845
|
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
846
|
|
|
847
|
+
## Git enrichment history
|
|
848
|
+
|
|
849
|
+
Current source requires **Git 2.31 or newer** for optional per-unit git
|
|
850
|
+
metadata. Extraction still succeeds when git is unavailable or history cannot
|
|
851
|
+
be read completely. Git enrichment is omitted in either case; a failed or
|
|
852
|
+
incomplete streamed history read logs a warning.
|
|
853
|
+
This requirement and the history policy below are unreleased after 2.0.0.beta2.
|
|
854
|
+
|
|
855
|
+
Per-unit enrichment also requires a non-shallow repository. A shallow checkout
|
|
856
|
+
or a failed repository-depth probe omits enrichment with one warning per
|
|
857
|
+
extractor instance. Fetch complete history (`git fetch --unshallow`, or
|
|
858
|
+
`actions/checkout` with `fetch-depth: 0`) and run full extraction to refresh
|
|
859
|
+
retained metadata. If depth cannot be verified, check git access and version.
|
|
860
|
+
A source archive without a repository remains quiet.
|
|
861
|
+
|
|
862
|
+
Full and incremental extraction use one streamed `HEAD` history walk, restricted
|
|
863
|
+
to the last 365 days by git's `--since` traversal. Only requested app-owned paths
|
|
864
|
+
are retained. Commit counts and contributors describe **HEAD-reachable touched-path
|
|
865
|
+
events**, independent of which other paths are requested:
|
|
866
|
+
|
|
867
|
+
- A root commit compares with an empty tree; a normal commit compares with its parent.
|
|
868
|
+
- A merge compares with its first parent, while traversal still visits all parents.
|
|
869
|
+
A normal merge can therefore count both a side commit and the merge that introduces
|
|
870
|
+
its change. An `ours` merge touches no paths itself, but its side commits remain
|
|
871
|
+
reachable and count. A conflict-resolution merge counts when its result differs
|
|
872
|
+
from the first parent.
|
|
873
|
+
- Renames are deletion/addition events at exact current names; Woods never follows
|
|
874
|
+
previous names. Unmerged branches, remote-only refs, and checkpoint refs are excluded.
|
|
875
|
+
- `change_frequency` uses total events and events newer than 90 days. Contributors
|
|
876
|
+
and recent commits retain their existing top-five limit; recent commits and
|
|
877
|
+
`last_modified` follow git's traversal order, not a separate global timestamp sort.
|
|
878
|
+
|
|
879
|
+
**Compatibility:** merge-related counts, authors, recent commits, and derived
|
|
880
|
+
churn analysis can differ from the old 500-path batches, which used git's
|
|
881
|
+
pathspec history simplification. This is an intentional semantics change.
|
|
882
|
+
Run a full extraction after upgrading to replace retained incremental metadata
|
|
883
|
+
consistently. No configuration key enables the new policy; rollback uses the
|
|
884
|
+
previous gem plus a full extraction. Shallow checkouts still provide truncated
|
|
885
|
+
history; fetch the complete history for complete 365-day evidence.
|
|
886
|
+
|
|
887
|
+
The single walk removes repeated pathspec matching. Its end-to-end speedup on
|
|
888
|
+
the original #305 host has not yet been measured.
|
|
889
|
+
|
|
648
890
|
## Database compatibility
|
|
649
891
|
|
|
650
892
|
All storage options work with both MySQL and PostgreSQL, except:
|
|
@@ -653,3 +895,12 @@ All storage options work with both MySQL and PostgreSQL, except:
|
|
|
653
895
|
- **SQLite metadata store**: uses a standalone SQLite database file, independent of your app's database
|
|
654
896
|
|
|
655
897
|
See [BACKEND_MATRIX.md](BACKEND_MATRIX.md) for the full compatibility matrix.
|
|
898
|
+
|
|
899
|
+
### Explicit retrieval and discovery scope
|
|
900
|
+
|
|
901
|
+
On a server whose tool schema advertises them, `packages` and `source_paths` narrow
|
|
902
|
+
`search` and `codebase_retrieve` before candidate limits. These are per-call
|
|
903
|
+
arguments, not configuration settings. Inspect applied scope and completeness;
|
|
904
|
+
a narrow graph query can omit relevant cross-boundary dependencies. See the
|
|
905
|
+
[scope contract](RETRIEVAL_GUIDE.md#explicit-package-and-source-path-scopes) for
|
|
906
|
+
root/nested ownership, path normalization, errors, storage support, and cost.
|
data/docs/CONSOLE_MCP_SETUP.md
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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
|
|
@@ -187,6 +198,12 @@ the Rails server environment. The middleware stack registers automatically via
|
|
|
187
198
|
the gem's Railtie and requires `Authorization: Bearer <token>` on every Console
|
|
188
199
|
request. Missing or incorrect tokens receive `401 Unauthorized`.
|
|
189
200
|
|
|
201
|
+
The HTTP authentication scheme is ASCII case-insensitive (`Bearer`, `bearer`,
|
|
202
|
+
or `BEARER`); the token remains case-sensitive and must match exactly after one
|
|
203
|
+
space. This applies to both Console HTTP and `woods-mcp-http`. Case-insensitive
|
|
204
|
+
scheme support is unreleased after `2.0.0.beta3`; use the canonical `Bearer`
|
|
205
|
+
spelling in client configuration for compatibility with earlier releases.
|
|
206
|
+
|
|
190
207
|
For non-loopback access, `console_mcp_allowed_origins` must include the public
|
|
191
208
|
Rails/MCP host. If a browser-based client sends an `Origin` header from a
|
|
192
209
|
different host, include that exact origin too. This allow-list controls both
|
|
@@ -483,6 +500,14 @@ Until this flag is `true`, none of the transports route traffic:
|
|
|
483
500
|
|
|
484
501
|
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
502
|
|
|
503
|
+
### `console_mcp_http_enabled` (HTTP transport gate)
|
|
504
|
+
|
|
505
|
+
Defaults to `true` for compatibility. Set to `false` alongside
|
|
506
|
+
`console_mcp_enabled = true` to retain stdio access without enabling HTTP or
|
|
507
|
+
requiring an HTTP token. Both flags are read at request time, so settings in
|
|
508
|
+
`config/initializers/woods.rb` take effect. Disabling HTTP does not disable
|
|
509
|
+
blocked-table, redaction, credential-scanning or rollback controls on stdio.
|
|
510
|
+
|
|
486
511
|
### `console_blocked_tables` (layer 1: table gate)
|
|
487
512
|
|
|
488
513
|
Entries are lowercased table names. A tool call is rejected at dispatch time when:
|
|
@@ -536,7 +561,11 @@ registered in a supported mode, so this setting does not enable eval.
|
|
|
536
561
|
Woods::Console::Server.rebuild_credential_index(rails_app: Rails.application)
|
|
537
562
|
```
|
|
538
563
|
|
|
539
|
-
This
|
|
564
|
+
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.
|
|
565
|
+
|
|
566
|
+
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.
|
|
567
|
+
|
|
568
|
+
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
569
|
|
|
541
570
|
**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
571
|
|
|
@@ -658,7 +687,8 @@ touching ActiveRecord, and neither is registered in `tools/list`.
|
|
|
658
687
|
```ruby
|
|
659
688
|
# config/initializers/woods.rb
|
|
660
689
|
Woods.configure do |config|
|
|
661
|
-
config.console_mcp_enabled = true #
|
|
690
|
+
config.console_mcp_enabled = true # master switch
|
|
691
|
+
config.console_mcp_http_enabled = true # false for stdio-only use
|
|
662
692
|
config.console_mcp_token = ENV.fetch('WOODS_CONSOLE_MCP_TOKEN')
|
|
663
693
|
config.console_embedded_read_tools = true # unlock console_sql / console_query
|
|
664
694
|
config.console_redacted_columns = Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS
|
|
@@ -736,12 +766,12 @@ Each transaction sets a statement timeout before any query runs. The default is
|
|
|
736
766
|
|
|
737
767
|
`SqlValidator` rejects non-read-only SQL at the string level, before any database interaction.
|
|
738
768
|
|
|
739
|
-
Validation runs **once**, inside the executor, with the dialect of the live adapter. There is deliberately no earlier dialect-blind pre-check in the tool handler: a validator built without a dialect is the conservative
|
|
769
|
+
Validation runs **once**, inside the executor, with the dialect of the live adapter. There is deliberately no earlier dialect-blind pre-check in the tool handler: a validator built without a dialect is the conservative union of supported dialects, and running it first meant a MySQL host rejected statements whose `\'`/backtick grammar produces a spuriously forbidden PostgreSQL view — the adapter-aware acceptance below could never be reached on a real transport. The executor raises `SqlValidationError` for anything it refuses, which the dispatch pipeline renders as a tool error, so nothing is ungated.
|
|
740
770
|
|
|
741
771
|
|
|
742
772
|
- **Allowed prefixes:** `SELECT`, `WITH...SELECT`, and plain `EXPLAIN`. `EXPLAIN ANALYZE` is rejected, it executes the query rather than just planning it (both the whitespace and `EXPLAIN (ANALYZE, …)` option-list spellings).
|
|
743
773
|
- **Rejected prefixes:** `INSERT`, `UPDATE`, `DELETE`, `MERGE`, `DROP`, `ALTER`, `TRUNCATE`, `CREATE`, `GRANT`, `REVOKE`
|
|
744
|
-
- **Rejected anywhere in query:** `UNION`, `INTO`, `COPY`; row-lock clauses (`FOR UPDATE`, `FOR NO KEY UPDATE`, `FOR SHARE`, `FOR KEY SHARE`, `FOR UPDATE NOWAIT`/`SKIP LOCKED`, MySQL `LOCK IN SHARE MODE`) — these take live row locks even inside the rolled-back transaction. The lock check is adapter-aware: `console_sql` validates with the active adapter's dialect, including MySQL double-quoted strings/backtick identifiers and PostgreSQL quoted identifiers/E-strings. Unknown adapters conservatively scan
|
|
774
|
+
- **Rejected anywhere in query:** `UNION`, `INTO`, `COPY`; row-lock clauses (`FOR UPDATE`, `FOR NO KEY UPDATE`, `FOR SHARE`, `FOR KEY SHARE`, `FOR UPDATE NOWAIT`/`SKIP LOCKED`, MySQL `LOCK IN SHARE MODE`) — these take live row locks even inside the rolled-back transaction. The lock check is adapter-aware: `console_sql` validates with the active adapter's dialect, including MySQL double-quoted strings/backtick identifiers and PostgreSQL quoted identifiers/E-strings. Unknown adapters conservatively scan all supported normalizations. Every view is scanned under both MySQL executable-comment (`/*!...*/`) semantics, so `#` comments and version-guarded comments cannot split a clause apart.
|
|
745
775
|
- **Function allowlist (the authoritative function control):** every function-call-shaped identifier must appear in `ALLOWED_FUNCTIONS`, a conservative set of pure read-only functions (aggregates, window functions, string/number/date/JSON readers) kept portable across MySQL, PostgreSQL, and SQLite. Anything else is rejected by name, quoted forms (`"pg_terminate_backend"(…)`) included. This is an allowlist because a denylist cannot enumerate every side-effecting function (`nextval`, `pg_advisory_lock`, `pg_terminate_backend`, …). A legacy `DANGEROUS_FUNCTIONS` denylist (`pg_sleep`, `lo_import`, `lo_export`, `pg_read_file`, `pg_write_file`, `load_file`, `sleep`, `benchmark`) still runs first as belt-and-suspenders.
|
|
746
776
|
- **Rejected patterns:** multiple statements (semicolons), writable CTEs (every `AS (...)` body is checked, so a writable CTE in any WITH position is refused — `WITH a AS (SELECT 1), b AS (DELETE FROM users RETURNING *) SELECT * FROM b`), a CTE list attached to top-level DML (`WITH a AS (SELECT 1) DELETE FROM users RETURNING *`), comment-hidden injections
|
|
747
777
|
|
|
@@ -753,6 +783,12 @@ For `console_query`, a schema-qualified column reference such as `orders.total`
|
|
|
753
783
|
|
|
754
784
|
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
785
|
|
|
786
|
+
The internal scope-array defense also uses the connected adapter's dialect and
|
|
787
|
+
MySQL session quote modes when rejecting subqueries and forbidden keywords.
|
|
788
|
+
This protects direct/legacy executor callers; supported tool schemas continue
|
|
789
|
+
to enforce their narrower parameterized scope grammar.
|
|
790
|
+
|
|
791
|
+
|
|
756
792
|
---
|
|
757
793
|
|
|
758
794
|
## Troubleshooting
|
|
@@ -823,7 +859,44 @@ quote and comment rules. MySQL also reads the executing session's `ANSI_QUOTES`
|
|
|
823
859
|
and `NO_BACKSLASH_ESCAPES` settings for validation, protected-column scanning, and
|
|
824
860
|
table gating; adjacent subtraction operators are not assumed to begin a comment.
|
|
825
861
|
Direct scanner callers without session settings use conservative quote-mode scans.
|
|
862
|
+
SQLite read SQL accepts simple ASCII bare, double-quoted, or backtick identifiers
|
|
863
|
+
(letters, digits, and underscores, starting with a letter or underscore).
|
|
864
|
+
Use whitespace after `FROM` and `JOIN`, and use `SELECT` subqueries rather than
|
|
865
|
+
parenthesized table groups. Bracket-quoted and string-quoted names, quoted names
|
|
866
|
+
containing punctuation, and unsupported table-reference syntax are refused before
|
|
867
|
+
execution, because they cannot be reliably checked against the configured table
|
|
868
|
+
policy. Ordinary string literals remain supported. This restriction is part of
|
|
869
|
+
2.0.0.beta4; keep read tools disabled on older versions when this policy is
|
|
870
|
+
needed. Confirm that the release is available before selecting it. The Rails-version
|
|
871
|
+
integration lane checks these boundaries on real SQLite.
|
|
872
|
+
|
|
826
873
|
The contributor live-backend lane exercises these boundaries
|
|
827
874
|
through Console requests against PostgreSQL and MySQL. Keep read tools disabled
|
|
828
875
|
unless live SQL access is needed, and retain the configured blocked-table and
|
|
829
876
|
redaction policies when diagnosing a rejected request.
|
|
877
|
+
|
|
878
|
+
## Console policy corrections in 2.0.0.beta4
|
|
879
|
+
|
|
880
|
+
In `2.0.0.beta4`, the default model-reading tools check the resolved relation
|
|
881
|
+
against `console_blocked_tables` before fetching records or counts. This includes
|
|
882
|
+
application-defined default scopes and the parent lookup for association counts.
|
|
883
|
+
The checked relation is reused for execution so a dynamic default scope is not
|
|
884
|
+
resolved twice. These checks do not change which Console tools are enabled.
|
|
885
|
+
|
|
886
|
+
Response handling redacts protected fields before invoking serializers, converts
|
|
887
|
+
the remaining response to JSON-compatible values, then redacts and scans that
|
|
888
|
+
normalized tree before either JSON or Markdown rendering. Symbol values and custom
|
|
889
|
+
JSON serializers therefore receive the same credential checks as ordinary strings.
|
|
890
|
+
Custom values in Markdown now use their JSON-compatible representation. Numbers,
|
|
891
|
+
booleans, nulls, and ordinary record shapes retain their existing meanings.
|
|
892
|
+
|
|
893
|
+
For SQLite SQL, keyword spellings receive function-policy exceptions only where
|
|
894
|
+
supported query grammar requires them. PostgreSQL reserved-keyword grammar is
|
|
895
|
+
preserved. Unsupported parenthesized offset expressions on other dialects may
|
|
896
|
+
require a plain numeric offset. Keep the configured access and credential policies
|
|
897
|
+
in place when adjusting a query.
|
|
898
|
+
|
|
899
|
+
These corrections require `2.0.0.beta4` or a reviewed development revision that
|
|
900
|
+
contains them. Confirm that a patched release is available before selecting it.
|
|
901
|
+
On affected versions, disable Console where these policies are required; Index MCP
|
|
902
|
+
can stay enabled because it reads the published code index separately.
|