woods 2.0.0.beta3 → 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 +500 -420
- data/CONTRIBUTING.md +29 -17
- data/README.md +78 -178
- data/docs/AGENT_GUIDE.md +52 -11
- data/docs/AGENT_SETUP.md +34 -17
- data/docs/AUTOMATIC_MAINTENANCE.md +222 -0
- data/docs/BACKEND_MATRIX.md +18 -7
- data/docs/CLIENT_HOOKS.md +1 -1
- data/docs/CONFIGURATION_REFERENCE.md +105 -29
- data/docs/CONSOLE_MCP_SETUP.md +54 -9
- data/docs/DOCKER_SETUP.md +16 -1
- data/docs/EVALUATION.md +10 -4
- data/docs/EXTRACTOR_REFERENCE.md +23 -3
- data/docs/FAQ.md +14 -3
- data/docs/GETTING_STARTED.md +18 -17
- data/docs/INCREMENTAL_EXTRACTION.md +37 -8
- data/docs/INDEX_LAYOUT.md +2 -2
- data/docs/MCP_SERVERS.md +79 -7
- data/docs/MCP_TOOL_COOKBOOK.md +5 -5
- data/docs/MCP_WORKTREE_SETUP.md +55 -83
- data/docs/PUBLISHED_INDEX.md +17 -0
- data/docs/README.md +2 -1
- data/docs/RETRIEVAL_GUIDE.md +81 -13
- data/docs/SOURCE_FRESHNESS.md +1 -1
- data/docs/TOKEN_BENCHMARK.md +16 -10
- data/docs/TROUBLESHOOTING.md +142 -47
- data/docs/UPGRADING_TO_2.md +12 -6
- data/docs/WATCH_DAEMON.md +189 -24
- data/docs/WHY_WOODS.md +9 -5
- data/exe/woods-console +13 -11
- data/exe/woods-mcp-start +14 -9
- data/exe/woods-watch +5 -0
- data/lib/generators/woods/pgvector_generator.rb +8 -2
- 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/agent_configuration/applier.rb +5 -3
- data/lib/woods/agent_configuration/cli.rb +2 -2
- data/lib/woods/agent_configuration/layout.rb +13 -0
- data/lib/woods/cache/cache_middleware.rb +6 -0
- data/lib/woods/console/credential_scanner.rb +4 -3
- data/lib/woods/console/dispatch_pipeline.rb +7 -0
- data/lib/woods/console/embedded_executor.rb +31 -9
- 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/console/stdio_transport.rb +27 -0
- data/lib/woods/coordination/pipeline_lock.rb +3 -2
- data/lib/woods/embedding/indexer.rb +24 -14
- data/lib/woods/extractor.rb +70 -19
- data/lib/woods/extractors/declared_parent.rb +55 -0
- data/lib/woods/extractors/graphql_extractor.rb +2 -11
- data/lib/woods/extractors/lib_extractor.rb +10 -8
- data/lib/woods/extractors/mailer_extractor.rb +6 -10
- data/lib/woods/extractors/model_extractor.rb +1 -15
- data/lib/woods/extractors/poro_extractor.rb +10 -8
- data/lib/woods/extractors/shared_utility_methods.rb +22 -5
- data/lib/woods/git_command.rb +6 -7
- data/lib/woods/git_provenance.rb +4 -6
- data/lib/woods/mcp/bearer_auth.rb +2 -1
- data/lib/woods/mcp/bootstrapper.rb +20 -5
- data/lib/woods/mcp/config_resolver.rb +2 -1
- data/lib/woods/mcp/index_reader.rb +11 -2
- data/lib/woods/mcp/initialization_guidance.rb +1 -1
- data/lib/woods/mcp/renderers/markdown_renderer.rb +14 -8
- data/lib/woods/mcp/renderers/plain_renderer.rb +11 -7
- data/lib/woods/mcp/server.rb +63 -37
- data/lib/woods/mcp/tool_contract.rb +1 -1
- data/lib/woods/mcp/tool_response_renderer.rb +16 -0
- data/lib/woods/mcp/traversal_evidence_text.rb +1 -1
- data/lib/woods/mcp/traversal_response.rb +22 -0
- data/lib/woods/path_dispatcher.rb +6 -5
- data/lib/woods/published_index/typed_unit_reader.rb +40 -3
- data/lib/woods/published_index.rb +2 -2
- data/lib/woods/rake_helpers.rb +2 -12
- data/lib/woods/retrieval/corpus_status.rb +46 -0
- data/lib/woods/retrieval/lexical_assembler.rb +14 -3
- data/lib/woods/retrieval/lexical_index.rb +2 -1
- data/lib/woods/retriever.rb +19 -7
- data/lib/woods/session_tracer/file_store.rb +6 -1
- data/lib/woods/source_inputs/consumer_errors.rb +4 -0
- data/lib/woods/storage/local_corpus_stats.rb +32 -0
- data/lib/woods/storage/metadata_store.rb +20 -0
- data/lib/woods/storage/pgvector.rb +6 -2
- data/lib/woods/storage/vector_store.rb +10 -0
- data/lib/woods/temporal/json_snapshot_store.rb +35 -7
- 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 +73 -11
- 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/hooks/woods-input-rules.sh +4 -4
- data/plugin/skills/woods-agent-enable/SKILL.md +7 -1
- data/plugin/skills/woods-diagnose/SKILL.md +134 -34
- data/plugin/skills/woods-investigate/SKILL.md +54 -15
- data/plugin/skills/woods-mcp-config/SKILL.md +38 -11
- data/plugin/skills/woods-setup/SKILL.md +72 -15
- metadata +38 -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
|
```
|
|
@@ -96,7 +241,7 @@ partial write:
|
|
|
96
241
|
|
|
97
242
|
| Failure | What happens |
|
|
98
243
|
|---|---|
|
|
99
|
-
| Reload raises (`SyntaxError`, `NameError`) | Degraded status naming the reason; index intact at generation N; retried on the next event |
|
|
244
|
+
| Reload raises (`SyntaxError`, `NameError`) | Degraded status naming the reason; index intact at generation N; pending paths retried on the next file event or heartbeat |
|
|
100
245
|
| Extraction raises | Degraded status; generation not advanced |
|
|
101
246
|
| 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) |
|
|
102
247
|
| 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 |
|
|
@@ -119,17 +264,23 @@ at a known generation, reason attached), `stopped` (nothing is maintaining this
|
|
|
119
264
|
index). A stale answer is only dangerous when nothing says so.
|
|
120
265
|
|
|
121
266
|
The file is written world-readable (0644) by design: host-side hooks read it
|
|
122
|
-
through a bind mount.
|
|
267
|
+
through a bind mount. Writes through `Woods::AtomicFile` default to owner-only
|
|
268
|
+
0600 unless the caller supplies another mode. This is not a guarantee for every
|
|
269
|
+
Woods artifact: the SQLite metadata store does not enforce 0600, and a newly
|
|
270
|
+
created database uses 0644 under umask 022. Restrict access to the output
|
|
271
|
+
directory according to the source and metadata it contains.
|
|
123
272
|
|
|
124
273
|
Note that `SyntaxError` is a `ScriptError`, not a `StandardError`. Rescuing
|
|
125
274
|
only the latter would let a half-typed file kill the daemon.
|
|
126
275
|
|
|
127
276
|
A cycle that fails to land its work never loses its paths. Lock contention, a
|
|
128
277
|
failed reload, and a raising extraction all carry the batch into `@pending`, and
|
|
129
|
-
the next cycle folds it back in
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
278
|
+
the next cycle folds it back in even if no new event mentions those files.
|
|
279
|
+
A degraded cycle ends the current drain to avoid a tight retry loop. Pending
|
|
280
|
+
paths are retried on the next file event or [heartbeat](#the-heartbeat), so a
|
|
281
|
+
finished contending writer does not require another edit to trigger recovery.
|
|
282
|
+
Heartbeat retries use a separate worker so status updates and lock refresh
|
|
283
|
+
continue while extraction runs.
|
|
133
284
|
|
|
134
285
|
### The heartbeat
|
|
135
286
|
|
|
@@ -163,6 +314,9 @@ is alive, so *alive has to mean covered*.
|
|
|
163
314
|
The standalone `woods:watch` task snapshots reload/restart inputs before invoking
|
|
164
315
|
Rails' `environment` task. Inputs unchanged across that boundary, including
|
|
165
316
|
carried paths that remain deleted, may be reconciled by a full extraction.
|
|
317
|
+
Included in Woods `2.0.0`: registered restart inputs deleted while the
|
|
318
|
+
daemon was stopped also trigger a full extraction after a fresh environment
|
|
319
|
+
boot. Nominal framework paths still use the bounded deletion sweep.
|
|
166
320
|
Changes during environment initialization still require restart. Lock contention,
|
|
167
321
|
extraction failure, and publication failure retain the full-reconciliation
|
|
168
322
|
obligation for retry; a successful publish clears it.
|
|
@@ -202,8 +356,9 @@ already tolerate the duplicate paths this produces against whatever catch-up
|
|
|
202
356
|
finds on its own via the tree scan.
|
|
203
357
|
|
|
204
358
|
Deletions need one extra step, because a deleted file leaves no mtime to scan:
|
|
205
|
-
|
|
206
|
-
|
|
359
|
+
registered restart inputs follow the full-reconciliation rule above. For other
|
|
360
|
+
registered paths gone from disk, a deletion-only startup runs one cycle with an
|
|
361
|
+
*empty* change set, which reaches the ghost units through the
|
|
207
362
|
extractor's bounded deletion sweep. Deliberately empty, naming the paths would
|
|
208
363
|
make the deletions authoritative for every unit type, and some registered paths
|
|
209
364
|
are nominal (on Rails < 7.1, `ActiveRecord::SchemaMigration` registers a
|
|
@@ -273,6 +428,14 @@ Ignored by default: `.git`, `node_modules`, `tmp`, `log`, `coverage`,
|
|
|
273
428
|
`vendor/bundle`, `public/assets`, `public/packs`, `storage`. That ignore list is
|
|
274
429
|
what keeps a polling scan bounded.
|
|
275
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
|
+
|
|
276
439
|
Polling and startup catch-up preserve each logical path when multiple directory
|
|
277
440
|
symlinks point to the same source tree. For example, `a_shared/user.rb` and
|
|
278
441
|
`app/models/user.rb` both remain visible; an earlier alias must not hide the path
|
|
@@ -283,15 +446,15 @@ aliases in large watched trees. Ignored logical paths are still pruned.
|
|
|
283
446
|
|
|
284
447
|
## Placement
|
|
285
448
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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:
|
|
290
453
|
|
|
291
454
|
| Option | Verdict |
|
|
292
455
|
|---|---|
|
|
293
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. |
|
|
294
|
-
| **(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. |
|
|
295
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. |
|
|
296
459
|
|
|
297
460
|
Measured on the fixture app (Ruby 3.3, Rails 8.0):
|
|
@@ -642,7 +805,7 @@ hooks (`plugin/hooks/hooks.json`), both shipped disabled:
|
|
|
642
805
|
Both read `cwd` from the hook payload, so a linked worktree uses its own index.
|
|
643
806
|
Both require an existing `generation.json` and `WOODS_HOOKS_ENABLED=1`;
|
|
644
807
|
`WOODS_HOOKS_DISABLED=1` overrides enablement. The broader refresh task is
|
|
645
|
-
**
|
|
808
|
+
**included in Woods `2.0.0`**. Check the installed gem's task list
|
|
646
809
|
(`bundle exec rake -T woods:hook_refresh`, through the application container
|
|
647
810
|
when appropriate) before enabling this plugin version. An older gem's unknown
|
|
648
811
|
task error leaves queued events in place; installing the plugin does not upgrade
|
|
@@ -740,8 +903,10 @@ per-process language-server index does not.
|
|
|
740
903
|
|
|
741
904
|
N resident daemons is N booted apps, and most slots are dormant most of the
|
|
742
905
|
time. `idle_timeout` (off by default) stops a daemon after that many seconds
|
|
743
|
-
without a file event
|
|
744
|
-
|
|
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.
|
|
745
910
|
|
|
746
911
|
```ruby
|
|
747
912
|
Woods::Watch::Daemon.new(output_dir: …, idle_timeout: 900).run # 15 minutes
|
|
@@ -803,8 +968,8 @@ supplies batches to it.
|
|
|
803
968
|
|
|
804
969
|
### Optional bounded context hints
|
|
805
970
|
|
|
806
|
-
Context hints are a separate Claude Code opt-in, **
|
|
807
|
-
|
|
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
|
|
808
973
|
application bundle before enabling `WOODS_HOOK_CONTEXT_ENABLED=1`. The plugin
|
|
809
974
|
version alone does not establish gem support. `WOODS_HOOKS_DISABLED=1` disables
|
|
810
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-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
|
|
data/exe/woods-watch
ADDED
|
@@ -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'
|
|
@@ -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)'
|
|
@@ -50,8 +50,10 @@ module Woods
|
|
|
50
50
|
"#{@layout.receipt_path}.pending"
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
-
def with_lock
|
|
54
|
-
|
|
53
|
+
def with_lock(paths = @layout.lock_paths, &operation)
|
|
54
|
+
return operation.call if paths.empty?
|
|
55
|
+
|
|
56
|
+
path, *remaining = paths
|
|
55
57
|
Document.validate_path!(path)
|
|
56
58
|
FileUtils.mkdir_p(File.dirname(path), mode: 0o700)
|
|
57
59
|
File.open(path, File::RDWR | File::CREAT | File::NOFOLLOW | File::NONBLOCK, 0o600) do |lock|
|
|
@@ -61,7 +63,7 @@ module Woods
|
|
|
61
63
|
'Another Woods configuration operation is active; retry after it finishes'
|
|
62
64
|
end
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
with_lock(remaining, &operation)
|
|
65
67
|
ensure
|
|
66
68
|
lock&.flock(File::LOCK_UN)
|
|
67
69
|
end
|
|
@@ -78,12 +78,12 @@ module Woods
|
|
|
78
78
|
write_plan(path, plan, layout)
|
|
79
79
|
PlanDiff.show(plan, @stderr) if options[:diff]
|
|
80
80
|
plan.summary.merge('plan_file' => File.expand_path(path),
|
|
81
|
-
'runtime_files' =>
|
|
81
|
+
'runtime_files' => layout.runtime_paths)
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
def write_plan(path, plan, layout)
|
|
85
85
|
target = File.expand_path(path)
|
|
86
|
-
protected_paths = layout.allowed_paths +
|
|
86
|
+
protected_paths = layout.allowed_paths + layout.runtime_paths
|
|
87
87
|
raise Conflict, 'Plan output must differ from every managed/runtime target' if protected_paths.include?(target)
|
|
88
88
|
|
|
89
89
|
Document.validate_path!(target)
|
|
@@ -50,6 +50,19 @@ module Woods
|
|
|
50
50
|
end
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
# Lock actual managed targets, since user-scoped paths can be shared
|
|
54
|
+
# by different applications. Keep the receipt lock name for existing
|
|
55
|
+
# same-application callers; receipts and recovery journals stay separate.
|
|
56
|
+
def lock_paths
|
|
57
|
+
allowed_paths.map do |path|
|
|
58
|
+
path == receipt_path ? "#{path}.lock" : "#{path}.woods.lock"
|
|
59
|
+
end.sort
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def runtime_paths
|
|
63
|
+
lock_paths + ["#{receipt_path}.pending"]
|
|
64
|
+
end
|
|
65
|
+
|
|
53
66
|
def identity
|
|
54
67
|
{ 'client' => 'claude', 'scope' => scope, 'root' => root, 'config_dir' => config_dir,
|
|
55
68
|
'config_path' => config_path, 'receipt_path' => receipt_path }
|