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,279 @@
1
+ # Unblocked Integration
2
+
3
+ Woods can sync extraction data to [Unblocked](https://getunblocked.com) via its
4
+ Documents API. This gives Unblocked's automated code review and Q&A tools
5
+ structural codebase context, associations, blast radius, entry points, side
6
+ effects, alongside the institutional context (PRs, Slack, tickets) it already
7
+ provides.
8
+
9
+ ## How It Works
10
+
11
+ 1. **Extract** codebase data with `woods:extract` (the usual pipeline)
12
+ 2. **Sync** to Unblocked with `woods:unblocked_sync` (or the alias `woods:relay`)
13
+ 3. Documents appear in your Unblocked collection within ~1 minute
14
+ 4. Unblocked's code review agent and Q&A tools reference the structural context
15
+
16
+ Documents are **upserted by URI**: running sync again updates existing documents
17
+ without creating duplicates. URIs point to your GitHub repository for working
18
+ citation links in Unblocked answers.
19
+
20
+ ## Setup
21
+
22
+ ### 1. Create an Unblocked Collection
23
+
24
+ Collections for custom sources are created **via the API** (the web app does
25
+ not currently offer a creation path for them):
26
+
27
+ ```bash
28
+ curl -X POST https://getunblocked.com/api/v1/collections \
29
+ -H "Authorization: Bearer $UNBLOCKED_API_TOKEN" \
30
+ -H "Content-Type: application/json" \
31
+ -d '{"name": "Codebase Architecture", "description": "Structural metadata from Woods extraction, models, controllers, services, dependencies, and blast radius analysis.", "iconUrl": "https://raw.githubusercontent.com/lost-in-the/woods/main/assets/woods-mark-black.svg"}'
32
+ ```
33
+
34
+ > **Known API quirk:** the live API rejects collection creation with a bare
35
+ > `400 Bad Request` unless `iconUrl` is included, even though the API docs mark
36
+ > it optional. Always pass an `iconUrl`, the Woods mark above is a stable,
37
+ > repo-hosted square SVG you can use directly.
38
+
39
+ Creating the collection from Ruby is simpler, `Client#create_collection`
40
+ defaults `iconUrl` to the Woods mark (`Client::DEFAULT_ICON_URL`), so the quirk
41
+ can't bite:
42
+
43
+ ```ruby
44
+ require 'woods/unblocked/client'
45
+
46
+ client = Woods::Unblocked::Client.new(api_token: ENV['UNBLOCKED_API_TOKEN'])
47
+ collection = client.create_collection(
48
+ name: 'Codebase Architecture',
49
+ description: 'Structural metadata from Woods extraction, models, ' \
50
+ 'controllers, services, dependencies, and blast radius analysis.'
51
+ )
52
+ collection['id'] # => use as UNBLOCKED_COLLECTION_ID
53
+ ```
54
+
55
+ Pass `icon_url:` to use your own icon instead of the default.
56
+
57
+ ### 2. Create an API Token
58
+
59
+ In the Unblocked web app: **Settings** → **API Tokens** → **Create Token**.
60
+
61
+ - **Personal Access Token**: 1,000 API calls/day, scoped to your account
62
+ - **Team Access Token**: access to all team documents (recommended for CI)
63
+
64
+ ### 3. Configure Woods
65
+
66
+ Via environment variables (recommended for CI):
67
+
68
+ ```bash
69
+ export UNBLOCKED_API_TOKEN="ubk_..."
70
+ export UNBLOCKED_COLLECTION_ID="12345678-abcd-..."
71
+ export UNBLOCKED_REPO_URL="https://github.com/your-org/your-repo"
72
+ ```
73
+
74
+ Or in your initializer:
75
+
76
+ ```ruby
77
+ Woods.configure do |config|
78
+ config.unblocked_api_token = ENV["UNBLOCKED_API_TOKEN"]
79
+ config.unblocked_collection_id = ENV["UNBLOCKED_COLLECTION_ID"]
80
+ config.unblocked_repo_url = "https://github.com/your-org/your-repo"
81
+ end
82
+ ```
83
+
84
+ ### 4. Run the Sync
85
+
86
+ ```bash
87
+ bundle exec rake woods:extract # Extract codebase data
88
+ bundle exec rake woods:unblocked_sync # Sync to Unblocked
89
+ ```
90
+
91
+ Or in Docker:
92
+
93
+ ```bash
94
+ docker compose exec app bin/rails woods:extract
95
+ docker compose exec app bin/rails woods:unblocked_sync
96
+ ```
97
+
98
+ ## What Gets Synced
99
+
100
+ | Unit Type | Strategy | Typical Count |
101
+ |-----------|----------|---------------|
102
+ | Models | All | 100-300 |
103
+ | Controllers | All | 100-400 |
104
+ | Services | All | 20-50 |
105
+ | Jobs | All | 50-150 |
106
+ | GraphQL types/resolvers | All | 100-400 |
107
+ | Concerns | All | 20-50 |
108
+ | Mailers | All | 3-10 |
109
+ | Managers | All | 10-30 |
110
+ | Decorators | All | 10-30 |
111
+ | POROs | Top 100 by dependents | 100 max |
112
+ | Libs | Top 50 by dependents | 50 max |
113
+
114
+ Total: ~800-1200 documents depending on app size.
115
+
116
+ ## Document Format
117
+
118
+ Each document is a condensed Markdown profile optimized for code review context.
119
+
120
+ **Models** (highest value) include:
121
+ - Association summary grouped by type (belongs_to/has_many/has_one)
122
+ - Dependent count by type with blast radius assessment
123
+ - Entry points (controllers, GraphQL resolvers, jobs)
124
+ - Schema highlights (enums, scopes, concerns, callbacks)
125
+ - Side effects (workers, mailers triggered)
126
+
127
+ **Controllers** include routes, inheritance chain, model dependencies, view templates.
128
+
129
+ **Services/Jobs/Other types** include dependencies, dependents summary, key
130
+ structural data.
131
+
132
+ ## URI Scheme
133
+
134
+ Document URIs point to the source file on GitHub:
135
+
136
+ ```
137
+ https://github.com/your-org/your-repo/blob/main/app/models/order.rb
138
+ ```
139
+
140
+ This means Unblocked citations link directly to the relevant source code.
141
+
142
+ ## Rate Limits
143
+
144
+ The Unblocked API allows 1,000 calls per day (resets at midnight PST). A typical
145
+ sync uses ~800-1200 calls for the initial build. If your app exceeds 1,000 units:
146
+
147
+ - The sync stops gracefully when the budget is exhausted
148
+ - Re-run the next day to continue where it left off (upsert is idempotent)
149
+ - Set `UNBLOCKED_DAILY_BUDGET` to adjust the limiter if your plan allows more
150
+
151
+ ## CI Integration
152
+
153
+ Add a post-merge step. The sync is incremental (see below), so the one thing CI
154
+ must do beyond running the task is **persist the sync manifest between runs**, otherwise every deploy starts from scratch and re-pushes everything.
155
+
156
+ ### GitHub Actions
157
+
158
+ `actions/cache` entries are **immutable**: an exact-key hit skips the post-job
159
+ save, which would freeze the manifest at its first-run state forever. Use a
160
+ unique key plus `restore-keys` so every run saves a fresh manifest and the next
161
+ run restores the most recent one:
162
+
163
+ ```yaml
164
+ env:
165
+ UNBLOCKED_COLLECTION_ID: "..."
166
+
167
+ steps:
168
+ - uses: actions/cache@v4
169
+ with:
170
+ path: tmp/woods/unblocked_sync_manifest.json
171
+ key: unblocked-sync-${{ env.UNBLOCKED_COLLECTION_ID }}-${{ github.run_id }}
172
+ restore-keys: |
173
+ unblocked-sync-${{ env.UNBLOCKED_COLLECTION_ID }}-
174
+ - run: bin/rails woods:extract
175
+ - run: bin/rails woods:unblocked_sync
176
+ env:
177
+ UNBLOCKED_API_TOKEN: "ubk_..."
178
+ UNBLOCKED_REPO_URL: "https://github.com/your-org/your-repo"
179
+ ```
180
+
181
+ ### Buildkite
182
+
183
+ Buildkite has no native cache primitive. Note that plain
184
+ `buildkite-agent artifact download` is **scoped to the current build**: it
185
+ cannot restore a previous build's manifest on its own. Use the S3-backed
186
+ [cache plugin](https://github.com/buildkite-plugins/cache-buildkite-plugin)
187
+ (simplest), or resolve the last successful build's ID via the REST API and pass
188
+ `--build`:
189
+
190
+ ```yaml
191
+ - label: ":trees: Woods → Unblocked"
192
+ plugins:
193
+ - cache#v1.7.0:
194
+ manifest: Gemfile.lock # any stable file; key below does the work
195
+ path: tmp/woods/unblocked_sync_manifest.json
196
+ restore: pipeline
197
+ save: pipeline
198
+ command:
199
+ - bin/rails woods:extract
200
+ - bin/rails woods:unblocked_sync
201
+ branches: main
202
+ env:
203
+ UNBLOCKED_API_TOKEN: "ubk_..."
204
+ UNBLOCKED_COLLECTION_ID: "..."
205
+ UNBLOCKED_REPO_URL: "https://github.com/your-org/your-repo"
206
+ ```
207
+
208
+ If you prefer the artifact API, the restore step must target a previous build
209
+ explicitly, e.g.
210
+ `buildkite-agent artifact download --build "$(previous_passed_build_id)" "tmp/woods/unblocked_sync_manifest.json" .`
211
+ where `previous_passed_build_id` queries the REST API for the last passed build
212
+ on the pipeline. Declaring `artifact_paths: tmp/woods/**/*` still uploads the
213
+ fresh manifest for free at the end of each run.
214
+
215
+ > **Concurrency:** two syncs racing (e.g. per-deploy CI on quick successive
216
+ > merges) can interleave manifest writes and put/delete calls. Gating this is
217
+ > the host pipeline's responsibility, set a Buildkite `concurrency_group` (or
218
+ > the GitHub Actions `concurrency:` key) on the sync step.
219
+
220
+ ## Incremental Updates
221
+
222
+ The sync is incremental. A **sync manifest**
223
+ (`<output_dir>/unblocked_sync_manifest.json`) records the content hash and remote
224
+ document id of everything last pushed. On each run the exporter:
225
+
226
+ - **skips** documents whose built body hash is unchanged,
227
+ - **pushes** only new or changed documents,
228
+ - **deletes** documents whose source unit has disappeared.
229
+
230
+ Documents are upserted by URI, so the sync is always safe to re-run. If the
231
+ manifest is missing (first run, or a CI cache miss) the exporter reconciles
232
+ document ids from the remote collection and falls back to a full re-push,
233
+ rebuilding the manifest, correct, just more API calls than one run. In steady
234
+ state an unchanged codebase costs ~0 calls.
235
+
236
+ Pair with `woods:incremental` to re-extract only changed files; the sync then
237
+ pushes only the documents whose content actually changed.
238
+
239
+ ### Escape hatches
240
+
241
+ - `UNBLOCKED_FORCE_FULL_SYNC=1`: re-push every document, ignoring the unchanged
242
+ check (still uses the manifest for deletes). Use after a `DocumentBuilder`
243
+ format change, which alters every body.
244
+ - `UNBLOCKED_FORCE_PURGE=1`: bypass the mass-deletion guard. The guard refuses
245
+ to delete more than 30% of a manifest tracking ≥10 documents in one run, which
246
+ protects against running the sync against a **partial index** (e.g.
247
+ `woods:incremental` output in a fresh directory), where the current unit set
248
+ is a small subset and an unguarded purge would wipe the collection.
249
+
250
+ The guard also fires on *intentional* large removals: dropping a unit type
251
+ from the sync set, changing `unblocked_repo_url` (every URI changes), or a
252
+ big codebase deletion can all legitimately exceed 30%. The refusal warning
253
+ names the counts, if the deletions are expected, re-run once with
254
+ `UNBLOCKED_FORCE_PURGE=1`.
255
+
256
+ ## Troubleshooting
257
+
258
+ **"call budget exhausted for this run"**: The per-run cap
259
+ (`UNBLOCKED_DAILY_BUDGET`, default 1000) stopped the sync. This is a guard rail
260
+ against one run spending the whole allowance; it is **not** a reading of what
261
+ your token has left. The real 1,000 call/day limit is enforced server-side and
262
+ resets at midnight PST, so a fresh run starts the local counter at zero
263
+ regardless of what earlier runs (or other machines sharing the token) consumed.
264
+ Raise the cap if a legitimate cold sync needs more, or use a Team Access Token
265
+ if higher server-side limits are available.
266
+
267
+ **"Unblocked API error 401"**: Check your API token. Personal tokens are scoped
268
+ to your account; Team tokens access all team data.
269
+
270
+ **"collection_id is required"**: Set `UNBLOCKED_COLLECTION_ID` env var or
271
+ configure `config.unblocked_collection_id`.
272
+
273
+ **Documents not appearing in answers**: Documents take ~1 minute to become
274
+ available. Also verify the collection is enabled in your Unblocked data source
275
+ settings.
276
+
277
+ ## Retries and Duplicates
278
+
279
+ 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 Unblocked's API can synthesize a 503 for a request the origin already committed, so a blind retry risks creating a duplicate. The client only retries a 503 for **idempotent** requests (reads, and document upserts keyed by URI). Collection *creation* has no idempotency key, so a 503 on `create_collection` is raised immediately instead of retried. Document upserts and syncs are unaffected, they're idempotent by URI, so a 503 there retries normally.
@@ -0,0 +1,321 @@
1
+ # Upgrade from Woods 1.x to 2.0
2
+
3
+ Woods 2.0 changes observable index identifiers, publication layout, vector-store reconciliation, and the supported MCP surface. Plan a clean re-index. Do not upgrade a shared or durable index in place without a backup and a rollback window.
4
+
5
+ This guide assumes the last v1 release, 1.6.1, and targets 2.0.0.
6
+
7
+ <!-- release-state:upgrade-availability -->
8
+ > RubyGems lists 2.0.0.beta2 as a prerelease. Pin it explicitly with
9
+ > `gem "woods", "2.0.0.beta2"`; `~> 2.0` resolves only once
10
+ > 2.0.0 is published.
11
+ <!-- release-state:end -->
12
+
13
+ ## Upgrade outcome
14
+
15
+ After this runbook you will have:
16
+
17
+ - Woods 2.0 selected in the application bundle;
18
+ - reviewed v2 configuration and migration state;
19
+ - a clean v2 extraction with corrected identifiers;
20
+ - rebuilt embeddings and exports if you use them;
21
+ - an MCP client connected to the v2 packaged tool surface;
22
+ - a documented way back to v1 if verification fails.
23
+
24
+ ## What changes
25
+
26
+ | v2 change | What can break | Required response |
27
+ |---|---|---|
28
+ | Correct namespaced and constrained-route identifiers | Saved identifiers, external links, retrieval vectors, and exports can miss renamed units | Clean extract; rebuild embeddings and exports |
29
+ | Wrapper-nested class identifiers resolved to the file's own constant | Files nested in class namespaces (e.g. `app/services/domain/container/parser.rb` defining `module Domain; class Container; class Parser`) gain identifiers like `Domain::Container::Parser` instead of sharing the wrapper's; layouts that still derive one type+identifier from two different files abort extraction naming both files | Clean extract; rebuild embeddings and exports |
30
+ | Typed graph identity variants | Custom graph consumers may assume one node per identifier | Re-index; update custom consumers to handle type variants |
31
+ | Atomic generations via `generation.json` | Custom scripts that read root `manifest.json` may fail | Follow the payload pointer or use Woods readers/tasks |
32
+ | `mcp >= 1.2, < 2.0` and protocol negotiation | Old lockfiles or manually pinned protocol versions can fail | Bundle update Woods/MCP; normally leave protocol version unset |
33
+ | Index MCP surface aligned to executable wiring | Agents may ask for tools that only exist as conditional schemas | Update agent instructions to the 14-tool default |
34
+ | Console surface tightened to 9 or 11 tools | Agents may ask for Tier 2/3 or eval schemas that do not execute | Use registered default/read tools only |
35
+ | Missing-token behavior changed outside production | An enabled Console HTTP endpoint now stays mounted but returns 401 without a valid token; production still refuses to boot without one | Preserve or configure a secret token of at least 32 characters; send it only to the HTTP transport |
36
+ | Durable-store reconciliation and a 30% purge guard | The first v2 embed may refuse a legitimate rename-heavy deletion | Back up, inspect the deletion, then use the one-run override only if correct |
37
+ | Embedding dimension preflight | A previously tolerated model/store mismatch now fails before writing | Rebuild into a store with the configured dimension |
38
+ | Export reconciliation guards | Obsidian or Unblocked can refuse a rename-heavy stale-document sweep | Back up and use exporter-specific override only after review |
39
+ | Notion column pages are grouped by physical table | Models sharing a table (STI, a shared `self.table_name`) previously rewrote each other's column pages on every run | Re-sync once after re-extraction; the shared pages settle and the churn stops |
40
+ | One-shot extraction tasks fail when the generation marker cannot be published | A run that wrote a payload readers cannot reach used to print success and exit 0 | Fix the write failure and re-run; the previous generation stays active meanwhile |
41
+ | `woods:incremental` and `woods:refresh` refuse an output directory with no baseline index | A restored-cache miss, a typo'd `WOODS_OUTPUT`, or a fresh runner now fails instead of publishing a near-empty index | Run a full `woods:extract` first, or point `WOODS_OUTPUT` at the directory holding the existing index |
42
+ | `woods:incremental` exits 1 over a git range it cannot resolve | An unresolvable base ref used to read as "nothing changed" and exit 0 | Fetch the base ref, set `CHANGED_FILES`, or accept the stand-down a live watch daemon provides |
43
+ | `woods:embed`, `woods:embed_incremental`, and `woods:notion_sync` exit 1 on reported errors | CI jobs that were green while every unit or page failed now fail | Read the printed errors, fix the cause, re-run; completed work is durable |
44
+ | The Index MCP `reload` tool needs write access to the index directory | A read-only index mount can serve structural reads but cannot reload in place | Grant write access, or restart the MCP process after publishing |
45
+ | `config.extractors` and `config.add_gem` warn as unimplemented | Old config may imply filtering that never occurred | Remove or comment the settings; do not rely on them |
46
+ | New watch, refresh, and evaluation tasks | New operational options become available | Optional; no migration action |
47
+
48
+ ## Before changing the bundle
49
+
50
+ ### 1. Record the current installation
51
+
52
+ Run in the same environment that boots Rails:
53
+
54
+ ```bash
55
+ git status --short --branch
56
+ bundle exec ruby -rwoods/version -e 'puts Woods::VERSION'
57
+ bundle exec rails woods:stats
58
+ bundle exec rails woods:validate
59
+ ```
60
+
61
+ Record the current Woods version, output directory, storage preset/providers, embedding model and dimension, Console configuration, and enabled exports. For Console, record the transport, token source or presence without recording its value, allowed origins, path, and embedded read-tool setting. Save current MCP client configuration and any custom scripts that read `tmp/woods/` directly.
62
+
63
+ ### 2. Back up durable data
64
+
65
+ The generated structural index can be recreated, but its location may also hold local vector dumps and exporter manifests. Copy or snapshot the complete configured output directory before cleaning it.
66
+
67
+ Back up external vector stores separately:
68
+
69
+ | Store | Backup |
70
+ |---|---|
71
+ | pgvector | Database/schema snapshot or `pg_dump` of the configured vector table |
72
+ | Qdrant | Collection snapshot through Qdrant's snapshot API |
73
+ | `:local` or `:shared_filesystem` | Copy the configured output directory, including `dumps/` |
74
+
75
+ Also back up managed Obsidian/Unblocked destinations before allowing a mass stale-document cleanup. Notion does not delete old pages during reconciliation, but save its sync manifest with the output directory.
76
+
77
+ ### 3. Choose a rollback point
78
+
79
+ Keep the v1 Gemfile/lockfile commit and all durable-store backups until v2 extraction, MCP calls, retrieval, and exports are verified. Downgrading the gem does not translate v2 identifiers back to v1.
80
+
81
+ ## Upgrade the application
82
+
83
+ ### 1. Update Woods without broad dependency churn
84
+
85
+ Change the development dependency:
86
+
87
+ ```ruby
88
+ gem "woods", "~> 2.0"
89
+ ```
90
+
91
+ Then update only Woods and the dependencies Bundler requires:
92
+
93
+ ```bash
94
+ bundle update woods
95
+ bundle exec ruby -rwoods/version -e 'puts Woods::VERSION'
96
+ ```
97
+
98
+ Confirm the result is 2.0.0 and the lockfile resolves `mcp` at `>= 1.2, < 2.0`.
99
+
100
+ ### 2. Review configuration
101
+
102
+ Generate the v2 template only when you can inspect conflicts safely. Do not overwrite an existing initializer blindly. Compare your initializer with the v2 [Configuration reference](CONFIGURATION_REFERENCE.md).
103
+
104
+ Pay particular attention to:
105
+
106
+ - `output_dir` and environment overrides;
107
+ - storage and embedding provider settings;
108
+ - the configured embedding model/dimension;
109
+ - `console_mcp_enabled`, the `console_mcp_token` secret source, allowed origins, path, and embedded read-tool flags;
110
+ - snapshot, session, Notion, Obsidian, and Unblocked settings;
111
+ - old `config.extractors` or `config.add_gem` calls, which are not implemented selectors.
112
+
113
+ Review existing Woods migrations and tables before accepting any newly generated migration. Do not create duplicate `woods_units`, `woods_edges`, or `woods_embeddings` tables.
114
+
115
+ ### 3. Clean and re-extract
116
+
117
+ After the backup is verified:
118
+
119
+ ```bash
120
+ bin/rails woods:clean
121
+ bin/rails woods:extract
122
+ bin/rails woods:validate
123
+ bin/rails woods:stats
124
+ ```
125
+
126
+ The clean extract is required for corrected identifier shapes. Do not use an incremental run as the first v2 extraction: after `woods:clean` there is no baseline, and v2 `woods:incremental` refuses that state rather than publishing a near-empty index as the application's complete truth.
127
+
128
+ An interrupted extraction leaves readers on the last complete generation because Woods publishes `generation.json` only after the payload is complete. Re-run the task; do not delete a partial directory speculatively. A run that completes its payload but cannot publish the marker now fails loudly instead of reporting success, so treat a non-zero exit as work to redo rather than as a partial success.
129
+
130
+ ## Rebuild optional systems
131
+
132
+ ### Embeddings
133
+
134
+ If semantic retrieval is enabled:
135
+
136
+ ```bash
137
+ bin/rails woods:embed
138
+ ```
139
+
140
+ The first v2 run reconciles durable vectors with the new identifiers. A purge guard refuses deletion of more than 30% of the store or a purge into an empty extraction.
141
+
142
+ If it refuses:
143
+
144
+ 1. Confirm extraction validation passed and unit counts are plausible.
145
+ 2. Compare the proposed deletion with the expected identifier rename.
146
+ 3. Confirm the vector-store backup is restorable.
147
+ 4. Only then authorize one run:
148
+
149
+ ```bash
150
+ WOODS_ALLOW_PURGE=1 bin/rails woods:embed
151
+ ```
152
+
153
+ The override permits deletion; it is not a repair command. Do not set it permanently.
154
+
155
+ If Woods reports a dimension mismatch, verify the configured embedding model. Rebuild into a store created for the new dimension. Vectors cannot be converted in place.
156
+
157
+ An interrupted embed is safe to re-run; durable checkpoints resume or repair the missing unit.
158
+
159
+ ### Exports
160
+
161
+ Re-run every export after extraction and embeddings are verified. Renamed identifiers appear as removal of the old document plus addition of the new one.
162
+
163
+ | Export | v2 behavior |
164
+ |---|---|
165
+ | Notion | Adds/updates current units and prunes manifest entries; it does not delete old Notion pages |
166
+ | Obsidian | Sweeps stale Woods-managed notes; refuses deletion beyond 30% unless `WOODS_OBSIDIAN_FORCE_PURGE` is explicitly set |
167
+ | Unblocked | Reconciles its sync manifest; for manifests with 10+ documents, refuses deletion beyond 30% unless `UNBLOCKED_FORCE_PURGE` is explicitly set |
168
+
169
+ Review the target and backup before any force-purge override. Use `WOODS_NOTION_FORCE=1` only when you intentionally want Notion to re-check unchanged content hashes.
170
+
171
+ **Notion needs one settling re-sync.** v2 groups column pages by physical table instead of by model, so models that share a table write one page per physical column, with the `Table` relation listing every owning model and their validations unioned. The page titles, and therefore the manifest keys, are unchanged, but the content hash of every shared-table column changes once. Expect the first post-upgrade `woods:notion_sync` to update those pages; subsequent runs skip them. This also ends the v1 behavior where two models sharing a table rewrote the same column page back and forth on every run. See [Notion integration](NOTION_INTEGRATION.md).
172
+
173
+ ## Update CI and scheduled automation
174
+
175
+ v2 tasks fail instead of printing an error and exiting 0. Review any pipeline that runs Woods unattended before the first v2 run:
176
+
177
+ | Task | v2 exit behavior |
178
+ |---|---|
179
+ | `woods:extract`, `woods:incremental`, `woods:refresh` | Raise when the payload cannot be published as a generation; the previous generation stays active |
180
+ | `woods:incremental` | Refuses an output directory with no baseline index, and exits 1 over a git range it cannot resolve unless a live watch daemon maintains the tree. See [Incremental extraction](INCREMENTAL_EXTRACTION.md) |
181
+ | `woods:embed`, `woods:embed_incremental`, `woods:notion_sync` | Exit 1 when the run reports errors, matching `woods:unblocked_sync` and `woods:obsidian` |
182
+
183
+ A job that restores the index directory from a cache must restore the whole thing. A missing or empty restore is exactly the state the baseline guard refuses, and the fix is a full `woods:extract` on that runner, not an override.
184
+
185
+ ## Update direct index consumers
186
+
187
+ Woods 2.0 publishes immutable payload directories and atomically points to the active one:
188
+
189
+ ```text
190
+ tmp/woods/
191
+ ├── generation.json
192
+ └── payloads/
193
+ └── gen-42/
194
+ ├── manifest.json
195
+ ├── dependency_graph.json
196
+ └── <type>/*.json
197
+ ```
198
+
199
+ Woods tasks, readers, exporters, and MCP servers resolve this automatically. Custom tooling must read `generation.json`, resolve its `payload` relative to the index root, reject paths that escape that root, and then read the payload files. A missing payload key represents the legacy flat layout.
200
+
201
+ Graph consumers must also tolerate multiple typed variants for the same textual identifier. Do not collapse nodes by identifier alone when type is part of identity.
202
+
203
+ The bundle requires patched MessagePack >=1.8.2 and JSON >=2.19.9, <3.
204
+ JSON 3 removes an encoder option used by older supported Rails versions; keep the
205
+ compatible JSON 2.x dependency when resolving the v2 bundle.
206
+
207
+ Embedding preserves coexisting types with internal `@woods-unit:` storage keys
208
+ (Base64-encoded JSON `[identifier, type]`, with the existing chunk suffix appended
209
+ when needed). Public identifiers and source attribution stay unchanged. Unique
210
+ ordinary names retain their previous keys; names beginning with this reserved
211
+ prefix are escaped too. An incremental embed migrates an ambiguous legacy key only
212
+ after storing its replacement vectors. Existing typed keys stay stable when one
213
+ variant disappears. Normal mass-deletion guards still apply to vanished units.
214
+ Custom vector consumers must treat storage IDs as opaque and use metadata for
215
+ public identifiers. Evaluation baselines normalize storage keys back to public
216
+ names and count same-named typed variants once, matching name-based ground truth.
217
+ Older dumps remain readable; re-embed to recover variants
218
+ that an older writer had already overwritten.
219
+
220
+ SQLite migration 007 preserves snapshot rows and permits one row per
221
+ `(snapshot_id, identifier, unit_type)`. JSON snapshot readers accept older untyped
222
+ records, while new records preserve both names and types. Lost historical variants
223
+ cannot be reconstructed from old snapshots. Back up `woods.sqlite3` and the whole
224
+ index before upgrading: reverting code alone does not reverse this migration.
225
+ Restore the matching backup or rebuild in a separate store when rolling back.
226
+
227
+ Flow documents for identifiers containing literal underscores now use a digest to
228
+ avoid collisions with namespaced controllers and combined action names. Read the
229
+ published flow index instead of constructing filenames. A full extraction rebuilds
230
+ precomputed flows consistently; use it when upgrading an index with existing flow
231
+ artifacts.
232
+
233
+ ## Reconnect MCP clients
234
+
235
+ Use the project bundle so the server and application resolve the same Woods version:
236
+
237
+ ```json
238
+ {
239
+ "mcpServers": {
240
+ "woods": {
241
+ "command": "bundle",
242
+ "args": ["exec", "woods-mcp-start", "./tmp/woods"],
243
+ "cwd": "/absolute/path/to/the-rails-app"
244
+ }
245
+ }
246
+ }
247
+ ```
248
+
249
+ Normally leave `MCP_PROTOCOL_VERSION` unset. The SDK negotiates with legacy clients through `initialize` and supports newer discovery in the same process. Pin only as a temporary workaround for a client known to require one revision; a pin reduces compatibility and is announced on stderr.
250
+
251
+ Update agent prompts that refer to the old inventory. Standard Index launch provides 14 tools. Standard Console launch provides nine, or eleven with explicitly enabled embedded read tools. See [MCP servers](MCP_SERVERS.md).
252
+
253
+ Structural reads still work from a read-only index mount, but the `reload` tool does not: its transactional refresh takes the same on-disk writer lock as extraction and embedding, so the MCP process needs write access to the index directory. Without it, `reload` returns a typed degraded error and keeps serving the previous aligned generation rather than swapping in a partial one. Grant write access, or restart the MCP process after publishing. [MCP servers](MCP_SERVERS.md) owns the detail.
254
+
255
+ ### Console users: preserve or configure the HTTP token
256
+
257
+ If Console MCP is disabled, no Console token action is required. If it is enabled, configure a token of at least 32 characters through a secret manager or environment variable rather than committing it to the initializer:
258
+
259
+ ```bash
260
+ WOODS_CONSOLE_MCP_TOKEN="$(openssl rand -hex 32)"
261
+ export WOODS_CONSOLE_MCP_TOKEN
262
+ ```
263
+
264
+ ```ruby
265
+ config.console_mcp_token = ENV.fetch("WOODS_CONSOLE_MCP_TOKEN")
266
+ ```
267
+
268
+ Persist the generated value in the application's normal secret store before opening a new shell or deploying. Never print, log, or commit the token during an agent-operated upgrade.
269
+
270
+ Rails-mounted Console HTTP clients must send the token as `Authorization: Bearer <token>`. With Console enabled, a missing token has these outcomes:
271
+
272
+ - outside production, Rails warns and the Console HTTP endpoint returns 401;
273
+ - in production, Rails refuses to boot;
274
+ - in every environment, a configured token shorter than 32 characters raises a configuration error.
275
+
276
+ The stdio Console transport does not send or authenticate with the bearer token. Outside production it can run without one, although Rails still warns because enabling Console also activates the guarded Rack endpoint. In production, Rails boot validation still requires the token even when stdio is the intended transport. The safest default is to configure the token whenever Console is enabled, or leave Console disabled.
277
+
278
+ See [Console MCP setup](CONSOLE_MCP_SETUP.md) for client examples and [Configuration reference](CONFIGURATION_REFERENCE.md) for the complete security settings.
279
+
280
+ ## Verify before rollout
281
+
282
+ Complete every applicable check:
283
+
284
+ - [ ] `bundle exec ruby -rwoods/version -e 'puts Woods::VERSION'` reports 2.0.0.
285
+ - [ ] Rails boots and eager-loads in the extraction environment.
286
+ - [ ] `woods:extract`, `woods:validate`, and `woods:stats` succeed.
287
+ - [ ] Expected namespaced and constrained identifiers appear.
288
+ - [ ] Custom index readers follow `generation.json`.
289
+ - [ ] `woods_status` reports the new generation.
290
+ - [ ] `search`, `lookup`, and `dependents` work with v2 identifiers.
291
+ - [ ] Semantic retrieval works after re-embedding, if enabled.
292
+ - [ ] Console exposes only the authorized 9/11 tools, if enabled.
293
+ - [ ] An enabled Console reads a token of at least 32 characters from a secret source; its value was not printed or committed.
294
+ - [ ] Console HTTP rejects a request without the bearer token with 401 and accepts the configured client, if HTTP is used.
295
+ - [ ] Console stdio starts through the application bundle, if stdio is used.
296
+ - [ ] Exports were reconciled and stale-document changes reviewed.
297
+ - [ ] A second `woods:notion_sync` reports the shared-table column pages as skipped, if Notion is enabled.
298
+ - [ ] Unattended pipelines were reviewed against the new exit behavior, and any job that restores the index directory restores a complete baseline.
299
+ - [ ] The MCP process has write access to the index directory, if agents call `reload`.
300
+ - [ ] MCP and automation prompts no longer name inventory-only tools.
301
+ - [ ] Backups remain available through the rollout window.
302
+
303
+ ## Roll back
304
+
305
+ If verification fails:
306
+
307
+ 1. stop v2 MCP, watcher, embedding, and exporter processes;
308
+ 2. restore the v1 Gemfile and lockfile or deploy the recorded v1 commit;
309
+ 3. run the v1 `woods:clean` before restoring anything under the configured output directory;
310
+ 4. either restore the complete pre-upgrade v1 output-directory backup, or run a fresh v1 extraction and then restore its v1 `dumps/` and configuration artifacts;
311
+ 5. restore external vector-store and managed export backups when v2 modified them;
312
+ 6. restore v1 MCP configuration and reconnect clients;
313
+ 7. verify v1 status and representative queries before reopening access.
314
+
315
+ A v1 gem cannot translate a v2 index or durable vector store back to v1 identifiers. Re-extraction and backup restoration are the rollback. Do not run `woods:clean` after restoring local or shared-filesystem dumps; v1 removes the entire output directory.
316
+
317
+ ## Agent-operated upgrade prompt
318
+
319
+ > Upgrade this Rails application from Woods 1.x to 2.0 using `docs/UPGRADING_TO_2.md`. Start with read-only inventory and preserve unrelated changes. Before cleaning or reconciling anything, identify the output directory, providers, exports, direct index consumers, and restorable backups. Report only whether a Console token exists and where it comes from, never its value. Do not use purge overrides, enable Console/read tools, create or rotate tokens, change credentials, or modify shared infrastructure without asking me. Perform a clean v2 extraction, validate it, update MCP instructions to the packaged 14-tool Index and 9/11 Console surfaces, and return the completed verification checklist plus rollback location.
320
+
321
+ For failures, use [Troubleshooting](TROUBLESHOOTING.md). For current setup, use [Getting started](GETTING_STARTED.md).