carson 3.22.1 → 3.23.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/API.md +15 -11
- data/MANUAL.md +32 -38
- data/README.md +6 -9
- data/RELEASE.md +15 -0
- data/VERSION +1 -1
- data/carson.gemspec +1 -0
- data/lib/carson/adapters/agent.rb +2 -2
- data/lib/carson/branch.rb +38 -0
- data/lib/carson/cli.rb +13 -14
- data/lib/carson/config.rb +34 -18
- data/lib/carson/delivery.rb +64 -0
- data/lib/carson/ledger.rb +305 -0
- data/lib/carson/repository.rb +47 -0
- data/lib/carson/revision.rb +30 -0
- data/lib/carson/runtime/audit.rb +6 -6
- data/lib/carson/runtime/deliver.rb +112 -169
- data/lib/carson/runtime/govern.rb +232 -368
- data/lib/carson/runtime/housekeep.rb +4 -4
- data/lib/carson/runtime/local/onboard.rb +5 -5
- data/lib/carson/runtime/local/prune.rb +4 -4
- data/lib/carson/runtime/local/template.rb +4 -4
- data/lib/carson/runtime/review/gate_support.rb +14 -12
- data/lib/carson/runtime/review/sweep_support.rb +2 -2
- data/lib/carson/runtime/review/utility.rb +1 -1
- data/lib/carson/runtime/setup.rb +10 -27
- data/lib/carson/runtime/status.rb +87 -226
- data/lib/carson/runtime.rb +25 -2
- data/lib/carson/worktree.rb +5 -5
- data/lib/carson.rb +5 -0
- metadata +27 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3b09da719cacfca1da3e00f8c542719283e0f1e0d73554371449fa01d3d1b331
|
|
4
|
+
data.tar.gz: 33d2ab2f5881e8f96934037f715893492b495354eda5c77c64b47ded8d1ee710
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d5c0dbbae4f9424054cbed42c0d6ed6430643a7f974252bf532f98c8aeeafb5e0962c9204e3e9d806752ac5ee063c98d005b4693d570c2b3d9699b2ac1808bef
|
|
7
|
+
data.tar.gz: d49e6eef6854bf55eb3fe788e6f549585806a03d9266ef5aefdd42b960b3ed9e9f8750d48c3d893d94415e43a56df685701989724d1ff8af439375218f5af743
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
- `
|
|
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
|
-
- `
|
|
138
|
-
- `
|
|
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
|
|
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.
|
|
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
|
|
161
|
-
# Output:
|
|
162
|
-
# Next:
|
|
160
|
+
carson deliver
|
|
161
|
+
# Output: PR #N, Delivery: queued|gated
|
|
162
|
+
# Next: carson status
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
-
**4.
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
##
|
|
295
|
+
## Governed Integration Policy
|
|
288
296
|
|
|
289
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
####
|
|
335
|
+
#### Governed integration
|
|
341
336
|
|
|
342
|
-
How
|
|
337
|
+
How Carson lands ready deliveries.
|
|
343
338
|
|
|
344
|
-
- **`squash`** (
|
|
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
|
-
|
|
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
|
-
####
|
|
376
|
+
#### Delivery authority
|
|
384
377
|
|
|
385
|
-
|
|
378
|
+
Where completed work rejoins shared truth.
|
|
386
379
|
|
|
387
|
-
-
|
|
380
|
+
- **`remote`** (default) — the PR lands on remote `main`.
|
|
381
|
+
- **`local`** — Carson integrates through local `main`, then pushes `main` as backup.
|
|
388
382
|
|
|
389
|
-
|
|
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
|
-
| `
|
|
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
|
|
64
|
+
carson deliver
|
|
65
|
+
carson status
|
|
68
66
|
|
|
69
|
-
|
|
70
|
-
carson
|
|
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`.
|
|
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
|
|
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,21 @@ 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.0
|
|
9
|
+
|
|
10
|
+
### What changed
|
|
11
|
+
|
|
12
|
+
- **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.
|
|
13
|
+
- **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.
|
|
14
|
+
- **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.
|
|
15
|
+
- **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.
|
|
16
|
+
|
|
17
|
+
### Migration required
|
|
18
|
+
|
|
19
|
+
- Set governed repositories to allow squash merge before using Carson-governed integration.
|
|
20
|
+
- Update Carson config so `govern.merge.method` is `squash`.
|
|
21
|
+
- Stop using `carson deliver --merge`; use `carson deliver`.
|
|
22
|
+
|
|
8
23
|
## 3.22.1
|
|
9
24
|
|
|
10
25
|
### What changed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.23.0
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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] [--
|
|
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
|
-
|
|
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 [--
|
|
555
|
+
parser.banner = "Usage: carson deliver [--json] [--title TITLE] [--body-file PATH]"
|
|
553
556
|
parser.separator ""
|
|
554
|
-
parser.separator "Push the current branch, create
|
|
555
|
-
parser.separator "
|
|
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
|
|
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, :
|
|
34
|
-
:govern_agent_provider, :
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
|
|
175
|
-
govern[ "
|
|
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 =
|
|
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
|
-
@
|
|
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
|
-
|
|
249
|
-
|
|
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.
|
|
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 ]
|