carson 3.7.0 → 3.9.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 +42 -0
- data/VERSION +1 -1
- data/lib/carson/cli.rb +33 -1
- data/lib/carson/runtime/deliver.rb +3 -0
- data/lib/carson/runtime/local/worktree.rb +6 -0
- data/lib/carson/runtime/session.rb +228 -0
- data/lib/carson/runtime/status.rb +53 -4
- 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: ffbada076dd17e9f8ae9a85e9abce9abbc84e23b71324c8cde1ffec1db537071
|
|
4
|
+
data.tar.gz: 4654c5d248f856818ffa83600b4db21838c0f665bb6a16f2d45d566643312ec6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b4e517ccc14a75a2389bf5743fdd095b2796605c5c7fbeb4d12af4b0f237f370a275e2ff244436da4439b10e44b38a7aceae44c26b858d2224a8b0a25b1d193f
|
|
7
|
+
data.tar.gz: a1de0764868ee6fda99d4f74f5ec10dfc2071a84e65102ab77749e83a8307aee37aa661d98b3670e1d62aeda28f1be1a05093283ac697de3a39ad542256e6da9
|
data/RELEASE.md
CHANGED
|
@@ -5,6 +5,48 @@ 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.9.0 — Agent Coordination Signals
|
|
9
|
+
|
|
10
|
+
### What changed
|
|
11
|
+
|
|
12
|
+
- **Per-session state files** — each Runtime instance gets a unique session ID (`<pid>-<timestamp>`) and its own session file at `~/.carson/sessions/<repo_slug>/<session_id>.json`. Multiple agents on the same repo each have independent state that does not collide.
|
|
13
|
+
- **Session ownership on worktrees** — `carson status` cross-references active session files to annotate each worktree with its owning session's PID, task, and staleness. Other agents can see which worktrees are in use and by whom.
|
|
14
|
+
- **Staleness detection** — sessions whose PID is no longer running and whose last update is older than 1 hour are marked as `stale`. This signals to other agents that the worktree may be abandoned and available for cleanup.
|
|
15
|
+
- **`session_list`** — new method that scans all session files for the current repo and returns structured data with staleness annotations.
|
|
16
|
+
- **Migration from 3.8 format** — old single-file session state (`<slug>.json`) is automatically migrated to the per-session directory format on first access.
|
|
17
|
+
|
|
18
|
+
### UX
|
|
19
|
+
|
|
20
|
+
- `carson status` worktree listing now shows owner context: task description if set, PID if no task, or "stale session" if the owner is no longer running.
|
|
21
|
+
- `carson session --json` now includes `session_id` and `pid` fields for correlation by other agents.
|
|
22
|
+
- Human output unchanged when no session ownership data exists.
|
|
23
|
+
|
|
24
|
+
### Migration
|
|
25
|
+
|
|
26
|
+
- Automatic. The old `<slug>.json` file is moved into the new `<slug>/` directory as `migrated.json` on first access. No user action required.
|
|
27
|
+
|
|
28
|
+
## 3.8.0 — Session State
|
|
29
|
+
|
|
30
|
+
### What changed
|
|
31
|
+
|
|
32
|
+
- **`carson session`** — reads and displays the current session state for a repository. Shows active worktree, PR, task description, and last-updated timestamp.
|
|
33
|
+
- **`carson session --task "description"`** — records a task description in session state so agents resuming work can discover the current objective.
|
|
34
|
+
- **`carson session clear`** — removes all session state for the current repository.
|
|
35
|
+
- **`carson session --json`** / **`carson session clear --json`** — machine-readable JSON output for all session commands.
|
|
36
|
+
- **Side-effect integration** — `worktree_create!` automatically records the active worktree in session state; `worktree_done!` clears it; `deliver!` records the PR number and URL.
|
|
37
|
+
- **Outsider-safe storage** — session files live at `~/.carson/sessions/<basename>-<hash>.json`, outside the user's repository.
|
|
38
|
+
|
|
39
|
+
### UX
|
|
40
|
+
|
|
41
|
+
- Human output shows a concise summary: session name, worktree, PR, task, and timestamp.
|
|
42
|
+
- "No active session state" displayed when no context has been recorded.
|
|
43
|
+
- Session state persists across Carson invocations — agents can `carson session --json` to discover context without re-running discovery commands.
|
|
44
|
+
- `update_session` uses a `:clear` sentinel to selectively remove fields without touching others.
|
|
45
|
+
|
|
46
|
+
### Migration
|
|
47
|
+
|
|
48
|
+
- No breaking changes. New command — no existing behaviour affected.
|
|
49
|
+
|
|
8
50
|
## 3.7.0 — Worktree JSON + Recovery
|
|
9
51
|
|
|
10
52
|
### What changed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.9.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 [status [--json]|setup|audit [--json]|sync [--json]|deliver [--merge] [--json] [--title T] [--body-file F]|prune [--all] [--json]|worktree [--json] create|done|remove <name>|onboard|refresh [--all]|offboard|template check|apply|review gate|sweep|govern [--dry-run] [--json] [--loop SECONDS]|version]"
|
|
56
|
+
opts.banner = "Usage: carson [status [--json]|setup|audit [--json]|sync [--json]|deliver [--merge] [--json] [--title T] [--body-file F]|prune [--all] [--json]|worktree [--json] create|done|remove <name>|onboard|refresh [--all]|offboard|template check|apply|review gate|sweep|govern [--dry-run] [--json] [--loop SECONDS]|session [--json] [--task T]|session clear [--json]|version]"
|
|
57
57
|
end
|
|
58
58
|
end
|
|
59
59
|
|
|
@@ -98,6 +98,8 @@ module Carson
|
|
|
98
98
|
parse_deliver_command( argv: argv, err: err )
|
|
99
99
|
when "govern"
|
|
100
100
|
parse_govern_subcommand( argv: argv, err: err )
|
|
101
|
+
when "session"
|
|
102
|
+
parse_session_command( argv: argv, err: err )
|
|
101
103
|
else
|
|
102
104
|
parser.parse!( argv )
|
|
103
105
|
{ command: command }
|
|
@@ -335,6 +337,29 @@ module Carson
|
|
|
335
337
|
{ command: :invalid }
|
|
336
338
|
end
|
|
337
339
|
|
|
340
|
+
def self.parse_session_command( argv:, err: )
|
|
341
|
+
json_flag = argv.delete( "--json" ) ? true : false
|
|
342
|
+
task_value = nil
|
|
343
|
+
# Check for --task "description"
|
|
344
|
+
task_index = argv.index( "--task" )
|
|
345
|
+
if task_index
|
|
346
|
+
argv.delete_at( task_index )
|
|
347
|
+
task_value = argv.delete_at( task_index )
|
|
348
|
+
if task_value.to_s.strip.empty?
|
|
349
|
+
err.puts "#{BADGE} Missing value for --task. Use: carson session --task \"description\""
|
|
350
|
+
return { command: :invalid }
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
action = argv.first
|
|
355
|
+
if action == "clear"
|
|
356
|
+
argv.shift
|
|
357
|
+
return { command: "session:clear", json: json_flag }
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
{ command: "session", json: json_flag, task: task_value }
|
|
361
|
+
end
|
|
362
|
+
|
|
338
363
|
def self.dispatch( parsed:, runtime: )
|
|
339
364
|
command = parsed.fetch( :command )
|
|
340
365
|
return Runtime::EXIT_ERROR if command == :invalid
|
|
@@ -387,6 +412,13 @@ module Carson
|
|
|
387
412
|
json_output: parsed.fetch( :json, false ),
|
|
388
413
|
loop_seconds: parsed.fetch( :loop_seconds, nil )
|
|
389
414
|
)
|
|
415
|
+
when "session"
|
|
416
|
+
runtime.session!(
|
|
417
|
+
task: parsed.fetch( :task, nil ),
|
|
418
|
+
json_output: parsed.fetch( :json, false )
|
|
419
|
+
)
|
|
420
|
+
when "session:clear"
|
|
421
|
+
runtime.session_clear!( json_output: parsed.fetch( :json, false ) )
|
|
390
422
|
else
|
|
391
423
|
runtime.send( :puts_line, "Unknown command: #{command}" )
|
|
392
424
|
Runtime::EXIT_ERROR
|
|
@@ -37,6 +37,9 @@ module Carson
|
|
|
37
37
|
result[ :pr_number ] = pr_number
|
|
38
38
|
result[ :pr_url ] = pr_url
|
|
39
39
|
|
|
40
|
+
# Record PR in session state.
|
|
41
|
+
update_session( pr: { number: pr_number, url: pr_url } )
|
|
42
|
+
|
|
40
43
|
# Without --merge, we are done.
|
|
41
44
|
unless merge
|
|
42
45
|
return deliver_finish( result: result, exit_code: EXIT_OK, json_output: json_output )
|
|
@@ -36,6 +36,9 @@ module Carson
|
|
|
36
36
|
)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
# Record active worktree in session state.
|
|
40
|
+
update_session( worktree: { name: name, path: wt_path, branch: name } )
|
|
41
|
+
|
|
39
42
|
worktree_finish(
|
|
40
43
|
result: { command: "worktree create", status: "ok", name: name, path: wt_path, branch: name },
|
|
41
44
|
exit_code: EXIT_OK, json_output: json_output
|
|
@@ -92,6 +95,9 @@ module Carson
|
|
|
92
95
|
end
|
|
93
96
|
end
|
|
94
97
|
|
|
98
|
+
# Clear worktree from session state.
|
|
99
|
+
update_session( worktree: :clear )
|
|
100
|
+
|
|
95
101
|
worktree_finish(
|
|
96
102
|
result: { command: "worktree done", status: "ok", name: name, branch: branch || "(detached)",
|
|
97
103
|
next_step: "carson worktree remove #{name}" },
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Session state persistence for coding agents.
|
|
2
|
+
# Maintains a lightweight JSON file per session in ~/.carson/sessions/<repo_slug>/
|
|
3
|
+
# so agents can discover the current working context without re-running discovery commands.
|
|
4
|
+
# Multiple agents on the same repo each get their own session file.
|
|
5
|
+
# Respects the outsider boundary: state lives in Carson's own space, not the repository.
|
|
6
|
+
require "digest"
|
|
7
|
+
|
|
8
|
+
module Carson
|
|
9
|
+
class Runtime
|
|
10
|
+
module Session
|
|
11
|
+
# Reads and displays current session state for this repository.
|
|
12
|
+
def session!( task: nil, json_output: false )
|
|
13
|
+
if task
|
|
14
|
+
update_session( worktree: nil, pr: nil, task: task )
|
|
15
|
+
state = read_session
|
|
16
|
+
return session_finish(
|
|
17
|
+
result: state.merge( command: "session", status: "ok" ),
|
|
18
|
+
exit_code: EXIT_OK, json_output: json_output
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
state = read_session
|
|
23
|
+
session_finish(
|
|
24
|
+
result: state.merge( command: "session", status: "ok" ),
|
|
25
|
+
exit_code: EXIT_OK, json_output: json_output
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Clears session state for the current session.
|
|
30
|
+
def session_clear!( json_output: false )
|
|
31
|
+
path = session_file_path
|
|
32
|
+
File.delete( path ) if File.exist?( path )
|
|
33
|
+
session_finish(
|
|
34
|
+
result: { command: "session clear", status: "ok", repo: repo_root },
|
|
35
|
+
exit_code: EXIT_OK, json_output: json_output
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Records session state — called as a side effect from other commands.
|
|
40
|
+
# Only non-nil values are updated; nil values preserve existing state.
|
|
41
|
+
def update_session( worktree: nil, pr: nil, task: nil )
|
|
42
|
+
state = read_session
|
|
43
|
+
|
|
44
|
+
if worktree == :clear
|
|
45
|
+
state.delete( :worktree )
|
|
46
|
+
elsif worktree
|
|
47
|
+
state[ :worktree ] = worktree
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if pr == :clear
|
|
51
|
+
state.delete( :pr )
|
|
52
|
+
elsif pr
|
|
53
|
+
state[ :pr ] = pr
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
state[ :task ] = task if task
|
|
57
|
+
state[ :repo ] = repo_root
|
|
58
|
+
state[ :session_id ] = session_id
|
|
59
|
+
state[ :pid ] = Process.pid
|
|
60
|
+
state[ :updated_at ] = Time.now.utc.iso8601
|
|
61
|
+
|
|
62
|
+
write_session( state )
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Returns all active sessions for this repository.
|
|
66
|
+
# Each entry is a parsed session state hash with staleness annotation.
|
|
67
|
+
def session_list
|
|
68
|
+
dir = session_repo_dir
|
|
69
|
+
return [] unless Dir.exist?( dir )
|
|
70
|
+
|
|
71
|
+
Dir.glob( File.join( dir, "*.json" ) ).filter_map do |path|
|
|
72
|
+
data = JSON.parse( File.read( path ), symbolize_names: true ) rescue next
|
|
73
|
+
data[ :stale ] = session_stale?( data )
|
|
74
|
+
data
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# Returns a stable session identifier for this Runtime instance.
|
|
81
|
+
# PID + start timestamp — unique per process, stable across calls.
|
|
82
|
+
def session_id
|
|
83
|
+
@session_id ||= "#{Process.pid}-#{Time.now.utc.strftime( '%Y%m%d%H%M%S' )}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Returns the per-repo session directory: ~/.carson/sessions/<slug>/
|
|
87
|
+
# Migrates from the pre-3.9 single-file format if found.
|
|
88
|
+
def session_repo_dir
|
|
89
|
+
slug = session_repo_slug
|
|
90
|
+
dir = File.join( carson_home, "sessions", slug )
|
|
91
|
+
|
|
92
|
+
# Migrate from pre-3.9 single-file format: <slug>.json → <slug>/<session_id>.json
|
|
93
|
+
old_file = File.join( carson_home, "sessions", "#{slug}.json" )
|
|
94
|
+
if File.file?( old_file ) && !Dir.exist?( dir )
|
|
95
|
+
FileUtils.mkdir_p( dir )
|
|
96
|
+
migrated = File.join( dir, "migrated.json" )
|
|
97
|
+
FileUtils.mv( old_file, migrated )
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
FileUtils.mkdir_p( dir )
|
|
101
|
+
dir
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Returns the session file path for the current session.
|
|
105
|
+
def session_file_path
|
|
106
|
+
File.join( session_repo_dir, "#{session_id}.json" )
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Generates a readable, unique slug for the repository: basename-shortsha.
|
|
110
|
+
def session_repo_slug
|
|
111
|
+
basename = File.basename( repo_root )
|
|
112
|
+
short_hash = Digest::SHA256.hexdigest( repo_root )[ 0, 8 ]
|
|
113
|
+
"#{basename}-#{short_hash}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Returns Carson's home directory (~/.carson).
|
|
117
|
+
def carson_home
|
|
118
|
+
home = ENV.fetch( "HOME", "" ).to_s
|
|
119
|
+
return File.join( home, ".carson" ) if !home.empty? && home.start_with?( "/" )
|
|
120
|
+
|
|
121
|
+
File.join( "/tmp", ".carson" )
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Reads session state from disk. Returns an empty hash if no state exists.
|
|
125
|
+
def read_session
|
|
126
|
+
path = session_file_path
|
|
127
|
+
return { repo: repo_root, session_id: session_id } unless File.exist?( path )
|
|
128
|
+
|
|
129
|
+
data = JSON.parse( File.read( path ), symbolize_names: true )
|
|
130
|
+
data[ :repo ] = repo_root
|
|
131
|
+
data[ :session_id ] = session_id
|
|
132
|
+
data
|
|
133
|
+
rescue JSON::ParserError, StandardError
|
|
134
|
+
{ repo: repo_root, session_id: session_id }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Writes session state to disk as formatted JSON.
|
|
138
|
+
def write_session( state )
|
|
139
|
+
path = session_file_path
|
|
140
|
+
# Convert symbol keys to strings for clean JSON output.
|
|
141
|
+
string_state = deep_stringify_keys( state )
|
|
142
|
+
File.write( path, JSON.pretty_generate( string_state ) + "\n" )
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Recursively converts symbol keys to strings for JSON serialisation.
|
|
146
|
+
def deep_stringify_keys( hash )
|
|
147
|
+
hash.each_with_object( {} ) do |( key, value ), result|
|
|
148
|
+
string_key = key.to_s
|
|
149
|
+
result[ string_key ] = value.is_a?( Hash ) ? deep_stringify_keys( value ) : value
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Detects whether a session is stale — PID no longer running and updated
|
|
154
|
+
# more than 1 hour ago.
|
|
155
|
+
def session_stale?( data )
|
|
156
|
+
pid = data[ :pid ]
|
|
157
|
+
updated = data[ :updated_at ]
|
|
158
|
+
|
|
159
|
+
# If PID is still running, not stale.
|
|
160
|
+
if pid
|
|
161
|
+
begin
|
|
162
|
+
Process.kill( 0, pid.to_i )
|
|
163
|
+
return false
|
|
164
|
+
rescue Errno::ESRCH
|
|
165
|
+
# Process not running — check age.
|
|
166
|
+
rescue Errno::EPERM
|
|
167
|
+
# Process exists but we lack permission — assume active.
|
|
168
|
+
return false
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# If no timestamp, assume stale.
|
|
173
|
+
return true unless updated
|
|
174
|
+
|
|
175
|
+
# Stale if last updated more than 1 hour ago.
|
|
176
|
+
age = Time.now.utc - Time.parse( updated )
|
|
177
|
+
age > 3600
|
|
178
|
+
rescue StandardError
|
|
179
|
+
true
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Unified output for session results — JSON or human-readable.
|
|
183
|
+
def session_finish( result:, exit_code:, json_output: )
|
|
184
|
+
result[ :exit_code ] = exit_code
|
|
185
|
+
|
|
186
|
+
if json_output
|
|
187
|
+
out.puts JSON.pretty_generate( result )
|
|
188
|
+
else
|
|
189
|
+
print_session_human( result: result )
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
exit_code
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Human-readable output for session state.
|
|
196
|
+
def print_session_human( result: )
|
|
197
|
+
if result[ :command ] == "session clear"
|
|
198
|
+
puts_line "Session state cleared."
|
|
199
|
+
return
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
puts_line "Session: #{File.basename( result[ :repo ].to_s )}"
|
|
203
|
+
|
|
204
|
+
if result[ :worktree ]
|
|
205
|
+
wt = result[ :worktree ]
|
|
206
|
+
puts_line " Worktree: #{wt[ :name ] || wt[ "name" ]} (#{wt[ :branch ] || wt[ "branch" ]})"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
if result[ :pr ]
|
|
210
|
+
pr = result[ :pr ]
|
|
211
|
+
puts_line " PR: ##{pr[ :number ] || pr[ "number" ]} #{pr[ :url ] || pr[ "url" ]}"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
if result[ :task ]
|
|
215
|
+
puts_line " Task: #{result[ :task ]}"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
if result[ :updated_at ]
|
|
219
|
+
puts_line " Updated: #{result[ :updated_at ]}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
puts_line " No active session state." unless result[ :worktree ] || result[ :pr ] || result[ :task ]
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
include Session
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -81,17 +81,47 @@ module Carson
|
|
|
81
81
|
end
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
-
# Lists all worktrees with branch and
|
|
84
|
+
# Lists all worktrees with branch, lifecycle state, and session ownership.
|
|
85
85
|
def gather_worktree_info
|
|
86
86
|
entries = worktree_list
|
|
87
|
+
sessions = session_list
|
|
88
|
+
ownership = build_worktree_ownership( sessions: sessions )
|
|
89
|
+
|
|
87
90
|
# Filter out the main worktree (the repository root itself).
|
|
88
91
|
entries.reject { |wt| wt.fetch( :path ) == repo_root }.map do |wt|
|
|
89
|
-
|
|
92
|
+
name = File.basename( wt.fetch( :path ) )
|
|
93
|
+
info = {
|
|
90
94
|
path: wt.fetch( :path ),
|
|
91
|
-
name:
|
|
95
|
+
name: name,
|
|
92
96
|
branch: wt.fetch( :branch, nil )
|
|
93
97
|
}
|
|
98
|
+
owner = ownership[ name ]
|
|
99
|
+
if owner
|
|
100
|
+
info[ :owner ] = owner[ :session_id ]
|
|
101
|
+
info[ :owner_pid ] = owner[ :pid ]
|
|
102
|
+
info[ :owner_task ] = owner[ :task ]
|
|
103
|
+
info[ :stale ] = owner[ :stale ]
|
|
104
|
+
end
|
|
105
|
+
info
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Builds a name-to-session mapping for worktree ownership.
|
|
110
|
+
def build_worktree_ownership( sessions: )
|
|
111
|
+
result = {}
|
|
112
|
+
sessions.each do |session|
|
|
113
|
+
wt = session[ :worktree ]
|
|
114
|
+
next unless wt
|
|
115
|
+
name = wt[ :name ] || wt[ "name" ]
|
|
116
|
+
next unless name
|
|
117
|
+
result[ name ] = {
|
|
118
|
+
session_id: session[ :session_id ] || session[ "session_id" ],
|
|
119
|
+
pid: session[ :pid ] || session[ "pid" ],
|
|
120
|
+
task: session[ :task ] || session[ "task" ],
|
|
121
|
+
stale: session[ :stale ]
|
|
122
|
+
}
|
|
94
123
|
end
|
|
124
|
+
result
|
|
95
125
|
end
|
|
96
126
|
|
|
97
127
|
# Queries open PRs via gh.
|
|
@@ -177,7 +207,8 @@ module Carson
|
|
|
177
207
|
puts_line "Worktrees:"
|
|
178
208
|
worktrees.each do |wt|
|
|
179
209
|
branch_label = wt.fetch( :branch ) || "(detached)"
|
|
180
|
-
|
|
210
|
+
owner_label = format_worktree_owner( worktree: wt )
|
|
211
|
+
puts_line " #{wt.fetch( :name )} #{branch_label}#{owner_label}"
|
|
181
212
|
end
|
|
182
213
|
end
|
|
183
214
|
|
|
@@ -211,6 +242,24 @@ module Carson
|
|
|
211
242
|
end
|
|
212
243
|
end
|
|
213
244
|
|
|
245
|
+
# Formats owner annotation for a worktree entry.
|
|
246
|
+
def format_worktree_owner( worktree: )
|
|
247
|
+
owner = worktree[ :owner ]
|
|
248
|
+
return "" unless owner
|
|
249
|
+
|
|
250
|
+
stale = worktree[ :stale ]
|
|
251
|
+
task = worktree[ :owner_task ]
|
|
252
|
+
pid = worktree[ :owner_pid ]
|
|
253
|
+
|
|
254
|
+
if stale
|
|
255
|
+
" (stale session #{pid})"
|
|
256
|
+
elsif task
|
|
257
|
+
" (#{task})"
|
|
258
|
+
else
|
|
259
|
+
" (session #{pid})"
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
214
263
|
# Formats sync status for display.
|
|
215
264
|
def format_sync( sync: )
|
|
216
265
|
case sync
|
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: 3.
|
|
4
|
+
version: 3.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Hailei Wang
|
|
@@ -67,6 +67,7 @@ files:
|
|
|
67
67
|
- lib/carson/runtime/review/query_text.rb
|
|
68
68
|
- lib/carson/runtime/review/sweep_support.rb
|
|
69
69
|
- lib/carson/runtime/review/utility.rb
|
|
70
|
+
- lib/carson/runtime/session.rb
|
|
70
71
|
- lib/carson/runtime/setup.rb
|
|
71
72
|
- lib/carson/runtime/status.rb
|
|
72
73
|
- lib/carson/version.rb
|