Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions .github/scripts/test_routing_and_cleanup_surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,18 +154,33 @@ def test_the_skill_carries_the_issue_308_acceptance_gates(self):
rel = f"{plugin}/{routine}/SKILL.md"
with self.subTest(rel=rel):
text = read(rel)
# Ancestry proven against the FETCHED default, not `: gone]` state.
self.assertRegex(text, r"(?i)ancestor of the fetched default")
# #586: containment proven against the FETCHED default, not bare
# `: gone]` state -- and NOT SHA-ancestry alone. This repo
# squash-merges by default, so ancestry fails for every ordinary
# landing; the squash case is proven instead via the merged PR
# record (a MERGED PR whose headRefOid equals local HEAD). Both
# phrases are pinned together: dropping either one -- reverting
# to the old ancestry-only gate, or losing the squash-proof
# instrument -- must fail this test.
self.assertRegex(text, r"(?i)contained\s+in\s+the\s+fetched\s+default")
self.assertRegex(text, r"(?i)PR-record\s+squash\s+proof")
self.assertRegex(text, r"(?i)\bMERGED\b")
self.assertRegex(text, r"(?i)headRefOid\s*==\s*HEAD")
# Three-way artifact classification, with uncertainty biased to unique.
for cls in ("unique", "redundant", "superseded"):
self.assertIn(cls, text.lower())
# Nothing unique discarded without a per-item confirmation.
self.assertRegex(text, r"(?i)without an explicit\s+confirmation naming that item")
# Fast-forward only; no merge commit, no rebase, no reset.
self.assertIn("--ff-only", text)
# Safety-checked delete only, and never the remote branch.
self.assertRegex(text, r"(?i)MUST use `git branch -d`, never `-D`")
self.assertRegex(text, r"(?i)MUST NOT delete a remote branch")
# Safety-checked delete by default. `-D` is forbidden everywhere
# EXCEPT the #586-scoped exception -- this run's PR-record squash
# proof plus a restated, named confirmation -- never as a blanket
# allowance alongside `-d`.
self.assertRegex(text, r"(?i)MUST\s+use\s+`git branch -d`")
self.assertRegex(text, r"(?i)MAY\s+use\s+`-D`\s+ONLY\s+when")
self.assertRegex(text, r"(?i)Everywhere\s+else\s+`-D`\s+is\s+forbidden")
self.assertRegex(text, r"(?i)MUST\s+NOT\s+delete\s+a\s+remote\s+branch")
# The point of the whole exercise: this never needs an override.
self.assertRegex(text, r"(?i)MUST NOT route to `.{0,20}override.{0,20}` when blocked")

Expand Down
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,28 @@ predate the plugin rewrite and are grouped by date.

## [Unreleased]

## [2.11.3] — 2026-08-04

### Fixed

- `post-merge-cleanup` (#586): Phase 1's containment proof no longer relies on
SHA-ancestry alone, which fails for every ordinary squash merge in this
repo's default merge mode. A squash merge is now proven by the merged PR
record (`headRefOid` == HEAD), reported alongside the ancestry check's
negative result as a fact rather than a failure. Phase 3's blocked-checkout
STOP now distinguishes a genuinely conflicting kept artifact from a stale
local default ref. Phase 4 fast-forwards the local default ref before
checking it out, so a stale ref can no longer be misattributed to a kept
artifact. Phase 5 documents why `git branch -d` can already accept a
squash-merged branch via its upstream, and sanctions `git branch -D` only
when this run's Phase 1 proof was the PR-record squash proof and `-d`
refused.
- `standup` (#596): stale worktrees can now be confirmed for removal as one
explicitly enumerated group naming every member, instead of forcing one
confirmation per worktree; declining the group falls back to per-item
confirmation. Branch pruning documents the matching squash-merge `-D`
exception.

## [2.11.2] — 2026-08-04

### Fixed
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ project context. You decide. codeArbiter enforces.
<img alt="Claude Code plugin" src="https://img.shields.io/badge/Claude_Code-plugin-d97757">
<img alt="Codex plugin" src="https://img.shields.io/badge/OpenAI_Codex-plugin-10a37f">
<img alt="Pi Feature Forge preview" src="https://img.shields.io/badge/ca--pi-Feature_Forge_preview-d97757">
<img alt="version 2.11.2" src="https://img.shields.io/badge/version-2.11.2-2b7489">
<img alt="version 2.11.3" src="https://img.shields.io/badge/version-2.11.3-2b7489">
<img alt="commands" src="https://img.shields.io/badge/commands-40-555">
<img alt="skills" src="https://img.shields.io/badge/skills-23-555">
<img alt="agents" src="https://img.shields.io/badge/agents-28-555">
Expand Down Expand Up @@ -119,7 +119,7 @@ Approve the normal plugin trust prompt, open the target repository, and continue

### Codex CLI

The public GitHub-slug flow is **available now**. The repository currently ships `ca-codex 0.4.1`;
The public GitHub-slug flow is **available now**. The repository currently ships `ca-codex 0.4.2`;
the dated end-to-end public-install record discovered `ca-codex 0.2.4` from release `v2.8.13`.
Current packaging and shared-core parity are continuously verified, while that dated live-install
record stays labeled rather than being silently promoted to evidence for a newer adapter:
Expand Down
17 changes: 10 additions & 7 deletions core/surface/commands/cleanup.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
description: Finish an already-merged branch — classify the leftover artifacts, return to a fast-forwarded default checkout, and delete the merged local branch. Every discard confirmed per item; ancestry proven, never assumed.
description: Finish an already-merged branch — classify the leftover artifacts, return to a fast-forwarded default checkout, and delete the merged local branch. Every discard confirmed per item; containment proven, never assumed.
argument-hint: (none)
---

Expand All @@ -20,7 +20,7 @@ coverage. This is that coverage.
## Routes to

The `post-merge-cleanup` skill (`{{PLUGIN_ROOT}}/skills/post-merge-cleanup/SKILL.md`),
which owns the ancestry proof, the artifact classification, and the per-item
which owns the containment proof, the artifact classification, and the per-item
confirmations.

## When NOT to use
Expand All @@ -35,17 +35,20 @@ confirmations.

## Hard gate

- MUST prove the current branch is an ancestor of the **fetched** default branch
before anything is deleted. An unproven or unfetched ancestry STOPs — a branch
that only *looks* merged is not merged.
- MUST prove the current branch is contained in the **fetched** default branch —
SHA-ancestry, or a squash-merge proven by the merged PR record (`headRefOid`
== HEAD) — before anything is deleted. An unproven or unfetched comparison
STOPs — a branch that only *looks* merged is not merged.
- MUST classify every dirty or untracked artifact as **unique**, **redundant**,
or **superseded**, and MUST NOT discard a unique or unclassifiable one without
explicit per-item confirmation naming it.
- MUST confirm each removal individually — no batched yes, no implied yes.
- MUST reach the default branch with a clean working tree, and MUST fast-forward
with `--ff-only` only — never a merge commit, never a rebase, never a reset
that discards work.
- MUST NOT force-delete a branch, force-push, delete a remote branch, or write to
the default branch.
- MUST use `git branch -d`, and MAY use `-D` only when the squash-merge PR-record
proof held this run and `-d` refused, with that proof restated and the branch
named in the confirmation. MUST NOT otherwise force-delete a branch, force-push,
delete a remote branch, or write to the default branch.
- MUST NOT require `{{CMD:override}}`. This is ordinary lifecycle work; if it
cannot proceed, the reason is a stated gate, not a bypass.
28 changes: 23 additions & 5 deletions core/surface/commands/standup.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ Arbiter gathers and proposes; you decide every mutation.
The orchestrator reads the current repo state (reusing the briefing's read-only
computation — branch, ahead/behind, dirty tree, stashes, prune-candidate branches,
stale worktrees) and presents it, then offers each applicable action in turn. Skip
an action that has no candidates; never bundle confirmations.
an action that has no candidates; never bundle confirmations across different
actions or items — branch deletions always stay per-item, and the sole exception
is step 3's explicitly enumerated worktree group, which is still one confirmation
that names every member, never an implied yes.

1. **Fetch + fast-forward pull** — kick `git fetch`, then offer a **`--ff-only`**
pull of the current branch. Eligibility is the briefing summary's
Expand All @@ -28,9 +31,21 @@ an action that has no candidates; never bundle confirmations.
(the `: gone]` upstream set), excluding the current branch and the default
(`main`). Delete a listed branch only after an explicit per-branch confirmation;
declining leaves it in place.

A `: gone]` branch that was squash-merged will typically **refuse** plain
`git branch -d` — its upstream is already pruned, so `-d` has nothing to test
reachability against. Before reporting that as a stop, check for a merged PR
record: `gh pr list --head <branch> --state merged --json headRefOid`. When a
`MERGED` PR's `headRefOid` equals that branch's local tip, containment is
proven and `git branch -D` is permitted — with the proof stated and the
branch named in the confirmation — mirroring the `post-merge-cleanup` Phase 5
contract. Without that proof, a refusal stays a report-and-stop; never guess.
3. **Remove stale worktrees** — list stale/merged worktrees (branch gone-or-merged,
or path missing on disk), never the main worktree. Remove one only after explicit
per-item confirmation; declining leaves it intact.
or path missing on disk), never the main worktree. Present the full stale list
together with each item's evidence, then offer removal as an explicitly
**enumerated group** — one confirmation that names every member — as well as
per-item confirmation for anyone who wants to keep some. Declining the group
falls back to per-item confirmation, and declining any item leaves it intact.
4. **Surface stashes / dirty / un-pushed** — list stashes, uncommitted changes, and
un-pushed commits, each with a suggested next step (`{{CMD:commit}}`, `git push`,
`git stash show`). Report-and-route only: never discard a stash, reset, or push.
Expand Down Expand Up @@ -84,8 +99,11 @@ Present a one-line summary of what was done and what was declined.
commit, never on a dirty tree, never a rebase.
- MUST exclude the current branch and the default branch from branch pruning, and
the main worktree from worktree cleanup.
- MUST confirm each destructive action (branch delete, worktree remove)
individually before performing it — no batched or implied yes.
- MUST confirm branch deletions individually — no batched or implied yes.
Worktree removals MAY be confirmed as one explicitly enumerated group (naming
every member) or individually; either way there is no implied yes, and
declining the group falls back to per-item confirmation rather than removing
anything.
- MUST treat stash / dirty / un-pushed state as report-and-route only — never
discard, reset, force, or push on the user's behalf.
- MUST NOT write to or force-push the default branch.
Expand Down
2 changes: 1 addition & 1 deletion core/surface/skills/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,5 @@ Skill bodies load on routing only. This index is the surface scan; never bulk-re
| [release](release/SKILL.md) | `/release` | Lean SemVer release, three phases: derive the bump from Conventional-Commits since the last tag + roll `CHANGELOG.md`; cut the annotated tag + report; publish (push tag + `gh release create`) on explicit authorization. No tag on a red suite; no direct-to-main/force-push; nothing published unbidden; a release commit routes through `commit-gate`. |
| [security-architecture](security-architecture/SKILL.md) | `/threat-model` (optional) | OPTIONAL lightweight STRIDE pass for sensitive features: attack surface → STRIDE → findings, governed by `security-controls.md`. MAY dispatch `security-reviewer` / `auth-crypto-reviewer`. Not a routine gate; hard-STOPs only on a critical unmitigated threat. |
| [context-check](context-check/SKILL.md) | `/context-check` | OPTIONAL manual drift audit: load `.codearbiter/.provenance/` via `load_provenance_dir` + `compute_drift`, report stale docs, then per stale doc: re-scout / re-baseline (`rebaseline`) / defer. Not the daily loop — commit-gate auto-heal owns routine maintenance. |
| [post-merge-cleanup](post-merge-cleanup/SKILL.md) | `{{CMD:cleanup}}` | The already-merged branch transition: fetch, prove `HEAD` is an ancestor of the fetched default, classify every dirty/untracked artifact as unique/redundant/superseded, resolve each under its own confirmation, `--ff-only` onto the default branch, then `branch -d` the merged local branch. Unique or unclassifiable artifacts are never discarded unbidden; the remote branch is never touched; never routes to `{{CMD:override}}`. |
| [post-merge-cleanup](post-merge-cleanup/SKILL.md) | `{{CMD:cleanup}}` | The already-merged branch transition: fetch, prove `HEAD` is contained in the fetched default (SHA-ancestry, or a squash-merge proven by the merged PR record), classify every dirty/untracked artifact as unique/redundant/superseded, resolve each under its own confirmation, fast-forward the local default ref then `--ff-only` onto it, then `branch -d` the merged local branch. Unique or unclassifiable artifacts are never discarded unbidden; the remote branch is never touched; never routes to `{{CMD:override}}`. |
| [tribunal](tribunal/SKILL.md) | `{{CMD:tribunal}}` | The deep, rarely-convened whole-codebase audit lane: seven gated phases across eleven specialist lenses, each finding persisted to its own file (plus append-only triage/run logs) under `.codearbiter/reports/<run-id>/`, resumable from disk, filing approved findings as GitHub issues; never a required gate. |
Loading
Loading