woods 1.6.1 → 2.0.0.beta2
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 +2035 -0
- data/CONTRIBUTING.md +253 -87
- data/README.md +161 -513
- data/SECURITY.md +92 -0
- data/assets/woods-wordmark-white-with-bg.png +0 -0
- data/docs/AGENT_GUIDE.md +204 -0
- data/docs/AGENT_SETUP.md +205 -0
- data/docs/BACKEND_MATRIX.md +470 -0
- data/docs/CONFIGURATION_REFERENCE.md +655 -0
- data/docs/CONSOLE_MCP_SETUP.md +829 -0
- data/docs/DOCKER_SETUP.md +454 -0
- data/docs/EMBEDDING_MODELS.md +136 -0
- data/docs/EVALUATION.md +91 -0
- data/docs/EXTRACTOR_REFERENCE.md +765 -0
- data/docs/FAQ.md +544 -0
- data/docs/GETTING_STARTED.md +183 -0
- data/docs/INCREMENTAL_EXTRACTION.md +455 -0
- data/docs/INTERNALS.md +418 -0
- data/docs/MCP_HTTP_TRANSPORT.md +144 -0
- data/docs/MCP_SERVERS.md +231 -0
- data/docs/MCP_TOOL_COOKBOOK.md +987 -0
- data/docs/MCP_WORKTREE_SETUP.md +127 -0
- data/docs/NOTION_INTEGRATION.md +283 -0
- data/docs/OBSIDIAN_INTEGRATION.md +170 -0
- data/docs/PUBLISHED_INDEX.md +213 -0
- data/docs/README.md +94 -0
- data/docs/RETRIEVAL_GUIDE.md +267 -0
- data/docs/TOKEN_BENCHMARK.md +68 -0
- data/docs/TROUBLESHOOTING.md +841 -0
- data/docs/UNBLOCKED_INTEGRATION.md +279 -0
- data/docs/UPGRADING_TO_2.md +321 -0
- data/docs/WATCH_DAEMON.md +667 -0
- data/docs/WHY_WOODS.md +219 -0
- data/exe/woods-console +40 -4
- data/exe/woods-console-mcp +21 -35
- data/exe/woods-mcp +20 -7
- data/exe/woods-mcp-http +80 -11
- data/exe/woods-mcp-start +57 -52
- data/lib/generators/woods/install_generator.rb +6 -5
- data/lib/generators/woods/pgvector_generator.rb +6 -3
- data/lib/generators/woods/templates/add_pgvector_to_woods.rb.erb +29 -9
- data/lib/generators/woods/templates/create_woods_tables.rb.erb +5 -1
- data/lib/generators/woods/templates/woods.rb.tt +49 -28
- data/lib/tasks/woods.rake +622 -168
- data/lib/tasks/woods_checks.rake +107 -0
- data/lib/tasks/woods_evaluation.rake +164 -80
- data/lib/woods/ast/call_site_extractor.rb +6 -15
- data/lib/woods/ast/method_extractor.rb +19 -9
- data/lib/woods/ast/parser.rb +54 -8
- data/lib/woods/atomic_file.rb +171 -2
- data/lib/woods/builder.rb +310 -22
- data/lib/woods/cache/cache_middleware.rb +7 -2
- data/lib/woods/cache/cache_store.rb +9 -1
- data/lib/woods/cache/solid_cache_store.rb +6 -4
- data/lib/woods/change_set.rb +88 -0
- data/lib/woods/checks/generation_resolution.rb +34 -0
- data/lib/woods/checks/moved_messages.rb +186 -0
- data/lib/woods/chunking/semantic_chunker.rb +160 -18
- data/lib/woods/console/audit_logger.rb +12 -3
- data/lib/woods/console/bridge_protocol.rb +3 -16
- data/lib/woods/console/connection_manager.rb +51 -136
- data/lib/woods/console/dispatch_pipeline.rb +42 -12
- data/lib/woods/console/embedded_executor.rb +806 -149
- data/lib/woods/console/eval_guard.rb +27 -20
- data/lib/woods/console/input_contract.rb +78 -0
- data/lib/woods/console/model_validator.rb +29 -1
- data/lib/woods/console/rack_middleware.rb +65 -42
- data/lib/woods/console/redactor.rb +26 -8
- data/lib/woods/console/safe_context.rb +58 -10
- data/lib/woods/console/scope_predicate_parser.rb +41 -0
- data/lib/woods/console/server.rb +119 -247
- data/lib/woods/console/sql_noise_stripper.rb +125 -16
- data/lib/woods/console/sql_table_scanner.rb +82 -22
- data/lib/woods/console/sql_validator.rb +459 -29
- data/lib/woods/console/table_gate.rb +2 -2
- data/lib/woods/console/tool_specs.rb +463 -90
- data/lib/woods/console/tools/tier1.rb +1 -5
- data/lib/woods/console/tools/tier4.rb +18 -9
- data/lib/woods/coordination/lock_heartbeat.rb +103 -0
- data/lib/woods/coordination/pipeline_lock.rb +263 -53
- data/lib/woods/db/migrations/007_typed_snapshot_units.rb +45 -0
- data/lib/woods/db/migrator.rb +3 -9
- data/lib/woods/db/schema_version.rb +47 -2
- data/lib/woods/dependency_graph.rb +898 -64
- data/lib/woods/embedding/fake.rb +138 -0
- data/lib/woods/embedding/indexer.rb +832 -40
- data/lib/woods/embedding/openai.rb +77 -19
- data/lib/woods/embedding/provider.rb +189 -11
- data/lib/woods/embedding/text_preparer.rb +1 -1
- data/lib/woods/embedding/token_counter.rb +0 -7
- data/lib/woods/evaluation/ablation_agent_payload.rb +38 -0
- data/lib/woods/evaluation/ablation_executor.rb +67 -0
- data/lib/woods/evaluation/ablation_provenance.rb +38 -0
- data/lib/woods/evaluation/ablation_report_writer.rb +43 -0
- data/lib/woods/evaluation/ablation_runner.rb +173 -0
- data/lib/woods/evaluation/ablation_summary.rb +65 -0
- data/lib/woods/evaluation/ablation_task.rb +66 -0
- data/lib/woods/evaluation/ablation_task_set.rb +77 -0
- data/lib/woods/evaluation/ablation_timed_executor.rb +91 -0
- data/lib/woods/evaluation/ablation_worktree.rb +71 -0
- data/lib/woods/evaluation/baseline.rb +60 -0
- data/lib/woods/evaluation/baseline_runner.rb +11 -3
- data/lib/woods/evaluation/evaluator.rb +41 -8
- data/lib/woods/evaluation/query_set.rb +79 -13
- data/lib/woods/evaluation/report_generator.rb +20 -1
- data/lib/woods/export/unit_facts.rb +0 -11
- data/lib/woods/extracted_unit.rb +22 -63
- data/lib/woods/extractor.rb +2783 -238
- data/lib/woods/extractors/action_cable_extractor.rb +9 -4
- data/lib/woods/extractors/ast_source_extraction.rb +20 -2
- data/lib/woods/extractors/caching_extractor.rb +46 -12
- data/lib/woods/extractors/callback_analyzer.rb +39 -9
- data/lib/woods/extractors/component_discovery.rb +123 -0
- data/lib/woods/extractors/concern_extractor.rb +17 -3
- data/lib/woods/extractors/controller_extractor.rb +389 -29
- data/lib/woods/extractors/decorator_extractor.rb +7 -14
- data/lib/woods/extractors/engine_extractor.rb +53 -8
- data/lib/woods/extractors/event_extractor.rb +55 -4
- data/lib/woods/extractors/factory_extractor.rb +49 -11
- data/lib/woods/extractors/graphql_extractor.rb +162 -66
- data/lib/woods/extractors/i18n_extractor.rb +6 -1
- data/lib/woods/extractors/job_extractor.rb +51 -21
- data/lib/woods/extractors/lib_extractor.rb +23 -17
- data/lib/woods/extractors/line_neutralizer.rb +171 -0
- data/lib/woods/extractors/mailer_extractor.rb +9 -1
- data/lib/woods/extractors/manager_extractor.rb +19 -2
- data/lib/woods/extractors/migration_extractor.rb +22 -11
- data/lib/woods/extractors/model_extractor.rb +292 -57
- data/lib/woods/extractors/package_extractor.rb +154 -0
- data/lib/woods/extractors/phlex_extractor.rb +18 -3
- data/lib/woods/extractors/policy_extractor.rb +6 -5
- data/lib/woods/extractors/poro_extractor.rb +13 -14
- data/lib/woods/extractors/pundit_extractor.rb +3 -3
- data/lib/woods/extractors/rails_source_extractor.rb +24 -7
- data/lib/woods/extractors/rake_task_extractor.rb +158 -30
- data/lib/woods/extractors/reference_patterns.rb +38 -0
- data/lib/woods/extractors/route_extractor.rb +58 -2
- data/lib/woods/extractors/scheduled_job_extractor.rb +51 -35
- data/lib/woods/extractors/serializer_extractor.rb +3 -4
- data/lib/woods/extractors/service_extractor.rb +11 -1
- data/lib/woods/extractors/shared_dependency_scanner.rb +24 -34
- data/lib/woods/extractors/shared_utility_methods.rb +36 -6
- data/lib/woods/extractors/source_nesting.rb +560 -0
- data/lib/woods/extractors/state_machine_extractor.rb +30 -18
- data/lib/woods/extractors/test_mapping_extractor.rb +26 -9
- data/lib/woods/extractors/view_component_extractor.rb +28 -3
- data/lib/woods/extractors/view_engines/erb.rb +17 -3
- data/lib/woods/feedback/gap_detector.rb +9 -3
- data/lib/woods/feedback/store.rb +7 -1
- data/lib/woods/filename_utils.rb +29 -1
- data/lib/woods/flow_analysis/operation_extractor.rb +22 -10
- data/lib/woods/flow_assembler.rb +147 -26
- data/lib/woods/flow_document.rb +1 -0
- data/lib/woods/flow_precomputer.rb +175 -22
- data/lib/woods/gem_mapper.rb +285 -0
- data/lib/woods/generation.rb +185 -0
- data/lib/woods/git_command.rb +38 -0
- data/lib/woods/git_provenance.rb +16 -2
- data/lib/woods/graph_analyzer.rb +564 -87
- data/lib/woods/index_artifact.rb +93 -23
- data/lib/woods/mcp/bearer_auth.rb +102 -13
- data/lib/woods/mcp/bootstrap_state.rb +77 -0
- data/lib/woods/mcp/bootstrapper.rb +582 -77
- data/lib/woods/mcp/config_resolver.rb +66 -6
- data/lib/woods/mcp/errors.rb +60 -0
- data/lib/woods/mcp/index_reader.rb +836 -117
- data/lib/woods/mcp/index_reader_pinning.rb +78 -0
- data/lib/woods/mcp/origin_guard.rb +66 -7
- data/lib/woods/mcp/protocol_policy.rb +98 -0
- data/lib/woods/mcp/provider_probe.rb +45 -6
- data/lib/woods/mcp/renderers/markdown_renderer.rb +72 -4
- data/lib/woods/mcp/renderers/plain_renderer.rb +54 -6
- data/lib/woods/mcp/server.rb +898 -152
- data/lib/woods/mcp/tasks/extension.rb +196 -0
- data/lib/woods/mcp/tasks/request_capture.rb +45 -0
- data/lib/woods/mcp/tasks/store.rb +518 -0
- data/lib/woods/mcp/tool_contract.rb +171 -0
- data/lib/woods/mcp/tool_response_renderer.rb +7 -0
- data/lib/woods/model_name_cache.rb +19 -1
- data/lib/woods/notion/client.rb +132 -36
- data/lib/woods/notion/exporter.rb +456 -61
- data/lib/woods/notion/mappers/column_mapper.rb +34 -5
- data/lib/woods/notion/mappers/migration_mapper.rb +32 -8
- data/lib/woods/notion/mappers/model_mapper.rb +21 -6
- data/lib/woods/notion/mappers/shared.rb +45 -3
- data/lib/woods/notion/sync_manifest.rb +258 -0
- data/lib/woods/obsidian/errors.rb +6 -0
- data/lib/woods/obsidian/name_mapper.rb +40 -24
- data/lib/woods/obsidian/vault_exporter.rb +103 -36
- data/lib/woods/operator/pipeline_guard.rb +118 -21
- data/lib/woods/operator/status_reporter.rb +20 -3
- data/lib/woods/path_dispatcher.rb +276 -0
- data/lib/woods/payload_store.rb +236 -0
- data/lib/woods/published_index/edge_shaper.rb +61 -0
- data/lib/woods/published_index/generation_catalog.rb +72 -0
- data/lib/woods/published_index/typed_unit_reader.rb +48 -0
- data/lib/woods/published_index.rb +287 -0
- data/lib/woods/railtie.rb +69 -30
- data/lib/woods/railtie_support.rb +167 -0
- data/lib/woods/release.rb +12 -0
- data/lib/woods/reload_policy.rb +206 -0
- data/lib/woods/resilience/circuit_breaker.rb +47 -8
- data/lib/woods/resilience/index_validator.rb +296 -10
- data/lib/woods/resilience/retryable_provider.rb +71 -6
- data/lib/woods/resolved_config.rb +55 -11
- data/lib/woods/retrieval/context_assembler.rb +132 -40
- data/lib/woods/retrieval/query_classifier.rb +26 -8
- data/lib/woods/retrieval/ranker.rb +193 -28
- data/lib/woods/retrieval/search_executor.rb +206 -39
- data/lib/woods/retriever.rb +317 -71
- data/lib/woods/retry_after.rb +22 -2
- data/lib/woods/ruby_analyzer/class_analyzer.rb +10 -14
- data/lib/woods/ruby_analyzer/fqn_builder.rb +2 -0
- data/lib/woods/ruby_analyzer/mermaid_renderer.rb +14 -4
- data/lib/woods/ruby_analyzer/method_analyzer.rb +1 -1
- data/lib/woods/ruby_analyzer/trace_enricher.rb +3 -0
- data/lib/woods/ruby_analyzer.rb +21 -5
- data/lib/woods/session_tracer/file_store.rb +138 -19
- data/lib/woods/session_tracer/middleware.rb +1 -2
- data/lib/woods/session_tracer/redis_store.rb +122 -12
- data/lib/woods/session_tracer/session_flow_assembler.rb +57 -17
- data/lib/woods/session_tracer/session_flow_document.rb +56 -14
- data/lib/woods/session_tracer/solid_cache_coordination.rb +192 -0
- data/lib/woods/session_tracer/solid_cache_store.rb +560 -91
- data/lib/woods/session_tracer/store.rb +14 -1
- data/lib/woods/storage/metadata_store.rb +230 -26
- data/lib/woods/storage/pgvector.rb +180 -22
- data/lib/woods/storage/qdrant.rb +367 -41
- data/lib/woods/storage/snapshotter/metadata.rb +79 -16
- data/lib/woods/storage/snapshotter/vector.rb +128 -17
- data/lib/woods/storage/snapshotter.rb +23 -5
- data/lib/woods/storage/vector_store.rb +49 -8
- data/lib/woods/storage_identity.rb +28 -0
- data/lib/woods/tasks.rb +53 -2
- data/lib/woods/temporal/json_snapshot_store.rb +112 -42
- data/lib/woods/temporal/snapshot_store.rb +139 -42
- data/lib/woods/unblocked/client.rb +119 -17
- data/lib/woods/unblocked/document_builder.rb +34 -2
- data/lib/woods/unblocked/exporter.rb +63 -27
- data/lib/woods/unblocked/rate_limiter.rb +23 -9
- data/lib/woods/unblocked/sync_manifest.rb +16 -8
- data/lib/woods/update_check.rb +24 -1
- data/lib/woods/util/uuid5.rb +124 -0
- data/lib/woods/version.rb +1 -1
- data/lib/woods/watch/daemon.rb +1345 -0
- data/lib/woods/watch/listen_watcher.rb +81 -0
- data/lib/woods/watch/polling_watcher.rb +137 -0
- data/lib/woods/watch/status.rb +169 -0
- data/lib/woods/watch/tree_scan.rb +163 -0
- data/lib/woods/watch/watcher.rb +100 -0
- data/lib/woods.rb +138 -9
- data/plugin/.claude-plugin/plugin.json +18 -0
- data/plugin/hooks/hooks.json +29 -0
- data/plugin/hooks/woods-post-edit.sh +226 -0
- data/plugin/hooks/woods-session-start.sh +77 -0
- data/plugin/skills/woods-agent-enable/SKILL.md +51 -0
- data/plugin/skills/woods-diagnose/SKILL.md +75 -0
- data/plugin/skills/woods-investigate/SKILL.md +39 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +101 -0
- data/plugin/skills/woods-setup/SKILL.md +99 -0
- metadata +134 -23
- data/lib/woods/console/adapters/cache_adapter.rb +0 -58
- data/lib/woods/console/adapters/good_job_adapter.rb +0 -33
- data/lib/woods/console/adapters/job_adapter.rb +0 -74
- data/lib/woods/console/adapters/sidekiq_adapter.rb +0 -33
- data/lib/woods/console/adapters/solid_queue_adapter.rb +0 -33
- data/lib/woods/console/bridge.rb +0 -210
- data/lib/woods/formatting/claude_adapter.rb +0 -98
- data/lib/woods/formatting/generic_adapter.rb +0 -56
- data/lib/woods/formatting/gpt_adapter.rb +0 -64
- data/lib/woods/notion/mapper.rb +0 -40
- data/lib/woods/observability/health_check.rb +0 -79
- data/lib/woods/observability/instrumentation.rb +0 -34
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
# Docker Setup Guide
|
|
2
|
+
|
|
3
|
+
This guide covers running Woods in a Dockerized Rails application, extraction, MCP server configuration, and troubleshooting.
|
|
4
|
+
|
|
5
|
+
## Architecture Overview
|
|
6
|
+
|
|
7
|
+
Woods extraction requires a booted Rails environment, so it runs inside the application container. The simplest MCP setup runs the Index Server from that same application bundle through Docker stdio. A host-side Index Server is an optional optimization when the host also has a compatible Ruby bundle and can read the published index.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
HOST APPLICATION CONTAINER
|
|
11
|
+
───────────────────────────── ───────────────────────────
|
|
12
|
+
MCP client Rails App + Woods bundle
|
|
13
|
+
docker compose exec -T ────────────▶ woods-mcp /app/tmp/woods
|
|
14
|
+
29 schemas; 14 registered
|
|
15
|
+
reads the published index
|
|
16
|
+
|
|
17
|
+
Extraction commands ─────────────────▶ bundle exec rake woods:extract
|
|
18
|
+
writes /app/tmp/woods/
|
|
19
|
+
|
|
20
|
+
Console Server, two launch paths:
|
|
21
|
+
|
|
22
|
+
Embedded (9 tools) rake woods:console
|
|
23
|
+
MCP client spawns via boots Rails, runs MCP in-process
|
|
24
|
+
docker compose exec -T ────────────▶ Tier 1 read-only tools only
|
|
25
|
+
|
|
26
|
+
Configured launcher (same tools)
|
|
27
|
+
woods-console-mcp on host rake woods:console
|
|
28
|
+
execs docker exec -i ──────────────▶ same embedded server
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The Index Server reads static files and does not boot Rails, even when its process runs in the application container. The Console Server queries live application state and does boot Rails. Treat Console access as a separate security decision.
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
### 1. Add the gem
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
# Gemfile
|
|
39
|
+
group :development do
|
|
40
|
+
gem 'woods', '~> 2.0'
|
|
41
|
+
end
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
docker compose exec app bundle install
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Run the install generator
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
docker compose exec app bundle exec rails generate woods:install
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This creates `config/initializers/woods.rb` with default configuration.
|
|
55
|
+
|
|
56
|
+
### 3. Decide whether to keep the legacy application migration
|
|
57
|
+
|
|
58
|
+
The generator emits `db/migrate/*_create_woods_tables.rb`, but Woods 2's shipped structural index and storage backends do not use those application tables. For a new default installation, remove that generated migration before the next Rails boot. Keep and run it only when deliberately preserving an older/custom integration that uses `woods_units`, `woods_edges`, and `woods_embeddings`, after normal schema-change review.
|
|
59
|
+
|
|
60
|
+
### 4. Configure
|
|
61
|
+
|
|
62
|
+
Edit `config/initializers/woods.rb` inside the container (or on the host if the app directory is volume-mounted):
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
Woods.configure do |config|
|
|
66
|
+
config.output_dir = Rails.root.join('tmp/woods')
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Extraction
|
|
71
|
+
|
|
72
|
+
Run extraction inside the container:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Full extraction
|
|
76
|
+
docker compose exec app bundle exec rake woods:extract
|
|
77
|
+
|
|
78
|
+
# Incremental (changed files only)
|
|
79
|
+
docker compose exec app bundle exec rake woods:incremental
|
|
80
|
+
|
|
81
|
+
# Automatic structural updates while the container is running
|
|
82
|
+
docker compose exec app bundle exec rake woods:watch
|
|
83
|
+
|
|
84
|
+
# Framework/gem sources only
|
|
85
|
+
docker compose exec app bundle exec rake woods:extract_framework
|
|
86
|
+
```
|
|
87
|
+
|
|
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
|
+
|
|
90
|
+
### Index persistence
|
|
91
|
+
|
|
92
|
+
Persist `tmp/woods/` if the index should survive container replacement. A bind mount also makes it available to optional host-side tools:
|
|
93
|
+
|
|
94
|
+
```yaml
|
|
95
|
+
services:
|
|
96
|
+
app:
|
|
97
|
+
volumes:
|
|
98
|
+
- .:/app # Full app mount, output lands at ./tmp/woods/
|
|
99
|
+
# OR mount just the output:
|
|
100
|
+
# - ./tmp/woods:/app/tmp/woods
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Verify the published index
|
|
104
|
+
|
|
105
|
+
Use Woods' generation-aware checks inside the same environment that performed extraction:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
docker compose exec app bundle exec rake woods:validate
|
|
109
|
+
docker compose exec app bundle exec rake woods:stats
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Woods 2 publishes `generation.json`, which points at the active payload manifest. A root-level `manifest.json` is valid only for a legacy layout; do not use its presence as the v2 health check.
|
|
113
|
+
|
|
114
|
+
### Path Translation
|
|
115
|
+
|
|
116
|
+
Use the path visible to the process that actually runs the command:
|
|
117
|
+
|
|
118
|
+
| Context | Path | Example |
|
|
119
|
+
|---------|------|---------|
|
|
120
|
+
| Rake tasks (inside container) | Container path | `/app/tmp/woods` |
|
|
121
|
+
| Index Server through Docker | Container path | `/app/tmp/woods` |
|
|
122
|
+
| Optional host-side Index Server | Host path | `./tmp/woods` or `/home/dev/my-app/tmp/woods` |
|
|
123
|
+
|
|
124
|
+
## Index Server setup
|
|
125
|
+
|
|
126
|
+
### Default: run it through the application container
|
|
127
|
+
|
|
128
|
+
This path needs no Ruby, Bundler, Woods executable, or host-visible index on the host. Compose must be able to resolve the project, so set `cwd` to the host application root. `-T` disables Compose's pseudo-TTY while keeping stdin attached for MCP:
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"mcpServers": {
|
|
133
|
+
"woods": {
|
|
134
|
+
"command": "docker",
|
|
135
|
+
"args": [
|
|
136
|
+
"compose", "exec", "-T", "app",
|
|
137
|
+
"bundle", "exec", "woods-mcp", "/app/tmp/woods"
|
|
138
|
+
],
|
|
139
|
+
"cwd": "/absolute/host/path/to/app"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Replace `app` and `/app` with the Compose service and container path used by the project. The service must already be running. For plain Docker, use the exact container name and interactive stdin:
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"mcpServers": {
|
|
150
|
+
"woods": {
|
|
151
|
+
"command": "docker",
|
|
152
|
+
"args": ["exec", "-i", "my_app_web_1", "bundle", "exec", "woods-mcp", "/app/tmp/woods"]
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Two persistence gotchas:
|
|
159
|
+
|
|
160
|
+
1. **tmpfs wipes the index on container restart.** If `/app/tmp` is tmpfs, re-run `rake woods:extract` after each restart, or persist the index by mounting a named volume at the index path (e.g. `- woods-data:/app/tmp/woods`) or writing the index outside the tmpfs mount.
|
|
161
|
+
2. **Use the container path.** The Docker-launched server needs `/app/tmp/woods`, not the host's `./tmp/woods`.
|
|
162
|
+
|
|
163
|
+
### Optional: run the Index Server on the host
|
|
164
|
+
|
|
165
|
+
Use this only when the application bundle, a supported Ruby, and the Woods executable are installed on the host **and** the index directory is host-visible:
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{
|
|
169
|
+
"mcpServers": {
|
|
170
|
+
"woods": {
|
|
171
|
+
"command": "bundle",
|
|
172
|
+
"args": ["exec", "woods-mcp-start", "./tmp/woods"],
|
|
173
|
+
"cwd": "/absolute/host/path/to/app"
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`woods-mcp-start` validates the directory and active published generation, then replaces itself with `woods-mcp`. It does not install dependencies or restart a failed process.
|
|
180
|
+
|
|
181
|
+
| | Container process (default) | Host process (optional) |
|
|
182
|
+
|---|---|---|
|
|
183
|
+
| **Index location** | Anywhere in the container | Host-visible bind mount |
|
|
184
|
+
| **Path in `.mcp.json`** | Container path (`/app/tmp/woods`) | Host path (`./tmp/woods`) |
|
|
185
|
+
| **Survives container replacement** | Only with a bind/named volume | Yes, on host disk |
|
|
186
|
+
| **Needs Ruby/Woods bundle on host** | No | Yes |
|
|
187
|
+
|
|
188
|
+
## Console Server Setup
|
|
189
|
+
|
|
190
|
+
The Console Server queries live Rails state. There are two launch paths for the same embedded server.
|
|
191
|
+
|
|
192
|
+
Before either path can start, deliberately enable live-data access in the Rails initializer:
|
|
193
|
+
|
|
194
|
+
```ruby
|
|
195
|
+
Woods.configure do |config|
|
|
196
|
+
config.console_mcp_enabled = true
|
|
197
|
+
config.console_mcp_token = ENV["WOODS_CONSOLE_MCP_TOKEN"]
|
|
198
|
+
end
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
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
|
+
|
|
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.
|
|
204
|
+
|
|
205
|
+
### Comparison
|
|
206
|
+
|
|
207
|
+
| | Direct Docker command | Configured launcher |
|
|
208
|
+
|---|---|---|
|
|
209
|
+
| **Where it runs** | Inside container via Docker stdio | `woods-console-mcp` execs `docker exec -i` |
|
|
210
|
+
| **Config needed** | Rails master switch + `.mcp.json` | Rails master switch + `console.yml` + `.mcp.json` |
|
|
211
|
+
| **Tools available** | 9 by default; 11 with read tools enabled | Same 9 or 11 |
|
|
212
|
+
| **Setup complexity** | Minimal | Moderate |
|
|
213
|
+
| **Best for** | Quick setup | Reusable launcher config |
|
|
214
|
+
|
|
215
|
+
### Option 1: Embedded (9 Tier 1 tools)
|
|
216
|
+
|
|
217
|
+
The MCP client spawns a Docker stdio process directly. The container boots Rails and runs the MCP server in-process. Only Tier 1 read-only tools are available (count, sample, find, pluck, aggregate, association_count, schema, recent, status).
|
|
218
|
+
|
|
219
|
+
```json
|
|
220
|
+
{
|
|
221
|
+
"mcpServers": {
|
|
222
|
+
"woods-console": {
|
|
223
|
+
"command": "docker",
|
|
224
|
+
"args": [
|
|
225
|
+
"compose", "exec", "-T", "app",
|
|
226
|
+
"bundle", "exec", "rake", "woods:console"
|
|
227
|
+
],
|
|
228
|
+
"cwd": "/absolute/host/path/to/app"
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
> **Compose uses `-T`; plain Docker uses `-i`.** Compose attaches stdin by default, and `-T` prevents a pseudo-TTY from corrupting MCP framing. Plain `docker exec` needs `-i` to keep stdin attached.
|
|
235
|
+
|
|
236
|
+
If you use `docker exec` (not `docker compose exec`), provide the exact container name:
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
{
|
|
240
|
+
"mcpServers": {
|
|
241
|
+
"woods-console": {
|
|
242
|
+
"command": "docker",
|
|
243
|
+
"args": [
|
|
244
|
+
"exec", "-i", "my_app_web_1",
|
|
245
|
+
"bundle", "exec", "rake", "woods:console"
|
|
246
|
+
]
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Option 2: Configured launcher
|
|
253
|
+
|
|
254
|
+
The `woods-console-mcp` binary runs on the host and replaces itself with
|
|
255
|
+
`docker exec -i ... bundle exec rake woods:console`. It exposes the same
|
|
256
|
+
embedded tool surface as Option 1.
|
|
257
|
+
|
|
258
|
+
This optional path requires Woods and the `woods-console-mcp` executable on
|
|
259
|
+
the host. If Woods is installed only in the application container, use the
|
|
260
|
+
direct Docker command in Option 1. If the host executable comes from the
|
|
261
|
+
application bundle, configure the client with `bundle exec` and an absolute
|
|
262
|
+
host `cwd`, as shown in the [launcher guide](CONSOLE_MCP_SETUP.md#option-d-launcher-wrapper).
|
|
263
|
+
|
|
264
|
+
**Step 1: Create `console.yml`**
|
|
265
|
+
|
|
266
|
+
```yaml
|
|
267
|
+
# ~/.woods/console.yml
|
|
268
|
+
mode: docker
|
|
269
|
+
container: my_app_web_1
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Find your container name with:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
docker ps --format '{{.Names}}'
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
For Docker Compose, names follow the pattern `<project>-<service>-<number>` (e.g., `my_app-app-1`).
|
|
279
|
+
|
|
280
|
+
**Step 2: Configure `.mcp.json`**
|
|
281
|
+
|
|
282
|
+
```json
|
|
283
|
+
{
|
|
284
|
+
"mcpServers": {
|
|
285
|
+
"woods-console": {
|
|
286
|
+
"command": "woods-console-mcp"
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
The launcher reads `~/.woods/console.yml` by default. To use a different path:
|
|
293
|
+
|
|
294
|
+
```json
|
|
295
|
+
{
|
|
296
|
+
"mcpServers": {
|
|
297
|
+
"woods-console": {
|
|
298
|
+
"command": "woods-console-mcp",
|
|
299
|
+
"env": {
|
|
300
|
+
"WOODS_CONSOLE_CONFIG": "/path/to/console.yml"
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Complete `.mcp.json` Example
|
|
308
|
+
|
|
309
|
+
Both servers configured together for a Docker environment, after `config.console_mcp_enabled = true` has been deliberately set:
|
|
310
|
+
|
|
311
|
+
```json
|
|
312
|
+
{
|
|
313
|
+
"mcpServers": {
|
|
314
|
+
"woods": {
|
|
315
|
+
"command": "docker",
|
|
316
|
+
"args": ["compose", "exec", "-T", "app", "bundle", "exec", "woods-mcp", "/app/tmp/woods"],
|
|
317
|
+
"cwd": "/absolute/host/path/to/app"
|
|
318
|
+
},
|
|
319
|
+
"woods-console": {
|
|
320
|
+
"command": "docker",
|
|
321
|
+
"args": [
|
|
322
|
+
"compose", "exec", "-T", "app",
|
|
323
|
+
"bundle", "exec", "rake", "woods:console"
|
|
324
|
+
],
|
|
325
|
+
"cwd": "/absolute/host/path/to/app"
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
This uses the embedded console directly. To use the equivalent configured
|
|
332
|
+
launcher, replace the `woods-console` entry:
|
|
333
|
+
|
|
334
|
+
```json
|
|
335
|
+
{
|
|
336
|
+
"mcpServers": {
|
|
337
|
+
"woods": {
|
|
338
|
+
"command": "docker",
|
|
339
|
+
"args": ["compose", "exec", "-T", "app", "bundle", "exec", "woods-mcp", "/app/tmp/woods"],
|
|
340
|
+
"cwd": "/absolute/host/path/to/app"
|
|
341
|
+
},
|
|
342
|
+
"woods-console": {
|
|
343
|
+
"command": "woods-console-mcp"
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
## Task Reference
|
|
350
|
+
|
|
351
|
+
Which tasks need Docker and which don't:
|
|
352
|
+
|
|
353
|
+
| Task | Needs Rails? | Run via |
|
|
354
|
+
|------|---|---|
|
|
355
|
+
| `woods:extract` | Yes | `docker compose exec app bundle exec rake ...` |
|
|
356
|
+
| `woods:incremental` | Yes | `docker compose exec app bundle exec rake ...` |
|
|
357
|
+
| `woods:watch` | Yes | Dedicated container/process-manager entry |
|
|
358
|
+
| `woods:extract_framework` | Yes | `docker compose exec app bundle exec rake ...` |
|
|
359
|
+
| `woods:embed` | Yes | `docker compose exec app bundle exec rake ...` |
|
|
360
|
+
| `woods:embed_incremental` | Yes | `docker compose exec app bundle exec rake ...` |
|
|
361
|
+
| `woods:console` | Yes | `docker compose exec app bundle exec rake ...` |
|
|
362
|
+
| `woods:flow[EntryPoint]` | Yes | `docker compose exec app bundle exec rake ...` |
|
|
363
|
+
| `woods:notion_sync` | Yes | `docker compose exec app bundle exec rake ...` |
|
|
364
|
+
| `woods:validate` | Yes | `docker compose exec app bundle exec rake ...` |
|
|
365
|
+
| `woods:stats` | Yes | `docker compose exec app bundle exec rake ...` |
|
|
366
|
+
| `woods:clean` | Yes | `docker compose exec app bundle exec rake ...` |
|
|
367
|
+
|
|
368
|
+
## Container Name Discovery
|
|
369
|
+
|
|
370
|
+
Docker Compose generates container names using the pattern `<project>-<service>-<number>`:
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
# List all running containers
|
|
374
|
+
docker ps --format '{{.Names}}'
|
|
375
|
+
|
|
376
|
+
# Filter for your app service
|
|
377
|
+
docker ps --format '{{.Names}}' | grep app
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
The project name defaults to the directory name of the `docker-compose.yml` file. Override it with `COMPOSE_PROJECT_NAME` or the `name:` key in `docker-compose.yml`.
|
|
381
|
+
|
|
382
|
+
## Troubleshooting
|
|
383
|
+
|
|
384
|
+
### Extraction output is not persisted
|
|
385
|
+
|
|
386
|
+
**Symptom:** The index disappears when the application container is replaced.
|
|
387
|
+
|
|
388
|
+
**Fix:** Volume-mount the app directory or the Woods output directory:
|
|
389
|
+
|
|
390
|
+
```yaml
|
|
391
|
+
volumes:
|
|
392
|
+
- .:/app
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Then re-run extraction and verify it inside the container with `woods:validate` and `woods:stats`.
|
|
396
|
+
|
|
397
|
+
### MCP client shows "connection refused" or no tools
|
|
398
|
+
|
|
399
|
+
**Symptom:** The Index or Console server doesn't respond.
|
|
400
|
+
|
|
401
|
+
**Check:**
|
|
402
|
+
1. Container is running: `docker ps`
|
|
403
|
+
2. For Compose stdio, `-T` is present; for plain `docker exec`, `-i` is present
|
|
404
|
+
3. The server path is visible in its execution context: container path for Docker launch, host path for host launch
|
|
405
|
+
|
|
406
|
+
### Incorrect stdio flags
|
|
407
|
+
|
|
408
|
+
**Symptom:** Console server starts but immediately exits, or MCP client reports "broken pipe."
|
|
409
|
+
|
|
410
|
+
**Fix:** Disable Compose's pseudo-TTY, or keep stdin open for plain Docker:
|
|
411
|
+
|
|
412
|
+
```text
|
|
413
|
+
Compose: "args": ["compose", "exec", "-T", "app", ...]
|
|
414
|
+
Docker: "args": ["exec", "-i", "container-name", ...]
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Wrong container name
|
|
418
|
+
|
|
419
|
+
**Symptom:** `Error response from daemon: No such container: ...`
|
|
420
|
+
|
|
421
|
+
**Fix:** Check the actual name with `docker ps --format '{{.Names}}'` and update your `.mcp.json` or `console.yml`.
|
|
422
|
+
|
|
423
|
+
### Path confusion between host and container
|
|
424
|
+
|
|
425
|
+
**Symptom:** Index Server reports "No manifest.json" even though extraction succeeded.
|
|
426
|
+
|
|
427
|
+
**Fix:** Match the path to the server process:
|
|
428
|
+
|
|
429
|
+
```
|
|
430
|
+
Container launch: /app/tmp/woods
|
|
431
|
+
Host launch: /absolute/host/project/tmp/woods
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Rails boot noise breaks MCP protocol
|
|
435
|
+
|
|
436
|
+
**Symptom:** MCP client shows JSON parse errors.
|
|
437
|
+
|
|
438
|
+
**Fix:** The `woods:console` rake task redirects stdout to stderr before Rails boots. If you still see issues, check for `puts` or `print` calls in your initializers that run before the task captures stdout.
|
|
439
|
+
|
|
440
|
+
### A tool from the 31-schema inventory is not listed
|
|
441
|
+
|
|
442
|
+
**Expected behavior.** Supported servers register 9 Tier 1 tools by default.
|
|
443
|
+
Tier 2, Tier 3, and `console_eval` are inventory only.
|
|
444
|
+
|
|
445
|
+
To register `console_sql` and `console_query`, enable
|
|
446
|
+
`console_embedded_read_tools` in Woods configuration or pass
|
|
447
|
+
`embedded_read_tools: true` to the Rack middleware. See
|
|
448
|
+
[CONSOLE_MCP_SETUP.md](CONSOLE_MCP_SETUP.md) for details.
|
|
449
|
+
|
|
450
|
+
### Woods MCP tools not available in a git worktree
|
|
451
|
+
|
|
452
|
+
When working in a git worktree, subagents may not find the woods MCP servers because `.mcp.json` discovery is path-based and the worktree has a different root directory. See [MCP_WORKTREE_SETUP.md](MCP_WORKTREE_SETUP.md) for the fix and verification steps.
|
|
453
|
+
|
|
454
|
+
See [CONSOLE_MCP_SETUP.md](CONSOLE_MCP_SETUP.md) for detailed console server documentation.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Embedding Models
|
|
2
|
+
|
|
3
|
+
Woods supports OpenAI's embedding API and any model served by a local Ollama
|
|
4
|
+
instance. This doc covers the Ollama side, which model to pick, why the
|
|
5
|
+
default is conservative, and how to add a new one.
|
|
6
|
+
|
|
7
|
+
## TL;DR
|
|
8
|
+
|
|
9
|
+
| Model | Native context | Dimensions | Size on disk | Recommended for |
|
|
10
|
+
|---|---|---|---|---|
|
|
11
|
+
| `nomic-embed-text` (default) | 2048 | 768 | 274 MB | General use, small footprint |
|
|
12
|
+
| `bge-m3` | **8192** | 1024 | 1.2 GB | Large Rails units, fewer chunks |
|
|
13
|
+
| `snowflake-arctic-embed2` | 8192 | 1024 | 1.2 GB | Multilingual projects |
|
|
14
|
+
| `mxbai-embed-large` | 512 | 1024 | 670 MB | Short text (tweets, commit msgs) |
|
|
15
|
+
| `all-minilm` | 512 | 384 | 46 MB | Tight-memory environments |
|
|
16
|
+
|
|
17
|
+
The default is `nomic-embed-text` because it's small, fast, and widely available
|
|
18
|
+
through Ollama. Pull it before the first embed with `ollama pull nomic-embed-text`.
|
|
19
|
+
If you're indexing a large Rails codebase and don't mind
|
|
20
|
+
pulling a bigger model, switching to `bge-m3` usually gives you:
|
|
21
|
+
|
|
22
|
+
- **Fewer chunks per unit**: 4× the context means most concern-inlined models
|
|
23
|
+
and service objects fit in a single embedding, which keeps retrieval scores
|
|
24
|
+
cleaner and the vector index smaller.
|
|
25
|
+
- **Stronger retrieval**: `bge-m3` benchmarks meaningfully better than
|
|
26
|
+
`nomic-embed-text` on code-search MTEB tasks.
|
|
27
|
+
|
|
28
|
+
The tradeoff is download size (1.2 GB vs 274 MB), RAM usage during inference,
|
|
29
|
+
and a dimension change (768 → 1024) that requires re-indexing when you switch.
|
|
30
|
+
|
|
31
|
+
## Switching models
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
Woods.configure do |config|
|
|
35
|
+
config.embedding_provider = :ollama
|
|
36
|
+
config.embedding_options = {
|
|
37
|
+
model: 'bge-m3',
|
|
38
|
+
host: 'http://localhost:11434'
|
|
39
|
+
}
|
|
40
|
+
# Everything else, chunker sizing, token counting, num_ctx, is picked
|
|
41
|
+
# up automatically from the model name.
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Pull the model first:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
ollama pull bge-m3
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
If you switch models on an existing install, drop your vector index before
|
|
52
|
+
re-indexing, the embedding dimension change is incompatible with existing
|
|
53
|
+
vectors. Woods raises `Woods::MCP::DimensionMismatch` if you miss it:
|
|
54
|
+
`rake woods:embed` refuses up front on pgvector/Qdrant, and the MCP server
|
|
55
|
+
refuses at boot when a dump's recorded dimension disagrees with the provider.
|
|
56
|
+
|
|
57
|
+
## Why `num_ctx` isn't enough
|
|
58
|
+
|
|
59
|
+
Ollama has a long-running regression (upstream issue
|
|
60
|
+
[ollama/ollama#14186](https://github.com/ollama/ollama/issues/14186)) where
|
|
61
|
+
`options.num_ctx` does **not** lift the effective context ceiling on
|
|
62
|
+
`/api/embed` for models whose native context is smaller than the requested
|
|
63
|
+
override. For `nomic-embed-text` (native 2048) the server rejects inputs above
|
|
64
|
+
that with a `400 "the input length exceeds the context length"` regardless of
|
|
65
|
+
`num_ctx`.
|
|
66
|
+
|
|
67
|
+
Woods works around this two ways:
|
|
68
|
+
|
|
69
|
+
1. **Advertise the native ceiling, not the override.** `Provider::Ollama` keeps
|
|
70
|
+
a model → native-context registry (`MODEL_CONTEXT_LENGTHS`) so the chunker
|
|
71
|
+
sizes inputs to what Ollama will actually accept. `num_ctx` is still passed
|
|
72
|
+
through the request body in case the regression is fixed upstream, but
|
|
73
|
+
nothing relies on it.
|
|
74
|
+
2. **Verify client-side with the real tokenizer.** When the
|
|
75
|
+
[`tokenizers`](https://github.com/ankane/tokenizers-ruby) gem is installed,
|
|
76
|
+
`Embedding::TokenCounter` loads the `bert-base-uncased` WordPiece tokenizer
|
|
77
|
+
(the one every BERT-family embedding model is built on) and the chunker
|
|
78
|
+
re-verifies every slice. WordPiece fragments CamelCase and `::` separators
|
|
79
|
+
differently than character-based estimation suggests, this verification is
|
|
80
|
+
what catches the 10–20 % gap between our estimate and Ollama's internal
|
|
81
|
+
count.
|
|
82
|
+
|
|
83
|
+
## Adding a new model to the registry
|
|
84
|
+
|
|
85
|
+
If you want Woods to auto-pick `num_ctx` for a model we don't ship support for:
|
|
86
|
+
|
|
87
|
+
1. Pull the model: `ollama pull your-model`.
|
|
88
|
+
2. Read its native context:
|
|
89
|
+
```bash
|
|
90
|
+
curl -s http://localhost:11434/api/show -d '{"model":"your-model"}' \
|
|
91
|
+
| jq '.model_info | to_entries[] | select(.key | contains("context_length"))'
|
|
92
|
+
```
|
|
93
|
+
3. Verify the server actually honours it (some models report a larger context
|
|
94
|
+
than the server enforces, see the nomic-embed-text case above). Send a
|
|
95
|
+
request with a known-large input and check for 400s:
|
|
96
|
+
```bash
|
|
97
|
+
curl -s http://localhost:11434/api/embed \
|
|
98
|
+
-d "{\"model\":\"your-model\",\"input\":\"$(ruby -e 'print "word " * 8500')\"}" \
|
|
99
|
+
| jq '.error // "no error, context held"'
|
|
100
|
+
```
|
|
101
|
+
4. Add it to `MODEL_CONTEXT_LENGTHS` in `lib/woods/embedding/provider.rb`
|
|
102
|
+
(keep the value at the **enforced** ceiling, not the advertised one).
|
|
103
|
+
|
|
104
|
+
## When to stay on `nomic-embed-text`
|
|
105
|
+
|
|
106
|
+
- Fresh installs, CI, evaluation environments, no extra pull step.
|
|
107
|
+
- Small/toy codebases where the 2048-token ceiling isn't a real constraint.
|
|
108
|
+
- Memory-constrained Ollama hosts (the 1.2 GB `bge-m3` weights vs 274 MB for
|
|
109
|
+
`nomic-embed-text` can matter on a shared laptop).
|
|
110
|
+
|
|
111
|
+
## When to switch to `bge-m3` or `snowflake-arctic-embed2`
|
|
112
|
+
|
|
113
|
+
- Indexing a real-world Rails app where concern-inlined models or long service
|
|
114
|
+
objects routinely exceed 2048 tokens.
|
|
115
|
+
- You already pay the `tokenizers` gem install cost and want the tighter
|
|
116
|
+
coupling between client-side verification and server-side enforcement.
|
|
117
|
+
- Multilingual content (Arctic Embed 2 is BGE M3 with a stronger non-English
|
|
118
|
+
story).
|
|
119
|
+
|
|
120
|
+
## Deterministic fake provider (no model at all)
|
|
121
|
+
|
|
122
|
+
For CI, sandboxes, and offline hosts where neither OpenAI nor Ollama is
|
|
123
|
+
reachable, `config.embedding_provider = :fake` wires
|
|
124
|
+
`Woods::Embedding::Provider::Fake`, deterministic bag-of-words hashing with
|
|
125
|
+
L2 normalization, no network endpoint, configurable dimension
|
|
126
|
+
(`embedding_options = { dims: 128 }`). Cosine similarity stays mechanically
|
|
127
|
+
meaningful (shared vocabulary ranks closer), but the vectors are **not
|
|
128
|
+
semantically meaningful embeddings**: use it to smoke-test the
|
|
129
|
+
embed → store → retrieve pipeline, never for production retrieval quality.
|
|
130
|
+
See [CONFIGURATION_REFERENCE.md](./CONFIGURATION_REFERENCE.md#fake-embeddings-ci--sandboxes--offline-hosts).
|
|
131
|
+
|
|
132
|
+
## Related
|
|
133
|
+
|
|
134
|
+
- [CONFIGURATION_REFERENCE.md](./CONFIGURATION_REFERENCE.md): full config surface
|
|
135
|
+
- [TOKEN_BENCHMARK.md](./TOKEN_BENCHMARK.md): where our chars/token numbers come from
|
|
136
|
+
- [BACKEND_MATRIX.md](./BACKEND_MATRIX.md): picking a vector store that matches
|
data/docs/EVALUATION.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Evaluating Woods
|
|
2
|
+
|
|
3
|
+
Three harnesses, three questions.
|
|
4
|
+
|
|
5
|
+
| Task | Question it answers | Command |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| Retrieval evaluation | Does `codebase_retrieve` return the right units for a query? | `bin/rails woods:evaluate` |
|
|
8
|
+
| Baseline comparison | How does a naive strategy score on the same queries? | `bin/rails "woods:evaluate:baseline[grep]"` |
|
|
9
|
+
| Agent-level ablation | Does an agent resolve real tasks better, or cheaper, with the index on? | `bin/rails "woods:evaluate:ablation[config/eval_ablation.json]"` |
|
|
10
|
+
|
|
11
|
+
## Retrieval evaluation
|
|
12
|
+
|
|
13
|
+
`woods:evaluate` runs a ground-truth query set (`config/eval_queries.json`, `EVAL_QUERY_SET`) through the retrieval pipeline and reports precision at 5 and 10, recall, MRR, context completeness, and token efficiency. `EVAL_BASELINE_FILE` (a versioned thresholds file, see `Woods::Evaluation::Baseline`) turns it into a gate; `EVAL_MIN_PRECISION`, `EVAL_MIN_RECALL`, and `EVAL_MIN_MRR` override single thresholds. It needs an embedded index; set `embedding_provider = :fake` for an offline run. Output: `tmp/eval_report.json` (`EVAL_OUTPUT`).
|
|
14
|
+
|
|
15
|
+
`woods:evaluate:baseline[grep|random|file_level]` scores a naive strategy on the same query set for comparison.
|
|
16
|
+
|
|
17
|
+
## Agent-level ablation
|
|
18
|
+
|
|
19
|
+
Retrieval scores say whether the right units come back. They do not say whether an agent finishes the job. The ablation runs the same task set twice per task, once with the Woods MCP server available and once without, and compares resolution rate, tokens, cost, and turns.
|
|
20
|
+
|
|
21
|
+
**This is a harness for collecting paired on/off runs, not a source of causal evidence.** Sample size, task selection, and agent nondeterminism all bear on what a result means. Treat it as one input, alongside the retrieval evaluation and manual review, not as a controlled experiment on its own. It is the on/off comparison "Code Isn't Memory" (arXiv 2606.22417, June 2026) reported, where gains concentrated in multi-file changes.
|
|
22
|
+
|
|
23
|
+
### Disposable checkouts
|
|
24
|
+
|
|
25
|
+
Every trial (one task, one condition) runs in its own disposable `git worktree` checkout, created from a single baseline SHA resolved once at the start of the run (the current `HEAD` of the application repository), never in the caller's own working directory:
|
|
26
|
+
|
|
27
|
+
1. `git worktree add --detach <tmp-checkout> <baseline-sha>`
|
|
28
|
+
2. the task set's optional `reset` command, inside the checkout
|
|
29
|
+
3. the agent command, inside the checkout
|
|
30
|
+
4. the task's `check` command, inside the checkout
|
|
31
|
+
5. `git worktree remove --force <tmp-checkout>`
|
|
32
|
+
|
|
33
|
+
Both conditions for a task start from the identical commit, so a difference between them is not confounded by one trial running against a dirtier tree than the other. A `reset` failure aborts the trial and is counted as an error; it is never silently ignored. The checkout is removed whether the trial succeeded, failed, or timed out.
|
|
34
|
+
|
|
35
|
+
### Provenance
|
|
36
|
+
|
|
37
|
+
Every result carries the agent command that ran, the model the agent's JSON payload reported (when present), the MCP wiring in effect (`--mcp-config <path>` or `--strict-mcp-config`), the Woods generation number found in the checkout's `tmp/woods/generation.json` (when present), and the baseline SHA the checkout came from. Compare results only across runs sharing the same baseline SHA and Woods generation.
|
|
38
|
+
|
|
39
|
+
### Woods availability preflight
|
|
40
|
+
|
|
41
|
+
Before a trial runs, the harness checks that Woods is actually available or actually absent, distinguishing "MCP enabled" (the agent command is wired to reach the Woods MCP server) from "index present" (an index exists on disk in the checkout):
|
|
42
|
+
|
|
43
|
+
- `on` requires both: an `--mcp-config` reference in the agent command, and a materialized index in the checkout.
|
|
44
|
+
- `off` requires confirmation that MCP is truly unreachable: `--strict-mcp-config` present, and no `--mcp-config` reference at all. Both flags together still fail, since `--mcp-config` wires the agent to Woods regardless of strict mode.
|
|
45
|
+
|
|
46
|
+
A failed check aborts the trial and is counted as an error, the same as a reset failure. The default probe is injectable (`woods_probe:` on `AblationRunner.new`) for a host app with a different wiring convention.
|
|
47
|
+
|
|
48
|
+
### Task set
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"schema_version": 1,
|
|
53
|
+
"agent_on": "claude -p {prompt} --output-format json --mcp-config .mcp.json",
|
|
54
|
+
"agent_off": "claude -p {prompt} --output-format json --strict-mcp-config",
|
|
55
|
+
"reset": "git checkout -- . && git clean -fdq",
|
|
56
|
+
"tasks": [
|
|
57
|
+
{ "id": "comment-count", "prompt": "Add a comments_count counter cache to Post.", "check": "bin/rspec spec/models/post_spec.rb" }
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| Field | Meaning |
|
|
63
|
+
|---|---|
|
|
64
|
+
| `agent_on`, `agent_off` | Shell command templates; `{prompt}` is replaced with the shell-escaped prompt. The agent must print one JSON object on stdout in the `claude -p --output-format json` shape (`usage`, `total_cost_usd`, `num_turns`, `duration_ms`, optionally `model`). `EVAL_AGENT_ON` and `EVAL_AGENT_OFF` override them. |
|
|
65
|
+
| `reset` | Optional command run inside each trial's disposable checkout before the agent runs, so both conditions start from the same tree |
|
|
66
|
+
| `tasks[].check` | Command whose exit status decides resolution (a spec file, a script, `bin/rails test`) |
|
|
67
|
+
| `tasks[].workdir` | Relative to the checkout root; default `.` |
|
|
68
|
+
|
|
69
|
+
`spec/fixtures/evaluation_ablation_tasks.example.json` is a format fixture only.
|
|
70
|
+
|
|
71
|
+
### Running it
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
bin/rails woods:extract # index the tree under test
|
|
75
|
+
bin/rails "woods:evaluate:ablation[config/eval_ablation.json]"
|
|
76
|
+
# EVAL_ABLATION_OUTPUT=tmp/eval_ablation.json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`woods:evaluate:ablation` never boots Rails: it shells out to the agent command and the task set's own `check` commands, and never touches `Rails.application`.
|
|
80
|
+
|
|
81
|
+
The report holds every attempt (`task_id`, `condition`, `resolved`, `total_tokens`, `cost_usd`, `turns`, `duration_ms`, `error`, `provenance`) and a summary per condition (`resolution_rate`, `mean_tokens`, `mean_cost_usd`, `mean_turns`, `tasks`, `errors`) plus a `delta` (on minus off, present only when both conditions ran). Tokens are the sum of input, output, cache-creation, and cache-read tokens. `mean_tokens` is `null` when every trial in that condition errored before the agent produced JSON.
|
|
82
|
+
|
|
83
|
+
A timeout (`AblationRunner.new(..., timeout: seconds)`, default 600) bounds every command a trial runs, applied independently to each one rather than as a single budget shared across the trial: worktree add/remove, the optional `reset`, the agent invocation, and the check. A timed-out `reset` or `check` counts as an error the same way a timed-out agent invocation does. When the default subprocess executor is in use, a timed-out command is terminated (`TERM`, then `KILL` if still alive after a short grace period) so it never outlives the trial that started it.
|
|
84
|
+
|
|
85
|
+
### Where to get tasks
|
|
86
|
+
|
|
87
|
+
The Rails Foundation's "Agents on Rails" benchmark (announced 2026-08-13, built by Evil Martians) is a ready-made task set once its tasks and checks are checked into a host app. Any set of tasks with a deterministic `check` works.
|
|
88
|
+
|
|
89
|
+
### Reading the numbers
|
|
90
|
+
|
|
91
|
+
A negative `delta.mean_tokens` with an equal or higher `delta.resolution_rate` is the result that sells the index, on the tasks actually run. It does not, by itself, establish that the index causes the difference: run at least ten tasks, two conditions on five tasks is noise, and remember that agent runs are not perfectly reproducible even at a fixed baseline SHA. Keep the baseline SHA and Woods generation fixed across a comparison (both are recorded per result), and do not let the `on` agent run `woods:extract` mid-task.
|