woods 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +94 -7
  3. data/CONTRIBUTING.md +134 -19
  4. data/README.md +1 -1
  5. data/docs/AGENT_GUIDE.md +19 -0
  6. data/docs/AGENT_SETUP.md +22 -2
  7. data/docs/BACKEND_MATRIX.md +7 -0
  8. data/docs/CLIENT_HOOKS.md +6 -0
  9. data/docs/CONFIGURATION_REFERENCE.md +133 -25
  10. data/docs/CONSOLE_MCP_SETUP.md +82 -30
  11. data/docs/EMBEDDING_MODELS.md +16 -19
  12. data/docs/EXTRACTOR_REFERENCE.md +219 -21
  13. data/docs/FAQ.md +11 -25
  14. data/docs/GETTING_STARTED.md +7 -1
  15. data/docs/INCREMENTAL_EXTRACTION.md +261 -19
  16. data/docs/INDEX_LAYOUT.md +5 -0
  17. data/docs/INTERNALS.md +9 -0
  18. data/docs/MCP_HTTP_TRANSPORT.md +20 -15
  19. data/docs/MCP_SERVERS.md +87 -8
  20. data/docs/MCP_TOOL_COOKBOOK.md +13 -55
  21. data/docs/NOTION_INTEGRATION.md +7 -1
  22. data/docs/PUBLISHED_INDEX.md +6 -0
  23. data/docs/README.md +6 -1
  24. data/docs/RETRIEVAL_GUIDE.md +17 -0
  25. data/docs/SOURCE_FRESHNESS.md +157 -5
  26. data/docs/TOKEN_BENCHMARK.md +10 -18
  27. data/docs/TROUBLESHOOTING.md +70 -14
  28. data/docs/UNBLOCKED_INTEGRATION.md +60 -8
  29. data/docs/UPGRADING_TO_2.md +153 -38
  30. data/docs/WATCH_DAEMON.md +97 -14
  31. data/exe/woods-console-mcp +2 -2
  32. data/lib/generators/woods/templates/woods.rb.tt +2 -1
  33. data/lib/tasks/woods.rake +23 -7
  34. data/lib/tasks/woods_checks.rake +2 -2
  35. data/lib/woods/agent_configuration/cli.rb +1 -1
  36. data/lib/woods/agent_configuration/layout.rb +16 -2
  37. data/lib/woods/agent_configuration/plan.rb +13 -3
  38. data/lib/woods/agent_configuration/planner_validation.rb +4 -2
  39. data/lib/woods/agent_configuration/preflight.rb +5 -3
  40. data/lib/woods/builder.rb +17 -57
  41. data/lib/woods/cache/cache_middleware.rb +56 -30
  42. data/lib/woods/chunking/contributor_chunks.rb +119 -0
  43. data/lib/woods/chunking/semantic_chunker.rb +44 -21
  44. data/lib/woods/console/connection_manager.rb +56 -3
  45. data/lib/woods/console/embedded_executor.rb +30 -5
  46. data/lib/woods/console/rack_middleware.rb +29 -1
  47. data/lib/woods/dependency_graph.rb +34 -10
  48. data/lib/woods/embedding/fake.rb +12 -0
  49. data/lib/woods/embedding/indexer.rb +195 -98
  50. data/lib/woods/embedding/input_budget.rb +67 -0
  51. data/lib/woods/embedding/openai.rb +70 -20
  52. data/lib/woods/embedding/provider.rb +37 -25
  53. data/lib/woods/embedding/text_preparer.rb +76 -32
  54. data/lib/woods/embedding/token_counter.rb +18 -81
  55. data/lib/woods/embedding/vector_configuration.rb +48 -0
  56. data/lib/woods/extraction_identities.rb +175 -0
  57. data/lib/woods/extractor.rb +304 -107
  58. data/lib/woods/extractors/action_cable_extractor.rb +8 -3
  59. data/lib/woods/extractors/assigned_value_discovery.rb +74 -0
  60. data/lib/woods/extractors/class_declarations.rb +121 -0
  61. data/lib/woods/extractors/configuration_extractor.rb +11 -3
  62. data/lib/woods/extractors/declaration_ancestry.rb +92 -0
  63. data/lib/woods/extractors/event_extractor.rb +8 -0
  64. data/lib/woods/extractors/graphql_extractor.rb +134 -77
  65. data/lib/woods/extractors/job_extractor.rb +5 -1
  66. data/lib/woods/extractors/lib_extractor.rb +132 -15
  67. data/lib/woods/extractors/mailer_extractor.rb +3 -5
  68. data/lib/woods/extractors/manager_extractor.rb +7 -21
  69. data/lib/woods/extractors/migration_declaration.rb +87 -0
  70. data/lib/woods/extractors/migration_extractor.rb +5 -39
  71. data/lib/woods/extractors/phlex_extractor.rb +6 -2
  72. data/lib/woods/extractors/policy_extractor.rb +9 -5
  73. data/lib/woods/extractors/poro_extractor.rb +112 -53
  74. data/lib/woods/extractors/pundit_extractor.rb +11 -6
  75. data/lib/woods/extractors/scheduled_job_extractor.rb +45 -4
  76. data/lib/woods/extractors/serializer_extractor.rb +34 -22
  77. data/lib/woods/extractors/shared_utility_methods.rb +18 -1
  78. data/lib/woods/extractors/source_nesting.rb +142 -106
  79. data/lib/woods/extractors/standalone_module_discovery.rb +123 -0
  80. data/lib/woods/extractors/state_machine_extractor.rb +46 -40
  81. data/lib/woods/extractors/view_component_extractor.rb +9 -7
  82. data/lib/woods/flow_assembler.rb +4 -1
  83. data/lib/woods/generation.rb +25 -0
  84. data/lib/woods/hooks/context_hint.rb +7 -2
  85. data/lib/woods/mcp/bootstrapper.rb +33 -7
  86. data/lib/woods/mcp/config_resolver.rb +26 -7
  87. data/lib/woods/mcp/index_reader.rb +125 -24
  88. data/lib/woods/mcp/index_reader_pinning.rb +16 -0
  89. data/lib/woods/mcp/renderers/markdown_renderer.rb +7 -1
  90. data/lib/woods/mcp/renderers/plain_renderer.rb +3 -1
  91. data/lib/woods/mcp/search_results.rb +7 -1
  92. data/lib/woods/mcp/server.rb +24 -4
  93. data/lib/woods/module_reconciliation.rb +151 -0
  94. data/lib/woods/path_dispatcher.rb +7 -2
  95. data/lib/woods/rake_helpers.rb +43 -11
  96. data/lib/woods/release.rb +1 -1
  97. data/lib/woods/resilience/index_validator.rb +8 -3
  98. data/lib/woods/resilience/retryable_provider.rb +18 -1
  99. data/lib/woods/resolved_config.rb +68 -8
  100. data/lib/woods/retrieval/context_assembler.rb +3 -3
  101. data/lib/woods/retrieval/lexical_assembler.rb +3 -2
  102. data/lib/woods/retrieval/scope.rb +18 -2
  103. data/lib/woods/retrieval/source_evidence.rb +14 -2
  104. data/lib/woods/source_contributor_validation.rb +78 -0
  105. data/lib/woods/source_contributors.rb +116 -0
  106. data/lib/woods/source_inputs/handoff.rb +37 -0
  107. data/lib/woods/source_inputs/launcher.rb +53 -13
  108. data/lib/woods/source_inputs/manifest.rb +84 -3
  109. data/lib/woods/source_inputs/private_key.rb +44 -12
  110. data/lib/woods/source_inputs/scanner.rb +98 -27
  111. data/lib/woods/source_inputs/scopes.rb +1 -1
  112. data/lib/woods/source_inputs/session.rb +147 -15
  113. data/lib/woods/source_inputs/stable_reader.rb +127 -0
  114. data/lib/woods/source_inputs/status.rb +40 -8
  115. data/lib/woods/source_inputs/verifier.rb +28 -5
  116. data/lib/woods/source_path_encoding.rb +33 -0
  117. data/lib/woods/source_references/cache.rb +284 -0
  118. data/lib/woods/source_references/collector.rb +120 -0
  119. data/lib/woods/source_references/extraction.rb +185 -0
  120. data/lib/woods/source_references/inputs.rb +134 -0
  121. data/lib/woods/source_references/parser_adapter.rb +134 -0
  122. data/lib/woods/source_references/pass.rb +152 -0
  123. data/lib/woods/source_references/prism_adapter.rb +116 -0
  124. data/lib/woods/source_references/registry.rb +178 -0
  125. data/lib/woods/source_references/runtime_lookup.rb +127 -0
  126. data/lib/woods/source_references/value_class.rb +82 -0
  127. data/lib/woods/storage/metadata_store.rb +4 -1
  128. data/lib/woods/storage/qdrant.rb +2 -2
  129. data/lib/woods/unblocked/client.rb +12 -7
  130. data/lib/woods/unblocked/document_builder.rb +4 -1
  131. data/lib/woods/unblocked/exporter.rb +127 -37
  132. data/lib/woods/unblocked/sync_manifest.rb +137 -21
  133. data/lib/woods/unblocked/uri_migration.rb +105 -0
  134. data/lib/woods/util/host_guard.rb +3 -2
  135. data/lib/woods/version.rb +1 -1
  136. data/lib/woods/watch/catch_up.rb +138 -0
  137. data/lib/woods/watch/claim_lease.rb +150 -0
  138. data/lib/woods/watch/cli.rb +26 -2
  139. data/lib/woods/watch/daemon.rb +80 -59
  140. data/lib/woods/watch/installation/options.rb +1 -1
  141. data/lib/woods/watch/installation/receipt.rb +6 -1
  142. data/lib/woods/watch/managed_child.rb +1 -1
  143. data/lib/woods/watch/supervisor.rb +1 -1
  144. data/lib/woods/watch/tree_scan.rb +14 -2
  145. data/plugin/.claude-plugin/plugin.json +1 -1
  146. data/plugin/hooks/adapters/normalize.rb +3 -2
  147. data/plugin/hooks/woods-input-rules.sh +4 -0
  148. data/plugin/hooks/woods-refresh.sh +15 -7
  149. data/plugin/hooks/woods-session-start.sh +60 -3
  150. data/plugin/skills/woods-diagnose/SKILL.md +334 -11
  151. data/plugin/skills/woods-investigate/SKILL.md +11 -0
  152. data/plugin/skills/woods-mcp-config/SKILL.md +79 -8
  153. data/plugin/skills/woods-setup/SKILL.md +53 -6
  154. metadata +32 -5
@@ -25,7 +25,7 @@ module Woods
25
25
  def initialize(command:, root:, env: ENV.to_h, logger: $stderr, boot_timeout: 300,
26
26
  shutdown_timeout: 10, retry_delays: RETRY_DELAYS)
27
27
  raise ArgumentError, 'WOODS_WATCH_IDLE_TIMEOUT must be unset for managed watching' unless
28
- env['WOODS_WATCH_IDLE_TIMEOUT'].to_s.empty?
28
+ env['WOODS_WATCH_IDLE_TIMEOUT'].to_s.strip.empty?
29
29
 
30
30
  @command = command
31
31
  @root = File.realpath(root)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'find'
4
4
  require 'set'
5
+ require 'woods/source_path_encoding'
5
6
 
6
7
  module Woods
7
8
  module Watch
@@ -48,12 +49,15 @@ module Woods
48
49
  # @yieldparam path [String] absolute path to a regular file
49
50
  # @return [void]
50
51
  def each_file(root:, ignored:, visited: nil, &block)
51
- base = root.to_s
52
+ base = SourcePathEncoding.utf8(root)
53
+ return unless base
54
+
52
55
  prefix = "#{base}/"
53
56
  visited = (visited || Set.new).dup
54
57
  visited << real_dir(base)
55
58
 
56
59
  Find.find(base) do |path|
60
+ path = decoded_entry(path)
57
61
  next if path == base
58
62
 
59
63
  skip = skip?(path.delete_prefix(prefix), ignored)
@@ -65,6 +69,11 @@ module Woods
65
69
  nil
66
70
  end
67
71
 
72
+ # Prune before any string matching or descent into an undecodable entry.
73
+ def decoded_entry(path)
74
+ SourcePathEncoding.utf8(path) || Find.prune
75
+ end
76
+
68
77
  # Classify one entry and act on it.
69
78
  #
70
79
  # @return [Symbol, nil] `:prune` when the caller should not descend
@@ -126,7 +135,7 @@ module Woods
126
135
 
127
136
  # @return [String, nil] resolved path, or nil for a broken/looping link
128
137
  def real_dir(path)
129
- File.realpath(path)
138
+ SourcePathEncoding.utf8(File.realpath(path))
130
139
  rescue SystemCallError
131
140
  nil
132
141
  end
@@ -147,6 +156,9 @@ module Woods
147
156
  # @param relative [String] path relative to the watched root
148
157
  # @return [Boolean] whether any segment is a dotfile Woods does not read
149
158
  def hidden?(relative)
159
+ relative = SourcePathEncoding.utf8(relative)
160
+ return true unless relative
161
+
150
162
  relative.split(File::SEPARATOR).any? { |segment| hidden_segment?(segment) }
151
163
  end
152
164
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "woods-plugin",
3
3
  "description": "Woods user guides for Claude Code: set up, upgrade, configure MCP servers for, investigate codebases with, enable a repository's agents on, and diagnose the Woods Rails code-intelligence gem.",
4
- "version": "2.3.54",
4
+ "version": "2.3.71",
5
5
  "author": {
6
6
  "name": "lost-in-the"
7
7
  },
@@ -55,8 +55,9 @@ end
55
55
 
56
56
  if $PROGRAM_NAME == __FILE__
57
57
  begin
58
- puts JSON.generate(WoodsHookEvent.normalize(ARGV.fetch(0), JSON.parse($stdin.read(1_048_577))))
59
- rescue JSON::ParserError, ArgumentError, KeyError, TypeError
58
+ input = $stdin.binmode.read(1_048_577).force_encoding(Encoding::UTF_8)
59
+ puts JSON.generate(WoodsHookEvent.normalize(ARGV.fetch(0), JSON.parse(input)))
60
+ rescue JSON::ParserError, EncodingError, ArgumentError, KeyError, TypeError
60
61
  warn '[Woods hooks] Unsupported or malformed edit event; no refresh queued.'
61
62
  exit 1
62
63
  end
@@ -111,6 +111,10 @@ woods_input_action() {
111
111
  case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
112
112
  return
113
113
  fi
114
+ if { { [[ "$path" != */tasks/* ]] && [[ "$path" != */generators/* ]] && { [[ "$path" == *.rb ]]; } && { [[ "$path" == lib/* ]]; }; }; }; then
115
+ case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
116
+ return
117
+ fi
114
118
  if { { { [[ "$path" == *.rake ]]; } && { [[ "$path" == lib/tasks/* ]]; }; }; }; then
115
119
  case "$operation" in delete|move) printf full ;; *) printf incremental ;; esac
116
120
  return
@@ -21,18 +21,26 @@ if command -v jq >/dev/null 2>&1; then
21
21
  printf '[Woods hooks] Unsupported or malformed edit event; no refresh queued.\n' >&2; exit 0;
22
22
  }
23
23
  elif command -v ruby >/dev/null 2>&1; then
24
- payload="$(ruby "$hook_dir/adapters/normalize.rb" "$client" <"$input_file")" || exit 0
24
+ payload="$(ruby -EUTF-8:UTF-8 "$hook_dir/adapters/normalize.rb" "$client" <"$input_file")" || exit 0
25
25
  else
26
26
  printf '[Woods hooks] jq or Ruby is required; no refresh queued.\n' >&2
27
27
  exit 0
28
28
  fi
29
29
  rm -f "$input_file"
30
30
  trap - EXIT TERM INT
31
+ # JSON and client paths are UTF-8 regardless of the host process locale.
32
+ # Keep malformed queue/input diagnostics bounded and free of event contents.
33
+ ruby_json() {
34
+ ruby -EUTF-8:UTF-8 "$@" 2>/dev/null || {
35
+ printf '[Woods hooks] Invalid UTF-8 or JSON data; refresh not confirmed.\n' >&2
36
+ return 1
37
+ }
38
+ }
31
39
  field() {
32
40
  if command -v jq >/dev/null 2>&1; then
33
41
  printf '%s' "$payload" | jq -ej '.root'
34
42
  else
35
- printf '%s' "$payload" | ruby -rjson -e 'print JSON.parse($stdin.read).fetch("root")'
43
+ printf '%s' "$payload" | ruby_json -rjson -e 'print JSON.parse($stdin.read).fetch("root")'
36
44
  fi
37
45
  }
38
46
  cwd="$(field; printf .)"
@@ -50,7 +58,7 @@ event_fields() {
50
58
  if command -v jq >/dev/null 2>&1; then
51
59
  printf '%s' "$payload" | jq -j '.events[] | .operation,"\u0000",.path,"\u0000"'
52
60
  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" }'
61
+ printf '%s' "$payload" | ruby_json -rjson -e 'JSON.parse($stdin.read).fetch("events").each { |e| print e.fetch("operation"), "\0", e.fetch("path"), "\0" }'
54
62
  fi
55
63
  }
56
64
  relevant=0
@@ -83,7 +91,7 @@ enqueue() {
83
91
  if command -v jq >/dev/null 2>&1; then
84
92
  jq -nc --arg path "$1" '{path:$path,operation:"update"}' >"$event.tmp" || return 1
85
93
  else
86
- ruby -rjson -e 'puts JSON.generate(path: ARGV[0], operation: "update")' -- "$1" >"$event.tmp" || return 1
94
+ ruby_json -rjson -e 'puts JSON.generate(path: ARGV[0], operation: "update")' -- "$1" >"$event.tmp" || return 1
87
95
  fi
88
96
  mv "$event.tmp" "$event.json"
89
97
  }
@@ -92,7 +100,7 @@ event="$queue/$$.$RANDOM.$RANDOM"
92
100
  if command -v jq >/dev/null 2>&1; then
93
101
  printf '%s' "$payload" | jq -c --arg root "$cwd" '.events | map(.path |= (if startswith($root + "/") then ltrimstr($root + "/") else . end))' >"$event.tmp" || exit 0
94
102
  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
103
+ printf '%s' "$payload" | ruby_json -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
104
  fi
97
105
  mv "$event.tmp" "$event.json" || exit 0
98
106
 
@@ -144,7 +152,7 @@ encode_batch() {
144
152
  if command -v jq >/dev/null 2>&1; then
145
153
  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
154
  else
147
- ruby -rjson -rbase64 -e '
155
+ ruby_json -rjson -rbase64 -e '
148
156
  output = ARGV.shift
149
157
  print Base64.strict_encode64(JSON.generate(version: 1, output: output,
150
158
  events: ARGV.flat_map { |path| value = JSON.parse(File.read(path)); value.is_a?(Array) ? value : [value] }))
@@ -237,7 +245,7 @@ while :; do
237
245
  if command -v jq >/dev/null 2>&1; then
238
246
  event_count="$(jq 'if type == "array" then length else 1 end' "$candidate")" || break
239
247
  else
240
- event_count="$(ruby -rjson -e 'v = JSON.parse(File.read(ARGV[0])); puts(v.is_a?(Array) ? v.size : 1)' "$candidate")" || break
248
+ event_count="$(ruby_json -rjson -e 'v = JSON.parse(File.read(ARGV[0])); puts(v.is_a?(Array) ? v.size : 1)' "$candidate")" || break
241
249
  fi
242
250
  [ "$((batch_events + event_count))" -le 1000 ] || break
243
251
  [ "$((batch_bytes + event_bytes))" -le 49152 ] || break
@@ -11,9 +11,59 @@ field() {
11
11
  if command -v jq >/dev/null 2>&1; then
12
12
  printf '%s' "$1" | jq -r --arg name "$2" '.[$name] // empty' 2>/dev/null
13
13
  elif command -v ruby >/dev/null 2>&1; then
14
- printf '%s' "$1" | ruby -rjson -e 'print JSON.parse($stdin.read).fetch(ARGV[0], "")' -- "$2" 2>/dev/null
14
+ printf '%s' "$1" | ruby -EUTF-8:UTF-8 -rjson -e 'print JSON.parse($stdin.read).fetch(ARGV[0], "")' -- "$2" 2>/dev/null
15
15
  fi
16
16
  }
17
+ unavailable_summary() {
18
+ if command -v jq >/dev/null 2>&1; then
19
+ printf '%s' "$1" | jq -r '.unavailable | select(type == "object") |
20
+ select(.reason == "source_manifest_too_large" and (.size_bytes | type) == "number" and
21
+ (.limit_bytes | type) == "number") |
22
+ select(.size_bytes > .limit_bytes and .limit_bytes > 0) |
23
+ "source_manifest_too_large; \(.size_bytes) bytes; limit \(.limit_bytes)"' 2>/dev/null
24
+ elif command -v ruby >/dev/null 2>&1; then
25
+ printf '%s' "$1" | ruby -EUTF-8:UTF-8 -rjson -e '
26
+ value = JSON.parse($stdin.read)["unavailable"]
27
+ if value.is_a?(Hash) && value["reason"] == "source_manifest_too_large"
28
+ size, limit = value.values_at("size_bytes", "limit_bytes")
29
+ if [size, limit].all?(Integer) && size > limit && limit > 0
30
+ print "source_manifest_too_large; #{size} bytes; limit #{limit}"
31
+ end
32
+ end
33
+ ' 2>/dev/null
34
+ fi
35
+ }
36
+ source_advice() {
37
+ if command -v jq >/dev/null 2>&1; then
38
+ printf '%s' "$1" | jq -r 'if (.recommendations | type) == "array" then
39
+ .recommendations[] | select(. == "deep_check" or . == "fresh_capture" or . == "inspect_source_scan")
40
+ else empty end' 2>/dev/null
41
+ elif command -v ruby >/dev/null 2>&1; then
42
+ printf '%s' "$1" | ruby -EUTF-8:UTF-8 -rjson -e '
43
+ values = JSON.parse($stdin.read)["recommendations"]
44
+ allowed = %w[deep_check fresh_capture inspect_source_scan]
45
+ puts values.select { |value| allowed.include?(value) } if values.is_a?(Array)
46
+ ' 2>/dev/null
47
+ fi
48
+ }
49
+ unknown_message() {
50
+ local advice action
51
+ advice="$(source_advice "$1")"
52
+ if [ -z "$advice" ]; then
53
+ echo 'Woods source freshness is unknown: this generation lacks complete verified source evidence. Inspect woods_status; use woods-extract full for a fresh capture.'
54
+ return
55
+ fi
56
+ printf '%s' 'Woods source freshness is unknown: this check could not verify the current source contents.'
57
+ # Only the three literal identifiers above reach this word split.
58
+ for action in $advice; do
59
+ case "$action" in
60
+ deep_check) printf '%s' ' Request woods_status with source_check: deep; the quick reader reached its time limit.' ;;
61
+ fresh_capture) printf '%s' ' Fix the capture or boot evidence reported by woods_status, then run woods-extract full in the application environment.' ;;
62
+ inspect_source_scan) printf '%s' ' Inspect woods_status verification_reasons, source mapping, permissions and scan limits.' ;;
63
+ esac
64
+ done
65
+ printf '\n'
66
+ }
17
67
  cwd="$(field "$payload" cwd)"
18
68
  [ -n "$cwd" ] && [ -d "$cwd" ] || exit 0
19
69
  configured_output="${WOODS_OUTPUT:-tmp/woods}"
@@ -25,7 +75,10 @@ esac
25
75
  if command -v jq >/dev/null 2>&1; then
26
76
  encoded="$(jq -nr --arg output "$configured_output" '{output:$output,mode:"quick"} | @base64')"
27
77
  elif command -v ruby >/dev/null 2>&1; then
28
- encoded="$(ruby -rjson -rbase64 -e 'print Base64.strict_encode64(JSON.generate(output: ARGV[0], mode: "quick"))' -- "$configured_output")"
78
+ encoded="$(ruby -EUTF-8:UTF-8 -rjson -rbase64 -e 'print Base64.strict_encode64(JSON.generate(output: ARGV[0], mode: "quick"))' -- "$configured_output" 2>/dev/null)" || {
79
+ echo 'Woods source freshness is unknown: hook options could not be encoded. Inspect woods_status.'
80
+ exit 0
81
+ }
29
82
  else
30
83
  exit 0
31
84
  fi
@@ -63,7 +116,11 @@ status="$(tail -n 1 "$record")"
63
116
  state="$(field "$status" state)"
64
117
  case "$state" in
65
118
  current) ;;
119
+ unavailable)
120
+ detail="$(unavailable_summary "$status")"
121
+ printf 'Woods source freshness is unavailable: %s. The code index is usable; inspect woods_status. Repeating a full extraction will not reduce this size.\n' "${detail:-inspect woods_status for the unavailable evidence reason}"
122
+ ;;
66
123
  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.' ;;
124
+ *) unknown_message "$status" ;;
68
125
  esac
69
126
  exit 0