woods 2.0.0.beta2 → 2.0.0.beta4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +339 -1
- data/CONTRIBUTING.md +188 -12
- data/README.md +93 -174
- data/SECURITY.md +9 -6
- data/docs/AGENT_GUIDE.md +109 -8
- data/docs/AGENT_SETUP.md +98 -7
- data/docs/BACKEND_MATRIX.md +25 -0
- data/docs/CLIENT_HOOKS.md +111 -0
- data/docs/CONFIGURATION_REFERENCE.md +267 -16
- data/docs/CONSOLE_MCP_SETUP.md +80 -7
- data/docs/DOCKER_SETUP.md +22 -3
- data/docs/EVALUATION.md +464 -1
- data/docs/EXTRACTOR_REFERENCE.md +45 -6
- data/docs/FAQ.md +11 -12
- data/docs/GETTING_STARTED.md +17 -5
- data/docs/INCREMENTAL_EXTRACTION.md +147 -7
- data/docs/INDEX_LAYOUT.md +382 -0
- data/docs/INTERNALS.md +7 -2
- data/docs/MCP_SERVERS.md +276 -5
- data/docs/MCP_TOOL_COOKBOOK.md +37 -22
- data/docs/MCP_WORKTREE_SETUP.md +43 -83
- data/docs/NOTION_INTEGRATION.md +13 -0
- data/docs/OBSIDIAN_INTEGRATION.md +57 -9
- data/docs/PUBLISHED_INDEX.md +72 -0
- data/docs/README.md +7 -0
- data/docs/RETRIEVAL_GUIDE.md +273 -12
- data/docs/RUNTIME_TRACING.md +71 -0
- data/docs/SOURCE_FRESHNESS.md +143 -0
- data/docs/TROUBLESHOOTING.md +129 -18
- data/docs/UNBLOCKED_INTEGRATION.md +25 -0
- data/docs/UPGRADING_TO_2.md +48 -22
- data/docs/WATCH_DAEMON.md +277 -67
- data/exe/woods-agent-config +6 -0
- data/exe/woods-extract +5 -0
- data/exe/woods-hook-context +6 -0
- data/exe/woods-mcp-start +14 -9
- data/lib/generators/woods/pgvector_generator.rb +8 -2
- data/lib/generators/woods/templates/woods.rb.tt +1 -3
- data/lib/tasks/woods.rake +47 -397
- data/lib/woods/agent_configuration/applier.rb +135 -0
- data/lib/woods/agent_configuration/cli.rb +101 -0
- data/lib/woods/agent_configuration/cli_options.rb +29 -0
- data/lib/woods/agent_configuration/document.rb +105 -0
- data/lib/woods/agent_configuration/error.rb +7 -0
- data/lib/woods/agent_configuration/launcher.rb +75 -0
- data/lib/woods/agent_configuration/layout.rb +72 -0
- data/lib/woods/agent_configuration/managed_section.rb +62 -0
- data/lib/woods/agent_configuration/plan.rb +98 -0
- data/lib/woods/agent_configuration/plan_diff.rb +38 -0
- data/lib/woods/agent_configuration/planned_files.rb +61 -0
- data/lib/woods/agent_configuration/planner.rb +63 -0
- data/lib/woods/agent_configuration/planner_validation.rb +77 -0
- data/lib/woods/agent_configuration/preflight.rb +100 -0
- data/lib/woods/agent_configuration/recovery.rb +49 -0
- data/lib/woods/ast/node.rb +2 -0
- data/lib/woods/ast/parser.rb +38 -5
- data/lib/woods/builder.rb +21 -5
- data/lib/woods/cache/cache_middleware.rb +28 -7
- data/lib/woods/cache/cache_store.rb +4 -5
- data/lib/woods/change_set.rb +5 -4
- data/lib/woods/console/credential_index.rb +20 -2
- data/lib/woods/console/credential_scanner.rb +18 -17
- data/lib/woods/console/credential_scanner_registry.rb +36 -0
- data/lib/woods/console/dispatch_pipeline.rb +7 -0
- data/lib/woods/console/embedded_executor.rb +32 -10
- data/lib/woods/console/encrypted_credential_snapshot.rb +16 -0
- data/lib/woods/console/rack_middleware.rb +22 -13
- data/lib/woods/console/server.rb +18 -16
- data/lib/woods/console/sql_noise_stripper.rb +9 -7
- data/lib/woods/console/sql_table_scanner.rb +47 -7
- data/lib/woods/console/sql_validator.rb +49 -9
- data/lib/woods/console/sqlite_read_guard.rb +46 -0
- data/lib/woods/coordination/pipeline_lock.rb +3 -2
- data/lib/woods/dependency_graph.rb +65 -13
- data/lib/woods/embedding/corpus.rb +94 -0
- data/lib/woods/embedding/indexer.rb +114 -60
- data/lib/woods/embedding/openai.rb +17 -6
- data/lib/woods/evaluation/ablation_executor.rb +6 -1
- data/lib/woods/evaluation/ablation_timed_executor.rb +22 -4
- data/lib/woods/export/typed_reader.rb +56 -0
- data/lib/woods/extractor.rb +277 -149
- data/lib/woods/extractors/action_cable_extractor.rb +3 -1
- data/lib/woods/extractors/behavioral_profile.rb +9 -7
- data/lib/woods/extractors/caching_extractor.rb +3 -1
- data/lib/woods/extractors/concern_extractor.rb +64 -6
- data/lib/woods/extractors/configuration_extractor.rb +7 -3
- data/lib/woods/extractors/controller_extractor.rb +13 -4
- data/lib/woods/extractors/database_view_extractor.rb +3 -1
- data/lib/woods/extractors/declared_parent.rb +55 -0
- data/lib/woods/extractors/decorator_extractor.rb +3 -1
- data/lib/woods/extractors/engine_extractor.rb +3 -1
- data/lib/woods/extractors/event_extractor.rb +4 -2
- data/lib/woods/extractors/factory_extractor.rb +3 -1
- data/lib/woods/extractors/graphql_extractor.rb +10 -13
- data/lib/woods/extractors/i18n_extractor.rb +3 -1
- data/lib/woods/extractors/job_extractor.rb +6 -19
- data/lib/woods/extractors/lib_extractor.rb +13 -9
- data/lib/woods/extractors/mailer_extractor.rb +26 -15
- data/lib/woods/extractors/manager_extractor.rb +3 -1
- data/lib/woods/extractors/method_parameters.rb +53 -0
- data/lib/woods/extractors/middleware_argument.rb +65 -0
- data/lib/woods/extractors/middleware_extractor.rb +9 -3
- data/lib/woods/extractors/migration_extractor.rb +3 -1
- data/lib/woods/extractors/model_extractor.rb +26 -34
- data/lib/woods/extractors/package_extractor.rb +24 -4
- data/lib/woods/extractors/phlex_extractor.rb +3 -1
- data/lib/woods/extractors/policy_extractor.rb +3 -1
- data/lib/woods/extractors/poro_extractor.rb +13 -9
- data/lib/woods/extractors/pundit_extractor.rb +3 -1
- data/lib/woods/extractors/rails_source_extractor.rb +4 -2
- data/lib/woods/extractors/rake_task_extractor.rb +4 -2
- data/lib/woods/extractors/route_extractor.rb +3 -1
- data/lib/woods/extractors/route_helper_resolver.rb +10 -33
- data/lib/woods/extractors/scheduled_job_extractor.rb +41 -15
- data/lib/woods/extractors/serializer_extractor.rb +4 -2
- data/lib/woods/extractors/service_extractor.rb +3 -1
- data/lib/woods/extractors/shared_dependency_scanner.rb +2 -2
- data/lib/woods/extractors/shared_utility_methods.rb +48 -19
- data/lib/woods/extractors/source_nesting.rb +1 -1
- data/lib/woods/extractors/state_machine_extractor.rb +3 -1
- data/lib/woods/extractors/test_mapping_extractor.rb +3 -1
- data/lib/woods/extractors/validator_extractor.rb +3 -1
- data/lib/woods/extractors/view_component_extractor.rb +3 -1
- data/lib/woods/extractors/view_template_extractor.rb +3 -1
- data/lib/woods/gem_mapper.rb +2 -0
- data/lib/woods/git_history.rb +116 -0
- data/lib/woods/graph_analyzer.rb +35 -6
- data/lib/woods/hooks/context_cli.rb +54 -0
- data/lib/woods/hooks/context_event.rb +88 -0
- data/lib/woods/hooks/context_hint.rb +73 -0
- data/lib/woods/hooks/context_impact.rb +77 -0
- data/lib/woods/hooks/context_output.rb +47 -0
- data/lib/woods/hooks/context_state.rb +102 -0
- data/lib/woods/hooks/refresh.rb +79 -0
- data/lib/woods/hooks/rule_projection.rb +78 -0
- data/lib/woods/input_rules.rb +19 -0
- data/lib/woods/mcp/bearer_auth.rb +22 -13
- data/lib/woods/mcp/bootstrapper.rb +79 -4
- data/lib/woods/mcp/config_resolver.rb +2 -1
- data/lib/woods/mcp/index_reader.rb +334 -162
- data/lib/woods/mcp/initialization_guidance.rb +27 -0
- data/lib/woods/mcp/origin_guard.rb +17 -9
- data/lib/woods/mcp/published_lexical_retriever.rb +115 -0
- data/lib/woods/mcp/renderers/markdown_renderer.rb +22 -9
- data/lib/woods/mcp/renderers/plain_renderer.rb +18 -8
- data/lib/woods/mcp/search_results.rb +74 -0
- data/lib/woods/mcp/server.rb +178 -63
- data/lib/woods/mcp/tool_contract.rb +3 -1
- data/lib/woods/mcp/tool_response_renderer.rb +41 -0
- data/lib/woods/mcp/traversal_evidence.rb +113 -0
- data/lib/woods/mcp/traversal_evidence_index.rb +100 -0
- data/lib/woods/mcp/traversal_evidence_page.rb +41 -0
- data/lib/woods/mcp/traversal_evidence_text.rb +52 -0
- data/lib/woods/mcp/traversal_response.rb +22 -0
- data/lib/woods/notion/exporter.rb +56 -17
- data/lib/woods/obsidian/destination_plan.rb +98 -0
- data/lib/woods/obsidian/name_mapper.rb +19 -3
- data/lib/woods/obsidian/note_builder.rb +19 -10
- data/lib/woods/obsidian/vault_exporter.rb +88 -32
- data/lib/woods/operator/pipeline_guard.rb +18 -13
- data/lib/woods/path_dispatcher.rb +13 -6
- data/lib/woods/payload_store.rb +27 -26
- data/lib/woods/published_index/typed_unit_reader.rb +40 -3
- data/lib/woods/published_index.rb +2 -2
- data/lib/woods/railtie.rb +3 -3
- data/lib/woods/railtie_support.rb +12 -12
- data/lib/woods/rake_helpers.rb +382 -0
- data/lib/woods/resilience/graph_invariant_validator/membership_checks.rb +71 -0
- data/lib/woods/resilience/graph_invariant_validator/node_checks.rb +61 -0
- data/lib/woods/resilience/graph_invariant_validator/reverse_relationship_checks.rb +46 -0
- data/lib/woods/resilience/graph_invariant_validator.rb +119 -0
- data/lib/woods/resilience/index_validator/graph_checks.rb +80 -0
- data/lib/woods/resilience/index_validator.rb +112 -23
- data/lib/woods/retrieval/context_assembler.rb +50 -15
- data/lib/woods/retrieval/lexical_assembler.rb +84 -0
- data/lib/woods/retrieval/lexical_index.rb +120 -0
- data/lib/woods/retrieval/ranker.rb +4 -2
- data/lib/woods/retrieval/scope.rb +108 -0
- data/lib/woods/retrieval/scoped_graph_store.rb +32 -0
- data/lib/woods/retrieval/scoped_vector_store.rb +55 -0
- data/lib/woods/retrieval/search_executor.rb +86 -27
- data/lib/woods/retrieval/source_evidence.rb +200 -0
- data/lib/woods/retriever.rb +98 -22
- data/lib/woods/ruby_analyzer/trace_enricher.rb +77 -38
- data/lib/woods/session_tracer/file_store.rb +6 -1
- data/lib/woods/session_tracer/middleware.rb +10 -12
- data/lib/woods/session_tracer/redis_store.rb +22 -6
- data/lib/woods/session_tracer/session_flow_assembler.rb +23 -17
- data/lib/woods/session_tracer/solid_cache_coordination.rb +6 -4
- data/lib/woods/session_tracer/unit_resolver.rb +63 -0
- data/lib/woods/source_inputs/consumer_errors.rb +31 -0
- data/lib/woods/source_inputs/handoff.rb +102 -0
- data/lib/woods/source_inputs/launcher.rb +157 -0
- data/lib/woods/source_inputs/manifest.rb +124 -0
- data/lib/woods/source_inputs/private_key.rb +55 -0
- data/lib/woods/source_inputs/scanner.rb +171 -0
- data/lib/woods/source_inputs/scopes.rb +71 -0
- data/lib/woods/source_inputs/session.rb +214 -0
- data/lib/woods/source_inputs/status.rb +84 -0
- data/lib/woods/source_inputs/verifier.rb +107 -0
- data/lib/woods/storage/metadata_store.rb +25 -25
- data/lib/woods/storage/pgvector.rb +35 -10
- data/lib/woods/storage/qdrant.rb +17 -7
- data/lib/woods/storage/vector_store.rb +18 -6
- data/lib/woods/tasks.rb +3 -2
- data/lib/woods/temporal/json_snapshot_store.rb +58 -9
- data/lib/woods/unblocked/exporter.rb +59 -70
- data/lib/woods/version.rb +1 -1
- data/lib/woods/watch/boot_snapshot.rb +52 -0
- data/lib/woods/watch/daemon.rb +154 -32
- data/lib/woods/watch/listen_watcher.rb +4 -0
- data/lib/woods/watch/polling_watcher.rb +5 -1
- data/lib/woods/watch/status.rb +20 -15
- data/lib/woods/watch/tree_scan.rb +21 -13
- data/lib/woods/watch/watcher.rb +4 -1
- data/lib/woods.rb +50 -11
- data/plugin/.claude-plugin/plugin.json +1 -1
- data/plugin/hooks/adapters/normalize.jq +15 -0
- data/plugin/hooks/adapters/normalize.rb +63 -0
- data/plugin/hooks/hooks.json +20 -0
- data/plugin/hooks/woods-context.sh +50 -0
- data/plugin/hooks/woods-input-rules.sh +159 -0
- data/plugin/hooks/woods-opencode.mjs +65 -0
- data/plugin/hooks/woods-post-edit.sh +2 -225
- data/plugin/hooks/woods-refresh.sh +260 -0
- data/plugin/hooks/woods-session-start.sh +47 -55
- data/plugin/skills/woods-agent-enable/SKILL.md +19 -0
- data/plugin/skills/woods-diagnose/SKILL.md +319 -1
- data/plugin/skills/woods-investigate/SKILL.md +145 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +90 -2
- data/plugin/skills/woods-setup/SKILL.md +110 -6
- metadata +87 -5
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# Generated by script/generate-hook-rules; do not edit.
|
|
2
|
+
woods_input_action() {
|
|
3
|
+
local path="$1" operation="${2:-update}"
|
|
4
|
+
if [[ "$path" == Gemfile ]]; then printf full; return; fi
|
|
5
|
+
if [[ "$path" == Gemfile.lock ]]; then printf full; return; fi
|
|
6
|
+
if [[ "$path" == .ruby-version ]]; then printf full; return; fi
|
|
7
|
+
if [[ "$path" == config/application.rb ]]; then printf full; return; fi
|
|
8
|
+
if [[ "$path" == config/boot.rb ]]; then printf full; return; fi
|
|
9
|
+
if [[ "$path" == config/environment.rb ]]; then printf full; return; fi
|
|
10
|
+
if [[ "$path" == config/database.yml ]]; then printf full; return; fi
|
|
11
|
+
if [[ "$path" == config/credentials.yml.enc ]]; then printf full; return; fi
|
|
12
|
+
if [[ "$path" == config/master.key ]]; then printf full; return; fi
|
|
13
|
+
if [[ "$path" == db/schema.rb ]]; then printf full; return; fi
|
|
14
|
+
if [[ "$path" == db/structure.sql ]]; then printf full; return; fi
|
|
15
|
+
if [[ "$path" == config/initializers/* ]]; then printf full; return; fi
|
|
16
|
+
if [[ "$path" == config/environments/* ]]; then printf full; return; fi
|
|
17
|
+
if [[ "$path" == config/credentials/* ]]; then printf full; return; fi
|
|
18
|
+
if [[ "$path" =~ ^config/settings\.ya?ml$ ]]; then printf full; return; fi
|
|
19
|
+
if [[ "$path" =~ ^config/settings/[^/]+\.ya?ml$ ]]; then printf full; return; fi
|
|
20
|
+
if [[ "$path" =~ ^config/(cable|storage|sidekiq|puma|cache|queue)\.ya?ml$ ]]; then printf full; return; fi
|
|
21
|
+
if [[ "$path" =~ ^\.env(\..+)?$ ]]; then printf full; return; fi
|
|
22
|
+
if { { { [[ "$path" == *.rb ]]; } && { [[ "$path" == app/* ]]; }; }; }; then
|
|
23
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
24
|
+
return
|
|
25
|
+
fi
|
|
26
|
+
if { { { [[ "$path" == *.rb ]]; } && { [[ "$path" == app/services/* ]] || [[ "$path" == app/interactors/* ]] || [[ "$path" == app/operations/* ]] || [[ "$path" == app/commands/* ]] || [[ "$path" == app/use_cases/* ]]; }; }; }; then
|
|
27
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
28
|
+
return
|
|
29
|
+
fi
|
|
30
|
+
if { { { [[ "$path" == *.rb ]]; } && { [[ "$path" == app/jobs/* ]] || [[ "$path" == app/workers/* ]] || [[ "$path" == app/sidekiq/* ]]; }; }; }; then
|
|
31
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
32
|
+
return
|
|
33
|
+
fi
|
|
34
|
+
if { { { [[ "$path" == *.rb ]]; } && { [[ "$path" == app/serializers/* ]] || [[ "$path" == app/blueprinters/* ]] || [[ "$path" == app/decorators/* ]]; }; }; }; then
|
|
35
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
36
|
+
return
|
|
37
|
+
fi
|
|
38
|
+
if { { { [[ "$path" == *.rb ]]; } && { [[ "$path" == app/managers/* ]]; }; }; }; then
|
|
39
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
40
|
+
return
|
|
41
|
+
fi
|
|
42
|
+
if { { { [[ "$path" == *.rb ]]; } && { [[ "$path" == app/policies/* ]]; }; }; }; then
|
|
43
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
44
|
+
return
|
|
45
|
+
fi
|
|
46
|
+
if { { { [[ "$path" == *.rb ]]; } && { [[ "$path" == app/validators/* ]]; }; }; }; then
|
|
47
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
48
|
+
return
|
|
49
|
+
fi
|
|
50
|
+
if { { { [[ "$path" == *.rb ]]; } && { [[ "$path" == app/policies/* ]]; }; }; }; then
|
|
51
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
52
|
+
return
|
|
53
|
+
fi
|
|
54
|
+
if { { { [[ "$path" == *.rb ]]; } && { [[ "$path" == app/decorators/* ]] || [[ "$path" == app/presenters/* ]] || [[ "$path" == app/form_objects/* ]]; }; }; }; then
|
|
55
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
56
|
+
return
|
|
57
|
+
fi
|
|
58
|
+
if { { { [[ "$path" == *.rb ]]; } && { [[ "$path" == config/initializers/* ]] || [[ "$path" == config/environments/* ]]; }; }; }; then
|
|
59
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
60
|
+
return
|
|
61
|
+
fi
|
|
62
|
+
if { { { [[ "$path" == *.rb ]]; } && [[ "$path" == */concerns/* ]] && { [[ "$path" == app/* ]]; }; }; }; then
|
|
63
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
64
|
+
return
|
|
65
|
+
fi
|
|
66
|
+
if { { [[ "$path" != */concerns/* ]] && { [[ "$path" == *.rb ]]; } && { [[ "$path" == app/* ]] || [[ "$path" == lib/* ]]; }; }; }; then
|
|
67
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
68
|
+
return
|
|
69
|
+
fi
|
|
70
|
+
if { { { [[ "$path" == *.rb ]]; } && { [[ "$path" == app/graphql/* ]]; }; }; }; then
|
|
71
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
72
|
+
return
|
|
73
|
+
fi
|
|
74
|
+
if { { { [[ "$path" == *.yml ]]; } && { [[ "$path" == config/locales/* ]]; }; }; }; then
|
|
75
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
76
|
+
return
|
|
77
|
+
fi
|
|
78
|
+
if { { { [[ "$path" == *.html.erb ]] || [[ "$path" == *.erb ]]; } && { [[ "$path" == app/views/* ]]; }; }; }; then
|
|
79
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
80
|
+
return
|
|
81
|
+
fi
|
|
82
|
+
if { { { [[ "$path" == *.rb ]]; } && { [[ "${path%/*}" == db/migrate ]]; }; }; }; then
|
|
83
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
84
|
+
return
|
|
85
|
+
fi
|
|
86
|
+
if { { [[ "$path" != */concerns/* ]] && { [[ "$path" == *.rb ]]; } && { [[ "$path" == app/models/* ]]; }; }; }; then
|
|
87
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
88
|
+
return
|
|
89
|
+
fi
|
|
90
|
+
if { { [[ "$path" != */tasks/* ]] && [[ "$path" != */generators/* ]] && { [[ "$path" == *.rb ]]; } && { [[ "$path" == lib/* ]]; }; }; }; then
|
|
91
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
92
|
+
return
|
|
93
|
+
fi
|
|
94
|
+
if { { { [[ "$path" == *_spec.rb ]]; } && { [[ "$path" == spec/* ]]; }; }; }; then
|
|
95
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
96
|
+
return
|
|
97
|
+
fi
|
|
98
|
+
if { { { [[ "$path" == *_test.rb ]]; } && { [[ "$path" == test/* ]]; }; }; }; then
|
|
99
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
100
|
+
return
|
|
101
|
+
fi
|
|
102
|
+
if { { { [[ "$path" == *.rb ]]; } && { [[ "$path" == app/controllers/* ]]; }; }; }; then
|
|
103
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
104
|
+
return
|
|
105
|
+
fi
|
|
106
|
+
if { { { [[ "$path" == *.rb ]]; } && { [[ "$path" == app/models/* ]]; }; }; }; then
|
|
107
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
108
|
+
return
|
|
109
|
+
fi
|
|
110
|
+
if { { { [[ "$path" == *.erb ]]; } && { [[ "$path" == app/views/* ]]; }; }; }; then
|
|
111
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
112
|
+
return
|
|
113
|
+
fi
|
|
114
|
+
if { { { [[ "$path" == *.rake ]]; } && { [[ "$path" == lib/tasks/* ]]; }; }; }; then
|
|
115
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
116
|
+
return
|
|
117
|
+
fi
|
|
118
|
+
if { [[ "$path" == config/routes.rb ]] || { { [[ "$path" == config/routes/* ]]; }; }; }; then
|
|
119
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
120
|
+
return
|
|
121
|
+
fi
|
|
122
|
+
if { [[ "$path" == config/routes.rb ]] || [[ "$path" == Gemfile.lock ]] || { { [[ "$path" == config/routes/* ]]; }; }; }; then
|
|
123
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
124
|
+
return
|
|
125
|
+
fi
|
|
126
|
+
if { [[ "$path" == config/application.rb ]] || [[ "$path" == Gemfile.lock ]] || { { [[ "$path" == config/initializers/* ]] || [[ "$path" == config/environments/* ]]; }; }; }; then
|
|
127
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
128
|
+
return
|
|
129
|
+
fi
|
|
130
|
+
if { [[ "$path" == config/recurring.yml ]] || [[ "$path" == config/sidekiq_cron.yml ]] || [[ "$path" == config/schedule.rb ]] || { false; }; }; then
|
|
131
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
132
|
+
return
|
|
133
|
+
fi
|
|
134
|
+
if { { { [[ "$path" == *.rb ]]; } && { [[ "$path" == app/models/* ]]; }; }; }; then
|
|
135
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
136
|
+
return
|
|
137
|
+
fi
|
|
138
|
+
if { { { [[ "$path" == *.rb ]]; } && { [[ "$path" == spec/factories/* ]] || [[ "$path" == test/factories/* ]]; }; }; }; then
|
|
139
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
140
|
+
return
|
|
141
|
+
fi
|
|
142
|
+
if { { { [[ "$path" == *.sql ]]; } && { [[ "$path" == db/views/* ]]; }; }; }; then
|
|
143
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
144
|
+
return
|
|
145
|
+
fi
|
|
146
|
+
if { { { [[ "$path" == *.rb ]]; } && { [[ "$path" == app/* ]]; }; }; }; then
|
|
147
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
148
|
+
return
|
|
149
|
+
fi
|
|
150
|
+
if { [[ "$path" == Gemfile.lock ]] || { false; }; }; then
|
|
151
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
152
|
+
return
|
|
153
|
+
fi
|
|
154
|
+
if { { [[ "$path" != *node_modules/* ]] && [[ "$path" != *vendor/* ]] && [[ "$path" != *tmp/* ]] && [[ "$path" != *bin/* ]] && [[ "$path" != *script/* ]] && { [[ "${path##*/}" == package.yml ]] || [[ "${path##*/}" == packwerk.yml ]]; }; }; }; then
|
|
155
|
+
case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
|
|
156
|
+
return
|
|
157
|
+
fi
|
|
158
|
+
printf ignore
|
|
159
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// OpenCode 1.18.27 tool.execute.after adapter. Keep callback metadata private:
|
|
2
|
+
// only the worktree and affected paths cross into the shared refresh runner.
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
const runner = fileURLToPath(new URL("./woods-refresh.sh", import.meta.url));
|
|
8
|
+
const enabled = () => process.env.WOODS_HOOKS_ENABLED === "1" && process.env.WOODS_HOOKS_DISABLED !== "1";
|
|
9
|
+
const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10
|
+
const pathString = (value) => typeof value === "string" && value.length > 0 && !value.includes("\0");
|
|
11
|
+
|
|
12
|
+
function eventsFor(input, output) {
|
|
13
|
+
if (!object(input) || !object(output) || !object(output.metadata)) throw new Error("invalid event");
|
|
14
|
+
const metadata = output.metadata;
|
|
15
|
+
if (input.tool === "apply_patch") {
|
|
16
|
+
if (!Array.isArray(metadata.files) || !metadata.files.length || metadata.files.length > 1000) throw new Error("missing file metadata");
|
|
17
|
+
return metadata.files.flatMap((file) => {
|
|
18
|
+
if (!object(file) || !pathString(file.filePath)) throw new Error("invalid file metadata");
|
|
19
|
+
if (file.type === "move") {
|
|
20
|
+
if (!pathString(file.movePath)) throw new Error("missing move target");
|
|
21
|
+
return [{ path: file.filePath, operation: "delete" }, { path: file.movePath, operation: "add" }];
|
|
22
|
+
}
|
|
23
|
+
if (!["add", "update", "delete"].includes(file.type)) throw new Error("unknown file operation");
|
|
24
|
+
return [{ path: file.filePath, operation: file.type }];
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (input.tool === "write") {
|
|
28
|
+
if (!pathString(metadata.filepath) || typeof metadata.exists !== "boolean") throw new Error("invalid write metadata");
|
|
29
|
+
return [{ path: metadata.filepath, operation: metadata.exists ? "update" : "add" }];
|
|
30
|
+
}
|
|
31
|
+
if (input.tool === "edit") {
|
|
32
|
+
if (!object(metadata.filediff) || !pathString(metadata.filediff.file)) throw new Error("invalid edit metadata");
|
|
33
|
+
return [{ path: metadata.filediff.file, operation: "update" }];
|
|
34
|
+
}
|
|
35
|
+
throw new Error("unsupported tool");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default async function WoodsHooks({ directory, worktree }) {
|
|
39
|
+
return {
|
|
40
|
+
"tool.execute.after": async (input, output) => {
|
|
41
|
+
if (!enabled() || !["apply_patch", "write", "edit"].includes(input?.tool)) return;
|
|
42
|
+
try {
|
|
43
|
+
if (!pathString(directory) || !pathString(worktree) || !path.isAbsolute(directory) || !path.isAbsolute(worktree)) {
|
|
44
|
+
throw new Error("invalid project context");
|
|
45
|
+
}
|
|
46
|
+
const relative = path.relative(worktree, directory);
|
|
47
|
+
if (relative === ".." || relative.startsWith("../") || path.isAbsolute(relative)) throw new Error("foreign project");
|
|
48
|
+
const payload = JSON.stringify({ version: 1, client: "opencode", root: directory, events: eventsFor(input, output) });
|
|
49
|
+
if (Buffer.byteLength(payload) > 49_152) throw new Error("event too large");
|
|
50
|
+
// The shared runner durably queues and owns its own deadline. Tool
|
|
51
|
+
// completion only hands off the event; it does not claim refresh success.
|
|
52
|
+
await new Promise((resolve, reject) => {
|
|
53
|
+
const child = spawn("bash", [runner, "opencode"], {
|
|
54
|
+
cwd: directory, detached: true, stdio: ["pipe", "ignore", "inherit"],
|
|
55
|
+
});
|
|
56
|
+
child.once("error", reject);
|
|
57
|
+
child.stdin.once("error", reject);
|
|
58
|
+
child.stdin.end(payload, () => { child.unref(); resolve(); });
|
|
59
|
+
});
|
|
60
|
+
} catch {
|
|
61
|
+
console.error("[Woods hooks] Unsupported, malformed or undeliverable OpenCode edit event; refresh not confirmed.");
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -1,226 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
|
|
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
|
|
2
|
+
# Claude PostToolUse adapter; the shared runner owns queueing and refresh.
|
|
3
|
+
exec "$BASH" "${BASH_SOURCE[0]%/*}/woods-refresh.sh" claude
|