carson 3.7.0 → 3.8.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 +22 -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 +161 -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: ec88c12e0edb3541ab43fbefd93c75ef87dfe6032217c1132f6e3cc9857428cf
|
|
4
|
+
data.tar.gz: cba06c9b0c85e0884157e6e2794a58140cbfd903da6eb61d9eb1e96f2f182860
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 33367440b238d8ba9872aac28da62a10bd74aa6179398bde682860ea798851af3943d9f932a5a157b0913449fa82c9961800f0d832a065fe73b6f9b902a148d6
|
|
7
|
+
data.tar.gz: 4fc299aa1cc6b15f0a7498659912395691213fdfd2d778594a4c6ba52ca9ad77f87db810dc91bdb4a47d1f4aa34a2f7c375d62a5b0d9a481847189798cc36e5d
|
data/RELEASE.md
CHANGED
|
@@ -5,6 +5,28 @@ 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.8.0 — Session State
|
|
9
|
+
|
|
10
|
+
### What changed
|
|
11
|
+
|
|
12
|
+
- **`carson session`** — reads and displays the current session state for a repository. Shows active worktree, PR, task description, and last-updated timestamp.
|
|
13
|
+
- **`carson session --task "description"`** — records a task description in session state so agents resuming work can discover the current objective.
|
|
14
|
+
- **`carson session clear`** — removes all session state for the current repository.
|
|
15
|
+
- **`carson session --json`** / **`carson session clear --json`** — machine-readable JSON output for all session commands.
|
|
16
|
+
- **Side-effect integration** — `worktree_create!` automatically records the active worktree in session state; `worktree_done!` clears it; `deliver!` records the PR number and URL.
|
|
17
|
+
- **Outsider-safe storage** — session files live at `~/.carson/sessions/<basename>-<hash>.json`, outside the user's repository.
|
|
18
|
+
|
|
19
|
+
### UX
|
|
20
|
+
|
|
21
|
+
- Human output shows a concise summary: session name, worktree, PR, task, and timestamp.
|
|
22
|
+
- "No active session state" displayed when no context has been recorded.
|
|
23
|
+
- Session state persists across Carson invocations — agents can `carson session --json` to discover context without re-running discovery commands.
|
|
24
|
+
- `update_session` uses a `:clear` sentinel to selectively remove fields without touching others.
|
|
25
|
+
|
|
26
|
+
### Migration
|
|
27
|
+
|
|
28
|
+
- No breaking changes. New command — no existing behaviour affected.
|
|
29
|
+
|
|
8
30
|
## 3.7.0 — Worktree JSON + Recovery
|
|
9
31
|
|
|
10
32
|
### What changed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.8.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,161 @@
|
|
|
1
|
+
# Session state persistence for coding agents.
|
|
2
|
+
# Maintains a lightweight JSON file per repository in ~/.carson/sessions/ so
|
|
3
|
+
# agents can discover the current working context without re-running discovery commands.
|
|
4
|
+
# Respects the outsider boundary: state lives in Carson's own space, not the repository.
|
|
5
|
+
require "digest"
|
|
6
|
+
|
|
7
|
+
module Carson
|
|
8
|
+
class Runtime
|
|
9
|
+
module Session
|
|
10
|
+
# Reads and displays current session state for this repository.
|
|
11
|
+
def session!( task: nil, json_output: false )
|
|
12
|
+
if task
|
|
13
|
+
update_session( worktree: nil, pr: nil, task: task )
|
|
14
|
+
state = read_session
|
|
15
|
+
return session_finish(
|
|
16
|
+
result: state.merge( command: "session", status: "ok" ),
|
|
17
|
+
exit_code: EXIT_OK, json_output: json_output
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
state = read_session
|
|
22
|
+
session_finish(
|
|
23
|
+
result: state.merge( command: "session", status: "ok" ),
|
|
24
|
+
exit_code: EXIT_OK, json_output: json_output
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Clears session state for this repository.
|
|
29
|
+
def session_clear!( json_output: false )
|
|
30
|
+
path = session_file_path
|
|
31
|
+
File.delete( path ) if File.exist?( path )
|
|
32
|
+
session_finish(
|
|
33
|
+
result: { command: "session clear", status: "ok", repo: repo_root },
|
|
34
|
+
exit_code: EXIT_OK, json_output: json_output
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Records session state — called as a side effect from other commands.
|
|
39
|
+
# Only non-nil values are updated; nil values preserve existing state.
|
|
40
|
+
def update_session( worktree: nil, pr: nil, task: nil )
|
|
41
|
+
state = read_session
|
|
42
|
+
|
|
43
|
+
if worktree == :clear
|
|
44
|
+
state.delete( :worktree )
|
|
45
|
+
elsif worktree
|
|
46
|
+
state[ :worktree ] = worktree
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
if pr == :clear
|
|
50
|
+
state.delete( :pr )
|
|
51
|
+
elsif pr
|
|
52
|
+
state[ :pr ] = pr
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
state[ :task ] = task if task
|
|
56
|
+
state[ :repo ] = repo_root
|
|
57
|
+
state[ :updated_at ] = Time.now.utc.iso8601
|
|
58
|
+
|
|
59
|
+
write_session( state )
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
# Returns the session file path for this repository.
|
|
65
|
+
def session_file_path
|
|
66
|
+
sessions_dir = File.join( carson_home, "sessions" )
|
|
67
|
+
FileUtils.mkdir_p( sessions_dir )
|
|
68
|
+
slug = session_repo_slug
|
|
69
|
+
File.join( sessions_dir, "#{slug}.json" )
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Generates a readable, unique slug for the repository: basename-shortsha.
|
|
73
|
+
def session_repo_slug
|
|
74
|
+
basename = File.basename( repo_root )
|
|
75
|
+
short_hash = Digest::SHA256.hexdigest( repo_root )[ 0, 8 ]
|
|
76
|
+
"#{basename}-#{short_hash}"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Returns Carson's home directory (~/.carson).
|
|
80
|
+
def carson_home
|
|
81
|
+
home = ENV.fetch( "HOME", "" ).to_s
|
|
82
|
+
return File.join( home, ".carson" ) if !home.empty? && home.start_with?( "/" )
|
|
83
|
+
|
|
84
|
+
File.join( "/tmp", ".carson" )
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Reads session state from disk. Returns an empty hash if no state exists.
|
|
88
|
+
def read_session
|
|
89
|
+
path = session_file_path
|
|
90
|
+
return { repo: repo_root } unless File.exist?( path )
|
|
91
|
+
|
|
92
|
+
data = JSON.parse( File.read( path ), symbolize_names: true )
|
|
93
|
+
data[ :repo ] = repo_root
|
|
94
|
+
data
|
|
95
|
+
rescue JSON::ParserError, StandardError
|
|
96
|
+
{ repo: repo_root }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Writes session state to disk as formatted JSON.
|
|
100
|
+
def write_session( state )
|
|
101
|
+
path = session_file_path
|
|
102
|
+
# Convert symbol keys to strings for clean JSON output.
|
|
103
|
+
string_state = deep_stringify_keys( state )
|
|
104
|
+
File.write( path, JSON.pretty_generate( string_state ) + "\n" )
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Recursively converts symbol keys to strings for JSON serialisation.
|
|
108
|
+
def deep_stringify_keys( hash )
|
|
109
|
+
hash.each_with_object( {} ) do |( key, value ), result|
|
|
110
|
+
string_key = key.to_s
|
|
111
|
+
result[ string_key ] = value.is_a?( Hash ) ? deep_stringify_keys( value ) : value
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Unified output for session results — JSON or human-readable.
|
|
116
|
+
def session_finish( result:, exit_code:, json_output: )
|
|
117
|
+
result[ :exit_code ] = exit_code
|
|
118
|
+
|
|
119
|
+
if json_output
|
|
120
|
+
out.puts JSON.pretty_generate( result )
|
|
121
|
+
else
|
|
122
|
+
print_session_human( result: result )
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
exit_code
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Human-readable output for session state.
|
|
129
|
+
def print_session_human( result: )
|
|
130
|
+
if result[ :command ] == "session clear"
|
|
131
|
+
puts_line "Session state cleared."
|
|
132
|
+
return
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
puts_line "Session: #{File.basename( result[ :repo ].to_s )}"
|
|
136
|
+
|
|
137
|
+
if result[ :worktree ]
|
|
138
|
+
wt = result[ :worktree ]
|
|
139
|
+
puts_line " Worktree: #{wt[ :name ] || wt[ "name" ]} (#{wt[ :branch ] || wt[ "branch" ]})"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
if result[ :pr ]
|
|
143
|
+
pr = result[ :pr ]
|
|
144
|
+
puts_line " PR: ##{pr[ :number ] || pr[ "number" ]} #{pr[ :url ] || pr[ "url" ]}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
if result[ :task ]
|
|
148
|
+
puts_line " Task: #{result[ :task ]}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
if result[ :updated_at ]
|
|
152
|
+
puts_line " Updated: #{result[ :updated_at ]}"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
puts_line " No active session state." unless result[ :worktree ] || result[ :pr ] || result[ :task ]
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
include Session
|
|
160
|
+
end
|
|
161
|
+
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: 3.
|
|
4
|
+
version: 3.8.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
|