woods 2.0.0.beta4 → 2.0.0
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 +480 -477
- data/CONTRIBUTING.md +2 -2
- data/README.md +11 -26
- data/docs/AGENT_GUIDE.md +31 -12
- data/docs/AGENT_SETUP.md +17 -10
- data/docs/AUTOMATIC_MAINTENANCE.md +222 -0
- data/docs/BACKEND_MATRIX.md +13 -7
- data/docs/CLIENT_HOOKS.md +1 -1
- data/docs/CONFIGURATION_REFERENCE.md +36 -26
- data/docs/CONSOLE_MCP_SETUP.md +10 -8
- data/docs/DOCKER_SETUP.md +15 -0
- data/docs/EVALUATION.md +10 -4
- data/docs/EXTRACTOR_REFERENCE.md +14 -2
- data/docs/FAQ.md +14 -3
- data/docs/GETTING_STARTED.md +18 -17
- data/docs/INCREMENTAL_EXTRACTION.md +8 -3
- data/docs/INDEX_LAYOUT.md +2 -2
- data/docs/MCP_SERVERS.md +28 -11
- data/docs/MCP_TOOL_COOKBOOK.md +1 -1
- data/docs/MCP_WORKTREE_SETUP.md +13 -1
- data/docs/PUBLISHED_INDEX.md +1 -1
- data/docs/README.md +2 -1
- data/docs/RETRIEVAL_GUIDE.md +57 -8
- data/docs/SOURCE_FRESHNESS.md +1 -1
- data/docs/TOKEN_BENCHMARK.md +16 -10
- data/docs/TROUBLESHOOTING.md +133 -37
- data/docs/UPGRADING_TO_2.md +9 -7
- data/docs/WATCH_DAEMON.md +172 -17
- data/docs/WHY_WOODS.md +9 -5
- data/exe/woods-console +13 -11
- data/exe/woods-watch +5 -0
- data/lib/generators/woods/watch_generator.rb +53 -0
- data/lib/puma/plugin/woods.rb +10 -0
- data/lib/tasks/woods.rake +14 -0
- data/lib/woods/cache/cache_middleware.rb +6 -0
- data/lib/woods/console/stdio_transport.rb +27 -0
- data/lib/woods/extractor.rb +25 -7
- data/lib/woods/git_command.rb +6 -7
- data/lib/woods/git_provenance.rb +4 -6
- data/lib/woods/mcp/bootstrapper.rb +3 -1
- data/lib/woods/mcp/initialization_guidance.rb +1 -1
- data/lib/woods/mcp/server.rb +41 -9
- data/lib/woods/retrieval/corpus_status.rb +46 -0
- data/lib/woods/retriever.rb +19 -7
- data/lib/woods/storage/local_corpus_stats.rb +32 -0
- data/lib/woods/storage/metadata_store.rb +20 -0
- data/lib/woods/storage/vector_store.rb +10 -0
- data/lib/woods/version.rb +1 -1
- data/lib/woods/watch/child_environment.rb +30 -0
- data/lib/woods/watch/cli.rb +91 -0
- data/lib/woods/watch/daemon.rb +55 -7
- data/lib/woods/watch/event_stream.rb +70 -0
- data/lib/woods/watch/guardian.rb +142 -0
- data/lib/woods/watch/installation/layout.rb +70 -0
- data/lib/woods/watch/installation/options.rb +128 -0
- data/lib/woods/watch/installation/planner.rb +128 -0
- data/lib/woods/watch/installation/probe.rb +101 -0
- data/lib/woods/watch/installation/receipt.rb +77 -0
- data/lib/woods/watch/installation/recovery.rb +64 -0
- data/lib/woods/watch/installation/templates.rb +58 -0
- data/lib/woods/watch/installation.rb +56 -0
- data/lib/woods/watch/lifecycle.rb +182 -0
- data/lib/woods/watch/managed_child.rb +113 -0
- data/lib/woods/watch/managed_cleanup.rb +48 -0
- data/lib/woods/watch/managed_process.rb +144 -0
- data/lib/woods/watch/puma_adapter.rb +87 -0
- data/lib/woods/watch/puma_child.rb +66 -0
- data/lib/woods/watch/supervision_records.rb +95 -0
- data/lib/woods/watch/supervision_status.rb +104 -0
- data/lib/woods/watch/supervisor.rb +161 -0
- data/lib/woods/watch/supervisor_reporting.rb +46 -0
- data/plugin/.claude-plugin/plugin.json +1 -1
- data/plugin/skills/woods-agent-enable/SKILL.md +1 -1
- data/plugin/skills/woods-diagnose/SKILL.md +77 -8
- data/plugin/skills/woods-investigate/SKILL.md +6 -6
- data/plugin/skills/woods-mcp-config/SKILL.md +28 -1
- data/plugin/skills/woods-setup/SKILL.md +58 -4
- metadata +35 -5
data/docs/WATCH_DAEMON.md
CHANGED
|
@@ -34,15 +34,160 @@ Ctrl-C to stop.
|
|
|
34
34
|
| `WOODS_WATCH_CATCH_UP` | `1` | `0` skips the startup reconciliation |
|
|
35
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) |
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
The raw task needs a supervisor that restarts it after exit `75` (`EX_TEMPFAIL`);
|
|
38
|
+
see [Restart triggers](#restart-triggers). Docker restart policies can supply
|
|
39
|
+
this. A bare task in Foreman cannot: Foreman shuts down its process group when a
|
|
40
|
+
child exits. Use the managed launcher below for native Procfile development.
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
### Managed development startup
|
|
43
|
+
|
|
44
|
+
**Included in Woods `2.0.0` ([#538](https://github.com/lost-in-the/woods/issues/538)).**
|
|
45
|
+
Record the installed gem path and Git revision as well as VERSION; a checkout
|
|
46
|
+
can have new behavior with the last beta's version. Verify both commands before
|
|
47
|
+
using the new setup:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
bundle exec woods-watch --help
|
|
51
|
+
bin/rails generate woods:watch --help
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Choose one lifecycle owner per application index:
|
|
55
|
+
|
|
56
|
+
| Existing development workflow | Setup |
|
|
57
|
+
|---|---|
|
|
58
|
+
| Rails/Puma, including a `bin/dev` that just starts Rails | Opt-in `puma` mode; the development Puma master starts one separate extraction process, regardless of worker count. |
|
|
59
|
+
| An existing Foreman workflow | `procfile` mode with the exact existing Procfile and selected normal Foreman command. |
|
|
60
|
+
| Docker Compose, Grove, or another supervisor | `external` mode returns the raw task command; configure its lifecycle through the existing supervisor. |
|
|
61
|
+
|
|
62
|
+
For Puma, preview before applying:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
bin/rails generate woods:watch --mode puma --pretend
|
|
66
|
+
bin/rails generate woods:watch --mode puma
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This adds an executable `bin/woods-watch` and an owned directive to
|
|
70
|
+
`config/puma.rb`. The directive loads the plugin only when the active Woods gem
|
|
71
|
+
contains it. An absent gem or an older version without the plugin leaves Puma
|
|
72
|
+
running without a watcher; this also works for Git/path bundles. The adapter
|
|
73
|
+
separately enforces Puma's finalized development environment.
|
|
74
|
+
Starting a console, task, or production
|
|
75
|
+
Puma does not start a watcher. The watcher still uses another Rails process and its associated memory.
|
|
76
|
+
Puma installation checks the application's installed Puma version (supported
|
|
77
|
+
majors: 6, 7, and 8 on native Unix Ruby) and selects the normal `bin/rails server`
|
|
78
|
+
path using `config/puma.rb`. If `config/puma/development.rb` exists, setup refuses
|
|
79
|
+
because Puma would choose it first. Custom `-C` configurations need an explicit
|
|
80
|
+
external/Foreman arrangement; this generator does not discover or rewrite them.
|
|
81
|
+
|
|
82
|
+
For an existing Foreman arrangement, select the command you will actually use:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
bin/rails generate woods:watch --mode procfile --procfile Procfile.dev \
|
|
86
|
+
--manager-command 'foreman start -f Procfile.dev' --pretend
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Repeat without `--pretend` to apply. Preflight checks task discovery and Foreman
|
|
90
|
+
availability with bounded commands; it does not start services. Installation
|
|
91
|
+
preserves `bin/dev` and every unowned service. If `bin/dev` only runs Rails, it
|
|
92
|
+
will still only run Rails: choose Puma mode or explicitly use the selected
|
|
93
|
+
Foreman command. No process-manager gem is installed automatically.
|
|
94
|
+
|
|
95
|
+
Run setup with the same application environment as normal startup. Preflight
|
|
96
|
+
preserves environment-based Bundler configuration, including `BUNDLE_PATH`,
|
|
97
|
+
`BUNDLE_APP_CONFIG`, and excluded groups, while resetting inherited activation
|
|
98
|
+
state and requesting frozen resolution. Earlier Git builds of this
|
|
99
|
+
installer dropped those settings ([#540](https://github.com/lost-in-the/woods/issues/540)).
|
|
100
|
+
If normal task discovery works but installer preflight cannot find Rails or an
|
|
101
|
+
installed dependency, check the loaded revision and bundle environment before
|
|
102
|
+
reinstalling gems or adding a persistent `.bundle/config` workaround.
|
|
103
|
+
|
|
104
|
+
Installer refusals return a nonzero exit status with their diagnostic (#544,
|
|
105
|
+
included in Woods `2.0.0`). Earlier Git builds printed the refusal but exited
|
|
106
|
+
successfully, so automation using those builds must also check the diagnostic and
|
|
107
|
+
whether installation actually applied.
|
|
108
|
+
|
|
109
|
+
Use `--child-command 'bundle exec rails woods:watch'` when that is the
|
|
110
|
+
application's actual Rails entrypoint. Command strings become explicit argument
|
|
111
|
+
vectors; shell pipelines, variable expansion, and shell setup scripts are not
|
|
112
|
+
supported. Do not prepend an `environment` task or an already booted Rails runner.
|
|
113
|
+
|
|
114
|
+
### Ownership, updates, and removal
|
|
115
|
+
|
|
116
|
+
The generator records relative owned paths and fingerprints in `.woods-watch.json`.
|
|
117
|
+
Commit it with the generated configuration so a new clone/worktree can update or
|
|
118
|
+
remove that setup. Runtime transaction state stays under `tmp/woods-watch-install/`;
|
|
119
|
+
keep that directory ignored. Changes to owned content or executable permissions
|
|
120
|
+
cause a conflict rather than an overwrite. Review the conflict and restore or
|
|
121
|
+
adapt the owned setup explicitly; do not delete the receipt to force an overwrite.
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Change modes or update owned setup (include the selected mode's options).
|
|
125
|
+
bin/rails generate woods:watch --operation update --mode puma --pretend
|
|
126
|
+
# Remove owned startup fragments and wrapper; preserve application edits.
|
|
127
|
+
bin/rails generate woods:watch --operation remove --pretend
|
|
44
128
|
```
|
|
45
129
|
|
|
130
|
+
Repeat without `--pretend` to apply. Stop an existing watcher through its current
|
|
131
|
+
manager before changing owners; the generator does not stop running processes.
|
|
132
|
+
Explicit updates refresh owned directives in place, preserving surrounding text
|
|
133
|
+
and the block's newline style. Repeating setup preserves the existing directive.
|
|
134
|
+
Earlier Git builds used a Puma guard that checked only whether Woods was loaded;
|
|
135
|
+
use the update command above with a supporting bundle to install the capability
|
|
136
|
+
guard ([#542](https://github.com/lost-in-the/woods/issues/542), included in Woods
|
|
137
|
+
`2.0.0`). Do not hand-edit the owned block or receipt.
|
|
138
|
+
|
|
139
|
+
For a permanent downgrade, remove the startup configuration while the supporting
|
|
140
|
+
gem is still installed. The updated Puma guard lets a branch with an older gem
|
|
141
|
+
boot without indexing; it does not make `bin/woods-watch` or a Foreman entry
|
|
142
|
+
compatible with that gem.
|
|
143
|
+
For Docker/Grove, see [automatic maintenance](AUTOMATIC_MAINTENANCE.md).
|
|
144
|
+
|
|
145
|
+
An interrupted apply retains its local transaction journal. When Rails boots,
|
|
146
|
+
preview recovery with `bin/rails generate woods:watch --operation recover
|
|
147
|
+
--pretend`, then repeat without `--pretend`. The Rails command boots the application
|
|
148
|
+
before invoking the generator; its `--pretend` prevents installation writes,
|
|
149
|
+
not application initialization.
|
|
150
|
+
|
|
151
|
+
If an initializer prevents Rails boot, run the boot-free recovery helper directly
|
|
152
|
+
from the application root using its bundle:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
bundle exec ruby -rwoods/watch/installation -e \
|
|
156
|
+
'puts Woods::Watch::Installation.new(root: Dir.pwd).recover(pretend: true)'
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Repeat with `pretend: false` to restore the recorded transaction. Recovery checks
|
|
160
|
+
snapshots and changes only owned files; concurrent edits keep the journal for
|
|
161
|
+
manual resolution. The helper does not load Rails or run task probes. Do not
|
|
162
|
+
delete a pending journal to force setup.
|
|
163
|
+
|
|
164
|
+
### Managed restart and failure behavior
|
|
165
|
+
|
|
166
|
+
`woods-watch -- bin/rails woods:watch` stays outside Rails and starts a fresh child
|
|
167
|
+
for each planned restart. It absorbs exit 75 so Foreman/Puma remain running.
|
|
168
|
+
Managed launching requires POSIX process groups and `fork` (Linux/macOS Ruby).
|
|
169
|
+
Other platforms should use the raw task with an external supervisor.
|
|
170
|
+
Boot failures retry with bounded backoff; a failed extraction leaves the last
|
|
171
|
+
good generation readable. `--boot-timeout SECONDS` defaults to 300 seconds and
|
|
172
|
+
bounds boot/handshake, not a valid extraction or writer-lock wait.
|
|
173
|
+
|
|
174
|
+
Managed mode requires `WOODS_WATCH_IDLE_TIMEOUT` to be unset: automatic maintenance
|
|
175
|
+
needs a resident child. An unexpected clean stop, incompatible child, or another
|
|
176
|
+
owner parks the launcher with a diagnostic until its normal owner restarts it.
|
|
177
|
+
There is no automatic ownership takeover. Managed duplicate prevention is scoped
|
|
178
|
+
to one host/process namespace; do not mix raw and managed writers across different
|
|
179
|
+
containers sharing an index. Use one external owner for that arrangement.
|
|
180
|
+
|
|
181
|
+
Daemon liveness, completed startup reconciliation, and source freshness remain
|
|
182
|
+
different facts. Managed supervision records under `watch_supervisors/` expose
|
|
183
|
+
starting/retrying/parked state separately in `woods_status`; an alive supervisor
|
|
184
|
+
without a maintaining child does not provide daemon coverage. If the first Rails
|
|
185
|
+
boot fails before resolving the configured index path, diagnostics are logs-only.
|
|
186
|
+
|
|
187
|
+
Verify startup catch-up, an edit, and an initializer restart through the existing
|
|
188
|
+
MCP connection before declaring automatic maintenance active. With worktree
|
|
189
|
+
management, also verify source and index mounts switch together.
|
|
190
|
+
|
|
46
191
|
## One cycle
|
|
47
192
|
|
|
48
193
|
```
|
|
@@ -169,7 +314,7 @@ is alive, so *alive has to mean covered*.
|
|
|
169
314
|
The standalone `woods:watch` task snapshots reload/restart inputs before invoking
|
|
170
315
|
Rails' `environment` task. Inputs unchanged across that boundary, including
|
|
171
316
|
carried paths that remain deleted, may be reconciled by a full extraction.
|
|
172
|
-
|
|
317
|
+
Included in Woods `2.0.0`: registered restart inputs deleted while the
|
|
173
318
|
daemon was stopped also trigger a full extraction after a fresh environment
|
|
174
319
|
boot. Nominal framework paths still use the bounded deletion sweep.
|
|
175
320
|
Changes during environment initialization still require restart. Lock contention,
|
|
@@ -283,6 +428,14 @@ Ignored by default: `.git`, `node_modules`, `tmp`, `log`, `coverage`,
|
|
|
283
428
|
`vendor/bundle`, `public/assets`, `public/packs`, `storage`. That ignore list is
|
|
284
429
|
what keeps a polling scan bounded.
|
|
285
430
|
|
|
431
|
+
A commit, ref update, or other Git-only change does not necessarily trigger a
|
|
432
|
+
source-file event. The watcher can therefore keep source current while the
|
|
433
|
+
published Git history or provenance still describes an earlier commit. Run full
|
|
434
|
+
`woods:extract` when current Git metadata is required; incremental extraction
|
|
435
|
+
refreshes history only for rewritten units. For containerized linked worktrees,
|
|
436
|
+
verify [Git directory selection](TROUBLESHOOTING.md#git-directory-mounts-for-linked-worktrees)
|
|
437
|
+
as well as source and index mounts.
|
|
438
|
+
|
|
286
439
|
Polling and startup catch-up preserve each logical path when multiple directory
|
|
287
440
|
symlinks point to the same source tree. For example, `a_shared/user.rb` and
|
|
288
441
|
`app/models/user.rb` both remain visible; an earlier alias must not hide the path
|
|
@@ -293,15 +446,15 @@ aliases in large watched trees. Ignored logical paths are still pruned.
|
|
|
293
446
|
|
|
294
447
|
## Placement
|
|
295
448
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
449
|
+
Run one dedicated extraction process per active worktree/index. The managed
|
|
450
|
+
Puma adapter and native launcher above retain that separation. Requiring Woods
|
|
451
|
+
or loading its Railtie does not start indexing by itself. Custom integrations
|
|
452
|
+
can use the injected daemon collaborators deliberately:
|
|
300
453
|
|
|
301
454
|
| Option | Verdict |
|
|
302
455
|
|---|---|
|
|
303
456
|
| **(b) Dedicated daemon**: *recommended default* | One extra booted app per worktree. Isolated: a crash, a restart, or a storm affects only the index. Lifecycle is drivable from worktree hooks. |
|
|
304
|
-
| **(a)
|
|
457
|
+
| **(a) Custom in-process integration** | A host can call public `Daemon#process` deliberately, but must own threading, reloading, and lifecycle. This is not an automatic Railtie feature or the managed Puma adapter. |
|
|
305
458
|
| **(c) Host watcher + in-container session** | Solves bind-mount event unreliability, but with the most moving parts. Forcing the polling backend solves the same problem with none. |
|
|
306
459
|
|
|
307
460
|
Measured on the fixture app (Ruby 3.3, Rails 8.0):
|
|
@@ -652,7 +805,7 @@ hooks (`plugin/hooks/hooks.json`), both shipped disabled:
|
|
|
652
805
|
Both read `cwd` from the hook payload, so a linked worktree uses its own index.
|
|
653
806
|
Both require an existing `generation.json` and `WOODS_HOOKS_ENABLED=1`;
|
|
654
807
|
`WOODS_HOOKS_DISABLED=1` overrides enablement. The broader refresh task is
|
|
655
|
-
**
|
|
808
|
+
**included in Woods `2.0.0`**. Check the installed gem's task list
|
|
656
809
|
(`bundle exec rake -T woods:hook_refresh`, through the application container
|
|
657
810
|
when appropriate) before enabling this plugin version. An older gem's unknown
|
|
658
811
|
task error leaves queued events in place; installing the plugin does not upgrade
|
|
@@ -750,8 +903,10 @@ per-process language-server index does not.
|
|
|
750
903
|
|
|
751
904
|
N resident daemons is N booted apps, and most slots are dormant most of the
|
|
752
905
|
time. `idle_timeout` (off by default) stops a daemon after that many seconds
|
|
753
|
-
without a file event
|
|
754
|
-
|
|
906
|
+
without a file event. Revival requires a separately configured supervisor or
|
|
907
|
+
startup hook; the shipped SessionStart hook only checks freshness. Managed
|
|
908
|
+
`woods-watch` rejects this setting because an idle child shutdown would leave
|
|
909
|
+
automatic maintenance inactive until the owner restarts it.
|
|
755
910
|
|
|
756
911
|
```ruby
|
|
757
912
|
Woods::Watch::Daemon.new(output_dir: …, idle_timeout: 900).run # 15 minutes
|
|
@@ -813,8 +968,8 @@ supplies batches to it.
|
|
|
813
968
|
|
|
814
969
|
### Optional bounded context hints
|
|
815
970
|
|
|
816
|
-
Context hints are a separate Claude Code opt-in, **
|
|
817
|
-
|
|
971
|
+
Context hints are a separate Claude Code opt-in, **included in Woods `2.0.0`**.
|
|
972
|
+
Verify `bundle exec woods-hook-context --help` in the installed
|
|
818
973
|
application bundle before enabling `WOODS_HOOK_CONTEXT_ENABLED=1`. The plugin
|
|
819
974
|
version alone does not establish gem support. `WOODS_HOOKS_DISABLED=1` disables
|
|
820
975
|
both context and refresh; `WOODS_HOOKS_ENABLED` controls only the existing
|
data/docs/WHY_WOODS.md
CHANGED
|
@@ -56,9 +56,11 @@ It describes what the service does, but misses that `order.save!` triggers `afte
|
|
|
56
56
|
:send_confirmation_email` on `Order`, which itself enqueues `InventoryJob` via
|
|
57
57
|
`after_save :reserve_stock` on `LineItem`.
|
|
58
58
|
|
|
59
|
-
With Woods,
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
With Woods, published graph relationships can connect related services, models,
|
|
60
|
+
callbacks, and jobs for retrieval. Arbitrary method-body constant references are
|
|
61
|
+
not exhaustively indexed, so the whole `CheckoutService` → `Order` → `LineItem` →
|
|
62
|
+
`InventoryJob` chain may not be recorded. Inspect the source and any flow evidence,
|
|
63
|
+
including reported ambiguity and traversal limits, before concluding what runs.
|
|
62
64
|
|
|
63
65
|
---
|
|
64
66
|
|
|
@@ -175,11 +177,13 @@ Three tools answered "what is in this Rails app" for coding agents in 2026. Wood
|
|
|
175
177
|
| Rubydex (Shopify) | 0.4.1, announced 2026-05-12 | Rust static index of declarations, references, ancestors; experimental `rdx mcp` | Symbol references across a large tree, fast re-index, reported 15 to 80 percent token reduction | Resolved callbacks, inlined concerns, routes as Rails builds them, database partition, churn |
|
|
176
178
|
| rails-mcp-server | 2.0.0 | Boots the app; `analyze_models`, `get_routes`, `get_schema` | Live model, route, and schema listings over MCP | Callback side effects, request flows, git churn, graph reports, persistent index with generations |
|
|
177
179
|
| ruby-lsp-rails | 0.5.0.beta1 | Runtime server over `rails runner` for the editor | Model columns, association targets, route info at the cursor | A persistent index other tools can read, graph analysis, multi-database facts |
|
|
178
|
-
| Woods | 2.0 | Boots the app once, publishes an atomic JSON generation, serves it without Rails | Resolved runtime behavior on top of structure: inlined concerns, callback side effects, flows, churn, PageRank; database
|
|
180
|
+
| Woods | 2.0 | Boots the app once, publishes an atomic JSON generation, serves it without Rails | Resolved runtime behavior on top of structure: inlined concerns, callback side effects, flows, churn, PageRank; recorded database partitions and Packwerk boundaries | Symbol-level references inside method bodies (Rubydex is the better fit and is complementary) |
|
|
179
181
|
|
|
180
182
|
Woods and Rubydex are complementary. Rubydex answers "where is this symbol referenced". Woods answers "what happens when this runs, and what does it touch". An agent can use both: Rubydex for references, Woods for behavior, boundaries, and blast radius.
|
|
181
183
|
|
|
182
|
-
|
|
184
|
+
Woods records database-partition metadata on models and Packwerk package ownership
|
|
185
|
+
and boundaries. See [Extractor reference](EXTRACTOR_REFERENCE.md#modelextractor)
|
|
186
|
+
for the fields and their coverage limits.
|
|
183
187
|
|
|
184
188
|
---
|
|
185
189
|
|
data/exe/woods-console
CHANGED
|
@@ -21,14 +21,14 @@
|
|
|
21
21
|
# Check if the rake task already captured stdout for us.
|
|
22
22
|
protocol_out = $woods_protocol_out # rubocop:disable Style/GlobalVars
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
end
|
|
24
|
+
# Running via rails runner — capture stdout ourselves.
|
|
25
|
+
protocol_out ||= $stdout.dup
|
|
26
|
+
# Keep application logs and writes on stderr for the entire server lifetime.
|
|
27
|
+
$stdout.reopen($stderr)
|
|
29
28
|
|
|
30
29
|
require 'woods'
|
|
31
30
|
require 'woods/console/server'
|
|
31
|
+
require 'woods/console/stdio_transport'
|
|
32
32
|
|
|
33
33
|
unless Woods.configuration.console_mcp_enabled
|
|
34
34
|
warn 'Woods Console MCP is disabled. Set ' \
|
|
@@ -132,9 +132,11 @@ server = Woods::Console::Server.build_embedded(
|
|
|
132
132
|
model_reflections: model_reflections
|
|
133
133
|
)
|
|
134
134
|
|
|
135
|
-
#
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
transport
|
|
140
|
-
|
|
135
|
+
# Only the transport writes to the saved pipe. Restoring process stdout here
|
|
136
|
+
# would also redirect Rails loggers and runtime puts back into the protocol.
|
|
137
|
+
transport = Woods::Console::StdioTransport.new(server, output: protocol_out)
|
|
138
|
+
begin
|
|
139
|
+
transport.open
|
|
140
|
+
ensure
|
|
141
|
+
protocol_out.close unless protocol_out.closed?
|
|
142
|
+
end
|
data/exe/woods-watch
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'woods/watch/installation'
|
|
5
|
+
|
|
6
|
+
module Woods
|
|
7
|
+
module Generators
|
|
8
|
+
# Reversible opt-in startup integration; never rewrites an application's bin/dev.
|
|
9
|
+
class WatchGenerator < Rails::Generators::Base
|
|
10
|
+
desc 'Install, update, or remove owned Woods watcher startup configuration'
|
|
11
|
+
|
|
12
|
+
class_option :mode, type: :string, desc: 'Explicit startup mode: procfile, puma, or external'
|
|
13
|
+
class_option :operation, type: :string, default: 'setup', desc: 'setup, update, remove, or recover'
|
|
14
|
+
class_option :procfile, type: :string, default: 'Procfile.dev', desc: 'Existing root-level Foreman Procfile'
|
|
15
|
+
class_option :manager_command, type: :string,
|
|
16
|
+
desc: 'Normal Foreman startup argv, e.g. foreman start -f Procfile.dev'
|
|
17
|
+
class_option :child_command, type: :string, default: 'bin/rails woods:watch',
|
|
18
|
+
desc: 'Application task command, parsed as argv (no shell evaluation)'
|
|
19
|
+
|
|
20
|
+
# Rails generators otherwise print Thor errors and return a successful status.
|
|
21
|
+
# @return [Boolean] whether a refused installation fails the CLI command
|
|
22
|
+
def self.exit_on_failure?
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [void]
|
|
27
|
+
def configure_watcher
|
|
28
|
+
operation = behavior == :revoke ? 'remove' : options[:operation]
|
|
29
|
+
installation = build_installation(operation)
|
|
30
|
+
if operation == 'recover'
|
|
31
|
+
say installation.recover(pretend: options[:pretend])
|
|
32
|
+
return
|
|
33
|
+
end
|
|
34
|
+
plan = installation.plan
|
|
35
|
+
say JSON.pretty_generate(plan.summary)
|
|
36
|
+
return if options[:pretend]
|
|
37
|
+
|
|
38
|
+
say installation.apply(plan)
|
|
39
|
+
say installation.handoff unless operation == 'remove'
|
|
40
|
+
rescue Woods::Watch::Installation::Conflict => e
|
|
41
|
+
raise Thor::Error, e.message
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def build_installation(operation)
|
|
47
|
+
Woods::Watch::Installation.new(root: destination_root, mode: options[:mode], operation: operation,
|
|
48
|
+
procfile: options[:procfile], manager_command: options[:manager_command],
|
|
49
|
+
child_command: options[:child_command])
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/tasks/woods.rake
CHANGED
|
@@ -123,6 +123,9 @@ namespace :woods do
|
|
|
123
123
|
|
|
124
124
|
desc 'Watch the app and keep the index current (resident daemon)'
|
|
125
125
|
task :watch do
|
|
126
|
+
require 'woods/watch/managed_child'
|
|
127
|
+
managed_child = Woods::Watch::ManagedChild.from_env
|
|
128
|
+
managed_child&.call(:task_loaded, woods_version: Woods::VERSION)
|
|
126
129
|
# Observe inputs before Rails initializes. A snapshot taken in Daemon.new
|
|
127
130
|
# would silently bless edits made while initializers were running.
|
|
128
131
|
require 'woods/watch/boot_snapshot'
|
|
@@ -130,6 +133,11 @@ namespace :woods do
|
|
|
130
133
|
fresh_environment = !environment.already_invoked && !Rails.application.initialized?
|
|
131
134
|
boot_snapshot = Woods::Watch::BootSnapshot.new(root: Rails.root) if fresh_environment
|
|
132
135
|
environment.invoke
|
|
136
|
+
if managed_child && !Rails.env.development?
|
|
137
|
+
managed_child.call(:terminal, reason: 'unsupported_environment')
|
|
138
|
+
warn 'Managed Woods watching requires the finalized Rails development environment.'
|
|
139
|
+
next
|
|
140
|
+
end
|
|
133
141
|
# Both, and the extractor is not optional. The daemon's default
|
|
134
142
|
# extractor_factory names Woods::Extractor lazily, so omitting this require
|
|
135
143
|
# loaded and started cleanly and then NameError'd on the first real cycle —
|
|
@@ -140,6 +148,7 @@ namespace :woods do
|
|
|
140
148
|
require 'woods/watch/daemon'
|
|
141
149
|
|
|
142
150
|
output_dir = ENV.fetch('WOODS_OUTPUT', Woods.configuration.output_dir)
|
|
151
|
+
managed_child&.call(:identity, root: File.expand_path(Rails.root.to_s), index: File.expand_path(output_dir.to_s))
|
|
143
152
|
|
|
144
153
|
poll_interval = begin
|
|
145
154
|
Float(ENV.fetch('WOODS_WATCH_POLL_INTERVAL', Woods::Watch::Watcher::DEFAULT_POLL_INTERVAL))
|
|
@@ -162,6 +171,8 @@ namespace :woods do
|
|
|
162
171
|
idle_timeout: ENV.fetch('WOODS_WATCH_IDLE_TIMEOUT', nil) && Float(ENV.fetch('WOODS_WATCH_IDLE_TIMEOUT')),
|
|
163
172
|
catch_up: ENV['WOODS_WATCH_CATCH_UP'] != '0',
|
|
164
173
|
boot_snapshot: boot_snapshot,
|
|
174
|
+
lifecycle: managed_child,
|
|
175
|
+
conservative_claims: !managed_child.nil?,
|
|
165
176
|
logger: Rails.logger
|
|
166
177
|
)
|
|
167
178
|
|
|
@@ -182,6 +193,7 @@ namespace :woods do
|
|
|
182
193
|
puts
|
|
183
194
|
|
|
184
195
|
reason = daemon.run
|
|
196
|
+
managed_child&.call(:terminal, reason: reason.to_s)
|
|
185
197
|
|
|
186
198
|
if reason == :restart_required
|
|
187
199
|
# Boot-captured state changed; Rails cannot reload it. Exit non-zero so
|
|
@@ -192,6 +204,8 @@ namespace :woods do
|
|
|
192
204
|
end
|
|
193
205
|
|
|
194
206
|
puts 'Watcher stopped.'
|
|
207
|
+
ensure
|
|
208
|
+
managed_child&.close
|
|
195
209
|
end
|
|
196
210
|
|
|
197
211
|
desc 'Keep watch over the woods — resident index daemon (alias for watch)'
|
|
@@ -421,6 +421,12 @@ module Woods
|
|
|
421
421
|
def mode = @retriever.respond_to?(:mode) ? @retriever.mode : :semantic
|
|
422
422
|
def default_budget = @retriever.respond_to?(:default_budget) ? @retriever.default_budget : 8000
|
|
423
423
|
|
|
424
|
+
# Read the live corpus rather than caching diagnostics across reloads.
|
|
425
|
+
# @return [Hash, nil] local semantic corpus statistics, when supported
|
|
426
|
+
def corpus_status(include_types: true)
|
|
427
|
+
@retriever.corpus_status(include_types: include_types) if @retriever.respond_to?(:corpus_status)
|
|
428
|
+
end
|
|
429
|
+
|
|
424
430
|
# Invalidate every cached context result. Called from the MCP +reload+
|
|
425
431
|
# tool after the retriever's stores have been re-hydrated from a fresh
|
|
426
432
|
# embed — otherwise cached results from the old embedding run would
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'mcp'
|
|
4
|
+
|
|
5
|
+
module Woods
|
|
6
|
+
module Console
|
|
7
|
+
# Keeps protocol writes separate from the host application's stdout.
|
|
8
|
+
# The SDK still owns framing, negotiation, notifications and shutdown.
|
|
9
|
+
class StdioTransport < ::MCP::Server::Transports::StdioTransport
|
|
10
|
+
# @param server [::MCP::Server] Console server
|
|
11
|
+
# @param output [IO] Original stdout saved before redirecting Rails output
|
|
12
|
+
def initialize(server, output:)
|
|
13
|
+
super(server)
|
|
14
|
+
@output = output
|
|
15
|
+
@output.set_encoding(Encoding::UTF_8)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# SDK responses, notifications and server requests share this writer.
|
|
19
|
+
# @param message [String, Hash] Encoded JSON or a JSON-compatible message
|
|
20
|
+
# @return [IO] Flushed protocol output
|
|
21
|
+
def send_response(message)
|
|
22
|
+
@output.puts(message.is_a?(String) ? message : JSON.generate(message))
|
|
23
|
+
@output.flush
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/woods/extractor.rb
CHANGED
|
@@ -2103,10 +2103,28 @@ module Woods
|
|
|
2103
2103
|
end
|
|
2104
2104
|
|
|
2105
2105
|
@git_available = complete_git_history?
|
|
2106
|
+
rescue Errno::ENOENT
|
|
2107
|
+
warn_missing_git_executable
|
|
2108
|
+
@git_available = false
|
|
2106
2109
|
rescue StandardError
|
|
2107
2110
|
@git_available = false
|
|
2108
2111
|
end
|
|
2109
2112
|
|
|
2113
|
+
# An explicit Git directory still requests history when .git is not mounted
|
|
2114
|
+
# at Rails.root. Ordinary source archives remain a supported, quiet path.
|
|
2115
|
+
#
|
|
2116
|
+
# @return [void]
|
|
2117
|
+
def warn_missing_git_executable
|
|
2118
|
+
configured = [GitCommand::OVERRIDE_KEY, 'GIT_DIR'].any? { |key| !ENV[key].to_s.empty? }
|
|
2119
|
+
return unless configured || File.exist?(File.join(Rails.root.to_s, '.git'))
|
|
2120
|
+
|
|
2121
|
+
Rails.logger.warn(
|
|
2122
|
+
'[Woods] Git history unavailable: git executable was not found in PATH. ' \
|
|
2123
|
+
'Extraction continues without per-unit Git metadata. Install Git in the extraction environment ' \
|
|
2124
|
+
'and run a full extraction to refresh Git metadata.'
|
|
2125
|
+
)
|
|
2126
|
+
end
|
|
2127
|
+
|
|
2110
2128
|
# A shallow HEAD resolves but represents an incomplete ancestry. Do not
|
|
2111
2129
|
# turn that boundary into apparent one-commit/new-file churn facts.
|
|
2112
2130
|
def complete_git_history?
|
|
@@ -2134,19 +2152,19 @@ module Woods
|
|
|
2134
2152
|
cause = error.to_s.lines.first.to_s.strip
|
|
2135
2153
|
Rails.logger.warn(
|
|
2136
2154
|
'[Woods] git cannot resolve HEAD for this working tree, so no unit will carry git ' \
|
|
2137
|
-
"metadata: #{cause}.
|
|
2138
|
-
'
|
|
2139
|
-
|
|
2155
|
+
"metadata: #{cause}. For a linked worktree, mount the complete shared .git directory " \
|
|
2156
|
+
'at its original path, or set WOODS_GIT_DIR to /mounted-common/worktrees/<id> within ' \
|
|
2157
|
+
'the relocated complete layout. Derive <id> from the worktree Git metadata, not its branch name; ' \
|
|
2158
|
+
"selecting /mounted-common itself uses the primary checkout's HEAD."
|
|
2140
2159
|
)
|
|
2141
2160
|
end
|
|
2142
2161
|
|
|
2143
2162
|
# The git command line every enrichment call runs.
|
|
2144
2163
|
#
|
|
2145
2164
|
# `-C <root>` keeps the result independent of the process working
|
|
2146
|
-
# directory. `WOODS_GIT_DIR` wins when set
|
|
2147
|
-
#
|
|
2148
|
-
#
|
|
2149
|
-
# (B-181).
|
|
2165
|
+
# directory. `WOODS_GIT_DIR` wins when set and selects that git directory's
|
|
2166
|
+
# HEAD. A relocated linked worktree needs its worktree-specific directory
|
|
2167
|
+
# inside the complete shared Git layout (B-181).
|
|
2150
2168
|
#
|
|
2151
2169
|
# @param args [Array<String>] git arguments
|
|
2152
2170
|
# @return [Array<String>] full argv
|
data/lib/woods/git_command.rb
CHANGED
|
@@ -7,18 +7,17 @@ module Woods
|
|
|
7
7
|
# than at the process working directory, so extraction launched from another
|
|
8
8
|
# checkout never reports that checkout's history.
|
|
9
9
|
#
|
|
10
|
-
# `WOODS_GIT_DIR` wins when set
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# `GIT_COMMON_DIR` does not override (B-181, B-186).
|
|
10
|
+
# `WOODS_GIT_DIR` wins when set and selects HEAD exactly as `--git-dir`
|
|
11
|
+
# does. For a container over a linked worktree, select its private directory
|
|
12
|
+
# within the complete mounted Git layout (shared objects, refs and worktrees).
|
|
13
|
+
# Selecting the shared root instead selects the primary checkout's HEAD.
|
|
14
|
+
# A same-path mount that resolves the worktree's .git pointer needs no override.
|
|
16
15
|
#
|
|
17
16
|
# Three call sites use this, and the documentation promises all three:
|
|
18
17
|
# per-unit enrichment (`Extractor`), manifest provenance (`GitProvenance`),
|
|
19
18
|
# and the `woods:incremental` diff range (`lib/tasks/woods.rake`).
|
|
20
19
|
module GitCommand
|
|
21
|
-
# Environment variable naming the
|
|
20
|
+
# Environment variable naming the explicitly selected git directory.
|
|
22
21
|
OVERRIDE_KEY = 'WOODS_GIT_DIR'
|
|
23
22
|
|
|
24
23
|
module_function
|
data/lib/woods/git_provenance.rb
CHANGED
|
@@ -85,12 +85,10 @@ module Woods
|
|
|
85
85
|
''
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
-
# +WOODS_GIT_DIR+
|
|
89
|
-
# set.
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
# private git directory reaches the shared one through a relative
|
|
93
|
-
# +commondir+ pointer that resolves outside the mount.
|
|
88
|
+
# +WOODS_GIT_DIR+ selects a git directory and its HEAD outright and wins when
|
|
89
|
+
# set. A relocated linked worktree needs its private directory inside the
|
|
90
|
+
# complete mounted Git layout, preserving access to shared objects and refs.
|
|
91
|
+
# Selecting the shared root instead uses the primary checkout's HEAD.
|
|
94
92
|
#
|
|
95
93
|
# @param args [Array<String>] git arguments
|
|
96
94
|
# @return [Array<String>] full argv
|
|
@@ -176,7 +176,9 @@ module Woods
|
|
|
176
176
|
retriever = build_retriever_from_config(config, resolved, artifact, state)
|
|
177
177
|
probe_and_mark_state(config, state)
|
|
178
178
|
derive_state_from_store_health(state)
|
|
179
|
-
|
|
179
|
+
corpus = retriever.corpus_status(include_types: false) if retriever.respond_to?(:corpus_status)
|
|
180
|
+
corpus_note = corpus ? "; corpus: #{corpus[:state]}" : ''
|
|
181
|
+
warn "[woods-mcp] semantic search: #{state.status} (#{config.embedding_provider})#{corpus_note}"
|
|
180
182
|
|
|
181
183
|
[retriever, state]
|
|
182
184
|
end
|
|
@@ -11,7 +11,7 @@ module Woods
|
|
|
11
11
|
|
|
12
12
|
WORKFLOW = <<~TEXT
|
|
13
13
|
Start with woods_status: check index readiness, generation freshness and relevant type counts before relying on results.
|
|
14
|
-
For exact names, discover identifiers with search (prefer literal exact_prefix/exact_suffix), then inspect with lookup. For conceptual questions,
|
|
14
|
+
For exact names, discover identifiers with search (prefer literal exact_prefix/exact_suffix), then inspect with lookup. For conceptual questions, check retrieval mode and data in woods_status before codebase_retrieve; structural readiness alone is insufficient. Otherwise use search and lookup.
|
|
15
15
|
Follow dependencies or dependents at depth 1 or 2; narrow types and via before paging. Respect partial results and limits: a missing match is not proof of absence, and a partial traversal does not establish every dependent or leaf.
|
|
16
16
|
Verify important conclusions against current source and tests. Recorded relationships and inferred downstream impact do not prove runtime execution or test coverage.
|
|
17
17
|
Registration does not authorize extraction, configuration changes, or live Console access. Use only the tools registered here and operate within the user's authorized scope.
|