carson 3.27.1 → 3.29.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/.github/workflows/carson_policy.yml +3 -3
- data/API.md +35 -8
- data/MANUAL.md +17 -14
- data/README.md +15 -8
- data/RELEASE.md +25 -0
- data/VERSION +1 -1
- data/lib/carson/delivery.rb +9 -2
- data/lib/carson/ledger.rb +72 -1
- data/lib/carson/runtime/abandon.rb +12 -9
- data/lib/carson/runtime/deliver.rb +779 -85
- data/lib/carson/runtime/govern.rb +163 -75
- data/lib/carson/runtime/housekeep.rb +5 -9
- data/lib/carson/runtime/local/merge_proof.rb +217 -0
- data/lib/carson/runtime/local/sync.rb +89 -0
- data/lib/carson/runtime/local/worktree.rb +9 -23
- data/lib/carson/runtime/local.rb +1 -0
- data/lib/carson/runtime/loop_runner.rb +90 -0
- data/lib/carson/runtime/status.rb +34 -1
- data/lib/carson/runtime.rb +9 -2
- data/lib/carson/worktree.rb +114 -26
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9d026cfc1581b568aaa9459ab21faf0d4f52d1026995b5ce62f21dca0072853a
|
|
4
|
+
data.tar.gz: '08bdaaebab6ccaf1dac6768057f55acf3833ddbe903001b2ca3934551f280bae'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 68d7fd88800a7a2756466ae88e3be1baae2df66b25d285cd54b676b6506d1ee86c69a110bf4f040c8259fc9dc8aa623120b0150b4a73c3081460c080940bfb2b
|
|
7
|
+
data.tar.gz: 35403e45bad7632031bbc9c56a8aece6e7b3dc7eafa1c36c4f183bc7c6271beb1b39d4b35985373cb9be0baee3b8cc58491920bb2f63dcf852fb20c4198c33ed
|
|
@@ -27,13 +27,13 @@ jobs:
|
|
|
27
27
|
|
|
28
28
|
steps:
|
|
29
29
|
- name: Checkout host repository
|
|
30
|
-
uses: actions/checkout@
|
|
30
|
+
uses: actions/checkout@v6
|
|
31
31
|
with:
|
|
32
32
|
path: host
|
|
33
33
|
fetch-depth: 0
|
|
34
34
|
|
|
35
35
|
- name: Checkout Carson runtime
|
|
36
|
-
uses: actions/checkout@
|
|
36
|
+
uses: actions/checkout@v6
|
|
37
37
|
with:
|
|
38
38
|
repository: wanghailei/carson
|
|
39
39
|
ref: ${{ inputs.carson_ref }}
|
|
@@ -42,7 +42,7 @@ jobs:
|
|
|
42
42
|
- name: Setup Ruby
|
|
43
43
|
uses: ruby/setup-ruby@v1
|
|
44
44
|
with:
|
|
45
|
-
ruby-version: "4
|
|
45
|
+
ruby-version: "3.4"
|
|
46
46
|
|
|
47
47
|
- name: Validate expected version
|
|
48
48
|
run: |
|
data/API.md
CHANGED
|
@@ -25,14 +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 [--commit MESSAGE]` | Run Carson-owned branch delivery for the current checkout. Plain `deliver` transports existing commits only; `--commit` creates one all-dirty delivery commit first,
|
|
28
|
+
| `carson deliver [--commit MESSAGE]` | Run Carson-owned branch delivery for the current checkout. Plain `deliver` transports existing commits only; `--commit` creates one all-dirty delivery commit first. Before push, Carson verifies the branch is fresh against the configured remote `main`; behind or unknown freshness blocks delivery without creating or refreshing a PR. If freshness is good, Carson pushes, creates or refreshes the PR, watches the delivery for a bounded settle window, merges when clear, syncs local `main`, and reports merge proof for the delivered branch. Non-integrated exits report `Merge deferred` or `Merge blocked` with explicit handoff commands. |
|
|
29
29
|
| `carson recover --check NAME [--json]` | Run the exceptional governed recovery path when one governance-owned required check is already red on the default branch. Carson proves the named baseline failure, keeps every other gate intact, merges through the recovery path, and records a machine-readable audit event. |
|
|
30
30
|
| `carson sync` | Fast-forward local `main` from configured remote when tree is clean. |
|
|
31
31
|
| `carson prune` | Remove stale local branches whose upstream refs no longer exist. |
|
|
32
|
-
| `carson housekeep [--json] [--dry-run]` | Attempt to sync the current repo, then reap
|
|
32
|
+
| `carson housekeep [--json] [--dry-run]` | Attempt to sync the current repo, then reap worktrees with strong abandonment evidence, reconcile integrated delivery worktree records from the ledger, and prune stale branches. Safe cleanup still runs when sync is blocked. |
|
|
33
33
|
| `carson template check` | Detect drift between managed templates and host `.github/*` files. |
|
|
34
34
|
| `carson template apply` | Write canonical managed template content into host `.github/*` files. |
|
|
35
|
-
| `carson status [--json]` | Show repository delivery state, including the next queued delivery and blocked-delivery summaries. Default output is Markdown/text; `--json` is the explicit machine contract. |
|
|
35
|
+
| `carson status [--json]` | Show repository delivery state, including the next queued delivery and blocked-delivery summaries. For the current branch, status also reports Carson's last observed PR state and merge proof when the branch has a Carson delivery record. Default output is Markdown/text; `--json` is the explicit machine contract. |
|
|
36
36
|
| `carson abandon <pr_number\|pr_url\|branch> [--json]` | Close abandoned delivery work and clean up its PR, worktree, and branch when safe. |
|
|
37
37
|
| `carson worktree create <name>` | Create an isolated worktree and branch for a new stream of work. |
|
|
38
38
|
| `carson worktree list [--json]` | Show every registered worktree with PR state and Carson's cleanup recommendation. |
|
|
@@ -50,23 +50,50 @@ All batch commands operate across every governed repository registered in `gover
|
|
|
50
50
|
| `carson prune --all` | Remove stale branches across all governed repos. |
|
|
51
51
|
| `carson status --all [--json]` | Portfolio-wide delivery overview per governed repository. |
|
|
52
52
|
| `carson template check --all` | Read-only template drift detection across all governed repos. |
|
|
53
|
-
| `carson housekeep --all [--loop SECONDS]` | Attempt sync, then reap
|
|
53
|
+
| `carson housekeep --all [--loop SECONDS]` | Attempt sync, then reap worktrees with strong abandonment evidence, reconcile integrated delivery worktree records from the ledger, and prune across all governed repos. Safe cleanup still runs when sync is blocked. |
|
|
54
54
|
|
|
55
|
-
`--loop SECONDS` runs the housekeep cycle continuously, sleeping SECONDS between cycles. It requires `--all`, accepts only positive integers, and exits cleanly on `Ctrl-C` with a cycle count summary.
|
|
55
|
+
`--loop SECONDS` runs the housekeep cycle continuously, sleeping SECONDS between cycles. It requires `--all`, accepts only positive integers, and exits cleanly on `Ctrl-C` or `SIGTERM` with a cycle count summary.
|
|
56
56
|
|
|
57
57
|
### Govern commands
|
|
58
58
|
|
|
59
59
|
| Command | Purpose |
|
|
60
60
|
|---|---|
|
|
61
|
-
| `carson govern [--dry-run] [--json] [--loop SECONDS]` | Portfolio-level delivery oversight: assess active deliveries, integrate ready branches, dispatch revisions, and escalate blocked work. |
|
|
61
|
+
| `carson govern [--dry-run] [--json] [--loop SECONDS]` | Portfolio-level delivery oversight: assess active deliveries, integrate ready branches, dispatch revisions, and escalate blocked work. Live integrated rows include merge proof. |
|
|
62
62
|
|
|
63
|
-
`--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.
|
|
63
|
+
`--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` or `SIGTERM` cleanly exits with a cycle count summary. SECONDS must be a positive integer.
|
|
64
64
|
|
|
65
65
|
Governed integration is fixed to `squash`. Non-squash `govern.merge.method` values are rejected by config validation.
|
|
66
66
|
|
|
67
67
|
After a live integration attempt, govern reports the actual outcome. Failed merges stay held at gate instead of being reported as integrated.
|
|
68
68
|
|
|
69
|
-
After CI and review pass, Carson still checks GitHub mergeability. Conflicting PRs
|
|
69
|
+
After CI and review pass, Carson still checks GitHub mergeability. Conflicting PRs exit as `Merge blocked` with an explicit merge-conflict summary. `BEHIND` is treated as freshness failure: Carson blocks and requires a branch refresh before it will continue.
|
|
70
|
+
|
|
71
|
+
In `--json` mode, `deliver` still suppresses human output. Every JSON result now includes `watch_window_seconds`, `waited_seconds`, and `merge_attempted`; integrated exits also include a `merge_proof` object; deferred and blocked exits also include a `handoff` object with `reason`, `expectation`, and ordered `next_steps`.
|
|
72
|
+
|
|
73
|
+
`status --json` extends the `branch` object with:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"pull_request": {
|
|
78
|
+
"number": 299,
|
|
79
|
+
"url": "https://github.com/example/repo/pull/299",
|
|
80
|
+
"state": "MERGED",
|
|
81
|
+
"draft": false,
|
|
82
|
+
"merged_at": "2026-03-16T09:00:00Z",
|
|
83
|
+
"summary": "PR #299 is merged."
|
|
84
|
+
},
|
|
85
|
+
"merge_proof": {
|
|
86
|
+
"applicable": true,
|
|
87
|
+
"proven": true,
|
|
88
|
+
"basis": "content_identical",
|
|
89
|
+
"summary": "proven on main — 6 changed files already match main.",
|
|
90
|
+
"main_branch": "main",
|
|
91
|
+
"changed_files_count": 6
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
On `main`, `branch.merge_proof` is still present with `basis: "not_applicable"`. On non-main branches with no Carson delivery record, `branch.pull_request` and `branch.merge_proof` are `null`. `status --all` remains summary-only in v1 and does not include per-repo merge proof.
|
|
70
97
|
|
|
71
98
|
After a successful govern merge, Carson runs the same cleanup path as `housekeep`: sync, reap safe worktrees, then prune.
|
|
72
99
|
|
data/MANUAL.md
CHANGED
|
@@ -66,12 +66,12 @@ on:
|
|
|
66
66
|
|
|
67
67
|
jobs:
|
|
68
68
|
governance:
|
|
69
|
-
uses: wanghailei/carson/.github/workflows/carson_policy.yml@v3.
|
|
69
|
+
uses: wanghailei/carson/.github/workflows/carson_policy.yml@v3.29.0
|
|
70
70
|
secrets:
|
|
71
71
|
CARSON_READ_TOKEN: ${{ secrets.CARSON_READ_TOKEN }}
|
|
72
72
|
with:
|
|
73
|
-
carson_ref: "v3.
|
|
74
|
-
carson_version: "3.
|
|
73
|
+
carson_ref: "v3.29.0"
|
|
74
|
+
carson_version: "3.29.0"
|
|
75
75
|
rubocop_version: "1.81.0"
|
|
76
76
|
```
|
|
77
77
|
|
|
@@ -157,16 +157,16 @@ On the governed main working tree, Carson blocks raw `git add` / `git commit` an
|
|
|
157
157
|
|
|
158
158
|
**2. Work** — make changes, test them, and either commit normally or let Carson create the delivery commit.
|
|
159
159
|
|
|
160
|
-
**3. Hand the branch to Carson** — `deliver` is the synchronous happy path. Carson pushes the branch, creates or refreshes the PR,
|
|
160
|
+
**3. Hand the branch to Carson** — `deliver` is the synchronous happy path. Before any push, Carson verifies the branch is fresh against the configured remote `main`. If freshness is behind or unknown, delivery stops immediately and no PR is created or refreshed. If freshness is good, Carson pushes the branch, creates or refreshes the PR, watches the delivery for a bounded settle window, merges when the path is clear, syncs local `main`, and then reports merge proof for the delivered branch. If the window expires without integration, Carson exits with an explicit `Merge deferred` or `Merge blocked` handoff that states whether merge was attempted and what to run next. Plain `carson deliver` transports existing commits and blocks if the worktree is dirty. `carson deliver --commit "..."` creates one all-dirty agent-authored commit first, then continues the same delivery flow. Managed template drift is still corrected in a separate Carson-managed commit before push.
|
|
161
161
|
|
|
162
162
|
```bash
|
|
163
163
|
carson deliver
|
|
164
164
|
# or, if the worktree is still dirty:
|
|
165
165
|
carson deliver --commit "fix: describe this delivery"
|
|
166
|
-
# Output: merged into main, or
|
|
166
|
+
# Output: merged into main, or an explicit deferred/blocked handoff
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
-
**4. Inspect or wait when needed** — when `deliver` cannot merge immediately, `status` shows the current branch, the next queued delivery, and
|
|
169
|
+
**4. Inspect or wait when needed** — when `deliver` cannot merge immediately, Carson tells you whether the PR was deferred or blocked, whether merge was attempted, and which command to run next. `status` still shows the current branch, the next queued delivery, and blocked-delivery summaries for the repository. When the current branch has a Carson delivery record, `status` also shows Carson's last observed PR state and merge proof. Keep `govern` running when you want unattended portfolio reassessment and revision dispatch across governed repositories:
|
|
170
170
|
|
|
171
171
|
```bash
|
|
172
172
|
carson status
|
|
@@ -193,7 +193,7 @@ carson worktree list
|
|
|
193
193
|
carson housekeep
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
-
`housekeep` still performs safe reaping and branch pruning when `sync` cannot complete. A blocked sync no longer prevents cleanup work that has its own safety evidence.
|
|
196
|
+
`housekeep` still performs safe reaping and branch pruning when `sync` cannot complete. A blocked sync no longer prevents cleanup work that has its own safety evidence. Absorbed-into-main detection is informational; Carson only auto-reaps when it also has stronger abandonment evidence such as a missing directory, merged PR, or closed abandoned PR.
|
|
197
197
|
|
|
198
198
|
`housekeep` also reconciles integrated delivery worktree records from the ledger. If the recorded worktree is already gone, Carson clears the stale ledger path. If the worktree still points at the integrated head and is safe to remove, Carson reaps it and clears the ledger path in the same pass.
|
|
199
199
|
|
|
@@ -209,11 +209,13 @@ carson abandon feature/stale-work
|
|
|
209
209
|
|
|
210
210
|
`abandon` closes the PR when it is still open, removes the matching worktree when safe, deletes the local and remote branch refs when allowed, and marks the delivery as failed in Carson's ledger.
|
|
211
211
|
|
|
212
|
-
|
|
212
|
+
`abandon` is an intentional discard: committed-but-unpushed branch work does not block abandonment. Typing `carson abandon` is explicit consent to discard committed work on that branch.
|
|
213
|
+
|
|
214
|
+
**Safety guards** — `abandon` blocks when:
|
|
213
215
|
- Shell CWD is inside the worktree (prevents session crash).
|
|
214
|
-
-
|
|
216
|
+
- Worktree has uncommitted changes (prevents accidental loss of unsaved work).
|
|
215
217
|
|
|
216
|
-
|
|
218
|
+
Committed-but-unpushed work is treated as intentional discard — `abandon` proceeds. The `worktree remove` command retains its own unpushed-commit guard and `--force` override for manual cleanup outside the abandon flow.
|
|
217
219
|
|
|
218
220
|
**Stale worktree recovery** — if a worktree directory is destroyed externally (for example by a raw GitHub merge/delete flow), `worktree remove`, `worktree list`, `housekeep`, and `prune` handle the stale entry gracefully: they clean up the git registration and delete the branch without error when Carson has enough evidence. Use Carson's delivery and cleanup commands instead of raw `gh pr merge --delete-branch` so the worktree directory stays intact for orderly cleanup.
|
|
219
221
|
|
|
@@ -299,7 +301,7 @@ carson housekeep --all --loop 300 # housekeep every 5 minutes
|
|
|
299
301
|
|
|
300
302
|
`refresh --all` checks each repo for safety before operating: repos with active worktrees or uncommitted changes are skipped with clear reasons. Other batch commands attempt each repo and report failures without stopping.
|
|
301
303
|
|
|
302
|
-
`housekeep --all --loop SECONDS` runs the full housekeep cycle continuously, sleeping SECONDS between passes. It requires `--all`, accepts only positive integers, and exits cleanly on `Ctrl-C` with a cycle count summary.
|
|
304
|
+
`housekeep --all --loop SECONDS` runs the full housekeep cycle continuously, sleeping SECONDS between passes. It requires `--all`, accepts only positive integers, and exits cleanly on `Ctrl-C` or `SIGTERM` with a cycle count summary.
|
|
303
305
|
|
|
304
306
|
**Periodic maintenance:**
|
|
305
307
|
|
|
@@ -319,15 +321,15 @@ carson govern --loop 300 --dry-run # observe mode, no integration or revision
|
|
|
319
321
|
|
|
320
322
|
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.
|
|
321
323
|
|
|
322
|
-
Each cycle runs independently: if one cycle fails (network error, GitHub API timeout), the error is logged and the next cycle proceeds normally. Press `Ctrl-C` to stop — Carson exits cleanly with a cycle count summary.
|
|
324
|
+
Each cycle runs independently: if one cycle fails (network error, GitHub API timeout), the error is logged and the next cycle proceeds normally. Press `Ctrl-C` or send `SIGTERM` to stop — Carson exits cleanly with a cycle count summary.
|
|
323
325
|
|
|
324
326
|
### Govern and Coding Agents
|
|
325
327
|
|
|
326
328
|
`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.
|
|
327
329
|
|
|
328
|
-
After a live merge attempt, govern reports the actual outcome. Failed merges stay held at gate instead of being reported as integrated.
|
|
330
|
+
After a live merge attempt, govern reports the actual outcome. Failed merges stay held at gate instead of being reported as integrated. Successful integrations also report merge proof for the landed branch.
|
|
329
331
|
|
|
330
|
-
After CI and review pass, Carson still checks GitHub mergeability. Conflicting PRs
|
|
332
|
+
After CI and review pass, Carson still checks GitHub mergeability. Conflicting PRs exit as `Merge blocked` with an explicit merge-conflict summary. `BEHIND` is treated as a freshness failure, not a harmless squash detail: Carson blocks and requires a branch refresh before it will continue.
|
|
331
333
|
|
|
332
334
|
After a successful govern merge, Carson runs the same cleanup path as `carson housekeep`: sync, reap safe worktrees, then prune.
|
|
333
335
|
|
|
@@ -359,6 +361,7 @@ These define what Carson *is*. They are not configurable.
|
|
|
359
361
|
- **Active review** — undisposed reviewer findings block merge; feedback must be acknowledged.
|
|
360
362
|
- **Self-diagnosing output** — every warning and error names what went wrong, why, and what to do next.
|
|
361
363
|
- **Transparent governance** — Carson prepares everything for merge but never makes decisions without telling you.
|
|
364
|
+
- **Structural-edit discipline** — coding agents must not use Python or other blind text-rewrite scripts to edit Carson's Ruby source. Ruby files are edited with scoped patches or Ruby-aware tools so structural `end` boundaries are not truncated by cross-language text munging.
|
|
362
365
|
|
|
363
366
|
### Configurable defaults
|
|
364
367
|
|
data/README.md
CHANGED
|
@@ -26,7 +26,7 @@ Carson lives on your workstation and in CI, never inside the repositories it gov
|
|
|
26
26
|
~/.carson/ ← Carson lives here, never inside your repos
|
|
27
27
|
│
|
|
28
28
|
├─ hooks ──────────────► commit gates and command guards
|
|
29
|
-
├─ worktree flow ──────► create → work → deliver →
|
|
29
|
+
├─ worktree flow ──────► create → work → deliver → housekeep
|
|
30
30
|
└─ portfolio layer ────► status --all | refresh --all | govern
|
|
31
31
|
```
|
|
32
32
|
|
|
@@ -35,6 +35,7 @@ The outsider boundary still matters: Carson governs repositories without becomin
|
|
|
35
35
|
## Principles
|
|
36
36
|
|
|
37
37
|
- **Worktree-first** — substantive work happens in worktrees, not on `main`.
|
|
38
|
+
- **Single landing path** — completed work rejoins shared truth through remote `main` via PR-based delivery.
|
|
38
39
|
- **Carson-owned operations** — Carson owns worktree and delivery operations in governed repositories. Raw `git worktree add/remove`, raw `git pull --rebase`, and raw `gh pr create/merge` are blocked, and `git add` / `git commit` are blocked on the main working tree until you create a Carson worktree.
|
|
39
40
|
- **Self-diagnosing output** — every block should say what happened and the exact next command.
|
|
40
41
|
- **Outsider boundary** — Carson governs repositories without becoming a host-repository runtime dependency.
|
|
@@ -50,18 +51,24 @@ carson onboard your/repo/path
|
|
|
50
51
|
carson worktree create your-worktree
|
|
51
52
|
cd your/repo/path/.claude/worktrees/your-worktree
|
|
52
53
|
|
|
53
|
-
# work and test, then
|
|
54
|
-
|
|
54
|
+
# work and test, then commit and hand the branch to Carson
|
|
55
|
+
git add -A
|
|
56
|
+
git commit -m "fix: describe this delivery"
|
|
57
|
+
carson deliver
|
|
55
58
|
|
|
56
|
-
#
|
|
57
|
-
carson
|
|
59
|
+
# or let Carson create one all-dirty delivery commit:
|
|
60
|
+
# carson deliver --commit "fix: describe this delivery"
|
|
61
|
+
|
|
62
|
+
# once the delivery is integrated, clean up from the repo root
|
|
63
|
+
cd your/repo/path
|
|
64
|
+
carson housekeep
|
|
58
65
|
```
|
|
59
66
|
|
|
60
|
-
`carson deliver`
|
|
67
|
+
`carson deliver` runs Carson-owned branch delivery. Before any push, Carson verifies that the branch is fresh against the configured remote `main`. If freshness is behind or unknown, delivery stops with an explicit block and no PR side effect. Plain `deliver` transports existing commits only; `carson deliver --commit "..."` creates one all-dirty delivery commit first, then continues the same flow. If the branch is fresh, Carson pushes it, creates or refreshes the PR, watches the delivery for a bounded settle window, merges when clear, and syncs local `main`. If the settle window expires without integration, Carson exits with an explicit `Merge deferred` or `Merge blocked` handoff instead of leaving the PR mysteriously open. Deferred and blocked exits say whether Carson attempted merge and list the next commands in order.
|
|
61
68
|
|
|
62
69
|
When one Carson-governed required check is already red on the default branch and the current PR is the repair, use `carson recover --check "..."`. Recovery is the explicit exceptional path: Carson proves the baseline failure, keeps every other gate intact, records an audit event, and never teaches operators to step outside Carson first.
|
|
63
70
|
|
|
64
|
-
`carson worktree list` is the visibility surface for cleanup: it shows every registered worktree, the branch, PR state, whether the content is already on `main`, and Carson's keep or reap recommendation. When work needs to be abandoned instead of landed, use `carson abandon <pr-number|pr-url|branch>` to close the PR and clean up the branch/worktree safely.
|
|
71
|
+
`carson worktree list` is the visibility surface for cleanup: it shows every registered worktree, the branch, PR state, whether the content is already on `main`, and Carson's keep or reap recommendation. Content matching `main` is diagnostic, not standalone proof that a worktree is abandoned. `carson housekeep` is the main cleanup pass: sync main, reap worktrees with strong abandonment evidence (for example a missing directory, merged PR, or closed abandoned PR), and prune stale branches. When work needs to be abandoned instead of landed, use `carson abandon <pr-number|pr-url|branch>` to close the PR and clean up the branch/worktree safely.
|
|
65
72
|
|
|
66
73
|
## Portfolio Layer
|
|
67
74
|
|
|
@@ -73,7 +80,7 @@ carson refresh --all
|
|
|
73
80
|
carson govern --dry-run
|
|
74
81
|
```
|
|
75
82
|
|
|
76
|
-
`carson govern` is the portfolio layer. It
|
|
83
|
+
`carson govern` is the portfolio layer. It assesses active deliveries across governed repositories, integrates ready branches, dispatches revisions for blocked work, and escalates what still needs human judgement. Governed integration is squash-only and happens one repository at a time.
|
|
77
84
|
|
|
78
85
|
## Where to Read Next
|
|
79
86
|
|
data/RELEASE.md
CHANGED
|
@@ -5,6 +5,31 @@ 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.29.0
|
|
9
|
+
|
|
10
|
+
### What changed
|
|
11
|
+
|
|
12
|
+
- **Govern and housekeep loops are clearer and safer to run unattended** — `carson govern --loop` now prints per-delivery progress hints and a sleep announcement with the next-cycle timestamp, and both `govern --loop` and `housekeep --all --loop` now stop cleanly on `SIGTERM` as well as `Ctrl-C`.
|
|
13
|
+
- **Govern and worktree creation no longer mutate the user's main worktree behind their back** — govern now fetches instead of syncing/pruning after merges, worktree creation branches from the remote tracking ref instead of pulling on main, and revision dispatch now defers when the target worktree is busy or dirty.
|
|
14
|
+
- **Abandon guidance now matches the actual safety contract** — `carson abandon` no longer suggests an unsupported `--force`, and committed-but-unpushed branch work no longer blocks abandonment; only dirty worktree changes do.
|
|
15
|
+
- **CI and release workflows are hardened for the Node 24 transition** — GitHub Actions checkouts now use `actions/checkout@v6`, Carson includes a dedicated forced-Node-24 probe for the RubyGems credentials action before changing the release path, and the standalone review smoke script now seeds its temporary `main` branch without tripping the shared main-branch commit guard.
|
|
16
|
+
|
|
17
|
+
### No migration required
|
|
18
|
+
|
|
19
|
+
- Existing workflows continue to work unchanged.
|
|
20
|
+
|
|
21
|
+
## 3.28.0
|
|
22
|
+
|
|
23
|
+
### What changed
|
|
24
|
+
|
|
25
|
+
- **Merge proof is now a first-class delivery surface** — Carson now proves whether a delivered branch's content is already on `main`, even after squash or rewritten history. `carson status` reports the current branch's last observed PR state and merge proof when Carson is tracking that branch, while `carson deliver` and live `carson govern` integrations report the same proof immediately after landing.
|
|
26
|
+
- **Delivery state now preserves PR telemetry and proof for machine consumers** — the JSON ledger stores the last observed PR state, draft flag, merged timestamp, and merge-proof payload so Carson can report stable `pull_request` and `merge_proof` objects without re-querying GitHub on every status read.
|
|
27
|
+
- **Delivery and worktree safety are harder to break** — sync now refuses to proceed from a detached main worktree, worktree lifecycle guards catch more partial-state failures, and govern/deliver reporting stays consistent when mergeability, freshness, and follow-up paths change underneath the branch.
|
|
28
|
+
|
|
29
|
+
### No migration required
|
|
30
|
+
|
|
31
|
+
- Existing workflows continue to work unchanged. Machine consumers may now see additive `merge_proof` and current-branch `pull_request` data in Carson JSON output.
|
|
32
|
+
|
|
8
33
|
## 3.27.1
|
|
9
34
|
|
|
10
35
|
### What changed
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.29.0
|
data/lib/carson/delivery.rb
CHANGED
|
@@ -8,13 +8,16 @@ module Carson
|
|
|
8
8
|
|
|
9
9
|
attr_reader :repo_path, :repository, :branch, :head, :worktree_path, :status,
|
|
10
10
|
:pull_request_number, :pull_request_url, :revisions, :cause, :summary,
|
|
11
|
-
:created_at, :updated_at, :integrated_at, :superseded_at
|
|
11
|
+
:created_at, :updated_at, :integrated_at, :superseded_at,
|
|
12
|
+
:pull_request_state, :pull_request_draft, :pull_request_merged_at, :merge_proof
|
|
12
13
|
|
|
13
14
|
def initialize(
|
|
14
15
|
repo_path:, branch:, head:, worktree_path:, status:,
|
|
15
16
|
pull_request_number:, pull_request_url:, cause:, summary:,
|
|
16
17
|
created_at:, updated_at:, integrated_at:, superseded_at:,
|
|
17
|
-
revisions: [], repository: nil
|
|
18
|
+
revisions: [], repository: nil,
|
|
19
|
+
pull_request_state: nil, pull_request_draft: nil, pull_request_merged_at: nil,
|
|
20
|
+
merge_proof: nil
|
|
18
21
|
)
|
|
19
22
|
@repo_path = repo_path
|
|
20
23
|
@repository = repository
|
|
@@ -31,6 +34,10 @@ module Carson
|
|
|
31
34
|
@updated_at = updated_at
|
|
32
35
|
@integrated_at = integrated_at
|
|
33
36
|
@superseded_at = superseded_at
|
|
37
|
+
@pull_request_state = pull_request_state
|
|
38
|
+
@pull_request_draft = pull_request_draft
|
|
39
|
+
@pull_request_merged_at = pull_request_merged_at
|
|
40
|
+
@merge_proof = merge_proof
|
|
34
41
|
end
|
|
35
42
|
|
|
36
43
|
def key
|
data/lib/carson/ledger.rb
CHANGED
|
@@ -18,7 +18,10 @@ module Carson
|
|
|
18
18
|
attr_reader :path
|
|
19
19
|
|
|
20
20
|
# Creates or refreshes a delivery for the same branch head.
|
|
21
|
-
def upsert_delivery(
|
|
21
|
+
def upsert_delivery(
|
|
22
|
+
repository:, branch_name:, head:, worktree_path:, pr_number:, pr_url:, status:, summary:, cause:,
|
|
23
|
+
pull_request_state: nil, pull_request_draft: nil, pull_request_merged_at: nil, merge_proof: nil
|
|
24
|
+
)
|
|
22
25
|
timestamp = now_utc
|
|
23
26
|
|
|
24
27
|
with_state do |state|
|
|
@@ -45,6 +48,10 @@ module Carson
|
|
|
45
48
|
"status" => status,
|
|
46
49
|
"pr_number" => pr_number,
|
|
47
50
|
"pr_url" => pr_url,
|
|
51
|
+
"pull_request_state" => pull_request_state,
|
|
52
|
+
"pull_request_draft" => pull_request_draft,
|
|
53
|
+
"pull_request_merged_at" => pull_request_merged_at,
|
|
54
|
+
"merge_proof" => serialise_merge_proof( merge_proof: merge_proof ),
|
|
48
55
|
"cause" => cause,
|
|
49
56
|
"summary" => summary,
|
|
50
57
|
"created_at" => created_at,
|
|
@@ -74,6 +81,22 @@ module Carson
|
|
|
74
81
|
build_delivery( key: key, data: data )
|
|
75
82
|
end
|
|
76
83
|
|
|
84
|
+
# Looks up the newest delivery for a branch across active and terminal states.
|
|
85
|
+
def latest_delivery( repo_path:, branch_name: )
|
|
86
|
+
state = load_state
|
|
87
|
+
repo_paths = repo_identity_paths( repo_path: repo_path )
|
|
88
|
+
|
|
89
|
+
candidates = state[ "deliveries" ].select do |_key, data|
|
|
90
|
+
repo_paths.include?( data[ "repo_path" ] ) &&
|
|
91
|
+
data[ "branch_name" ] == branch_name
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
return nil if candidates.empty?
|
|
95
|
+
|
|
96
|
+
key, data = candidates.max_by { |k, d| [ d[ "updated_at" ].to_s, delivery_sequence( data: d ), k ] }
|
|
97
|
+
build_delivery( key: key, data: data )
|
|
98
|
+
end
|
|
99
|
+
|
|
77
100
|
# Lists active deliveries for a repository in creation order.
|
|
78
101
|
def active_deliveries( repo_path: )
|
|
79
102
|
state = load_state
|
|
@@ -106,6 +129,10 @@ module Carson
|
|
|
106
129
|
status: UNSET,
|
|
107
130
|
pr_number: UNSET,
|
|
108
131
|
pr_url: UNSET,
|
|
132
|
+
pull_request_state: UNSET,
|
|
133
|
+
pull_request_draft: UNSET,
|
|
134
|
+
pull_request_merged_at: UNSET,
|
|
135
|
+
merge_proof: UNSET,
|
|
109
136
|
cause: UNSET,
|
|
110
137
|
summary: UNSET,
|
|
111
138
|
worktree_path: UNSET,
|
|
@@ -118,6 +145,10 @@ module Carson
|
|
|
118
145
|
data[ "status" ] = status unless status.equal?( UNSET )
|
|
119
146
|
data[ "pr_number" ] = pr_number unless pr_number.equal?( UNSET )
|
|
120
147
|
data[ "pr_url" ] = pr_url unless pr_url.equal?( UNSET )
|
|
148
|
+
data[ "pull_request_state" ] = pull_request_state unless pull_request_state.equal?( UNSET )
|
|
149
|
+
data[ "pull_request_draft" ] = pull_request_draft unless pull_request_draft.equal?( UNSET )
|
|
150
|
+
data[ "pull_request_merged_at" ] = pull_request_merged_at unless pull_request_merged_at.equal?( UNSET )
|
|
151
|
+
data[ "merge_proof" ] = serialise_merge_proof( merge_proof: merge_proof ) unless merge_proof.equal?( UNSET )
|
|
121
152
|
data[ "cause" ] = cause unless cause.equal?( UNSET )
|
|
122
153
|
data[ "summary" ] = summary unless summary.equal?( UNSET )
|
|
123
154
|
data[ "worktree_path" ] = worktree_path unless worktree_path.equal?( UNSET )
|
|
@@ -214,6 +245,10 @@ module Carson
|
|
|
214
245
|
status: data.fetch( "status" ),
|
|
215
246
|
pull_request_number: data[ "pr_number" ],
|
|
216
247
|
pull_request_url: data[ "pr_url" ],
|
|
248
|
+
pull_request_state: data[ "pull_request_state" ],
|
|
249
|
+
pull_request_draft: data[ "pull_request_draft" ],
|
|
250
|
+
pull_request_merged_at: data[ "pull_request_merged_at" ],
|
|
251
|
+
merge_proof: deserialise_merge_proof( merge_proof: data[ "merge_proof" ] ),
|
|
217
252
|
revisions: revisions,
|
|
218
253
|
cause: data[ "cause" ],
|
|
219
254
|
summary: data[ "summary" ],
|
|
@@ -239,6 +274,11 @@ module Carson
|
|
|
239
274
|
end
|
|
240
275
|
|
|
241
276
|
def migrate_legacy_state_if_needed!
|
|
277
|
+
# Skip lock acquisition entirely when no legacy SQLite file exists.
|
|
278
|
+
# Read-only file checks are safe without the lock; the migration
|
|
279
|
+
# itself is idempotent so a narrow race is harmless.
|
|
280
|
+
return unless state_path_requires_migration?
|
|
281
|
+
|
|
242
282
|
with_state_lock do |lock_file|
|
|
243
283
|
lock_file.flock( File::LOCK_EX )
|
|
244
284
|
source_path = legacy_sqlite_source_path
|
|
@@ -321,6 +361,10 @@ module Carson
|
|
|
321
361
|
"status" => row.fetch( "status" ),
|
|
322
362
|
"pr_number" => row.fetch( "pr_number" ),
|
|
323
363
|
"pr_url" => row.fetch( "pr_url" ),
|
|
364
|
+
"pull_request_state" => nil,
|
|
365
|
+
"pull_request_draft" => nil,
|
|
366
|
+
"pull_request_merged_at" => nil,
|
|
367
|
+
"merge_proof" => nil,
|
|
324
368
|
"cause" => row.fetch( "cause" ),
|
|
325
369
|
"summary" => row.fetch( "summary" ),
|
|
326
370
|
"created_at" => row.fetch( "created_at" ),
|
|
@@ -367,6 +411,7 @@ module Carson
|
|
|
367
411
|
sequence_counts = Hash.new( 0 )
|
|
368
412
|
deliveries.each_value do |data|
|
|
369
413
|
data[ "revisions" ] = Array( data[ "revisions" ] )
|
|
414
|
+
data[ "merge_proof" ] = serialise_merge_proof( merge_proof: data[ "merge_proof" ] ) if data.key?( "merge_proof" )
|
|
370
415
|
sequence = integer_or_nil( value: data[ "sequence" ] )
|
|
371
416
|
sequence_counts[ sequence ] += 1 unless sequence.nil? || sequence <= 0
|
|
372
417
|
end
|
|
@@ -511,6 +556,32 @@ module Carson
|
|
|
511
556
|
nil
|
|
512
557
|
end
|
|
513
558
|
|
|
559
|
+
def serialise_merge_proof( merge_proof: )
|
|
560
|
+
return nil unless merge_proof.is_a?( Hash )
|
|
561
|
+
|
|
562
|
+
{
|
|
563
|
+
"applicable" => merge_proof[ :applicable ].nil? ? merge_proof[ "applicable" ] : merge_proof[ :applicable ],
|
|
564
|
+
"proven" => merge_proof[ :proven ].nil? ? merge_proof[ "proven" ] : merge_proof[ :proven ],
|
|
565
|
+
"basis" => merge_proof[ :basis ] || merge_proof[ "basis" ],
|
|
566
|
+
"summary" => merge_proof[ :summary ] || merge_proof[ "summary" ],
|
|
567
|
+
"main_branch" => merge_proof[ :main_branch ] || merge_proof[ "main_branch" ],
|
|
568
|
+
"changed_files_count" => ( merge_proof[ :changed_files_count ] || merge_proof[ "changed_files_count" ] || 0 ).to_i
|
|
569
|
+
}
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def deserialise_merge_proof( merge_proof: )
|
|
573
|
+
return nil unless merge_proof.is_a?( Hash )
|
|
574
|
+
|
|
575
|
+
{
|
|
576
|
+
applicable: merge_proof[ "applicable" ],
|
|
577
|
+
proven: merge_proof[ "proven" ],
|
|
578
|
+
basis: merge_proof[ "basis" ],
|
|
579
|
+
summary: merge_proof[ "summary" ],
|
|
580
|
+
main_branch: merge_proof[ "main_branch" ],
|
|
581
|
+
changed_files_count: merge_proof.fetch( "changed_files_count", 0 ).to_i
|
|
582
|
+
}
|
|
583
|
+
end
|
|
584
|
+
|
|
514
585
|
def now_utc
|
|
515
586
|
Time.now.utc.iso8601( 6 )
|
|
516
587
|
end
|
|
@@ -44,7 +44,7 @@ module Carson
|
|
|
44
44
|
|
|
45
45
|
if worktree
|
|
46
46
|
remove_exit = with_captured_output do
|
|
47
|
-
worktree_remove!( worktree_path: worktree.path, json_output: false )
|
|
47
|
+
worktree_remove!( worktree_path: worktree.path, skip_unpushed: true, json_output: false )
|
|
48
48
|
end
|
|
49
49
|
unless remove_exit == EXIT_OK
|
|
50
50
|
result[ :error ] = "worktree cleanup failed for #{worktree.path}"
|
|
@@ -131,29 +131,32 @@ module Carson
|
|
|
131
131
|
nil
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
+
# Abandon is an intentional discard: committed-but-unpushed work
|
|
135
|
+
# does not block abandonment. Only uncommitted (dirty) worktree
|
|
136
|
+
# changes block, because those may be accidental.
|
|
134
137
|
def abandon_preflight_issue( branch:, worktree: )
|
|
135
138
|
if config.protected_branches.include?( branch )
|
|
136
139
|
return { exit_code: EXIT_BLOCK, error: "cannot abandon protected branch #{branch}", recovery: "choose a feature branch instead" }
|
|
137
140
|
end
|
|
138
141
|
|
|
139
142
|
if worktree
|
|
140
|
-
check = Worktree.remove_check( path: worktree.path, runtime: self, force: false )
|
|
143
|
+
check = Worktree.remove_check( path: worktree.path, runtime: self, force: false, skip_unpushed: true )
|
|
141
144
|
return nil if check.fetch( :status ) == :ok
|
|
142
145
|
|
|
146
|
+
recovery = check.fetch( :recovery )
|
|
147
|
+
if check.fetch( :error ) == "worktree has uncommitted changes"
|
|
148
|
+
recovery = "commit or discard the changes, then retry carson abandon #{branch}"
|
|
149
|
+
end
|
|
150
|
+
|
|
143
151
|
return {
|
|
144
152
|
exit_code: check.fetch( :exit_code ),
|
|
145
153
|
error: check.fetch( :error ),
|
|
146
|
-
recovery:
|
|
154
|
+
recovery: recovery
|
|
147
155
|
}
|
|
148
156
|
end
|
|
149
157
|
|
|
150
158
|
return { exit_code: EXIT_BLOCK, error: "current branch is #{branch}", recovery: "switch to main or a different branch, then retry" } if current_branch == branch
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
unpushed = Worktree.branch_unpushed_issue( branch: branch, worktree_path: repo_root, runtime: self )
|
|
154
|
-
return nil if unpushed.nil?
|
|
155
|
-
|
|
156
|
-
{ exit_code: EXIT_BLOCK, error: unpushed.fetch( :error ), recovery: unpushed.fetch( :recovery ) }
|
|
159
|
+
nil
|
|
157
160
|
end
|
|
158
161
|
|
|
159
162
|
def close_pull_request!( number:, result: )
|