woods 2.0.0.beta4 → 2.0.1

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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +495 -471
  3. data/CONTRIBUTING.md +12 -2
  4. data/README.md +11 -26
  5. data/docs/AGENT_GUIDE.md +31 -12
  6. data/docs/AGENT_SETUP.md +17 -10
  7. data/docs/AUTOMATIC_MAINTENANCE.md +222 -0
  8. data/docs/BACKEND_MATRIX.md +13 -7
  9. data/docs/CLIENT_HOOKS.md +1 -1
  10. data/docs/CONFIGURATION_REFERENCE.md +44 -27
  11. data/docs/CONSOLE_MCP_SETUP.md +95 -16
  12. data/docs/DOCKER_SETUP.md +15 -0
  13. data/docs/EVALUATION.md +10 -4
  14. data/docs/EXTRACTOR_REFERENCE.md +14 -2
  15. data/docs/FAQ.md +14 -3
  16. data/docs/GETTING_STARTED.md +18 -17
  17. data/docs/INCREMENTAL_EXTRACTION.md +8 -3
  18. data/docs/INDEX_LAYOUT.md +2 -2
  19. data/docs/MCP_HTTP_TRANSPORT.md +54 -2
  20. data/docs/MCP_SERVERS.md +28 -11
  21. data/docs/MCP_TOOL_COOKBOOK.md +1 -1
  22. data/docs/MCP_WORKTREE_SETUP.md +13 -1
  23. data/docs/PUBLISHED_INDEX.md +1 -1
  24. data/docs/README.md +2 -1
  25. data/docs/RETRIEVAL_GUIDE.md +57 -8
  26. data/docs/SOURCE_FRESHNESS.md +1 -1
  27. data/docs/TOKEN_BENCHMARK.md +16 -10
  28. data/docs/TROUBLESHOOTING.md +133 -37
  29. data/docs/UPGRADING_TO_2.md +69 -7
  30. data/docs/WATCH_DAEMON.md +172 -17
  31. data/docs/WHY_WOODS.md +9 -5
  32. data/exe/woods-console +13 -11
  33. data/exe/woods-mcp-http +16 -9
  34. data/exe/woods-watch +5 -0
  35. data/lib/generators/woods/watch_generator.rb +53 -0
  36. data/lib/puma/plugin/woods.rb +10 -0
  37. data/lib/tasks/woods.rake +14 -0
  38. data/lib/woods/cache/cache_middleware.rb +18 -11
  39. data/lib/woods/console/adapter_family.rb +39 -0
  40. data/lib/woods/console/credential_index.rb +33 -3
  41. data/lib/woods/console/embedded_executor.rb +401 -43
  42. data/lib/woods/console/model_validator.rb +8 -0
  43. data/lib/woods/console/rack_middleware.rb +39 -10
  44. data/lib/woods/console/redactor.rb +24 -10
  45. data/lib/woods/console/safe_context.rb +44 -7
  46. data/lib/woods/console/sql_noise_stripper.rb +41 -12
  47. data/lib/woods/console/sql_table_scanner.rb +45 -34
  48. data/lib/woods/console/sql_validator.rb +37 -2
  49. data/lib/woods/console/stdio_transport.rb +27 -0
  50. data/lib/woods/extractor.rb +25 -7
  51. data/lib/woods/git_command.rb +6 -7
  52. data/lib/woods/git_provenance.rb +4 -6
  53. data/lib/woods/mcp/bearer_auth.rb +1 -1
  54. data/lib/woods/mcp/bootstrapper.rb +3 -1
  55. data/lib/woods/mcp/initialization_guidance.rb +1 -1
  56. data/lib/woods/mcp/origin_guard.rb +24 -77
  57. data/lib/woods/mcp/origin_policy.rb +124 -0
  58. data/lib/woods/mcp/server.rb +41 -9
  59. data/lib/woods/railtie_support.rb +8 -0
  60. data/lib/woods/retrieval/corpus_status.rb +46 -0
  61. data/lib/woods/retriever.rb +19 -7
  62. data/lib/woods/storage/local_corpus_stats.rb +32 -0
  63. data/lib/woods/storage/metadata_store.rb +20 -0
  64. data/lib/woods/storage/vector_store.rb +10 -0
  65. data/lib/woods/version.rb +1 -1
  66. data/lib/woods/watch/child_environment.rb +30 -0
  67. data/lib/woods/watch/cli.rb +91 -0
  68. data/lib/woods/watch/daemon.rb +55 -7
  69. data/lib/woods/watch/event_stream.rb +70 -0
  70. data/lib/woods/watch/guardian.rb +142 -0
  71. data/lib/woods/watch/installation/layout.rb +70 -0
  72. data/lib/woods/watch/installation/options.rb +128 -0
  73. data/lib/woods/watch/installation/planner.rb +128 -0
  74. data/lib/woods/watch/installation/probe.rb +101 -0
  75. data/lib/woods/watch/installation/receipt.rb +77 -0
  76. data/lib/woods/watch/installation/recovery.rb +64 -0
  77. data/lib/woods/watch/installation/templates.rb +58 -0
  78. data/lib/woods/watch/installation.rb +56 -0
  79. data/lib/woods/watch/lifecycle.rb +182 -0
  80. data/lib/woods/watch/managed_child.rb +113 -0
  81. data/lib/woods/watch/managed_cleanup.rb +48 -0
  82. data/lib/woods/watch/managed_process.rb +144 -0
  83. data/lib/woods/watch/puma_adapter.rb +87 -0
  84. data/lib/woods/watch/puma_child.rb +66 -0
  85. data/lib/woods/watch/supervision_records.rb +95 -0
  86. data/lib/woods/watch/supervision_status.rb +104 -0
  87. data/lib/woods/watch/supervisor.rb +161 -0
  88. data/lib/woods/watch/supervisor_reporting.rb +46 -0
  89. data/plugin/.claude-plugin/plugin.json +1 -1
  90. data/plugin/skills/woods-agent-enable/SKILL.md +1 -1
  91. data/plugin/skills/woods-diagnose/SKILL.md +88 -8
  92. data/plugin/skills/woods-investigate/SKILL.md +6 -6
  93. data/plugin/skills/woods-mcp-config/SKILL.md +43 -1
  94. data/plugin/skills/woods-setup/SKILL.md +66 -4
  95. metadata +37 -5
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Woods
4
+ module Watch
5
+ # Bounded readers and liveness correlation for persisted supervision records.
6
+ module SupervisionRecords
7
+ # Read bounded records without granting signal or writer-lock authority.
8
+ # @param index [String] published index root
9
+ # @return [Hash] supervision records with separately evaluated liveness
10
+ def self.read(index)
11
+ directory = File.join(index, SupervisionStatus::DIRECTORY)
12
+ return { records: [] } unless readable_directory?(directory)
13
+
14
+ names = Dir.each_child(directory).take(SupervisionStatus::MAX_SCAN + 1)
15
+ records = names.first(SupervisionStatus::MAX_SCAN).filter_map do |name|
16
+ read_record(File.join(directory, name)) if /\A[a-f0-9]{32}\.json\z/.match?(name)
17
+ end
18
+ records.sort_by! { |record| [record['alive'] ? 0 : 1, -Time.iso8601(record['updated_at']).to_f] }
19
+ correlate(records.first(SupervisionStatus::MAX_RECORDS), index)
20
+ truncated = names.size > SupervisionStatus::MAX_SCAN || records.size > SupervisionStatus::MAX_RECORDS
21
+ { records: records.first(SupervisionStatus::MAX_RECORDS), truncated: truncated }
22
+ rescue SystemCallError
23
+ { records: [] }
24
+ end
25
+
26
+ def self.readable_directory?(directory)
27
+ File.directory?(directory) && !File.symlink?(directory)
28
+ end
29
+ private_class_method :readable_directory?
30
+
31
+ def self.read_record(path)
32
+ bytes = bounded_read(path)
33
+ return unless bytes
34
+
35
+ record = JSON.parse(bytes)
36
+ return unless valid_record?(record, path)
37
+
38
+ age = Time.now - Time.iso8601(record['updated_at'])
39
+ record['alive'] = record['state'] != 'stopped' &&
40
+ age.between?(-30, SupervisionStatus::STALE_AFTER) && local_alive?(record)
41
+ record
42
+ rescue SystemCallError, JSON::ParserError, TypeError, ArgumentError
43
+ nil
44
+ end
45
+ private_class_method :read_record
46
+
47
+ def self.bounded_read(path)
48
+ return if File.symlink?(path)
49
+
50
+ flags = File::RDONLY | File::NONBLOCK
51
+ flags |= File::NOFOLLOW if File.const_defined?(:NOFOLLOW)
52
+ bytes = File.open(path, flags) { |file| file.read(SupervisionStatus::MAX_BYTES + 1) if file.stat.file? }
53
+ bytes if bytes && bytes.bytesize <= SupervisionStatus::MAX_BYTES
54
+ end
55
+ private_class_method :bounded_read
56
+
57
+ def self.valid_record?(record, path)
58
+ record.is_a?(Hash) && record['version'] == 1 && record['launcher'] == File.basename(path, '.json') &&
59
+ SupervisionStatus::STATES.include?(record['state'])
60
+ end
61
+ private_class_method :valid_record?
62
+
63
+ def self.local_alive?(record)
64
+ unless record['host'] == Status.host_identity && record['pid'].is_a?(Integer) && record['pid'].positive?
65
+ return false
66
+ end
67
+
68
+ Process.kill(0, record['pid'])
69
+ true
70
+ rescue Errno::ESRCH
71
+ false
72
+ rescue Errno::EPERM
73
+ true
74
+ end
75
+ private_class_method :local_alive?
76
+
77
+ def self.correlate(records, index)
78
+ status = Status.new(output_dir: index)
79
+ daemon = status.read
80
+ daemon_alive = daemon.is_a?(Hash) && status.alive?
81
+ records.each do |record|
82
+ record['active_child'] = daemon_alive && matches_child?(record, daemon)
83
+ end
84
+ rescue SystemCallError, TypeError, NoMethodError
85
+ records.each { |record| record['active_child'] = false }
86
+ end
87
+ private_class_method :correlate
88
+
89
+ def self.matches_child?(record, daemon)
90
+ record['alive'] && record['child_pid'] == daemon['pid'] && record['host'] == daemon['host']
91
+ end
92
+ private_class_method :matches_child?
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require_relative 'status'
5
+ require_relative 'supervision_records'
6
+
7
+ module Woods
8
+ module Watch
9
+ # Independent owner records: a parked duplicate cannot overwrite the
10
+ # daemon heartbeat or make incremental hooks defer to an inactive writer.
11
+ class SupervisionStatus
12
+ DIRECTORY = 'watch_supervisors'
13
+ MAX_RECORDS = 32
14
+ MAX_SCAN = 256
15
+ MAX_BYTES = 8192
16
+ STALE_AFTER = 30
17
+ STATES = %w[starting booting reconciling ready degraded retrying parked stopped].freeze
18
+ FIELDS = %i[state reason attempt child_pid retry_at].freeze
19
+
20
+ # @param index [String] application-resolved output directory
21
+ # @param token [String] unguessable launcher identity
22
+ def initialize(index:, token:)
23
+ raise ArgumentError, 'invalid launcher token' unless /\A[a-f0-9]{32}\z/.match?(token)
24
+
25
+ @token = token
26
+ @directory = File.join(index, DIRECTORY)
27
+ @path = File.join(@directory, "#{token}.json")
28
+ end
29
+
30
+ # @param fields [Hash] state, owned child, attempt and bounded reason
31
+ # @return [void]
32
+ def write(**fields)
33
+ validate_fields!(fields)
34
+ verify_ownership!
35
+ data = { version: 1, launcher: @token, pid: Process.pid, host: Status.host_identity,
36
+ updated_at: Time.now.utc.iso8601 }.merge(fields)
37
+ bytes = JSON.generate(data)
38
+ raise ArgumentError, 'oversized supervision record' if bytes.bytesize > MAX_BYTES
39
+
40
+ AtomicFile.write(@path, bytes, mode: 0o644)
41
+ prune_stale
42
+ end
43
+
44
+ # @param index [String] published index root
45
+ # @return [Hash] bounded supervision diagnostics
46
+ def self.read(index)
47
+ SupervisionRecords.read(index)
48
+ end
49
+
50
+ def self.read_record(path)
51
+ SupervisionRecords.send(:read_record, path)
52
+ end
53
+ private_class_method :read_record
54
+
55
+ def self.local_alive?(record)
56
+ SupervisionRecords.send(:local_alive?, record)
57
+ end
58
+ private_class_method :local_alive?
59
+
60
+ private
61
+
62
+ def verify_ownership!
63
+ raise ArgumentError, 'invalid supervision directory' if File.symlink?(@directory)
64
+ return unless File.exist?(@path) || File.symlink?(@path)
65
+
66
+ previous = self.class.send(:read_record, @path)
67
+ return if previous && previous['pid'] == Process.pid && previous['host'] == Status.host_identity
68
+
69
+ raise ArgumentError, 'supervision record belongs to another owner'
70
+ end
71
+
72
+ def validate_fields!(fields)
73
+ valid = (fields.keys - FIELDS).empty? && STATES.include?(fields[:state]) &&
74
+ fields[:reason].is_a?(String) && /\A[a-z_]{1,80}\z/.match?(fields[:reason]) &&
75
+ fields[:attempt].is_a?(String) && /\A[a-f0-9]{32}\z/.match?(fields[:attempt])
76
+ raise ArgumentError, 'invalid supervision fields' unless valid
77
+ end
78
+
79
+ def prune_stale
80
+ records = self.class.read(File.dirname(@directory))[:records]
81
+ records.each do |record|
82
+ next unless expired?(record) && removable?(record)
83
+
84
+ path = File.join(@directory, "#{record['launcher']}.json")
85
+ current = self.class.send(:read_record, path)
86
+ stored = record.except('active_child')
87
+ File.unlink(path) if current == stored
88
+ end
89
+ rescue SystemCallError
90
+ nil
91
+ end
92
+
93
+ def expired?(record)
94
+ record['launcher'] != @token && !record['alive'] &&
95
+ Time.now - Time.iso8601(record['updated_at']) > STALE_AFTER
96
+ end
97
+
98
+ def removable?(record)
99
+ record['state'] == 'stopped' ||
100
+ (record['host'] == Status.host_identity && !self.class.send(:local_alive?, record))
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'managed_process'
4
+ require_relative 'lifecycle'
5
+ require_relative 'supervision_status'
6
+ require_relative 'supervisor_reporting'
7
+
8
+ module Woods
9
+ module Watch
10
+ # Foreground retry owner shared by Procfile and Puma installations.
11
+ # It never retries configuration conflicts or silently takes over ownership.
12
+ class Supervisor # rubocop:disable Metrics/ClassLength -- retry, park and shutdown share one foreground attempt state
13
+ include SupervisorReporting
14
+
15
+ RETRY_DELAYS = [1, 2, 4, 8, 16, 30].freeze
16
+
17
+ # @param command [Array<String>] fresh application invocation on each attempt
18
+ # @param root [String] application root
19
+ # @param env [Hash] application environment
20
+ # @param logger [#puts] diagnostic destination (stderr by default)
21
+ # @param boot_timeout [Numeric] pre-identity boot deadline
22
+ # @param shutdown_timeout [Numeric] graceful application shutdown
23
+ # @param retry_delays [Array<Numeric>] bounded retry schedule
24
+ # rubocop:disable-next Metrics/ParameterLists -- independently injectable process policy and diagnostics
25
+ def initialize(command:, root:, env: ENV.to_h, logger: $stderr, boot_timeout: 300,
26
+ shutdown_timeout: 10, retry_delays: RETRY_DELAYS)
27
+ raise ArgumentError, 'WOODS_WATCH_IDLE_TIMEOUT must be unset for managed watching' unless
28
+ env['WOODS_WATCH_IDLE_TIMEOUT'].to_s.empty?
29
+
30
+ @command = command
31
+ @root = File.realpath(root)
32
+ @env = env
33
+ @logger = logger
34
+ @boot_timeout = boot_timeout
35
+ @shutdown_timeout = shutdown_timeout
36
+ @retry_delays = retry_delays
37
+ @token = SecureRandom.hex(16)
38
+ @attempts = @failures = 0
39
+ @stopping = false
40
+ end
41
+
42
+ # @return [String, nil] honest lifecycle state
43
+ attr_reader :state, :attempts
44
+
45
+ # @return [Integer] zero after owner-requested shutdown
46
+ def run
47
+ until @stopping
48
+ run_attempt
49
+ break if @stopping
50
+
51
+ reason = retry_reason
52
+ reason ? retry_after(reason) : park(@protocol.terminal || 'incompatible_or_stopped_task')
53
+ end
54
+ 0
55
+ ensure
56
+ @process&.stop
57
+ publish('stopped', 'owner_stopped')
58
+ end
59
+
60
+ # Signal-safe request; process cleanup occurs on the run thread.
61
+ # @return [void]
62
+ def stop
63
+ @stopping = true
64
+ end
65
+
66
+ private
67
+
68
+ def run_attempt
69
+ prepare_attempt
70
+ @process.start
71
+ monitor_attempt
72
+ rescue ArgumentError => e
73
+ @invalid = true
74
+ @logger.puts("[woods-watch] incompatible lifecycle: #{e.message}")
75
+ ensure
76
+ @process&.stop
77
+ end
78
+
79
+ def prepare_attempt
80
+ retire_status
81
+ @attempts += 1
82
+ @attempt = SecureRandom.hex(16)
83
+ @invalid = @timed_out = false
84
+ @ready_at = nil
85
+ @started_at = monotonic
86
+ @protocol = Lifecycle.new(launcher: @token, attempt: @attempt, root: @root)
87
+ @process = ManagedProcess.new(command: @command, root: @root, env: @env, events: true,
88
+ launcher: @token, attempt: @attempt, shutdown_timeout: @shutdown_timeout)
89
+ publish('starting', 'boot_pending')
90
+ end
91
+
92
+ def monitor_attempt
93
+ loop do
94
+ consume_events
95
+ break if @stopping || @protocol.finished? || !@process.alive?
96
+
97
+ if !@protocol.booted? && monotonic - @started_at >= @boot_timeout
98
+ @timed_out = true
99
+ break
100
+ end
101
+ raise ArgumentError, 'lifecycle stream ended while task was running' if @process.eof?
102
+
103
+ heartbeat
104
+ sleep 0.05
105
+ end
106
+ consume_events
107
+ end
108
+
109
+ def consume_events
110
+ @process.read_events.each do |line|
111
+ record = @protocol.accept(line)
112
+ @status = SupervisionStatus.new(index: @protocol.index, token: @token) if record['event'] == 'identity'
113
+ track_readiness(record) if record['event'] == 'startup'
114
+ publish(@protocol.state, @protocol.reason) if @protocol.state && !%w[exit terminal].include?(record['event'])
115
+ end
116
+ end
117
+
118
+ def track_readiness(record)
119
+ @ready_at = record['state'] == 'ready' ? (@ready_at || monotonic) : nil
120
+ end
121
+
122
+ def retry_reason
123
+ return if parked_outcome?
124
+ return 'boot_timeout' if @timed_out
125
+ return 'restart_required' if @protocol.terminal == 'restart_required' && @protocol.exit_code == 75
126
+ return if [0, 127].include?(@protocol.exit_code)
127
+
128
+ 'child_failed'
129
+ end
130
+
131
+ def parked_outcome?
132
+ @invalid || %w[already_running unsupported_environment].include?(@protocol.terminal)
133
+ end
134
+
135
+ def retry_after(reason)
136
+ @failures = 0 if @ready_at && monotonic - @ready_at >= 60
137
+ delay = @retry_delays.fetch([@failures, @retry_delays.size - 1].min)
138
+ @failures += 1
139
+ publish('retrying', reason, retry_at: (Time.now + delay).utc.iso8601)
140
+ wait_until(monotonic + delay)
141
+ end
142
+
143
+ def park(reason)
144
+ publish('parked', reason)
145
+ @logger.puts('[woods-watch] restart the owner after correcting configuration; no automatic takeover')
146
+ wait_until(Float::INFINITY)
147
+ end
148
+
149
+ def wait_until(deadline)
150
+ until @stopping || monotonic >= deadline
151
+ heartbeat
152
+ sleep 0.05
153
+ end
154
+ end
155
+
156
+ def monotonic
157
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
158
+ end
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Woods
4
+ module Watch
5
+ # Publication and retirement of one launcher's diagnostic records.
6
+ module SupervisorReporting
7
+ private
8
+
9
+ def publish(state, reason, **details)
10
+ return unless state
11
+
12
+ reason ||= 'pending'
13
+ changed = @state != state || @reason != reason
14
+ @state = state
15
+ @reason = reason
16
+ @details = details
17
+ @logger.puts("[woods-watch] #{state}: #{reason || 'pending'} (attempt #{@attempts})") if changed
18
+ heartbeat(force: true)
19
+ end
20
+
21
+ def heartbeat(force: false)
22
+ return unless @status && heartbeat_due?(force)
23
+
24
+ @status.write(state: @state, reason: @reason, attempt: @attempt,
25
+ child_pid: @process&.alive? ? @protocol.child_pid : nil, **(@details || {}))
26
+ @heartbeat_at = monotonic
27
+ rescue SystemCallError, ArgumentError => e
28
+ @logger.puts("[woods-watch] cannot write supervision status (#{e.class})")
29
+ @heartbeat_at = monotonic
30
+ end
31
+
32
+ def heartbeat_due?(force)
33
+ force || !@heartbeat_at || monotonic - @heartbeat_at >= 5
34
+ end
35
+
36
+ def retire_status
37
+ @status&.write(state: 'stopped', reason: 'attempt_finished', attempt: @attempt, child_pid: nil)
38
+ rescue SystemCallError, ArgumentError => e
39
+ @logger.puts("[woods-watch] cannot retire supervision status (#{e.class})")
40
+ ensure
41
+ @status = nil
42
+ @heartbeat_at = nil
43
+ end
44
+ end
45
+ end
46
+ end
@@ -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.47",
4
+ "version": "2.3.54",
5
5
  "author": {
6
6
  "name": "lost-in-the"
7
7
  },
@@ -23,7 +23,7 @@ and removal. Preserve manual setup for older installed versions.
23
23
  For concurrent configuration operations, wait for the active operation to
24
24
  finish and generate a fresh plan if the saved snapshots changed. Do not remove
25
25
  its lock or overwrite the other application's entry. Coordination across
26
- applications sharing user configuration is unreleased after `2.0.0.beta3`;
26
+ applications sharing user configuration is included in Woods `2.0.0`;
27
27
  check the installed revision before relying on it.
28
28
 
29
29
  ## Preflight
@@ -36,26 +36,57 @@ bundle exec rails runner 'Rails.application.eager_load!; puts "eager load ok"'
36
36
 
37
37
  Use the application's normal Docker command and environment variables when applicable. Fix boot/eager-load failures before Woods.
38
38
 
39
+ ### Watcher setup cannot find already installed Rails or dependencies
40
+
41
+ Compare normal task discovery with installer preflight in the same container and
42
+ application environment. Early Git builds of the watcher installer stripped
43
+ `BUNDLE_PATH` and `BUNDLE_APP_CONFIG`; Woods `2.0.0` includes the #540 fix.
44
+ Record the loaded revision and bundle configuration source before
45
+ reinstalling dependencies or writing a local bundle-path workaround. Follow the
46
+ [watcher installation guide](https://github.com/lost-in-the/woods/blob/main/docs/WATCH_DAEMON.md#managed-development-startup).
47
+
48
+ ### Puma cannot load the Woods plugin after switching branches
49
+
50
+ Early generated guards checked only whether Woods was activated. An older gem
51
+ can satisfy that check without providing `puma/plugin/woods.rb`. The #542 fix is
52
+ included in Woods `2.0.0`: verify the loaded revision, then use a supporting
53
+ bundle to preview and apply `bin/rails generate woods:watch --operation update
54
+ --mode puma`. The updated guard checks the active gem's require paths, so older
55
+ gems boot without a watcher. Repeating setup does not upgrade the guard. Follow
56
+ the [owned setup runbook](https://github.com/lost-in-the/woods/blob/main/docs/WATCH_DAEMON.md#ownership-updates-and-removal);
57
+ do not change the owned directive or receipt manually.
58
+
39
59
  ### Watch repeatedly exits 75
40
60
 
61
+ First identify the lifecycle owner. The raw task deliberately exits 75; a bare
62
+ Foreman entry then stops all services. Managed `woods-watch`/Puma setup (#538)
63
+ is included in Woods `2.0.0`: verify installed executable/generator help and
64
+ loaded revision before proposing it. Supporting launchers retry boot failures,
65
+ reject idle TTL, and park ownership/protocol conflicts until owner restart.
66
+ Inspect separate supervision state rather than treating its parent PID as a
67
+ healthy daemon. Before the first resolved index path, use launcher logs.
68
+ Do not remove claims or kill PIDs from status to force takeover. See
69
+ [startup diagnosis](https://github.com/lost-in-the/woods/blob/main/docs/TROUBLESHOOTING.md#watcher-startup-or-planned-restart-fails).
70
+
41
71
  Check the installed version's watch guide. Older releases, including
42
72
  `2.0.0.beta2`, can rediscover the same restart-trigger paths on every boot. Stop
43
73
  the supervisor, run one successful full extraction, then restart the standalone
44
74
  watch task. Do not assume automatic startup reconciliation exists in that release.
45
75
  For versions documenting environment-boot snapshots, confirm that the command is
46
- `bundle exec rake woods:watch`, with no preceding `environment` task, and check
76
+ the application's actual `rails woods:watch` or `rake woods:watch` entrypoint,
77
+ with no preceding `environment` task, and check
47
78
  whether boot inputs keep changing during initialization or catch-up.
48
79
 
49
80
  ### Watch retains facts from an initializer deleted while stopped
50
81
 
51
- Record the installed revision. Unreleased after `2.0.0.beta3`, startup preserves
82
+ Record the installed revision. In Woods `2.0.0`, startup preserves
52
83
  registered deleted boot inputs as full-extraction obligations. On earlier builds,
53
84
  stop watch, run a successful full extraction in a fresh process, then restart
54
85
  standalone `woods:watch`. See the installed version's watch guide.
55
86
 
56
87
  ### A cleaned index directory still exists
57
88
 
58
- Unreleased after `2.0.0.beta3`, `woods:clean` retains the output directory and
89
+ In Woods `2.0.0`, `woods:clean` retains the output directory and
59
90
  hidden extraction guard for concurrent writer coordination. Verify published
60
91
  artifacts are gone; do not remove that guard while writers may be running.
61
92
 
@@ -134,7 +165,7 @@ with `server.version`; missing/null is unknown, not a failure. A validator
134
165
  major-version warning calls for full extraction and upgrade review, while a match
135
166
  does not certify retained units were migrated. See [writer provenance](https://github.com/lost-in-the/woods/blob/main/docs/PUBLISHED_INDEX.md#manifest-writer-provenance).
136
167
 
137
- Unreleased after `2.0.0.beta3`: incremental/refresh handled source errors keep
168
+ Included in Woods `2.0.0`: incremental/refresh handled source errors keep
138
169
  the previous generation active and leave watch batches pending. Repair the
139
170
  logged source error and retry the complete batch; see
140
171
  [handled source errors](https://github.com/lost-in-the/woods/blob/main/docs/INCREMENTAL_EXTRACTION.md#handled-source-errors-and-retry).
@@ -177,6 +208,24 @@ access with its `WOODS_GIT_DIR` setting. A failed history stream is discarded;
177
208
  repair git access and run full extraction to refresh retained metadata. See the
178
209
  [history contract](https://github.com/lost-in-the/woods/blob/main/docs/CONFIGURATION_REFERENCE.md#git-enrichment-history).
179
210
 
211
+ If per-unit Git metadata is absent, check `git --version` inside the extraction
212
+ process/container as well as repository access. Builds with #551 warn when Git
213
+ cannot execute and a repository is expected; older versions may be silent.
214
+ Source archives without a Git directory remain supported. `GIT_SHA` and
215
+ structural `ready` do not certify history availability. After repairing Git,
216
+ run full extraction; see the
217
+ [missing-executable diagnostic](https://github.com/lost-in-the/woods/blob/main/docs/TROUBLESHOOTING.md#git-executable-is-missing-from-the-extraction-environment).
218
+
219
+ For linked-worktree provenance/history mismatches, check the installed version's
220
+ `WOODS_GIT_DIR` support and compare the selected branch and exact SHA inside the
221
+ extraction environment. Mount the complete shared `.git` at its original path
222
+ with no override, or select `/mounted-common/worktrees/<id>` within a relocated
223
+ complete mount. Derive `<id>` from Git metadata, not the branch name. Selecting
224
+ the shared root uses the primary checkout's HEAD and also changes incremental
225
+ ranges. A commit alone may leave the source-file watcher idle; run full
226
+ extraction after repair or when current Git history is required. See the
227
+ [worktree mount guide](https://github.com/lost-in-the/woods/blob/main/docs/TROUBLESHOOTING.md#git-directory-mounts-for-linked-worktrees).
228
+
180
229
  After a bundle change or removal of a dynamically defined job, incremental
181
230
  extraction can retain stale runtime units. Use a fresh process with the updated
182
231
  bundle for full extraction, then validate. For missing external gem paths,
@@ -205,13 +254,13 @@ Compare the client config with the exact command, absolute `cwd`, bundle, and in
205
254
  bundle exec woods-mcp-start ./tmp/woods
206
255
  ```
207
256
 
208
- If startup says `Could not resolve a published Woods index` (unreleased after
209
- `2.0.0.beta3`) or names a missing `manifest.json` on older versions, first check
257
+ If startup says `Could not resolve a published Woods index` (included in Woods
258
+ `2.0.0`) or names a missing `manifest.json` on older versions, first check
210
259
  the selected index path: an atomic index uses `generation.json` to locate its
211
260
  payload manifest. The new headline does not change index validation or recovery.
212
261
  Point at an existing index before suggesting a new extraction. Prefer the
213
- explicit path above; `WOODS_DIR` is also supported. An unreleased change after
214
- `2.0.0.beta3` adds `WOODS_OUTPUT` after those two choices, so verify the installed
262
+ explicit path above; `WOODS_DIR` is also supported. Woods `2.0.0` includes
263
+ `WOODS_OUTPUT` after those two choices, so verify the installed
215
264
  version's configuration guide before relying on that fallback.
216
265
 
217
266
  Then reconnect through the MCP client and call `woods_status`. Use client-native tool inspection after initialization. Expect 14 packaged Index tools, not all conditional schemas.
@@ -253,6 +302,18 @@ paging alone only visits the discovered prefix. See the
253
302
 
254
303
  ## 4. Check semantic retrieval
255
304
 
305
+ Do not treat structural `ready: true` or bootstrap `hydrated` as proof that
306
+ embeddings exist. Check the installed reader's capabilities: builds with #549
307
+ expose `woods_status.retriever.corpus`, including locally known vector and
308
+ metadata record counts by type. Missing fields or `null` counts mean unknown,
309
+ not zero. Counts include chunks and do not certify complete unit coverage.
310
+ When both stores are known empty, supporting readers return `empty_index` with
311
+ embed or explicit lexical-mode guidance. Follow the
312
+ [corpus diagnostic contract](https://github.com/lost-in-the/woods/blob/main/docs/RETRIEVAL_GUIDE.md#semantic-corpus-diagnostics).
313
+ Older readers need direct embedding-artifact checks; installing this plugin
314
+ does not update the serving gem. Keep reader revision and index writer version
315
+ separate when comparing results.
316
+
256
317
  Configured retrieval defaults (#446) are available in Woods `2.0.0.beta3`. For an installed
257
318
  version that supports them, an omitted tool budget uses the serving retriever's
258
319
  configured default; an explicit budget overrides it. Standalone MCP does not
@@ -296,6 +357,14 @@ endpoint to silence this warning.
296
357
 
297
358
  Console failures are live Rails/config/security failures, not Index failures. Verify authorized environment, Rails boot, `WOODS_CONSOLE_CONFIG` or direct `cwd`, blocked-table policy, credentials, and stderr.
298
359
 
360
+ For stdio parse errors or response mismatches during tool calls, check for Rails
361
+ logs on stdout. Through Woods `2.0.0.beta4`, stdout is restored after boot;
362
+ configure the Console process's logger to use stderr or a file. Runtime stdout
363
+ isolation is included in Woods `2.0.0`: verify a patched installed revision
364
+ before relying on it. Prefer `bundle exec rake woods:console`; direct Rails
365
+ runner invocation cannot capture output already emitted during Rails boot.
366
+ See the [Console logging diagnosis](https://github.com/lost-in-the/woods/blob/main/docs/CONSOLE_MCP_SETUP.md#rails-logs-break-mcp-protocol).
367
+
299
368
  For MySQL SQL refusals, inspect the executing session's `sql_mode` and the installed version's Console guide. Do not change quote modes to bypass a security refusal.
300
369
 
301
370
  For SQLite SQL refusals on `2.0.0.beta4` or a reviewed revision containing its Console corrections, consult the installed Console guide for supported identifier and table-reference syntax. Simplify the query to supported syntax; never relax the blocked-table or function policy. These builds also check resolved default scopes and scan normalized response values. Confirm a patched gem is published before recommending it, and check the installed version’s canonical Console guide; do not infer release availability from this plugin.
@@ -391,3 +460,14 @@ the named file before moving it aside, or choose a new export directory. Older v
391
460
  byte-identical generated assets into `_woods/ownership.json`; changed legacy sidecars may need this
392
461
  manual recovery. Never fabricate ownership receipts or remove personal files to silence the error.
393
462
  See the installed version's `docs/OBSIDIAN_INTEGRATION.md` for the exact safety contract.
463
+
464
+ ### Console read compatibility on security-patch candidates
465
+
466
+ Verify the loaded revision for corrections after 2.0.0. Active redaction refuses
467
+ SQL relation/CTE column alias lists; typed EAV matching can intentionally mask
468
+ extra values where tables share a final name or differ only by case. Keep the
469
+ policy enabled and use explicit scalar columns or structured tools. Check exact
470
+ sensitive-key spelling and configure binary secret columns for column redaction.
471
+ Read the [Console compatibility guide](https://github.com/lost-in-the/woods/blob/main/docs/CONSOLE_MCP_SETUP.md#read-policy-compatibility)
472
+ at the installed revision for adapter, timeout and projection limits; a plugin
473
+ update does not patch Woods.
@@ -16,7 +16,7 @@ Follow the [agent guide](https://github.com/lost-in-the/woods/blob/main/docs/AGE
16
16
  when instructions are absent. A registered tool does not establish retrieval
17
17
  readiness or authorize maintenance or live Console access.
18
18
 
19
- Call `woods_status` before relying on the index. Require a ready index with a current generation and non-zero counts for the types you need; use `codebase_retrieve` only when status reports retrieval enabled. If status is unhealthy or the generation predates the code under review, report that and ask the owner to run `woods:incremental` or `woods:extract` — do not present "not found" as proof the code does not exist.
19
+ Call `woods_status` before relying on the index. Require a ready index with a current generation and non-zero counts for the types you need; verify the retrieval mode and its data before using `codebase_retrieve` (see Conceptual questions below). If status is unhealthy or the generation predates the code under review, report that and ask the owner to run `woods:incremental` or `woods:extract` — do not present "not found" as proof the code does not exist.
20
20
 
21
21
  ## The default loop
22
22
 
@@ -31,9 +31,9 @@ Identifiers are namespaced and typed; never invent one from a filename when `sea
31
31
 
32
32
  - **Code review / change impact**: `lookup` the changed unit, then `dependents` at depth 1 before going deeper. Group results by relationship type and layer; report direct dependents separately from inferred downstream impact. A graph edge is not test coverage — select tests from mappings and repository search.
33
33
  - **Audit / architecture assessment**: `graph_analysis` for orphans, dead ends, hubs, cycles, bridges, cross-database edges, volatile dependencies, and undeclared package edges; `domain_clusters` for architectural domains; `pagerank` for high-impact units worth reading first.
34
- - **Investigating behavior / debugging**: `trace_flow` from the user-visible entry point (route, controller action, job, mailer, service), `lookup` at ambiguous steps, and verify anything conditional or dynamically dispatched in source and tests — do not infer call order from a dependency edge.
34
+ - **Investigating behavior / debugging**: find the exact indexed unit with `search` and `lookup`, then use `trace_flow` with `UnitIdentifier` or `UnitIdentifier#method` (for example, `CheckoutService#order`). Bare `order` names a unit, potentially a factory, rather than locating an application method. Receiverless local calls may remain unexpanded; inspect their source or trace the owning unit's method explicitly. Flow output is not proof of runtime execution or exhaustive call coverage. See the [flow workflow](https://github.com/lost-in-the/woods/blob/main/docs/AGENT_GUIDE.md#trace-a-feature-flow).
35
35
  - **Onboarding**: `structure` for the codebase overview, `lookup` and `dependencies`/`dependents` for a unit's neighborhood, and `domain_clusters` for the domain map, then the default loop on the units that matter.
36
- - **Conceptual questions**: `codebase_retrieve` when status says ready; govern with `budget` (never `limit`), then verify key units with `lookup`.
36
+ - **Conceptual questions**: check retrieval mode and data before `codebase_retrieve`; structural `ready` alone does not establish semantic availability. On readers supporting #549, inspect `retriever.corpus`; missing or unknown counts require checking embedding artifacts. Govern with `budget` (never `limit`), then verify key units with `lookup`.
37
37
 
38
38
  ## Boundaries
39
39
 
@@ -60,12 +60,12 @@ PORO and library targets. No dependents or test-only dependents do not establish
60
60
  absence of production callers; check source before making that claim.
61
61
 
62
62
  The response `graph_coverage` notice, `total_is_exact` field, and human label
63
- `witness types unambiguous` (#470/#471) are unreleased after Woods `2.0.0.beta3`.
63
+ `witness types unambiguous` (#470/#471) are included in Woods `2.0.0`.
64
64
  Verify the installed server version and actual response fields; this plugin does
65
65
  not add them. Apply these limits to older servers even without the notice.
66
66
  Supporting stdio and HTTP servers expose the paginated traversal payload in
67
67
  `structuredContent.data` independently of the text renderer (#481, also
68
- unreleased after `2.0.0.beta3`). Check the installed response; older default
68
+ included in Woods `2.0.0`). Check the installed response; older default
69
69
  responses may carry only text. Do not pass an unsupported `format` argument.
70
70
 
71
71
  `total_is_exact: false` means a budget-limited prefix; a true value describes only
@@ -106,7 +106,7 @@ recorded reachability does not establish observed execution. See the
106
106
 
107
107
  Pass explicit `limit` and `offset` when paging `graph_analysis`. Enforcing the
108
108
  advertised default of 20 rows per section and preserving total/offset on last
109
- and empty pages (#519) are unreleased after `2.0.0.beta3`; check the installed
109
+ and empty pages (#519) are included in Woods `2.0.0`; check the installed
110
110
  response rather than inferring support from the plugin version. On supporting
111
111
  servers, read `<section>_total` and `<section>_offset` in JSON, or the human
112
112
  pagination notice. An empty later page does not mean no findings. Totals count