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,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,25 @@ 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 available in Woods `2.0.0.beta3`. 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
|
+
|
|
23
|
+
For concurrent configuration operations, wait for the active operation to
|
|
24
|
+
finish and generate a fresh plan if the saved snapshots changed. Do not remove
|
|
25
|
+
its lock or overwrite the other application's entry. Coordination across
|
|
26
|
+
applications sharing user configuration is unreleased after `2.0.0.beta3`;
|
|
27
|
+
check the installed revision before relying on it.
|
|
28
|
+
|
|
10
29
|
## Preflight
|
|
11
30
|
|
|
12
31
|
Confirm Woods actually works before advertising it to every future session:
|