woods 2.0.0.beta2 → 2.0.0.beta4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +339 -1
- data/CONTRIBUTING.md +188 -12
- data/README.md +93 -174
- data/SECURITY.md +9 -6
- data/docs/AGENT_GUIDE.md +109 -8
- data/docs/AGENT_SETUP.md +98 -7
- data/docs/BACKEND_MATRIX.md +25 -0
- data/docs/CLIENT_HOOKS.md +111 -0
- data/docs/CONFIGURATION_REFERENCE.md +267 -16
- data/docs/CONSOLE_MCP_SETUP.md +80 -7
- data/docs/DOCKER_SETUP.md +22 -3
- data/docs/EVALUATION.md +464 -1
- data/docs/EXTRACTOR_REFERENCE.md +45 -6
- data/docs/FAQ.md +11 -12
- data/docs/GETTING_STARTED.md +17 -5
- data/docs/INCREMENTAL_EXTRACTION.md +147 -7
- data/docs/INDEX_LAYOUT.md +382 -0
- data/docs/INTERNALS.md +7 -2
- data/docs/MCP_SERVERS.md +276 -5
- data/docs/MCP_TOOL_COOKBOOK.md +37 -22
- data/docs/MCP_WORKTREE_SETUP.md +43 -83
- data/docs/NOTION_INTEGRATION.md +13 -0
- data/docs/OBSIDIAN_INTEGRATION.md +57 -9
- data/docs/PUBLISHED_INDEX.md +72 -0
- data/docs/README.md +7 -0
- data/docs/RETRIEVAL_GUIDE.md +273 -12
- data/docs/RUNTIME_TRACING.md +71 -0
- data/docs/SOURCE_FRESHNESS.md +143 -0
- data/docs/TROUBLESHOOTING.md +129 -18
- data/docs/UNBLOCKED_INTEGRATION.md +25 -0
- data/docs/UPGRADING_TO_2.md +48 -22
- data/docs/WATCH_DAEMON.md +277 -67
- data/exe/woods-agent-config +6 -0
- data/exe/woods-extract +5 -0
- data/exe/woods-hook-context +6 -0
- data/exe/woods-mcp-start +14 -9
- data/lib/generators/woods/pgvector_generator.rb +8 -2
- data/lib/generators/woods/templates/woods.rb.tt +1 -3
- data/lib/tasks/woods.rake +47 -397
- data/lib/woods/agent_configuration/applier.rb +135 -0
- data/lib/woods/agent_configuration/cli.rb +101 -0
- data/lib/woods/agent_configuration/cli_options.rb +29 -0
- data/lib/woods/agent_configuration/document.rb +105 -0
- data/lib/woods/agent_configuration/error.rb +7 -0
- data/lib/woods/agent_configuration/launcher.rb +75 -0
- data/lib/woods/agent_configuration/layout.rb +72 -0
- data/lib/woods/agent_configuration/managed_section.rb +62 -0
- data/lib/woods/agent_configuration/plan.rb +98 -0
- data/lib/woods/agent_configuration/plan_diff.rb +38 -0
- data/lib/woods/agent_configuration/planned_files.rb +61 -0
- data/lib/woods/agent_configuration/planner.rb +63 -0
- data/lib/woods/agent_configuration/planner_validation.rb +77 -0
- data/lib/woods/agent_configuration/preflight.rb +100 -0
- data/lib/woods/agent_configuration/recovery.rb +49 -0
- data/lib/woods/ast/node.rb +2 -0
- data/lib/woods/ast/parser.rb +38 -5
- data/lib/woods/builder.rb +21 -5
- data/lib/woods/cache/cache_middleware.rb +28 -7
- data/lib/woods/cache/cache_store.rb +4 -5
- data/lib/woods/change_set.rb +5 -4
- data/lib/woods/console/credential_index.rb +20 -2
- data/lib/woods/console/credential_scanner.rb +18 -17
- data/lib/woods/console/credential_scanner_registry.rb +36 -0
- data/lib/woods/console/dispatch_pipeline.rb +7 -0
- data/lib/woods/console/embedded_executor.rb +32 -10
- data/lib/woods/console/encrypted_credential_snapshot.rb +16 -0
- data/lib/woods/console/rack_middleware.rb +22 -13
- data/lib/woods/console/server.rb +18 -16
- data/lib/woods/console/sql_noise_stripper.rb +9 -7
- data/lib/woods/console/sql_table_scanner.rb +47 -7
- data/lib/woods/console/sql_validator.rb +49 -9
- data/lib/woods/console/sqlite_read_guard.rb +46 -0
- data/lib/woods/coordination/pipeline_lock.rb +3 -2
- data/lib/woods/dependency_graph.rb +65 -13
- data/lib/woods/embedding/corpus.rb +94 -0
- data/lib/woods/embedding/indexer.rb +114 -60
- data/lib/woods/embedding/openai.rb +17 -6
- data/lib/woods/evaluation/ablation_executor.rb +6 -1
- data/lib/woods/evaluation/ablation_timed_executor.rb +22 -4
- data/lib/woods/export/typed_reader.rb +56 -0
- data/lib/woods/extractor.rb +277 -149
- data/lib/woods/extractors/action_cable_extractor.rb +3 -1
- data/lib/woods/extractors/behavioral_profile.rb +9 -7
- data/lib/woods/extractors/caching_extractor.rb +3 -1
- data/lib/woods/extractors/concern_extractor.rb +64 -6
- data/lib/woods/extractors/configuration_extractor.rb +7 -3
- data/lib/woods/extractors/controller_extractor.rb +13 -4
- data/lib/woods/extractors/database_view_extractor.rb +3 -1
- data/lib/woods/extractors/declared_parent.rb +55 -0
- data/lib/woods/extractors/decorator_extractor.rb +3 -1
- data/lib/woods/extractors/engine_extractor.rb +3 -1
- data/lib/woods/extractors/event_extractor.rb +4 -2
- data/lib/woods/extractors/factory_extractor.rb +3 -1
- data/lib/woods/extractors/graphql_extractor.rb +10 -13
- data/lib/woods/extractors/i18n_extractor.rb +3 -1
- data/lib/woods/extractors/job_extractor.rb +6 -19
- data/lib/woods/extractors/lib_extractor.rb +13 -9
- data/lib/woods/extractors/mailer_extractor.rb +26 -15
- data/lib/woods/extractors/manager_extractor.rb +3 -1
- data/lib/woods/extractors/method_parameters.rb +53 -0
- data/lib/woods/extractors/middleware_argument.rb +65 -0
- data/lib/woods/extractors/middleware_extractor.rb +9 -3
- data/lib/woods/extractors/migration_extractor.rb +3 -1
- data/lib/woods/extractors/model_extractor.rb +26 -34
- data/lib/woods/extractors/package_extractor.rb +24 -4
- data/lib/woods/extractors/phlex_extractor.rb +3 -1
- data/lib/woods/extractors/policy_extractor.rb +3 -1
- data/lib/woods/extractors/poro_extractor.rb +13 -9
- data/lib/woods/extractors/pundit_extractor.rb +3 -1
- data/lib/woods/extractors/rails_source_extractor.rb +4 -2
- data/lib/woods/extractors/rake_task_extractor.rb +4 -2
- data/lib/woods/extractors/route_extractor.rb +3 -1
- data/lib/woods/extractors/route_helper_resolver.rb +10 -33
- data/lib/woods/extractors/scheduled_job_extractor.rb +41 -15
- data/lib/woods/extractors/serializer_extractor.rb +4 -2
- data/lib/woods/extractors/service_extractor.rb +3 -1
- data/lib/woods/extractors/shared_dependency_scanner.rb +2 -2
- data/lib/woods/extractors/shared_utility_methods.rb +48 -19
- data/lib/woods/extractors/source_nesting.rb +1 -1
- data/lib/woods/extractors/state_machine_extractor.rb +3 -1
- data/lib/woods/extractors/test_mapping_extractor.rb +3 -1
- data/lib/woods/extractors/validator_extractor.rb +3 -1
- data/lib/woods/extractors/view_component_extractor.rb +3 -1
- data/lib/woods/extractors/view_template_extractor.rb +3 -1
- data/lib/woods/gem_mapper.rb +2 -0
- data/lib/woods/git_history.rb +116 -0
- data/lib/woods/graph_analyzer.rb +35 -6
- data/lib/woods/hooks/context_cli.rb +54 -0
- data/lib/woods/hooks/context_event.rb +88 -0
- data/lib/woods/hooks/context_hint.rb +73 -0
- data/lib/woods/hooks/context_impact.rb +77 -0
- data/lib/woods/hooks/context_output.rb +47 -0
- data/lib/woods/hooks/context_state.rb +102 -0
- data/lib/woods/hooks/refresh.rb +79 -0
- data/lib/woods/hooks/rule_projection.rb +78 -0
- data/lib/woods/input_rules.rb +19 -0
- data/lib/woods/mcp/bearer_auth.rb +22 -13
- data/lib/woods/mcp/bootstrapper.rb +79 -4
- data/lib/woods/mcp/config_resolver.rb +2 -1
- data/lib/woods/mcp/index_reader.rb +334 -162
- data/lib/woods/mcp/initialization_guidance.rb +27 -0
- data/lib/woods/mcp/origin_guard.rb +17 -9
- data/lib/woods/mcp/published_lexical_retriever.rb +115 -0
- data/lib/woods/mcp/renderers/markdown_renderer.rb +22 -9
- data/lib/woods/mcp/renderers/plain_renderer.rb +18 -8
- data/lib/woods/mcp/search_results.rb +74 -0
- data/lib/woods/mcp/server.rb +178 -63
- data/lib/woods/mcp/tool_contract.rb +3 -1
- data/lib/woods/mcp/tool_response_renderer.rb +41 -0
- data/lib/woods/mcp/traversal_evidence.rb +113 -0
- data/lib/woods/mcp/traversal_evidence_index.rb +100 -0
- data/lib/woods/mcp/traversal_evidence_page.rb +41 -0
- data/lib/woods/mcp/traversal_evidence_text.rb +52 -0
- data/lib/woods/mcp/traversal_response.rb +22 -0
- data/lib/woods/notion/exporter.rb +56 -17
- data/lib/woods/obsidian/destination_plan.rb +98 -0
- data/lib/woods/obsidian/name_mapper.rb +19 -3
- data/lib/woods/obsidian/note_builder.rb +19 -10
- data/lib/woods/obsidian/vault_exporter.rb +88 -32
- data/lib/woods/operator/pipeline_guard.rb +18 -13
- data/lib/woods/path_dispatcher.rb +13 -6
- data/lib/woods/payload_store.rb +27 -26
- data/lib/woods/published_index/typed_unit_reader.rb +40 -3
- data/lib/woods/published_index.rb +2 -2
- data/lib/woods/railtie.rb +3 -3
- data/lib/woods/railtie_support.rb +12 -12
- data/lib/woods/rake_helpers.rb +382 -0
- data/lib/woods/resilience/graph_invariant_validator/membership_checks.rb +71 -0
- data/lib/woods/resilience/graph_invariant_validator/node_checks.rb +61 -0
- data/lib/woods/resilience/graph_invariant_validator/reverse_relationship_checks.rb +46 -0
- data/lib/woods/resilience/graph_invariant_validator.rb +119 -0
- data/lib/woods/resilience/index_validator/graph_checks.rb +80 -0
- data/lib/woods/resilience/index_validator.rb +112 -23
- data/lib/woods/retrieval/context_assembler.rb +50 -15
- data/lib/woods/retrieval/lexical_assembler.rb +84 -0
- data/lib/woods/retrieval/lexical_index.rb +120 -0
- data/lib/woods/retrieval/ranker.rb +4 -2
- data/lib/woods/retrieval/scope.rb +108 -0
- data/lib/woods/retrieval/scoped_graph_store.rb +32 -0
- data/lib/woods/retrieval/scoped_vector_store.rb +55 -0
- data/lib/woods/retrieval/search_executor.rb +86 -27
- data/lib/woods/retrieval/source_evidence.rb +200 -0
- data/lib/woods/retriever.rb +98 -22
- data/lib/woods/ruby_analyzer/trace_enricher.rb +77 -38
- data/lib/woods/session_tracer/file_store.rb +6 -1
- data/lib/woods/session_tracer/middleware.rb +10 -12
- data/lib/woods/session_tracer/redis_store.rb +22 -6
- data/lib/woods/session_tracer/session_flow_assembler.rb +23 -17
- data/lib/woods/session_tracer/solid_cache_coordination.rb +6 -4
- data/lib/woods/session_tracer/unit_resolver.rb +63 -0
- data/lib/woods/source_inputs/consumer_errors.rb +31 -0
- data/lib/woods/source_inputs/handoff.rb +102 -0
- data/lib/woods/source_inputs/launcher.rb +157 -0
- data/lib/woods/source_inputs/manifest.rb +124 -0
- data/lib/woods/source_inputs/private_key.rb +55 -0
- data/lib/woods/source_inputs/scanner.rb +171 -0
- data/lib/woods/source_inputs/scopes.rb +71 -0
- data/lib/woods/source_inputs/session.rb +214 -0
- data/lib/woods/source_inputs/status.rb +84 -0
- data/lib/woods/source_inputs/verifier.rb +107 -0
- data/lib/woods/storage/metadata_store.rb +25 -25
- data/lib/woods/storage/pgvector.rb +35 -10
- data/lib/woods/storage/qdrant.rb +17 -7
- data/lib/woods/storage/vector_store.rb +18 -6
- data/lib/woods/tasks.rb +3 -2
- data/lib/woods/temporal/json_snapshot_store.rb +58 -9
- data/lib/woods/unblocked/exporter.rb +59 -70
- data/lib/woods/version.rb +1 -1
- data/lib/woods/watch/boot_snapshot.rb +52 -0
- data/lib/woods/watch/daemon.rb +154 -32
- data/lib/woods/watch/listen_watcher.rb +4 -0
- data/lib/woods/watch/polling_watcher.rb +5 -1
- data/lib/woods/watch/status.rb +20 -15
- data/lib/woods/watch/tree_scan.rb +21 -13
- data/lib/woods/watch/watcher.rb +4 -1
- data/lib/woods.rb +50 -11
- data/plugin/.claude-plugin/plugin.json +1 -1
- data/plugin/hooks/adapters/normalize.jq +15 -0
- data/plugin/hooks/adapters/normalize.rb +63 -0
- data/plugin/hooks/hooks.json +20 -0
- data/plugin/hooks/woods-context.sh +50 -0
- data/plugin/hooks/woods-input-rules.sh +159 -0
- data/plugin/hooks/woods-opencode.mjs +65 -0
- data/plugin/hooks/woods-post-edit.sh +2 -225
- data/plugin/hooks/woods-refresh.sh +260 -0
- data/plugin/hooks/woods-session-start.sh +47 -55
- data/plugin/skills/woods-agent-enable/SKILL.md +19 -0
- data/plugin/skills/woods-diagnose/SKILL.md +319 -1
- data/plugin/skills/woods-investigate/SKILL.md +145 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +90 -2
- data/plugin/skills/woods-setup/SKILL.md +110 -6
- metadata +87 -5
data/docs/WATCH_DAEMON.md
CHANGED
|
@@ -29,8 +29,10 @@ Ctrl-C to stop.
|
|
|
29
29
|
| `WOODS_WATCH_DEBOUNCE` | `0.4` | Seconds of quiet before a batch is considered settled |
|
|
30
30
|
| `WOODS_WATCH_FULL_THRESHOLD` | `50` | Actionable changed-file count above which a full extraction replaces incremental |
|
|
31
31
|
| `WOODS_WATCH_POLL` | unset | `1` forces the polling backend, set this inside a container watching a bind mount |
|
|
32
|
+
| `WOODS_WATCH_POLL_INTERVAL` | `1.0` | Positive, finite seconds of sleep between polling scans; does not select the polling backend |
|
|
32
33
|
| `WOODS_WATCH_IDLE_TIMEOUT` | unset | Seconds of quiet after which a dormant daemon exits |
|
|
33
34
|
| `WOODS_WATCH_CATCH_UP` | `1` | `0` skips the startup reconciliation |
|
|
35
|
+
| `WOODS_WATCH_TRUST_FOREIGN_HOST` | unset | `1` lets a reader trust a fresh foreign-host heartbeat without checking its pid locally; see [cross-host liveness](#cross-host-liveness) |
|
|
34
36
|
|
|
35
37
|
Run it under a supervisor. When boot-captured configuration changes the daemon
|
|
36
38
|
exits `75` (`EX_TEMPFAIL`) on purpose, see [Restart triggers](#restart-triggers).
|
|
@@ -75,6 +77,13 @@ This is `rails/spring`'s contract, copied deliberately: Spring's staleness bugs
|
|
|
75
77
|
came from under-scoping exactly this set, so the boundary here is drawn on the
|
|
76
78
|
generous side.
|
|
77
79
|
|
|
80
|
+
A restart-trigger change found at startup is reconciled with one full extraction
|
|
81
|
+
when it is covered by the task's environment-boot snapshot. That advances the
|
|
82
|
+
generation through real extraction, so a supervisor restart does not repeatedly
|
|
83
|
+
exit `75` over the same files. Live restart triggers still stop the daemon,
|
|
84
|
+
including edits during startup extraction. Their paths survive shutdown even
|
|
85
|
+
when the preceding extraction has advanced the generation watermark.
|
|
86
|
+
|
|
78
87
|
The same escalation happens when the app *can't* reload at all, a boot with
|
|
79
88
|
`config.enable_reloading = false`. Extracting against constants that no longer
|
|
80
89
|
match their source would be worse than saying so.
|
|
@@ -87,7 +96,7 @@ partial write:
|
|
|
87
96
|
|
|
88
97
|
| Failure | What happens |
|
|
89
98
|
|---|---|
|
|
90
|
-
| Reload raises (`SyntaxError`, `NameError`) | Degraded status naming the reason; index intact at generation N; retried on the next event |
|
|
99
|
+
| Reload raises (`SyntaxError`, `NameError`) | Degraded status naming the reason; index intact at generation N; pending paths retried on the next file event or heartbeat |
|
|
91
100
|
| Extraction raises | Degraded status; generation not advanced |
|
|
92
101
|
| Payload directory can't be opened, over a payload-born index | Degraded status; generation not advanced. An incremental run only writes the units it touched, so there is no complete flat index it could fall back to publishing, see [Payload publishing](#payload-publishing) |
|
|
93
102
|
| Index written but the generation bump failed | Degraded status; paths carried forward. The extractor deliberately does not fail an otherwise-good extraction over an unwritable marker, but the marker *is* what readers refresh on, so the daemon cross-checks that the number moved rather than reporting `running` over an index nothing can see |
|
|
@@ -110,17 +119,23 @@ at a known generation, reason attached), `stopped` (nothing is maintaining this
|
|
|
110
119
|
index). A stale answer is only dangerous when nothing says so.
|
|
111
120
|
|
|
112
121
|
The file is written world-readable (0644) by design: host-side hooks read it
|
|
113
|
-
through a bind mount.
|
|
122
|
+
through a bind mount. Writes through `Woods::AtomicFile` default to owner-only
|
|
123
|
+
0600 unless the caller supplies another mode. This is not a guarantee for every
|
|
124
|
+
Woods artifact: the SQLite metadata store does not enforce 0600, and a newly
|
|
125
|
+
created database uses 0644 under umask 022. Restrict access to the output
|
|
126
|
+
directory according to the source and metadata it contains.
|
|
114
127
|
|
|
115
128
|
Note that `SyntaxError` is a `ScriptError`, not a `StandardError`. Rescuing
|
|
116
129
|
only the latter would let a half-typed file kill the daemon.
|
|
117
130
|
|
|
118
131
|
A cycle that fails to land its work never loses its paths. Lock contention, a
|
|
119
132
|
failed reload, and a raising extraction all carry the batch into `@pending`, and
|
|
120
|
-
the next cycle folds it back in
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
133
|
+
the next cycle folds it back in even if no new event mentions those files.
|
|
134
|
+
A degraded cycle ends the current drain to avoid a tight retry loop. Pending
|
|
135
|
+
paths are retried on the next file event or [heartbeat](#the-heartbeat), so a
|
|
136
|
+
finished contending writer does not require another edit to trigger recovery.
|
|
137
|
+
Heartbeat retries use a separate worker so status updates and lock refresh
|
|
138
|
+
continue while extraction runs.
|
|
124
139
|
|
|
125
140
|
### The heartbeat
|
|
126
141
|
|
|
@@ -151,6 +166,24 @@ it starts: edits and pulled commits that landed while nothing was watching are
|
|
|
151
166
|
invisible to it forever. That matters because callers stand down when a daemon
|
|
152
167
|
is alive, so *alive has to mean covered*.
|
|
153
168
|
|
|
169
|
+
The standalone `woods:watch` task snapshots reload/restart inputs before invoking
|
|
170
|
+
Rails' `environment` task. Inputs unchanged across that boundary, including
|
|
171
|
+
carried paths that remain deleted, may be reconciled by a full extraction.
|
|
172
|
+
Unreleased after `2.0.0.beta3`: registered restart inputs deleted while the
|
|
173
|
+
daemon was stopped also trigger a full extraction after a fresh environment
|
|
174
|
+
boot. Nominal framework paths still use the bounded deletion sweep.
|
|
175
|
+
Changes during environment initialization still require restart. Lock contention,
|
|
176
|
+
extraction failure, and publication failure retain the full-reconciliation
|
|
177
|
+
obligation for retry; a successful publish clears it.
|
|
178
|
+
|
|
179
|
+
This boundary covers **environment initialization**. Bundler and
|
|
180
|
+
`config/application.rb` can run before the task begins; the snapshot does not
|
|
181
|
+
prove that edits during those earlier stages were incorporated. Start the task
|
|
182
|
+
against a settled boot configuration. If Rails is already initialized or the
|
|
183
|
+
`environment` task was already invoked, the daemon keeps conservative restart
|
|
184
|
+
handling. Use `bundle exec rake woods:watch` as a separate process, rather than
|
|
185
|
+
`bundle exec rake environment woods:watch`.
|
|
186
|
+
|
|
154
187
|
So `run` reconciles before it waits. The watermark is `generation.json`'s mtime, written last on every successful run, so it means "when this index was last
|
|
155
188
|
known good", and everything modified since is uncovered, whoever changed it.
|
|
156
189
|
With no generation file there is no index, every file is uncovered, and the
|
|
@@ -161,7 +194,12 @@ external cleanup targeting the large directories), and readers deliberately
|
|
|
161
194
|
degrade a dangling pointer to the index root, so trusting the mtime there would
|
|
162
195
|
report "current at startup" over a directory holding nothing.
|
|
163
196
|
|
|
164
|
-
**The watcher
|
|
197
|
+
**The built-in watcher establishes detection before reconciliation runs.**
|
|
198
|
+
Polling signals readiness after its baseline scan; native watching signals after
|
|
199
|
+
listener startup, including a fallback to polling. Startup waits up to 30 seconds
|
|
200
|
+
for readiness and reports an error if detection cannot start. Callbacks enqueue
|
|
201
|
+
live events immediately, while extraction waits until startup obligations are
|
|
202
|
+
established. A
|
|
165
203
|
file saved while catch-up's own extraction is still in flight (which can take
|
|
166
204
|
minutes on a storm-triggered full run) used to be lost twice: no watcher
|
|
167
205
|
existed yet to see it, and the polling watcher takes its baseline snapshot
|
|
@@ -173,8 +211,9 @@ already tolerate the duplicate paths this produces against whatever catch-up
|
|
|
173
211
|
finds on its own via the tree scan.
|
|
174
212
|
|
|
175
213
|
Deletions need one extra step, because a deleted file leaves no mtime to scan:
|
|
176
|
-
|
|
177
|
-
|
|
214
|
+
registered restart inputs follow the full-reconciliation rule above. For other
|
|
215
|
+
registered paths gone from disk, a deletion-only startup runs one cycle with an
|
|
216
|
+
*empty* change set, which reaches the ghost units through the
|
|
178
217
|
extractor's bounded deletion sweep. Deliberately empty, naming the paths would
|
|
179
218
|
make the deletions authoritative for every unit type, and some registered paths
|
|
180
219
|
are nominal (on Rails < 7.1, `ActiveRecord::SchemaMigration` registers a
|
|
@@ -185,7 +224,7 @@ supplies the trigger.
|
|
|
185
224
|
This is what makes the documented hook pattern safe:
|
|
186
225
|
|
|
187
226
|
```bash
|
|
188
|
-
bundle exec rake woods:watch_status || start_the_daemon
|
|
227
|
+
bundle exec rake woods:watch_status || start_the_daemon # same host; see cross-host liveness below
|
|
189
228
|
bundle exec rake woods:incremental # stands down, the daemon has these
|
|
190
229
|
```
|
|
191
230
|
|
|
@@ -215,9 +254,19 @@ polling rather than trust a watcher that may sit silent while files change
|
|
|
215
254
|
under it:
|
|
216
255
|
|
|
217
256
|
```bash
|
|
218
|
-
WOODS_WATCH_POLL=1 bundle exec rake woods:watch
|
|
257
|
+
WOODS_WATCH_POLL=1 WOODS_WATCH_POLL_INTERVAL=2.5 bundle exec rake woods:watch
|
|
219
258
|
```
|
|
220
259
|
|
|
260
|
+
### Polling cost
|
|
261
|
+
|
|
262
|
+
For a slow bind mount, increase `WOODS_WATCH_POLL_INTERVAL` to reduce scan
|
|
263
|
+
frequency. The default is 1.0 second; the example above uses 2.5 seconds.
|
|
264
|
+
Each cycle also includes the scan's duration. Longer intervals can delay
|
|
265
|
+
change detection and polling shutdown. The value also applies when a native
|
|
266
|
+
watcher fails and falls back to polling; it has no effect while native watching
|
|
267
|
+
is active. Blank, malformed, nonfinite, zero and negative values are rejected
|
|
268
|
+
before the daemon starts.
|
|
269
|
+
|
|
221
270
|
Selection is also self-correcting at runtime. If `listen` cannot start at all, inotify watch exhaustion (`ENOSPC`) is the usual reason on a large tree, the
|
|
222
271
|
daemon logs it and falls back to polling rather than exiting, because a daemon
|
|
223
272
|
costing some CPU beats one that never fires. Failures *after* startup are not
|
|
@@ -234,6 +283,14 @@ Ignored by default: `.git`, `node_modules`, `tmp`, `log`, `coverage`,
|
|
|
234
283
|
`vendor/bundle`, `public/assets`, `public/packs`, `storage`. That ignore list is
|
|
235
284
|
what keeps a polling scan bounded.
|
|
236
285
|
|
|
286
|
+
Polling and startup catch-up preserve each logical path when multiple directory
|
|
287
|
+
symlinks point to the same source tree. For example, `a_shared/user.rb` and
|
|
288
|
+
`app/models/user.rb` both remain visible; an earlier alias must not hide the path
|
|
289
|
+
that extraction recognizes. Cycles back to a directory already on the current
|
|
290
|
+
traversal branch are pruned, while independent sibling aliases remain visible.
|
|
291
|
+
This can increase scan work for deliberately repeated aliases; avoid unnecessary
|
|
292
|
+
aliases in large watched trees. Ignored logical paths are still pruned.
|
|
293
|
+
|
|
237
294
|
## Placement
|
|
238
295
|
|
|
239
296
|
The spike asked for three placements to be compared and one chosen. Every
|
|
@@ -418,13 +475,11 @@ party) behaves exactly as it always did.
|
|
|
418
475
|
was told the index matched HEAD while every answer described the tree before
|
|
419
476
|
those edits.
|
|
420
477
|
|
|
421
|
-
The fingerprint
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
changed and the index followed" from "tree changed and the index has not caught
|
|
427
|
-
up".
|
|
478
|
+
The fingerprint hashes the current `git status --porcelain` path/status list.
|
|
479
|
+
Repeated edits to the same already-dirty file can leave it identical. It is not
|
|
480
|
+
content identity. Use `index.source_freshness` for generation-bound content
|
|
481
|
+
verification; see [source freshness](SOURCE_FRESHNESS.md) for `current`, `drifted`,
|
|
482
|
+
`unknown`, scan budgets, fresh-process capture and partial-runtime limitations.
|
|
428
483
|
|
|
429
484
|
### Multi-file read consistency
|
|
430
485
|
|
|
@@ -534,7 +589,7 @@ and a hook-triggered `woods:incremental`. They share the existing file-based
|
|
|
534
589
|
A hook can check cheaply:
|
|
535
590
|
|
|
536
591
|
```bash
|
|
537
|
-
bundle exec rake woods:watch_status || start_the_daemon # exit 0 = alive
|
|
592
|
+
bundle exec rake woods:watch_status || start_the_daemon # same host, exit 0 = alive
|
|
538
593
|
```
|
|
539
594
|
|
|
540
595
|
The check does not boot Rails. Without `WOODS_OUTPUT`, it resolves
|
|
@@ -543,63 +598,145 @@ launcher's current directory, so `rake -f /app/Rakefile woods:watch_status`
|
|
|
543
598
|
and worktree-manager invocations inspect the same per-app status. Set
|
|
544
599
|
`WOODS_OUTPUT` when the daemon uses a non-default index directory.
|
|
545
600
|
|
|
546
|
-
|
|
547
|
-
status file lies: a state a live daemon writes, a pid that still exists (a
|
|
548
|
-
`kill -9` leaves the file behind), and a recent timestamp (a machine that lost
|
|
549
|
-
power leaves a `running` record whose pid some unrelated process now owns).
|
|
601
|
+
### Cross-host liveness
|
|
550
602
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
603
|
+
By default, a reader trusts only a same-host record: a `running` or `degraded`
|
|
604
|
+
state, a positive pid that still exists, and a recent ISO8601 timestamp. Foreign
|
|
605
|
+
hostnames are rejected because a container pid cannot be checked on the host.
|
|
606
|
+
|
|
607
|
+
For a daemon and reader sharing the same index through a bind mount, opt in in
|
|
608
|
+
each reader's environment:
|
|
609
|
+
|
|
610
|
+
```bash
|
|
611
|
+
export WOODS_WATCH_TRUST_FOREIGN_HOST=1
|
|
612
|
+
bundle exec rake woods:watch_status || start_the_daemon
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
Set the variable inside one-off containers running `woods:incremental`, and in
|
|
616
|
+
the host MCP process when it reports `woods_status`. Docker does not forward a
|
|
617
|
+
host environment variable automatically: pass `-e WOODS_WATCH_TRUST_FOREIGN_HOST=1`
|
|
618
|
+
to `docker compose run` or `docker compose exec`, or configure that service's
|
|
619
|
+
environment. Use the same shared index (`WOODS_OUTPUT` when needed) in each process.
|
|
620
|
+
|
|
621
|
+
Opted-in readers accept foreign `running` and `degraded` records on heartbeat
|
|
622
|
+
freshness, without any local pid lookup. Heartbeats run every five minutes; a
|
|
623
|
+
crashed foreign daemon can still be believed for up to 15 minutes after its last
|
|
624
|
+
heartbeat. Missing or malformed timestamps and timestamps more than 30 seconds
|
|
625
|
+
in the future are rejected. Keep the participating clocks synchronized.
|
|
626
|
+
|
|
627
|
+
`degraded` means alive but unable to update: `watch_status` exits 0, incremental
|
|
628
|
+
still attempts extraction, and clean refuses. `WOODS_IGNORE_WATCH=1` still
|
|
629
|
+
overrides writer stand-down and clean protection. It does not alter the status
|
|
630
|
+
report. Direct Ruby callers can override the environment with
|
|
631
|
+
`Status#alive?(trust_foreign_host: true)` or `false`.
|
|
632
|
+
|
|
633
|
+
This is liveness evidence for an established daemon, not a cross-container
|
|
634
|
+
startup lease. Simultaneous starts in foreign namespaces still need one
|
|
635
|
+
supervisor to coordinate ownership. Hostnames are also imperfect identity:
|
|
636
|
+
custom or reused identical container hostnames retain the local-pid limitation.
|
|
558
637
|
|
|
559
638
|
### Hooks for agent sessions
|
|
560
639
|
|
|
640
|
+
For client registration and the supported Claude/OpenCode event shapes, see
|
|
641
|
+
[edit client adapters](CLIENT_HOOKS.md). Both use the shared queue below.
|
|
642
|
+
|
|
561
643
|
The daemon covers a human's editor session. A `claude -p` run in a worktree
|
|
562
|
-
with no daemon needs a different trigger, so the Woods plugin ships two
|
|
644
|
+
with no daemon needs a different trigger, so the Woods plugin ships two freshness
|
|
563
645
|
hooks (`plugin/hooks/hooks.json`), both shipped disabled:
|
|
564
646
|
|
|
565
647
|
| Hook | When | What it does |
|
|
566
648
|
|---|---|---|
|
|
567
|
-
| `PostToolUse` (`Edit`, `Write`, `MultiEdit`), async |
|
|
568
|
-
| `SessionStart` (`startup`, `resume`) | Session begins |
|
|
569
|
-
|
|
570
|
-
Both read `cwd` from the hook payload,
|
|
571
|
-
|
|
572
|
-
`
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
`
|
|
580
|
-
|
|
581
|
-
A
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
649
|
+
| `PostToolUse` (`Edit`, `Write`, `MultiEdit`), async | A supported extraction or boot input changes | Queues an immutable JSON event, then calls `woods:hook_refresh[<encoded batch>]`; output goes to `hook.log` |
|
|
650
|
+
| `SessionStart` (`startup`, `resume`) | Session begins | Checks source content through `woods:source_status`; warns on drift or unknown evidence |
|
|
651
|
+
|
|
652
|
+
Both read `cwd` from the hook payload, so a linked worktree uses its own index.
|
|
653
|
+
Both require an existing `generation.json` and `WOODS_HOOKS_ENABLED=1`;
|
|
654
|
+
`WOODS_HOOKS_DISABLED=1` overrides enablement. The broader refresh task is
|
|
655
|
+
**unreleased after Woods 2.0.0.beta2**. Check the installed gem's task list
|
|
656
|
+
(`bundle exec rake -T woods:hook_refresh`, through the application container
|
|
657
|
+
when appropriate) before enabling this plugin version. An older gem's unknown
|
|
658
|
+
task error leaves queued events in place; installing the plugin does not upgrade
|
|
659
|
+
the gem.
|
|
660
|
+
|
|
661
|
+
The portable path predicate is generated from `PathDispatcher` and
|
|
662
|
+
`ReloadPolicy` with `bundle exec ruby -Ilib script/generate-hook-rules`.
|
|
663
|
+
A contract test rejects stale generated rules. It covers services, controllers,
|
|
664
|
+
jobs, concerns, views, locales, supported test/lib files, routes, package
|
|
665
|
+
boundaries, and the remaining standard extractor triggers. Unrelated documents
|
|
666
|
+
stay quiet. Normal edits use fresh-process incremental extraction; changes to
|
|
667
|
+
initializers, boot configuration, dependencies, schema, or other restart inputs
|
|
668
|
+
use fresh-process full extraction. The transport also preserves explicit
|
|
669
|
+
`add`, `update`, `delete`, and `move` operations; a relevant deletion/move selects
|
|
670
|
+
full extraction to remove runtime classes absent from the next boot. The Claude
|
|
671
|
+
adapter receives one `tool_input.file_path`. The OpenCode adapter supplies every
|
|
672
|
+
verified patch metadata path, including both rename sides. Neither infers paths
|
|
673
|
+
from shell commands or parses patch text. Custom runtime roots outside the
|
|
674
|
+
standard dispatcher rules require an explicit refresh; the portable predicate
|
|
675
|
+
cannot discover application configuration without booting it.
|
|
676
|
+
|
|
677
|
+
`WOODS_HOOK_RAKE` sets the command prefix (Docker:
|
|
678
|
+
`docker compose exec -T app bundle exec rake`). The encoded JSON task argument
|
|
679
|
+
carries paths and the output setting across the container boundary, without
|
|
680
|
+
assuming Docker forwards host environment variables or can read a host queue
|
|
681
|
+
filename. The host needs Bash 3.2 or later, standard Unix tools, and either `jq`
|
|
682
|
+
or Ruby; it does not need the application bundle. Prefix words are split without
|
|
683
|
+
shell evaluation: use an executable wrapper for quoted arguments or extra
|
|
684
|
+
container environment settings. `WOODS_OUTPUT` overrides `tmp/woods`, relative
|
|
685
|
+
to each process's application root or as an explicitly supplied absolute path;
|
|
686
|
+
absolute paths must be valid on both sides of a container bind mount.
|
|
687
|
+
|
|
688
|
+
Each event remains under `<output>/hook-pending/` until the task succeeds.
|
|
689
|
+
Successful no-op consumption is acknowledged too. Contending invocations enqueue
|
|
690
|
+
and return while the owner drains bounded batches (up to 16 queue files /
|
|
691
|
+
1,000 paths / 48 KiB of JSON, without splitting a multi-file event). Commas,
|
|
692
|
+
spaces, and newlines are preserved. An empty drain releases the lock before
|
|
693
|
+
checking again, so a final arriving event can acquire ownership.
|
|
694
|
+
A failed command, killed worker, incompatible gem, or publication failure retains
|
|
695
|
+
its batch for retry: delivery is **at least once**, so crash recovery can repeat
|
|
696
|
+
already completed work. Pending events in the previous `hook-pending.txt` format
|
|
697
|
+
are imported on the next relevant edit. Event filenames are private hook state;
|
|
698
|
+
do not modify them while a worker is running.
|
|
699
|
+
|
|
700
|
+
An active daemon produces exit **75** before Rails boots. This is a deferral,
|
|
701
|
+
not acknowledgement: the hook cannot prove which queued events the daemon has
|
|
702
|
+
consumed. It retains the queue and writes a diagnostic. It does not start, stop,
|
|
703
|
+
or restart the daemon. After resolving the cause, the next relevant edit retries
|
|
704
|
+
the queue. To retry immediately, invoke `woods-post-edit.sh` with the original
|
|
705
|
+
JSON event on stdin and the same opt-in/output/prefix settings; with a running
|
|
706
|
+
daemon, stop it first or explicitly configure `WOODS_IGNORE_WATCH=1` in the
|
|
707
|
+
application command's environment. A quiet `SessionStart` does not acknowledge
|
|
708
|
+
the queue. Prefer a resident watcher for sustained edits; enabling both does not
|
|
709
|
+
make refresh faster and can accumulate deferred events.
|
|
710
|
+
|
|
711
|
+
After the complete event input has been read, validated, and queued, the refresh
|
|
712
|
+
hook starts its `WOODS_HOOK_TIMEOUT_SECONDS` deadline (default 600, integer range
|
|
713
|
+
1–3600), including subsequent batches. The producer must close stdin: the 1 MiB
|
|
714
|
+
input limit bounds bytes, not time waiting for EOF. The deadline terminates the local
|
|
715
|
+
command process group and retains work on timeout. For a Docker exec prefix,
|
|
716
|
+
local process termination cannot guarantee cancellation inside the container;
|
|
717
|
+
check the application process and extraction lock before retrying a timed-out
|
|
718
|
+
container run. Async client hook timeouts are not a reliable worker deadline.
|
|
719
|
+
With `flock`, kernel locks release after process exit. The mkdir fallback records
|
|
720
|
+
an owner PID and reclaims dead owners; it never steals a live owner's lock based
|
|
721
|
+
only on age. Only the invocation that removes the recorded dead-owner marker
|
|
722
|
+
may replace its lock directory; competing reclaimers leave their events queued
|
|
723
|
+
for the winning owner. Legacy empty lock directories use `stat` and
|
|
724
|
+
`WOODS_HOOK_LOCK_STALE_SECONDS` (default 1800) for conservative recovery.
|
|
725
|
+
A reused PID can delay recovery until that process exits; inspect the recorded
|
|
726
|
+
owner before manually removing a lock. Hooks sharing this filesystem must run in
|
|
727
|
+
the same host PID namespace; run the actual extraction through the container
|
|
728
|
+
prefix instead of running competing host/container hook workers.
|
|
729
|
+
|
|
730
|
+
Broader coverage increases the number of Rails boots. A view or locale edit now
|
|
731
|
+
costs a fresh incremental run, while a boot/config edit costs a full run. There
|
|
732
|
+
is no provider or embedding call added by this hook. Opt in for occasional agent
|
|
733
|
+
edits; use `woods:watch` for repeated work, and keep full extraction for large
|
|
734
|
+
change sets as described above.
|
|
735
|
+
|
|
736
|
+
The `SessionStart` hook uses the shared quick source verifier through
|
|
737
|
+
`WOODS_HOOK_RAKE`. It has a ten-second command deadline, including startup;
|
|
738
|
+
failed commands and old gems lacking `woods:source_status` report unknown.
|
|
739
|
+
It does not initialize Rails or start a provider. See [source freshness](SOURCE_FRESHNESS.md#containers-and-hooks).
|
|
603
740
|
|
|
604
741
|
### Reader multiplicity is free
|
|
605
742
|
|
|
@@ -663,5 +800,78 @@ result = daemon.process(changed_paths)
|
|
|
663
800
|
# => { action: :incremental, state: :running, generation: 42, count: 1, duration_ms: 61 }
|
|
664
801
|
```
|
|
665
802
|
|
|
803
|
+
For an embedded `#run`, pass `boot_snapshot: Woods::Watch::BootSnapshot.new(root: …)`
|
|
804
|
+
with the snapshot captured **before** environment initialization if the host can
|
|
805
|
+
establish that boundary. Without it, startup restart inputs remain restart
|
|
806
|
+
requests. Direct `#process` calls always preserve conservative restart handling.
|
|
807
|
+
Injected watchers retain their existing `start`/`stop` interface; those with
|
|
808
|
+
asynchronous startup can implement `ready_callback=` and call it after detection
|
|
809
|
+
is established to participate in the readiness handshake.
|
|
810
|
+
|
|
666
811
|
`#process` is one whole cycle and is the supported embedding point. `#run` only
|
|
667
812
|
supplies batches to it.
|
|
813
|
+
|
|
814
|
+
### Optional bounded context hints
|
|
815
|
+
|
|
816
|
+
Context hints are a separate Claude Code opt-in, **unreleased after Woods
|
|
817
|
+
2.0.0.beta2**. Verify `bundle exec woods-hook-context --help` in the installed
|
|
818
|
+
application bundle before enabling `WOODS_HOOK_CONTEXT_ENABLED=1`. The plugin
|
|
819
|
+
version alone does not establish gem support. `WOODS_HOOKS_DISABLED=1` disables
|
|
820
|
+
both context and refresh; `WOODS_HOOKS_ENABLED` controls only the existing
|
|
821
|
+
freshness/refresh hooks. Either feature can work without the other.
|
|
822
|
+
|
|
823
|
+
Separate synchronous SessionStart and PostToolUse entries emit Claude's
|
|
824
|
+
`hookSpecificOutput.additionalContext`. SessionStart gives a short served-index
|
|
825
|
+
orientation. After a relevant native Edit/Write/MultiEdit, the hint identifies
|
|
826
|
+
candidates from one retained published generation. Direct candidates and
|
|
827
|
+
transitive candidates are distinguished; test mappings are suggestions, never
|
|
828
|
+
proof of coverage. Post-edit hints always say **pre-refresh snapshot** because
|
|
829
|
+
an edit can precede publication. Source freshness is checked against that same
|
|
830
|
+
payload; unknown/drifted evidence remains explicit. An unresolved or ambiguous
|
|
831
|
+
edited identity directs the agent to manual search and typed lookup. No match
|
|
832
|
+
within the bounded snapshot establishes neither absence nor no impact.
|
|
833
|
+
|
|
834
|
+
Limits are fixed: depth 2, at most 10 visited nodes including the root, 100
|
|
835
|
+
examined edges, and 2 KiB for the **entire JSON output**, preserving whole rows.
|
|
836
|
+
An index is refused above 16 MiB per required artifact or 50,000 combined graph
|
|
837
|
+
nodes/variants. These preparation checks, JSON parsing, cache construction,
|
|
838
|
+
source verification, path/content hashing, formatting and suppression state all
|
|
839
|
+
run within the hook's private process-group deadline: the worker is killed at
|
|
840
|
+
850 ms, leaving dispatch/cleanup headroom within a one-second work budget.
|
|
841
|
+
The helper also has a 650 ms inner deadline. OS scheduling can delay observation
|
|
842
|
+
of a deadline. Cold bundle/container startup can therefore produce no hint;
|
|
843
|
+
the deadline is not extended. Oversized evidence is marked truncated; missing,
|
|
844
|
+
corrupt, unsupported or timed-out input produces a short unknown notice or
|
|
845
|
+
silence. Silence is never a complete/no-impact claim. The hint boots no Rails
|
|
846
|
+
application and calls no provider.
|
|
847
|
+
|
|
848
|
+
The synchronous opt-in can add up to this budget to a supported tool call. It
|
|
849
|
+
makes context available to Claude's next model request; it does not rely on the
|
|
850
|
+
later-turn delivery of the independent asynchronous refresh worker. A reminder
|
|
851
|
+
need not appear as a visible transcript entry. See the
|
|
852
|
+
[Claude context-output contract](https://code.claude.com/docs/en/hooks#add-context-for-claude).
|
|
853
|
+
|
|
854
|
+
The default command is `bundle exec woods-hook-context`. Set
|
|
855
|
+
`WOODS_HOOK_CONTEXT_COMMAND` to an executable wrapper or argv prefix for a
|
|
856
|
+
container-only bundle. Prefix words are split without shell evaluation; a
|
|
857
|
+
wrapper handles quoted arguments. Explicit `WOODS_HOOK_CONTEXT_ROOT` maps the
|
|
858
|
+
hook payload's original cwd and contained edit path onto a runtime-visible
|
|
859
|
+
application root. For example, a container prefix can include
|
|
860
|
+
`docker compose exec -T -e WOODS_HOOK_CONTEXT_ENABLED=1 -e WOODS_HOOK_CONTEXT_ROOT=/app app bundle exec woods-hook-context`.
|
|
861
|
+
Forward a custom `WOODS_OUTPUT` explicitly too. Running Claude inside the
|
|
862
|
+
application container avoids external container startup and path mapping.
|
|
863
|
+
|
|
864
|
+
Repeat suppression uses session/worktree, served generation/token, changed-file
|
|
865
|
+
content identity and normalized hint content. Repeated identical evidence stays
|
|
866
|
+
quiet; later same-file edits and generation changes can reappear. Missing session
|
|
867
|
+
or bounded content identity disables suppression. Private `hook-context-state.json`
|
|
868
|
+
retains at most 32 sessions and 32 emitted identities per session under a separate
|
|
869
|
+
nonblocking lock. It records **emitted**, not confirmed delivered, hints.
|
|
870
|
+
Contending or unavailable state may skip optional context. Its bounded atomic
|
|
871
|
+
state update never reads, acknowledges, or clears `hook-pending`, nor acquires
|
|
872
|
+
refresh/watch locks. Disable context to roll back without changing refresh.
|
|
873
|
+
|
|
874
|
+
Only the native Claude context entries are supported here; the OpenCode adapter
|
|
875
|
+
continues to provide refresh events. No prompt-triggered retrieval is injected.
|
|
876
|
+
|
|
877
|
+
See the [matched public Rails task comparison](EVALUATION.md#matched-optional-context-hook-tasks-406) for delivered hints, task outcomes, measured overhead and observed limitations.
|
data/exe/woods-extract
ADDED
data/exe/woods-mcp-start
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# same as when launched directly. RubyGems loads gem executables as Ruby, so
|
|
8
8
|
# this wrapper must remain a Ruby program when packaged.
|
|
9
9
|
|
|
10
|
-
index_dir = ARGV[0] || ENV.fetch('WOODS_DIR', nil)
|
|
10
|
+
index_dir = ARGV[0] || ENV.fetch('WOODS_DIR', nil) || ENV.fetch('WOODS_OUTPUT', nil)
|
|
11
11
|
|
|
12
12
|
if index_dir.nil? || index_dir.empty?
|
|
13
13
|
warn 'Error: No index directory specified.'
|
|
@@ -15,9 +15,13 @@ if index_dir.nil? || index_dir.empty?
|
|
|
15
15
|
exit 1
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
index_dir = File.expand_path(index_dir)
|
|
19
|
+
path_remedy = 'Point at the existing index with an explicit path, WOODS_DIR, or WOODS_OUTPUT. ' \
|
|
20
|
+
'If no index exists, run `bundle exec rake woods:extract` in your Rails app.'
|
|
21
|
+
|
|
18
22
|
unless File.directory?(index_dir)
|
|
19
23
|
warn "Error: Index directory does not exist: #{index_dir}"
|
|
20
|
-
warn
|
|
24
|
+
warn path_remedy
|
|
21
25
|
exit 1
|
|
22
26
|
end
|
|
23
27
|
|
|
@@ -27,7 +31,7 @@ end
|
|
|
27
31
|
# whole library just to check one file is wasted work on every boot. A
|
|
28
32
|
# payload-born index has no manifest.json at the root — it lives under the
|
|
29
33
|
# directory generation.json's `payload` pointer names — so the pointer is
|
|
30
|
-
# followed here too, with the same
|
|
34
|
+
# followed here too, with the same realpath containment check, before concluding the
|
|
31
35
|
# directory holds no index. woods-mcp re-checks this properly through
|
|
32
36
|
# Bootstrapper regardless; this is just an early, friendlier exit.
|
|
33
37
|
def manifest_present?(index_dir)
|
|
@@ -39,20 +43,21 @@ def manifest_present?(index_dir)
|
|
|
39
43
|
require 'json'
|
|
40
44
|
require_relative '../lib/woods/atomic_file'
|
|
41
45
|
payload_name = JSON.parse(Woods::AtomicFile.read(generation_path))['payload']
|
|
42
|
-
return false
|
|
46
|
+
return false unless payload_name.is_a?(String) && !payload_name.empty?
|
|
43
47
|
|
|
44
|
-
root = File.
|
|
45
|
-
candidate = File.
|
|
48
|
+
root = File.realpath(index_dir)
|
|
49
|
+
candidate = File.realpath(payload_name, root)
|
|
46
50
|
return false unless candidate.start_with?("#{root}#{File::SEPARATOR}")
|
|
47
51
|
|
|
48
52
|
File.file?(File.join(candidate, 'manifest.json'))
|
|
49
|
-
rescue JSON::ParserError, SystemCallError
|
|
53
|
+
rescue JSON::ParserError, SystemCallError, TypeError, NoMethodError
|
|
50
54
|
false
|
|
51
55
|
end
|
|
52
56
|
|
|
53
57
|
unless manifest_present?(index_dir)
|
|
54
|
-
warn "Error:
|
|
55
|
-
warn '
|
|
58
|
+
warn "Error: Could not resolve a published Woods index in: #{index_dir}"
|
|
59
|
+
warn 'Expected generation.json pointing to a payload manifest.json, or a legacy flat manifest.json.'
|
|
60
|
+
warn path_remedy
|
|
56
61
|
exit 1
|
|
57
62
|
end
|
|
58
63
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'rails/generators'
|
|
4
4
|
require 'rails/generators/active_record'
|
|
5
|
+
require 'woods/storage/pgvector'
|
|
5
6
|
|
|
6
7
|
module Woods
|
|
7
8
|
module Generators
|
|
@@ -15,7 +16,7 @@ module Woods
|
|
|
15
16
|
#
|
|
16
17
|
# Usage:
|
|
17
18
|
# rails generate woods:pgvector
|
|
18
|
-
# rails generate woods:pgvector --dimensions
|
|
19
|
+
# rails generate woods:pgvector --dimensions 768
|
|
19
20
|
#
|
|
20
21
|
class PgvectorGenerator < Rails::Generators::Base
|
|
21
22
|
include ActiveRecord::Generators::Migration
|
|
@@ -25,11 +26,16 @@ module Woods
|
|
|
25
26
|
desc 'Creates the woods_vectors table (pgvector column + HNSW index) used by the Woods vector store'
|
|
26
27
|
|
|
27
28
|
class_option :dimensions, type: :numeric, default: 1536,
|
|
28
|
-
desc: 'Vector dimensions (1536 for text-embedding-3-small
|
|
29
|
+
desc: 'Vector dimensions (1-2000; default 1536 for text-embedding-3-small)'
|
|
29
30
|
|
|
30
31
|
# @return [void]
|
|
31
32
|
def create_migration_file
|
|
32
33
|
@dimensions = options[:dimensions]
|
|
34
|
+
maximum = Woods::Storage::VectorStore::Pgvector::MAX_HNSW_DIMENSIONS
|
|
35
|
+
unless @dimensions.is_a?(Integer) && @dimensions.between?(1, maximum)
|
|
36
|
+
raise ArgumentError, "dimensions must be a positive Integer no greater than #{maximum} for pgvector HNSW"
|
|
37
|
+
end
|
|
38
|
+
|
|
33
39
|
migration_template(
|
|
34
40
|
'add_pgvector_to_woods.rb.erb',
|
|
35
41
|
'db/migrate/add_pgvector_to_woods.rb'
|
|
@@ -24,9 +24,6 @@ Woods.configure do |config|
|
|
|
24
24
|
# Maximum tokens returned in a retrieval context window.
|
|
25
25
|
# config.max_context_tokens = 8_000
|
|
26
26
|
|
|
27
|
-
# Minimum vector similarity score (0.0–1.0) for retrieval results.
|
|
28
|
-
# config.similarity_threshold = 0.7
|
|
29
|
-
|
|
30
27
|
# Output format for retrieval: :claude, :markdown, :plain, :json
|
|
31
28
|
# config.context_format = :markdown
|
|
32
29
|
|
|
@@ -124,6 +121,7 @@ Woods.configure do |config|
|
|
|
124
121
|
# HTTP calls, callbacks, threads, or other connections/shards.
|
|
125
122
|
|
|
126
123
|
# config.console_mcp_enabled = false
|
|
124
|
+
# config.console_mcp_http_enabled = true # set false for stdio-only Console use
|
|
127
125
|
# config.console_mcp_path = '/mcp/console'
|
|
128
126
|
|
|
129
127
|
# Console HTTP requires a strong bearer token. Its Origin/Host guard is
|