woods 1.6.1 → 2.0.0.beta2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2035 -0
- data/CONTRIBUTING.md +253 -87
- data/README.md +161 -513
- data/SECURITY.md +92 -0
- data/assets/woods-wordmark-white-with-bg.png +0 -0
- data/docs/AGENT_GUIDE.md +204 -0
- data/docs/AGENT_SETUP.md +205 -0
- data/docs/BACKEND_MATRIX.md +470 -0
- data/docs/CONFIGURATION_REFERENCE.md +655 -0
- data/docs/CONSOLE_MCP_SETUP.md +829 -0
- data/docs/DOCKER_SETUP.md +454 -0
- data/docs/EMBEDDING_MODELS.md +136 -0
- data/docs/EVALUATION.md +91 -0
- data/docs/EXTRACTOR_REFERENCE.md +765 -0
- data/docs/FAQ.md +544 -0
- data/docs/GETTING_STARTED.md +183 -0
- data/docs/INCREMENTAL_EXTRACTION.md +455 -0
- data/docs/INTERNALS.md +418 -0
- data/docs/MCP_HTTP_TRANSPORT.md +144 -0
- data/docs/MCP_SERVERS.md +231 -0
- data/docs/MCP_TOOL_COOKBOOK.md +987 -0
- data/docs/MCP_WORKTREE_SETUP.md +127 -0
- data/docs/NOTION_INTEGRATION.md +283 -0
- data/docs/OBSIDIAN_INTEGRATION.md +170 -0
- data/docs/PUBLISHED_INDEX.md +213 -0
- data/docs/README.md +94 -0
- data/docs/RETRIEVAL_GUIDE.md +267 -0
- data/docs/TOKEN_BENCHMARK.md +68 -0
- data/docs/TROUBLESHOOTING.md +841 -0
- data/docs/UNBLOCKED_INTEGRATION.md +279 -0
- data/docs/UPGRADING_TO_2.md +321 -0
- data/docs/WATCH_DAEMON.md +667 -0
- data/docs/WHY_WOODS.md +219 -0
- data/exe/woods-console +40 -4
- data/exe/woods-console-mcp +21 -35
- data/exe/woods-mcp +20 -7
- data/exe/woods-mcp-http +80 -11
- data/exe/woods-mcp-start +57 -52
- data/lib/generators/woods/install_generator.rb +6 -5
- data/lib/generators/woods/pgvector_generator.rb +6 -3
- data/lib/generators/woods/templates/add_pgvector_to_woods.rb.erb +29 -9
- data/lib/generators/woods/templates/create_woods_tables.rb.erb +5 -1
- data/lib/generators/woods/templates/woods.rb.tt +49 -28
- data/lib/tasks/woods.rake +622 -168
- data/lib/tasks/woods_checks.rake +107 -0
- data/lib/tasks/woods_evaluation.rake +164 -80
- data/lib/woods/ast/call_site_extractor.rb +6 -15
- data/lib/woods/ast/method_extractor.rb +19 -9
- data/lib/woods/ast/parser.rb +54 -8
- data/lib/woods/atomic_file.rb +171 -2
- data/lib/woods/builder.rb +310 -22
- data/lib/woods/cache/cache_middleware.rb +7 -2
- data/lib/woods/cache/cache_store.rb +9 -1
- data/lib/woods/cache/solid_cache_store.rb +6 -4
- data/lib/woods/change_set.rb +88 -0
- data/lib/woods/checks/generation_resolution.rb +34 -0
- data/lib/woods/checks/moved_messages.rb +186 -0
- data/lib/woods/chunking/semantic_chunker.rb +160 -18
- data/lib/woods/console/audit_logger.rb +12 -3
- data/lib/woods/console/bridge_protocol.rb +3 -16
- data/lib/woods/console/connection_manager.rb +51 -136
- data/lib/woods/console/dispatch_pipeline.rb +42 -12
- data/lib/woods/console/embedded_executor.rb +806 -149
- data/lib/woods/console/eval_guard.rb +27 -20
- data/lib/woods/console/input_contract.rb +78 -0
- data/lib/woods/console/model_validator.rb +29 -1
- data/lib/woods/console/rack_middleware.rb +65 -42
- data/lib/woods/console/redactor.rb +26 -8
- data/lib/woods/console/safe_context.rb +58 -10
- data/lib/woods/console/scope_predicate_parser.rb +41 -0
- data/lib/woods/console/server.rb +119 -247
- data/lib/woods/console/sql_noise_stripper.rb +125 -16
- data/lib/woods/console/sql_table_scanner.rb +82 -22
- data/lib/woods/console/sql_validator.rb +459 -29
- data/lib/woods/console/table_gate.rb +2 -2
- data/lib/woods/console/tool_specs.rb +463 -90
- data/lib/woods/console/tools/tier1.rb +1 -5
- data/lib/woods/console/tools/tier4.rb +18 -9
- data/lib/woods/coordination/lock_heartbeat.rb +103 -0
- data/lib/woods/coordination/pipeline_lock.rb +263 -53
- data/lib/woods/db/migrations/007_typed_snapshot_units.rb +45 -0
- data/lib/woods/db/migrator.rb +3 -9
- data/lib/woods/db/schema_version.rb +47 -2
- data/lib/woods/dependency_graph.rb +898 -64
- data/lib/woods/embedding/fake.rb +138 -0
- data/lib/woods/embedding/indexer.rb +832 -40
- data/lib/woods/embedding/openai.rb +77 -19
- data/lib/woods/embedding/provider.rb +189 -11
- data/lib/woods/embedding/text_preparer.rb +1 -1
- data/lib/woods/embedding/token_counter.rb +0 -7
- data/lib/woods/evaluation/ablation_agent_payload.rb +38 -0
- data/lib/woods/evaluation/ablation_executor.rb +67 -0
- data/lib/woods/evaluation/ablation_provenance.rb +38 -0
- data/lib/woods/evaluation/ablation_report_writer.rb +43 -0
- data/lib/woods/evaluation/ablation_runner.rb +173 -0
- data/lib/woods/evaluation/ablation_summary.rb +65 -0
- data/lib/woods/evaluation/ablation_task.rb +66 -0
- data/lib/woods/evaluation/ablation_task_set.rb +77 -0
- data/lib/woods/evaluation/ablation_timed_executor.rb +91 -0
- data/lib/woods/evaluation/ablation_worktree.rb +71 -0
- data/lib/woods/evaluation/baseline.rb +60 -0
- data/lib/woods/evaluation/baseline_runner.rb +11 -3
- data/lib/woods/evaluation/evaluator.rb +41 -8
- data/lib/woods/evaluation/query_set.rb +79 -13
- data/lib/woods/evaluation/report_generator.rb +20 -1
- data/lib/woods/export/unit_facts.rb +0 -11
- data/lib/woods/extracted_unit.rb +22 -63
- data/lib/woods/extractor.rb +2783 -238
- data/lib/woods/extractors/action_cable_extractor.rb +9 -4
- data/lib/woods/extractors/ast_source_extraction.rb +20 -2
- data/lib/woods/extractors/caching_extractor.rb +46 -12
- data/lib/woods/extractors/callback_analyzer.rb +39 -9
- data/lib/woods/extractors/component_discovery.rb +123 -0
- data/lib/woods/extractors/concern_extractor.rb +17 -3
- data/lib/woods/extractors/controller_extractor.rb +389 -29
- data/lib/woods/extractors/decorator_extractor.rb +7 -14
- data/lib/woods/extractors/engine_extractor.rb +53 -8
- data/lib/woods/extractors/event_extractor.rb +55 -4
- data/lib/woods/extractors/factory_extractor.rb +49 -11
- data/lib/woods/extractors/graphql_extractor.rb +162 -66
- data/lib/woods/extractors/i18n_extractor.rb +6 -1
- data/lib/woods/extractors/job_extractor.rb +51 -21
- data/lib/woods/extractors/lib_extractor.rb +23 -17
- data/lib/woods/extractors/line_neutralizer.rb +171 -0
- data/lib/woods/extractors/mailer_extractor.rb +9 -1
- data/lib/woods/extractors/manager_extractor.rb +19 -2
- data/lib/woods/extractors/migration_extractor.rb +22 -11
- data/lib/woods/extractors/model_extractor.rb +292 -57
- data/lib/woods/extractors/package_extractor.rb +154 -0
- data/lib/woods/extractors/phlex_extractor.rb +18 -3
- data/lib/woods/extractors/policy_extractor.rb +6 -5
- data/lib/woods/extractors/poro_extractor.rb +13 -14
- data/lib/woods/extractors/pundit_extractor.rb +3 -3
- data/lib/woods/extractors/rails_source_extractor.rb +24 -7
- data/lib/woods/extractors/rake_task_extractor.rb +158 -30
- data/lib/woods/extractors/reference_patterns.rb +38 -0
- data/lib/woods/extractors/route_extractor.rb +58 -2
- data/lib/woods/extractors/scheduled_job_extractor.rb +51 -35
- data/lib/woods/extractors/serializer_extractor.rb +3 -4
- data/lib/woods/extractors/service_extractor.rb +11 -1
- data/lib/woods/extractors/shared_dependency_scanner.rb +24 -34
- data/lib/woods/extractors/shared_utility_methods.rb +36 -6
- data/lib/woods/extractors/source_nesting.rb +560 -0
- data/lib/woods/extractors/state_machine_extractor.rb +30 -18
- data/lib/woods/extractors/test_mapping_extractor.rb +26 -9
- data/lib/woods/extractors/view_component_extractor.rb +28 -3
- data/lib/woods/extractors/view_engines/erb.rb +17 -3
- data/lib/woods/feedback/gap_detector.rb +9 -3
- data/lib/woods/feedback/store.rb +7 -1
- data/lib/woods/filename_utils.rb +29 -1
- data/lib/woods/flow_analysis/operation_extractor.rb +22 -10
- data/lib/woods/flow_assembler.rb +147 -26
- data/lib/woods/flow_document.rb +1 -0
- data/lib/woods/flow_precomputer.rb +175 -22
- data/lib/woods/gem_mapper.rb +285 -0
- data/lib/woods/generation.rb +185 -0
- data/lib/woods/git_command.rb +38 -0
- data/lib/woods/git_provenance.rb +16 -2
- data/lib/woods/graph_analyzer.rb +564 -87
- data/lib/woods/index_artifact.rb +93 -23
- data/lib/woods/mcp/bearer_auth.rb +102 -13
- data/lib/woods/mcp/bootstrap_state.rb +77 -0
- data/lib/woods/mcp/bootstrapper.rb +582 -77
- data/lib/woods/mcp/config_resolver.rb +66 -6
- data/lib/woods/mcp/errors.rb +60 -0
- data/lib/woods/mcp/index_reader.rb +836 -117
- data/lib/woods/mcp/index_reader_pinning.rb +78 -0
- data/lib/woods/mcp/origin_guard.rb +66 -7
- data/lib/woods/mcp/protocol_policy.rb +98 -0
- data/lib/woods/mcp/provider_probe.rb +45 -6
- data/lib/woods/mcp/renderers/markdown_renderer.rb +72 -4
- data/lib/woods/mcp/renderers/plain_renderer.rb +54 -6
- data/lib/woods/mcp/server.rb +898 -152
- data/lib/woods/mcp/tasks/extension.rb +196 -0
- data/lib/woods/mcp/tasks/request_capture.rb +45 -0
- data/lib/woods/mcp/tasks/store.rb +518 -0
- data/lib/woods/mcp/tool_contract.rb +171 -0
- data/lib/woods/mcp/tool_response_renderer.rb +7 -0
- data/lib/woods/model_name_cache.rb +19 -1
- data/lib/woods/notion/client.rb +132 -36
- data/lib/woods/notion/exporter.rb +456 -61
- data/lib/woods/notion/mappers/column_mapper.rb +34 -5
- data/lib/woods/notion/mappers/migration_mapper.rb +32 -8
- data/lib/woods/notion/mappers/model_mapper.rb +21 -6
- data/lib/woods/notion/mappers/shared.rb +45 -3
- data/lib/woods/notion/sync_manifest.rb +258 -0
- data/lib/woods/obsidian/errors.rb +6 -0
- data/lib/woods/obsidian/name_mapper.rb +40 -24
- data/lib/woods/obsidian/vault_exporter.rb +103 -36
- data/lib/woods/operator/pipeline_guard.rb +118 -21
- data/lib/woods/operator/status_reporter.rb +20 -3
- data/lib/woods/path_dispatcher.rb +276 -0
- data/lib/woods/payload_store.rb +236 -0
- data/lib/woods/published_index/edge_shaper.rb +61 -0
- data/lib/woods/published_index/generation_catalog.rb +72 -0
- data/lib/woods/published_index/typed_unit_reader.rb +48 -0
- data/lib/woods/published_index.rb +287 -0
- data/lib/woods/railtie.rb +69 -30
- data/lib/woods/railtie_support.rb +167 -0
- data/lib/woods/release.rb +12 -0
- data/lib/woods/reload_policy.rb +206 -0
- data/lib/woods/resilience/circuit_breaker.rb +47 -8
- data/lib/woods/resilience/index_validator.rb +296 -10
- data/lib/woods/resilience/retryable_provider.rb +71 -6
- data/lib/woods/resolved_config.rb +55 -11
- data/lib/woods/retrieval/context_assembler.rb +132 -40
- data/lib/woods/retrieval/query_classifier.rb +26 -8
- data/lib/woods/retrieval/ranker.rb +193 -28
- data/lib/woods/retrieval/search_executor.rb +206 -39
- data/lib/woods/retriever.rb +317 -71
- data/lib/woods/retry_after.rb +22 -2
- data/lib/woods/ruby_analyzer/class_analyzer.rb +10 -14
- data/lib/woods/ruby_analyzer/fqn_builder.rb +2 -0
- data/lib/woods/ruby_analyzer/mermaid_renderer.rb +14 -4
- data/lib/woods/ruby_analyzer/method_analyzer.rb +1 -1
- data/lib/woods/ruby_analyzer/trace_enricher.rb +3 -0
- data/lib/woods/ruby_analyzer.rb +21 -5
- data/lib/woods/session_tracer/file_store.rb +138 -19
- data/lib/woods/session_tracer/middleware.rb +1 -2
- data/lib/woods/session_tracer/redis_store.rb +122 -12
- data/lib/woods/session_tracer/session_flow_assembler.rb +57 -17
- data/lib/woods/session_tracer/session_flow_document.rb +56 -14
- data/lib/woods/session_tracer/solid_cache_coordination.rb +192 -0
- data/lib/woods/session_tracer/solid_cache_store.rb +560 -91
- data/lib/woods/session_tracer/store.rb +14 -1
- data/lib/woods/storage/metadata_store.rb +230 -26
- data/lib/woods/storage/pgvector.rb +180 -22
- data/lib/woods/storage/qdrant.rb +367 -41
- data/lib/woods/storage/snapshotter/metadata.rb +79 -16
- data/lib/woods/storage/snapshotter/vector.rb +128 -17
- data/lib/woods/storage/snapshotter.rb +23 -5
- data/lib/woods/storage/vector_store.rb +49 -8
- data/lib/woods/storage_identity.rb +28 -0
- data/lib/woods/tasks.rb +53 -2
- data/lib/woods/temporal/json_snapshot_store.rb +112 -42
- data/lib/woods/temporal/snapshot_store.rb +139 -42
- data/lib/woods/unblocked/client.rb +119 -17
- data/lib/woods/unblocked/document_builder.rb +34 -2
- data/lib/woods/unblocked/exporter.rb +63 -27
- data/lib/woods/unblocked/rate_limiter.rb +23 -9
- data/lib/woods/unblocked/sync_manifest.rb +16 -8
- data/lib/woods/update_check.rb +24 -1
- data/lib/woods/util/uuid5.rb +124 -0
- data/lib/woods/version.rb +1 -1
- data/lib/woods/watch/daemon.rb +1345 -0
- data/lib/woods/watch/listen_watcher.rb +81 -0
- data/lib/woods/watch/polling_watcher.rb +137 -0
- data/lib/woods/watch/status.rb +169 -0
- data/lib/woods/watch/tree_scan.rb +163 -0
- data/lib/woods/watch/watcher.rb +100 -0
- data/lib/woods.rb +138 -9
- data/plugin/.claude-plugin/plugin.json +18 -0
- data/plugin/hooks/hooks.json +29 -0
- data/plugin/hooks/woods-post-edit.sh +226 -0
- data/plugin/hooks/woods-session-start.sh +77 -0
- data/plugin/skills/woods-agent-enable/SKILL.md +51 -0
- data/plugin/skills/woods-diagnose/SKILL.md +75 -0
- data/plugin/skills/woods-investigate/SKILL.md +39 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +101 -0
- data/plugin/skills/woods-setup/SKILL.md +99 -0
- metadata +134 -23
- data/lib/woods/console/adapters/cache_adapter.rb +0 -58
- data/lib/woods/console/adapters/good_job_adapter.rb +0 -33
- data/lib/woods/console/adapters/job_adapter.rb +0 -74
- data/lib/woods/console/adapters/sidekiq_adapter.rb +0 -33
- data/lib/woods/console/adapters/solid_queue_adapter.rb +0 -33
- data/lib/woods/console/bridge.rb +0 -210
- data/lib/woods/formatting/claude_adapter.rb +0 -98
- data/lib/woods/formatting/generic_adapter.rb +0 -56
- data/lib/woods/formatting/gpt_adapter.rb +0 -64
- data/lib/woods/notion/mapper.rb +0 -40
- data/lib/woods/observability/health_check.rb +0 -79
- data/lib/woods/observability/instrumentation.rb +0 -34
data/lib/woods.rb
CHANGED
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
# end
|
|
23
23
|
#
|
|
24
24
|
require_relative 'woods/version'
|
|
25
|
+
# Configuration's `component_paths` default lives with the discovery it feeds.
|
|
26
|
+
require_relative 'woods/extractors/component_discovery'
|
|
27
|
+
# Configuration defaults the cycle caps to the analyzer's own constants.
|
|
28
|
+
require_relative 'woods/graph_analyzer'
|
|
25
29
|
|
|
26
30
|
module Woods
|
|
27
31
|
class Error < StandardError; end
|
|
@@ -120,8 +124,13 @@ module Woods
|
|
|
120
124
|
# ════════════════════════════════════════════════════════════════════════
|
|
121
125
|
|
|
122
126
|
class Configuration # rubocop:disable Metrics/ClassLength
|
|
123
|
-
|
|
124
|
-
|
|
127
|
+
# The set every full extraction always runs, regardless of `extractors=`.
|
|
128
|
+
# See the {#extractors=} YARD note: this knob does not gate extraction.
|
|
129
|
+
DEFAULT_EXTRACTORS = %i[models controllers services components view_components jobs mailers graphql
|
|
130
|
+
serializers managers policies validators rails_source].freeze
|
|
131
|
+
|
|
132
|
+
attr_accessor :include_framework_sources, :gem_configs,
|
|
133
|
+
:vector_store, :metadata_store, :graph_store, :embedding_provider,
|
|
125
134
|
:vector_store_options, :metadata_store_options, :embedding_options,
|
|
126
135
|
:concurrent_extraction, :precompute_flows, :extract_navigation_edges, :enable_snapshots,
|
|
127
136
|
:session_tracer_enabled, :session_tracer_allow_production,
|
|
@@ -137,19 +146,21 @@ module Woods
|
|
|
137
146
|
:notion_api_token, :notion_database_ids,
|
|
138
147
|
:unblocked_api_token, :unblocked_collection_id, :unblocked_repo_url,
|
|
139
148
|
:cache_store, :cache_options,
|
|
140
|
-
:dump_retention_count
|
|
141
|
-
attr_reader :max_context_tokens, :similarity_threshold, :extractors, :pretty_json,
|
|
142
|
-
:cache_enabled
|
|
149
|
+
:dump_retention_count, :component_paths
|
|
150
|
+
attr_reader :embedding_model, :max_context_tokens, :similarity_threshold, :extractors, :pretty_json,
|
|
151
|
+
:context_format, :cache_enabled, :volatile_dependency_ratio,
|
|
152
|
+
:graph_cycle_limit, :graph_cycle_max_length,
|
|
153
|
+
:incremental_blast_radius_depth, :durable_payload_writes
|
|
143
154
|
|
|
144
155
|
def initialize # rubocop:disable Metrics/MethodLength
|
|
145
156
|
@output_dir = nil # Resolved lazily; Rails.root is nil at require time
|
|
146
157
|
@embedding_model = 'text-embedding-3-small'
|
|
158
|
+
@embedding_model_explicit = false
|
|
147
159
|
@max_context_tokens = 8000
|
|
148
160
|
@similarity_threshold = 0.7
|
|
149
161
|
@include_framework_sources = true
|
|
150
162
|
@gem_configs = {}
|
|
151
|
-
@extractors =
|
|
152
|
-
managers policies validators rails_source]
|
|
163
|
+
@extractors = DEFAULT_EXTRACTORS.dup
|
|
153
164
|
@pretty_json = true
|
|
154
165
|
@concurrent_extraction = false
|
|
155
166
|
@precompute_flows = false
|
|
@@ -197,6 +208,28 @@ module Woods
|
|
|
197
208
|
@cache_store = nil # :redis, :solid_cache, :memory, or a CacheStore instance
|
|
198
209
|
@cache_options = {} # { redis: client, cache: store, ttl: { embeddings: 86400, ... } }
|
|
199
210
|
@dump_retention_count = 3
|
|
211
|
+
@volatile_dependency_ratio = 3.0
|
|
212
|
+
@graph_cycle_limit = GraphAnalyzer::DEFAULT_CYCLE_LIMIT
|
|
213
|
+
@graph_cycle_max_length = GraphAnalyzer::DEFAULT_CYCLE_MAX_LENGTH
|
|
214
|
+
# nil = the unbounded transitive closure. See the setter for why the
|
|
215
|
+
# default is not a small number.
|
|
216
|
+
@incremental_blast_radius_depth = nil
|
|
217
|
+
# false = payload files are written without a per-file fsync and the
|
|
218
|
+
# whole payload is flushed once before `generation.json` is written.
|
|
219
|
+
# See the setter.
|
|
220
|
+
@durable_payload_writes = false
|
|
221
|
+
# Directories the component extractors walk before reading `descendants`,
|
|
222
|
+
# relative to Rails.root. See Extractors::ComponentDiscovery.
|
|
223
|
+
@component_paths = Extractors::ComponentDiscovery::DEFAULT_COMPONENT_PATHS.dup
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def embedding_model=(value)
|
|
227
|
+
@embedding_model = value
|
|
228
|
+
@embedding_model_explicit = true
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def embedding_model_explicit?
|
|
232
|
+
@embedding_model_explicit
|
|
200
233
|
end
|
|
201
234
|
|
|
202
235
|
# @return [Pathname, String] Output directory, defaulting to Rails.root/tmp/woods
|
|
@@ -235,6 +268,10 @@ module Woods
|
|
|
235
268
|
@similarity_threshold = float_val
|
|
236
269
|
end
|
|
237
270
|
|
|
271
|
+
# Accepted for forward compatibility. Extraction always runs all 34
|
|
272
|
+
# extractors; this array is not consulted anywhere in the extraction
|
|
273
|
+
# path. Setting it to anything other than {DEFAULT_EXTRACTORS} warns.
|
|
274
|
+
#
|
|
238
275
|
# @param value [Array<Symbol>] List of extractor names
|
|
239
276
|
# @raise [ConfigurationError] if value is not an Array of Symbols
|
|
240
277
|
def extractors=(value)
|
|
@@ -242,9 +279,37 @@ module Woods
|
|
|
242
279
|
raise ConfigurationError, "extractors must be an Array of Symbols, got #{value.inspect}"
|
|
243
280
|
end
|
|
244
281
|
|
|
282
|
+
if value.sort_by(&:to_s) != DEFAULT_EXTRACTORS.sort_by(&:to_s)
|
|
283
|
+
warn 'config.extractors is accepted for forward compatibility but extractor selection is not ' \
|
|
284
|
+
'implemented; all extractors run.'
|
|
285
|
+
end
|
|
286
|
+
|
|
245
287
|
@extractors = value
|
|
246
288
|
end
|
|
247
289
|
|
|
290
|
+
# Force an fsync on every payload file as it is written, on top of the
|
|
291
|
+
# single flush the publish already performs.
|
|
292
|
+
#
|
|
293
|
+
# Off by default, and off is not a weaker guarantee. Readers resolve only
|
|
294
|
+
# through `generation.json`; a payload file has no reader until that
|
|
295
|
+
# pointer names it, and {Woods::Extractor#sync_payload} makes the whole
|
|
296
|
+
# payload durable before the pointer is written. Turning this on buys
|
|
297
|
+
# exactly one thing: an individual payload file being durable before the
|
|
298
|
+
# pointer exists.
|
|
299
|
+
#
|
|
300
|
+
# It costs a lot for it. Two forced flushes per file, 8.9ms each on btrfs:
|
|
301
|
+
# 8000 units is 71s of writing against 1s.
|
|
302
|
+
#
|
|
303
|
+
# Turning it on cannot disable the publish flush. That flush *is* the
|
|
304
|
+
# durability contract, so it has no opt-out.
|
|
305
|
+
#
|
|
306
|
+
# @param value [Boolean]
|
|
307
|
+
# @raise [Woods::ConfigurationError] when value is not a boolean
|
|
308
|
+
def durable_payload_writes=(value)
|
|
309
|
+
validate_boolean!(:durable_payload_writes, value)
|
|
310
|
+
@durable_payload_writes = value
|
|
311
|
+
end
|
|
312
|
+
|
|
248
313
|
# @param value [Boolean] Must be true or false
|
|
249
314
|
# @raise [ConfigurationError] if value is not a boolean
|
|
250
315
|
def pretty_json=(value)
|
|
@@ -270,17 +335,81 @@ module Woods
|
|
|
270
335
|
@cache_enabled = value
|
|
271
336
|
end
|
|
272
337
|
|
|
273
|
-
#
|
|
338
|
+
# Commit-count ratio above which {Woods::GraphAnalyzer#volatile_dependencies}
|
|
339
|
+
# reports an edge. Stored as a Float.
|
|
340
|
+
#
|
|
341
|
+
# @param value [Numeric] must be greater than 1
|
|
342
|
+
# @raise [ConfigurationError] otherwise
|
|
343
|
+
def volatile_dependency_ratio=(value)
|
|
344
|
+
unless value.is_a?(Numeric) && value > 1
|
|
345
|
+
raise ConfigurationError, "volatile_dependency_ratio must be a number greater than 1, got #{value.inspect}"
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
@volatile_dependency_ratio = value.to_f
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# How many distinct cycles {Woods::GraphAnalyzer#cycles} enumerates before
|
|
352
|
+
# it stops. `nil` removes the cap and restores exhaustive enumeration.
|
|
353
|
+
#
|
|
354
|
+
# @param value [Integer, nil] must be a positive Integer, or nil
|
|
355
|
+
# @raise [ConfigurationError] otherwise
|
|
356
|
+
def graph_cycle_limit=(value)
|
|
357
|
+
@graph_cycle_limit = validate_optional_positive_integer!(:graph_cycle_limit, value)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# The longest cycle {Woods::GraphAnalyzer#cycles} records, in distinct
|
|
361
|
+
# nodes. `nil` removes the cap.
|
|
362
|
+
#
|
|
363
|
+
# @param value [Integer, nil] must be a positive Integer, or nil
|
|
364
|
+
# @raise [ConfigurationError] otherwise
|
|
365
|
+
def graph_cycle_max_length=(value)
|
|
366
|
+
@graph_cycle_max_length = validate_optional_positive_integer!(:graph_cycle_max_length, value)
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# How many reverse hops an incremental run walks from a changed file
|
|
370
|
+
# before it stops re-extracting dependents. `nil` (the default) keeps the
|
|
371
|
+
# unbounded transitive closure.
|
|
372
|
+
#
|
|
373
|
+
# A unit's extracted content is mostly a function of its own source and
|
|
374
|
+
# its own reflection, so on most graphs a small cap re-extracts the same
|
|
375
|
+
# bytes far faster. It is not safe everywhere, which is why the default
|
|
376
|
+
# is unbounded: an STI grandchild reads its grandparent's reflection
|
|
377
|
+
# (`reflect_on_all_associations`, `_validators` and the callback chain
|
|
378
|
+
# are all inherited), so `SportsCar < Car < Vehicle` has content that
|
|
379
|
+
# changes when `Vehicle` changes while sitting two hops away in the
|
|
380
|
+
# graph. A nested `has_many :through` resolves through the same kind of
|
|
381
|
+
# chain. Set this only on a tree you know has neither.
|
|
382
|
+
#
|
|
383
|
+
# @param value [Integer, nil] must be a positive Integer, or nil
|
|
384
|
+
# @raise [ConfigurationError] otherwise
|
|
385
|
+
def incremental_blast_radius_depth=(value)
|
|
386
|
+
@incremental_blast_radius_depth = validate_optional_positive_integer!(
|
|
387
|
+
:incremental_blast_radius_depth, value
|
|
388
|
+
)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Accepted for forward compatibility. Nothing reads {gem_configs}; gem
|
|
392
|
+
# source indexing is not implemented.
|
|
274
393
|
#
|
|
275
394
|
# @param gem_name [String] Name of the gem
|
|
276
395
|
# @param paths [Array<String>] Relative paths within the gem to index
|
|
277
396
|
# @param priority [Symbol] :high, :medium, or :low
|
|
278
397
|
def add_gem(gem_name, paths:, priority: :medium)
|
|
398
|
+
warn 'config.add_gem is accepted for forward compatibility but gem configs are not used.'
|
|
279
399
|
@gem_configs[gem_name] = { paths: paths, priority: priority }
|
|
280
400
|
end
|
|
281
401
|
|
|
282
402
|
private
|
|
283
403
|
|
|
404
|
+
# @return [Integer, nil] +value+ when it is nil or a positive Integer
|
|
405
|
+
# @raise [ConfigurationError] otherwise
|
|
406
|
+
def validate_optional_positive_integer!(name, value)
|
|
407
|
+
return value if value.nil?
|
|
408
|
+
return value if value.is_a?(Integer) && value.positive?
|
|
409
|
+
|
|
410
|
+
raise ConfigurationError, "#{name} must be a positive Integer or nil, got #{value.inspect}"
|
|
411
|
+
end
|
|
412
|
+
|
|
284
413
|
def validate_boolean!(name, value)
|
|
285
414
|
return if value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
286
415
|
|
|
@@ -305,7 +434,7 @@ module Woods
|
|
|
305
434
|
|
|
306
435
|
# Configure the module using a named preset and optional block customization.
|
|
307
436
|
#
|
|
308
|
-
# Valid preset names: :local, :postgresql, :production
|
|
437
|
+
# Valid preset names: :local, :shared_filesystem, :postgresql, :production
|
|
309
438
|
#
|
|
310
439
|
# @param name [Symbol] Preset name
|
|
311
440
|
# @yield [config] Optional block for further customization after preset is applied
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "woods-plugin",
|
|
3
|
+
"description": "Woods user guides for Claude Code: set up, upgrade, configure MCP servers for, investigate codebases with, enable a repository's agents on, and diagnose the Woods Rails code-intelligence gem.",
|
|
4
|
+
"version": "2.3.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "lost-in-the"
|
|
7
|
+
},
|
|
8
|
+
"homepage": "https://github.com/lost-in-the/woods",
|
|
9
|
+
"repository": "https://github.com/lost-in-the/woods",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"rails",
|
|
13
|
+
"woods",
|
|
14
|
+
"mcp",
|
|
15
|
+
"code-intelligence",
|
|
16
|
+
"agent"
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PostToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Edit|Write|MultiEdit",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/woods-post-edit.sh\"",
|
|
10
|
+
"async": true,
|
|
11
|
+
"timeout": 600
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"SessionStart": [
|
|
17
|
+
{
|
|
18
|
+
"matcher": "startup|resume",
|
|
19
|
+
"hooks": [
|
|
20
|
+
{
|
|
21
|
+
"type": "command",
|
|
22
|
+
"command": "\"${CLAUDE_PLUGIN_ROOT}/hooks/woods-session-start.sh\"",
|
|
23
|
+
"timeout": 20
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Woods PostToolUse hook (#280): after an edit to a graph-changing path,
|
|
3
|
+
# refresh the index with `woods:incremental` in the background.
|
|
4
|
+
#
|
|
5
|
+
# Reads `cwd` from the hook payload, not CLAUDE_PROJECT_DIR, so a session in
|
|
6
|
+
# a linked worktree refreshes that worktree's own index. Runs only when an
|
|
7
|
+
# index already exists there. `woods:incremental` stands down under a
|
|
8
|
+
# running watch daemon and takes the extraction lock itself; the lock here
|
|
9
|
+
# only stops this hook from queueing one rake process per keystroke.
|
|
10
|
+
#
|
|
11
|
+
# Opt-in: this hook is shipped disabled. It does nothing until
|
|
12
|
+
# WOODS_HOOKS_ENABLED=1 is set (see docs/WATCH_DAEMON.md and the woods-setup
|
|
13
|
+
# skill for where to set it). WOODS_HOOKS_DISABLED=1 turns it back off even
|
|
14
|
+
# after it has been enabled, without touching the enable setting.
|
|
15
|
+
#
|
|
16
|
+
# Lock contention must not drop an edit. A hook invocation that finds the
|
|
17
|
+
# run lock busy appends its path to hook-pending.txt and returns immediately
|
|
18
|
+
# instead of waiting; whichever invocation is holding the run lock drains
|
|
19
|
+
# that file in a loop, passing every path collected on each drain as one
|
|
20
|
+
# CHANGED_FILES batch, until a drain comes back empty. The one narrow gap
|
|
21
|
+
# this leaves: an append that lands after the holder's last (empty) drain
|
|
22
|
+
# but before it releases the run lock sits in the file, unprocessed, until
|
|
23
|
+
# the next graph-changing edit triggers a hook that both appends it and
|
|
24
|
+
# then wins the now-free run lock itself. That edit is delayed, not lost,
|
|
25
|
+
# and the SessionStart hook's staleness warning is the backstop for it.
|
|
26
|
+
#
|
|
27
|
+
# The mkdir-based lock fallback has no kernel-enforced release: a `flock`
|
|
28
|
+
# held by a killed process is freed automatically, but a lock *directory*
|
|
29
|
+
# a killed process made is not. A hook killed mid-drain (OOM, a `kill -9`,
|
|
30
|
+
# the host restarting) leaves `hook.lock.d` or `hook-pending.lock.d` behind
|
|
31
|
+
# forever, so every later hook either skips draining permanently (the run
|
|
32
|
+
# lock) or spins until the 600s hook timeout on every single invocation
|
|
33
|
+
# (the pending lock, which blocks). Both lock directories are therefore
|
|
34
|
+
# reclaimed once their mtime is older than WOODS_HOOK_LOCK_STALE_SECONDS: a
|
|
35
|
+
# fresh lock directory is still respected as busy.
|
|
36
|
+
#
|
|
37
|
+
# Knobs:
|
|
38
|
+
# WOODS_HOOK_RAKE command prefix, default "bundle exec rake"
|
|
39
|
+
# (Docker: "docker compose exec -T app bundle exec rake")
|
|
40
|
+
# WOODS_OUTPUT index directory override, same variable
|
|
41
|
+
# woods:incremental/woods:watch_status already read;
|
|
42
|
+
# default tmp/woods under the payload's cwd
|
|
43
|
+
# WOODS_HOOKS_ENABLED set to 1 to turn the hook on
|
|
44
|
+
# WOODS_HOOKS_DISABLED set to 1 to turn it back off
|
|
45
|
+
# WOODS_HOOK_LOCK_STALE_SECONDS
|
|
46
|
+
# age (mtime) after which a leftover mkdir-based lock
|
|
47
|
+
# directory is reclaimed instead of respected as busy;
|
|
48
|
+
# default 1800 (a few multiples of the 600s hook
|
|
49
|
+
# timeout). Only the mkdir fallback needs this;
|
|
50
|
+
# flock has no equivalent problem.
|
|
51
|
+
set -u
|
|
52
|
+
|
|
53
|
+
[ "${WOODS_HOOKS_DISABLED:-0}" = "1" ] && exit 0
|
|
54
|
+
[ "${WOODS_HOOKS_ENABLED:-0}" = "1" ] || exit 0
|
|
55
|
+
|
|
56
|
+
payload="$(cat)"
|
|
57
|
+
|
|
58
|
+
# $1: dotted key path. Prefers jq, falls back to ruby, else gives up quietly.
|
|
59
|
+
field() {
|
|
60
|
+
if command -v jq >/dev/null 2>&1; then
|
|
61
|
+
printf '%s' "$payload" | jq -r ".$1 // empty"
|
|
62
|
+
elif command -v ruby >/dev/null 2>&1; then
|
|
63
|
+
printf '%s' "$payload" | ruby -rjson -e '
|
|
64
|
+
value = JSON.parse($stdin.read)
|
|
65
|
+
ARGV[0].split(".").each { |key| value = value.is_a?(Hash) ? value[key] : nil }
|
|
66
|
+
print value.to_s' -- "$1"
|
|
67
|
+
else
|
|
68
|
+
printf ''
|
|
69
|
+
fi
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
cwd="$(field cwd)"
|
|
73
|
+
file="$(field tool_input.file_path)"
|
|
74
|
+
[ -z "$cwd" ] && exit 0
|
|
75
|
+
[ -z "$file" ] && exit 0
|
|
76
|
+
|
|
77
|
+
# Same no-boot resolution woods:watch_status uses: an explicit WOODS_OUTPUT
|
|
78
|
+
# wins outright (absolute or relative to cwd), otherwise tmp/woods under cwd.
|
|
79
|
+
# This is the config's own override knob, not a second hardcoded path.
|
|
80
|
+
configured_output="${WOODS_OUTPUT:-tmp/woods}"
|
|
81
|
+
case "$configured_output" in
|
|
82
|
+
/*) tmp_dir="$configured_output" ;;
|
|
83
|
+
*) tmp_dir="$cwd/$configured_output" ;;
|
|
84
|
+
esac
|
|
85
|
+
|
|
86
|
+
[ -f "$tmp_dir/generation.json" ] || exit 0
|
|
87
|
+
|
|
88
|
+
case "$file" in
|
|
89
|
+
"$cwd"/*) rel="${file#"$cwd"/}" ;;
|
|
90
|
+
/*) exit 0 ;;
|
|
91
|
+
*) rel="$file" ;;
|
|
92
|
+
esac
|
|
93
|
+
|
|
94
|
+
case "$rel" in
|
|
95
|
+
app/models/*|config/routes.rb|config/routes/*|db/migrate/*|db/*_migrate/*|db/schema.rb|db/structure.sql|package.yml|*/package.yml|packwerk.yml) ;;
|
|
96
|
+
*) exit 0 ;;
|
|
97
|
+
esac
|
|
98
|
+
|
|
99
|
+
rake="${WOODS_HOOK_RAKE:-bundle exec rake}"
|
|
100
|
+
log="$tmp_dir/hook.log"
|
|
101
|
+
pending_file="$tmp_dir/hook-pending.txt"
|
|
102
|
+
pending_lock="$tmp_dir/hook-pending.lock"
|
|
103
|
+
pending_lock_dir="$tmp_dir/hook-pending.lock.d"
|
|
104
|
+
run_lock="$tmp_dir/hook.lock"
|
|
105
|
+
run_lock_dir="$tmp_dir/hook.lock.d"
|
|
106
|
+
|
|
107
|
+
mkdir -p "$tmp_dir" 2>/dev/null || true
|
|
108
|
+
|
|
109
|
+
have_flock() { command -v flock >/dev/null 2>&1; }
|
|
110
|
+
|
|
111
|
+
# mtime of a directory in epoch seconds, GNU stat then BSD stat, empty if
|
|
112
|
+
# neither exists (treated as "not stale": fail closed, keep waiting rather
|
|
113
|
+
# than reclaim on a guess).
|
|
114
|
+
dir_mtime() {
|
|
115
|
+
stat -c %Y "$1" 2>/dev/null || stat -f %m "$1" 2>/dev/null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# True when $1 is an mkdir-based lock directory old enough that it can only
|
|
119
|
+
# be a crash leftover, never a legitimately still-running hook (a hook run
|
|
120
|
+
# is one rake invocation, bounded by the 600s hook timeout).
|
|
121
|
+
lock_dir_stale() {
|
|
122
|
+
mtime="$(dir_mtime "$1")"
|
|
123
|
+
[ -z "$mtime" ] && return 1
|
|
124
|
+
now="$(date +%s)"
|
|
125
|
+
age=$((now - mtime))
|
|
126
|
+
[ "$age" -gt "${WOODS_HOOK_LOCK_STALE_SECONDS:-1800}" ]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Blocking mkdir-based lock acquire: waits for $1, reclaiming it once stale
|
|
130
|
+
# rather than waiting for a crashed holder that will never release it.
|
|
131
|
+
acquire_mkdir_lock() {
|
|
132
|
+
while ! mkdir "$1" 2>/dev/null; do
|
|
133
|
+
if lock_dir_stale "$1"; then
|
|
134
|
+
rmdir "$1" 2>/dev/null || true
|
|
135
|
+
continue
|
|
136
|
+
fi
|
|
137
|
+
sleep 0.1
|
|
138
|
+
done
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
release_mkdir_lock() {
|
|
142
|
+
rmdir "$1" 2>/dev/null || true
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# Non-blocking mkdir-based lock attempt: one reclaim retry when stale, no
|
|
146
|
+
# wait otherwise (contention here means "someone else is already draining,"
|
|
147
|
+
# not "someone crashed").
|
|
148
|
+
try_acquire_mkdir_lock() {
|
|
149
|
+
mkdir "$1" 2>/dev/null && return 0
|
|
150
|
+
if lock_dir_stale "$1"; then
|
|
151
|
+
rmdir "$1" 2>/dev/null || true
|
|
152
|
+
mkdir "$1" 2>/dev/null && return 0
|
|
153
|
+
fi
|
|
154
|
+
return 1
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Append one path to the pending file. Blocking: the critical section is a
|
|
158
|
+
# single append, so any wait here is brief regardless of who else holds it.
|
|
159
|
+
append_pending() {
|
|
160
|
+
if have_flock; then
|
|
161
|
+
exec 7>"$pending_lock"
|
|
162
|
+
flock 7
|
|
163
|
+
printf '%s\n' "$1" >>"$pending_file"
|
|
164
|
+
flock -u 7
|
|
165
|
+
exec 7>&-
|
|
166
|
+
else
|
|
167
|
+
acquire_mkdir_lock "$pending_lock_dir"
|
|
168
|
+
printf '%s\n' "$1" >>"$pending_file"
|
|
169
|
+
release_mkdir_lock "$pending_lock_dir"
|
|
170
|
+
fi
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# Print and clear whatever is currently pending, empty output if nothing is.
|
|
174
|
+
drain_pending() {
|
|
175
|
+
if have_flock; then
|
|
176
|
+
exec 7>"$pending_lock"
|
|
177
|
+
flock 7
|
|
178
|
+
if [ -s "$pending_file" ]; then
|
|
179
|
+
cat "$pending_file"
|
|
180
|
+
: >"$pending_file"
|
|
181
|
+
fi
|
|
182
|
+
flock -u 7
|
|
183
|
+
exec 7>&-
|
|
184
|
+
else
|
|
185
|
+
acquire_mkdir_lock "$pending_lock_dir"
|
|
186
|
+
if [ -s "$pending_file" ]; then
|
|
187
|
+
cat "$pending_file"
|
|
188
|
+
: >"$pending_file"
|
|
189
|
+
fi
|
|
190
|
+
release_mkdir_lock "$pending_lock_dir"
|
|
191
|
+
fi
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
run_incremental() {
|
|
195
|
+
# A drain can return several paths at once; CHANGED_FILES takes a
|
|
196
|
+
# comma-separated list (see lib/tasks/woods.rake).
|
|
197
|
+
changed="$(printf '%s\n' "$1" | tr '\n' ',' | sed 's/,$//')"
|
|
198
|
+
# shellcheck disable=SC2086
|
|
199
|
+
( cd "$cwd" && CHANGED_FILES="$changed" $rake woods:incremental ) >>"$log" 2>&1
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
drain_until_empty() {
|
|
203
|
+
while :; do
|
|
204
|
+
batch="$(drain_pending)"
|
|
205
|
+
[ -z "$batch" ] && break
|
|
206
|
+
run_incremental "$batch"
|
|
207
|
+
done
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
append_pending "$rel"
|
|
211
|
+
|
|
212
|
+
if have_flock; then
|
|
213
|
+
exec 8>"$run_lock"
|
|
214
|
+
if flock -n 8; then
|
|
215
|
+
drain_until_empty
|
|
216
|
+
flock -u 8
|
|
217
|
+
fi
|
|
218
|
+
exec 8>&-
|
|
219
|
+
else
|
|
220
|
+
if try_acquire_mkdir_lock "$run_lock_dir"; then
|
|
221
|
+
drain_until_empty
|
|
222
|
+
release_mkdir_lock "$run_lock_dir"
|
|
223
|
+
fi
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
exit 0
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Woods SessionStart hook (#280): say so when the published generation is
|
|
3
|
+
# older than the last commit. Stdout from a SessionStart hook is added to
|
|
4
|
+
# the session context, so the agent sees the warning before it trusts the
|
|
5
|
+
# index.
|
|
6
|
+
#
|
|
7
|
+
# This check only compares two commit-adjacent timestamps: the generation's
|
|
8
|
+
# `updated_at` and `git log -1`'s commit time. It says nothing about
|
|
9
|
+
# uncommitted edits (the index can be stale against a dirty working tree
|
|
10
|
+
# with no stale commit to detect) or about a checkout sitting on an older
|
|
11
|
+
# commit than the one that produced the generation (the timestamp comparison
|
|
12
|
+
# can read as fresh there even though the code and the index disagree). Read
|
|
13
|
+
# a quiet run as "not behind the last commit," not as "definitely current."
|
|
14
|
+
#
|
|
15
|
+
# Opt-in: shipped disabled. Nothing prints until WOODS_HOOKS_ENABLED=1 is
|
|
16
|
+
# set; WOODS_HOOKS_DISABLED=1 turns it back off without touching that
|
|
17
|
+
# setting.
|
|
18
|
+
set -u
|
|
19
|
+
|
|
20
|
+
[ "${WOODS_HOOKS_DISABLED:-0}" = "1" ] && exit 0
|
|
21
|
+
[ "${WOODS_HOOKS_ENABLED:-0}" = "1" ] || exit 0
|
|
22
|
+
|
|
23
|
+
payload="$(cat)"
|
|
24
|
+
|
|
25
|
+
field() {
|
|
26
|
+
if command -v jq >/dev/null 2>&1; then
|
|
27
|
+
printf '%s' "$payload" | jq -r ".$1 // empty"
|
|
28
|
+
elif command -v ruby >/dev/null 2>&1; then
|
|
29
|
+
printf '%s' "$payload" | ruby -rjson -e '
|
|
30
|
+
value = JSON.parse($stdin.read)
|
|
31
|
+
ARGV[0].split(".").each { |key| value = value.is_a?(Hash) ? value[key] : nil }
|
|
32
|
+
print value.to_s' -- "$1"
|
|
33
|
+
else
|
|
34
|
+
printf ''
|
|
35
|
+
fi
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
cwd="$(field cwd)"
|
|
39
|
+
[ -z "$cwd" ] && exit 0
|
|
40
|
+
|
|
41
|
+
# Same no-boot resolution the PostToolUse hook and woods:watch_status use:
|
|
42
|
+
# WOODS_OUTPUT overrides outright, otherwise tmp/woods under cwd.
|
|
43
|
+
configured_output="${WOODS_OUTPUT:-tmp/woods}"
|
|
44
|
+
case "$configured_output" in
|
|
45
|
+
/*) tmp_dir="$configured_output" ;;
|
|
46
|
+
*) tmp_dir="$cwd/$configured_output" ;;
|
|
47
|
+
esac
|
|
48
|
+
|
|
49
|
+
marker="$tmp_dir/generation.json"
|
|
50
|
+
[ -f "$marker" ] || exit 0
|
|
51
|
+
|
|
52
|
+
if command -v jq >/dev/null 2>&1; then
|
|
53
|
+
updated="$(jq -r '.updated_at // empty' "$marker")"
|
|
54
|
+
number="$(jq -r '.number // empty' "$marker")"
|
|
55
|
+
elif command -v ruby >/dev/null 2>&1; then
|
|
56
|
+
updated="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["updated_at"].to_s' -- "$marker")"
|
|
57
|
+
number="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["number"].to_s' -- "$marker")"
|
|
58
|
+
else
|
|
59
|
+
exit 0
|
|
60
|
+
fi
|
|
61
|
+
[ -z "$updated" ] && exit 0
|
|
62
|
+
|
|
63
|
+
last_commit="$(git -C "$cwd" log -1 --format=%cI 2>/dev/null)"
|
|
64
|
+
[ -z "$last_commit" ] && exit 0
|
|
65
|
+
|
|
66
|
+
stale=0
|
|
67
|
+
if command -v ruby >/dev/null 2>&1; then
|
|
68
|
+
ruby -rtime -e 'exit(Time.parse(ARGV[0]) < Time.parse(ARGV[1]) ? 1 : 0)' -- "$updated" "$last_commit" || stale=1
|
|
69
|
+
elif date -d "$updated" +%s >/dev/null 2>&1; then
|
|
70
|
+
[ "$(date -d "$updated" +%s)" -lt "$(date -d "$last_commit" +%s)" ] && stale=1
|
|
71
|
+
fi
|
|
72
|
+
|
|
73
|
+
if [ "$stale" = "1" ]; then
|
|
74
|
+
echo "Woods index is stale: generation ${number:-?} was published at $updated, before the last commit at $last_commit." \
|
|
75
|
+
"Run bin/rails woods:incremental (or start bin/rails woods:watch) before trusting woods answers."
|
|
76
|
+
fi
|
|
77
|
+
exit 0
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: woods-agent-enable
|
|
3
|
+
description: Make a repository's coding agents use the Woods index by default — wire the MCP server into project configuration, add index-first guidance to CLAUDE.md/AGENTS.md, and optionally create a project skill. Use when a user wants agents to "know" their Rails codebase, asks to add Woods to a team's agent setup or repository instructions, or wants codebase intelligence available in every session without per-user installs.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Enable Woods for a repository's agents
|
|
7
|
+
|
|
8
|
+
Installing the gem gives one operator an index; this skill gives every future agent session one. Wiring is three small, reviewable changes to the host repository. Each edits shared, checked-in files, so propose the diff and get approval before writing, match the repository's existing conventions, and leave unrelated content untouched.
|
|
9
|
+
|
|
10
|
+
## Preflight
|
|
11
|
+
|
|
12
|
+
Confirm Woods actually works before advertising it to every future session:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
bundle info woods
|
|
16
|
+
bin/rails woods:validate
|
|
17
|
+
bin/rails woods:stats
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
A missing gem or index means setup comes first (woods-setup). Record the index path and whether the app runs on the host or in Docker — the MCP entry must use the command shape that matches (woods-mcp-config has the shapes).
|
|
21
|
+
|
|
22
|
+
## 1. Project MCP configuration
|
|
23
|
+
|
|
24
|
+
Add the Index Server to the repository's checked-in MCP configuration (`.mcp.json` for Claude Code; adapt to the team's client) using the Index-only shape from woods-mcp-config, so every clone gets the server without per-user setup. Do not wire Console MCP at the project level: it is live-data access and stays a per-user, explicitly authorized opt-in.
|
|
25
|
+
|
|
26
|
+
## 2. Index-first guidance in the agent instructions
|
|
27
|
+
|
|
28
|
+
Add a short section to the repository's agent instructions (CLAUDE.md, AGENTS.md, or the team's equivalent). Keep it to the contract, not a tool manual — the connected server's own tool list is the manual:
|
|
29
|
+
|
|
30
|
+
```markdown
|
|
31
|
+
## Codebase index (Woods)
|
|
32
|
+
|
|
33
|
+
A Woods MCP server ("woods") serves a runtime-accurate index of this app.
|
|
34
|
+
Call `woods_status` first; for structural questions prefer `search` →
|
|
35
|
+
`lookup` → `dependencies`/`dependents`/`trace_flow` over broad file reading
|
|
36
|
+
or grep. "Not found" is evidence about the index, not the code — check
|
|
37
|
+
generation freshness before concluding. After changing files, refresh with
|
|
38
|
+
`bin/rails woods:incremental` (or run the `woods:watch` daemon).
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Adjust names and commands to the app (Docker prefix, custom index path, task aliases). If the repository already documents agent tooling, extend that section rather than adding a competing one.
|
|
42
|
+
|
|
43
|
+
## 3. Optional: a project skill
|
|
44
|
+
|
|
45
|
+
When the team wants stronger triggering than standing instructions provide, create a repository-level skill (for Claude Code: `.claude/skills/<app>-codebase/SKILL.md`) whose description names the application and its domains, and whose body applies the woods-investigate workflow to this codebase specifically: the entry-point identifiers worth knowing, domain cluster names, and local conventions. Seed that content from real `domain_clusters` and `pagerank` output rather than inventing it, and note in the skill that the index — not the skill text — is the source of truth as the app evolves.
|
|
46
|
+
|
|
47
|
+
## Handoff
|
|
48
|
+
|
|
49
|
+
Report the files changed, the MCP entry added, and one verified end-to-end call (`woods_status` through the configured client, not a raw JSON-RPC probe). Remind the owner that agent sessions pick up the changes on their next start, and that the index only stays useful if extraction stays current.
|
|
50
|
+
|
|
51
|
+
Canonical guides: [AGENT_SETUP.md](https://github.com/lost-in-the/woods/blob/main/docs/AGENT_SETUP.md), [AGENT_GUIDE.md](https://github.com/lost-in-the/woods/blob/main/docs/AGENT_GUIDE.md).
|