carson 3.22.1 → 3.23.1

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: 0e8e1399ec6024845aecc0b88bd5549a2dc2c4f9a4b8a9852a725a718eb837d9
4
- data.tar.gz: cc4e9c3b9fe55e238c394d2056a2fe25afe100bdf1191a7b58cfd9b9f05e0fc6
3
+ metadata.gz: 74175b83073b45d8ffdf3ee6aa1af6ebe02f4e8c7eacbc315d5cb7fb42d89578
4
+ data.tar.gz: 9f7eae79be834f3a7b78f9caa5b415946a1205a3d5e09029b88b5f9234a962bc
5
5
  SHA512:
6
- metadata.gz: 63148df8fdf6cd881b4a63a9c3f330221a60481943e2ec97647242a791b0b672d86cc557fd38062e003d531c8ca2007390d0312731595cb24d115f806a73a6e0
7
- data.tar.gz: 74df1a6e073a1c7d4cce7b227344295146d0a3d69c4941b3befe129660ab3eb50ccbbe5465cc35fbabaa7d56f6912ae2f9b6a38f6347e1b1ace2f9eae2e797ed
6
+ metadata.gz: dfc053ce86819ec64133a52a436a52c5719f2f2af60aebbe85ee0316f1173bc706b6c9e9158254319ff3d3c5cd457bd5583d86c818bbc3b4c866a5b90f53c427
7
+ data.tar.gz: 608c6af987bc8351db9febdb9c5d5ef7f181f4b19b0991c20e4033b98f788ba227c9da1bde01f3b40b92299110e91c0bee0f7a23acc95d25f26f44c0ccaddee9
data/API.md CHANGED
@@ -15,7 +15,7 @@ carson <command> [subcommand] [arguments]
15
15
 
16
16
  | Command | Purpose |
17
17
  |---|---|
18
- | `carson setup` | Interactive quiz to configure remote, main branch, workflow, and merge method. Writes `~/.carson/config.json`. |
18
+ | `carson setup` | Interactive quiz to configure remote, main branch, workflow, and canonical lint-policy path. Writes `~/.carson/config.json`. |
19
19
  | `carson onboard [repo_path]` | Apply one-command baseline setup for a target git repository. Auto-triggers `setup` on first run. Installs or refreshes Carson-managed global hooks. |
20
20
  | `carson refresh [repo_path]` | Re-apply hooks, templates, and audit after upgrading Carson. Auto-propagates template updates to the remote via worktree (branch workflow: PR on `carson/template-sync`; trunk workflow: push to main). |
21
21
  | `carson offboard [repo_path]` | Remove Carson-managed host artefacts, detach Carson hooks path, and deregister from `govern.repos`. |
@@ -25,11 +25,14 @@ carson <command> [subcommand] [arguments]
25
25
  | Command | Purpose |
26
26
  |---|---|
27
27
  | `carson audit` | Evaluate governance status and generate report output. |
28
+ | `carson deliver` | Start autonomous branch delivery for the current checkout: push, create or refresh PR, record delivery state, and return immediately. |
28
29
  | `carson sync` | Fast-forward local `main` from configured remote when tree is clean. |
29
30
  | `carson prune` | Remove stale local branches whose upstream refs no longer exist. |
30
31
  | `carson template check` | Detect drift between managed templates and host `.github/*` files. |
31
32
  | `carson template apply` | Write canonical managed template content into host `.github/*` files. |
32
- | `carson status` | Show repository state (branch, worktrees, PRs, governance). |
33
+ | `carson status [--json]` | Show repository delivery state. Default output is Markdown/text; `--json` is the explicit machine contract. |
34
+ | `carson worktree create <name>` | Create an isolated worktree and branch for a new stream of work. |
35
+ | `carson worktree remove <path_or_name>` | Remove a worktree safely and clean up its branch when allowed. |
33
36
 
34
37
  ### Batch commands (Layer 2)
35
38
 
@@ -41,7 +44,7 @@ All batch commands operate across every governed repository registered in `gover
41
44
  | `carson audit --all` | Run governance audit across all governed repos. Reports pass/block/fail per repo. |
42
45
  | `carson sync --all` | Sync main branch across all governed repos. |
43
46
  | `carson prune --all` | Remove stale branches across all governed repos. |
44
- | `carson status --all [--json]` | Portfolio-wide status overview with branch, worktrees, and governance state per repo. |
47
+ | `carson status --all [--json]` | Portfolio-wide delivery overview per governed repository. |
45
48
  | `carson template check --all` | Read-only template drift detection across all governed repos. |
46
49
  | `carson housekeep --all` | Sync, reap dead worktrees, and prune across all governed repos. |
47
50
 
@@ -49,11 +52,11 @@ All batch commands operate across every governed repository registered in `gover
49
52
 
50
53
  | Command | Purpose |
51
54
  |---|---|
52
- | `carson govern [--dry-run] [--json] [--loop SECONDS]` | Portfolio-level PR triage: classify, merge, dispatch agents, escalate. |
55
+ | `carson govern [--dry-run] [--json] [--loop SECONDS]` | Portfolio-level delivery oversight: assess active deliveries, integrate ready branches, dispatch revisions, and escalate blocked work. |
53
56
 
54
57
  `--loop SECONDS` runs the govern cycle continuously, sleeping SECONDS between cycles. The loop isolates errors per cycle — a single failing cycle does not stop the daemon. `Ctrl-C` cleanly exits with a cycle count summary. SECONDS must be a positive integer.
55
58
 
56
- `govern.merge.method` accepts `squash`, `merge`, or `rebase` (default: `squash`). Squash keeps main linear — one PR, one commit. When the target repository enforces linear history via branch protection, both `squash` and `rebase` are accepted by GitHub — only `merge` is rejected.
59
+ Governed integration is fixed to `squash`. Non-squash `govern.merge.method` values are rejected by config validation.
57
60
 
58
61
  ### Review commands
59
62
 
@@ -104,8 +107,7 @@ Environment overrides:
104
107
  - `CARSON_REVIEW_SWEEP_STATES`
105
108
  - `CARSON_WORKFLOW_STYLE`
106
109
  - `CARSON_GOVERN_REPOS`
107
- - `CARSON_GOVERN_AUTO_MERGE`
108
- - `CARSON_GOVERN_MERGE_METHOD`
110
+ - `CARSON_GOVERN_AUTHORITY`
109
111
  - `CARSON_GOVERN_AGENT_PROVIDER`
110
112
  - `CARSON_GOVERN_CHECK_WAIT`
111
113
 
@@ -115,27 +117,29 @@ Environment overrides:
115
117
  {
116
118
  "govern": {
117
119
  "repos": ["~/Dev/project-a", "~/Dev/project-b"],
120
+ "authority": "remote",
118
121
  "agent": {
119
122
  "provider": "auto",
120
123
  "codex": {},
121
124
  "claude": {}
122
125
  },
123
126
  "check_wait": 30,
124
- "auto_merge": true,
125
127
  "merge": {
126
128
  "method": "squash"
127
- }
129
+ },
130
+ "state_path": "~/.carson/state.sqlite3"
128
131
  }
129
132
  }
130
133
  ```
131
134
 
132
135
  `govern` semantics:
133
136
  - `repos`: list of local repo paths to govern (empty = current repo only).
137
+ - `authority`: `"remote"` (default) or `"local"`.
134
138
  - `agent.provider`: `"auto"`, `"codex"`, or `"claude"`.
135
139
  - `agent.codex` / `agent.claude`: provider-specific options (reserved).
136
140
  - `check_wait`: seconds to wait for CI checks before classifying (default: `30`).
137
- - `auto_merge`: `true` (default) — Carson may merge autonomously. Set to `false` to require explicit enablement.
138
- - `merge.method`: `"squash"` (default), `"merge"`, or `"rebase"`.
141
+ - `merge.method`: `"squash"` only in governed mode.
142
+ - `state_path`: SQLite ledger path for active deliveries and revisions.
139
143
 
140
144
  `template` schema:
141
145
 
data/MANUAL.md CHANGED
@@ -48,7 +48,7 @@ On first run (no `~/.carson/config.json` exists), `onboard` launches `carson set
48
48
  carson setup
49
49
  ```
50
50
 
51
- Re-run the interactive setup quiz to change your remote, main branch, workflow style, or merge method. Choices are saved to `~/.carson/config.json`.
51
+ Re-run the interactive setup quiz to change your remote, main branch, workflow style, or canonical lint-policy path. Choices are saved to `~/.carson/config.json`.
52
52
 
53
53
  ### Commit generated files
54
54
 
@@ -154,18 +154,26 @@ cd /path/to/.claude/worktrees/my-feature
154
154
 
155
155
  **2. Work** — make changes, commit, iterate.
156
156
 
157
- **3. Deliver and merge** use Carson's landing path for the repo authority. In remote authority that means push, PR, and merge; in local authority that means integrate into local `main` and back it up by pushing `main`. Managed template drift is corrected and committed automatically before push (3.22.1+). After delivery, Carson prints the exact next command:
157
+ **3. Hand the branch to Carson** `deliver` is the asynchronous branch handoff. In remote authority Carson pushes the branch, creates or refreshes the PR, records delivery state, and returns immediately. Managed template drift is corrected and committed automatically before push (3.23.0+).
158
158
 
159
159
  ```bash
160
- carson deliver --merge
161
- # Output: Merged PR #N via squash.
162
- # Next: cd /path/to/repo && carson worktree remove my-feature
160
+ carson deliver
161
+ # Output: PR #N, Delivery: queued|gated
162
+ # Next: carson status
163
163
  ```
164
164
 
165
- **4. Clean up** — follow the printed next step. After squash merge, Carson detects the content is on main and allows removal without `--force` (3.13.1+):
165
+ **4. Monitor and advance** — `status` is the delivery surface. It shows the current branch plus active deliveries for the repository. Keep `govern` running to advance queued deliveries and revisions across governed repositories:
166
166
 
167
167
  ```bash
168
- cd /path/to/repo && carson worktree remove my-feature
168
+ carson status
169
+ carson govern --loop 300
170
+ ```
171
+
172
+ **5. Clean up landed work** — once the delivery is integrated, use Carson cleanup commands from the main worktree:
173
+
174
+ ```bash
175
+ cd /path/to/repo
176
+ carson worktree remove my-feature
169
177
  carson prune
170
178
  ```
171
179
 
@@ -175,7 +183,7 @@ carson prune
175
183
 
176
184
  After squash or rebase merge, the content matches main — removal proceeds without `--force`.
177
185
 
178
- **Stale worktree recovery** — if a worktree directory is destroyed externally (e.g. by running `gh pr merge --delete-branch` from inside it), `worktree remove` and `prune` handle the stale entry gracefully: they clean up the git registration and delete the branch without error. Use `carson deliver --merge` instead of raw `gh pr merge --delete-branch` to avoid this situation — `deliver` deliberately omits `--delete-branch` so the worktree directory stays intact for orderly cleanup.
186
+ **Stale worktree recovery** — if a worktree directory is destroyed externally (for example by a raw GitHub merge/delete flow), `worktree remove` and `prune` handle the stale entry gracefully: they clean up the git registration and delete the branch without error. Use Carson's `deliver` + `govern` flow instead of raw `gh pr merge --delete-branch` so the worktree directory stays intact for orderly cleanup.
179
187
 
180
188
  ### Carson vs Claude Code EnterWorktree
181
189
 
@@ -271,7 +279,7 @@ Use `--loop SECONDS` to run `carson govern` as a persistent daemon that cycles o
271
279
 
272
280
  ```bash
273
281
  carson govern --loop 300 # cycle every 5 minutes
274
- carson govern --loop 300 --dry-run # observe mode, no merges or dispatches
282
+ carson govern --loop 300 --dry-run # observe mode, no integration or revision dispatch
275
283
  ```
276
284
 
277
285
  The loop is built-in and cross-platform — no cron, launchd, or Task Scheduler required. Run it in a terminal, tmux, screen, or as a system service.
@@ -280,25 +288,15 @@ Each cycle runs independently: if one cycle fails (network error, GitHub API tim
280
288
 
281
289
  ### Govern and Coding Agents
282
290
 
283
- `carson govern` dispatches coding agents (Codex or Claude) when a PR has failing CI checks. The agent receives the failure context and attempts to fix the issues in a follow-up commit. If the agent succeeds, the PR re-enters the governance pipeline. If it fails or times out, the PR is escalated for human attention.
291
+ `carson govern` dispatches coding agents (Codex or Claude) when an active delivery is blocked by CI, review, or policy feedback. The agent receives the failure context and attempts a revision. If the agent succeeds, the delivery re-enters the governance pipeline. If it fails repeatedly or times out, the delivery is escalated for human attention.
284
292
 
285
293
  The agent provider is configurable via `govern.agent.provider` (`auto`, `codex`, or `claude`). In `auto` mode, Carson selects the first available provider.
286
294
 
287
- ## Merge Method and Linear History
295
+ ## Governed Integration Policy
288
296
 
289
- Carson's `govern.merge.method` controls how `carson govern` merges ready PRs. The options are `squash`, `merge`, and `rebase` (default: `squash`). Set this in `~/.carson/config.json`:
297
+ Governed integration is fixed to `squash`. Carson no longer exposes merge-method choice for governed delivery, and config validation rejects non-squash values.
290
298
 
291
- ```json
292
- {
293
- "govern": {
294
- "merge": {
295
- "method": "squash"
296
- }
297
- }
298
- }
299
- ```
300
-
301
- **Why squash is the default.** Squash-to-main keeps history linear: one PR = one commit on main. Every commit on main corresponds to a reviewed, CI-passing unit of work. The benefits:
299
+ **Why squash is fixed.** Squash-to-main keeps history linear: one delivered branch = one commit on main. Every commit on main corresponds to a reviewed, CI-passing unit of work. The benefits:
302
300
 
303
301
  - `git log --oneline` on main tells the full story without merge noise or work-in-progress commits.
304
302
  - Every commit is individually revertable — `git revert <sha>` undoes exactly one PR.
@@ -307,10 +305,7 @@ Carson's `govern.merge.method` controls how `carson govern` merges ready PRs. Th
307
305
 
308
306
  **When to use other methods:**
309
307
 
310
- - `rebase` if you want to preserve individual commits from the branch on main. Both `squash` and `rebase` are compatible with GitHub's "Require linear history" branch protection — only `merge` is rejected.
311
- - `merge` — if you want explicit merge commits. This creates a non-linear graph but preserves branch topology.
312
-
313
- **Important:** Carson's merge method must match your GitHub repository's allowed merge types. If your repo only allows squash merges and Carson is set to `merge`, govern will fail when it tries to auto-merge. Check your repository settings under Settings > General > Pull Requests.
308
+ There is no governed manual-final-merge mode. Repositories that require a human to perform the final merge are outside Carson's governed delivery contract.
314
309
 
315
310
  ## Defaults and Why
316
311
 
@@ -337,15 +332,13 @@ How code reaches main.
337
332
 
338
333
  Change: `carson setup` or `CARSON_WORKFLOW_STYLE`.
339
334
 
340
- #### Merge method
335
+ #### Governed integration
341
336
 
342
- How `carson govern` merges ready PRs.
337
+ How Carson lands ready deliveries.
343
338
 
344
- - **`squash`** (default) — one PR = one commit on main. Linear, bisectable history. Every commit is individually revertable. Branch commits are preserved in the PR on GitHub.
345
- - **`rebase`** — preserves individual branch commits on main. Linear history. Use when commit-level attribution matters.
346
- - **`merge`** — creates merge commits. Non-linear graph but preserves branch topology. Use when branch structure is meaningful.
339
+ - **`squash`** (fixed) — one delivered branch = one commit on main. Linear, bisectable history. Branch commits remain visible in the PR on GitHub.
347
340
 
348
- Must match your GitHub repo's allowed merge types. Change: `carson setup` or `govern.merge.method` in config.
341
+ This is part of Carson's governed contract, not a setup preference.
349
342
 
350
343
  #### Git remote
351
344
 
@@ -380,13 +373,14 @@ Whether reviewer findings require acknowledgement.
380
373
 
381
374
  Change: `CARSON_REVIEW_DISPOSITION`.
382
375
 
383
- #### Merge authority
376
+ #### Delivery authority
384
377
 
385
- Whether `carson govern` can merge PRs autonomously.
378
+ Where completed work rejoins shared truth.
386
379
 
387
- - Default: **enabled**. Carson merges PRs that pass all gates (CI green, review clean, audit clean). PRs that need human judgement are escalated, never silently merged.
380
+ - **`remote`** (default) the PR lands on remote `main`.
381
+ - **`local`** — Carson integrates through local `main`, then pushes `main` as backup.
388
382
 
389
- Disable: `govern.auto_merge: false` in config or `CARSON_GOVERN_AUTO_MERGE=false`.
383
+ Change: `govern.authority` in config.
390
384
 
391
385
  #### Output verbosity
392
386
 
@@ -415,7 +409,7 @@ Common environment overrides:
415
409
  | `CARSON_REVIEW_SWEEP_WINDOW_DAYS` | Lookback window for review sweep. |
416
410
  | `CARSON_REVIEW_SWEEP_STATES` | PR states to include in sweep. |
417
411
  | `CARSON_REVIEW_BOT_USERNAMES` | Comma-separated bot usernames to ignore in review gate and sweep. |
418
- | `CARSON_GOVERN_AUTO_MERGE` | Enable or disable autonomous PR merging. |
412
+ | `CARSON_GOVERN_AUTHORITY` | Override delivery authority (`remote` or `local`). |
419
413
  | `CARSON_WORKFLOW_STYLE` | Workflow style override (`branch` or `trunk`). |
420
414
  | `CARSON_RUBY_INDENTATION` | Ruby indentation policy (`tabs`, `spaces`, or `either`). |
421
415
 
data/README.md CHANGED
@@ -57,21 +57,18 @@ Prerequisites: Ruby `>= 3.4`, `git`, and `gem` in your `PATH`. `gh` (GitHub CLI)
57
57
  gem install carson
58
58
  carson onboard your/repo/path
59
59
 
60
- # Optional: switch from the default remote authority
61
- carson repo authority local
62
-
63
60
  carson worktree create your-worktree
64
61
  cd your/repo/path/.claude/worktrees/your-worktree
65
62
 
66
63
  # work, test, commit
67
- carson deliver --merge
64
+ carson deliver
65
+ carson status
68
66
 
69
- cd your/repo/path
70
- carson worktree remove your-worktree
71
- carson prune
67
+ # keep govern running to advance queued deliveries
68
+ carson govern --loop 300
72
69
  ```
73
70
 
74
- By default, repositories onboard as `remote`. In `remote`, `deliver` lands through remote `main`. In `local`, the same loop lands on local `main` and then pushes `main` to the remote as backup.
71
+ By default, repositories onboard as `remote`. `carson deliver` is the branch handoff: it pushes the branch, creates or refreshes the PR, records delivery state, and returns immediately. `carson status` shows the active branch deliveries, and `carson govern` advances queued work across the governed portfolio.
75
72
 
76
73
  ## Portfolio Layer
77
74
 
@@ -83,7 +80,7 @@ carson refresh --all
83
80
  carson govern --dry-run
84
81
  ```
85
82
 
86
- `carson govern` is the portfolio layer. It triages open PRs, merges what is ready, dispatches agents to fix what is failing, and reports what needs human judgement.
83
+ `carson govern` is the portfolio layer. It advances queued deliveries, dispatches revision work for blocked branches, and surfaces what needs human judgement. Governed integration is squash-only and happens one repository at a time.
87
84
 
88
85
  ## Where to Read Next
89
86
 
data/RELEASE.md CHANGED
@@ -5,6 +5,29 @@ 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.23.1
9
+
10
+ ### What changed
11
+
12
+ - **Govern no longer depends on cache-report writes** — `carson govern` no longer writes `govern_latest.md/json` under `~/.carson/cache` as part of its normal execution. Govern now advances deliveries directly from the ledger and prints results to stdout/JSON only.
13
+
14
+ ### No migration required
15
+
16
+ ## 3.23.0
17
+
18
+ ### What changed
19
+
20
+ - **Single-actor branch delivery redesign** — Carson is now the sole actor. Repository and branch state are recorded in a SQLite delivery ledger, `carson deliver` becomes the asynchronous branch handoff, and `carson govern` advances queued deliveries and revision cycles without blocking the caller process.
21
+ - **Status redesigned around deliveries** — `carson status` now reports repository authority, the current branch, active deliveries, and stale-branch counts. `--json` exposes the same delivery-centred model explicitly for software consumers.
22
+ - **Governed integration is squash-only** — Carson now enforces a single governed integration policy. Repository settings must allow squash merge, and Carson rejects governed configs that specify any other merge method.
23
+ - **Setup and help contract simplified** — `carson setup` no longer offers merge-method choice for governed repositories, and `carson deliver --merge` is removed in favour of the single autonomous `carson deliver` path.
24
+
25
+ ### Migration required
26
+
27
+ - Set governed repositories to allow squash merge before using Carson-governed integration.
28
+ - Update Carson config so `govern.merge.method` is `squash`.
29
+ - Stop using `carson deliver --merge`; use `carson deliver`.
30
+
8
31
  ## 3.22.1
9
32
 
10
33
  ### What changed
data/VERSION CHANGED
@@ -1 +1 @@
1
- 3.22.1
1
+ 3.23.1
data/carson.gemspec CHANGED
@@ -28,6 +28,7 @@ Gem::Specification.new do |spec|
28
28
  spec.bindir = "exe"
29
29
  spec.executables = [ "carson" ]
30
30
  spec.require_paths = [ "lib" ]
31
+ spec.add_dependency "sqlite3", ">= 1.3", "< 3"
31
32
  spec.files = Dir.glob( "{lib,exe,templates,hooks}/**/*", File::FNM_DOTMATCH ).select { |path| File.file?( path ) } + [
32
33
  ".github/workflows/carson_policy.yml",
33
34
  "README.md",
@@ -2,14 +2,14 @@
2
2
  module Carson
3
3
  module Adapters
4
4
  module Agent
5
- WorkOrder = Data.define( :repo, :branch, :pr_number, :objective, :context, :acceptance_checks )
5
+ WorkOrder = Struct.new( :repo, :branch, :pr_number, :objective, :context, :acceptance_checks, keyword_init: true )
6
6
  # objective: "fix_ci" | "address_review" | "fix_audit"
7
7
  # context: String (legacy — PR title) or Hash with structured evidence:
8
8
  # fix_ci: { title:, ci_logs:, ci_run_url:, prior_attempt: { summary:, dispatched_at: } }
9
9
  # address_review: { title:, review_findings: [{ kind:, url:, body: }], prior_attempt: ... }
10
10
  # acceptance_checks: what must pass for the fix to be accepted
11
11
 
12
- Result = Data.define( :status, :summary, :evidence, :commit_sha )
12
+ Result = Struct.new( :status, :summary, :evidence, :commit_sha, keyword_init: true )
13
13
  # status: "done" | "failed" | "timeout"
14
14
  end
15
15
  end
@@ -0,0 +1,38 @@
1
+ # Passive branch record. Branch holds identity and current facts only.
2
+ module Carson
3
+ class Branch
4
+ attr_reader :repository, :name, :purpose, :head, :worktree, :delivery
5
+
6
+ def initialize( repository:, name:, runtime:, purpose: nil, head: nil, worktree: nil, delivery: nil )
7
+ @repository = repository
8
+ @name = name
9
+ @runtime = runtime
10
+ @purpose = purpose
11
+ @head = head
12
+ @worktree = worktree
13
+ @delivery = delivery
14
+ end
15
+
16
+ # Re-reads the branch facts from git and Carson's ledger.
17
+ def reload
18
+ refreshed_head = runtime.git_capture!( "rev-parse", name ).strip
19
+ refreshed_worktree = runtime.worktree_list.find { |entry| entry.branch == name }&.path || worktree
20
+ refreshed_delivery = runtime.ledger.active_delivery( repo_path: repository.path, branch_name: name )
21
+ self.class.new(
22
+ repository: repository,
23
+ name: name,
24
+ runtime: runtime,
25
+ purpose: purpose,
26
+ head: refreshed_head,
27
+ worktree: refreshed_worktree,
28
+ delivery: refreshed_delivery
29
+ )
30
+ rescue StandardError
31
+ self
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :runtime
37
+ end
38
+ end
data/lib/carson/cli.rb CHANGED
@@ -61,11 +61,11 @@ module Carson
61
61
  parser.separator "Repository governance and workflow automation for coding agents."
62
62
  parser.separator ""
63
63
  parser.separator "Commands:"
64
- parser.separator " status Show repository state (branch, PRs, worktrees)"
64
+ parser.separator " status Show repository delivery state"
65
65
  parser.separator " setup Initialise Carson configuration"
66
66
  parser.separator " audit Run pre-commit health checks"
67
67
  parser.separator " sync Sync local main with remote"
68
- parser.separator " deliver Push, create PR, and optionally merge"
68
+ parser.separator " deliver Start autonomous branch delivery"
69
69
  parser.separator " prune Remove stale local branches"
70
70
  parser.separator " worktree Manage isolated coding worktrees"
71
71
  parser.separator " housekeep Sync, reap worktrees, and prune branches"
@@ -138,7 +138,7 @@ module Carson
138
138
  def self.parse_setup_command( arguments:, error: )
139
139
  options = {}
140
140
  setup_parser = OptionParser.new do |parser|
141
- parser.banner = "Usage: carson setup [--remote NAME] [--main-branch NAME] [--workflow STYLE] [--merge METHOD] [--canonical PATH]"
141
+ parser.banner = "Usage: carson setup [--remote NAME] [--main-branch NAME] [--workflow STYLE] [--canonical PATH]"
142
142
  parser.separator ""
143
143
  parser.separator "Initialise Carson configuration for the current repository."
144
144
  parser.separator "Detects git remote, main branch, and workflow style, then writes .carson.yml."
@@ -148,13 +148,11 @@ module Carson
148
148
  parser.on( "--remote NAME", "Git remote name" ) { |value| options[ "git.remote" ] = value }
149
149
  parser.on( "--main-branch NAME", "Main branch name" ) { |value| options[ "git.main_branch" ] = value }
150
150
  parser.on( "--workflow STYLE", "Workflow style (branch or trunk)" ) { |value| options[ "workflow.style" ] = value }
151
- parser.on( "--merge METHOD", "Merge method (squash, rebase, or merge)" ) { |value| options[ "govern.merge.method" ] = value }
152
151
  parser.on( "--canonical PATH", "Canonical lint policy directory path" ) { |value| options[ "lint.canonical" ] = value }
153
152
  parser.separator ""
154
153
  parser.separator "Examples:"
155
154
  parser.separator " carson setup Auto-detect and write config"
156
155
  parser.separator " carson setup --remote github Use 'github' as the git remote"
157
- parser.separator " carson setup --merge squash Set squash as the merge method"
158
156
  end
159
157
  setup_parser.parse!( arguments )
160
158
  unless arguments.empty?
@@ -547,22 +545,25 @@ module Carson
547
545
  # --- deliver ---
548
546
 
549
547
  def self.parse_deliver_command( arguments:, error: )
550
- options = { merge: false, json: false, title: nil, body_file: nil }
548
+ if arguments.include?( "--merge" )
549
+ error.puts "#{BADGE} carson deliver --merge is no longer supported; use carson deliver"
550
+ return { command: :invalid }
551
+ end
552
+
553
+ options = { json: false, title: nil, body_file: nil }
551
554
  deliver_parser = OptionParser.new do |parser|
552
- parser.banner = "Usage: carson deliver [--merge] [--json] [--title TITLE] [--body-file PATH]"
555
+ parser.banner = "Usage: carson deliver [--json] [--title TITLE] [--body-file PATH]"
553
556
  parser.separator ""
554
- parser.separator "Push the current branch, create a pull request, and optionally merge."
555
- parser.separator "Collapses the manual push PR merge flow into a single command."
557
+ parser.separator "Push the current branch, create or refresh the pull request, and hand the branch to Carson."
558
+ parser.separator "Carson records delivery state and continues from there."
556
559
  parser.separator ""
557
560
  parser.separator "Options:"
558
- parser.on( "--merge", "Also merge the PR if CI passes" ) { options[ :merge ] = true }
559
561
  parser.on( "--json", "Machine-readable JSON output" ) { options[ :json ] = true }
560
562
  parser.on( "--title TITLE", "PR title (defaults to branch name)" ) { |value| options[ :title ] = value }
561
563
  parser.on( "--body-file PATH", "File containing PR body text" ) { |value| options[ :body_file ] = value }
562
564
  parser.separator ""
563
565
  parser.separator "Examples:"
564
- parser.separator " carson deliver Push and open a PR"
565
- parser.separator " carson deliver --merge Push, open a PR, and merge if CI passes"
566
+ parser.separator " carson deliver Push, open a PR, and register delivery state"
566
567
  end
567
568
  deliver_parser.parse!( arguments )
568
569
  unless arguments.empty?
@@ -572,7 +573,6 @@ module Carson
572
573
  end
573
574
  {
574
575
  command: "deliver",
575
- merge: options.fetch( :merge ),
576
576
  json: options.fetch( :json ),
577
577
  title: options[ :title ],
578
578
  body_file: options[ :body_file ]
@@ -761,7 +761,6 @@ module Carson
761
761
  runtime.template_apply!( push_prep: parsed.fetch( :push_prep, false ) )
762
762
  when "deliver"
763
763
  runtime.deliver!(
764
- merge: parsed.fetch( :merge, false ),
765
764
  title: parsed.fetch( :title, nil ),
766
765
  body_file: parsed.fetch( :body_file, nil ),
767
766
  json_output: parsed.fetch( :json, false )
data/lib/carson/config.rb CHANGED
@@ -30,8 +30,8 @@ module Carson
30
30
  :review_tracking_issue_title, :review_tracking_issue_label, :review_bot_usernames,
31
31
  :audit_advisory_check_names,
32
32
  :workflow_style,
33
- :govern_repos, :govern_auto_merge, :govern_merge_method,
34
- :govern_agent_provider, :govern_dispatch_state_path,
33
+ :govern_repos, :govern_authority, :govern_merge_method,
34
+ :govern_agent_provider, :govern_state_path,
35
35
  :govern_check_wait
36
36
 
37
37
  def self.load( repo_root: )
@@ -83,7 +83,7 @@ module Carson
83
83
  },
84
84
  "govern" => {
85
85
  "repos" => [],
86
- "auto_merge" => true,
86
+ "authority" => "remote",
87
87
  "merge" => {
88
88
  "method" => "squash"
89
89
  },
@@ -92,7 +92,7 @@ module Carson
92
92
  "codex" => {},
93
93
  "claude" => {}
94
94
  },
95
- "dispatch_state_path" => "~/.carson/govern/dispatch_state.json",
95
+ "state_path" => "~/.carson/state.sqlite3",
96
96
  "check_wait" => 30
97
97
  }
98
98
  }
@@ -171,8 +171,8 @@ module Carson
171
171
  govern = fetch_hash_section( data: copy, key: "govern" )
172
172
  govern_repos = env_string_array( key: "CARSON_GOVERN_REPOS" )
173
173
  govern[ "repos" ] = govern_repos unless govern_repos.empty?
174
- govern_auto_merge = ENV.fetch( "CARSON_GOVERN_AUTO_MERGE", "" ).to_s.strip
175
- govern[ "auto_merge" ] = ( govern_auto_merge == "true" ) unless govern_auto_merge.empty?
174
+ govern_authority = ENV.fetch( "CARSON_GOVERN_AUTHORITY", "" ).to_s.strip
175
+ govern[ "authority" ] = govern_authority unless govern_authority.empty?
176
176
  govern_method = ENV.fetch( "CARSON_GOVERN_MERGE_METHOD", "" ).to_s.strip
177
177
  unless govern_method.empty?
178
178
  govern[ "merge" ] ||= {}
@@ -209,7 +209,10 @@ module Carson
209
209
  @main_branch = fetch_string( hash: fetch_hash( hash: data, key: "git" ), key: "main_branch" )
210
210
  @protected_branches = fetch_string_array( hash: fetch_hash( hash: data, key: "git" ), key: "protected_branches" )
211
211
 
212
- @hooks_path = fetch_string( hash: fetch_hash( hash: data, key: "hooks" ), key: "path" )
212
+ @hooks_path = resolve_runtime_path(
213
+ path: fetch_string( hash: fetch_hash( hash: data, key: "hooks" ), key: "path" ),
214
+ fallback_leaf: "hooks"
215
+ )
213
216
  @managed_hooks = fetch_string_array( hash: fetch_hash( hash: data, key: "hooks" ), key: "managed" )
214
217
 
215
218
  template_hash = fetch_hash( hash: data, key: "template" )
@@ -240,13 +243,15 @@ module Carson
240
243
 
241
244
  govern_hash = fetch_hash( hash: data, key: "govern" )
242
245
  @govern_repos = fetch_optional_string_array( hash: govern_hash, key: "repos" ).map { |path| safe_expand_path( path ) }
243
- @govern_auto_merge = fetch_optional_boolean( hash: govern_hash, key: "auto_merge", default: true, key_path: "govern.auto_merge" )
246
+ @govern_authority = fetch_string( hash: govern_hash, key: "authority" ).downcase
244
247
  govern_merge_hash = fetch_hash( hash: govern_hash, key: "merge" )
245
248
  @govern_merge_method = fetch_string( hash: govern_merge_hash, key: "method" ).downcase
246
249
  govern_agent_hash = fetch_hash( hash: govern_hash, key: "agent" )
247
250
  @govern_agent_provider = fetch_string( hash: govern_agent_hash, key: "provider" ).downcase
248
- dispatch_path = govern_hash.fetch( "dispatch_state_path" ).to_s
249
- @govern_dispatch_state_path = safe_expand_path( dispatch_path )
251
+ @govern_state_path = resolve_runtime_path(
252
+ path: govern_hash.fetch( "state_path" ).to_s,
253
+ fallback_leaf: "state.sqlite3"
254
+ )
250
255
  @govern_check_wait = fetch_non_negative_integer( hash: govern_hash, key: "check_wait" )
251
256
 
252
257
  validate!
@@ -267,7 +272,8 @@ module Carson
267
272
  raise ConfigError, "review.tracking_issue.title cannot be empty" if review_tracking_issue_title.empty?
268
273
  raise ConfigError, "review.tracking_issue.label cannot be empty" if review_tracking_issue_label.empty?
269
274
  raise ConfigError, "workflow.style must be one of trunk, branch" unless [ "trunk", "branch" ].include?( workflow_style )
270
- raise ConfigError, "govern.merge.method must be one of merge, squash, rebase" unless [ "merge", "squash", "rebase" ].include?( govern_merge_method )
275
+ raise ConfigError, "govern.authority must be one of remote, local" unless [ "remote", "local" ].include?( govern_authority )
276
+ raise ConfigError, "govern.merge.method must be squash" unless govern_merge_method == "squash"
271
277
  raise ConfigError, "govern.agent.provider must be one of auto, codex, claude" unless [ "auto", "codex", "claude" ].include?( govern_agent_provider )
272
278
  end
273
279
 
@@ -319,13 +325,6 @@ module Carson
319
325
  rescue ArgumentError, TypeError
320
326
  raise ConfigError, "config key #{key} must be an integer"
321
327
  end
322
- def fetch_optional_boolean( hash:, key:, default:, key_path: nil )
323
- value = hash.fetch( key, default )
324
- return true if value == true
325
- return false if value == false
326
-
327
- raise ConfigError, "config key #{key_path || key} must be boolean"
328
- end
329
328
 
330
329
  def safe_expand_path( path )
331
330
  return path unless path.start_with?( "~" )
@@ -335,6 +334,23 @@ module Carson
335
334
  path
336
335
  end
337
336
 
337
+ # Resolves Carson-owned runtime paths even when HOME is intentionally invalid.
338
+ # CI smoke uses that condition to verify TMPDIR and /tmp fallbacks.
339
+ def resolve_runtime_path( path:, fallback_leaf: )
340
+ expanded = safe_expand_path( path )
341
+ return expanded unless expanded.start_with?( "~" )
342
+
343
+ File.join( runtime_fallback_root, fallback_leaf )
344
+ end
345
+
346
+ # Shared fallback root for Carson-owned artefacts when HOME cannot be expanded.
347
+ def runtime_fallback_root
348
+ tmpdir = ENV.fetch( "TMPDIR", "" ).to_s.strip
349
+ return File.join( tmpdir, "carson" ) if tmpdir.start_with?( "/" )
350
+
351
+ "/tmp/carson"
352
+ end
353
+
338
354
  # Returns an expanded path string, or nil when the value is absent/blank.
339
355
  def fetch_optional_path( hash:, key: )
340
356
  value = hash[ key ]