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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1551ba09ad6dd7d0d2f0341f75e764d41334085b8e27015d7dc4d4e09007a25a
4
- data.tar.gz: 9abc7ea81d920afd602f444c534731e8b5b3cf2b8762da40a9ef411d9d984845
3
+ metadata.gz: 9d026cfc1581b568aaa9459ab21faf0d4f52d1026995b5ce62f21dca0072853a
4
+ data.tar.gz: '08bdaaebab6ccaf1dac6768057f55acf3833ddbe903001b2ca3934551f280bae'
5
5
  SHA512:
6
- metadata.gz: 402d821608dcb4b5a448b3c6a1d9b7cf3de3415b6ab87323e035623f118d0a91e91ce50405bb25d4d608324682eff6ccdddfd7e4c13319b8e339dcc46e699dd0
7
- data.tar.gz: 184128f03321ef5e59c6168a1fcea4c9a223f7d040aaeafa61661de7c0fad77fbd79549d143021c90db25cb7217d53c29eb18da46882744eaf0d85a705604eb8
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@v4
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@v4
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.0"
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, then Carson pushes, creates or refreshes the PR, waits for merge readiness, merges when clear, and syncs local `main`. |
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 dead worktrees, reconcile integrated delivery worktree records from the ledger, and prune stale branches. Safe cleanup still runs when sync is blocked. |
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 dead worktrees, reconcile integrated delivery worktree records from the ledger, and prune across all governed repos. Safe cleanup still runs when sync is blocked. |
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 stay held at gate with an explicit merge-conflict summary, while `BEHIND` PRs remain eligible under Carson's current squash policy.
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.27.1
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.27.1"
74
- carson_version: "3.27.1"
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, waits for the CI and review signals it can observe, merges when the path is clear, and syncs local `main`. 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.
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 held at gate with the next command
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 any blocked-delivery summaries for the repository. Keep `govern` running when you want unattended portfolio reassessment and revision dispatch across governed repositories:
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
- **Safety guards** `worktree remove` blocks when:
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
- - Branch has unpushed commits with content that differs from main (prevents data loss).
216
+ - Worktree has uncommitted changes (prevents accidental loss of unsaved work).
215
217
 
216
- After squash or rebase merge, the content matches main removal proceeds without `--force`.
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 stay held at gate with an explicit merge-conflict summary, while `BEHIND` PRs remain eligible under Carson's current squash policy.
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 → clean up
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 either commit yourself or let Carson create the delivery commit
54
- carson deliver --commit "fix: describe this delivery"
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
- # inspect cleanup recommendations once the work is landed
57
- carson worktree list
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` owns the normal branch-delivery path: it pushes the branch, creates or refreshes the PR, waits for the review and CI gates Carson can observe, merges when clear, and syncs local `main`. If CI or review is still pending after Carson's configured wait windows, `deliver` returns a gated state instead of merging. Use plain `carson deliver` when the branch is already committed. Use `carson deliver --commit "..."` when the worktree is dirty and Carson should create one all-dirty delivery commit first.
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 reassesses active deliveries across governed repositories, dispatches revision work for blocked branches, and surfaces what needs human judgement. Governed integration is squash-only and happens one repository at a time.
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.27.1
1
+ 3.29.0
@@ -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( repository:, branch_name:, head:, worktree_path:, pr_number:, pr_url:, status:, summary:, cause: )
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: check.fetch( :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
- return nil unless local_branch_exists?( branch: branch )
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: )