carson 2.33.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/RELEASE.md +40 -0
- data/VERSION +1 -1
- data/lib/carson/cli.rb +30 -3
- data/lib/carson/runtime/local/worktree.rb +77 -1
- data/lib/carson/runtime/status.rb +229 -0
- data/lib/carson/runtime.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 046c14a614687e668eae6d9f3df0eadbe02fcb103a1c93f25657791a0d72962c
|
|
4
|
+
data.tar.gz: 26ebca4203a0a9b3df1e38795580188bcf6951049304ae0eb4899deda35106f8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 38cb7ebf2e65293d5ea26bfb527314d86f7889949db033649fc49822b65feb7b7ea0e8fa5f35b6d959024ff4d280691432efde50dc15c37bf223665cd7d908b7
|
|
7
|
+
data.tar.gz: d05902ee1637919dd0a9781fa2de46e4370d2806df83c02c730584095f18811baea6168f3614d81900229dd5ec3c141ffb2ce6a40a8c338f233aa1a88a2a9b81
|
data/RELEASE.md
CHANGED
|
@@ -5,6 +5,46 @@ 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.1.0 — Worktree Lifecycle
|
|
9
|
+
|
|
10
|
+
### What changed
|
|
11
|
+
|
|
12
|
+
- **`carson worktree create <name>`** — creates a worktree under `.claude/worktrees/<name>` with a new branch based on main. One command, one result: path and branch name reported.
|
|
13
|
+
- **`carson worktree done <name>`** — marks a worktree as completed without deleting it. Verifies all changes are committed and pushed. Blocks with actionable guidance if uncommitted changes or unpushed commits exist. The worktree directory persists for batch cleanup later.
|
|
14
|
+
- **Deferred deletion model.** The full worktree lifecycle is now: create → work → done → batch cleanup. No worktree is deleted during an active session. Cleanup happens later via `carson worktree remove` or `carson housekeep`.
|
|
15
|
+
|
|
16
|
+
### UX
|
|
17
|
+
|
|
18
|
+
- `carson worktree create` reports path and branch — ready to `cd` into immediately.
|
|
19
|
+
- `carson worktree done` gives recovery commands when changes are uncommitted or unpushed.
|
|
20
|
+
- Error messages across worktree subcommands now show `create|done|remove` in usage hints.
|
|
21
|
+
|
|
22
|
+
### Migration
|
|
23
|
+
|
|
24
|
+
- No breaking changes. `carson worktree remove` continues to work as before.
|
|
25
|
+
|
|
26
|
+
## 3.0.0 — Agent-Oriented Carson
|
|
27
|
+
|
|
28
|
+
### Theme
|
|
29
|
+
|
|
30
|
+
Carson is for coding agents. The primary consumer of Carson's commands, lifecycle management, and governance is the coding agent working on behalf of the developer. Carson 3.0 reorients the product around this truth.
|
|
31
|
+
|
|
32
|
+
### What changed
|
|
33
|
+
|
|
34
|
+
- **`carson status` — agent session briefing.** One command to know the full state of the estate: current branch and dirty/sync state, active worktrees, open PRs with CI and review status, stale branches ready for pruning, and governance health. Supports `--json` for machine-readable structured output — agents can parse the response directly instead of scraping human text.
|
|
35
|
+
- **Deferred worktree cleanup model.** Worktrees are no longer expected to be deleted immediately after use. The new lifecycle: create a worktree, do work, mark it done, clean up later in batch via `carson housekeep` or `carson prune`. This eliminates the #1 agent session crash: worktree directory disappearing while the agent's shell CWD is inside it.
|
|
36
|
+
- **`docs/agent-orient.md` — the agent's needs document.** Written from the coding agent's authentic perspective: what it experiences, what friction exists, and what it needs Carson to become. This document guides all 3.0 development.
|
|
37
|
+
|
|
38
|
+
### UX
|
|
39
|
+
|
|
40
|
+
- `carson status` prints a concise briefing by default. Silence is preserved — status reports only what needs attention.
|
|
41
|
+
- `carson status --json` produces a stable JSON schema for programmatic consumption.
|
|
42
|
+
|
|
43
|
+
### Migration
|
|
44
|
+
|
|
45
|
+
- No breaking changes. All 2.x commands continue to work unchanged.
|
|
46
|
+
- `docs/plan.md` has been removed — superseded by `docs/agent-orient.md`.
|
|
47
|
+
|
|
8
48
|
## 2.33.0 — Safe Worktree Remove
|
|
9
49
|
|
|
10
50
|
### What changed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
3.1.0
|
data/lib/carson/cli.rb
CHANGED
|
@@ -53,7 +53,7 @@ module Carson
|
|
|
53
53
|
|
|
54
54
|
def self.build_parser
|
|
55
55
|
OptionParser.new do |opts|
|
|
56
|
-
opts.banner = "Usage: carson [
|
|
56
|
+
opts.banner = "Usage: carson [status [--json]|setup|audit|sync|prune [--all]|worktree create|done|remove <name>|onboard|refresh [--all]|offboard|template check|apply|review gate|sweep|govern [--dry-run] [--json] [--loop SECONDS]|version]"
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
|
|
@@ -88,6 +88,8 @@ module Carson
|
|
|
88
88
|
parse_worktree_subcommand( argv: argv, parser: parser, err: err )
|
|
89
89
|
when "review"
|
|
90
90
|
parse_named_subcommand( command: command, usage: "gate|sweep", argv: argv, parser: parser, err: err )
|
|
91
|
+
when "status"
|
|
92
|
+
parse_status_command( argv: argv, err: err )
|
|
91
93
|
when "govern"
|
|
92
94
|
parse_govern_subcommand( argv: argv, err: err )
|
|
93
95
|
else
|
|
@@ -168,12 +170,22 @@ module Carson
|
|
|
168
170
|
def self.parse_worktree_subcommand( argv:, parser:, err: )
|
|
169
171
|
action = argv.shift
|
|
170
172
|
if action.to_s.strip.empty?
|
|
171
|
-
err.puts "#{BADGE} Missing subcommand for worktree. Use: carson worktree remove <name
|
|
173
|
+
err.puts "#{BADGE} Missing subcommand for worktree. Use: carson worktree create|done|remove <name>"
|
|
172
174
|
err.puts parser
|
|
173
175
|
return { command: :invalid }
|
|
174
176
|
end
|
|
175
177
|
|
|
176
178
|
case action
|
|
179
|
+
when "create"
|
|
180
|
+
name = argv.shift
|
|
181
|
+
if name.to_s.strip.empty?
|
|
182
|
+
err.puts "#{BADGE} Missing name for worktree create. Use: carson worktree create <name>"
|
|
183
|
+
return { command: :invalid }
|
|
184
|
+
end
|
|
185
|
+
{ command: "worktree:create", worktree_name: name }
|
|
186
|
+
when "done"
|
|
187
|
+
name = argv.shift
|
|
188
|
+
{ command: "worktree:done", worktree_name: name }
|
|
177
189
|
when "remove"
|
|
178
190
|
force = argv.delete( "--force" ) ? true : false
|
|
179
191
|
worktree_path = argv.shift
|
|
@@ -183,7 +195,7 @@ module Carson
|
|
|
183
195
|
end
|
|
184
196
|
{ command: "worktree:remove", worktree_path: worktree_path, force: force }
|
|
185
197
|
else
|
|
186
|
-
err.puts "#{BADGE} Unknown worktree subcommand: #{action}. Use: carson worktree remove <name
|
|
198
|
+
err.puts "#{BADGE} Unknown worktree subcommand: #{action}. Use: carson worktree create|done|remove <name>"
|
|
187
199
|
{ command: :invalid }
|
|
188
200
|
end
|
|
189
201
|
end
|
|
@@ -228,6 +240,15 @@ module Carson
|
|
|
228
240
|
{ command: :invalid }
|
|
229
241
|
end
|
|
230
242
|
|
|
243
|
+
def self.parse_status_command( argv:, err: )
|
|
244
|
+
json_flag = argv.delete( "--json" ) ? true : false
|
|
245
|
+
unless argv.empty?
|
|
246
|
+
err.puts "#{BADGE} Unexpected arguments for status: #{argv.join( ' ' )}"
|
|
247
|
+
return { command: :invalid }
|
|
248
|
+
end
|
|
249
|
+
{ command: "status", json: json_flag }
|
|
250
|
+
end
|
|
251
|
+
|
|
231
252
|
def self.parse_govern_subcommand( argv:, err: )
|
|
232
253
|
options = {
|
|
233
254
|
dry_run: false,
|
|
@@ -266,6 +287,8 @@ module Carson
|
|
|
266
287
|
return Runtime::EXIT_ERROR if command == :invalid
|
|
267
288
|
|
|
268
289
|
case command
|
|
290
|
+
when "status"
|
|
291
|
+
runtime.status!( json_output: parsed.fetch( :json, false ) )
|
|
269
292
|
when "setup"
|
|
270
293
|
runtime.setup!( cli_choices: parsed.fetch( :cli_choices, {} ) )
|
|
271
294
|
when "audit"
|
|
@@ -276,6 +299,10 @@ module Carson
|
|
|
276
299
|
runtime.prune!
|
|
277
300
|
when "prune:all"
|
|
278
301
|
runtime.prune_all!
|
|
302
|
+
when "worktree:create"
|
|
303
|
+
runtime.worktree_create!( name: parsed.fetch( :worktree_name ) )
|
|
304
|
+
when "worktree:done"
|
|
305
|
+
runtime.worktree_done!( name: parsed.fetch( :worktree_name, nil ) )
|
|
279
306
|
when "worktree:remove"
|
|
280
307
|
runtime.worktree_remove!( worktree_path: parsed.fetch( :worktree_path ), force: parsed.fetch( :force, false ) )
|
|
281
308
|
when "onboard"
|
|
@@ -2,7 +2,83 @@ module Carson
|
|
|
2
2
|
class Runtime
|
|
3
3
|
module Local
|
|
4
4
|
# Safe worktree lifecycle management for coding agents.
|
|
5
|
-
#
|
|
5
|
+
# Three operations: create, done (mark completed), remove (batch cleanup).
|
|
6
|
+
# The deferred deletion model: worktrees persist after use, cleaned up later.
|
|
7
|
+
|
|
8
|
+
# Creates a new worktree under .claude/worktrees/<name> with a fresh branch.
|
|
9
|
+
def worktree_create!( name: )
|
|
10
|
+
worktrees_dir = File.join( repo_root, ".claude", "worktrees" )
|
|
11
|
+
wt_path = File.join( worktrees_dir, name )
|
|
12
|
+
|
|
13
|
+
if Dir.exist?( wt_path )
|
|
14
|
+
puts_line "ERROR: worktree already exists: #{name}"
|
|
15
|
+
puts_line " Path: #{wt_path}"
|
|
16
|
+
return EXIT_ERROR
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Determine the base branch (main branch from config).
|
|
20
|
+
base = config.main_branch
|
|
21
|
+
|
|
22
|
+
# Create the worktree with a new branch based on the main branch.
|
|
23
|
+
FileUtils.mkdir_p( worktrees_dir )
|
|
24
|
+
_, wt_stderr, wt_success, = git_run( "worktree", "add", wt_path, "-b", name, base )
|
|
25
|
+
unless wt_success
|
|
26
|
+
error_text = wt_stderr.to_s.strip
|
|
27
|
+
error_text = "unable to create worktree" if error_text.empty?
|
|
28
|
+
puts_line "ERROR: #{error_text}"
|
|
29
|
+
return EXIT_ERROR
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
puts_line "Worktree created: #{name}"
|
|
33
|
+
puts_line " Path: #{wt_path}"
|
|
34
|
+
puts_line " Branch: #{name}"
|
|
35
|
+
EXIT_OK
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Marks a worktree as completed without deleting it.
|
|
39
|
+
# Verifies all changes are committed. Deferred deletion — cleanup happens later.
|
|
40
|
+
def worktree_done!( name: nil )
|
|
41
|
+
if name.to_s.strip.empty?
|
|
42
|
+
# Try to detect current worktree from CWD.
|
|
43
|
+
puts_line "ERROR: missing worktree name. Use: carson worktree done <name>"
|
|
44
|
+
return EXIT_ERROR
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
resolved_path = resolve_worktree_path( worktree_path: name )
|
|
48
|
+
|
|
49
|
+
unless worktree_registered?( path: resolved_path )
|
|
50
|
+
puts_line "ERROR: #{name} is not a registered worktree."
|
|
51
|
+
return EXIT_ERROR
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Check for uncommitted changes in the worktree.
|
|
55
|
+
wt_status, _, status_success, = Open3.capture3( "git", "status", "--porcelain", chdir: resolved_path )
|
|
56
|
+
if status_success && !wt_status.strip.empty?
|
|
57
|
+
puts_line "Worktree has uncommitted changes: #{name}"
|
|
58
|
+
puts_line " Commit your changes first, then run `carson worktree done #{name}` again."
|
|
59
|
+
return EXIT_BLOCK
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Check for unpushed commits.
|
|
63
|
+
branch = worktree_branch( path: resolved_path )
|
|
64
|
+
if branch
|
|
65
|
+
remote = config.git_remote
|
|
66
|
+
remote_ref = "#{remote}/#{branch}"
|
|
67
|
+
ahead, _, ahead_ok, = Open3.capture3( "git", "rev-list", "--count", "#{remote_ref}..#{branch}", chdir: resolved_path )
|
|
68
|
+
if ahead_ok && ahead.strip.to_i > 0
|
|
69
|
+
puts_line "Worktree has unpushed commits: #{name}"
|
|
70
|
+
puts_line " Push with `git -C #{resolved_path} push #{remote} #{branch}` first."
|
|
71
|
+
return EXIT_BLOCK
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
puts_line "Worktree done: #{name}"
|
|
76
|
+
puts_line " Branch: #{branch || '(detached)'}"
|
|
77
|
+
puts_line " Cleanup later with `carson worktree remove #{name}` or `carson housekeep`."
|
|
78
|
+
EXIT_OK
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Removes a worktree: directory, git registration, and branch.
|
|
6
82
|
# Never forces removal — if the worktree has uncommitted changes, refuses unless
|
|
7
83
|
# the user explicitly passes force: true via CLI --force flag.
|
|
8
84
|
def worktree_remove!( worktree_path:, force: false )
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Agent session briefing — one command to know the full state of the estate.
|
|
2
|
+
# Gathers branch, working tree, worktrees, open PRs, stale branches,
|
|
3
|
+
# governance health, and version. Supports human-readable and JSON output.
|
|
4
|
+
module Carson
|
|
5
|
+
class Runtime
|
|
6
|
+
module Status
|
|
7
|
+
# Entry point for `carson status`. Collects estate state and reports.
|
|
8
|
+
def status!( json_output: false )
|
|
9
|
+
data = gather_status
|
|
10
|
+
|
|
11
|
+
if json_output
|
|
12
|
+
out.puts JSON.pretty_generate( data )
|
|
13
|
+
else
|
|
14
|
+
print_status( data: data )
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
EXIT_OK
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
# Collects all status facets into a structured hash.
|
|
23
|
+
def gather_status
|
|
24
|
+
data = {
|
|
25
|
+
version: Carson::VERSION,
|
|
26
|
+
branch: gather_branch_info,
|
|
27
|
+
worktrees: gather_worktree_info,
|
|
28
|
+
governance: gather_governance_info
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# PR and stale branch data require gh — gather with graceful fallback.
|
|
32
|
+
if gh_available?
|
|
33
|
+
data[ :pull_requests ] = gather_pr_info
|
|
34
|
+
data[ :stale_branches ] = gather_stale_branch_info
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
data
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Branch name, clean/dirty state, sync status with remote.
|
|
41
|
+
def gather_branch_info
|
|
42
|
+
branch = current_branch
|
|
43
|
+
dirty = working_tree_dirty?
|
|
44
|
+
sync = remote_sync_status( branch: branch )
|
|
45
|
+
|
|
46
|
+
{ name: branch, dirty: dirty, sync: sync }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Returns true when the working tree has uncommitted changes.
|
|
50
|
+
def working_tree_dirty?
|
|
51
|
+
stdout, _, success, = git_run( "status", "--porcelain" )
|
|
52
|
+
return true unless success
|
|
53
|
+
!stdout.strip.empty?
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Compares local branch against its remote tracking ref.
|
|
57
|
+
# Returns :in_sync, :ahead, :behind, :diverged, or :no_remote.
|
|
58
|
+
def remote_sync_status( branch: )
|
|
59
|
+
remote = config.git_remote
|
|
60
|
+
remote_ref = "#{remote}/#{branch}"
|
|
61
|
+
|
|
62
|
+
# Check if the remote ref exists.
|
|
63
|
+
_, _, exists, = git_run( "rev-parse", "--verify", remote_ref )
|
|
64
|
+
return :no_remote unless exists
|
|
65
|
+
|
|
66
|
+
ahead_behind, _, success, = git_run( "rev-list", "--left-right", "--count", "#{branch}...#{remote_ref}" )
|
|
67
|
+
return :unknown unless success
|
|
68
|
+
|
|
69
|
+
parts = ahead_behind.strip.split( /\s+/ )
|
|
70
|
+
ahead = parts[ 0 ].to_i
|
|
71
|
+
behind = parts[ 1 ].to_i
|
|
72
|
+
|
|
73
|
+
if ahead.zero? && behind.zero?
|
|
74
|
+
:in_sync
|
|
75
|
+
elsif ahead.positive? && behind.zero?
|
|
76
|
+
:ahead
|
|
77
|
+
elsif ahead.zero? && behind.positive?
|
|
78
|
+
:behind
|
|
79
|
+
else
|
|
80
|
+
:diverged
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Lists all worktrees with branch and lifecycle state.
|
|
85
|
+
def gather_worktree_info
|
|
86
|
+
entries = worktree_list
|
|
87
|
+
# Filter out the main worktree (the repository root itself).
|
|
88
|
+
entries.reject { |wt| wt.fetch( :path ) == repo_root }.map do |wt|
|
|
89
|
+
{
|
|
90
|
+
path: wt.fetch( :path ),
|
|
91
|
+
name: File.basename( wt.fetch( :path ) ),
|
|
92
|
+
branch: wt.fetch( :branch, nil )
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Queries open PRs via gh.
|
|
98
|
+
def gather_pr_info
|
|
99
|
+
stdout, _, success, = gh_run(
|
|
100
|
+
"pr", "list", "--state", "open",
|
|
101
|
+
"--json", "number,title,headRefName,statusCheckRollup,reviewDecision"
|
|
102
|
+
)
|
|
103
|
+
return [] unless success
|
|
104
|
+
|
|
105
|
+
prs = JSON.parse( stdout ) rescue []
|
|
106
|
+
prs.map do |pr|
|
|
107
|
+
ci = summarise_checks( rollup: pr[ "statusCheckRollup" ] )
|
|
108
|
+
review = pr[ "reviewDecision" ].to_s
|
|
109
|
+
review_label = review_decision_label( decision: review )
|
|
110
|
+
|
|
111
|
+
{
|
|
112
|
+
number: pr[ "number" ],
|
|
113
|
+
title: pr[ "title" ],
|
|
114
|
+
branch: pr[ "headRefName" ],
|
|
115
|
+
ci: ci,
|
|
116
|
+
review: review_label
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Summarises check rollup into a single status word.
|
|
122
|
+
def summarise_checks( rollup: )
|
|
123
|
+
entries = Array( rollup )
|
|
124
|
+
return :none if entries.empty?
|
|
125
|
+
|
|
126
|
+
states = entries.map { |c| c[ "conclusion" ].to_s.upcase }
|
|
127
|
+
return :fail if states.any? { |s| s == "FAILURE" || s == "CANCELLED" || s == "TIMED_OUT" }
|
|
128
|
+
return :pending if states.any? { |s| s == "" || s == "PENDING" || s == "QUEUED" || s == "IN_PROGRESS" }
|
|
129
|
+
|
|
130
|
+
:pass
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Translates GitHub review decision to a concise label.
|
|
134
|
+
def review_decision_label( decision: )
|
|
135
|
+
case decision.upcase
|
|
136
|
+
when "APPROVED" then :approved
|
|
137
|
+
when "CHANGES_REQUESTED" then :changes_requested
|
|
138
|
+
when "REVIEW_REQUIRED" then :review_required
|
|
139
|
+
else :none
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Counts local branches that are stale (tracking a deleted upstream).
|
|
144
|
+
def gather_stale_branch_info
|
|
145
|
+
stdout, _, success, = git_run( "branch", "-vv" )
|
|
146
|
+
return { count: 0 } unless success
|
|
147
|
+
|
|
148
|
+
gone_branches = stdout.lines.select { |l| l.include?( ": gone]" ) }
|
|
149
|
+
{ count: gone_branches.size }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Quick governance health check: are templates in sync?
|
|
153
|
+
def gather_governance_info
|
|
154
|
+
result = with_captured_output { template_check! }
|
|
155
|
+
{
|
|
156
|
+
templates: result == EXIT_OK ? :in_sync : :drifted
|
|
157
|
+
}
|
|
158
|
+
rescue StandardError
|
|
159
|
+
{ templates: :unknown }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Prints the human-readable status report.
|
|
163
|
+
def print_status( data: )
|
|
164
|
+
puts_line "Carson #{data.fetch( :version )}"
|
|
165
|
+
puts_line ""
|
|
166
|
+
|
|
167
|
+
# Branch
|
|
168
|
+
branch = data.fetch( :branch )
|
|
169
|
+
dirty_marker = branch.fetch( :dirty ) ? " (dirty)" : ""
|
|
170
|
+
sync_marker = format_sync( sync: branch.fetch( :sync ) )
|
|
171
|
+
puts_line "Branch: #{branch.fetch( :name )}#{dirty_marker}#{sync_marker}"
|
|
172
|
+
|
|
173
|
+
# Worktrees
|
|
174
|
+
worktrees = data.fetch( :worktrees )
|
|
175
|
+
if worktrees.any?
|
|
176
|
+
puts_line ""
|
|
177
|
+
puts_line "Worktrees:"
|
|
178
|
+
worktrees.each do |wt|
|
|
179
|
+
branch_label = wt.fetch( :branch ) || "(detached)"
|
|
180
|
+
puts_line " #{wt.fetch( :name )} #{branch_label}"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Pull requests
|
|
185
|
+
prs = data.fetch( :pull_requests, nil )
|
|
186
|
+
if prs && prs.any?
|
|
187
|
+
puts_line ""
|
|
188
|
+
puts_line "Pull requests:"
|
|
189
|
+
prs.each do |pr|
|
|
190
|
+
ci_label = pr.fetch( :ci ).to_s
|
|
191
|
+
review_label = pr.fetch( :review ).to_s.tr( "_", " " )
|
|
192
|
+
puts_line " ##{pr.fetch( :number )} #{pr.fetch( :title )}"
|
|
193
|
+
puts_line " CI: #{ci_label} Review: #{review_label}"
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Stale branches
|
|
198
|
+
stale = data.fetch( :stale_branches, nil )
|
|
199
|
+
if stale && stale.fetch( :count ) > 0
|
|
200
|
+
count = stale.fetch( :count )
|
|
201
|
+
puts_line ""
|
|
202
|
+
puts_line "#{count} stale branch#{plural_suffix( count: count )} ready for pruning."
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Governance
|
|
206
|
+
gov = data.fetch( :governance )
|
|
207
|
+
templates = gov.fetch( :templates )
|
|
208
|
+
unless templates == :in_sync
|
|
209
|
+
puts_line ""
|
|
210
|
+
puts_line "Templates: #{templates} — run `carson sync` to fix."
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Formats sync status for display.
|
|
215
|
+
def format_sync( sync: )
|
|
216
|
+
case sync
|
|
217
|
+
when :in_sync then ""
|
|
218
|
+
when :ahead then " (ahead of remote)"
|
|
219
|
+
when :behind then " (behind remote)"
|
|
220
|
+
when :diverged then " (diverged from remote)"
|
|
221
|
+
when :no_remote then " (no remote tracking)"
|
|
222
|
+
else ""
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
include Status
|
|
228
|
+
end
|
|
229
|
+
end
|
data/lib/carson/runtime.rb
CHANGED
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:
|
|
4
|
+
version: 3.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hailei Wang
|
|
@@ -67,6 +67,7 @@ files:
|
|
|
67
67
|
- lib/carson/runtime/review/sweep_support.rb
|
|
68
68
|
- lib/carson/runtime/review/utility.rb
|
|
69
69
|
- lib/carson/runtime/setup.rb
|
|
70
|
+
- lib/carson/runtime/status.rb
|
|
70
71
|
- lib/carson/version.rb
|
|
71
72
|
- templates/.github/AGENTS.md
|
|
72
73
|
- templates/.github/CLAUDE.md
|