woods 2.0.0.beta2 → 2.0.0.beta3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +262 -1
- data/CONTRIBUTING.md +173 -9
- data/README.md +7 -3
- data/SECURITY.md +9 -6
- data/docs/AGENT_GUIDE.md +83 -4
- data/docs/AGENT_SETUP.md +82 -1
- data/docs/BACKEND_MATRIX.md +20 -0
- data/docs/CLIENT_HOOKS.md +111 -0
- data/docs/CONFIGURATION_REFERENCE.md +199 -14
- data/docs/CONSOLE_MCP_SETUP.md +35 -5
- data/docs/DOCKER_SETUP.md +21 -2
- data/docs/EVALUATION.md +464 -1
- data/docs/EXTRACTOR_REFERENCE.md +36 -5
- data/docs/FAQ.md +11 -12
- data/docs/GETTING_STARTED.md +17 -5
- data/docs/INCREMENTAL_EXTRACTION.md +117 -1
- data/docs/INDEX_LAYOUT.md +382 -0
- data/docs/INTERNALS.md +7 -2
- data/docs/MCP_SERVERS.md +221 -5
- data/docs/MCP_TOOL_COOKBOOK.md +33 -18
- data/docs/NOTION_INTEGRATION.md +13 -0
- data/docs/OBSIDIAN_INTEGRATION.md +57 -9
- data/docs/PUBLISHED_INDEX.md +55 -0
- data/docs/README.md +7 -0
- data/docs/RETRIEVAL_GUIDE.md +253 -11
- data/docs/RUNTIME_TRACING.md +71 -0
- data/docs/SOURCE_FRESHNESS.md +143 -0
- data/docs/TROUBLESHOOTING.md +117 -5
- data/docs/UNBLOCKED_INTEGRATION.md +25 -0
- data/docs/UPGRADING_TO_2.md +44 -22
- data/docs/WATCH_DAEMON.md +259 -59
- data/exe/woods-agent-config +6 -0
- data/exe/woods-extract +5 -0
- data/exe/woods-hook-context +6 -0
- data/lib/generators/woods/templates/woods.rb.tt +1 -3
- data/lib/tasks/woods.rake +47 -397
- data/lib/woods/agent_configuration/applier.rb +133 -0
- data/lib/woods/agent_configuration/cli.rb +101 -0
- data/lib/woods/agent_configuration/cli_options.rb +29 -0
- data/lib/woods/agent_configuration/document.rb +105 -0
- data/lib/woods/agent_configuration/error.rb +7 -0
- data/lib/woods/agent_configuration/launcher.rb +75 -0
- data/lib/woods/agent_configuration/layout.rb +59 -0
- data/lib/woods/agent_configuration/managed_section.rb +62 -0
- data/lib/woods/agent_configuration/plan.rb +98 -0
- data/lib/woods/agent_configuration/plan_diff.rb +38 -0
- data/lib/woods/agent_configuration/planned_files.rb +61 -0
- data/lib/woods/agent_configuration/planner.rb +63 -0
- data/lib/woods/agent_configuration/planner_validation.rb +77 -0
- data/lib/woods/agent_configuration/preflight.rb +100 -0
- data/lib/woods/agent_configuration/recovery.rb +49 -0
- data/lib/woods/ast/node.rb +2 -0
- data/lib/woods/ast/parser.rb +38 -5
- data/lib/woods/builder.rb +21 -5
- data/lib/woods/cache/cache_middleware.rb +28 -7
- data/lib/woods/cache/cache_store.rb +4 -5
- data/lib/woods/change_set.rb +5 -4
- data/lib/woods/console/credential_index.rb +20 -2
- data/lib/woods/console/credential_scanner.rb +14 -14
- data/lib/woods/console/credential_scanner_registry.rb +36 -0
- data/lib/woods/console/embedded_executor.rb +1 -1
- data/lib/woods/console/encrypted_credential_snapshot.rb +16 -0
- data/lib/woods/console/rack_middleware.rb +22 -13
- data/lib/woods/console/server.rb +18 -16
- data/lib/woods/dependency_graph.rb +65 -13
- data/lib/woods/embedding/corpus.rb +94 -0
- data/lib/woods/embedding/indexer.rb +90 -46
- data/lib/woods/embedding/openai.rb +17 -6
- data/lib/woods/evaluation/ablation_executor.rb +6 -1
- data/lib/woods/evaluation/ablation_timed_executor.rb +22 -4
- data/lib/woods/export/typed_reader.rb +56 -0
- data/lib/woods/extractor.rb +232 -137
- data/lib/woods/extractors/action_cable_extractor.rb +3 -1
- data/lib/woods/extractors/behavioral_profile.rb +9 -7
- data/lib/woods/extractors/caching_extractor.rb +3 -1
- data/lib/woods/extractors/concern_extractor.rb +64 -6
- data/lib/woods/extractors/configuration_extractor.rb +7 -3
- data/lib/woods/extractors/controller_extractor.rb +13 -4
- data/lib/woods/extractors/database_view_extractor.rb +3 -1
- data/lib/woods/extractors/decorator_extractor.rb +3 -1
- data/lib/woods/extractors/engine_extractor.rb +3 -1
- data/lib/woods/extractors/event_extractor.rb +4 -2
- data/lib/woods/extractors/factory_extractor.rb +3 -1
- data/lib/woods/extractors/graphql_extractor.rb +8 -2
- data/lib/woods/extractors/i18n_extractor.rb +3 -1
- data/lib/woods/extractors/job_extractor.rb +6 -19
- data/lib/woods/extractors/lib_extractor.rb +3 -1
- data/lib/woods/extractors/mailer_extractor.rb +20 -5
- data/lib/woods/extractors/manager_extractor.rb +3 -1
- data/lib/woods/extractors/method_parameters.rb +53 -0
- data/lib/woods/extractors/middleware_argument.rb +65 -0
- data/lib/woods/extractors/middleware_extractor.rb +9 -3
- data/lib/woods/extractors/migration_extractor.rb +3 -1
- data/lib/woods/extractors/model_extractor.rb +39 -33
- data/lib/woods/extractors/package_extractor.rb +24 -4
- data/lib/woods/extractors/phlex_extractor.rb +3 -1
- data/lib/woods/extractors/policy_extractor.rb +3 -1
- data/lib/woods/extractors/poro_extractor.rb +3 -1
- data/lib/woods/extractors/pundit_extractor.rb +3 -1
- data/lib/woods/extractors/rails_source_extractor.rb +4 -2
- data/lib/woods/extractors/rake_task_extractor.rb +4 -2
- data/lib/woods/extractors/route_extractor.rb +3 -1
- data/lib/woods/extractors/route_helper_resolver.rb +10 -33
- data/lib/woods/extractors/scheduled_job_extractor.rb +41 -15
- data/lib/woods/extractors/serializer_extractor.rb +4 -2
- data/lib/woods/extractors/service_extractor.rb +3 -1
- data/lib/woods/extractors/shared_dependency_scanner.rb +2 -2
- data/lib/woods/extractors/shared_utility_methods.rb +27 -15
- data/lib/woods/extractors/source_nesting.rb +1 -1
- data/lib/woods/extractors/state_machine_extractor.rb +3 -1
- data/lib/woods/extractors/test_mapping_extractor.rb +3 -1
- data/lib/woods/extractors/validator_extractor.rb +3 -1
- data/lib/woods/extractors/view_component_extractor.rb +3 -1
- data/lib/woods/extractors/view_template_extractor.rb +3 -1
- data/lib/woods/gem_mapper.rb +2 -0
- data/lib/woods/git_history.rb +116 -0
- data/lib/woods/graph_analyzer.rb +35 -6
- data/lib/woods/hooks/context_cli.rb +54 -0
- data/lib/woods/hooks/context_event.rb +88 -0
- data/lib/woods/hooks/context_hint.rb +73 -0
- data/lib/woods/hooks/context_impact.rb +77 -0
- data/lib/woods/hooks/context_output.rb +47 -0
- data/lib/woods/hooks/context_state.rb +102 -0
- data/lib/woods/hooks/refresh.rb +79 -0
- data/lib/woods/hooks/rule_projection.rb +78 -0
- data/lib/woods/input_rules.rb +19 -0
- data/lib/woods/mcp/bearer_auth.rb +20 -12
- data/lib/woods/mcp/bootstrapper.rb +62 -0
- data/lib/woods/mcp/index_reader.rb +323 -160
- data/lib/woods/mcp/initialization_guidance.rb +27 -0
- data/lib/woods/mcp/origin_guard.rb +17 -9
- data/lib/woods/mcp/published_lexical_retriever.rb +115 -0
- data/lib/woods/mcp/renderers/markdown_renderer.rb +8 -1
- data/lib/woods/mcp/renderers/plain_renderer.rb +7 -1
- data/lib/woods/mcp/search_results.rb +74 -0
- data/lib/woods/mcp/server.rb +158 -37
- data/lib/woods/mcp/tool_contract.rb +2 -0
- data/lib/woods/mcp/tool_response_renderer.rb +25 -0
- data/lib/woods/mcp/traversal_evidence.rb +113 -0
- data/lib/woods/mcp/traversal_evidence_index.rb +100 -0
- data/lib/woods/mcp/traversal_evidence_page.rb +41 -0
- data/lib/woods/mcp/traversal_evidence_text.rb +52 -0
- data/lib/woods/notion/exporter.rb +56 -17
- data/lib/woods/obsidian/destination_plan.rb +98 -0
- data/lib/woods/obsidian/name_mapper.rb +19 -3
- data/lib/woods/obsidian/note_builder.rb +19 -10
- data/lib/woods/obsidian/vault_exporter.rb +88 -32
- data/lib/woods/operator/pipeline_guard.rb +18 -13
- data/lib/woods/path_dispatcher.rb +7 -1
- data/lib/woods/payload_store.rb +27 -26
- data/lib/woods/railtie.rb +3 -3
- data/lib/woods/railtie_support.rb +12 -12
- data/lib/woods/rake_helpers.rb +392 -0
- data/lib/woods/resilience/graph_invariant_validator/membership_checks.rb +71 -0
- data/lib/woods/resilience/graph_invariant_validator/node_checks.rb +61 -0
- data/lib/woods/resilience/graph_invariant_validator/reverse_relationship_checks.rb +46 -0
- data/lib/woods/resilience/graph_invariant_validator.rb +119 -0
- data/lib/woods/resilience/index_validator/graph_checks.rb +80 -0
- data/lib/woods/resilience/index_validator.rb +112 -23
- data/lib/woods/retrieval/context_assembler.rb +50 -15
- data/lib/woods/retrieval/lexical_assembler.rb +73 -0
- data/lib/woods/retrieval/lexical_index.rb +119 -0
- data/lib/woods/retrieval/ranker.rb +4 -2
- data/lib/woods/retrieval/scope.rb +108 -0
- data/lib/woods/retrieval/scoped_graph_store.rb +32 -0
- data/lib/woods/retrieval/scoped_vector_store.rb +55 -0
- data/lib/woods/retrieval/search_executor.rb +86 -27
- data/lib/woods/retrieval/source_evidence.rb +200 -0
- data/lib/woods/retriever.rb +98 -22
- data/lib/woods/ruby_analyzer/trace_enricher.rb +77 -38
- data/lib/woods/session_tracer/middleware.rb +10 -12
- data/lib/woods/session_tracer/redis_store.rb +22 -6
- data/lib/woods/session_tracer/session_flow_assembler.rb +23 -17
- data/lib/woods/session_tracer/solid_cache_coordination.rb +6 -4
- data/lib/woods/session_tracer/unit_resolver.rb +63 -0
- data/lib/woods/source_inputs/consumer_errors.rb +27 -0
- data/lib/woods/source_inputs/handoff.rb +102 -0
- data/lib/woods/source_inputs/launcher.rb +157 -0
- data/lib/woods/source_inputs/manifest.rb +124 -0
- data/lib/woods/source_inputs/private_key.rb +55 -0
- data/lib/woods/source_inputs/scanner.rb +171 -0
- data/lib/woods/source_inputs/scopes.rb +71 -0
- data/lib/woods/source_inputs/session.rb +214 -0
- data/lib/woods/source_inputs/status.rb +84 -0
- data/lib/woods/source_inputs/verifier.rb +107 -0
- data/lib/woods/storage/metadata_store.rb +25 -25
- data/lib/woods/storage/pgvector.rb +29 -8
- data/lib/woods/storage/qdrant.rb +17 -7
- data/lib/woods/storage/vector_store.rb +18 -6
- data/lib/woods/tasks.rb +3 -2
- data/lib/woods/temporal/json_snapshot_store.rb +29 -8
- data/lib/woods/unblocked/exporter.rb +59 -70
- data/lib/woods/version.rb +1 -1
- data/lib/woods/watch/boot_snapshot.rb +52 -0
- data/lib/woods/watch/daemon.rb +136 -28
- data/lib/woods/watch/listen_watcher.rb +4 -0
- data/lib/woods/watch/polling_watcher.rb +5 -1
- data/lib/woods/watch/status.rb +20 -15
- data/lib/woods/watch/tree_scan.rb +21 -13
- data/lib/woods/watch/watcher.rb +4 -1
- data/lib/woods.rb +50 -11
- data/plugin/.claude-plugin/plugin.json +1 -1
- data/plugin/hooks/adapters/normalize.jq +15 -0
- data/plugin/hooks/adapters/normalize.rb +63 -0
- data/plugin/hooks/hooks.json +20 -0
- data/plugin/hooks/woods-context.sh +50 -0
- data/plugin/hooks/woods-input-rules.sh +159 -0
- data/plugin/hooks/woods-opencode.mjs +65 -0
- data/plugin/hooks/woods-post-edit.sh +2 -225
- data/plugin/hooks/woods-refresh.sh +260 -0
- data/plugin/hooks/woods-session-start.sh +47 -55
- data/plugin/skills/woods-agent-enable/SKILL.md +13 -0
- data/plugin/skills/woods-diagnose/SKILL.md +288 -1
- data/plugin/skills/woods-investigate/SKILL.md +106 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +89 -1
- data/plugin/skills/woods-setup/SKILL.md +107 -6
- metadata +84 -5
|
@@ -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
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Opt-in refresh dispatcher. Each edit is durable until its task succeeds.
|
|
3
|
+
# See docs/WATCH_DAEMON.md for retry, daemon deferral and Docker transport.
|
|
4
|
+
set -u
|
|
5
|
+
umask 077
|
|
6
|
+
[ "${WOODS_HOOKS_DISABLED:-0}" = 1 ] && exit 0
|
|
7
|
+
[ "${WOODS_HOOKS_ENABLED:-0}" = 1 ] || exit 0
|
|
8
|
+
|
|
9
|
+
client="${1:-}"
|
|
10
|
+
hook_dir="${BASH_SOURCE[0]%/*}"
|
|
11
|
+
# Preserve raw bytes until JSON validation: Bash command substitution silently
|
|
12
|
+
# removes NUL bytes and could turn a malformed path into a different valid one.
|
|
13
|
+
input_file="$(mktemp "${TMPDIR:-/tmp}/woods-hook-input.XXXXXX")" || exit 0
|
|
14
|
+
trap 'rm -f "$input_file"' EXIT
|
|
15
|
+
trap 'exit 143' TERM INT
|
|
16
|
+
head -c 1048577 >"$input_file" || exit 0
|
|
17
|
+
payload_bytes="$(wc -c <"$input_file")"
|
|
18
|
+
[ "$payload_bytes" -le 1048576 ] || { printf '[Woods hooks] Oversized edit event; no refresh queued.\n' >&2; exit 0; }
|
|
19
|
+
if command -v jq >/dev/null 2>&1; then
|
|
20
|
+
payload="$(jq -c --arg client "$client" -f "$hook_dir/adapters/normalize.jq" <"$input_file" 2>/dev/null)" || {
|
|
21
|
+
printf '[Woods hooks] Unsupported or malformed edit event; no refresh queued.\n' >&2; exit 0;
|
|
22
|
+
}
|
|
23
|
+
elif command -v ruby >/dev/null 2>&1; then
|
|
24
|
+
payload="$(ruby "$hook_dir/adapters/normalize.rb" "$client" <"$input_file")" || exit 0
|
|
25
|
+
else
|
|
26
|
+
printf '[Woods hooks] jq or Ruby is required; no refresh queued.\n' >&2
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
rm -f "$input_file"
|
|
30
|
+
trap - EXIT TERM INT
|
|
31
|
+
field() {
|
|
32
|
+
if command -v jq >/dev/null 2>&1; then
|
|
33
|
+
printf '%s' "$payload" | jq -ej '.root'
|
|
34
|
+
else
|
|
35
|
+
printf '%s' "$payload" | ruby -rjson -e 'print JSON.parse($stdin.read).fetch("root")'
|
|
36
|
+
fi
|
|
37
|
+
}
|
|
38
|
+
cwd="$(field; printf .)"
|
|
39
|
+
cwd="${cwd%.}"
|
|
40
|
+
cwd="${cwd%/}"
|
|
41
|
+
[ -n "$cwd" ] || exit 0
|
|
42
|
+
[ -d "$cwd" ] || exit 0
|
|
43
|
+
configured_output="${WOODS_OUTPUT:-tmp/woods}"
|
|
44
|
+
case "$configured_output" in /*) tmp_dir="$configured_output" ;; *) tmp_dir="$cwd/$configured_output" ;; esac
|
|
45
|
+
[ -f "$tmp_dir/generation.json" ] || exit 0
|
|
46
|
+
source "$hook_dir/woods-input-rules.sh" || exit 0
|
|
47
|
+
# Validate every member before publishing the event. Missing/deleted paths are
|
|
48
|
+
# allowed; symlink components are deliberately unsupported, including escapes.
|
|
49
|
+
event_fields() {
|
|
50
|
+
if command -v jq >/dev/null 2>&1; then
|
|
51
|
+
printf '%s' "$payload" | jq -j '.events[] | .operation,"\u0000",.path,"\u0000"'
|
|
52
|
+
else
|
|
53
|
+
printf '%s' "$payload" | ruby -rjson -e 'JSON.parse($stdin.read).fetch("events").each { |e| print e.fetch("operation"), "\0", e.fetch("path"), "\0" }'
|
|
54
|
+
fi
|
|
55
|
+
}
|
|
56
|
+
relevant=0
|
|
57
|
+
invalid=0
|
|
58
|
+
while IFS= read -r -d '' operation && IFS= read -r -d '' file; do
|
|
59
|
+
case "$file" in "$cwd"/*) rel="${file#"$cwd"/}" ;; /*) invalid=1; break ;; *) rel="$file" ;; esac
|
|
60
|
+
case "/$rel/" in */../*|*/./*|*//* ) invalid=1; break ;; esac
|
|
61
|
+
parent="$cwd/$rel"
|
|
62
|
+
while [ "$parent" != "$cwd" ]; do
|
|
63
|
+
if [ -L "$parent" ]; then invalid=1; break; fi
|
|
64
|
+
parent="${parent%/*}"
|
|
65
|
+
done
|
|
66
|
+
[ "$invalid" = 0 ] || break
|
|
67
|
+
[ "$(woods_input_action "$rel")" = ignore ] || relevant=1
|
|
68
|
+
done < <(event_fields)
|
|
69
|
+
if [ "$invalid" = 1 ]; then
|
|
70
|
+
printf '[Woods hooks] Foreign, escaping or symlinked edit path; no refresh queued.\n' >&2
|
|
71
|
+
exit 0
|
|
72
|
+
fi
|
|
73
|
+
[ "$relevant" = 1 ] || exit 0
|
|
74
|
+
|
|
75
|
+
log="$tmp_dir/hook.log"
|
|
76
|
+
queue="$tmp_dir/hook-pending"
|
|
77
|
+
run_lock="$tmp_dir/hook.lock"
|
|
78
|
+
run_lock_dir="$tmp_dir/hook.lock.d"
|
|
79
|
+
mkdir -p "$queue" || exit 0
|
|
80
|
+
# Files are immutable once published. A killed worker never destroys its batch.
|
|
81
|
+
enqueue() {
|
|
82
|
+
event="$queue/$$.$RANDOM.$RANDOM"
|
|
83
|
+
if command -v jq >/dev/null 2>&1; then
|
|
84
|
+
jq -nc --arg path "$1" '{path:$path,operation:"update"}' >"$event.tmp" || return 1
|
|
85
|
+
else
|
|
86
|
+
ruby -rjson -e 'puts JSON.generate(path: ARGV[0], operation: "update")' -- "$1" >"$event.tmp" || return 1
|
|
87
|
+
fi
|
|
88
|
+
mv "$event.tmp" "$event.json"
|
|
89
|
+
}
|
|
90
|
+
# One immutable file keeps a multi-file event indivisible across crashes.
|
|
91
|
+
event="$queue/$$.$RANDOM.$RANDOM"
|
|
92
|
+
if command -v jq >/dev/null 2>&1; then
|
|
93
|
+
printf '%s' "$payload" | jq -c --arg root "$cwd" '.events | map(.path |= (if startswith($root + "/") then ltrimstr($root + "/") else . end))' >"$event.tmp" || exit 0
|
|
94
|
+
else
|
|
95
|
+
printf '%s' "$payload" | ruby -rjson -e 'v = JSON.parse($stdin.read); puts JSON.generate(v.fetch("events").map { |e| e.merge("path" => e.fetch("path").delete_prefix(ARGV.fetch(0) + "/")) })' -- "$cwd" >"$event.tmp" || exit 0
|
|
96
|
+
fi
|
|
97
|
+
mv "$event.tmp" "$event.json" || exit 0
|
|
98
|
+
|
|
99
|
+
# A private deadline applies even when the client backgrounds async hooks.
|
|
100
|
+
budget="${WOODS_HOOK_TIMEOUT_SECONDS:-600}"
|
|
101
|
+
case "$budget" in ''|*[!0-9]*|0) printf 'Invalid WOODS_HOOK_TIMEOUT_SECONDS; queued work retained.\n' >>"$log"; exit 0 ;; esac
|
|
102
|
+
[ "$budget" -le 3600 ] || exit 0
|
|
103
|
+
started=$SECONDS
|
|
104
|
+
have_flock() { command -v flock >/dev/null 2>&1; }
|
|
105
|
+
acquire() {
|
|
106
|
+
if have_flock; then
|
|
107
|
+
exec 8>"$run_lock"
|
|
108
|
+
flock -n 8
|
|
109
|
+
else
|
|
110
|
+
if ! mkdir "$run_lock_dir" 2>/dev/null; then
|
|
111
|
+
owners=("$run_lock_dir"/owner-*)
|
|
112
|
+
[ "${#owners[@]}" -le 1 ] || return 1
|
|
113
|
+
if [ "${#owners[@]}" -eq 1 ]; then
|
|
114
|
+
owner="${owners[0]##*/owner-}"
|
|
115
|
+
# Never steal a live owner's lease solely because its mtime is old.
|
|
116
|
+
case "$owner" in *[!0-9]*) return 1 ;; esac
|
|
117
|
+
kill -0 "$owner" 2>/dev/null && return 1
|
|
118
|
+
# Only the reclaimer that removes this marker may replace the directory.
|
|
119
|
+
# Another reclaimer may already have created a new, still-empty lock.
|
|
120
|
+
rm "${owners[0]}" 2>/dev/null || return 1
|
|
121
|
+
rmdir "$run_lock_dir" 2>/dev/null || return 1
|
|
122
|
+
else
|
|
123
|
+
# Compatibility recovery for an old empty mkdir lock after a crash.
|
|
124
|
+
mtime="$(stat -c %Y "$run_lock_dir" 2>/dev/null || stat -f %m "$run_lock_dir" 2>/dev/null)"
|
|
125
|
+
[ -n "$mtime" ] || return 1
|
|
126
|
+
now="$(date +%s)"
|
|
127
|
+
[ "$((now - mtime))" -gt "${WOODS_HOOK_LOCK_STALE_SECONDS:-1800}" ] || return 1
|
|
128
|
+
rmdir "$run_lock_dir" 2>/dev/null || return 1
|
|
129
|
+
fi
|
|
130
|
+
mkdir "$run_lock_dir" 2>/dev/null || return 1
|
|
131
|
+
fi
|
|
132
|
+
: >"$run_lock_dir/owner-$$" 2>/dev/null || return 1
|
|
133
|
+
owners=("$run_lock_dir"/owner-*)
|
|
134
|
+
if [ "${#owners[@]}" -ne 1 ]; then
|
|
135
|
+
rm -f "$run_lock_dir/owner-$$"
|
|
136
|
+
return 1
|
|
137
|
+
fi
|
|
138
|
+
fi
|
|
139
|
+
}
|
|
140
|
+
release() {
|
|
141
|
+
if have_flock; then flock -u 8; exec 8>&-; else rm -f "$run_lock_dir/owner-$$"; rmdir "$run_lock_dir" 2>/dev/null || true; fi
|
|
142
|
+
}
|
|
143
|
+
encode_batch() {
|
|
144
|
+
if command -v jq >/dev/null 2>&1; then
|
|
145
|
+
jq -sc --arg output "$configured_output" '{version:1,output:$output,events:(map(if type == "array" then . else [.] end) | add)} | @base64' "${batch[@]}" | tr -d '"\n'
|
|
146
|
+
else
|
|
147
|
+
ruby -rjson -rbase64 -e '
|
|
148
|
+
output = ARGV.shift
|
|
149
|
+
print Base64.strict_encode64(JSON.generate(version: 1, output: output,
|
|
150
|
+
events: ARGV.flat_map { |path| value = JSON.parse(File.read(path)); value.is_a?(Array) ? value : [value] }))
|
|
151
|
+
' -- "$configured_output" "${batch[@]}"
|
|
152
|
+
fi
|
|
153
|
+
}
|
|
154
|
+
run_batch() {
|
|
155
|
+
encoded="$(encode_batch)" || return 1
|
|
156
|
+
[ -n "$encoded" ] || return 1
|
|
157
|
+
remaining=$((budget - SECONDS + started))
|
|
158
|
+
[ "$remaining" -gt 0 ] || return 124
|
|
159
|
+
rake="${WOODS_HOOK_RAKE:-bundle exec rake}"
|
|
160
|
+
# Job control gives this command and its descendants a dedicated process group.
|
|
161
|
+
# The task argument crosses Docker exec without requiring env forwarding or a
|
|
162
|
+
# host-only queue path to exist inside the application container.
|
|
163
|
+
set -m
|
|
164
|
+
( exec 8>&-; cd "$cwd" || exit 1; exec $rake "woods:hook_refresh[$encoded]" ) >>"$log" 2>&1 &
|
|
165
|
+
runner=$!
|
|
166
|
+
( exec 8>&-; sleep "$remaining"; kill -TERM -- "-$runner" 2>/dev/null; sleep 1; kill -KILL -- "-$runner" 2>/dev/null ) >/dev/null 2>&1 &
|
|
167
|
+
timer=$!
|
|
168
|
+
wait "$runner" 2>/dev/null
|
|
169
|
+
result=$?
|
|
170
|
+
kill -KILL -- "-$runner" 2>/dev/null || true
|
|
171
|
+
runner=""
|
|
172
|
+
kill -TERM -- "-$timer" 2>/dev/null || true
|
|
173
|
+
wait "$timer" 2>/dev/null || true
|
|
174
|
+
timer=""
|
|
175
|
+
set +m
|
|
176
|
+
return "$result"
|
|
177
|
+
}
|
|
178
|
+
cleanup() {
|
|
179
|
+
[ -z "${runner:-}" ] || kill -KILL -- "-$runner" 2>/dev/null || true
|
|
180
|
+
[ -z "${timer:-}" ] || kill -TERM -- "-$timer" 2>/dev/null || true
|
|
181
|
+
if [ "${legacy_directory_owned:-0}" = 1 ]; then
|
|
182
|
+
rmdir "$tmp_dir/hook-pending.lock.d" 2>/dev/null || true
|
|
183
|
+
fi
|
|
184
|
+
release
|
|
185
|
+
}
|
|
186
|
+
shopt -s nullglob
|
|
187
|
+
acquire || exit 0
|
|
188
|
+
trap 'exit 143' TERM INT
|
|
189
|
+
trap 'cleanup' EXIT
|
|
190
|
+
# Upgrade recovery: import the previous newline queue before retiring it.
|
|
191
|
+
# A crash during import can duplicate an event but cannot erase its obligation.
|
|
192
|
+
legacy_pending="$tmp_dir/hook-pending.txt"
|
|
193
|
+
if [ -s "$legacy_pending" ]; then
|
|
194
|
+
if have_flock; then
|
|
195
|
+
exec 7>"$tmp_dir/hook-pending.lock"
|
|
196
|
+
flock -n 7 || exit 0
|
|
197
|
+
else
|
|
198
|
+
if ! mkdir "$tmp_dir/hook-pending.lock.d" 2>/dev/null; then
|
|
199
|
+
printf 'Legacy queue import deferred: hook-pending.lock.d is busy; both queues retained.\n' >>"$log"
|
|
200
|
+
exit 0
|
|
201
|
+
fi
|
|
202
|
+
legacy_directory_owned=1
|
|
203
|
+
fi
|
|
204
|
+
while IFS= read -r legacy_path || [ -n "$legacy_path" ]; do
|
|
205
|
+
[ "$((SECONDS - started))" -lt "$budget" ] || exit 0
|
|
206
|
+
[ -n "$legacy_path" ] || continue
|
|
207
|
+
enqueue "$legacy_path" || exit 0
|
|
208
|
+
done <"$legacy_pending"
|
|
209
|
+
rm -f "$legacy_pending"
|
|
210
|
+
if have_flock; then
|
|
211
|
+
flock -u 7
|
|
212
|
+
exec 7>&-
|
|
213
|
+
else
|
|
214
|
+
rmdir "$tmp_dir/hook-pending.lock.d" 2>/dev/null || true
|
|
215
|
+
legacy_directory_owned=0
|
|
216
|
+
fi
|
|
217
|
+
fi
|
|
218
|
+
while :; do
|
|
219
|
+
batch=("$queue"/*.json)
|
|
220
|
+
if [ "${#batch[@]}" -eq 0 ]; then
|
|
221
|
+
# Release before checking again: an arrival either becomes our next batch
|
|
222
|
+
# or acquires ownership itself. There is no last-drain/release gap.
|
|
223
|
+
release
|
|
224
|
+
trap - EXIT
|
|
225
|
+
batch=("$queue"/*.json)
|
|
226
|
+
[ "${#batch[@]}" -gt 0 ] || break
|
|
227
|
+
acquire || break
|
|
228
|
+
trap 'cleanup' EXIT
|
|
229
|
+
continue
|
|
230
|
+
fi
|
|
231
|
+
candidates=("${batch[@]:0:16}")
|
|
232
|
+
batch=()
|
|
233
|
+
batch_bytes=0
|
|
234
|
+
batch_events=0
|
|
235
|
+
for candidate in "${candidates[@]}"; do
|
|
236
|
+
event_bytes="$(wc -c <"$candidate")"
|
|
237
|
+
if command -v jq >/dev/null 2>&1; then
|
|
238
|
+
event_count="$(jq 'if type == "array" then length else 1 end' "$candidate")" || break
|
|
239
|
+
else
|
|
240
|
+
event_count="$(ruby -rjson -e 'v = JSON.parse(File.read(ARGV[0])); puts(v.is_a?(Array) ? v.size : 1)' "$candidate")" || break
|
|
241
|
+
fi
|
|
242
|
+
[ "$((batch_events + event_count))" -le 1000 ] || break
|
|
243
|
+
[ "$((batch_bytes + event_bytes))" -le 49152 ] || break
|
|
244
|
+
batch+=("$candidate")
|
|
245
|
+
batch_bytes=$((batch_bytes + event_bytes))
|
|
246
|
+
batch_events=$((batch_events + event_count))
|
|
247
|
+
done
|
|
248
|
+
if [ "${#batch[@]}" -eq 0 ]; then
|
|
249
|
+
printf 'Oversized hook event; pending work retained for inspection.\n' >>"$log"
|
|
250
|
+
break
|
|
251
|
+
fi
|
|
252
|
+
if run_batch; then
|
|
253
|
+
rm -f "${batch[@]}"
|
|
254
|
+
else
|
|
255
|
+
result=$?
|
|
256
|
+
printf 'Woods hook deferred/failed (status %s); pending events retained in %s. Retry on the next edit or invoke the hook again after resolving the cause.\n' "$result" "$queue" >>"$log"
|
|
257
|
+
break
|
|
258
|
+
fi
|
|
259
|
+
done
|
|
260
|
+
exit 0
|
|
@@ -1,77 +1,69 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
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.
|
|
2
|
+
# Opt-in, bounded source-content verification through the application's rake
|
|
3
|
+
# command. The status task has no Rails environment prerequisite; Docker-only
|
|
4
|
+
# installations do not need the Woods gem or an application bundle on the host.
|
|
18
5
|
set -u
|
|
19
6
|
|
|
20
7
|
[ "${WOODS_HOOKS_DISABLED:-0}" = "1" ] && exit 0
|
|
21
8
|
[ "${WOODS_HOOKS_ENABLED:-0}" = "1" ] || exit 0
|
|
22
|
-
|
|
23
9
|
payload="$(cat)"
|
|
24
|
-
|
|
25
10
|
field() {
|
|
26
11
|
if command -v jq >/dev/null 2>&1; then
|
|
27
|
-
printf '%s' "$
|
|
12
|
+
printf '%s' "$1" | jq -r --arg name "$2" '.[$name] // empty' 2>/dev/null
|
|
28
13
|
elif command -v ruby >/dev/null 2>&1; then
|
|
29
|
-
printf '%s' "$
|
|
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 ''
|
|
14
|
+
printf '%s' "$1" | ruby -rjson -e 'print JSON.parse($stdin.read).fetch(ARGV[0], "")' -- "$2" 2>/dev/null
|
|
35
15
|
fi
|
|
36
16
|
}
|
|
37
|
-
|
|
38
|
-
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.
|
|
17
|
+
cwd="$(field "$payload" cwd)"
|
|
18
|
+
[ -n "$cwd" ] && [ -d "$cwd" ] || exit 0
|
|
43
19
|
configured_output="${WOODS_OUTPUT:-tmp/woods}"
|
|
44
20
|
case "$configured_output" in
|
|
45
|
-
/*)
|
|
46
|
-
*)
|
|
21
|
+
/*) output_dir="$configured_output" ;;
|
|
22
|
+
*) output_dir="$cwd/$configured_output" ;;
|
|
47
23
|
esac
|
|
48
|
-
|
|
49
|
-
marker="$tmp_dir/generation.json"
|
|
50
|
-
[ -f "$marker" ] || exit 0
|
|
51
|
-
|
|
24
|
+
[ -f "$output_dir/generation.json" ] || exit 0
|
|
52
25
|
if command -v jq >/dev/null 2>&1; then
|
|
53
|
-
|
|
54
|
-
number="$(jq -r '.number // empty' "$marker")"
|
|
26
|
+
encoded="$(jq -nr --arg output "$configured_output" '{output:$output,mode:"quick"} | @base64')"
|
|
55
27
|
elif command -v ruby >/dev/null 2>&1; then
|
|
56
|
-
|
|
57
|
-
number="$(ruby -rjson -e 'print JSON.parse(File.read(ARGV[0]))["number"].to_s' -- "$marker")"
|
|
28
|
+
encoded="$(ruby -rjson -rbase64 -e 'print Base64.strict_encode64(JSON.generate(output: ARGV[0], mode: "quick"))' -- "$configured_output")"
|
|
58
29
|
else
|
|
59
30
|
exit 0
|
|
60
31
|
fi
|
|
61
|
-
[ -z "$updated" ] && exit 0
|
|
62
32
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
33
|
+
record="$(mktemp "${TMPDIR:-/tmp}/woods-source-status.XXXXXX")" || exit 0
|
|
34
|
+
runner=""; timer=""
|
|
35
|
+
cleanup() {
|
|
36
|
+
[ -z "$runner" ] || kill -KILL -- "-$runner" 2>/dev/null || true
|
|
37
|
+
[ -z "$timer" ] || kill -TERM -- "-$timer" 2>/dev/null || true
|
|
38
|
+
rm -f "$record"
|
|
39
|
+
}
|
|
40
|
+
trap cleanup EXIT
|
|
41
|
+
trap 'exit 0' INT TERM
|
|
42
|
+
# Ten seconds includes command startup; the shared content verifier itself has
|
|
43
|
+
# a 250ms scan budget. A client hook timeout alone is not a process deadline.
|
|
44
|
+
set -m
|
|
45
|
+
rake="${WOODS_HOOK_RAKE:-bundle exec rake}"
|
|
46
|
+
( cd "$cwd" || exit 1; exec $rake "woods:source_status[$encoded]" ) >"$record" 2>/dev/null &
|
|
47
|
+
runner=$!
|
|
48
|
+
( sleep 10; kill -TERM -- "-$runner" 2>/dev/null; sleep 1; kill -KILL -- "-$runner" 2>/dev/null ) >/dev/null 2>&1 &
|
|
49
|
+
timer=$!
|
|
50
|
+
wait "$runner" 2>/dev/null
|
|
51
|
+
result=$?
|
|
52
|
+
kill -KILL -- "-$runner" 2>/dev/null || true
|
|
53
|
+
runner=""
|
|
54
|
+
kill -TERM -- "-$timer" 2>/dev/null || true
|
|
55
|
+
wait "$timer" 2>/dev/null || true
|
|
56
|
+
timer=""
|
|
57
|
+
set +m
|
|
58
|
+
if [ "$result" -ne 0 ]; then
|
|
59
|
+
echo 'Woods source freshness is unknown: source verification was unavailable or exceeded its deadline. Inspect woods_status; use woods-extract full for a fresh capture.'
|
|
60
|
+
exit 0
|
|
76
61
|
fi
|
|
62
|
+
status="$(tail -n 1 "$record")"
|
|
63
|
+
state="$(field "$status" state)"
|
|
64
|
+
case "$state" in
|
|
65
|
+
current) ;;
|
|
66
|
+
drifted) echo 'Woods source freshness is drifted: indexed application inputs differ from the working tree. Run woods-extract full, or inspect woods_status before choosing a targeted refresh.' ;;
|
|
67
|
+
*) echo 'Woods source freshness is unknown: this generation lacks complete verified source evidence. Inspect woods_status; use woods-extract full for a fresh capture.' ;;
|
|
68
|
+
esac
|
|
77
69
|
exit 0
|
|
@@ -7,6 +7,19 @@ description: Make a repository's coding agents use the Woods index by default
|
|
|
7
7
|
|
|
8
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
9
|
|
|
10
|
+
## Managed configuration availability
|
|
11
|
+
|
|
12
|
+
`woods-agent-config` (#407) is unreleased after `2.0.0.beta2`. First record the
|
|
13
|
+
installed version and test `bundle exec woods-agent-config --help` in the
|
|
14
|
+
selected application bundle. When supported, use its saved setup/update/remove
|
|
15
|
+
plan and explicit client/scope/root selection; apply the reviewed plan within
|
|
16
|
+
the user's existing authorization. Do not infer ownership from a server name
|
|
17
|
+
or repair edited managed sections by overwriting them. Plans and recovery
|
|
18
|
+
journals contain private configuration bytes. See the canonical
|
|
19
|
+
[managed configuration runbook](https://github.com/lost-in-the/woods/blob/main/docs/AGENT_SETUP.md#managed-claude-code-configuration)
|
|
20
|
+
for host/Compose preflight, actual Claude file locations, conflict recovery,
|
|
21
|
+
and removal. Preserve manual setup for older installed versions.
|
|
22
|
+
|
|
10
23
|
## Preflight
|
|
11
24
|
|
|
12
25
|
Confirm Woods actually works before advertising it to every future session:
|