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,127 @@
1
+ # MCP Registration in Git Worktrees
2
+
3
+ > **Claude Code specific.** This page covers Claude Code's MCP registration model (`/mcp`, `~/.claude/plugins/`); other MCP clients manage per-directory registration their own way.
4
+
5
+ When you work in a git worktree, a separate directory checked out from the same repository, your MCP tools may not be available to subagents running in that directory. This page explains why, how to fix it, and how to confirm the registration took effect.
6
+
7
+ ## Why Worktree Subagents May Not See Woods Tools
8
+
9
+ MCP server registration in Claude Code is controlled by `.mcp.json` files. Claude Code discovers these files by walking up the directory tree from the working directory. It stops at the first `.mcp.json` it finds (or at `~/.claude/settings.json` for global registrations).
10
+
11
+ A git worktree has its own root directory separate from the main repository checkout. When a subagent starts inside the worktree root, it walks up from that path, not from the main repository root. Unless a `.mcp.json` exists inside the worktree directory tree or in an ancestor shared with both checkouts, the subagent sees no MCP servers.
12
+
13
+ Example directory layout:
14
+
15
+ ```
16
+ ~/work/my-app/ ← main checkout, has .mcp.json here
17
+ ~/work/my-app-feature/ ← worktree, no .mcp.json, so MCP tools are missing
18
+ ```
19
+
20
+ A subagent spawned in `~/work/my-app-feature/` will not find the `.mcp.json` from `~/work/my-app/`.
21
+
22
+ ## Fix: Add a `.mcp.json` to the Worktree Root
23
+
24
+ Create a `.mcp.json` in the worktree's root directory with the same woods server entries you use in the main checkout:
25
+
26
+ ```json
27
+ {
28
+ "mcpServers": {
29
+ "woods": {
30
+ "command": "woods-mcp-start",
31
+ "args": ["./tmp/woods"]
32
+ },
33
+ "woods-console": {
34
+ "command": "docker",
35
+ "args": [
36
+ "compose", "exec", "-T", "app",
37
+ "bundle", "exec", "rake", "woods:console"
38
+ ],
39
+ "cwd": "/absolute/host/path/to/worktree"
40
+ }
41
+ }
42
+ }
43
+ ```
44
+
45
+ Adjust the paths and arguments to match your project's setup. In particular:
46
+
47
+ - `./tmp/woods` is a relative path, it resolves against the worktree root, which is correct if extraction output is written into each worktree separately.
48
+ - If you share a single extraction output directory between checkouts, use the absolute path to the shared output: `"/absolute/path/to/main-checkout/tmp/woods"`.
49
+ - For Docker projects, the `docker compose exec` command works the same from any host path.
50
+
51
+ ## How Plugin Discovery Works
52
+
53
+ Claude Code also discovers MCP servers registered in plugin manifests. A plugin at `~/.claude/plugins/<plugin-name>/admin-tools/.mcp.json` is loaded globally, its servers are available in any session regardless of working directory.
54
+
55
+ If your team distributes the woods MCP registration through a shared plugin, the worktree problem does not apply. Check whether woods is already registered this way:
56
+
57
+ ```bash
58
+ ls ~/.claude/plugins/
59
+ # look for a directory containing admin-tools/.mcp.json
60
+ cat ~/.claude/plugins/<plugin-name>/admin-tools/.mcp.json
61
+ ```
62
+
63
+ If you find the woods servers registered there, subagents in any worktree will have access automatically, you do not need a per-worktree `.mcp.json`.
64
+
65
+ ## Verifying MCP Registration for a Subagent
66
+
67
+ ### Option 1: List tools from a claude session in the worktree
68
+
69
+ Open a new Claude Code session with the worktree as the working directory and run:
70
+
71
+ ```
72
+ /mcp
73
+ ```
74
+
75
+ This lists all connected MCP servers and their tools. If `woods` and/or `woods-console` appear, registration is working.
76
+
77
+ ### Option 2: Check via the woods status tool
78
+
79
+ Ask Claude to call the status tool:
80
+
81
+ ```
82
+ Use woods-console console_status to check what models are available.
83
+ ```
84
+
85
+ If the tool runs successfully, MCP is registered and the console server is reachable.
86
+
87
+ ### Option 3: Inspect the MCP config that Claude Code loaded
88
+
89
+ From the worktree directory, run:
90
+
91
+ ```bash
92
+ cat .mcp.json
93
+ ```
94
+
95
+ If the file exists and contains the woods entries, Claude Code will use it. If the file is missing, check parent directories up to your home directory for any `.mcp.json` that would be discovered.
96
+
97
+ ## Extraction Provenance in Worktrees (`git_branch` / `git_sha`)
98
+
99
+ `manifest.json` records the `git_branch` and `git_sha` the extraction ran against. In a linked worktree, `.git` is a **file** containing a `gitdir:` pointer to the real git directory, frequently an absolute host path, rather than a `.git` directory.
100
+
101
+ Woods resolves provenance with worktree-aware git plumbing (`git -C <root> rev-parse`), so an ordinary worktree reports the correct branch and SHA. When a `.git` is present but the pointed-to git directory **cannot be resolved**: most commonly a worktree extracted inside a container where the host path isn't mounted. Woods records `git_branch: "unknown"` / `git_sha: "unknown"` rather than a stale, misleading value: a baked `GIT_BRANCH`/`GIT_SHA` build arg is **not** trusted here (it could be stale). The env vars are honored only when there is no `.git` at the root at all (a non-repo checkout, e.g. a Docker `COPY` that excludes `.git`) or git is unavailable.
102
+
103
+ To get correct provenance from a containerized worktree, mount the directory the `gitdir:` pointer references (the parent repository's `.git`) into the container so git can resolve it. Temporal snapshots skip an `"unknown"` SHA, so misleading provenance never keys a snapshot.
104
+
105
+ ## Troubleshooting
106
+
107
+ **"Unknown tool" or "no MCP server named woods"**
108
+
109
+ The woods MCP server is not registered for this session. Add a `.mcp.json` to the worktree root as shown above, then restart the session.
110
+
111
+ **Console server starts but returns "unsupported: Not yet implemented in embedded mode" for console_sql / console_query**
112
+
113
+ The console server is registered and reachable, but `embedded_read_tools` is disabled (the default). To enable console_sql and console_query in embedded mode, see the [Console MCP Setup guide](CONSOLE_MCP_SETUP.md), specifically the `embedded_read_tools: true` option for the Rack middleware.
114
+
115
+ **Extraction output is empty or stale in the worktree**
116
+
117
+ Extraction writes to `tmp/woods/` relative to the Rails application root (inside the container). If the worktree's volume mount points to a different host path than the main checkout, run extraction again from the worktree:
118
+
119
+ ```bash
120
+ docker compose exec app bundle exec rake woods:extract
121
+ ```
122
+
123
+ See [DOCKER_SETUP.md](DOCKER_SETUP.md) for the full Docker workflow.
124
+
125
+ **Worktree `.mcp.json` conflicts with main checkout `.mcp.json`**
126
+
127
+ Each file is independent. Claude Code loads the one closest to the working directory. There is no inheritance or merging between them. Keep both files in sync manually, or move the shared configuration into a global plugin manifest.
@@ -0,0 +1,283 @@
1
+ # Notion Integration
2
+
3
+ Sync your Rails application's data model documentation to Notion databases, making schema, associations, validations, callbacks, and column-level detail accessible to non-technical stakeholders without GitHub access.
4
+
5
+ ## What Gets Synced
6
+
7
+ Woods extracts your Rails application via runtime introspection and pushes structured data to two Notion databases:
8
+
9
+ **Data Models Database**: One page per ActiveRecord model:
10
+ - Table name, model class name, file path
11
+ - Associations (has_many, belongs_to, has_one, through, polymorphic)
12
+ - Validations (grouped by attribute)
13
+ - Callbacks with side-effect analysis (jobs enqueued, services called)
14
+ - Scopes
15
+ - Column count
16
+ - Git metadata (last modified date, change frequency)
17
+ - Dependencies (services, jobs, other models referenced)
18
+ - Last schema change (from migration history)
19
+
20
+ **Columns Database**: One page per database column:
21
+ - Column name, data type, nullable, default value
22
+ - Validation rules (matched from model-level validations)
23
+ - Relation to parent Data Models page
24
+
25
+ All syncs are **idempotent**: existing pages are updated, new pages are created. Re-running the sync is always safe.
26
+
27
+ ## Sync Manifest (Incremental Sync)
28
+
29
+ Sync is incremental. A **sync manifest** (`<output_dir>/notion_sync_manifest.json`) records the content hash and Notion page id of every page last pushed. On each run the exporter:
30
+
31
+ - **skips** a page whose built content hash is unchanged (zero API calls),
32
+ - **updates** only pages whose content actually changed, via the cached page id,
33
+ - **self-heals** the cached id if the page was deleted or archived behind it (one lookup, then a normal create).
34
+
35
+ Manifest entries for models/columns that vanished from the current extraction are pruned, but **no Notion page is ever deleted** by the sync, there is no delete path. A renamed or removed model just leaves its old page in Notion untouched.
36
+
37
+ If the manifest is missing (first run, or a CI cache miss), the exporter falls back to the full lookup/create path for every page and rebuilds the manifest, correct, just more API calls than a steady-state run.
38
+
39
+ ### Escape hatch
40
+
41
+ Set `WOODS_NOTION_FORCE=1` (or pass `force_full: true` to the exporter) to ignore the manifest for one run and re-check every page, useful after a mapper/format change that alters every page's content, where the unchanged-hash skip would otherwise mask the update.
42
+
43
+ ## Setup
44
+
45
+ ### 1. Create a Notion Integration
46
+
47
+ 1. Go to [notion.so/my-integrations](https://www.notion.so/my-integrations)
48
+ 2. Create a new internal integration
49
+ 3. Copy the API token (starts with `secret_`)
50
+
51
+ ### 2. Create Notion Databases
52
+
53
+ Create two databases in your Notion workspace with these exact property names and types:
54
+
55
+ **Data Models Database:**
56
+
57
+ | Property | Type |
58
+ |---|---|
59
+ | Table Name | Title |
60
+ | Model Name | Text |
61
+ | Description | Text |
62
+ | Associations | Text |
63
+ | Validations | Text |
64
+ | Callbacks | Text |
65
+ | Scopes | Text |
66
+ | Column Count | Number |
67
+ | Last Modified | Date |
68
+ | Change Frequency | Select (options: new, hot, active, stable, dormant) |
69
+ | Last Schema Change | Date |
70
+ | File Path | Text |
71
+ | Dependencies | Text |
72
+
73
+ **Columns Database:**
74
+
75
+ | Property | Type |
76
+ |---|---|
77
+ | Column Name | Title |
78
+ | Table | Relation (→ Data Models database) |
79
+ | Data Type | Select (options: string, integer, bigint, boolean, datetime, text, decimal, float, date, binary, json, jsonb) |
80
+ | Nullable | Checkbox |
81
+ | Default Value | Text |
82
+ | Validation Rules | Text |
83
+
84
+ ### 3. Share Databases with Integration
85
+
86
+ Open each database in Notion, click the `...` menu → "Connections" → add your integration.
87
+
88
+ ### 4. Get Database IDs
89
+
90
+ Each database URL contains its ID: `https://notion.so/{workspace}/{database_id}?v=...`
91
+
92
+ ### 5. Configure Woods
93
+
94
+ ```ruby
95
+ # config/initializers/woods.rb
96
+ Woods.configure do |config|
97
+ config.notion_api_token = Rails.application.credentials.dig(:notion, :api_token)
98
+ config.notion_database_ids = {
99
+ data_models: 'your-data-models-database-id',
100
+ columns: 'your-columns-database-id'
101
+ }
102
+ end
103
+ ```
104
+
105
+ Or via environment variables:
106
+
107
+ ```bash
108
+ export NOTION_API_TOKEN=secret_...
109
+ ```
110
+
111
+ ## Common Workflows
112
+
113
+ ### Full Extraction + Sync
114
+
115
+ ```bash
116
+ # Extract everything from the Rails app, then push to Notion
117
+ bundle exec rake woods:extract
118
+ bundle exec rake woods:notion_sync
119
+ ```
120
+
121
+ ### Post-Migration Sync
122
+
123
+ ```bash
124
+ # Re-extract changed files after a migration, then sync
125
+ bundle exec rake woods:incremental
126
+ bundle exec rake woods:notion_sync
127
+ ```
128
+
129
+ ### Buildkite CI Integration
130
+
131
+ Add to your `pipeline.yml`:
132
+
133
+ ```yaml
134
+ steps:
135
+ - label: ":database: Extract & Sync to Notion"
136
+ command: |
137
+ bundle exec rake woods:extract
138
+ bundle exec rake woods:notion_sync
139
+ if: build.branch == "main" && build.state == "passed"
140
+ soft_fail: true
141
+ env:
142
+ NOTION_API_TOKEN: $NOTION_API_TOKEN # Set in pipeline environment settings
143
+ ```
144
+
145
+ For post-migration syncs only:
146
+
147
+ ```yaml
148
+ steps:
149
+ - label: ":database: Sync Schema to Notion"
150
+ command: |
151
+ bundle exec rake woods:incremental
152
+ bundle exec rake woods:notion_sync
153
+ if: |
154
+ build.message =~ /migrate/i ||
155
+ build.env("FORCE_SCHEMA_SYNC") == "true"
156
+ soft_fail: true
157
+ ```
158
+
159
+ ### MCP Server
160
+
161
+ If using the MCP Index Server, the `notion_sync` tool is available:
162
+
163
+ ```json
164
+ {
165
+ "tool": "notion_sync",
166
+ "arguments": {}
167
+ }
168
+ ```
169
+
170
+ ### Selective Sync
171
+
172
+ Only sync Data Models (skip Columns):
173
+
174
+ ```ruby
175
+ Woods.configure do |config|
176
+ config.notion_database_ids = { data_models: 'db-uuid' }
177
+ # columns key omitted → column sync is skipped
178
+ end
179
+ ```
180
+
181
+ ## What the Gem Handles vs. the Host App
182
+
183
+ ### Gem (automated)
184
+
185
+ | Database | Content | Source |
186
+ |---|---|---|
187
+ | Data Models | Schema, associations, validations, callbacks, scopes, git metadata | ModelExtractor + MigrationExtractor |
188
+ | Columns | Column details, types, defaults, validation rules | ModelExtractor metadata |
189
+
190
+ ### Host App (you build these)
191
+
192
+ | Database | Content | How to Build |
193
+ |---|---|---|
194
+ | Features | Feature ownership, status, user stories, acceptance criteria | Human-authored in Notion |
195
+ | User Flows | User-facing flow documentation, triggers, endpoints | Human-authored (future: gem can auto-populate from controller/route extraction) |
196
+ | Data Handling | PII classification, encryption, retention policies | Human policy decisions |
197
+ | Deploys | Build numbers, environments, deployers, commit SHAs | Buildkite webhook → Rails controller → Notion API |
198
+
199
+ ### Example: Deploy Tracking (Host App)
200
+
201
+ ```ruby
202
+ # app/controllers/webhooks/buildkite_controller.rb
203
+ module Webhooks
204
+ class BuildkiteController < ApplicationController
205
+ skip_before_action :verify_authenticity_token
206
+ before_action :verify_buildkite_token
207
+
208
+ def create
209
+ payload = JSON.parse(request.body.read)
210
+ NotionDeploySyncJob.perform_later(payload)
211
+ head :ok
212
+ end
213
+ end
214
+ end
215
+
216
+ # app/jobs/notion_deploy_sync_job.rb
217
+ class NotionDeploySyncJob < ApplicationJob
218
+ def perform(payload)
219
+ build = payload["build"]
220
+ # Use your own NotionClient to create a Deploys page
221
+ # This is outside Woods's scope
222
+ end
223
+ end
224
+ ```
225
+
226
+ ## Rate Limiting
227
+
228
+ Notion's API allows 3 requests per second. The gem's built-in rate limiter handles this automatically. For large codebases (100+ models), expect the sync to take a few minutes.
229
+
230
+ If you see 429 errors, the client retries up to 3 times with exponential backoff using the `Retry-After` header.
231
+
232
+ ### Retries and duplicates
233
+
234
+ A 429 is always retried, for any request, the server rejected it before doing any work. A 503 is different: an intermediary in front of Notion's API can synthesize a 503 for a request the origin already committed, so blindly retrying it risks creating a duplicate page. The client only retries a 503 for **idempotent** requests (reads, and page updates keyed by an existing page id). Page *creation* has no idempotency key, so a 503 on `create_page` is raised immediately instead of retried, you'll see the error and can re-run the sync, which is safe because the manifest either already recorded the created page or will create it fresh.
235
+
236
+ ## Error Handling
237
+
238
+ The sync collects errors per-model and per-column without stopping. The stats hash returned by `sync_all` includes an `errors` array:
239
+
240
+ ```ruby
241
+ stats = exporter.sync_all
242
+ stats[:errors] # => ["User: Notion API error 400: ...", ...]
243
+ ```
244
+
245
+ Common errors:
246
+ - **401 Unauthorized**: Check your API token
247
+ - **404 Not Found**: Check database IDs and ensure the integration has access
248
+ - **400 Validation Error**: Check that Notion database properties match the expected schema above
249
+ - **429 Rate Limited**: Automatic retry (up to 3 times)
250
+
251
+ `rake woods:notion_sync` prints the first five errors and then **exits 1**, matching `woods:unblocked_sync` and `woods:obsidian`. A printed-but-green run is invisible in a post-merge pipeline: a dead token would otherwise keep the job green while the Notion database went stale indefinitely. Whatever synced before the failure is durable, so re-running after the fix converges. The Buildkite examples above use `soft_fail: true`; drop it if you want the sync failure to fail the build.
252
+
253
+ ## Architecture
254
+
255
+ ```
256
+ Extraction Output (JSON on disk)
257
+ ↓
258
+ ┌─────────────────────┐
259
+ │ IndexReader │ ← Reads model, column, migration data
260
+ └─────────────────────┘
261
+ ↓
262
+ ┌─────────────────────┐
263
+ │ Exporter │ ← Orchestrates sync flow
264
+ ├─────────────────────┤
265
+ │ ModelMapper │ ← Maps ExtractedUnit → Notion Data Models properties
266
+ │ ColumnMapper │ ← Maps column metadata → Notion Columns properties
267
+ │ MigrationMapper │ ← Extracts latest migration dates per table
268
+ └─────────────────────┘
269
+ ↓
270
+ ┌─────────────────────┐
271
+ │ Client │ ← Notion API wrapper (Net::HTTP, rate-limited)
272
+ │ RateLimiter │ ← 3 req/sec token bucket
273
+ └─────────────────────┘
274
+ ↓
275
+ Notion API
276
+ ```
277
+
278
+ ## Future Extensions
279
+
280
+ - **User Flows**: Auto-populate from controller/route extraction (controller actions, HTTP methods, filters, dependencies)
281
+ - **Data Handling**: Surface column types and model metadata to assist with PII classification
282
+ - **ERD Diagrams**: Generate and attach relationship diagrams to Data Models pages
283
+ - **Diff Reporting**: Use temporal snapshots to show what changed between syncs
@@ -0,0 +1,170 @@
1
+ # Obsidian Integration
2
+
3
+ Export Woods extraction artifacts to a self-contained [Obsidian](https://obsidian.md) vault, a
4
+ folder of interlinked Markdown notes, one per extracted unit. The vault is designed to be read two
5
+ ways at once:
6
+
7
+ - **By humans**: explore the app's structure in Obsidian's graph view, filter and sort units in an
8
+ Obsidian [Bases](https://help.obsidian.md/bases) table, and drill into a single unit's note with its
9
+ dependencies and dependents as clickable wikilinks.
10
+ - **By agents**: load the entire dependency topology from a single `_woods/` sidecar (one read,
11
+ no per-note fan-out), with a stable `id → note path` manifest for navigation.
12
+
13
+ Unlike the Notion and Unblocked exporters, this one writes **local files only**: there is no API
14
+ token, no network call, and no rate limit. An Obsidian vault is just a folder.
15
+
16
+ ## Quick start
17
+
18
+ ```bash
19
+ # 1. Extract (produces tmp/woods/ with the dependency graph + per-unit JSON)
20
+ bundle exec rake woods:extract
21
+
22
+ # 2. Generate the vault (defaults to tmp/woods/obsidian_vault/)
23
+ bundle exec rake woods:obsidian
24
+ ```
25
+
26
+ Then in Obsidian: **Open folder as vault** → point it at `tmp/woods/obsidian_vault`. For the full
27
+ experience (graph colors, Bases, link format) open the generated folder *as its own vault* rather than
28
+ nesting it inside an existing one, a nested folder inherits the host vault's config and ignores the
29
+ shipped `.obsidian/` settings.
30
+
31
+ ## What gets generated
32
+
33
+ ```
34
+ obsidian_vault/
35
+ ├── .woods-vault # ownership sentinel (marks this dir as woods-managed)
36
+ ├── .obsidian/ # vault config, only written into a woods-owned vault
37
+ │ ├── app.json # newLinkFormat: absolute, useMarkdownLinks: false
38
+ │ ├── types.json # property types (pagerank → number, tags → tags)
39
+ │ └── graph.json # color groups by #woods/<type> (global graph)
40
+ ├── _woods/ # machine sidecar (the agent interface)
41
+ │ ├── manifest.json # { notes: {id → {path,type,pagerank,tags}}, paths: {path → id} }
42
+ │ ├── dependency_graph.json # verbatim copy, full topology for traversal
43
+ │ └── graph_analysis.json # verbatim copy, hubs / cycles / orphans / bridges
44
+ ├── Units.base # Obsidian Bases view (filterable/sortable unit inventory)
45
+ ├── _Overview.md # top-level map of contents
46
+ ├── models/
47
+ │ ├── _index.md # MOC: every model as a wikilink
48
+ │ └── User.md
49
+ ├── controllers/
50
+ │ └── Users__RegistrationsController.md
51
+ └── … # one folder per extracted unit type
52
+ ```
53
+
54
+ ## A unit note
55
+
56
+ Each note carries **flat YAML frontmatter** (the machine-queryable surface) and a Markdown body with
57
+ wikilinks (the human surface). Frontmatter is kept flat on purpose. Obsidian's Properties UI does
58
+ not support nested objects or arrays-of-objects, so the structured edge data lives in the `_woods/`
59
+ sidecar instead.
60
+
61
+ ```markdown
62
+ ---
63
+ woods_managed: true
64
+ id: User
65
+ type: model
66
+ file: app/models/user.rb
67
+ source_hash: a3c5f8e9
68
+ pagerank: 0.0421
69
+ dependency_count: 3
70
+ dependent_count: 12
71
+ tags:
72
+ - woods/model
73
+ - woods/hub
74
+ aliases:
75
+ - User
76
+ ---
77
+
78
+ # User
79
+
80
+ **File:** `app/models/user.rb` | **LOC:** 84 | **Table:** users (17 columns)
81
+
82
+ > [!warning] Hub, high blast radius
83
+ > 12 units depend on this (PageRank 0.0421).
84
+
85
+ ## Depends on
86
+ - [[models/Account|Account]], *belongs_to*
87
+ - [[mailers/WelcomeMailer|WelcomeMailer]], *job_enqueue*
88
+
89
+ ## Used by (12)
90
+ **controllers:** [[controllers/Users__RegistrationsController|Users::RegistrationsController]]
91
+ **jobs:** [[jobs/SyncProfileJob|SyncProfileJob]]
92
+ ```
93
+
94
+ Wikilinks are path-qualified with an alias (`[[models/Account|Account]]`): the target is the
95
+ sanitized vault path so the link always resolves, and the alias shows the original identifier. The
96
+ note's `# H1` carries the clean identifier so the sanitized filename never shows as the title.
97
+
98
+ ## The three visualizer surfaces
99
+
100
+ | Surface | Best for | Notes |
101
+ |---|---|---|
102
+ | **Graph view** | seeing connections / blast radius at a glance | colored by `#woods/<type>` tag (global graph); for the *local* graph, create the color-by-tag groups once. Obsidian persists them per-vault |
103
+ | **Bases (`Units.base`)** | inventory & triage, "all models sorted by PageRank", "show me the hubs" | requires Obsidian ≥ 1.9 (cards/list views ≥ 1.10); inert and harmless on older versions |
104
+ | **Note bodies + backlinks** | drilling into one unit and navigating outward | "Depends on" / "Used by" are clickable; Obsidian's backlink panel shows inbound links |
105
+
106
+ ## Configuration
107
+
108
+ All options are passed to the rake task via environment variables, there are **no global
109
+ `Woods.configure` settings** for this exporter (the vault path is an output location, not a
110
+ credential):
111
+
112
+ | Env var | Default | Effect |
113
+ |---|---|---|
114
+ | `WOODS_OUTPUT` | `config.output_dir` (`tmp/woods`) | extraction directory to read from |
115
+ | `WOODS_OBSIDIAN_VAULT` | `<output>/obsidian_vault` | where to write the vault |
116
+ | `WOODS_OBSIDIAN_INCLUDE_SOURCE` | off | embed each unit's source code (credential-scrubbed) in its note |
117
+ | `WOODS_OBSIDIAN_INCLUDE_FRAMEWORK` | off | include `rails_source` units (large; off by default) |
118
+ | `WOODS_OBSIDIAN_FORCE_PURGE` | off | bypass the 30% mass-deletion guard during the stale-note sweep |
119
+
120
+ ```bash
121
+ WOODS_OBSIDIAN_VAULT=~/notes/myapp-code WOODS_OBSIDIAN_INCLUDE_SOURCE=1 bundle exec rake woods:obsidian
122
+ ```
123
+
124
+ Or call the exporter directly:
125
+
126
+ ```ruby
127
+ require 'woods/obsidian/vault_exporter'
128
+
129
+ Woods::Obsidian::VaultExporter.new(
130
+ index_dir: 'tmp/woods',
131
+ vault_path: 'tmp/woods/obsidian_vault',
132
+ include_source: false,
133
+ include_framework: false
134
+ ).export_all
135
+ # => { exported: 412, indexes: 19, swept: 3, skipped: 0, errors: [] }
136
+ ```
137
+
138
+ ## Re-running and safety
139
+
140
+ The exporter **fully regenerates** the vault on every run (no incremental manifest, local writes are
141
+ cheap). Output is deterministic: re-running against an unchanged extraction produces byte-identical
142
+ notes, so unchanged units never show up in a git diff.
143
+
144
+ Notes Woods manages carry `woods_managed: true` in their frontmatter. On each run, after all notes are
145
+ written successfully, a **sweep** removes managed notes whose unit no longer exists, so deletions in
146
+ your code propagate. Several guards make the sweep safe to point at a real vault:
147
+
148
+ - It only runs against a directory carrying the `.woods-vault` sentinel (written by Woods on first run).
149
+ - It only deletes notes with the `woods_managed: true` marker, your own notes are never touched.
150
+ - It refuses to delete more than 30% of managed notes at once (the signature of a partial extraction)
151
+ unless `WOODS_OBSIDIAN_FORCE_PURGE=1` is set.
152
+ - It resolves symlinks and confirms every deletion target is inside the vault root.
153
+ - It is skipped entirely if any note failed to write that run (a stale note is harmless; a deleted
154
+ reviewed note is not).
155
+
156
+ The same ownership check guards the `.obsidian/` config: Woods will **not** overwrite an existing,
157
+ foreign `.obsidian/` folder, it leaves your Obsidian settings untouched and warns instead.
158
+
159
+ ## Limitations
160
+
161
+ - **Bases needs Obsidian ≥ 1.9** (≥ 1.10 for card/list views). The `.base` file is harmless on older
162
+ versions, it simply doesn't render.
163
+ - **`gem_source` units are not exported.** They aren't reachable through the index reader; only
164
+ `rails_source` is covered by `include_framework`.
165
+ - **Hand-edits diverge.** The vault is meant to be regenerated. Editing a note's properties in
166
+ Obsidian rewrites its frontmatter, after which a re-export will overwrite your changes.
167
+ - **Nested vaults ignore the shipped config.** Open the generated folder as its own vault for graph
168
+ colors, Bases, and link-format settings to apply.
169
+
170
+ See `docs/AGENT_GUIDE.md` for how an agent consumes the `_woods/` sidecar.