woods 1.6.0 → 2.0.0.beta1

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 (273) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1908 -0
  3. data/CONTRIBUTING.md +202 -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 +620 -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 +415 -0
  19. data/docs/INTERNALS.md +415 -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 +197 -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 +40 -1
  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 +2509 -189
  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 +63 -21
  154. data/lib/woods/flow_document.rb +1 -0
  155. data/lib/woods/flow_precomputer.rb +138 -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 +408 -34
  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 +223 -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.rb +21 -5
  218. data/lib/woods/session_tracer/file_store.rb +138 -19
  219. data/lib/woods/session_tracer/middleware.rb +1 -2
  220. data/lib/woods/session_tracer/redis_store.rb +122 -12
  221. data/lib/woods/session_tracer/session_flow_assembler.rb +57 -17
  222. data/lib/woods/session_tracer/session_flow_document.rb +56 -14
  223. data/lib/woods/session_tracer/solid_cache_coordination.rb +192 -0
  224. data/lib/woods/session_tracer/solid_cache_store.rb +560 -91
  225. data/lib/woods/session_tracer/store.rb +14 -1
  226. data/lib/woods/storage/metadata_store.rb +230 -26
  227. data/lib/woods/storage/pgvector.rb +180 -22
  228. data/lib/woods/storage/qdrant.rb +367 -41
  229. data/lib/woods/storage/snapshotter/metadata.rb +79 -16
  230. data/lib/woods/storage/snapshotter/vector.rb +128 -17
  231. data/lib/woods/storage/snapshotter.rb +23 -5
  232. data/lib/woods/storage/vector_store.rb +49 -8
  233. data/lib/woods/storage_identity.rb +28 -0
  234. data/lib/woods/tasks.rb +53 -2
  235. data/lib/woods/temporal/json_snapshot_store.rb +112 -42
  236. data/lib/woods/temporal/snapshot_store.rb +139 -42
  237. data/lib/woods/unblocked/client.rb +119 -17
  238. data/lib/woods/unblocked/document_builder.rb +34 -2
  239. data/lib/woods/unblocked/exporter.rb +63 -27
  240. data/lib/woods/unblocked/rate_limiter.rb +23 -9
  241. data/lib/woods/unblocked/sync_manifest.rb +16 -8
  242. data/lib/woods/update_check.rb +24 -1
  243. data/lib/woods/util/uuid5.rb +124 -0
  244. data/lib/woods/version.rb +1 -1
  245. data/lib/woods/watch/daemon.rb +1345 -0
  246. data/lib/woods/watch/listen_watcher.rb +81 -0
  247. data/lib/woods/watch/polling_watcher.rb +137 -0
  248. data/lib/woods/watch/status.rb +169 -0
  249. data/lib/woods/watch/tree_scan.rb +163 -0
  250. data/lib/woods/watch/watcher.rb +100 -0
  251. data/lib/woods.rb +53 -9
  252. data/plugin/.claude-plugin/plugin.json +18 -0
  253. data/plugin/hooks/hooks.json +29 -0
  254. data/plugin/hooks/woods-post-edit.sh +226 -0
  255. data/plugin/hooks/woods-session-start.sh +77 -0
  256. data/plugin/skills/woods-agent-enable/SKILL.md +51 -0
  257. data/plugin/skills/woods-diagnose/SKILL.md +75 -0
  258. data/plugin/skills/woods-investigate/SKILL.md +39 -0
  259. data/plugin/skills/woods-mcp-config/SKILL.md +101 -0
  260. data/plugin/skills/woods-setup/SKILL.md +99 -0
  261. metadata +134 -23
  262. data/lib/woods/console/adapters/cache_adapter.rb +0 -58
  263. data/lib/woods/console/adapters/good_job_adapter.rb +0 -33
  264. data/lib/woods/console/adapters/job_adapter.rb +0 -74
  265. data/lib/woods/console/adapters/sidekiq_adapter.rb +0 -33
  266. data/lib/woods/console/adapters/solid_queue_adapter.rb +0 -33
  267. data/lib/woods/console/bridge.rb +0 -210
  268. data/lib/woods/formatting/claude_adapter.rb +0 -98
  269. data/lib/woods/formatting/generic_adapter.rb +0 -56
  270. data/lib/woods/formatting/gpt_adapter.rb +0 -64
  271. data/lib/woods/notion/mapper.rb +0 -40
  272. data/lib/woods/observability/health_check.rb +0 -79
  273. data/lib/woods/observability/instrumentation.rb +0 -34
data/lib/woods.rb CHANGED
@@ -22,6 +22,8 @@
22
22
  # end
23
23
  #
24
24
  require_relative 'woods/version'
25
+ # Configuration's `component_paths` default lives with the discovery it feeds.
26
+ require_relative 'woods/extractors/component_discovery'
25
27
 
26
28
  module Woods
27
29
  class Error < StandardError; end
@@ -120,8 +122,13 @@ module Woods
120
122
  # ════════════════════════════════════════════════════════════════════════
121
123
 
122
124
  class Configuration # rubocop:disable Metrics/ClassLength
123
- attr_accessor :embedding_model, :include_framework_sources, :gem_configs,
124
- :vector_store, :metadata_store, :graph_store, :embedding_provider, :log_level,
125
+ # The set every full extraction always runs, regardless of `extractors=`.
126
+ # See the {#extractors=} YARD note: this knob does not gate extraction.
127
+ DEFAULT_EXTRACTORS = %i[models controllers services components view_components jobs mailers graphql
128
+ serializers managers policies validators rails_source].freeze
129
+
130
+ attr_accessor :include_framework_sources, :gem_configs,
131
+ :vector_store, :metadata_store, :graph_store, :embedding_provider,
125
132
  :vector_store_options, :metadata_store_options, :embedding_options,
126
133
  :concurrent_extraction, :precompute_flows, :extract_navigation_edges, :enable_snapshots,
127
134
  :session_tracer_enabled, :session_tracer_allow_production,
@@ -137,19 +144,19 @@ module Woods
137
144
  :notion_api_token, :notion_database_ids,
138
145
  :unblocked_api_token, :unblocked_collection_id, :unblocked_repo_url,
139
146
  :cache_store, :cache_options,
140
- :dump_retention_count
141
- attr_reader :max_context_tokens, :similarity_threshold, :extractors, :pretty_json, :context_format,
142
- :cache_enabled
147
+ :dump_retention_count, :component_paths
148
+ attr_reader :embedding_model, :max_context_tokens, :similarity_threshold, :extractors, :pretty_json,
149
+ :context_format, :cache_enabled, :volatile_dependency_ratio
143
150
 
144
151
  def initialize # rubocop:disable Metrics/MethodLength
145
152
  @output_dir = nil # Resolved lazily; Rails.root is nil at require time
146
153
  @embedding_model = 'text-embedding-3-small'
154
+ @embedding_model_explicit = false
147
155
  @max_context_tokens = 8000
148
156
  @similarity_threshold = 0.7
149
157
  @include_framework_sources = true
150
158
  @gem_configs = {}
151
- @extractors = %i[models controllers services components view_components jobs mailers graphql serializers
152
- managers policies validators rails_source]
159
+ @extractors = DEFAULT_EXTRACTORS.dup
153
160
  @pretty_json = true
154
161
  @concurrent_extraction = false
155
162
  @precompute_flows = false
@@ -197,6 +204,19 @@ module Woods
197
204
  @cache_store = nil # :redis, :solid_cache, :memory, or a CacheStore instance
198
205
  @cache_options = {} # { redis: client, cache: store, ttl: { embeddings: 86400, ... } }
199
206
  @dump_retention_count = 3
207
+ @volatile_dependency_ratio = 3.0
208
+ # Directories the component extractors walk before reading `descendants`,
209
+ # relative to Rails.root. See Extractors::ComponentDiscovery.
210
+ @component_paths = Extractors::ComponentDiscovery::DEFAULT_COMPONENT_PATHS.dup
211
+ end
212
+
213
+ def embedding_model=(value)
214
+ @embedding_model = value
215
+ @embedding_model_explicit = true
216
+ end
217
+
218
+ def embedding_model_explicit?
219
+ @embedding_model_explicit
200
220
  end
201
221
 
202
222
  # @return [Pathname, String] Output directory, defaulting to Rails.root/tmp/woods
@@ -235,6 +255,10 @@ module Woods
235
255
  @similarity_threshold = float_val
236
256
  end
237
257
 
258
+ # Accepted for forward compatibility. Extraction always runs all 34
259
+ # extractors; this array is not consulted anywhere in the extraction
260
+ # path. Setting it to anything other than {DEFAULT_EXTRACTORS} warns.
261
+ #
238
262
  # @param value [Array<Symbol>] List of extractor names
239
263
  # @raise [ConfigurationError] if value is not an Array of Symbols
240
264
  def extractors=(value)
@@ -242,6 +266,11 @@ module Woods
242
266
  raise ConfigurationError, "extractors must be an Array of Symbols, got #{value.inspect}"
243
267
  end
244
268
 
269
+ if value.sort_by(&:to_s) != DEFAULT_EXTRACTORS.sort_by(&:to_s)
270
+ warn 'config.extractors is accepted for forward compatibility but extractor selection is not ' \
271
+ 'implemented; all extractors run.'
272
+ end
273
+
245
274
  @extractors = value
246
275
  end
247
276
 
@@ -270,12 +299,27 @@ module Woods
270
299
  @cache_enabled = value
271
300
  end
272
301
 
273
- # Add a gem to be indexed
302
+ # Commit-count ratio above which {Woods::GraphAnalyzer#volatile_dependencies}
303
+ # reports an edge. Stored as a Float.
304
+ #
305
+ # @param value [Numeric] must be greater than 1
306
+ # @raise [ConfigurationError] otherwise
307
+ def volatile_dependency_ratio=(value)
308
+ unless value.is_a?(Numeric) && value > 1
309
+ raise ConfigurationError, "volatile_dependency_ratio must be a number greater than 1, got #{value.inspect}"
310
+ end
311
+
312
+ @volatile_dependency_ratio = value.to_f
313
+ end
314
+
315
+ # Accepted for forward compatibility. Nothing reads {gem_configs}; gem
316
+ # source indexing is not implemented.
274
317
  #
275
318
  # @param gem_name [String] Name of the gem
276
319
  # @param paths [Array<String>] Relative paths within the gem to index
277
320
  # @param priority [Symbol] :high, :medium, or :low
278
321
  def add_gem(gem_name, paths:, priority: :medium)
322
+ warn 'config.add_gem is accepted for forward compatibility but gem configs are not used.'
279
323
  @gem_configs[gem_name] = { paths: paths, priority: priority }
280
324
  end
281
325
 
@@ -305,7 +349,7 @@ module Woods
305
349
 
306
350
  # Configure the module using a named preset and optional block customization.
307
351
  #
308
- # Valid preset names: :local, :postgresql, :production
352
+ # Valid preset names: :local, :shared_filesystem, :postgresql, :production
309
353
  #
310
354
  # @param name [Symbol] Preset name
311
355
  # @yield [config] Optional block for further customization after preset is applied
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "woods-plugin",
3
+ "description": "Woods user guides for Claude Code: set up, upgrade, configure MCP servers for, investigate codebases with, enable a repository's agents on, and diagnose the Woods Rails code-intelligence gem.",
4
+ "version": "2.3.0",
5
+ "author": {
6
+ "name": "lost-in-the"
7
+ },
8
+ "homepage": "https://github.com/lost-in-the/woods",
9
+ "repository": "https://github.com/lost-in-the/woods",
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "rails",
13
+ "woods",
14
+ "mcp",
15
+ "code-intelligence",
16
+ "agent"
17
+ ]
18
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "Edit|Write|MultiEdit",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/woods-post-edit.sh\"",
10
+ "async": true,
11
+ "timeout": 600
12
+ }
13
+ ]
14
+ }
15
+ ],
16
+ "SessionStart": [
17
+ {
18
+ "matcher": "startup|resume",
19
+ "hooks": [
20
+ {
21
+ "type": "command",
22
+ "command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/woods-session-start.sh\"",
23
+ "timeout": 20
24
+ }
25
+ ]
26
+ }
27
+ ]
28
+ }
29
+ }
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env bash
2
+ # Woods PostToolUse hook (#280): after an edit to a graph-changing path,
3
+ # refresh the index with `woods:incremental` in the background.
4
+ #
5
+ # Reads `cwd` from the hook payload, not CLAUDE_PROJECT_DIR, so a session in
6
+ # a linked worktree refreshes that worktree's own index. Runs only when an
7
+ # index already exists there. `woods:incremental` stands down under a
8
+ # running watch daemon and takes the extraction lock itself; the lock here
9
+ # only stops this hook from queueing one rake process per keystroke.
10
+ #
11
+ # Opt-in: this hook is shipped disabled. It does nothing until
12
+ # WOODS_HOOKS_ENABLED=1 is set (see docs/WATCH_DAEMON.md and the woods-setup
13
+ # skill for where to set it). WOODS_HOOKS_DISABLED=1 turns it back off even
14
+ # after it has been enabled, without touching the enable setting.
15
+ #
16
+ # Lock contention must not drop an edit. A hook invocation that finds the
17
+ # run lock busy appends its path to hook-pending.txt and returns immediately
18
+ # instead of waiting; whichever invocation is holding the run lock drains
19
+ # that file in a loop, passing every path collected on each drain as one
20
+ # CHANGED_FILES batch, until a drain comes back empty. The one narrow gap
21
+ # this leaves: an append that lands after the holder's last (empty) drain
22
+ # but before it releases the run lock sits in the file, unprocessed, until
23
+ # the next graph-changing edit triggers a hook that both appends it and
24
+ # then wins the now-free run lock itself. That edit is delayed, not lost,
25
+ # and the SessionStart hook's staleness warning is the backstop for it.
26
+ #
27
+ # The mkdir-based lock fallback has no kernel-enforced release: a `flock`
28
+ # held by a killed process is freed automatically, but a lock *directory*
29
+ # a killed process made is not. A hook killed mid-drain (OOM, a `kill -9`,
30
+ # the host restarting) leaves `hook.lock.d` or `hook-pending.lock.d` behind
31
+ # forever, so every later hook either skips draining permanently (the run
32
+ # lock) or spins until the 600s hook timeout on every single invocation
33
+ # (the pending lock, which blocks). Both lock directories are therefore
34
+ # reclaimed once their mtime is older than WOODS_HOOK_LOCK_STALE_SECONDS: a
35
+ # fresh lock directory is still respected as busy.
36
+ #
37
+ # Knobs:
38
+ # WOODS_HOOK_RAKE command prefix, default "bundle exec rake"
39
+ # (Docker: "docker compose exec -T app bundle exec rake")
40
+ # WOODS_OUTPUT index directory override, same variable
41
+ # woods:incremental/woods:watch_status already read;
42
+ # default tmp/woods under the payload's cwd
43
+ # WOODS_HOOKS_ENABLED set to 1 to turn the hook on
44
+ # WOODS_HOOKS_DISABLED set to 1 to turn it back off
45
+ # WOODS_HOOK_LOCK_STALE_SECONDS
46
+ # age (mtime) after which a leftover mkdir-based lock
47
+ # directory is reclaimed instead of respected as busy;
48
+ # default 1800 (a few multiples of the 600s hook
49
+ # timeout). Only the mkdir fallback needs this;
50
+ # flock has no equivalent problem.
51
+ set -u
52
+
53
+ [ "${WOODS_HOOKS_DISABLED:-0}" = "1" ] && exit 0
54
+ [ "${WOODS_HOOKS_ENABLED:-0}" = "1" ] || exit 0
55
+
56
+ payload="$(cat)"
57
+
58
+ # $1: dotted key path. Prefers jq, falls back to ruby, else gives up quietly.
59
+ field() {
60
+ if command -v jq >/dev/null 2>&1; then
61
+ printf '%s' "$payload" | jq -r ".$1 // empty"
62
+ elif command -v ruby >/dev/null 2>&1; then
63
+ printf '%s' "$payload" | ruby -rjson -e '
64
+ value = JSON.parse($stdin.read)
65
+ ARGV[0].split(".").each { |key| value = value.is_a?(Hash) ? value[key] : nil }
66
+ print value.to_s' -- "$1"
67
+ else
68
+ printf ''
69
+ fi
70
+ }
71
+
72
+ cwd="$(field cwd)"
73
+ file="$(field tool_input.file_path)"
74
+ [ -z "$cwd" ] && exit 0
75
+ [ -z "$file" ] && exit 0
76
+
77
+ # Same no-boot resolution woods:watch_status uses: an explicit WOODS_OUTPUT
78
+ # wins outright (absolute or relative to cwd), otherwise tmp/woods under cwd.
79
+ # This is the config's own override knob, not a second hardcoded path.
80
+ configured_output="${WOODS_OUTPUT:-tmp/woods}"
81
+ case "$configured_output" in
82
+ /*) tmp_dir="$configured_output" ;;
83
+ *) tmp_dir="$cwd/$configured_output" ;;
84
+ esac
85
+
86
+ [ -f "$tmp_dir/generation.json" ] || exit 0
87
+
88
+ case "$file" in
89
+ "$cwd"/*) rel="${file#"$cwd"/}" ;;
90
+ /*) exit 0 ;;
91
+ *) rel="$file" ;;
92
+ esac
93
+
94
+ case "$rel" in
95
+ app/models/*|config/routes.rb|config/routes/*|db/migrate/*|db/*_migrate/*|db/schema.rb|db/structure.sql|package.yml|*/package.yml|packwerk.yml) ;;
96
+ *) exit 0 ;;
97
+ esac
98
+
99
+ rake="${WOODS_HOOK_RAKE:-bundle exec rake}"
100
+ log="$tmp_dir/hook.log"
101
+ pending_file="$tmp_dir/hook-pending.txt"
102
+ pending_lock="$tmp_dir/hook-pending.lock"
103
+ pending_lock_dir="$tmp_dir/hook-pending.lock.d"
104
+ run_lock="$tmp_dir/hook.lock"
105
+ run_lock_dir="$tmp_dir/hook.lock.d"
106
+
107
+ mkdir -p "$tmp_dir" 2>/dev/null || true
108
+
109
+ have_flock() { command -v flock >/dev/null 2>&1; }
110
+
111
+ # mtime of a directory in epoch seconds, GNU stat then BSD stat, empty if
112
+ # neither exists (treated as "not stale": fail closed, keep waiting rather
113
+ # than reclaim on a guess).
114
+ dir_mtime() {
115
+ stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null
116
+ }
117
+
118
+ # True when $1 is an mkdir-based lock directory old enough that it can only
119
+ # be a crash leftover, never a legitimately still-running hook (a hook run
120
+ # is one rake invocation, bounded by the 600s hook timeout).
121
+ lock_dir_stale() {
122
+ mtime="$(dir_mtime "$1")"
123
+ [ -z "$mtime" ] && return 1
124
+ now="$(date +%s)"
125
+ age=$((now - mtime))
126
+ [ "$age" -gt "${WOODS_HOOK_LOCK_STALE_SECONDS:-1800}" ]
127
+ }
128
+
129
+ # Blocking mkdir-based lock acquire: waits for $1, reclaiming it once stale
130
+ # rather than waiting for a crashed holder that will never release it.
131
+ acquire_mkdir_lock() {
132
+ while ! mkdir "$1" 2>/dev/null; do
133
+ if lock_dir_stale "$1"; then
134
+ rmdir "$1" 2>/dev/null || true
135
+ continue
136
+ fi
137
+ sleep 0.1
138
+ done
139
+ }
140
+
141
+ release_mkdir_lock() {
142
+ rmdir "$1" 2>/dev/null || true
143
+ }
144
+
145
+ # Non-blocking mkdir-based lock attempt: one reclaim retry when stale, no
146
+ # wait otherwise (contention here means "someone else is already draining,"
147
+ # not "someone crashed").
148
+ try_acquire_mkdir_lock() {
149
+ mkdir "$1" 2>/dev/null && return 0
150
+ if lock_dir_stale "$1"; then
151
+ rmdir "$1" 2>/dev/null || true
152
+ mkdir "$1" 2>/dev/null && return 0
153
+ fi
154
+ return 1
155
+ }
156
+
157
+ # Append one path to the pending file. Blocking: the critical section is a
158
+ # single append, so any wait here is brief regardless of who else holds it.
159
+ append_pending() {
160
+ if have_flock; then
161
+ exec 7>"$pending_lock"
162
+ flock 7
163
+ printf '%s\n' "$1" >>"$pending_file"
164
+ flock -u 7
165
+ exec 7>&-
166
+ else
167
+ acquire_mkdir_lock "$pending_lock_dir"
168
+ printf '%s\n' "$1" >>"$pending_file"
169
+ release_mkdir_lock "$pending_lock_dir"
170
+ fi
171
+ }
172
+
173
+ # Print and clear whatever is currently pending, empty output if nothing is.
174
+ drain_pending() {
175
+ if have_flock; then
176
+ exec 7>"$pending_lock"
177
+ flock 7
178
+ if [ -s "$pending_file" ]; then
179
+ cat "$pending_file"
180
+ : >"$pending_file"
181
+ fi
182
+ flock -u 7
183
+ exec 7>&-
184
+ else
185
+ acquire_mkdir_lock "$pending_lock_dir"
186
+ if [ -s "$pending_file" ]; then
187
+ cat "$pending_file"
188
+ : >"$pending_file"
189
+ fi
190
+ release_mkdir_lock "$pending_lock_dir"
191
+ fi
192
+ }
193
+
194
+ run_incremental() {
195
+ # A drain can return several paths at once; CHANGED_FILES takes a
196
+ # comma-separated list (see lib/tasks/woods.rake).
197
+ changed="$(printf '%s\n' "$1" | tr '\n' ',' | sed 's/,$//')"
198
+ # shellcheck disable=SC2086
199
+ ( cd "$cwd" && CHANGED_FILES="$changed" $rake woods:incremental ) >>"$log" 2>&1
200
+ }
201
+
202
+ drain_until_empty() {
203
+ while :; do
204
+ batch="$(drain_pending)"
205
+ [ -z "$batch" ] && break
206
+ run_incremental "$batch"
207
+ done
208
+ }
209
+
210
+ append_pending "$rel"
211
+
212
+ if have_flock; then
213
+ exec 8>"$run_lock"
214
+ if flock -n 8; then
215
+ drain_until_empty
216
+ flock -u 8
217
+ fi
218
+ exec 8>&-
219
+ else
220
+ if try_acquire_mkdir_lock "$run_lock_dir"; then
221
+ drain_until_empty
222
+ release_mkdir_lock "$run_lock_dir"
223
+ fi
224
+ fi
225
+
226
+ exit 0
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bash
2
+ # Woods SessionStart hook (#280): say so when the published generation is
3
+ # older than the last commit. Stdout from a SessionStart hook is added to
4
+ # the session context, so the agent sees the warning before it trusts the
5
+ # index.
6
+ #
7
+ # This check only compares two commit-adjacent timestamps: the generation's
8
+ # `updated_at` and `git log -1`'s commit time. It says nothing about
9
+ # uncommitted edits (the index can be stale against a dirty working tree
10
+ # with no stale commit to detect) or about a checkout sitting on an older
11
+ # commit than the one that produced the generation (the timestamp comparison
12
+ # can read as fresh there even though the code and the index disagree). Read
13
+ # a quiet run as "not behind the last commit," not as "definitely current."
14
+ #
15
+ # Opt-in: shipped disabled. Nothing prints until WOODS_HOOKS_ENABLED=1 is
16
+ # set; WOODS_HOOKS_DISABLED=1 turns it back off without touching that
17
+ # setting.
18
+ set -u
19
+
20
+ [ "${WOODS_HOOKS_DISABLED:-0}" = "1" ] && exit 0
21
+ [ "${WOODS_HOOKS_ENABLED:-0}" = "1" ] || exit 0
22
+
23
+ payload="$(cat)"
24
+
25
+ field() {
26
+ if command -v jq >/dev/null 2>&1; then
27
+ printf '%s' "$payload" | jq -r ".$1 // empty"
28
+ elif command -v ruby >/dev/null 2>&1; then
29
+ printf '%s' "$payload" | ruby -rjson -e '
30
+ value = JSON.parse($stdin.read)
31
+ ARGV[0].split(".").each { |key| value = value.is_a?(Hash) ? value[key] : nil }
32
+ print value.to_s' -- "$1"
33
+ else
34
+ printf ''
35
+ fi
36
+ }
37
+
38
+ cwd="$(field cwd)"
39
+ [ -z "$cwd" ] && exit 0
40
+
41
+ # Same no-boot resolution the PostToolUse hook and woods:watch_status use:
42
+ # WOODS_OUTPUT overrides outright, otherwise tmp/woods under cwd.
43
+ configured_output="${WOODS_OUTPUT:-tmp/woods}"
44
+ case "$configured_output" in
45
+ /*) tmp_dir="$configured_output" ;;
46
+ *) tmp_dir="$cwd/$configured_output" ;;
47
+ esac
48
+
49
+ marker="$tmp_dir/generation.json"
50
+ [ -f "$marker" ] || exit 0
51
+
52
+ if command -v jq >/dev/null 2>&1; then
53
+ updated="$(jq -r '.updated_at // empty' "$marker")"
54
+ number="$(jq -r '.number // empty' "$marker")"
55
+ elif command -v ruby >/dev/null 2>&1; then
56
+ updated="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["updated_at"].to_s' -- "$marker")"
57
+ number="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["number"].to_s' -- "$marker")"
58
+ else
59
+ exit 0
60
+ fi
61
+ [ -z "$updated" ] && exit 0
62
+
63
+ last_commit="$(git -C "$cwd" log -1 --format=%cI 2>/dev/null)"
64
+ [ -z "$last_commit" ] && exit 0
65
+
66
+ stale=0
67
+ if command -v ruby >/dev/null 2>&1; then
68
+ ruby -rtime -e 'exit(Time.parse(ARGV[0]) < Time.parse(ARGV[1]) ? 1 : 0)' -- "$updated" "$last_commit" || stale=1
69
+ elif date -d "$updated" +%s >/dev/null 2>&1; then
70
+ [ "$(date -d "$updated" +%s)" -lt "$(date -d "$last_commit" +%s)" ] && stale=1
71
+ fi
72
+
73
+ if [ "$stale" = "1" ]; then
74
+ echo "Woods index is stale: generation ${number:-?} was published at $updated, before the last commit at $last_commit." \
75
+ "Run bin/rails woods:incremental (or start bin/rails woods:watch) before trusting woods answers."
76
+ fi
77
+ exit 0
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: woods-agent-enable
3
+ description: Make a repository's coding agents use the Woods index by default — wire the MCP server into project configuration, add index-first guidance to CLAUDE.md/AGENTS.md, and optionally create a project skill. Use when a user wants agents to "know" their Rails codebase, asks to add Woods to a team's agent setup or repository instructions, or wants codebase intelligence available in every session without per-user installs.
4
+ ---
5
+
6
+ # Enable Woods for a repository's agents
7
+
8
+ Installing the gem gives one operator an index; this skill gives every future agent session one. Wiring is three small, reviewable changes to the host repository. Each edits shared, checked-in files, so propose the diff and get approval before writing, match the repository's existing conventions, and leave unrelated content untouched.
9
+
10
+ ## Preflight
11
+
12
+ Confirm Woods actually works before advertising it to every future session:
13
+
14
+ ```bash
15
+ bundle info woods
16
+ bin/rails woods:validate
17
+ bin/rails woods:stats
18
+ ```
19
+
20
+ A missing gem or index means setup comes first (woods-setup). Record the index path and whether the app runs on the host or in Docker — the MCP entry must use the command shape that matches (woods-mcp-config has the shapes).
21
+
22
+ ## 1. Project MCP configuration
23
+
24
+ Add the Index Server to the repository's checked-in MCP configuration (`.mcp.json` for Claude Code; adapt to the team's client) using the Index-only shape from woods-mcp-config, so every clone gets the server without per-user setup. Do not wire Console MCP at the project level: it is live-data access and stays a per-user, explicitly authorized opt-in.
25
+
26
+ ## 2. Index-first guidance in the agent instructions
27
+
28
+ Add a short section to the repository's agent instructions (CLAUDE.md, AGENTS.md, or the team's equivalent). Keep it to the contract, not a tool manual — the connected server's own tool list is the manual:
29
+
30
+ ```markdown
31
+ ## Codebase index (Woods)
32
+
33
+ A Woods MCP server ("woods") serves a runtime-accurate index of this app.
34
+ Call `woods_status` first; for structural questions prefer `search` →
35
+ `lookup` → `dependencies`/`dependents`/`trace_flow` over broad file reading
36
+ or grep. "Not found" is evidence about the index, not the code — check
37
+ generation freshness before concluding. After changing files, refresh with
38
+ `bin/rails woods:incremental` (or run the `woods:watch` daemon).
39
+ ```
40
+
41
+ Adjust names and commands to the app (Docker prefix, custom index path, task aliases). If the repository already documents agent tooling, extend that section rather than adding a competing one.
42
+
43
+ ## 3. Optional: a project skill
44
+
45
+ When the team wants stronger triggering than standing instructions provide, create a repository-level skill (for Claude Code: `.claude/skills/<app>-codebase/SKILL.md`) whose description names the application and its domains, and whose body applies the woods-investigate workflow to this codebase specifically: the entry-point identifiers worth knowing, domain cluster names, and local conventions. Seed that content from real `domain_clusters` and `pagerank` output rather than inventing it, and note in the skill that the index — not the skill text — is the source of truth as the app evolves.
46
+
47
+ ## Handoff
48
+
49
+ Report the files changed, the MCP entry added, and one verified end-to-end call (`woods_status` through the configured client, not a raw JSON-RPC probe). Remind the owner that agent sessions pick up the changes on their next start, and that the index only stays useful if extraction stays current.
50
+
51
+ Canonical guides: [AGENT_SETUP.md](https://github.com/lost-in-the/woods/blob/main/docs/AGENT_SETUP.md), [AGENT_GUIDE.md](https://github.com/lost-in-the/woods/blob/main/docs/AGENT_GUIDE.md).
@@ -0,0 +1,75 @@
1
+ ---
2
+ name: woods-diagnose
3
+ description: Diagnose Woods failures layer by layer — Rails boot, published index, MCP process and path, semantic retrieval, Console — changing nothing until the failing layer is identified. Use when Woods extraction, index validation, an MCP connection, retrieval, storage, or Console access fails or looks stale, or when expected tools are missing from a connected server.
4
+ ---
5
+
6
+ # Woods diagnosis
7
+
8
+ Change nothing until the failing layer is identified. Diagnose the installed version:
9
+
10
+ ```bash
11
+ bundle info woods
12
+ git status --short --branch
13
+ ```
14
+
15
+ This skill describes the Woods 2.x line; the authoritative minimum version lives in the marketplace entry. Diagnose against capabilities the recorded installed version actually provides.
16
+
17
+ ## 1. Check Rails
18
+
19
+ ```bash
20
+ bundle exec rails runner 'puts Rails.application.class.name'
21
+ bundle exec rails runner 'Rails.application.eager_load!; puts "eager load ok"'
22
+ ```
23
+
24
+ Use the application's normal Docker command and environment variables when applicable. Fix boot/eager-load failures before Woods.
25
+
26
+ ## 2. Check the published index
27
+
28
+ ```bash
29
+ bin/rails woods:validate
30
+ bin/rails woods:stats
31
+ ```
32
+
33
+ If missing or stale, run the narrow maintenance path justified by the evidence: `woods:incremental` for known file changes or `woods:extract` for first run, broad change, upgrade, or drift. Woods tasks understand `generation.json`; do not assume `manifest.json` is at the root.
34
+
35
+ If a one-shot extraction raises `Could not publish generation`, the candidate
36
+ payload was written but never made visible; readers still serve the previous
37
+ complete generation. Fix the named filesystem, permission, space, or mount
38
+ failure and rerun the same task. Never edit `generation.json` or point a reader
39
+ at the unreachable payload by hand.
40
+
41
+ ## 3. Check the MCP process and path
42
+
43
+ Compare the client config with the exact command, absolute `cwd`, bundle, and index path visible to that process. Run the configured executable manually to read stderr. For a host bundle:
44
+
45
+ ```bash
46
+ bundle exec woods-mcp-start ./tmp/woods
47
+ ```
48
+
49
+ Then reconnect through the MCP client and call `woods_status`. Use client-native tool inspection after initialization. Expect 14 packaged Index tools, not all conditional schemas.
50
+
51
+ For Docker-only bundles, test the configured container command instead, for example `docker compose exec -T app bundle exec woods-mcp /app/tmp/woods`. Use the container path for a container process and a host path only for a host process.
52
+
53
+ ## 4. Check semantic retrieval
54
+
55
+ Only diagnose this layer when structural tools work and `codebase_retrieve` fails. Check `woods_status`, configured provider/model/vector store, provider reachability, and whether `woods:embed` completed.
56
+
57
+ - OpenAI: verify the key exists without printing it.
58
+ - Ollama: verify the service and configured model locally.
59
+ - Stale vectors or missing same-name types: follow the installed version's upgrade guide and run the documented embed refresh; do not rename public identifiers or edit vector IDs by hand.
60
+ - Dimension mismatch: rebuild into a store matching the configured model; do not suppress the preflight.
61
+ - Purge guard: back up and inspect the proposed deletion; never set `WOODS_ALLOW_PURGE` without explicit approval.
62
+
63
+ ## 5. Check Console separately
64
+
65
+ Console failures are live Rails/config/security failures, not Index failures. Verify authorized environment, Rails boot, `WOODS_CONSOLE_CONFIG` or direct `cwd`, blocked-table policy, credentials, and stderr.
66
+
67
+ For MySQL SQL refusals, inspect the executing session's `sql_mode` and the installed version's Console guide. Do not change quote modes to bypass a security refusal.
68
+
69
+ Nine tools are normal. Eleven appear only with `console_embedded_read_tools`. Do not chase Tier 2/3 or `console_eval`; they do not register in supported packaged modes. Never work around redaction, credential scanning, SQL validation, or a block.
70
+
71
+ ## Report
72
+
73
+ Return the first failing layer, commands/evidence, root-cause hypothesis, whether any file changed, and the smallest next action. If a fix is requested, change one thing and rerun the failing check before proceeding.
74
+
75
+ Canonical guide: [TROUBLESHOOTING.md](https://github.com/lost-in-the/woods/blob/main/docs/TROUBLESHOOTING.md).
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: woods-investigate
3
+ description: Answer codebase questions from the Woods index — search, lookup, dependency and dependents traversal, flow tracing, and graph analysis, with verification rules. Use when auditing, code-reviewing, investigating, debugging, onboarding onto, or assessing change impact in a Rails application where a Woods MCP server is connected; query the index before broad file reading or grep.
4
+ ---
5
+
6
+ # Woods investigation
7
+
8
+ Woods is runtime evidence: resolved routes, schema, associations, callbacks, inlined concerns, dependencies, and execution flows extracted from the booted application. Use it to answer structural questions with less noise than file search, then verify important claims in source.
9
+
10
+ ## Preflight
11
+
12
+ Call `woods_status` before relying on the index. Require a ready index with a current generation and non-zero counts for the types you need; use `codebase_retrieve` only when status reports retrieval enabled. If status is unhealthy or the generation predates the code under review, report that and ask the owner to run `woods:incremental` or `woods:extract` — do not present "not found" as proof the code does not exist.
13
+
14
+ ## The default loop
15
+
16
+ 1. **Discover** with `search` when the exact identifier is unknown (regex `query`, or safer `exact_prefix`/`exact_suffix` for namespaced names; restrict `types`).
17
+ 2. **Inspect** the best match with `lookup`.
18
+ 3. **Traverse** with `dependencies` (what it uses), `dependents` (what uses it), or `trace_flow`, starting at depth 1–2 with filters.
19
+ 4. **Verify** important claims against the returned source paths and current repository files.
20
+
21
+ Identifiers are namespaced and typed; never invent one from a filename when `search` can return the exact value.
22
+
23
+ ## By task shape
24
+
25
+ - **Code review / change impact**: `lookup` the changed unit, then `dependents` at depth 1 before going deeper. Group results by relationship type and layer; report direct dependents separately from inferred downstream impact. A graph edge is not test coverage — select tests from mappings and repository search.
26
+ - **Audit / architecture assessment**: `graph_analysis` for orphans, dead ends, hubs, cycles, bridges, cross-database edges, volatile dependencies, and undeclared package edges; `domain_clusters` for architectural domains; `pagerank` for high-impact units worth reading first.
27
+ - **Investigating behavior / debugging**: `trace_flow` from the user-visible entry point (route, controller action, job, mailer, service), `lookup` at ambiguous steps, and verify anything conditional or dynamically dispatched in source and tests — do not infer call order from a dependency edge.
28
+ - **Onboarding**: `structure` for the codebase overview, `lookup` and `dependencies`/`dependents` for a unit's neighborhood, and `domain_clusters` for the domain map, then the default loop on the units that matter.
29
+ - **Conceptual questions**: `codebase_retrieve` when status says ready; govern with `budget` (never `limit`), then verify key units with `lookup`.
30
+
31
+ ## Boundaries
32
+
33
+ The normal packaged Index Server registers 14 tools; conditional schemas register only when their wiring is configured — use the connected server's own tool list, never the source inventory. Console MCP is authorized live-data access, not another code-search mode; use Index tools for structure. Never work around a block, validation error, or redaction.
34
+
35
+ ## Report evidence
36
+
37
+ Name the tools and exact identifiers used, cite the source paths Woods returned, separate direct Woods evidence from inference, and state generation/staleness caveats. Say when a claim still needs source or test verification.
38
+
39
+ Canonical guides: [AGENT_GUIDE.md](https://github.com/lost-in-the/woods/blob/main/docs/AGENT_GUIDE.md), [MCP_TOOL_COOKBOOK.md](https://github.com/lost-in-the/woods/blob/main/docs/MCP_TOOL_COOKBOOK.md).