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.
Files changed (274) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2035 -0
  3. data/CONTRIBUTING.md +253 -87
  4. data/README.md +161 -513
  5. data/SECURITY.md +92 -0
  6. data/assets/woods-wordmark-white-with-bg.png +0 -0
  7. data/docs/AGENT_GUIDE.md +204 -0
  8. data/docs/AGENT_SETUP.md +205 -0
  9. data/docs/BACKEND_MATRIX.md +470 -0
  10. data/docs/CONFIGURATION_REFERENCE.md +655 -0
  11. data/docs/CONSOLE_MCP_SETUP.md +829 -0
  12. data/docs/DOCKER_SETUP.md +454 -0
  13. data/docs/EMBEDDING_MODELS.md +136 -0
  14. data/docs/EVALUATION.md +91 -0
  15. data/docs/EXTRACTOR_REFERENCE.md +765 -0
  16. data/docs/FAQ.md +544 -0
  17. data/docs/GETTING_STARTED.md +183 -0
  18. data/docs/INCREMENTAL_EXTRACTION.md +455 -0
  19. data/docs/INTERNALS.md +418 -0
  20. data/docs/MCP_HTTP_TRANSPORT.md +144 -0
  21. data/docs/MCP_SERVERS.md +231 -0
  22. data/docs/MCP_TOOL_COOKBOOK.md +987 -0
  23. data/docs/MCP_WORKTREE_SETUP.md +127 -0
  24. data/docs/NOTION_INTEGRATION.md +283 -0
  25. data/docs/OBSIDIAN_INTEGRATION.md +170 -0
  26. data/docs/PUBLISHED_INDEX.md +213 -0
  27. data/docs/README.md +94 -0
  28. data/docs/RETRIEVAL_GUIDE.md +267 -0
  29. data/docs/TOKEN_BENCHMARK.md +68 -0
  30. data/docs/TROUBLESHOOTING.md +841 -0
  31. data/docs/UNBLOCKED_INTEGRATION.md +279 -0
  32. data/docs/UPGRADING_TO_2.md +321 -0
  33. data/docs/WATCH_DAEMON.md +667 -0
  34. data/docs/WHY_WOODS.md +219 -0
  35. data/exe/woods-console +40 -4
  36. data/exe/woods-console-mcp +21 -35
  37. data/exe/woods-mcp +20 -7
  38. data/exe/woods-mcp-http +80 -11
  39. data/exe/woods-mcp-start +57 -52
  40. data/lib/generators/woods/install_generator.rb +6 -5
  41. data/lib/generators/woods/pgvector_generator.rb +6 -3
  42. data/lib/generators/woods/templates/add_pgvector_to_woods.rb.erb +29 -9
  43. data/lib/generators/woods/templates/create_woods_tables.rb.erb +5 -1
  44. data/lib/generators/woods/templates/woods.rb.tt +49 -28
  45. data/lib/tasks/woods.rake +622 -168
  46. data/lib/tasks/woods_checks.rake +107 -0
  47. data/lib/tasks/woods_evaluation.rake +164 -80
  48. data/lib/woods/ast/call_site_extractor.rb +6 -15
  49. data/lib/woods/ast/method_extractor.rb +19 -9
  50. data/lib/woods/ast/parser.rb +54 -8
  51. data/lib/woods/atomic_file.rb +171 -2
  52. data/lib/woods/builder.rb +310 -22
  53. data/lib/woods/cache/cache_middleware.rb +7 -2
  54. data/lib/woods/cache/cache_store.rb +9 -1
  55. data/lib/woods/cache/solid_cache_store.rb +6 -4
  56. data/lib/woods/change_set.rb +88 -0
  57. data/lib/woods/checks/generation_resolution.rb +34 -0
  58. data/lib/woods/checks/moved_messages.rb +186 -0
  59. data/lib/woods/chunking/semantic_chunker.rb +160 -18
  60. data/lib/woods/console/audit_logger.rb +12 -3
  61. data/lib/woods/console/bridge_protocol.rb +3 -16
  62. data/lib/woods/console/connection_manager.rb +51 -136
  63. data/lib/woods/console/dispatch_pipeline.rb +42 -12
  64. data/lib/woods/console/embedded_executor.rb +806 -149
  65. data/lib/woods/console/eval_guard.rb +27 -20
  66. data/lib/woods/console/input_contract.rb +78 -0
  67. data/lib/woods/console/model_validator.rb +29 -1
  68. data/lib/woods/console/rack_middleware.rb +65 -42
  69. data/lib/woods/console/redactor.rb +26 -8
  70. data/lib/woods/console/safe_context.rb +58 -10
  71. data/lib/woods/console/scope_predicate_parser.rb +41 -0
  72. data/lib/woods/console/server.rb +119 -247
  73. data/lib/woods/console/sql_noise_stripper.rb +125 -16
  74. data/lib/woods/console/sql_table_scanner.rb +82 -22
  75. data/lib/woods/console/sql_validator.rb +459 -29
  76. data/lib/woods/console/table_gate.rb +2 -2
  77. data/lib/woods/console/tool_specs.rb +463 -90
  78. data/lib/woods/console/tools/tier1.rb +1 -5
  79. data/lib/woods/console/tools/tier4.rb +18 -9
  80. data/lib/woods/coordination/lock_heartbeat.rb +103 -0
  81. data/lib/woods/coordination/pipeline_lock.rb +263 -53
  82. data/lib/woods/db/migrations/007_typed_snapshot_units.rb +45 -0
  83. data/lib/woods/db/migrator.rb +3 -9
  84. data/lib/woods/db/schema_version.rb +47 -2
  85. data/lib/woods/dependency_graph.rb +898 -64
  86. data/lib/woods/embedding/fake.rb +138 -0
  87. data/lib/woods/embedding/indexer.rb +832 -40
  88. data/lib/woods/embedding/openai.rb +77 -19
  89. data/lib/woods/embedding/provider.rb +189 -11
  90. data/lib/woods/embedding/text_preparer.rb +1 -1
  91. data/lib/woods/embedding/token_counter.rb +0 -7
  92. data/lib/woods/evaluation/ablation_agent_payload.rb +38 -0
  93. data/lib/woods/evaluation/ablation_executor.rb +67 -0
  94. data/lib/woods/evaluation/ablation_provenance.rb +38 -0
  95. data/lib/woods/evaluation/ablation_report_writer.rb +43 -0
  96. data/lib/woods/evaluation/ablation_runner.rb +173 -0
  97. data/lib/woods/evaluation/ablation_summary.rb +65 -0
  98. data/lib/woods/evaluation/ablation_task.rb +66 -0
  99. data/lib/woods/evaluation/ablation_task_set.rb +77 -0
  100. data/lib/woods/evaluation/ablation_timed_executor.rb +91 -0
  101. data/lib/woods/evaluation/ablation_worktree.rb +71 -0
  102. data/lib/woods/evaluation/baseline.rb +60 -0
  103. data/lib/woods/evaluation/baseline_runner.rb +11 -3
  104. data/lib/woods/evaluation/evaluator.rb +41 -8
  105. data/lib/woods/evaluation/query_set.rb +79 -13
  106. data/lib/woods/evaluation/report_generator.rb +20 -1
  107. data/lib/woods/export/unit_facts.rb +0 -11
  108. data/lib/woods/extracted_unit.rb +22 -63
  109. data/lib/woods/extractor.rb +2783 -238
  110. data/lib/woods/extractors/action_cable_extractor.rb +9 -4
  111. data/lib/woods/extractors/ast_source_extraction.rb +20 -2
  112. data/lib/woods/extractors/caching_extractor.rb +46 -12
  113. data/lib/woods/extractors/callback_analyzer.rb +39 -9
  114. data/lib/woods/extractors/component_discovery.rb +123 -0
  115. data/lib/woods/extractors/concern_extractor.rb +17 -3
  116. data/lib/woods/extractors/controller_extractor.rb +389 -29
  117. data/lib/woods/extractors/decorator_extractor.rb +7 -14
  118. data/lib/woods/extractors/engine_extractor.rb +53 -8
  119. data/lib/woods/extractors/event_extractor.rb +55 -4
  120. data/lib/woods/extractors/factory_extractor.rb +49 -11
  121. data/lib/woods/extractors/graphql_extractor.rb +162 -66
  122. data/lib/woods/extractors/i18n_extractor.rb +6 -1
  123. data/lib/woods/extractors/job_extractor.rb +51 -21
  124. data/lib/woods/extractors/lib_extractor.rb +23 -17
  125. data/lib/woods/extractors/line_neutralizer.rb +171 -0
  126. data/lib/woods/extractors/mailer_extractor.rb +9 -1
  127. data/lib/woods/extractors/manager_extractor.rb +19 -2
  128. data/lib/woods/extractors/migration_extractor.rb +22 -11
  129. data/lib/woods/extractors/model_extractor.rb +292 -57
  130. data/lib/woods/extractors/package_extractor.rb +154 -0
  131. data/lib/woods/extractors/phlex_extractor.rb +18 -3
  132. data/lib/woods/extractors/policy_extractor.rb +6 -5
  133. data/lib/woods/extractors/poro_extractor.rb +13 -14
  134. data/lib/woods/extractors/pundit_extractor.rb +3 -3
  135. data/lib/woods/extractors/rails_source_extractor.rb +24 -7
  136. data/lib/woods/extractors/rake_task_extractor.rb +158 -30
  137. data/lib/woods/extractors/reference_patterns.rb +38 -0
  138. data/lib/woods/extractors/route_extractor.rb +58 -2
  139. data/lib/woods/extractors/scheduled_job_extractor.rb +51 -35
  140. data/lib/woods/extractors/serializer_extractor.rb +3 -4
  141. data/lib/woods/extractors/service_extractor.rb +11 -1
  142. data/lib/woods/extractors/shared_dependency_scanner.rb +24 -34
  143. data/lib/woods/extractors/shared_utility_methods.rb +36 -6
  144. data/lib/woods/extractors/source_nesting.rb +560 -0
  145. data/lib/woods/extractors/state_machine_extractor.rb +30 -18
  146. data/lib/woods/extractors/test_mapping_extractor.rb +26 -9
  147. data/lib/woods/extractors/view_component_extractor.rb +28 -3
  148. data/lib/woods/extractors/view_engines/erb.rb +17 -3
  149. data/lib/woods/feedback/gap_detector.rb +9 -3
  150. data/lib/woods/feedback/store.rb +7 -1
  151. data/lib/woods/filename_utils.rb +29 -1
  152. data/lib/woods/flow_analysis/operation_extractor.rb +22 -10
  153. data/lib/woods/flow_assembler.rb +147 -26
  154. data/lib/woods/flow_document.rb +1 -0
  155. data/lib/woods/flow_precomputer.rb +175 -22
  156. data/lib/woods/gem_mapper.rb +285 -0
  157. data/lib/woods/generation.rb +185 -0
  158. data/lib/woods/git_command.rb +38 -0
  159. data/lib/woods/git_provenance.rb +16 -2
  160. data/lib/woods/graph_analyzer.rb +564 -87
  161. data/lib/woods/index_artifact.rb +93 -23
  162. data/lib/woods/mcp/bearer_auth.rb +102 -13
  163. data/lib/woods/mcp/bootstrap_state.rb +77 -0
  164. data/lib/woods/mcp/bootstrapper.rb +582 -77
  165. data/lib/woods/mcp/config_resolver.rb +66 -6
  166. data/lib/woods/mcp/errors.rb +60 -0
  167. data/lib/woods/mcp/index_reader.rb +836 -117
  168. data/lib/woods/mcp/index_reader_pinning.rb +78 -0
  169. data/lib/woods/mcp/origin_guard.rb +66 -7
  170. data/lib/woods/mcp/protocol_policy.rb +98 -0
  171. data/lib/woods/mcp/provider_probe.rb +45 -6
  172. data/lib/woods/mcp/renderers/markdown_renderer.rb +72 -4
  173. data/lib/woods/mcp/renderers/plain_renderer.rb +54 -6
  174. data/lib/woods/mcp/server.rb +898 -152
  175. data/lib/woods/mcp/tasks/extension.rb +196 -0
  176. data/lib/woods/mcp/tasks/request_capture.rb +45 -0
  177. data/lib/woods/mcp/tasks/store.rb +518 -0
  178. data/lib/woods/mcp/tool_contract.rb +171 -0
  179. data/lib/woods/mcp/tool_response_renderer.rb +7 -0
  180. data/lib/woods/model_name_cache.rb +19 -1
  181. data/lib/woods/notion/client.rb +132 -36
  182. data/lib/woods/notion/exporter.rb +456 -61
  183. data/lib/woods/notion/mappers/column_mapper.rb +34 -5
  184. data/lib/woods/notion/mappers/migration_mapper.rb +32 -8
  185. data/lib/woods/notion/mappers/model_mapper.rb +21 -6
  186. data/lib/woods/notion/mappers/shared.rb +45 -3
  187. data/lib/woods/notion/sync_manifest.rb +258 -0
  188. data/lib/woods/obsidian/errors.rb +6 -0
  189. data/lib/woods/obsidian/name_mapper.rb +40 -24
  190. data/lib/woods/obsidian/vault_exporter.rb +103 -36
  191. data/lib/woods/operator/pipeline_guard.rb +118 -21
  192. data/lib/woods/operator/status_reporter.rb +20 -3
  193. data/lib/woods/path_dispatcher.rb +276 -0
  194. data/lib/woods/payload_store.rb +236 -0
  195. data/lib/woods/published_index/edge_shaper.rb +61 -0
  196. data/lib/woods/published_index/generation_catalog.rb +72 -0
  197. data/lib/woods/published_index/typed_unit_reader.rb +48 -0
  198. data/lib/woods/published_index.rb +287 -0
  199. data/lib/woods/railtie.rb +69 -30
  200. data/lib/woods/railtie_support.rb +167 -0
  201. data/lib/woods/release.rb +12 -0
  202. data/lib/woods/reload_policy.rb +206 -0
  203. data/lib/woods/resilience/circuit_breaker.rb +47 -8
  204. data/lib/woods/resilience/index_validator.rb +296 -10
  205. data/lib/woods/resilience/retryable_provider.rb +71 -6
  206. data/lib/woods/resolved_config.rb +55 -11
  207. data/lib/woods/retrieval/context_assembler.rb +132 -40
  208. data/lib/woods/retrieval/query_classifier.rb +26 -8
  209. data/lib/woods/retrieval/ranker.rb +193 -28
  210. data/lib/woods/retrieval/search_executor.rb +206 -39
  211. data/lib/woods/retriever.rb +317 -71
  212. data/lib/woods/retry_after.rb +22 -2
  213. data/lib/woods/ruby_analyzer/class_analyzer.rb +10 -14
  214. data/lib/woods/ruby_analyzer/fqn_builder.rb +2 -0
  215. data/lib/woods/ruby_analyzer/mermaid_renderer.rb +14 -4
  216. data/lib/woods/ruby_analyzer/method_analyzer.rb +1 -1
  217. data/lib/woods/ruby_analyzer/trace_enricher.rb +3 -0
  218. data/lib/woods/ruby_analyzer.rb +21 -5
  219. data/lib/woods/session_tracer/file_store.rb +138 -19
  220. data/lib/woods/session_tracer/middleware.rb +1 -2
  221. data/lib/woods/session_tracer/redis_store.rb +122 -12
  222. data/lib/woods/session_tracer/session_flow_assembler.rb +57 -17
  223. data/lib/woods/session_tracer/session_flow_document.rb +56 -14
  224. data/lib/woods/session_tracer/solid_cache_coordination.rb +192 -0
  225. data/lib/woods/session_tracer/solid_cache_store.rb +560 -91
  226. data/lib/woods/session_tracer/store.rb +14 -1
  227. data/lib/woods/storage/metadata_store.rb +230 -26
  228. data/lib/woods/storage/pgvector.rb +180 -22
  229. data/lib/woods/storage/qdrant.rb +367 -41
  230. data/lib/woods/storage/snapshotter/metadata.rb +79 -16
  231. data/lib/woods/storage/snapshotter/vector.rb +128 -17
  232. data/lib/woods/storage/snapshotter.rb +23 -5
  233. data/lib/woods/storage/vector_store.rb +49 -8
  234. data/lib/woods/storage_identity.rb +28 -0
  235. data/lib/woods/tasks.rb +53 -2
  236. data/lib/woods/temporal/json_snapshot_store.rb +112 -42
  237. data/lib/woods/temporal/snapshot_store.rb +139 -42
  238. data/lib/woods/unblocked/client.rb +119 -17
  239. data/lib/woods/unblocked/document_builder.rb +34 -2
  240. data/lib/woods/unblocked/exporter.rb +63 -27
  241. data/lib/woods/unblocked/rate_limiter.rb +23 -9
  242. data/lib/woods/unblocked/sync_manifest.rb +16 -8
  243. data/lib/woods/update_check.rb +24 -1
  244. data/lib/woods/util/uuid5.rb +124 -0
  245. data/lib/woods/version.rb +1 -1
  246. data/lib/woods/watch/daemon.rb +1345 -0
  247. data/lib/woods/watch/listen_watcher.rb +81 -0
  248. data/lib/woods/watch/polling_watcher.rb +137 -0
  249. data/lib/woods/watch/status.rb +169 -0
  250. data/lib/woods/watch/tree_scan.rb +163 -0
  251. data/lib/woods/watch/watcher.rb +100 -0
  252. data/lib/woods.rb +138 -9
  253. data/plugin/.claude-plugin/plugin.json +18 -0
  254. data/plugin/hooks/hooks.json +29 -0
  255. data/plugin/hooks/woods-post-edit.sh +226 -0
  256. data/plugin/hooks/woods-session-start.sh +77 -0
  257. data/plugin/skills/woods-agent-enable/SKILL.md +51 -0
  258. data/plugin/skills/woods-diagnose/SKILL.md +75 -0
  259. data/plugin/skills/woods-investigate/SKILL.md +39 -0
  260. data/plugin/skills/woods-mcp-config/SKILL.md +101 -0
  261. data/plugin/skills/woods-setup/SKILL.md +99 -0
  262. metadata +134 -23
  263. data/lib/woods/console/adapters/cache_adapter.rb +0 -58
  264. data/lib/woods/console/adapters/good_job_adapter.rb +0 -33
  265. data/lib/woods/console/adapters/job_adapter.rb +0 -74
  266. data/lib/woods/console/adapters/sidekiq_adapter.rb +0 -33
  267. data/lib/woods/console/adapters/solid_queue_adapter.rb +0 -33
  268. data/lib/woods/console/bridge.rb +0 -210
  269. data/lib/woods/formatting/claude_adapter.rb +0 -98
  270. data/lib/woods/formatting/generic_adapter.rb +0 -56
  271. data/lib/woods/formatting/gpt_adapter.rb +0 -64
  272. data/lib/woods/notion/mapper.rb +0 -40
  273. data/lib/woods/observability/health_check.rb +0 -79
  274. 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
@@ -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.