carson 3.19.0 → 3.20.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36c68c06630159e0dca3560f62993befe8b50dd324fac5a6f8232895b2a667bd
4
- data.tar.gz: 1a4462a8b56b6b7fe505705c30ded47e24a279f4fa775baf56b05ae2e48f450a
3
+ metadata.gz: 0e7fdc44d330fd415b8f6d9ee479d5256d6fc860e5d63c7c9287f28ebbdb2ba4
4
+ data.tar.gz: 19056bd376bcab3b7077e38ba4e42f2b9f5a3ffbf2c8027f0aa6626bb70587fb
5
5
  SHA512:
6
- metadata.gz: 3f644435f5ca88527edc597a7b19134b701bc970748b5ef075f9b1e3c600b3be60724d80c8567bb919cf378a9a5da2dc54fcc27f29b0da811c0f3880c990f016
7
- data.tar.gz: 12e3651de47f5f741115c58308c2fe344d542bcc94c3f1234e08990b506b6ffa2c518e5f782ea9cb0a86a699111926c6df1f6571724b5ef3d318666889f2a13f
6
+ metadata.gz: 13f0d5ac83272711329d435a935dd940afc461c5c3b10e20d23842492e8e53f7032cf34a8b027d70db80563af148cda5f99c2c8e7ec4025b94f4d72609ea9d1d
7
+ data.tar.gz: b1903c6a8f8682adc7b942e49b553579d014d56c0bb65587465568119d41583a14e0cb4a7b00bc61b9165fac79094567395dbf2b39191e3c9d673dc0750dc4f5
data/RELEASE.md CHANGED
@@ -5,6 +5,17 @@ Release-note scope rule:
5
5
  - `RELEASE.md` records only version deltas, breaking changes, and migration actions.
6
6
  - Operational usage guides live in `MANUAL.md` and `API.md`.
7
7
 
8
+ ## 3.20.0
9
+
10
+ ### What changed
11
+
12
+ - **Cross-process CWD guard for worktree removal** — `worktree_remove!` and `sweep_stale_worktrees!` now detect when another process (e.g. an agent session) has its working directory inside a worktree before attempting removal. Uses `lsof -d cwd` to scan for cross-process CWD holds. Blocks removal with a clear recovery message instead of silently destroying the directory and crashing the other session's shell.
13
+ - **Code quality sweep** — fixed indentation errors in `deliver.rb` (`check_pr_ci` method). Converted single-parameter blocks to Ruby 3.4 `it` implicit parameter across `deliver.rb`, `govern.rb`, `setup.rb`, and all review submodules (`gate_support.rb`, `sweep_support.rb`, `utility.rb`). Added file-level purpose comments to review submodules (`gate_support.rb`, `sweep_support.rb`, `utility.rb`, `data_access.rb`, `query_text.rb`).
14
+
15
+ ### UX improvement
16
+
17
+ - Worktree removal is now safe across process boundaries. Previously, Carson only checked if the *current* process's CWD was inside the worktree. A separate cleanup process (hook, batch sweep, or another agent) could still remove a worktree while another session's shell was inside it, permanently crashing that shell. The new guard detects this scenario and refuses removal with actionable recovery advice. Fails safe: if `lsof` is unavailable, removal proceeds as before.
18
+
8
19
  ## 3.19.0
9
20
 
10
21
  ### What changed
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.19.0
1
+ 3.20.0
@@ -212,20 +212,20 @@ module Carson
212
212
  private
213
213
  def pr_and_check_report
214
214
  report = {
215
- generated_at: Time.now.utc.iso8601,
216
- branch: current_branch,
217
- status: "ok",
218
- skip_reason: nil,
219
- pr: nil,
220
- checks: {
221
- status: "unknown",
222
- skip_reason: nil,
223
- required_total: 0,
224
- failing_count: 0,
225
- pending_count: 0,
226
- failing: [],
227
- pending: []
228
- }
215
+ generated_at: Time.now.utc.iso8601,
216
+ branch: current_branch,
217
+ status: "ok",
218
+ skip_reason: nil,
219
+ pr: nil,
220
+ checks: {
221
+ status: "unknown",
222
+ skip_reason: nil,
223
+ required_total: 0,
224
+ failing_count: 0,
225
+ pending_count: 0,
226
+ failing: [],
227
+ pending: []
228
+ }
229
229
  }
230
230
  unless gh_available?
231
231
  report[ :status ] = "skipped"
@@ -243,11 +243,11 @@ module Carson
243
243
  end
244
244
  pr_data = JSON.parse( pr_stdout )
245
245
  report[ :pr ] = {
246
- number: pr_data[ "number" ],
247
- title: pr_data[ "title" ].to_s,
248
- url: pr_data[ "url" ].to_s,
249
- state: pr_data[ "state" ].to_s,
250
- review_decision: blank_to( value: pr_data[ "reviewDecision" ], default: "NONE" )
246
+ number: pr_data[ "number" ],
247
+ title: pr_data[ "title" ].to_s,
248
+ url: pr_data[ "url" ].to_s,
249
+ state: pr_data[ "state" ].to_s,
250
+ review_decision: blank_to( value: pr_data[ "reviewDecision" ], default: "NONE" )
251
251
  }
252
252
  puts_verbose "pr: ##{report.dig( :pr, :number )} #{report.dig( :pr, :title )}"
253
253
  puts_verbose "url: #{report.dig( :pr, :url )}"
@@ -287,22 +287,22 @@ module Carson
287
287
  # Evaluates default-branch CI health so stale workflow drift blocks before merge.
288
288
  def default_branch_ci_baseline_report
289
289
  report = {
290
- status: "ok",
291
- skip_reason: nil,
292
- repository: nil,
293
- default_branch: nil,
294
- head_sha: nil,
295
- workflows_total: 0,
296
- check_runs_total: 0,
297
- failing_count: 0,
298
- pending_count: 0,
299
- advisory_failing_count: 0,
300
- advisory_pending_count: 0,
301
- no_check_evidence: false,
302
- failing: [],
303
- pending: [],
304
- advisory_failing: [],
305
- advisory_pending: []
290
+ status: "ok",
291
+ skip_reason: nil,
292
+ repository: nil,
293
+ default_branch: nil,
294
+ head_sha: nil,
295
+ workflows_total: 0,
296
+ check_runs_total: 0,
297
+ failing_count: 0,
298
+ pending_count: 0,
299
+ advisory_failing_count: 0,
300
+ advisory_pending_count: 0,
301
+ no_check_evidence: false,
302
+ failing: [],
303
+ pending: [],
304
+ advisory_failing: [],
305
+ advisory_pending: []
306
306
  }
307
307
  unless gh_available?
308
308
  report[ :status ] = "skipped"
@@ -313,30 +313,30 @@ module Carson
313
313
  owner, repo = repository_coordinates
314
314
  report[ :repository ] = "#{owner}/#{repo}"
315
315
  repository_data = gh_json_payload!(
316
- "api", "repos/#{owner}/#{repo}",
317
- "--method", "GET",
318
- fallback: "unable to read repository metadata for #{owner}/#{repo}"
316
+ "api", "repos/#{owner}/#{repo}",
317
+ "--method", "GET",
318
+ fallback: "unable to read repository metadata for #{owner}/#{repo}"
319
319
  )
320
320
  default_branch = blank_to( value: repository_data[ "default_branch" ], default: config.main_branch )
321
321
  report[ :default_branch ] = default_branch
322
322
  branch_data = gh_json_payload!(
323
- "api", "repos/#{owner}/#{repo}/branches/#{CGI.escape( default_branch )}",
324
- "--method", "GET",
325
- fallback: "unable to read default branch #{default_branch}"
323
+ "api", "repos/#{owner}/#{repo}/branches/#{CGI.escape( default_branch )}",
324
+ "--method", "GET",
325
+ fallback: "unable to read default branch #{default_branch}"
326
326
  )
327
327
  head_sha = branch_data.dig( "commit", "sha" ).to_s.strip
328
328
  raise "default branch #{default_branch} has no commit SHA" if head_sha.empty?
329
329
  report[ :head_sha ] = head_sha
330
330
  workflow_entries = default_branch_workflow_entries(
331
- owner: owner,
332
- repo: repo,
333
- default_branch: default_branch
331
+ owner: owner,
332
+ repo: repo,
333
+ default_branch: default_branch
334
334
  )
335
335
  report[ :workflows_total ] = workflow_entries.count
336
336
  check_runs_payload = gh_json_payload!(
337
- "api", "repos/#{owner}/#{repo}/commits/#{head_sha}/check-runs",
338
- "--method", "GET",
339
- fallback: "unable to read check-runs for #{default_branch}@#{head_sha}"
337
+ "api", "repos/#{owner}/#{repo}/commits/#{head_sha}/check-runs",
338
+ "--method", "GET",
339
+ fallback: "unable to read check-runs for #{default_branch}@#{head_sha}"
340
340
  )
341
341
  check_runs = Array( check_runs_payload[ "check_runs" ] )
342
342
  failing, pending = partition_default_branch_check_runs( check_runs: check_runs )
@@ -399,15 +399,15 @@ module Carson
399
399
  # Reads workflow files from default branch; missing workflow directory is valid and returns none.
400
400
  def default_branch_workflow_entries( owner:, repo:, default_branch: )
401
401
  stdout_text, stderr_text, success, = gh_run(
402
- "api", "repos/#{owner}/#{repo}/contents/.github/workflows",
403
- "--method", "GET",
404
- "-f", "ref=#{default_branch}"
402
+ "api", "repos/#{owner}/#{repo}/contents/.github/workflows",
403
+ "--method", "GET",
404
+ "-f", "ref=#{default_branch}"
405
405
  )
406
406
  unless success
407
407
  error_text = gh_error_text(
408
- stdout_text: stdout_text,
409
- stderr_text: stderr_text,
410
- fallback: "unable to read workflow files for #{default_branch}"
408
+ stdout_text: stdout_text,
409
+ stderr_text: stderr_text,
410
+ fallback: "unable to read workflow files for #{default_branch}"
411
411
  )
412
412
  return [] if error_text.match?( /\b404\b/ )
413
413
  raise error_text
@@ -474,10 +474,10 @@ module Carson
474
474
  blank_to( value: entry[ "status" ], default: "UNKNOWN" )
475
475
  end
476
476
  {
477
- workflow: blank_to( value: entry.dig( "app", "name" ), default: "workflow" ),
478
- name: blank_to( value: entry[ "name" ], default: "check" ),
479
- state: state.upcase,
480
- link: entry[ "html_url" ].to_s
477
+ workflow: blank_to( value: entry.dig( "app", "name" ), default: "workflow" ),
478
+ name: blank_to( value: entry[ "name" ], default: "check" ),
479
+ state: state.upcase,
480
+ link: entry[ "html_url" ].to_s
481
481
  }
482
482
  end
483
483
  end
@@ -210,27 +210,27 @@ module Carson
210
210
 
211
211
  # Generates a default PR title from the branch name.
212
212
  def default_pr_title( branch: )
213
- branch.tr( "-", " " ).gsub( "/", ": " ).sub( /\A\w/ ) { |c| c.upcase }
213
+ branch.tr( "-", " " ).gsub( "/", ": " ).sub( /\A\w/ ) { it.upcase }
214
214
  end
215
215
 
216
216
  # Checks CI status on a PR. Returns :pass, :fail, :pending, or :none.
217
- # Uses the `bucket` field (pass/fail/pending) from `gh pr checks --json`.
218
- def check_pr_ci( number: )
219
- stdout, _, success, = gh_run(
220
- "pr", "checks", number.to_s,
221
- "--json", "name,bucket"
222
- )
223
- return :none unless success
224
-
225
- checks = JSON.parse( stdout ) rescue []
226
- return :none if checks.empty?
227
-
228
- buckets = checks.map { |c| c[ "bucket" ].to_s.downcase }
229
- return :fail if buckets.include?( "fail" )
230
- return :pending if buckets.include?( "pending" )
231
-
232
- :pass
233
- end
217
+ # Uses the `bucket` field (pass/fail/pending) from `gh pr checks --json`.
218
+ def check_pr_ci( number: )
219
+ stdout, _, success, = gh_run(
220
+ "pr", "checks", number.to_s,
221
+ "--json", "name,bucket"
222
+ )
223
+ return :none unless success
224
+
225
+ checks = JSON.parse( stdout ) rescue []
226
+ return :none if checks.empty?
227
+
228
+ buckets = checks.map { it[ "bucket" ].to_s.downcase }
229
+ return :fail if buckets.include?( "fail" )
230
+ return :pending if buckets.include?( "pending" )
231
+
232
+ :pass
233
+ end
234
234
 
235
235
  # Checks review decision on a PR. Returns :approved, :changes_requested, :review_required, or :none.
236
236
  def check_pr_review( number: )
@@ -298,8 +298,8 @@ end
298
298
  def compute_post_merge_next_step!( result: )
299
299
  main_root = main_worktree_root
300
300
  cwd = realpath_safe( Dir.pwd )
301
- current_wt = worktree_list.select { |wt| wt.fetch( :path ) != realpath_safe( main_root ) }
302
- .find { |wt| cwd == wt.fetch( :path ) || cwd.start_with?( File.join( wt.fetch( :path ), "" ) ) }
301
+ current_wt = worktree_list.select { it.fetch( :path ) != realpath_safe( main_root ) }
302
+ .find { cwd == it.fetch( :path ) || cwd.start_with?( File.join( it.fetch( :path ), "" ) ) }
303
303
 
304
304
  if current_wt
305
305
  wt_name = File.basename( current_wt.fetch( :path ) )
@@ -208,10 +208,10 @@ module Carson
208
208
  checks = Array( pr[ "statusCheckRollup" ] )
209
209
  return :green if checks.empty?
210
210
 
211
- has_failure = checks.any? { |c| check_state_failing?( state: c[ "state" ].to_s ) || check_conclusion_failing?( conclusion: c[ "conclusion" ].to_s ) }
211
+ has_failure = checks.any? { check_state_failing?( state: it[ "state" ].to_s ) || check_conclusion_failing?( conclusion: it[ "conclusion" ].to_s ) }
212
212
  return :red if has_failure
213
213
 
214
- has_pending = checks.any? { |c| check_state_pending?( state: c[ "state" ].to_s ) }
214
+ has_pending = checks.any? { check_state_pending?( state: it[ "state" ].to_s ) }
215
215
  return :pending if has_pending
216
216
 
217
217
  :green
@@ -1,7 +1,8 @@
1
1
  # Safe worktree lifecycle management for coding agents.
2
2
  # Two operations: create and remove. Create auto-syncs main before branching.
3
- # Remove is safe by default — guards against CWD-inside-worktree and unpushed
4
- # commits. Content-aware: allows removal after squash/rebase merge without --force.
3
+ # Remove is safe by default — guards against CWD-inside-worktree, cross-process
4
+ # CWD holds, and unpushed commits. Content-aware: allows removal after
5
+ # squash/rebase merge without --force.
5
6
  # Supports --json for machine-readable structured output with recovery commands.
6
7
  module Carson
7
8
  class Runtime
@@ -78,12 +79,12 @@ module Carson
78
79
  resolved_path = resolve_worktree_path( worktree_path: worktree_path )
79
80
 
80
81
  # Missing directory: worktree was destroyed externally (e.g. gh pr merge
81
- # --delete-branch). Clean up the stale git registration and delete the branch.
82
- if !Dir.exist?( resolved_path ) && worktree_registered?( path: resolved_path )
83
- return worktree_remove_missing!( resolved_path: resolved_path, json_output: json_output )
84
- end
82
+ # --delete-branch). Clean up the stale git registration and delete the branch.
83
+ if !Dir.exist?( resolved_path ) && worktree_registered?( path: resolved_path )
84
+ return worktree_remove_missing!( resolved_path: resolved_path, json_output: json_output )
85
+ end
85
86
 
86
- unless worktree_registered?( path: resolved_path )
87
+ unless worktree_registered?( path: resolved_path )
87
88
  return worktree_finish(
88
89
  result: { command: "worktree remove", status: "error", name: File.basename( resolved_path ),
89
90
  error: "#{resolved_path} is not a registered worktree",
@@ -104,6 +105,18 @@ module Carson
104
105
  )
105
106
  end
106
107
 
108
+ # Safety: refuse if another process has its CWD inside the worktree.
109
+ # Protects against cross-process CWD crashes (e.g. an agent session
110
+ # removed by a separate cleanup process while the agent's shell is inside).
111
+ if worktree_held_by_other_process?( worktree_path: resolved_path )
112
+ return worktree_finish(
113
+ result: { command: "worktree remove", status: "block", name: File.basename( resolved_path ),
114
+ error: "another process has its working directory inside this worktree",
115
+ recovery: "wait for the other session to finish, then retry" },
116
+ exit_code: EXIT_BLOCK, json_output: json_output
117
+ )
118
+ end
119
+
107
120
  branch = worktree_branch( path: resolved_path )
108
121
  puts_verbose "worktree_remove: path=#{resolved_path} branch=#{branch} force=#{force}"
109
122
 
@@ -195,6 +208,7 @@ module Carson
195
208
  next unless branch
196
209
  next unless agent_prefixes.any? { |prefix| path.start_with?( prefix ) }
197
210
  next if cwd_inside_worktree?( worktree_path: path )
211
+ next if worktree_held_by_other_process?( worktree_path: path )
198
212
  next unless branch_absorbed_into_main?( branch: branch )
199
213
 
200
214
  # Remove the worktree (no --force: refuses if dirty working tree).
@@ -303,6 +317,35 @@ module Carson
303
317
  false
304
318
  end
305
319
 
320
+ # Checks whether any other process has its working directory inside the worktree.
321
+ # Uses lsof to query CWD file descriptors system-wide, then matches against
322
+ # the worktree path. Catches the cross-process CWD crash scenario: a cleanup
323
+ # process removing a worktree while another session's shell is still inside it.
324
+ # Fails safe: returns false if lsof is unavailable or any error occurs.
325
+ def worktree_held_by_other_process?( worktree_path: )
326
+ canonical = realpath_safe( worktree_path )
327
+ return false if canonical.nil? || canonical.empty?
328
+ return false unless Dir.exist?( canonical )
329
+
330
+ stdout, _, status = Open3.capture3( "lsof", "-d", "cwd" )
331
+ return false unless status.success?
332
+
333
+ normalised = File.join( canonical, "" )
334
+ my_pid = Process.pid
335
+ stdout.lines.drop( 1 ).any? do |line|
336
+ fields = line.strip.split( /\s+/ )
337
+ next false unless fields.length >= 9
338
+ next false if fields[ 1 ].to_i == my_pid
339
+ name = fields[ 8.. ].join( " " )
340
+ name == canonical || name.start_with?( normalised )
341
+ end
342
+ rescue Errno::ENOENT
343
+ # lsof not installed.
344
+ false
345
+ rescue StandardError
346
+ false
347
+ end
348
+
306
349
  # Checks whether a branch has unpushed commits that would be lost on removal.
307
350
  # Content-aware: after squash/rebase merge, SHAs differ but tree content may match main. Compares content, not SHAs.
308
351
  # Returns nil if safe, or { error:, recovery: } hash if unpushed work exists.
@@ -1,3 +1,4 @@
1
+ # GraphQL data access: PR details, pagination, and normalisation for review gate and sweep.
1
2
  module Carson
2
3
  class Runtime
3
4
  module Review
@@ -1,3 +1,4 @@
1
+ # Review gate logic: snapshot convergence, disposition acknowledgements, and merge-readiness checks.
1
2
  module Carson
2
3
  class Runtime
3
4
  module Review
@@ -30,7 +31,7 @@ module Carson
30
31
  unresolved_threads = unresolved_thread_entries( details: details )
31
32
  actionable_top_level = actionable_top_level_items( details: details, pr_author: pr_author )
32
33
  acknowledgements = disposition_acknowledgements( details: details, pr_author: pr_author )
33
- unacknowledged_actionable = actionable_top_level.reject { |item| acknowledged_by_disposition?( item: item, acknowledgements: acknowledgements ) }
34
+ unacknowledged_actionable = actionable_top_level.reject { acknowledged_by_disposition?( item: it, acknowledgements: acknowledgements ) }
34
35
  {
35
36
  latest_activity: latest_review_activity( details: details ),
36
37
  unresolved_threads: unresolved_threads,
@@ -44,8 +45,8 @@ module Carson
44
45
  def review_gate_signature( snapshot: )
45
46
  {
46
47
  latest_activity: snapshot.fetch( :latest_activity ).to_s,
47
- unresolved_urls: snapshot.fetch( :unresolved_threads ).map { |entry| entry.fetch( :url ) }.sort,
48
- unacknowledged_urls: snapshot.fetch( :unacknowledged_actionable ).map { |entry| entry.fetch( :url ) }.sort
48
+ unresolved_urls: snapshot.fetch( :unresolved_threads ).map { it.fetch( :url ) }.sort,
49
+ unacknowledged_urls: snapshot.fetch( :unacknowledged_actionable ).map { it.fetch( :url ) }.sort
49
50
  }
50
51
  end
51
52
 
@@ -67,7 +68,7 @@ module Carson
67
68
  end
68
69
 
69
70
  def bot_username?( author: )
70
- config.review_bot_usernames.any? { |bot| bot.downcase == author.to_s.downcase }
71
+ config.review_bot_usernames.any? { it.downcase == author.to_s.downcase }
71
72
  end
72
73
 
73
74
  def unresolved_thread_entries( details: )
@@ -78,7 +79,7 @@ module Carson
78
79
  comments = thread.fetch( :comments )
79
80
  first_comment = comments.first || {}
80
81
  next if bot_username?( author: first_comment.fetch( :author, "" ) )
81
- latest_time = comments.map { |entry| entry.fetch( :created_at ) }.max.to_s
82
+ latest_time = comments.map { it.fetch( :created_at ) }.max.to_s
82
83
  {
83
84
  url: blank_to( value: first_comment.fetch( :url, "" ), default: "#{details.fetch( :url )}#thread-#{index + 1}" ),
84
85
  author: first_comment.fetch( :author, "" ),
@@ -130,7 +131,7 @@ module Carson
130
131
  sources = []
131
132
  sources.concat( Array( details.fetch( :comments ) ) )
132
133
  sources.concat( Array( details.fetch( :reviews ) ) )
133
- sources.concat( Array( details.fetch( :review_threads ) ).flat_map { |thread| thread.fetch( :comments ) } )
134
+ sources.concat( Array( details.fetch( :review_threads ) ).flat_map { it.fetch( :comments ) } )
134
135
  sources.map do |entry|
135
136
  next unless entry.fetch( :author, "" ) == pr_author
136
137
  body = entry.fetch( :body, "" ).to_s
@@ -151,7 +152,7 @@ module Carson
151
152
  # True when any disposition acknowledgement references the specific finding URL.
152
153
  def acknowledged_by_disposition?( item:, acknowledgements: )
153
154
  acknowledgements.any? do |ack|
154
- Array( ack.fetch( :target_urls ) ).any? { |url| url == item.fetch( :url ) }
155
+ Array( ack.fetch( :target_urls ) ).any? { it == item.fetch( :url ) }
155
156
  end
156
157
  end
157
158
 
@@ -159,10 +160,10 @@ module Carson
159
160
  def latest_review_activity( details: )
160
161
  timestamps = []
161
162
  timestamps << details.fetch( :updated_at )
162
- timestamps.concat( Array( details.fetch( :comments ) ).map { |entry| entry.fetch( :created_at ) } )
163
- timestamps.concat( Array( details.fetch( :reviews ) ).map { |entry| entry.fetch( :created_at ) } )
164
- timestamps.concat( Array( details.fetch( :review_threads ) ).flat_map { |thread| thread.fetch( :comments ) }.map { |entry| entry.fetch( :created_at ) } )
165
- timestamps.map { |text| parse_time_or_nil( text: text ) }.compact.max&.utc&.iso8601
163
+ timestamps.concat( Array( details.fetch( :comments ) ).map { it.fetch( :created_at ) } )
164
+ timestamps.concat( Array( details.fetch( :reviews ) ).map { it.fetch( :created_at ) } )
165
+ timestamps.concat( Array( details.fetch( :review_threads ) ).flat_map { it.fetch( :comments ) }.map { it.fetch( :created_at ) } )
166
+ timestamps.map { parse_time_or_nil( text: it ) }.compact.max&.utc&.iso8601
166
167
  end
167
168
 
168
169
  # Writes review gate artefacts using fixed report names in global report output.
@@ -208,7 +209,7 @@ module Carson
208
209
  if report.fetch( :block_reasons ).empty?
209
210
  lines << "- none"
210
211
  else
211
- report.fetch( :block_reasons ).each { |reason| lines << "- #{reason}" }
212
+ report.fetch( :block_reasons ).each { lines << "- #{it}" }
212
213
  end
213
214
  lines << ""
214
215
  lines << "## Unresolved Threads"
@@ -1,3 +1,4 @@
1
+ # GraphQL query templates for pull request review data retrieval.
1
2
  module Carson
2
3
  class Runtime
3
4
  module Review
@@ -1,3 +1,4 @@
1
+ # Review sweep: late-event detection, tracking issue management, and sweep reporting.
1
2
  module Carson
2
3
  class Runtime
3
4
  module Review
@@ -59,7 +60,7 @@ module Carson
59
60
  )
60
61
  end
61
62
 
62
- Array( details.fetch( :review_threads ) ).flat_map { |thread| thread.fetch( :comments ) }.each do |comment|
63
+ Array( details.fetch( :review_threads ) ).flat_map { it.fetch( :comments ) }.each do |comment|
63
64
  next if comment.fetch( :author ) == pr_author
64
65
  next if bot_username?( author: comment.fetch( :author ) )
65
66
  hits = matched_risk_keywords( text: comment.fetch( :body ) )
@@ -152,7 +153,7 @@ module Carson
152
153
  stdout_text, stderr_text, success, = gh_run( "issue", "list", "--repo", repo_slug, "--state", "all", "--limit", "100", "--json", "number,title,state,url,labels" )
153
154
  raise gh_error_text( stdout_text: stdout_text, stderr_text: stderr_text, fallback: "unable to list issues for review sweep" ) unless success
154
155
  issues = Array( JSON.parse( stdout_text ) )
155
- node = issues.find { |entry| entry[ "title" ].to_s == config.review_tracking_issue_title }
156
+ node = issues.find { it[ "title" ].to_s == config.review_tracking_issue_title }
156
157
  return nil if node.nil?
157
158
  {
158
159
  number: node[ "number" ],
@@ -1,3 +1,4 @@
1
+ # Review utilities: risk keyword matching, disposition parsing, URL extraction, and deduplication.
1
2
  module Carson
2
3
  class Runtime
3
4
  module Review
@@ -24,7 +25,7 @@ module Carson
24
25
 
25
26
  # GitHub URL extraction for mapping disposition acknowledgements to finding URLs.
26
27
  def extract_github_urls( text: )
27
- text.to_s.scan( %r{https://github\.com/[^\s\)\]]+} ).map { |value| value.sub( /[.,;:]+$/, "" ) }.uniq
28
+ text.to_s.scan( %r{https://github\.com/[^\s\)\]]+} ).map { it.sub( /[.,;:]+$/, "" ) }.uniq
28
29
  end
29
30
 
30
31
  # Parse RFC3339 timestamps and return nil on blank/invalid values.
@@ -137,7 +137,7 @@ module Carson
137
137
  { label: "branch — enforce PR-only merges (default)", value: "branch" },
138
138
  { label: "trunk — commit directly to main", value: "trunk" }
139
139
  ]
140
- default_index = options.index { |o| o.fetch( :value ) == current } || 0
140
+ default_index = options.index { it.fetch( :value ) == current } || 0
141
141
  prompt_choice( options: options, default: default_index )
142
142
  end
143
143
 
@@ -151,7 +151,7 @@ module Carson
151
151
  { label: "rebase — linear history, individual commits", value: "rebase" },
152
152
  { label: "merge — merge commits", value: "merge" }
153
153
  ]
154
- default_index = options.index { |o| o.fetch( :value ) == current } || 0
154
+ default_index = options.index { it.fetch( :value ) == current } || 0
155
155
  prompt_choice( options: options, default: default_index )
156
156
  end
157
157
 
@@ -219,7 +219,7 @@ module Carson
219
219
  others << entry
220
220
  end
221
221
  end
222
- well_known.sort_by { |e| WELL_KNOWN_REMOTES.index( e.fetch( :name ) ) || 999 } + others.sort_by { |e| e.fetch( :name ) }
222
+ well_known.sort_by { WELL_KNOWN_REMOTES.index( it.fetch( :name ) ) || 999 } + others.sort_by { it.fetch( :name ) }
223
223
  end
224
224
 
225
225
  # Normalises a remote URL so SSH and HTTPS variants of the same host/path compare equal.
@@ -295,7 +295,7 @@ module Carson
295
295
 
296
296
  def detect_git_remote
297
297
  remotes = list_git_remotes
298
- remote_names = remotes.map { |entry| entry.fetch( :name ) }
298
+ remote_names = remotes.map { it.fetch( :name ) }
299
299
  return nil if remote_names.empty?
300
300
 
301
301
  return config.git_remote if remote_names.include?( config.git_remote )
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: carson
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.19.0
4
+ version: 3.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Hailei Wang