carson 2.32.0 → 3.0.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 +37 -0
- data/VERSION +1 -1
- data/lib/carson/cli.rb +17 -3
- data/lib/carson/runtime/local/prune.rb +7 -13
- data/lib/carson/runtime/local/template.rb +3 -1
- data/lib/carson/runtime/local/worktree.rb +15 -4
- 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: 20f9db167969ae68e42a19efcc96c40108f4f3ab3f8b460369283457c9449e4b
|
|
4
|
+
data.tar.gz: 7e94bf5adcafdbd532bd6e789304e04c2a555b7adcbce441363e681a27493c25
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7bdc870e0ae003717b5e624d61386a41a89b5dcd20ec0e0c6fa5106b26791b53106bc43f763ca6199e79ca2bac8e76efcbff21d1ebbb126996550b009848f79f
|
|
7
|
+
data.tar.gz: 19dce95ec2fd4fc5a89de09f8e26b814e6b077285159d425639e68e635eda195d52171c7e388ce27efd27b685f7486e2fc6f8fcf0e7005407cdf672bb9c9f7a2
|
data/RELEASE.md
CHANGED
|
@@ -5,6 +5,43 @@ 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.0.0 — Agent-Oriented Carson
|
|
9
|
+
|
|
10
|
+
### Theme
|
|
11
|
+
|
|
12
|
+
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.
|
|
13
|
+
|
|
14
|
+
### What changed
|
|
15
|
+
|
|
16
|
+
- **`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.
|
|
17
|
+
- **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.
|
|
18
|
+
- **`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.
|
|
19
|
+
|
|
20
|
+
### UX
|
|
21
|
+
|
|
22
|
+
- `carson status` prints a concise briefing by default. Silence is preserved — status reports only what needs attention.
|
|
23
|
+
- `carson status --json` produces a stable JSON schema for programmatic consumption.
|
|
24
|
+
|
|
25
|
+
### Migration
|
|
26
|
+
|
|
27
|
+
- No breaking changes. All 2.x commands continue to work unchanged.
|
|
28
|
+
- `docs/plan.md` has been removed — superseded by `docs/agent-orient.md`.
|
|
29
|
+
|
|
30
|
+
## 2.33.0 — Safe Worktree Remove
|
|
31
|
+
|
|
32
|
+
### What changed
|
|
33
|
+
|
|
34
|
+
- **`carson worktree remove` no longer force-removes by default.** If the worktree has uncommitted or untracked changes, Carson refuses with a clear message instead of silently discarding work. Pass `--force` to override when you intentionally want to discard changes.
|
|
35
|
+
- **Template sync cleanup is safer.** Carson's internal template propagation tries safe worktree removal first, falling back to `--force` only for its own ephemeral sync worktree.
|
|
36
|
+
|
|
37
|
+
### UX
|
|
38
|
+
|
|
39
|
+
- Dirty worktree removal now shows the worktree name and actionable guidance: "Commit or discard changes first, or use --force to override."
|
|
40
|
+
|
|
41
|
+
### Migration
|
|
42
|
+
|
|
43
|
+
- No action required. Existing `carson worktree remove` calls without `--force` will now refuse on dirty worktrees instead of silently destroying uncommitted work.
|
|
44
|
+
|
|
8
45
|
## 2.32.0 — Worktree-Aware Pruning
|
|
9
46
|
|
|
10
47
|
### What changed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
3.0.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 [setup [--remote NAME] [--main-branch NAME] [--workflow STYLE] [--merge METHOD] [--canonical PATH]|audit|sync|prune [--all]|worktree remove <name-or-path>|onboard [repo_path]|refresh [--all|repo_path]|offboard [repo_path]|template check|template apply|review gate|review sweep|govern [--dry-run] [--json] [--loop SECONDS]|version]"
|
|
56
|
+
opts.banner = "Usage: carson [status [--json]|setup [--remote NAME] [--main-branch NAME] [--workflow STYLE] [--merge METHOD] [--canonical PATH]|audit|sync|prune [--all]|worktree remove <name-or-path>|onboard [repo_path]|refresh [--all|repo_path]|offboard [repo_path]|template check|template apply|review gate|review 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
|
|
@@ -175,12 +177,13 @@ module Carson
|
|
|
175
177
|
|
|
176
178
|
case action
|
|
177
179
|
when "remove"
|
|
180
|
+
force = argv.delete( "--force" ) ? true : false
|
|
178
181
|
worktree_path = argv.shift
|
|
179
182
|
if worktree_path.to_s.strip.empty?
|
|
180
183
|
err.puts "#{BADGE} Missing path for worktree remove. Use: carson worktree remove <name-or-path>"
|
|
181
184
|
return { command: :invalid }
|
|
182
185
|
end
|
|
183
|
-
{ command: "worktree:remove", worktree_path: worktree_path }
|
|
186
|
+
{ command: "worktree:remove", worktree_path: worktree_path, force: force }
|
|
184
187
|
else
|
|
185
188
|
err.puts "#{BADGE} Unknown worktree subcommand: #{action}. Use: carson worktree remove <name-or-path>"
|
|
186
189
|
{ command: :invalid }
|
|
@@ -227,6 +230,15 @@ module Carson
|
|
|
227
230
|
{ command: :invalid }
|
|
228
231
|
end
|
|
229
232
|
|
|
233
|
+
def self.parse_status_command( argv:, err: )
|
|
234
|
+
json_flag = argv.delete( "--json" ) ? true : false
|
|
235
|
+
unless argv.empty?
|
|
236
|
+
err.puts "#{BADGE} Unexpected arguments for status: #{argv.join( ' ' )}"
|
|
237
|
+
return { command: :invalid }
|
|
238
|
+
end
|
|
239
|
+
{ command: "status", json: json_flag }
|
|
240
|
+
end
|
|
241
|
+
|
|
230
242
|
def self.parse_govern_subcommand( argv:, err: )
|
|
231
243
|
options = {
|
|
232
244
|
dry_run: false,
|
|
@@ -265,6 +277,8 @@ module Carson
|
|
|
265
277
|
return Runtime::EXIT_ERROR if command == :invalid
|
|
266
278
|
|
|
267
279
|
case command
|
|
280
|
+
when "status"
|
|
281
|
+
runtime.status!( json_output: parsed.fetch( :json, false ) )
|
|
268
282
|
when "setup"
|
|
269
283
|
runtime.setup!( cli_choices: parsed.fetch( :cli_choices, {} ) )
|
|
270
284
|
when "audit"
|
|
@@ -276,7 +290,7 @@ module Carson
|
|
|
276
290
|
when "prune:all"
|
|
277
291
|
runtime.prune_all!
|
|
278
292
|
when "worktree:remove"
|
|
279
|
-
runtime.worktree_remove!( worktree_path: parsed.fetch( :worktree_path ) )
|
|
293
|
+
runtime.worktree_remove!( worktree_path: parsed.fetch( :worktree_path ), force: parsed.fetch( :force, false ) )
|
|
280
294
|
when "onboard"
|
|
281
295
|
runtime.onboard!
|
|
282
296
|
when "refresh"
|
|
@@ -126,25 +126,19 @@ module Carson
|
|
|
126
126
|
text.empty? ? "unknown error" : text
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
-
# Attempts git branch -D. If blocked by a worktree,
|
|
130
|
-
#
|
|
129
|
+
# Attempts git branch -D. If blocked by a worktree, skips with a diagnostic —
|
|
130
|
+
# prune never removes worktrees because another session may own them.
|
|
131
131
|
def force_delete_local_branch( branch: )
|
|
132
132
|
stdout, stderr, success, = git_run( "branch", "-D", branch )
|
|
133
133
|
return [ stdout, stderr, success ] if success
|
|
134
|
-
return [ stdout, stderr, false ] unless worktree_blocked_error?( error_text: stderr )
|
|
135
134
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
unless rm_success
|
|
141
|
-
error_text = rm_stderr.to_s.strip
|
|
142
|
-
puts_verbose "skip_worktree_remove: #{wt_path} (branch=#{branch}) reason=#{error_text}"
|
|
143
|
-
return [ stdout, stderr, false ]
|
|
135
|
+
if worktree_blocked_error?( error_text: stderr )
|
|
136
|
+
wt_path = worktree_path_for_branch( branch: branch )
|
|
137
|
+
hint = wt_path ? "run: carson worktree remove #{File.basename( wt_path )}" : "remove the worktree first"
|
|
138
|
+
puts_verbose "skip_worktree_blocked: #{branch} (#{hint})"
|
|
144
139
|
end
|
|
145
|
-
puts_verbose "worktree_removed_for_prune: #{wt_path} (branch=#{branch})"
|
|
146
140
|
|
|
147
|
-
|
|
141
|
+
[ stdout, stderr, false ]
|
|
148
142
|
end
|
|
149
143
|
|
|
150
144
|
def worktree_blocked_error?( error_text: )
|
|
@@ -244,7 +244,9 @@ module Carson
|
|
|
244
244
|
end
|
|
245
245
|
|
|
246
246
|
def template_propagate_cleanup!( worktree_dir: )
|
|
247
|
-
|
|
247
|
+
# Try safe removal first; fall back to force only for Carson's own sync worktree.
|
|
248
|
+
_, _, safe_success, = git_run( "worktree", "remove", worktree_dir )
|
|
249
|
+
git_run( "worktree", "remove", "--force", worktree_dir ) unless safe_success
|
|
248
250
|
git_run( "branch", "-D", TEMPLATE_SYNC_BRANCH )
|
|
249
251
|
puts_verbose "template_propagate: worktree and local branch cleaned up"
|
|
250
252
|
rescue StandardError => e
|
|
@@ -3,7 +3,9 @@ module Carson
|
|
|
3
3
|
module Local
|
|
4
4
|
# Safe worktree lifecycle management for coding agents.
|
|
5
5
|
# Enforces the teardown order: exit worktree → git worktree remove → branch cleanup.
|
|
6
|
-
|
|
6
|
+
# Never forces removal — if the worktree has uncommitted changes, refuses unless
|
|
7
|
+
# the user explicitly passes force: true via CLI --force flag.
|
|
8
|
+
def worktree_remove!( worktree_path:, force: false )
|
|
7
9
|
fingerprint_status = block_if_outsider_fingerprints!
|
|
8
10
|
return fingerprint_status unless fingerprint_status.nil?
|
|
9
11
|
|
|
@@ -17,14 +19,23 @@ module Carson
|
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
branch = worktree_branch( path: resolved_path )
|
|
20
|
-
puts_verbose "worktree_remove: path=#{resolved_path} branch=#{branch}"
|
|
22
|
+
puts_verbose "worktree_remove: path=#{resolved_path} branch=#{branch} force=#{force}"
|
|
21
23
|
|
|
22
24
|
# Step 1: remove the worktree (directory + git registration).
|
|
23
|
-
|
|
25
|
+
# Try safe removal first. Only use --force if the user explicitly requested it.
|
|
26
|
+
rm_args = [ "worktree", "remove" ]
|
|
27
|
+
rm_args << "--force" if force
|
|
28
|
+
rm_args << resolved_path
|
|
29
|
+
rm_stdout, rm_stderr, rm_success, = git_run( *rm_args )
|
|
24
30
|
unless rm_success
|
|
25
31
|
error_text = rm_stderr.to_s.strip
|
|
26
32
|
error_text = "unable to remove worktree" if error_text.empty?
|
|
27
|
-
|
|
33
|
+
if !force && ( error_text.downcase.include?( "untracked" ) || error_text.downcase.include?( "modified" ) )
|
|
34
|
+
puts_line "Worktree has uncommitted changes: #{File.basename( resolved_path )}"
|
|
35
|
+
puts_line " Commit or discard changes first, or use --force to override."
|
|
36
|
+
else
|
|
37
|
+
puts_line "ERROR: #{error_text}"
|
|
38
|
+
end
|
|
28
39
|
return EXIT_ERROR
|
|
29
40
|
end
|
|
30
41
|
puts_verbose "worktree_removed: #{resolved_path}"
|
|
@@ -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.0.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
|