carson 3.22.1 → 3.23.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.
- checksums.yaml +4 -4
- data/API.md +15 -11
- data/MANUAL.md +32 -38
- data/README.md +6 -9
- data/RELEASE.md +23 -0
- data/VERSION +1 -1
- data/carson.gemspec +1 -0
- data/lib/carson/adapters/agent.rb +2 -2
- data/lib/carson/branch.rb +38 -0
- data/lib/carson/cli.rb +13 -14
- data/lib/carson/config.rb +34 -18
- 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 +6 -6
- data/lib/carson/runtime/deliver.rb +112 -169
- data/lib/carson/runtime/govern.rb +213 -399
- data/lib/carson/runtime/housekeep.rb +4 -4
- data/lib/carson/runtime/local/onboard.rb +5 -5
- data/lib/carson/runtime/local/prune.rb +4 -4
- data/lib/carson/runtime/local/template.rb +4 -4
- data/lib/carson/runtime/review/gate_support.rb +14 -12
- data/lib/carson/runtime/review/sweep_support.rb +2 -2
- data/lib/carson/runtime/review/utility.rb +1 -1
- data/lib/carson/runtime/setup.rb +10 -27
- data/lib/carson/runtime/status.rb +87 -226
- data/lib/carson/runtime.rb +25 -2
- data/lib/carson/worktree.rb +5 -5
- data/lib/carson.rb +5 -0
- metadata +27 -2
|
@@ -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,115 +17,78 @@ 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}: not found"
|
|
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 = format_dirty_marker( branch: branch )
|
|
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}: could not read (#{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
|
|
82
50
|
end
|
|
83
51
|
|
|
84
|
-
# rubocop:disable Layout/AccessModifierIndentation -- tab-width calculation produces unfixable mixed tabs+spaces
|
|
85
52
|
private
|
|
86
|
-
# rubocop:enable Layout/AccessModifierIndentation
|
|
87
53
|
|
|
88
|
-
# Returns an array of human-readable pending descriptions for a repo.
|
|
89
|
-
def status_pending_for_repo( all_pending:, repo_path: )
|
|
90
|
-
descriptions = []
|
|
91
|
-
all_pending.each do |command, repos|
|
|
92
|
-
next unless repos.is_a?( Hash ) && repos.key?( repo_path )
|
|
93
|
-
|
|
94
|
-
info = repos[ repo_path ]
|
|
95
|
-
attempts = info.fetch( "attempts", 0 )
|
|
96
|
-
skipped_at = info.fetch( "skipped_at", nil )
|
|
97
|
-
time_part = skipped_at ? ", since #{skipped_at[ 11..15 ]}" : ""
|
|
98
|
-
descriptions << "#{command} (#{attempts} attempt#{attempts == 1 ? '' : 's'}#{time_part})"
|
|
99
|
-
end
|
|
100
|
-
descriptions
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Collects all status facets into a structured hash.
|
|
104
54
|
def gather_status
|
|
105
|
-
|
|
55
|
+
repository = repository_record
|
|
56
|
+
branch = branch_record
|
|
57
|
+
deliveries = ledger.active_deliveries( repo_path: repo_root )
|
|
58
|
+
|
|
59
|
+
{
|
|
106
60
|
version: Carson::VERSION,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
110
76
|
}
|
|
111
|
-
|
|
112
|
-
# PR and stale branch data require gh — gather with graceful fallback.
|
|
113
|
-
if gh_available?
|
|
114
|
-
data[ :pull_requests ] = gather_pr_info
|
|
115
|
-
data[ :stale_branches ] = gather_stale_branch_info
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
data
|
|
119
77
|
end
|
|
120
78
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
+
}
|
|
128
90
|
end
|
|
129
91
|
|
|
130
|
-
# Returns true when the working tree has uncommitted changes.
|
|
131
92
|
def working_tree_dirty?
|
|
132
93
|
stdout, _, success, = git_run( "status", "--porcelain" )
|
|
133
94
|
return true unless success
|
|
@@ -145,92 +106,22 @@ module Carson
|
|
|
145
106
|
realpath_safe( repo_root ) == realpath_safe( main_worktree_root )
|
|
146
107
|
end
|
|
147
108
|
|
|
148
|
-
# Compares local branch against its remote tracking ref.
|
|
149
|
-
# Returns :in_sync, :ahead, :behind, :diverged, or :no_remote.
|
|
150
109
|
def remote_sync_status( branch: )
|
|
151
110
|
remote = config.git_remote
|
|
152
111
|
remote_ref = "#{remote}/#{branch}"
|
|
153
|
-
|
|
154
|
-
# Check if the remote ref exists.
|
|
155
112
|
_, _, exists, = git_run( "rev-parse", "--verify", remote_ref )
|
|
156
113
|
return :no_remote unless exists
|
|
157
114
|
|
|
158
115
|
ahead_behind, _, success, = git_run( "rev-list", "--left-right", "--count", "#{branch}...#{remote_ref}" )
|
|
159
116
|
return :unknown unless success
|
|
160
117
|
|
|
161
|
-
|
|
162
|
-
ahead = parts[ 0 ].to_i
|
|
163
|
-
behind = parts[ 1 ].to_i
|
|
164
|
-
|
|
118
|
+
ahead, behind = ahead_behind.strip.split.map( &:to_i )
|
|
165
119
|
return :in_sync if ahead.zero? && behind.zero?
|
|
166
120
|
return :ahead if behind.zero?
|
|
167
121
|
return :behind if ahead.zero?
|
|
168
122
|
:diverged
|
|
169
123
|
end
|
|
170
124
|
|
|
171
|
-
# Lists all worktrees with branch name.
|
|
172
|
-
def gather_worktree_info
|
|
173
|
-
entries = worktree_list
|
|
174
|
-
|
|
175
|
-
# Filter output the main worktree (the repository root itself).
|
|
176
|
-
# Use realpath for comparison — git returns canonical paths that may differ from repo_root.
|
|
177
|
-
canonical_root = realpath_safe( repo_root )
|
|
178
|
-
entries.reject { it.path == canonical_root }.map do |worktree|
|
|
179
|
-
{
|
|
180
|
-
path: worktree.path,
|
|
181
|
-
name: File.basename( worktree.path ),
|
|
182
|
-
branch: worktree.branch
|
|
183
|
-
}
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
# Queries open PRs via gh.
|
|
188
|
-
def gather_pr_info
|
|
189
|
-
stdout, _, success, = gh_run(
|
|
190
|
-
"pr", "list", "--state", "open",
|
|
191
|
-
"--json", "number,title,headRefName,statusCheckRollup,reviewDecision"
|
|
192
|
-
)
|
|
193
|
-
return [] unless success
|
|
194
|
-
|
|
195
|
-
prs = JSON.parse( stdout ) rescue []
|
|
196
|
-
prs.map do |pr|
|
|
197
|
-
ci = summarise_checks( rollup: pr[ "statusCheckRollup" ] )
|
|
198
|
-
review = pr[ "reviewDecision" ].to_s
|
|
199
|
-
review_label = review_decision_label( decision: review )
|
|
200
|
-
|
|
201
|
-
{
|
|
202
|
-
number: pr[ "number" ],
|
|
203
|
-
title: pr[ "title" ],
|
|
204
|
-
branch: pr[ "headRefName" ],
|
|
205
|
-
ci: ci,
|
|
206
|
-
review: review_label
|
|
207
|
-
}
|
|
208
|
-
end
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
# Summarises check rollup into a single status word.
|
|
212
|
-
def summarise_checks( rollup: )
|
|
213
|
-
entries = Array( rollup )
|
|
214
|
-
return :none if entries.empty?
|
|
215
|
-
|
|
216
|
-
states = entries.map { it[ "conclusion" ].to_s.upcase }
|
|
217
|
-
return :fail if states.any? { it == "FAILURE" || it == "CANCELLED" || it == "TIMED_OUT" }
|
|
218
|
-
return :pending if states.any? { it == "" || it == "PENDING" || it == "QUEUED" || it == "IN_PROGRESS" }
|
|
219
|
-
|
|
220
|
-
:pass
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
# Translates GitHub review decision to a concise label.
|
|
224
|
-
def review_decision_label( decision: )
|
|
225
|
-
case decision.upcase
|
|
226
|
-
when "APPROVED" then :approved
|
|
227
|
-
when "CHANGES_REQUESTED" then :changes_requested
|
|
228
|
-
when "REVIEW_REQUIRED" then :review_required
|
|
229
|
-
else :none
|
|
230
|
-
end
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
# Counts local branches that are stale (tracking a deleted upstream).
|
|
234
125
|
def gather_stale_branch_info
|
|
235
126
|
stdout, _, success, = git_run( "branch", "-vv" )
|
|
236
127
|
return { count: 0 } unless success
|
|
@@ -239,91 +130,61 @@ module Carson
|
|
|
239
130
|
{ count: gone_branches.size }
|
|
240
131
|
end
|
|
241
132
|
|
|
242
|
-
# Quick governance health check: are templates in sync?
|
|
243
|
-
def gather_governance_info
|
|
244
|
-
result = with_captured_output { template_check! }
|
|
245
|
-
{
|
|
246
|
-
templates: result == EXIT_OK ? :in_sync : :drifted
|
|
247
|
-
}
|
|
248
|
-
rescue StandardError
|
|
249
|
-
{ templates: :unknown }
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
# Prints the human-readable status report.
|
|
253
133
|
def print_status( data: )
|
|
254
134
|
puts_line "Carson #{data.fetch( :version )}"
|
|
255
|
-
puts_line ""
|
|
135
|
+
puts_line "Repository: #{data.dig( :repository, :name )}"
|
|
136
|
+
puts_line "Authority: #{data.dig( :repository, :authority )}"
|
|
256
137
|
|
|
257
|
-
# Branch
|
|
258
138
|
branch = data.fetch( :branch )
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
264
148
|
end
|
|
265
149
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
puts_line " #{worktree.fetch( :name )} #{branch_label}"
|
|
274
|
-
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?
|
|
275
157
|
end
|
|
158
|
+
end
|
|
276
159
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
puts_line "Pull requests:"
|
|
282
|
-
prs.each do |pr|
|
|
283
|
-
ci_label = pr.fetch( :ci ).to_s
|
|
284
|
-
review_label = pr.fetch( :review ).to_s.tr( "_", " " )
|
|
285
|
-
puts_line " ##{pr.fetch( :number )} #{pr.fetch( :title )}"
|
|
286
|
-
puts_line " CI: #{ci_label} Review: #{review_label}"
|
|
287
|
-
end
|
|
160
|
+
def print_portfolio_status( result: )
|
|
161
|
+
if result.fetch( :status ) == "error"
|
|
162
|
+
puts_line "#{result.fetch( :name )}: #{result.fetch( :error )}"
|
|
163
|
+
return
|
|
288
164
|
end
|
|
289
165
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
# Governance
|
|
299
|
-
gov = data.fetch( :governance )
|
|
300
|
-
templates = gov.fetch( :templates )
|
|
301
|
-
unless templates == :in_sync
|
|
302
|
-
puts_line ""
|
|
303
|
-
puts_line "Templates: #{templates} — run `carson sync` to fix."
|
|
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( ", " )
|
|
304
172
|
end
|
|
173
|
+
puts_line "#{result.fetch( :name )}: #{result.dig( :repository, :authority )} — #{summary}"
|
|
305
174
|
end
|
|
306
175
|
|
|
307
|
-
# Formats sync status for display.
|
|
308
176
|
def format_sync( sync: )
|
|
309
177
|
case sync
|
|
310
|
-
when :in_sync then ""
|
|
311
|
-
when :ahead then "
|
|
312
|
-
when :behind then "
|
|
313
|
-
when :diverged then "
|
|
314
|
-
when :no_remote then "
|
|
315
|
-
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"
|
|
316
184
|
end
|
|
317
185
|
end
|
|
186
|
+
end
|
|
318
187
|
|
|
319
|
-
|
|
320
|
-
return "" unless branch.fetch( :dirty )
|
|
321
|
-
return " (dirty main worktree)" if branch.fetch( :dirty_reason, nil ) == "main_worktree"
|
|
322
|
-
|
|
323
|
-
" (dirty)"
|
|
324
|
-
end
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
include Status
|
|
188
|
+
include Status
|
|
328
189
|
end
|
|
329
190
|
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}" )
|
|
@@ -355,6 +377,7 @@ module Carson
|
|
|
355
377
|
class Runtime
|
|
356
378
|
public :config, :output, :verbose?, :puts_verbose, :puts_line,
|
|
357
379
|
:git_run, :git_capture!, :main_worktree_root, :realpath_safe,
|
|
358
|
-
:block_if_outsider_fingerprints!, :branch_absorbed_into_main
|
|
380
|
+
:block_if_outsider_fingerprints!, :branch_absorbed_into_main?,
|
|
381
|
+
:ledger, :current_head, :repository_record, :branch_record
|
|
359
382
|
end
|
|
360
383
|
end
|
data/lib/carson/worktree.rb
CHANGED
|
@@ -52,13 +52,13 @@ module Carson
|
|
|
52
52
|
# Compares using realpath to handle symlink differences.
|
|
53
53
|
def self.find( path:, runtime: )
|
|
54
54
|
canonical = runtime.realpath_safe( path )
|
|
55
|
-
list( runtime: runtime ).find {
|
|
55
|
+
list( runtime: runtime ).find { |worktree| worktree.path == canonical }
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
# Returns true if the path is a registered git worktree.
|
|
59
59
|
def self.registered?( path:, runtime: )
|
|
60
60
|
canonical = runtime.realpath_safe( path )
|
|
61
|
-
list( runtime: runtime ).any? {
|
|
61
|
+
list( runtime: runtime ).any? { |worktree| worktree.path == canonical }
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
# Creates a new worktree under .claude/worktrees/<name> with a fresh branch.
|
|
@@ -247,10 +247,10 @@ module Carson
|
|
|
247
247
|
main_root = runtime.main_worktree_root
|
|
248
248
|
worktrees = list( runtime: runtime )
|
|
249
249
|
|
|
250
|
-
agent_prefixes = AGENT_DIRS.
|
|
250
|
+
agent_prefixes = AGENT_DIRS.map do |dir|
|
|
251
251
|
full = File.join( main_root, dir, "worktrees" )
|
|
252
252
|
File.join( runtime.realpath_safe( full ), "" ) if Dir.exist?( full )
|
|
253
|
-
end
|
|
253
|
+
end.compact
|
|
254
254
|
return if agent_prefixes.empty?
|
|
255
255
|
|
|
256
256
|
worktrees.each do |worktree|
|
|
@@ -454,7 +454,7 @@ module Carson
|
|
|
454
454
|
return canonical if registered?( path: canonical, runtime: runtime )
|
|
455
455
|
|
|
456
456
|
# Bare name didn't match flat layout — search registered worktrees by dirname.
|
|
457
|
-
matches = list( runtime: runtime ).select { File.basename(
|
|
457
|
+
matches = list( runtime: runtime ).select { |worktree| File.basename( worktree.path ) == path }
|
|
458
458
|
return matches.first.path if matches.size == 1
|
|
459
459
|
|
|
460
460
|
# No match or ambiguous — return the flat candidate and let the caller
|
data/lib/carson.rb
CHANGED
|
@@ -5,6 +5,11 @@ module Carson
|
|
|
5
5
|
BADGE = "\u29D3".freeze # ⧓ BLACK BOWTIE (U+29D3)
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
+
require_relative "carson/repository"
|
|
9
|
+
require_relative "carson/branch"
|
|
10
|
+
require_relative "carson/delivery"
|
|
11
|
+
require_relative "carson/revision"
|
|
12
|
+
require_relative "carson/ledger"
|
|
8
13
|
require_relative "carson/worktree"
|
|
9
14
|
require_relative "carson/config"
|
|
10
15
|
require_relative "carson/adapters/git"
|
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.
|
|
4
|
+
version: 3.23.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hailei Wang
|
|
@@ -10,7 +10,27 @@ authors:
|
|
|
10
10
|
bindir: exe
|
|
11
11
|
cert_chain: []
|
|
12
12
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
13
|
-
dependencies:
|
|
13
|
+
dependencies:
|
|
14
|
+
- !ruby/object:Gem::Dependency
|
|
15
|
+
name: sqlite3
|
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
|
17
|
+
requirements:
|
|
18
|
+
- - ">="
|
|
19
|
+
- !ruby/object:Gem::Version
|
|
20
|
+
version: '1.3'
|
|
21
|
+
- - "<"
|
|
22
|
+
- !ruby/object:Gem::Version
|
|
23
|
+
version: '3'
|
|
24
|
+
type: :runtime
|
|
25
|
+
prerelease: false
|
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
27
|
+
requirements:
|
|
28
|
+
- - ">="
|
|
29
|
+
- !ruby/object:Gem::Version
|
|
30
|
+
version: '1.3'
|
|
31
|
+
- - "<"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3'
|
|
14
34
|
description: 'Carson is an autonomous git strategist and repositories governor that
|
|
15
35
|
lives outside the repositories it governs — no Carson-owned artefacts in your repo.
|
|
16
36
|
As strategist, Carson knows when to branch, how to isolate concurrent work, and
|
|
@@ -48,8 +68,13 @@ files:
|
|
|
48
68
|
- lib/carson/adapters/git.rb
|
|
49
69
|
- lib/carson/adapters/github.rb
|
|
50
70
|
- lib/carson/adapters/prompt.rb
|
|
71
|
+
- lib/carson/branch.rb
|
|
51
72
|
- lib/carson/cli.rb
|
|
52
73
|
- lib/carson/config.rb
|
|
74
|
+
- lib/carson/delivery.rb
|
|
75
|
+
- lib/carson/ledger.rb
|
|
76
|
+
- lib/carson/repository.rb
|
|
77
|
+
- lib/carson/revision.rb
|
|
53
78
|
- lib/carson/runtime.rb
|
|
54
79
|
- lib/carson/runtime/audit.rb
|
|
55
80
|
- lib/carson/runtime/deliver.rb
|