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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 43f5ee17800cad35a51d2aaaf6ce306197bf2823cc6b0de4858912213d06e01b
4
- data.tar.gz: 51b3a3e91d84a98266b2bccee868e583c25609c5466e34c938b2fa2bcd6564fc
3
+ metadata.gz: ec88c12e0edb3541ab43fbefd93c75ef87dfe6032217c1132f6e3cc9857428cf
4
+ data.tar.gz: cba06c9b0c85e0884157e6e2794a58140cbfd903da6eb61d9eb1e96f2f182860
5
5
  SHA512:
6
- metadata.gz: c7bca3554a185fc1951292d62e0bfcfd6b63d77488d22013f14b93885e6b8d043d2d30e20e8e9725796d26bc3aaa031bcce8f0b28474ab3198bf77733c56f0c5
7
- data.tar.gz: ce3240db72d7fb1c6a5aa9f590628d23e3533778cc492def9c73951a8f5b6772a347cb8d64c79e0063fb79f67f16fb663c9a8ecdc33db7f9af15e0204678adfd
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.7.0
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
@@ -222,3 +222,4 @@ require_relative "runtime/govern"
222
222
  require_relative "runtime/setup"
223
223
  require_relative "runtime/status"
224
224
  require_relative "runtime/deliver"
225
+ require_relative "runtime/session"
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.7.0
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