From a1d6409163185284fb40ecb9c752edd1ead60990 Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 14 Jul 2026 12:32:26 +0200 Subject: [PATCH] feat(conventions): substantive work isolates into git worktrees, merge user-ratified MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce the worktree convention the corpus previously lacked: any substantive work stream targeting main — in every mode, autonomous (/boss) and interactive alike — runs in a git worktree on its own branch (issue-N-), and the branch reaches main only through a user-ratified merge performed by the orchestrator (fast-forward preferred, push covered by the ratification, no pull request). Background sessions were already harness-isolated; this extends the same isolation to the sessions the harness does not guard. The convention depends on worktree.baseRef=head (the default 'fresh' bases branches on origin's routinely-stale tip under the no-autonomous-push discipline), now pinned in conventions.md. boss/SKILL.md gains the run-level mechanics (§ The run worktree): one worktree per run named after the reference issue, entered before the first dispatch; the merge is a user-reserved run-close act; the done-state notify names the branch as the review handle; closes #N fires at merge-and-push time. Iron Law, rollback-sandbox framing, user-reserved-act enumerations, rationalisations, and red flags updated to match. Corollary reconciliations after adversarial review: implement's 'there is no branch' narrowed to 'the loop creates no branch of its own'; specify's auto-signed-commits-on-main heading reworded to ratified-history framing; the sandbox bullets in conventions.md and the CLAUDE.md fragment note the anchor is the run branch's base. The skills meta-repo itself stays exempt (its CLAUDE.md: install symlinks resolve to the primary clone) and direct-on-main. --- boss/SKILL.md | 103 +++++++++++++++++++++++++++++------ docs/conventions.md | 32 ++++++++++- implement/SKILL.md | 4 +- specify/SKILL.md | 4 +- templates/CLAUDE.md.fragment | 19 ++++++- 5 files changed, 138 insertions(+), 24 deletions(-) diff --git a/boss/SKILL.md b/boss/SKILL.md index fd8a714..52f5cdb 100644 --- a/boss/SKILL.md +++ b/boss/SKILL.md @@ -60,19 +60,24 @@ BOUNCE-BACK ONLY ON THE FOUR NAMED TRIGGERS. A NEW CYCLE'S DESIGN RUNS AUTONOMOUSLY VIA THE `brainstorm` SWARM — IT DECIDES THE CONVERGENT (CITED-GROUND) FORKS AND BOUNCES ONLY A DIVERGENT DIRECTION FORK OR A GREENFIELD DESIGN BELOW THE SWARM'S GROUNDING FLOOR. +AUTONOMOUS COMMITS LAND ON THE RUN'S WORKTREE BRANCH — `main` MOVES ONLY VIA + THE USER-RATIFIED MERGE (§ THE RUN WORKTREE). ``` Universal rules (only-orchestrator-commits, main-is-sacrosanct, -language, RED-first for bugs) come from the project's `CLAUDE.md` +worktree isolation for substantive work, language, RED-first for +bugs) come from the project's `CLAUDE.md` — and from the user-level `~/.claude/CLAUDE.md` identity/security layer above it, loaded in every session. Both bind regardless of mode; autonomy does not lift a user-level constraint (e.g. an external-service-consent rule that names `/boss` explicitly). This -skill does not restate them — with one deliberate local refinement: -`main`-is-sacrosanct holds **below the session anchor**, and the -rollback sandbox (§ Direction freedom) is the one narrow `/boss` +skill does not restate them — with two deliberate local refinements: +`main`-is-sacrosanct holds **below the session anchor**, with the +rollback sandbox (§ Direction freedom) as the one narrow `/boss` exception, letting the orchestrator wind back its *own* unpushed -autonomous commits above that anchor. +autonomous commits above that anchor; and the worktree convention is +fixed at run granularity by § The run worktree (one worktree per run, +user-ratified merge). ## The Process @@ -97,6 +102,50 @@ autonomously when the path is clear. If the top item is a real fork (two substantive options with no clear default), bounce back to the user. +### The run worktree (mandatory) + +Before the first dispatch, the orchestrator enters a git worktree +for the run (harness `EnterWorktree`; name it `issue-N-` after +the reference issue, or the run's topic slug while no issue exists +yet — keep the slug short, the harness caps names at 64 chars, and +the name keys the worktree and its branch). A branch opened on the +topic slug keeps its name when the reference issue is created later +in the run — the issue's decision log names the branch; no rename +churn. Every commit the run makes lands on that branch; +the primary clone's `main` checkout is never touched. This is the +project-universal worktree convention +(`../docs/conventions.md` § Git discipline — which also pins +`worktree.baseRef: "head"`, so the branch bases on the local, +possibly-unpushed `main`) applied at run +granularity — one worktree per run, spanning its iterations. A run +that bounces at Step 2, before any dispatch, never opened a worktree +— there is then no branch to report. + +- **Already isolated → don't nest.** A background session the + harness has already placed in a worktree needs no second one; a + resumed run whose worktree still exists re-enters it + (`EnterWorktree` with `path`, per the mid-flight rule in Step 3) + instead of opening a parallel tree. +- **The merge is user-reserved — a run-close act, not a queue + item.** At done-state or bounce-back after work has begun, the + branch stays where it is, unmerged; the notify names it as + awaiting review (§ Done-state notifications). Only after the + user's feedback does the orchestrator integrate: merge into + `main` from the primary clone — fast-forward preferred (rebase + the branch onto `main` first if `main` has moved) — push (the + ratification covers the push), then remove the worktree and + delete the branch (`git worktree remove` from outside the tree; + `ExitWorktree` does not remove a re-entered worktree). No pull + request is opened; the review is the user reading the branch, + human-in-the-loop. The merge joins push, formal milestone close, + and external-service consent in the user-reserved set — and, + being run-close integration, it never makes a queue item + non-doable mid-run. +- **`closes #N` fires at merge-and-push time.** Close-markers in + the run's commits reach the default branch only with the ratified + merge and its push — the reference issue stays open, correctly, + while the work awaits review. + ### Step 3 — Dispatch the appropriate downstream skill Pipeline: @@ -238,7 +287,9 @@ floor). If the working tree is mid-flight (uncommitted changes left over from a previous session): inspect first, then resume the right -step. Do not restart from scratch. +step. Do not restart from scratch. If a previous run's worktree +still exists, resume *inside it* (`EnterWorktree` with its path, +§ The run worktree) rather than starting a parallel tree. ### Step 4 — Loop, deciding on each agent output @@ -268,8 +319,9 @@ For each dispatched skill's result, decide: When everything still open genuinely needs the user without blocking on a decision — an `idea`-labelled item (droppable by definition; its triage is the user's prioritisation call) - or a user-reserved act (push, formal milestone close, - external-service consent) — the run is done; name those + or a user-reserved act (push, the ratified merge of the run's + branch into `main`, formal milestone close, external-service + consent) — the run is done; name those leftovers in the notify. A remaining **design fork** is the one user-reserved residue that is *not* done-state: it matches bounce-back triggers 1/4 and takes the problem-state @@ -317,7 +369,8 @@ The same test is the **done-state discriminator**. An open item is **autonomously-doable** when it has a clear path with no genuine design fork (a bug with an established fix pattern, a mechanical/type edit, a test-specifiable feature) and requires no user-reserved act (no push, no -formal milestone close, no external-service consent). Follow-ups the +ratified merge into `main`, no formal milestone close, no +external-service consent). Follow-ups the run's own closing gate discovered — audit drift items, fieldtest findings, routed inline or filed onto the tracker — re-enter the queue and take the same test; being discovered *after* the primary task does @@ -391,12 +444,14 @@ decide. `main` stays sacrosanct **below the session anchor**. At the start of an autonomous run the orchestrator captures the HEAD commit it starts from — the last user-ratified state — as the **session anchor**. The anchor is -that captured revision (a fixed sha), not the live `main` ref: in the -primary clone on `main` the two coincide; in a git worktree the run sits -on its own branch and the anchor is that branch's base, so a `reset` to -the anchor only ever moves the session branch and cannot reach `main` at -all. Everything the orchestrator then commits autonomously, this run, and -has **not pushed**, is its own sandbox above that anchor. +that captured revision (a fixed sha), not the live `main` ref: in a git +worktree — the normal seat of a `/boss` run (§ The run worktree) — the +run sits on its own branch and the anchor is that branch's base, so a +`reset` to the anchor only ever moves the session branch and cannot +reach `main` at all; in the primary clone on `main` (the exception — +e.g. a repo that forbids worktrees) the two coincide. Everything the +orchestrator then commits autonomously, this run, and has **not +pushed**, is its own sandbox above that anchor. Inside the sandbox, and only there, the forward-only rule is relaxed: when the orchestrator runs itself into a dead end — `implement` stays @@ -411,8 +466,9 @@ hard: autonomous commits. - **Never a pushed commit.** Pushing hands work to the outside; a pushed commit leaves the sandbox and reverts to forward-only (`git revert`), - never `reset`. In practice the orchestrator does not push autonomously, - so the sandbox is local. + never `reset`. In practice the orchestrator neither pushes autonomously + nor merges the run's branch into `main` (§ The run worktree), so the + sandbox is local. - **Hard-drop, no parking.** A discarded attempt is gone — no `attempt-N` branch or tag is kept. The reference-issue thread is its trace; the transcript holds the rest. (The user's standing preference: @@ -543,6 +599,13 @@ watch the orchestrator work**: identifiers, type names, agent names, or iteration codes (`ct.1.7`, `18a`, etc.). Describe the *change in the project* — what is now possible, fixed, or visible — not the mechanics. +- **Name the review handle.** When the run worked in its worktree + (§ The run worktree), its work sits unmerged on the branch; the + notify says so and names the branch. The no-internals rule bans + identifiers the user cannot act on; the branch is the one + identifier that exists *for* the user's action (review, then the + ratified merge), so it is carried. A run that bounced before + entering a worktree has no branch and names none. - **No assumption of context.** The user is on their phone and has not seen this session. Lead with the result; the *why* is fine if it fits in one short clause. @@ -740,6 +803,7 @@ the user wants it — there is no autonomous filing to govern. | "Iteration just landed cleanly, let me notify so user can see" | Mid-flow notifications burn user attention without giving them a decision to make. Iteration-clean is internal progress, not a notify event. | | "Sub-goal of the cycle is done, that counts as done-state" | Done-state is queue-empty-of-autonomously-doable, not sub-goal-complete. If the next iteration is obvious and substantive, keep dispatching. | | "Primary task done, the audit/fieldtest findings are filed as issues — that's a wrap, notify done-state" | Filing the findings *repopulated the queue* — and findings routed inline instead (a `bug` → `debug`, a drift item → tidy iteration) are queue work all the same. Each follow-up takes the direction-freedom test like any other item: a clear, fork-free bug or mechanical edit continues the loop — being discovered after the primary task is not a stopping reason. Only a residue of `idea` items and user-reserved acts is a legitimate done-state; a remaining fork bounces instead. | +| "The picked item is tiny — skip the worktree, work on `main` directly" | The worktree is per-run, not per-item, and it is entered before the first dispatch — its cost is one `EnterWorktree`, once. Item size never decides where autonomous commits land; `main` moves only via the user-ratified merge (§ The run worktree). | | "Top of the backlog has two equally good options, I'll just pick one" | If both are substantive and the choice is non-obvious, that's a real fork — bounce back. The cost of a wrong pick (working-tree discard) is higher than the cost of one short ping to the user. | | "User said 'until X then notify', X just completed, notify now" | Check whether X was actually the last actionable thing. If hitting X leaves an open cycle with obvious follow-up, the user's intent was "wake me when there's nothing else to do" — continue. | | "Notify can name the iteration code — user will figure out what it means" | No. The notify text is for the user-as-reader who did not watch the orchestrator work. Iteration codes, crate names, function identifiers, type names — all internal, all banned. | @@ -766,6 +830,8 @@ the user wants it — there is no autonomous filing to govern. - About to send a notify on an internal-progress event (test suite landed, audit clean before close-fixes, iter N done with N+1 in scope). - About to declare done-state while the queue holds a follow-up that passes the direction-freedom test — including one this run's own closing gate just surfaced. Primary-task-complete is a sub-goal; "discovered after the primary task" and "outside the originally discussed tickets" are not stopping reasons. (The inverse holds too: a queue of only `idea` items and user-reserved acts IS done-state — do not grind through user-reserved work to avoid the notify; a remaining design fork bounces as problem-state instead.) - About to dispatch the next skill without checking that the previous output was actually good. +- About to dispatch substantive work while sitting on the primary clone's `main` checkout — the run's work belongs in its worktree (§ The run worktree); enter it before the first dispatch. +- About to merge the run's worktree branch into `main` — or push it — without the user's review; the merge is human-in-the-loop, never autonomous, and no pull request is opened. - About to autonomously dispatch in a session where the user did NOT type `/boss`. - About to send a notify that names a crate, an iteration code, or an agent. - About to bounce back at every iteration boundary "just to be safe" — that's reactive deference, not direction freedom. @@ -801,6 +867,9 @@ the user wants it — there is no autonomous filing to govern. - **Issue-writing conventions:** `../issue/SKILL.md` — used both for the run's reference issue on the *project* tracker and for skill-system feedback filed on the *plugin's own* tracker (§ Skill-system feedback). +- **Worktree convention:** `../docs/conventions.md` § Git discipline — + substantive work in a git worktree, user-ratified merge, no pull + request; § The run worktree fixes the run-level mechanics. - **Glossary write-rule:** `../docs/glossary-convention.md` — record-reality discipline for the only autonomous glossary writer. - **Spec auto-sign gate:** owned by `../specify` Step 6 — under `/boss` diff --git a/docs/conventions.md b/docs/conventions.md index 14905bb..f4674ae 100644 --- a/docs/conventions.md +++ b/docs/conventions.md @@ -139,12 +139,40 @@ too. recovery is forward-editing or `BLOCKED` (`implement/SKILL.md` Iron Law, issue #23). **One narrow `/boss` exception — the rollback sandbox:** inside an - autonomous run the orchestrator anchors on the `main` HEAD it started - from (the last user-ratified state) and MAY `git reset --hard` its + autonomous run the orchestrator anchors on the HEAD it started + from (the last user-ratified state; under the worktree bullet below + that is the run branch's base, so a reset moves only the branch and + can never reach `main`) and MAY `git reset --hard` its *own* commits *above* that anchor — autonomous, **unpushed** work only — back toward (never below) the anchor when it has run into a dead end. A pushed commit has left the sandbox and is forward-only (`git revert`). See `../boss/SKILL.md` § Direction freedom. +- **Substantive work runs in a git worktree — merge is human-in-the-loop.** + Any substantive work stream targeting `main` — a feature, a bug fix, a + refactor; anything beyond a trivial mechanical edit — is isolated into + a git worktree on its own branch before work starts, in every mode: + autonomous (`/boss`, see `../boss/SKILL.md` § The run worktree) and + interactive alike. (Background sessions are worktree-isolated by the + harness already; this convention extends the same isolation to the + sessions the harness does not guard.) The worktree branches from the + current **local** HEAD — pin `worktree.baseRef: "head"` in settings; + the harness default `fresh` bases the branch on origin's tip, which + is routinely stale under this convention's no-autonomous-push + discipline. Name the branch after the + tracker issue where one exists (`issue-N-`); a live worktree + branch is then also the visible being-worked-on signal in + `git worktree list`. The branch reaches `main` only through a + **user-ratified merge**: the user reviews the result, then the + orchestrator merges — fast-forward preferred (rebase the branch onto + `main` first if `main` has moved) — pushes (the ratification covers + the push), and removes the worktree and + deletes the branch. No pull request is opened, and the merge is never + autonomous. `closes #N` markers consequently fire at merge-and-push + time, when the commits reach the default branch. + **One exception:** the skills meta-repo itself forbids worktree + development outright (its `CLAUDE.md`: the install symlinks resolve to + the primary clone, so worktree edits are silently ineffective) and + stays direct-on-`main`. These also appear in the universal-discipline fragment (`../templates/CLAUDE.md.fragment`) that each project's CLAUDE.md diff --git a/implement/SKILL.md b/implement/SKILL.md index 4b0c2a7..24a6879 100644 --- a/implement/SKILL.md +++ b/implement/SKILL.md @@ -219,7 +219,9 @@ Workflow({ name: "implement-loop", args: { The carrier fields are defined authoritatively at the top of `workflows/implement-loop.js`. The load-bearing `iter_id` rule is there too: it names the scratch dir and the stats filename, NOT a branch -(there is no branch). +(the loop creates no branch of its own; it runs in whatever checkout +the session sits in — under the worktree convention, the run's +worktree branch). ### Step 2 — Read the end-report diff --git a/specify/SKILL.md b/specify/SKILL.md index 8c5e376..9868729 100644 --- a/specify/SKILL.md +++ b/specify/SKILL.md @@ -443,8 +443,8 @@ leaning. This is a *tool it reaches for*, not a gate it must clear: the lens returns a finding, the orchestrator decides. Carrier and lenses are defined in `agents/spec-skeptic.md`. -**Auto-signed CODE commits on `main` are forward-only — with one `/boss` -exception.** The spec itself is never a commit (it is a git-ignored +**Auto-signed CODE commits are forward-only once ratified — with one +`/boss` exception.** The spec itself is never a commit (it is a git-ignored working file), so a later user veto is a forward correction: shell-`rm` (or edit) the git-ignored spec, and `git revert` / `git checkout` only the downstream **code** it produced. The `/boss` rollback sandbox is the diff --git a/templates/CLAUDE.md.fragment b/templates/CLAUDE.md.fragment index 35069b4..bf57c88 100644 --- a/templates/CLAUDE.md.fragment +++ b/templates/CLAUDE.md.fragment @@ -44,7 +44,7 @@ discipline the agents are designed to enforce. ## Commit discipline and main-branch sanctity -Two rules govern who touches git history and how: +Three rules govern who touches git history and how: - **Only the orchestrator commits.** No skill agent runs `git commit`. Every agent writes its output into the working @@ -63,7 +63,22 @@ Two rules govern who touches git history and how: autonomous commits — those above the HEAD the run started from (the session anchor) — back toward, never below, that anchor, when I have run into a dead end. A pushed commit has left the - sandbox and is forward-only (`git revert`). + sandbox and is forward-only (`git revert`). (Under the worktree + rule below, the anchor is the run branch's base — a reset moves + the branch, never `main`.) +- **Substantive work runs in a git worktree.** Before starting any + substantive work stream — a feature, a bug fix, a refactor; + anything beyond a trivial mechanical edit — I enter a git + worktree on its own branch (harness `EnterWorktree`, or + `git worktree add`), based on the current local HEAD + (`worktree.baseRef: "head"`) and named after the tracker issue + where one exists (`issue-N-`). The branch reaches `main` only after + the user has reviewed the result: they ratify, I merge — + fast-forward preferred, no pull request — then remove the + worktree and delete the branch. Background sessions are + worktree-isolated by the harness already; this extends the same + isolation to interactive and autonomous sessions (see + `~/dev/skills/docs/conventions.md` § Git discipline). ## Design rationale ≠ implementation effort