woods 2.0.0.beta2 → 2.0.0.beta3
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 +262 -1
- data/CONTRIBUTING.md +173 -9
- data/README.md +7 -3
- data/SECURITY.md +9 -6
- data/docs/AGENT_GUIDE.md +83 -4
- data/docs/AGENT_SETUP.md +82 -1
- data/docs/BACKEND_MATRIX.md +20 -0
- data/docs/CLIENT_HOOKS.md +111 -0
- data/docs/CONFIGURATION_REFERENCE.md +199 -14
- data/docs/CONSOLE_MCP_SETUP.md +35 -5
- data/docs/DOCKER_SETUP.md +21 -2
- data/docs/EVALUATION.md +464 -1
- data/docs/EXTRACTOR_REFERENCE.md +36 -5
- data/docs/FAQ.md +11 -12
- data/docs/GETTING_STARTED.md +17 -5
- data/docs/INCREMENTAL_EXTRACTION.md +117 -1
- data/docs/INDEX_LAYOUT.md +382 -0
- data/docs/INTERNALS.md +7 -2
- data/docs/MCP_SERVERS.md +221 -5
- data/docs/MCP_TOOL_COOKBOOK.md +33 -18
- data/docs/NOTION_INTEGRATION.md +13 -0
- data/docs/OBSIDIAN_INTEGRATION.md +57 -9
- data/docs/PUBLISHED_INDEX.md +55 -0
- data/docs/README.md +7 -0
- data/docs/RETRIEVAL_GUIDE.md +253 -11
- data/docs/RUNTIME_TRACING.md +71 -0
- data/docs/SOURCE_FRESHNESS.md +143 -0
- data/docs/TROUBLESHOOTING.md +117 -5
- data/docs/UNBLOCKED_INTEGRATION.md +25 -0
- data/docs/UPGRADING_TO_2.md +44 -22
- data/docs/WATCH_DAEMON.md +259 -59
- data/exe/woods-agent-config +6 -0
- data/exe/woods-extract +5 -0
- data/exe/woods-hook-context +6 -0
- data/lib/generators/woods/templates/woods.rb.tt +1 -3
- data/lib/tasks/woods.rake +47 -397
- data/lib/woods/agent_configuration/applier.rb +133 -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 +59 -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 +14 -14
- data/lib/woods/console/credential_scanner_registry.rb +36 -0
- data/lib/woods/console/embedded_executor.rb +1 -1
- 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/dependency_graph.rb +65 -13
- data/lib/woods/embedding/corpus.rb +94 -0
- data/lib/woods/embedding/indexer.rb +90 -46
- 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 +232 -137
- 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/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 +8 -2
- 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 +3 -1
- data/lib/woods/extractors/mailer_extractor.rb +20 -5
- 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 +39 -33
- 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 +3 -1
- 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 +27 -15
- 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 +20 -12
- data/lib/woods/mcp/bootstrapper.rb +62 -0
- data/lib/woods/mcp/index_reader.rb +323 -160
- 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 +8 -1
- data/lib/woods/mcp/renderers/plain_renderer.rb +7 -1
- data/lib/woods/mcp/search_results.rb +74 -0
- data/lib/woods/mcp/server.rb +158 -37
- data/lib/woods/mcp/tool_contract.rb +2 -0
- data/lib/woods/mcp/tool_response_renderer.rb +25 -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/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 +7 -1
- data/lib/woods/payload_store.rb +27 -26
- data/lib/woods/railtie.rb +3 -3
- data/lib/woods/railtie_support.rb +12 -12
- data/lib/woods/rake_helpers.rb +392 -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 +73 -0
- data/lib/woods/retrieval/lexical_index.rb +119 -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/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 +27 -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 +29 -8
- 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 +29 -8
- 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 +136 -28
- 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 +13 -0
- data/plugin/skills/woods-diagnose/SKILL.md +288 -1
- data/plugin/skills/woods-investigate/SKILL.md +106 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +89 -1
- data/plugin/skills/woods-setup/SKILL.md +107 -6
- metadata +84 -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.
|
|
@@ -151,6 +160,21 @@ it starts: edits and pulled commits that landed while nothing was watching are
|
|
|
151
160
|
invisible to it forever. That matters because callers stand down when a daemon
|
|
152
161
|
is alive, so *alive has to mean covered*.
|
|
153
162
|
|
|
163
|
+
The standalone `woods:watch` task snapshots reload/restart inputs before invoking
|
|
164
|
+
Rails' `environment` task. Inputs unchanged across that boundary, including
|
|
165
|
+
carried paths that remain deleted, may be reconciled by a full extraction.
|
|
166
|
+
Changes during environment initialization still require restart. Lock contention,
|
|
167
|
+
extraction failure, and publication failure retain the full-reconciliation
|
|
168
|
+
obligation for retry; a successful publish clears it.
|
|
169
|
+
|
|
170
|
+
This boundary covers **environment initialization**. Bundler and
|
|
171
|
+
`config/application.rb` can run before the task begins; the snapshot does not
|
|
172
|
+
prove that edits during those earlier stages were incorporated. Start the task
|
|
173
|
+
against a settled boot configuration. If Rails is already initialized or the
|
|
174
|
+
`environment` task was already invoked, the daemon keeps conservative restart
|
|
175
|
+
handling. Use `bundle exec rake woods:watch` as a separate process, rather than
|
|
176
|
+
`bundle exec rake environment woods:watch`.
|
|
177
|
+
|
|
154
178
|
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
179
|
known good", and everything modified since is uncovered, whoever changed it.
|
|
156
180
|
With no generation file there is no index, every file is uncovered, and the
|
|
@@ -161,7 +185,12 @@ external cleanup targeting the large directories), and readers deliberately
|
|
|
161
185
|
degrade a dangling pointer to the index root, so trusting the mtime there would
|
|
162
186
|
report "current at startup" over a directory holding nothing.
|
|
163
187
|
|
|
164
|
-
**The watcher
|
|
188
|
+
**The built-in watcher establishes detection before reconciliation runs.**
|
|
189
|
+
Polling signals readiness after its baseline scan; native watching signals after
|
|
190
|
+
listener startup, including a fallback to polling. Startup waits up to 30 seconds
|
|
191
|
+
for readiness and reports an error if detection cannot start. Callbacks enqueue
|
|
192
|
+
live events immediately, while extraction waits until startup obligations are
|
|
193
|
+
established. A
|
|
165
194
|
file saved while catch-up's own extraction is still in flight (which can take
|
|
166
195
|
minutes on a storm-triggered full run) used to be lost twice: no watcher
|
|
167
196
|
existed yet to see it, and the polling watcher takes its baseline snapshot
|
|
@@ -185,7 +214,7 @@ supplies the trigger.
|
|
|
185
214
|
This is what makes the documented hook pattern safe:
|
|
186
215
|
|
|
187
216
|
```bash
|
|
188
|
-
bundle exec rake woods:watch_status || start_the_daemon
|
|
217
|
+
bundle exec rake woods:watch_status || start_the_daemon # same host; see cross-host liveness below
|
|
189
218
|
bundle exec rake woods:incremental # stands down, the daemon has these
|
|
190
219
|
```
|
|
191
220
|
|
|
@@ -215,9 +244,19 @@ polling rather than trust a watcher that may sit silent while files change
|
|
|
215
244
|
under it:
|
|
216
245
|
|
|
217
246
|
```bash
|
|
218
|
-
WOODS_WATCH_POLL=1 bundle exec rake woods:watch
|
|
247
|
+
WOODS_WATCH_POLL=1 WOODS_WATCH_POLL_INTERVAL=2.5 bundle exec rake woods:watch
|
|
219
248
|
```
|
|
220
249
|
|
|
250
|
+
### Polling cost
|
|
251
|
+
|
|
252
|
+
For a slow bind mount, increase `WOODS_WATCH_POLL_INTERVAL` to reduce scan
|
|
253
|
+
frequency. The default is 1.0 second; the example above uses 2.5 seconds.
|
|
254
|
+
Each cycle also includes the scan's duration. Longer intervals can delay
|
|
255
|
+
change detection and polling shutdown. The value also applies when a native
|
|
256
|
+
watcher fails and falls back to polling; it has no effect while native watching
|
|
257
|
+
is active. Blank, malformed, nonfinite, zero and negative values are rejected
|
|
258
|
+
before the daemon starts.
|
|
259
|
+
|
|
221
260
|
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
261
|
daemon logs it and falls back to polling rather than exiting, because a daemon
|
|
223
262
|
costing some CPU beats one that never fires. Failures *after* startup are not
|
|
@@ -234,6 +273,14 @@ Ignored by default: `.git`, `node_modules`, `tmp`, `log`, `coverage`,
|
|
|
234
273
|
`vendor/bundle`, `public/assets`, `public/packs`, `storage`. That ignore list is
|
|
235
274
|
what keeps a polling scan bounded.
|
|
236
275
|
|
|
276
|
+
Polling and startup catch-up preserve each logical path when multiple directory
|
|
277
|
+
symlinks point to the same source tree. For example, `a_shared/user.rb` and
|
|
278
|
+
`app/models/user.rb` both remain visible; an earlier alias must not hide the path
|
|
279
|
+
that extraction recognizes. Cycles back to a directory already on the current
|
|
280
|
+
traversal branch are pruned, while independent sibling aliases remain visible.
|
|
281
|
+
This can increase scan work for deliberately repeated aliases; avoid unnecessary
|
|
282
|
+
aliases in large watched trees. Ignored logical paths are still pruned.
|
|
283
|
+
|
|
237
284
|
## Placement
|
|
238
285
|
|
|
239
286
|
The spike asked for three placements to be compared and one chosen. Every
|
|
@@ -418,13 +465,11 @@ party) behaves exactly as it always did.
|
|
|
418
465
|
was told the index matched HEAD while every answer described the tree before
|
|
419
466
|
those edits.
|
|
420
467
|
|
|
421
|
-
The fingerprint
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
changed and the index followed" from "tree changed and the index has not caught
|
|
427
|
-
up".
|
|
468
|
+
The fingerprint hashes the current `git status --porcelain` path/status list.
|
|
469
|
+
Repeated edits to the same already-dirty file can leave it identical. It is not
|
|
470
|
+
content identity. Use `index.source_freshness` for generation-bound content
|
|
471
|
+
verification; see [source freshness](SOURCE_FRESHNESS.md) for `current`, `drifted`,
|
|
472
|
+
`unknown`, scan budgets, fresh-process capture and partial-runtime limitations.
|
|
428
473
|
|
|
429
474
|
### Multi-file read consistency
|
|
430
475
|
|
|
@@ -534,7 +579,7 @@ and a hook-triggered `woods:incremental`. They share the existing file-based
|
|
|
534
579
|
A hook can check cheaply:
|
|
535
580
|
|
|
536
581
|
```bash
|
|
537
|
-
bundle exec rake woods:watch_status || start_the_daemon # exit 0 = alive
|
|
582
|
+
bundle exec rake woods:watch_status || start_the_daemon # same host, exit 0 = alive
|
|
538
583
|
```
|
|
539
584
|
|
|
540
585
|
The check does not boot Rails. Without `WOODS_OUTPUT`, it resolves
|
|
@@ -543,63 +588,145 @@ launcher's current directory, so `rake -f /app/Rakefile woods:watch_status`
|
|
|
543
588
|
and worktree-manager invocations inspect the same per-app status. Set
|
|
544
589
|
`WOODS_OUTPUT` when the daemon uses a non-default index directory.
|
|
545
590
|
|
|
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).
|
|
591
|
+
### Cross-host liveness
|
|
550
592
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
593
|
+
By default, a reader trusts only a same-host record: a `running` or `degraded`
|
|
594
|
+
state, a positive pid that still exists, and a recent ISO8601 timestamp. Foreign
|
|
595
|
+
hostnames are rejected because a container pid cannot be checked on the host.
|
|
596
|
+
|
|
597
|
+
For a daemon and reader sharing the same index through a bind mount, opt in in
|
|
598
|
+
each reader's environment:
|
|
599
|
+
|
|
600
|
+
```bash
|
|
601
|
+
export WOODS_WATCH_TRUST_FOREIGN_HOST=1
|
|
602
|
+
bundle exec rake woods:watch_status || start_the_daemon
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
Set the variable inside one-off containers running `woods:incremental`, and in
|
|
606
|
+
the host MCP process when it reports `woods_status`. Docker does not forward a
|
|
607
|
+
host environment variable automatically: pass `-e WOODS_WATCH_TRUST_FOREIGN_HOST=1`
|
|
608
|
+
to `docker compose run` or `docker compose exec`, or configure that service's
|
|
609
|
+
environment. Use the same shared index (`WOODS_OUTPUT` when needed) in each process.
|
|
610
|
+
|
|
611
|
+
Opted-in readers accept foreign `running` and `degraded` records on heartbeat
|
|
612
|
+
freshness, without any local pid lookup. Heartbeats run every five minutes; a
|
|
613
|
+
crashed foreign daemon can still be believed for up to 15 minutes after its last
|
|
614
|
+
heartbeat. Missing or malformed timestamps and timestamps more than 30 seconds
|
|
615
|
+
in the future are rejected. Keep the participating clocks synchronized.
|
|
616
|
+
|
|
617
|
+
`degraded` means alive but unable to update: `watch_status` exits 0, incremental
|
|
618
|
+
still attempts extraction, and clean refuses. `WOODS_IGNORE_WATCH=1` still
|
|
619
|
+
overrides writer stand-down and clean protection. It does not alter the status
|
|
620
|
+
report. Direct Ruby callers can override the environment with
|
|
621
|
+
`Status#alive?(trust_foreign_host: true)` or `false`.
|
|
622
|
+
|
|
623
|
+
This is liveness evidence for an established daemon, not a cross-container
|
|
624
|
+
startup lease. Simultaneous starts in foreign namespaces still need one
|
|
625
|
+
supervisor to coordinate ownership. Hostnames are also imperfect identity:
|
|
626
|
+
custom or reused identical container hostnames retain the local-pid limitation.
|
|
558
627
|
|
|
559
628
|
### Hooks for agent sessions
|
|
560
629
|
|
|
630
|
+
For client registration and the supported Claude/OpenCode event shapes, see
|
|
631
|
+
[edit client adapters](CLIENT_HOOKS.md). Both use the shared queue below.
|
|
632
|
+
|
|
561
633
|
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
|
|
634
|
+
with no daemon needs a different trigger, so the Woods plugin ships two freshness
|
|
563
635
|
hooks (`plugin/hooks/hooks.json`), both shipped disabled:
|
|
564
636
|
|
|
565
637
|
| Hook | When | What it does |
|
|
566
638
|
|---|---|---|
|
|
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
|
-
|
|
639
|
+
| `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` |
|
|
640
|
+
| `SessionStart` (`startup`, `resume`) | Session begins | Checks source content through `woods:source_status`; warns on drift or unknown evidence |
|
|
641
|
+
|
|
642
|
+
Both read `cwd` from the hook payload, so a linked worktree uses its own index.
|
|
643
|
+
Both require an existing `generation.json` and `WOODS_HOOKS_ENABLED=1`;
|
|
644
|
+
`WOODS_HOOKS_DISABLED=1` overrides enablement. The broader refresh task is
|
|
645
|
+
**unreleased after Woods 2.0.0.beta2**. Check the installed gem's task list
|
|
646
|
+
(`bundle exec rake -T woods:hook_refresh`, through the application container
|
|
647
|
+
when appropriate) before enabling this plugin version. An older gem's unknown
|
|
648
|
+
task error leaves queued events in place; installing the plugin does not upgrade
|
|
649
|
+
the gem.
|
|
650
|
+
|
|
651
|
+
The portable path predicate is generated from `PathDispatcher` and
|
|
652
|
+
`ReloadPolicy` with `bundle exec ruby -Ilib script/generate-hook-rules`.
|
|
653
|
+
A contract test rejects stale generated rules. It covers services, controllers,
|
|
654
|
+
jobs, concerns, views, locales, supported test/lib files, routes, package
|
|
655
|
+
boundaries, and the remaining standard extractor triggers. Unrelated documents
|
|
656
|
+
stay quiet. Normal edits use fresh-process incremental extraction; changes to
|
|
657
|
+
initializers, boot configuration, dependencies, schema, or other restart inputs
|
|
658
|
+
use fresh-process full extraction. The transport also preserves explicit
|
|
659
|
+
`add`, `update`, `delete`, and `move` operations; a relevant deletion/move selects
|
|
660
|
+
full extraction to remove runtime classes absent from the next boot. The Claude
|
|
661
|
+
adapter receives one `tool_input.file_path`. The OpenCode adapter supplies every
|
|
662
|
+
verified patch metadata path, including both rename sides. Neither infers paths
|
|
663
|
+
from shell commands or parses patch text. Custom runtime roots outside the
|
|
664
|
+
standard dispatcher rules require an explicit refresh; the portable predicate
|
|
665
|
+
cannot discover application configuration without booting it.
|
|
666
|
+
|
|
667
|
+
`WOODS_HOOK_RAKE` sets the command prefix (Docker:
|
|
668
|
+
`docker compose exec -T app bundle exec rake`). The encoded JSON task argument
|
|
669
|
+
carries paths and the output setting across the container boundary, without
|
|
670
|
+
assuming Docker forwards host environment variables or can read a host queue
|
|
671
|
+
filename. The host needs Bash 3.2 or later, standard Unix tools, and either `jq`
|
|
672
|
+
or Ruby; it does not need the application bundle. Prefix words are split without
|
|
673
|
+
shell evaluation: use an executable wrapper for quoted arguments or extra
|
|
674
|
+
container environment settings. `WOODS_OUTPUT` overrides `tmp/woods`, relative
|
|
675
|
+
to each process's application root or as an explicitly supplied absolute path;
|
|
676
|
+
absolute paths must be valid on both sides of a container bind mount.
|
|
677
|
+
|
|
678
|
+
Each event remains under `<output>/hook-pending/` until the task succeeds.
|
|
679
|
+
Successful no-op consumption is acknowledged too. Contending invocations enqueue
|
|
680
|
+
and return while the owner drains bounded batches (up to 16 queue files /
|
|
681
|
+
1,000 paths / 48 KiB of JSON, without splitting a multi-file event). Commas,
|
|
682
|
+
spaces, and newlines are preserved. An empty drain releases the lock before
|
|
683
|
+
checking again, so a final arriving event can acquire ownership.
|
|
684
|
+
A failed command, killed worker, incompatible gem, or publication failure retains
|
|
685
|
+
its batch for retry: delivery is **at least once**, so crash recovery can repeat
|
|
686
|
+
already completed work. Pending events in the previous `hook-pending.txt` format
|
|
687
|
+
are imported on the next relevant edit. Event filenames are private hook state;
|
|
688
|
+
do not modify them while a worker is running.
|
|
689
|
+
|
|
690
|
+
An active daemon produces exit **75** before Rails boots. This is a deferral,
|
|
691
|
+
not acknowledgement: the hook cannot prove which queued events the daemon has
|
|
692
|
+
consumed. It retains the queue and writes a diagnostic. It does not start, stop,
|
|
693
|
+
or restart the daemon. After resolving the cause, the next relevant edit retries
|
|
694
|
+
the queue. To retry immediately, invoke `woods-post-edit.sh` with the original
|
|
695
|
+
JSON event on stdin and the same opt-in/output/prefix settings; with a running
|
|
696
|
+
daemon, stop it first or explicitly configure `WOODS_IGNORE_WATCH=1` in the
|
|
697
|
+
application command's environment. A quiet `SessionStart` does not acknowledge
|
|
698
|
+
the queue. Prefer a resident watcher for sustained edits; enabling both does not
|
|
699
|
+
make refresh faster and can accumulate deferred events.
|
|
700
|
+
|
|
701
|
+
After the complete event input has been read, validated, and queued, the refresh
|
|
702
|
+
hook starts its `WOODS_HOOK_TIMEOUT_SECONDS` deadline (default 600, integer range
|
|
703
|
+
1–3600), including subsequent batches. The producer must close stdin: the 1 MiB
|
|
704
|
+
input limit bounds bytes, not time waiting for EOF. The deadline terminates the local
|
|
705
|
+
command process group and retains work on timeout. For a Docker exec prefix,
|
|
706
|
+
local process termination cannot guarantee cancellation inside the container;
|
|
707
|
+
check the application process and extraction lock before retrying a timed-out
|
|
708
|
+
container run. Async client hook timeouts are not a reliable worker deadline.
|
|
709
|
+
With `flock`, kernel locks release after process exit. The mkdir fallback records
|
|
710
|
+
an owner PID and reclaims dead owners; it never steals a live owner's lock based
|
|
711
|
+
only on age. Only the invocation that removes the recorded dead-owner marker
|
|
712
|
+
may replace its lock directory; competing reclaimers leave their events queued
|
|
713
|
+
for the winning owner. Legacy empty lock directories use `stat` and
|
|
714
|
+
`WOODS_HOOK_LOCK_STALE_SECONDS` (default 1800) for conservative recovery.
|
|
715
|
+
A reused PID can delay recovery until that process exits; inspect the recorded
|
|
716
|
+
owner before manually removing a lock. Hooks sharing this filesystem must run in
|
|
717
|
+
the same host PID namespace; run the actual extraction through the container
|
|
718
|
+
prefix instead of running competing host/container hook workers.
|
|
719
|
+
|
|
720
|
+
Broader coverage increases the number of Rails boots. A view or locale edit now
|
|
721
|
+
costs a fresh incremental run, while a boot/config edit costs a full run. There
|
|
722
|
+
is no provider or embedding call added by this hook. Opt in for occasional agent
|
|
723
|
+
edits; use `woods:watch` for repeated work, and keep full extraction for large
|
|
724
|
+
change sets as described above.
|
|
725
|
+
|
|
726
|
+
The `SessionStart` hook uses the shared quick source verifier through
|
|
727
|
+
`WOODS_HOOK_RAKE`. It has a ten-second command deadline, including startup;
|
|
728
|
+
failed commands and old gems lacking `woods:source_status` report unknown.
|
|
729
|
+
It does not initialize Rails or start a provider. See [source freshness](SOURCE_FRESHNESS.md#containers-and-hooks).
|
|
603
730
|
|
|
604
731
|
### Reader multiplicity is free
|
|
605
732
|
|
|
@@ -663,5 +790,78 @@ result = daemon.process(changed_paths)
|
|
|
663
790
|
# => { action: :incremental, state: :running, generation: 42, count: 1, duration_ms: 61 }
|
|
664
791
|
```
|
|
665
792
|
|
|
793
|
+
For an embedded `#run`, pass `boot_snapshot: Woods::Watch::BootSnapshot.new(root: …)`
|
|
794
|
+
with the snapshot captured **before** environment initialization if the host can
|
|
795
|
+
establish that boundary. Without it, startup restart inputs remain restart
|
|
796
|
+
requests. Direct `#process` calls always preserve conservative restart handling.
|
|
797
|
+
Injected watchers retain their existing `start`/`stop` interface; those with
|
|
798
|
+
asynchronous startup can implement `ready_callback=` and call it after detection
|
|
799
|
+
is established to participate in the readiness handshake.
|
|
800
|
+
|
|
666
801
|
`#process` is one whole cycle and is the supported embedding point. `#run` only
|
|
667
802
|
supplies batches to it.
|
|
803
|
+
|
|
804
|
+
### Optional bounded context hints
|
|
805
|
+
|
|
806
|
+
Context hints are a separate Claude Code opt-in, **unreleased after Woods
|
|
807
|
+
2.0.0.beta2**. Verify `bundle exec woods-hook-context --help` in the installed
|
|
808
|
+
application bundle before enabling `WOODS_HOOK_CONTEXT_ENABLED=1`. The plugin
|
|
809
|
+
version alone does not establish gem support. `WOODS_HOOKS_DISABLED=1` disables
|
|
810
|
+
both context and refresh; `WOODS_HOOKS_ENABLED` controls only the existing
|
|
811
|
+
freshness/refresh hooks. Either feature can work without the other.
|
|
812
|
+
|
|
813
|
+
Separate synchronous SessionStart and PostToolUse entries emit Claude's
|
|
814
|
+
`hookSpecificOutput.additionalContext`. SessionStart gives a short served-index
|
|
815
|
+
orientation. After a relevant native Edit/Write/MultiEdit, the hint identifies
|
|
816
|
+
candidates from one retained published generation. Direct candidates and
|
|
817
|
+
transitive candidates are distinguished; test mappings are suggestions, never
|
|
818
|
+
proof of coverage. Post-edit hints always say **pre-refresh snapshot** because
|
|
819
|
+
an edit can precede publication. Source freshness is checked against that same
|
|
820
|
+
payload; unknown/drifted evidence remains explicit. An unresolved or ambiguous
|
|
821
|
+
edited identity directs the agent to manual search and typed lookup. No match
|
|
822
|
+
within the bounded snapshot establishes neither absence nor no impact.
|
|
823
|
+
|
|
824
|
+
Limits are fixed: depth 2, at most 10 visited nodes including the root, 100
|
|
825
|
+
examined edges, and 2 KiB for the **entire JSON output**, preserving whole rows.
|
|
826
|
+
An index is refused above 16 MiB per required artifact or 50,000 combined graph
|
|
827
|
+
nodes/variants. These preparation checks, JSON parsing, cache construction,
|
|
828
|
+
source verification, path/content hashing, formatting and suppression state all
|
|
829
|
+
run within the hook's private process-group deadline: the worker is killed at
|
|
830
|
+
850 ms, leaving dispatch/cleanup headroom within a one-second work budget.
|
|
831
|
+
The helper also has a 650 ms inner deadline. OS scheduling can delay observation
|
|
832
|
+
of a deadline. Cold bundle/container startup can therefore produce no hint;
|
|
833
|
+
the deadline is not extended. Oversized evidence is marked truncated; missing,
|
|
834
|
+
corrupt, unsupported or timed-out input produces a short unknown notice or
|
|
835
|
+
silence. Silence is never a complete/no-impact claim. The hint boots no Rails
|
|
836
|
+
application and calls no provider.
|
|
837
|
+
|
|
838
|
+
The synchronous opt-in can add up to this budget to a supported tool call. It
|
|
839
|
+
makes context available to Claude's next model request; it does not rely on the
|
|
840
|
+
later-turn delivery of the independent asynchronous refresh worker. A reminder
|
|
841
|
+
need not appear as a visible transcript entry. See the
|
|
842
|
+
[Claude context-output contract](https://code.claude.com/docs/en/hooks#add-context-for-claude).
|
|
843
|
+
|
|
844
|
+
The default command is `bundle exec woods-hook-context`. Set
|
|
845
|
+
`WOODS_HOOK_CONTEXT_COMMAND` to an executable wrapper or argv prefix for a
|
|
846
|
+
container-only bundle. Prefix words are split without shell evaluation; a
|
|
847
|
+
wrapper handles quoted arguments. Explicit `WOODS_HOOK_CONTEXT_ROOT` maps the
|
|
848
|
+
hook payload's original cwd and contained edit path onto a runtime-visible
|
|
849
|
+
application root. For example, a container prefix can include
|
|
850
|
+
`docker compose exec -T -e WOODS_HOOK_CONTEXT_ENABLED=1 -e WOODS_HOOK_CONTEXT_ROOT=/app app bundle exec woods-hook-context`.
|
|
851
|
+
Forward a custom `WOODS_OUTPUT` explicitly too. Running Claude inside the
|
|
852
|
+
application container avoids external container startup and path mapping.
|
|
853
|
+
|
|
854
|
+
Repeat suppression uses session/worktree, served generation/token, changed-file
|
|
855
|
+
content identity and normalized hint content. Repeated identical evidence stays
|
|
856
|
+
quiet; later same-file edits and generation changes can reappear. Missing session
|
|
857
|
+
or bounded content identity disables suppression. Private `hook-context-state.json`
|
|
858
|
+
retains at most 32 sessions and 32 emitted identities per session under a separate
|
|
859
|
+
nonblocking lock. It records **emitted**, not confirmed delivered, hints.
|
|
860
|
+
Contending or unavailable state may skip optional context. Its bounded atomic
|
|
861
|
+
state update never reads, acknowledges, or clears `hook-pending`, nor acquires
|
|
862
|
+
refresh/watch locks. Disable context to roll back without changing refresh.
|
|
863
|
+
|
|
864
|
+
Only the native Claude context entries are supported here; the OpenCode adapter
|
|
865
|
+
continues to provide refresh events. No prompt-triggered retrieval is injected.
|
|
866
|
+
|
|
867
|
+
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
|
@@ -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
|