carson 3.22.0 → 3.23.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 +4 -4
- data/API.md +19 -20
- data/MANUAL.md +76 -65
- data/README.md +42 -50
- data/RELEASE.md +24 -1
- data/SKILL.md +1 -1
- data/VERSION +1 -1
- data/carson.gemspec +3 -4
- data/hooks/command-guard +1 -1
- data/hooks/pre-push +17 -20
- data/lib/carson/adapters/agent.rb +2 -2
- data/lib/carson/branch.rb +38 -0
- data/lib/carson/cli.rb +45 -30
- data/lib/carson/config.rb +80 -29
- data/lib/carson/delivery.rb +64 -0
- data/lib/carson/ledger.rb +305 -0
- data/lib/carson/repository.rb +47 -0
- data/lib/carson/revision.rb +30 -0
- data/lib/carson/runtime/audit.rb +43 -17
- data/lib/carson/runtime/deliver.rb +163 -149
- data/lib/carson/runtime/govern.rb +233 -357
- data/lib/carson/runtime/housekeep.rb +233 -27
- data/lib/carson/runtime/local/onboard.rb +29 -29
- data/lib/carson/runtime/local/prune.rb +120 -35
- data/lib/carson/runtime/local/sync.rb +29 -7
- data/lib/carson/runtime/local/template.rb +30 -12
- data/lib/carson/runtime/local/worktree.rb +37 -442
- data/lib/carson/runtime/review/gate_support.rb +144 -12
- data/lib/carson/runtime/review/sweep_support.rb +2 -2
- data/lib/carson/runtime/review/utility.rb +1 -1
- data/lib/carson/runtime/review.rb +21 -77
- data/lib/carson/runtime/setup.rb +25 -33
- data/lib/carson/runtime/status.rb +96 -212
- data/lib/carson/runtime.rb +39 -4
- data/lib/carson/worktree.rb +497 -0
- data/lib/carson.rb +6 -0
- metadata +37 -17
- data/.github/copilot-instructions.md +0 -1
- data/.github/pull_request_template.md +0 -12
- data/templates/.github/AGENTS.md +0 -1
- data/templates/.github/CLAUDE.md +0 -1
- data/templates/.github/carson.md +0 -47
- data/templates/.github/copilot-instructions.md +0 -1
- data/templates/.github/pull_request_template.md +0 -12
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
# Agent
|
|
2
|
-
# Gathers branch, worktrees, open PRs, stale branches,
|
|
3
|
-
# governance health, and version. Supports human-readable and JSON output.
|
|
1
|
+
# Agent-readable repository status centred on branch deliveries.
|
|
4
2
|
module Carson
|
|
5
3
|
class Runtime
|
|
6
4
|
module Status
|
|
7
|
-
# Entry point for `carson status`.
|
|
5
|
+
# Entry point for `carson status`.
|
|
8
6
|
def status!( json_output: false )
|
|
9
7
|
data = gather_status
|
|
10
8
|
|
|
@@ -19,63 +17,33 @@ module Carson
|
|
|
19
17
|
|
|
20
18
|
# Portfolio-wide status overview across all governed repositories.
|
|
21
19
|
def status_all!( json_output: false )
|
|
22
|
-
|
|
23
|
-
if
|
|
20
|
+
repositories = config.govern_repos
|
|
21
|
+
if repositories.empty?
|
|
24
22
|
puts_line "No governed repositories configured."
|
|
25
23
|
puts_line " Run carson onboard in each repo to register."
|
|
26
24
|
return EXIT_ERROR
|
|
27
25
|
end
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
repo_name
|
|
33
|
-
|
|
34
|
-
results << { name: repo_name, status: "error", error: "path not found" }
|
|
35
|
-
next
|
|
36
|
-
end
|
|
27
|
+
results = repositories.map do |repo_path|
|
|
28
|
+
repo_name = File.basename( repo_path )
|
|
29
|
+
unless Dir.exist?( repo_path )
|
|
30
|
+
{ name: repo_name, status: "error", error: "not found" }
|
|
31
|
+
else
|
|
37
32
|
begin
|
|
38
33
|
scoped_runtime = build_scoped_runtime( repo_path: repo_path )
|
|
39
|
-
|
|
40
|
-
results << { name: repo_name, status: "ok" }.merge( data )
|
|
34
|
+
{ name: repo_name, status: "ok" }.merge( scoped_runtime.send( :gather_status ) )
|
|
41
35
|
rescue StandardError => exception
|
|
42
|
-
|
|
36
|
+
{ name: repo_name, status: "error", error: exception.message }
|
|
43
37
|
end
|
|
44
38
|
end
|
|
45
|
-
output.puts JSON.pretty_generate( { command: "status", repos: results } )
|
|
46
|
-
return EXIT_OK
|
|
47
39
|
end
|
|
48
40
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
unless Dir.exist?( repo_path )
|
|
56
|
-
puts_line "#{repo_name}: MISSING"
|
|
57
|
-
next
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
begin
|
|
61
|
-
scoped_runtime = build_scoped_runtime( repo_path: repo_path )
|
|
62
|
-
data = scoped_runtime.send( :gather_status )
|
|
63
|
-
branch = data.fetch( :branch )
|
|
64
|
-
dirty = branch.fetch( :dirty ) ? " (dirty)" : ""
|
|
65
|
-
worktrees = data.fetch( :worktrees )
|
|
66
|
-
gov = data.fetch( :governance )
|
|
67
|
-
parts = []
|
|
68
|
-
parts << branch.fetch( :name ) + dirty
|
|
69
|
-
parts << "#{worktrees.count} worktree#{plural_suffix( count: worktrees.count )}" if worktrees.any?
|
|
70
|
-
parts << "templates #{gov.fetch( :templates )}" unless gov.fetch( :templates ) == :in_sync
|
|
71
|
-
puts_line "#{repo_name}: #{parts.join( ' ' )}"
|
|
72
|
-
|
|
73
|
-
# Show pending operations for this repo.
|
|
74
|
-
repo_pending = status_pending_for_repo( all_pending: all_pending, repo_path: repo_path )
|
|
75
|
-
repo_pending.each { |description| puts_line " pending: #{description}" }
|
|
76
|
-
rescue StandardError => exception
|
|
77
|
-
puts_line "#{repo_name}: FAIL (#{exception.message})"
|
|
78
|
-
end
|
|
41
|
+
if json_output
|
|
42
|
+
output.puts JSON.pretty_generate( { command: "status", repos: results, repositories: results } )
|
|
43
|
+
else
|
|
44
|
+
puts_line "Carson #{Carson::VERSION} — Portfolio (#{repositories.length} repo#{plural_suffix( count: repositories.length )})"
|
|
45
|
+
puts_line ""
|
|
46
|
+
results.each { |result| print_portfolio_status( result: result ) }
|
|
79
47
|
end
|
|
80
48
|
|
|
81
49
|
EXIT_OK
|
|
@@ -83,141 +51,77 @@ module Carson
|
|
|
83
51
|
|
|
84
52
|
private
|
|
85
53
|
|
|
86
|
-
# Returns an array of human-readable pending descriptions for a repo.
|
|
87
|
-
def status_pending_for_repo( all_pending:, repo_path: )
|
|
88
|
-
descriptions = []
|
|
89
|
-
all_pending.each do |command, repos|
|
|
90
|
-
next unless repos.is_a?( Hash ) && repos.key?( repo_path )
|
|
91
|
-
|
|
92
|
-
info = repos[ repo_path ]
|
|
93
|
-
attempts = info.fetch( "attempts", 0 )
|
|
94
|
-
skipped_at = info.fetch( "skipped_at", nil )
|
|
95
|
-
time_part = skipped_at ? ", since #{skipped_at[ 11..15 ]}" : ""
|
|
96
|
-
descriptions << "#{command} (#{attempts} attempt#{attempts == 1 ? '' : 's'}#{time_part})"
|
|
97
|
-
end
|
|
98
|
-
descriptions
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
# Collects all status facets into a structured hash.
|
|
102
54
|
def gather_status
|
|
103
|
-
|
|
55
|
+
repository = repository_record
|
|
56
|
+
branch = branch_record
|
|
57
|
+
deliveries = ledger.active_deliveries( repo_path: repo_root )
|
|
58
|
+
|
|
59
|
+
{
|
|
104
60
|
version: Carson::VERSION,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
61
|
+
repository: {
|
|
62
|
+
name: repository.name,
|
|
63
|
+
path: repository.path,
|
|
64
|
+
authority: repository.authority
|
|
65
|
+
},
|
|
66
|
+
branch: {
|
|
67
|
+
name: branch.name,
|
|
68
|
+
head: branch.head,
|
|
69
|
+
worktree: branch.worktree,
|
|
70
|
+
dirty: working_tree_dirty?,
|
|
71
|
+
dirty_reason: dirty_worktree_reason,
|
|
72
|
+
sync: remote_sync_status( branch: branch.name )
|
|
73
|
+
},
|
|
74
|
+
branches: deliveries.map { |delivery| status_branch_entry( delivery: delivery ) },
|
|
75
|
+
stale_branches: gather_stale_branch_info
|
|
108
76
|
}
|
|
109
|
-
|
|
110
|
-
# PR and stale branch data require gh — gather with graceful fallback.
|
|
111
|
-
if gh_available?
|
|
112
|
-
data[ :pull_requests ] = gather_pr_info
|
|
113
|
-
data[ :stale_branches ] = gather_stale_branch_info
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
data
|
|
117
77
|
end
|
|
118
78
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
79
|
+
def status_branch_entry( delivery: )
|
|
80
|
+
{
|
|
81
|
+
branch: delivery.branch,
|
|
82
|
+
worktree_path: delivery.worktree_path,
|
|
83
|
+
head: delivery.head,
|
|
84
|
+
pr_number: delivery.pull_request_number,
|
|
85
|
+
delivery_state: delivery.status,
|
|
86
|
+
revision_count: delivery.revision_count,
|
|
87
|
+
summary: delivery.summary,
|
|
88
|
+
updated_at: delivery.updated_at
|
|
89
|
+
}
|
|
126
90
|
end
|
|
127
91
|
|
|
128
|
-
# Returns true when the working tree has uncommitted changes.
|
|
129
92
|
def working_tree_dirty?
|
|
130
93
|
stdout, _, success, = git_run( "status", "--porcelain" )
|
|
131
94
|
return true unless success
|
|
132
95
|
!stdout.strip.empty?
|
|
133
96
|
end
|
|
134
97
|
|
|
135
|
-
|
|
136
|
-
|
|
98
|
+
def dirty_worktree_reason
|
|
99
|
+
return nil unless working_tree_dirty?
|
|
100
|
+
return "main_worktree" if main_worktree_context?
|
|
101
|
+
|
|
102
|
+
"working_tree"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def main_worktree_context?
|
|
106
|
+
realpath_safe( repo_root ) == realpath_safe( main_worktree_root )
|
|
107
|
+
end
|
|
108
|
+
|
|
137
109
|
def remote_sync_status( branch: )
|
|
138
110
|
remote = config.git_remote
|
|
139
111
|
remote_ref = "#{remote}/#{branch}"
|
|
140
|
-
|
|
141
|
-
# Check if the remote ref exists.
|
|
142
112
|
_, _, exists, = git_run( "rev-parse", "--verify", remote_ref )
|
|
143
113
|
return :no_remote unless exists
|
|
144
114
|
|
|
145
115
|
ahead_behind, _, success, = git_run( "rev-list", "--left-right", "--count", "#{branch}...#{remote_ref}" )
|
|
146
116
|
return :unknown unless success
|
|
147
117
|
|
|
148
|
-
|
|
149
|
-
ahead = parts[ 0 ].to_i
|
|
150
|
-
behind = parts[ 1 ].to_i
|
|
151
|
-
|
|
118
|
+
ahead, behind = ahead_behind.strip.split.map( &:to_i )
|
|
152
119
|
return :in_sync if ahead.zero? && behind.zero?
|
|
153
120
|
return :ahead if behind.zero?
|
|
154
121
|
return :behind if ahead.zero?
|
|
155
122
|
:diverged
|
|
156
123
|
end
|
|
157
124
|
|
|
158
|
-
# Lists all worktrees with branch name.
|
|
159
|
-
def gather_worktree_info
|
|
160
|
-
entries = worktree_list
|
|
161
|
-
|
|
162
|
-
# Filter output the main worktree (the repository root itself).
|
|
163
|
-
# Use realpath for comparison — git returns canonical paths that may differ from repo_root.
|
|
164
|
-
canonical_root = realpath_safe( repo_root )
|
|
165
|
-
entries.reject { it.fetch( :path ) == canonical_root }.map do |worktree|
|
|
166
|
-
{
|
|
167
|
-
path: worktree.fetch( :path ),
|
|
168
|
-
name: File.basename( worktree.fetch( :path ) ),
|
|
169
|
-
branch: worktree.fetch( :branch, nil )
|
|
170
|
-
}
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
# Queries open PRs via gh.
|
|
175
|
-
def gather_pr_info
|
|
176
|
-
stdout, _, success, = gh_run(
|
|
177
|
-
"pr", "list", "--state", "open",
|
|
178
|
-
"--json", "number,title,headRefName,statusCheckRollup,reviewDecision"
|
|
179
|
-
)
|
|
180
|
-
return [] unless success
|
|
181
|
-
|
|
182
|
-
prs = JSON.parse( stdout ) rescue []
|
|
183
|
-
prs.map do |pr|
|
|
184
|
-
ci = summarise_checks( rollup: pr[ "statusCheckRollup" ] )
|
|
185
|
-
review = pr[ "reviewDecision" ].to_s
|
|
186
|
-
review_label = review_decision_label( decision: review )
|
|
187
|
-
|
|
188
|
-
{
|
|
189
|
-
number: pr[ "number" ],
|
|
190
|
-
title: pr[ "title" ],
|
|
191
|
-
branch: pr[ "headRefName" ],
|
|
192
|
-
ci: ci,
|
|
193
|
-
review: review_label
|
|
194
|
-
}
|
|
195
|
-
end
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
# Summarises check rollup into a single status word.
|
|
199
|
-
def summarise_checks( rollup: )
|
|
200
|
-
entries = Array( rollup )
|
|
201
|
-
return :none if entries.empty?
|
|
202
|
-
|
|
203
|
-
states = entries.map { it[ "conclusion" ].to_s.upcase }
|
|
204
|
-
return :fail if states.any? { it == "FAILURE" || it == "CANCELLED" || it == "TIMED_OUT" }
|
|
205
|
-
return :pending if states.any? { it == "" || it == "PENDING" || it == "QUEUED" || it == "IN_PROGRESS" }
|
|
206
|
-
|
|
207
|
-
:pass
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
# Translates GitHub review decision to a concise label.
|
|
211
|
-
def review_decision_label( decision: )
|
|
212
|
-
case decision.upcase
|
|
213
|
-
when "APPROVED" then :approved
|
|
214
|
-
when "CHANGES_REQUESTED" then :changes_requested
|
|
215
|
-
when "REVIEW_REQUIRED" then :review_required
|
|
216
|
-
else :none
|
|
217
|
-
end
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
# Counts local branches that are stale (tracking a deleted upstream).
|
|
221
125
|
def gather_stale_branch_info
|
|
222
126
|
stdout, _, success, = git_run( "branch", "-vv" )
|
|
223
127
|
return { count: 0 } unless success
|
|
@@ -226,77 +130,57 @@ module Carson
|
|
|
226
130
|
{ count: gone_branches.size }
|
|
227
131
|
end
|
|
228
132
|
|
|
229
|
-
# Quick governance health check: are templates in sync?
|
|
230
|
-
def gather_governance_info
|
|
231
|
-
result = with_captured_output { template_check! }
|
|
232
|
-
{
|
|
233
|
-
templates: result == EXIT_OK ? :in_sync : :drifted
|
|
234
|
-
}
|
|
235
|
-
rescue StandardError
|
|
236
|
-
{ templates: :unknown }
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
# Prints the human-readable status report.
|
|
240
133
|
def print_status( data: )
|
|
241
134
|
puts_line "Carson #{data.fetch( :version )}"
|
|
242
|
-
puts_line ""
|
|
135
|
+
puts_line "Repository: #{data.dig( :repository, :name )}"
|
|
136
|
+
puts_line "Authority: #{data.dig( :repository, :authority )}"
|
|
243
137
|
|
|
244
|
-
# Branch
|
|
245
138
|
branch = data.fetch( :branch )
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if
|
|
253
|
-
puts_line ""
|
|
254
|
-
|
|
255
|
-
worktrees.each do |worktree|
|
|
256
|
-
branch_label = worktree.fetch( :branch ) || "(detached)"
|
|
257
|
-
puts_line " #{worktree.fetch( :name )} #{branch_label}"
|
|
258
|
-
end
|
|
139
|
+
branch_line = "Branch: #{branch.fetch( :name )}"
|
|
140
|
+
branch_line += " (dirty #{branch.fetch( :dirty_reason )})" if branch.fetch( :dirty )
|
|
141
|
+
branch_line += " [#{format_sync( sync: branch.fetch( :sync ) )}]"
|
|
142
|
+
puts_line branch_line
|
|
143
|
+
|
|
144
|
+
deliveries = data.fetch( :branches )
|
|
145
|
+
if deliveries.empty?
|
|
146
|
+
puts_line "Deliveries: none"
|
|
147
|
+
return
|
|
259
148
|
end
|
|
260
149
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
review_label = pr.fetch( :review ).to_s.tr( "_", " " )
|
|
269
|
-
puts_line " ##{pr.fetch( :number )} #{pr.fetch( :title )}"
|
|
270
|
-
puts_line " CI: #{ci_label} Review: #{review_label}"
|
|
271
|
-
end
|
|
150
|
+
puts_line "Deliveries:"
|
|
151
|
+
deliveries.each do |delivery|
|
|
152
|
+
line = "- #{delivery.fetch( :delivery_state )}: #{delivery.fetch( :branch )}"
|
|
153
|
+
pr_number = delivery.fetch( :pr_number )
|
|
154
|
+
line += " (#{"PR ##{pr_number}"} )" if pr_number
|
|
155
|
+
puts_line line.gsub( " )", ")" )
|
|
156
|
+
puts_line " #{delivery.fetch( :summary )}" unless delivery.fetch( :summary ).to_s.empty?
|
|
272
157
|
end
|
|
158
|
+
end
|
|
273
159
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
puts_line ""
|
|
279
|
-
puts_line "#{count} stale #{ count == 1 ? 'branch' : 'branches' } ready for pruning."
|
|
160
|
+
def print_portfolio_status( result: )
|
|
161
|
+
if result.fetch( :status ) == "error"
|
|
162
|
+
puts_line "#{result.fetch( :name )}: #{result.fetch( :error )}"
|
|
163
|
+
return
|
|
280
164
|
end
|
|
281
165
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
166
|
+
deliveries = Array( result.fetch( :branches, [] ) )
|
|
167
|
+
counts = deliveries.each_with_object( Hash.new( 0 ) ) { |delivery, memo| memo[ delivery.fetch( :delivery_state ) ] += 1 }
|
|
168
|
+
summary = if counts.empty?
|
|
169
|
+
"no active deliveries"
|
|
170
|
+
else
|
|
171
|
+
counts.map { |state, count| "#{count} #{state}" }.join( ", " )
|
|
288
172
|
end
|
|
173
|
+
puts_line "#{result.fetch( :name )}: #{result.dig( :repository, :authority )} — #{summary}"
|
|
289
174
|
end
|
|
290
175
|
|
|
291
|
-
# Formats sync status for display.
|
|
292
176
|
def format_sync( sync: )
|
|
293
177
|
case sync
|
|
294
|
-
when :in_sync then ""
|
|
295
|
-
when :ahead then "
|
|
296
|
-
when :behind then "
|
|
297
|
-
when :diverged then "
|
|
298
|
-
when :no_remote then "
|
|
299
|
-
else ""
|
|
178
|
+
when :in_sync then "in sync"
|
|
179
|
+
when :ahead then "ahead"
|
|
180
|
+
when :behind then "behind"
|
|
181
|
+
when :diverged then "diverged"
|
|
182
|
+
when :no_remote then "no remote"
|
|
183
|
+
else "unknown"
|
|
300
184
|
end
|
|
301
185
|
end
|
|
302
186
|
end
|
data/lib/carson/runtime.rb
CHANGED
|
@@ -33,15 +33,22 @@ module Carson
|
|
|
33
33
|
@config = Config.load( repo_root: repo_root )
|
|
34
34
|
@git_adapter = Adapters::Git.new( repo_root: repo_root )
|
|
35
35
|
@github_adapter = Adapters::GitHub.new( repo_root: repo_root )
|
|
36
|
+
@ledger = Ledger.new( path: @config.govern_state_path )
|
|
36
37
|
@template_sync_result = nil
|
|
37
38
|
end
|
|
38
39
|
|
|
39
|
-
attr_reader :template_sync_result
|
|
40
|
+
attr_reader :template_sync_result, :ledger
|
|
40
41
|
|
|
41
42
|
private
|
|
42
43
|
|
|
43
44
|
attr_reader :repo_root, :tool_root, :output, :error, :in, :config, :git_adapter, :github_adapter
|
|
44
45
|
|
|
46
|
+
# Ruby 2.6 treats bare `in` awkwardly because of pattern-matching parsing.
|
|
47
|
+
# Keep the original ivar/reader for compatibility, but expose a safe helper name.
|
|
48
|
+
def input_stream
|
|
49
|
+
instance_variable_get( :@in )
|
|
50
|
+
end
|
|
51
|
+
|
|
45
52
|
# Returns true when full diagnostic output is enabled via --verbose.
|
|
46
53
|
def verbose?
|
|
47
54
|
@verbose
|
|
@@ -74,6 +81,21 @@ module Carson
|
|
|
74
81
|
git_capture!( "rev-parse", "--abbrev-ref", "HEAD" ).strip
|
|
75
82
|
end
|
|
76
83
|
|
|
84
|
+
# Current branch head SHA for delivery identity.
|
|
85
|
+
def current_head
|
|
86
|
+
git_capture!( "rev-parse", "HEAD" ).strip
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Passive repository record for the current runtime context.
|
|
90
|
+
def repository_record
|
|
91
|
+
Repository.new( path: repo_root, authority: config.govern_authority, runtime: self )
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Passive branch record for the current checkout.
|
|
95
|
+
def branch_record( name: current_branch )
|
|
96
|
+
repository_record.branch( name ).reload
|
|
97
|
+
end
|
|
98
|
+
|
|
77
99
|
# Checks local branch existence before restore attempts in ensure blocks.
|
|
78
100
|
def branch_exists?( branch_name: )
|
|
79
101
|
_, _, success, = git_run( "show-ref", "--verify", "--quiet", "refs/heads/#{branch_name}" )
|
|
@@ -310,9 +332,9 @@ module Carson
|
|
|
310
332
|
# so only genuinely active worktrees block the operation.
|
|
311
333
|
scoped_runtime = build_scoped_runtime( repo_path: repo_path )
|
|
312
334
|
scoped_runtime.sweep_stale_worktrees!
|
|
313
|
-
worktrees = scoped_runtime.
|
|
314
|
-
main_root = scoped_runtime.
|
|
315
|
-
active = worktrees.reject { |worktree| worktree.
|
|
335
|
+
worktrees = scoped_runtime.worktree_list
|
|
336
|
+
main_root = scoped_runtime.realpath_safe( repo_path )
|
|
337
|
+
active = worktrees.reject { |worktree| worktree.path == main_root }
|
|
316
338
|
if active.any?
|
|
317
339
|
reasons << "#{active.count} active worktree#{active.count == 1 ? '' : 's'}"
|
|
318
340
|
end
|
|
@@ -346,3 +368,16 @@ require_relative "runtime/govern"
|
|
|
346
368
|
require_relative "runtime/setup"
|
|
347
369
|
require_relative "runtime/status"
|
|
348
370
|
require_relative "runtime/deliver"
|
|
371
|
+
|
|
372
|
+
# Infrastructure interface for domain objects.
|
|
373
|
+
# Carson::Worktree and future domain objects call these methods
|
|
374
|
+
# on a runtime reference — the way ActiveRecord models use a connection.
|
|
375
|
+
# Defined private for internal use, exposed here for domain object access.
|
|
376
|
+
module Carson
|
|
377
|
+
class Runtime
|
|
378
|
+
public :config, :output, :verbose?, :puts_verbose, :puts_line,
|
|
379
|
+
:git_run, :git_capture!, :main_worktree_root, :realpath_safe,
|
|
380
|
+
:block_if_outsider_fingerprints!, :branch_absorbed_into_main?,
|
|
381
|
+
:ledger, :current_head, :repository_record, :branch_record
|
|
382
|
+
end
|
|
383
|
+
end
|