Execute a phased implementation plan from docs/plans/*.md. Pick the next phase, implement it, open a PR, watch CI and review, and merge when the gates pass. Use when asked to run, continue, resume, or drive the next phase of a plan, to check the status of a plan or its open phase PRs, or to merge, pause, skip, or re-plan a phase. Also use when a PR webhook or a check-in wakes the session and a plan file is present.
Scanned 9/5/2026
Install to Claude Code
npx -y skills add sixoverground/claude-phases --skill phase-driver --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Phase Driver?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/sixoverground-phase-driver)More formats (shields.io, HTML) on the badges page.
---
name: phase-driver
description: Execute a phased implementation plan from docs/plans/*.md. Pick the next phase, implement it, open a PR, watch CI and review, and merge when the gates pass. Use when asked to run, continue, resume, or drive the next phase of a plan, to check the status of a plan or its open phase PRs, or to merge, pause, skip, or re-plan a phase. Also use when a PR webhook or a check-in wakes the session and a plan file is present.
---
# Phase driver
You execute a phased plan. The plan file is your only durable memory: the session running you can be reclaimed at any moment, and the next one starts knowing nothing. Write state down before you act on it, and everything survives.
Read `references/recovery.md`, `references/gates.md`, and `references/vocabulary.md` when you reach the steps that need them, not up front.
## Golden rules
1. **Never modify the plan file from a phase branch.** Plan writes go to `home_repo`'s **plan branch**, always.
2. **Write status before the action it describes**, not after. Claim before branching; record the PR before waiting on it.
3. **Pass the `sha` you just read** on every plan write. A stale sha means another driver moved: re-read, re-check whose claim it is now, and don't overwrite.
4. **Every plan commit message ends `[skip ci]`.**
5. **One phase = one PR = one repo.**
6. **Re-read the head SHA immediately before anything irreversible** and re-check the gate against it.
7. **If your context was compacted or you don't remember starting this: re-read this file and the plan, run Reconcile, and continue.** That is the normal path, not an error.
8. **Never claim work you didn't do.** If tests didn't run, say so in the PR body. If you can't tell why CI failed, say that and link the run.
## 1. Find the plan
Look for `docs/plans/*.md` in the current repo. More than one, and no phase named: list them and ask.
Parse the front matter under `phases:`. That is your entire configuration. Resolve per-repo settings by deep-merging each repo's entry over `defaults`. Never look for configuration anywhere else; there is no other config file.
**The plan branch** is `plan_branch`, or the home repo's default branch when that key is absent. Every read and every write of the plan file uses it. It is usually the default branch; on feature-branch work. Where phases target `feature/x` and nothing reaches `main` until the feature is whole. The plan lives there instead, so that status is visible without merging a phase PR. If `plan_branch` names a branch that doesn't exist, stop and say so. Never fall back to the default branch: a plan read from the wrong branch reports every phase as unstarted, and the driver would start them all a second time.
## 2. Reconcile, always, before anything
Run this on every invocation and every wake, including when you think you know the state. It is cheap and it is what makes crash recovery work.
1. Read the plan: front matter, the PR Sequence Table, the Driver State block.
2. Check the **Driver-ID** rules below.
3. For **every** non-terminal row, ask GitHub what's actually true: `list_pull_requests` filtered by that row's head branch in that row's repo, then `pull_request_read{get}` for any match.
4. Apply `references/recovery.md` per row. Where the plan and GitHub disagree, **GitHub is the truth**, fix the plan, never assume.
5. Rewrite `Heartbeat` and, if you took over, `Driver-ID`.
### Driver-ID
| Condition | Action |
|---|---|
| `Driver-ID` matches the one you hold | It's you. Proceed regardless of heartbeat age |
| Different ID, `Heartbeat` under 90 minutes old | Another driver is live. Report and stop; do not touch the plan |
| Different ID, `Heartbeat` stale | Take over: mint a new random ID, write it, continue |
| No ID, or `-` | Unclaimed. Claim it |
You hold an ID only if you minted it in this session. After compaction you may have forgotten; if you can't establish that you hold it, treat it as someone else's.
## 3. Start phases
Startable rows are `Pending`, with every id in `Depends` now `Merged` or `Skipped` (blank `Depends` means the row above), and **no other open phase PR in that row's repo**. Start every startable row. Repos run in parallel, and only declared dependencies serialize anything. Honour `max_concurrent` if set.
For each, in order:
1. **Ensure the repo is reachable.** If it isn't in the session's scope, add it and clone it. If access is denied, mark that row `Blocked` with the error and carry on with the others. One inaccessible repo must not stall the rest.
2. **Commit `In Progress`** to the plan branch. This is the claim, and it comes first.
3. **Branch** from that repo's `target_branch`, named from the row's `Branch`.
4. **Implement** the phase against its Phase Details: scope, acceptance criteria, risks. Match the surrounding code. Stay inside the phase's scope: if you find unrelated problems, note them in the PR body rather than fixing them.
5. **Verify** per that repo's `verify`:
- `local`. Run the tests and build. They must pass before you push.
- `ci`. Don't attempt a local build.
- `auto`. Try local; if the toolchain isn't there, fall back to CI.
- `none`. Skip verification.
6. **Push and open the PR**, titled `Phase N: <scope>`. Body: the acceptance criteria as a checklist, **which verification path actually ran**, the UAT checklist per the rules below, anything out of scope you noticed, and `Driven by phase-driver, do not edit the plan file in this PR`.
7. **Commit `In Review`** and the `Link`, immediately, before subscribing, before any wait. A PR that exists but isn't recorded is the most expensive state to recover from.
8. **Read back who opened it**, from the PR's own `user.login`. See below.
### Who opens the PR decides whether it gets reviewed
Some reviewer integrations attach to an account. Codex reviews pull requests opened by the user who connected it and ignores everyone else's. So for those, the identity that *opens* the PR is a functional part of the merge gate, not a cosmetic detail: open a phase PR as a bot or app identity and no review is ever requested, gate 6 waits on proof that was never coming, and YOLO stops with nothing red to look at.
**Not every reviewer works that way**, and saying so matters because the wrong diagnosis here recommends closing a healthy PR. A review workflow triggered by `pull_request` reviews whatever lands in the repo whoever opened it, and a `check:` entry has no connected user to compare against at all. The opener matters for a `logins` entry backed by an account-scoped integration; elsewhere an unexpected opener is worth a sentence, not an alarm.
**Open the PR with the GitHub MCP tools, not `gh pr create`.** This is the whole rule, and it is not a style preference. In a cloud session `gh` is authenticated as the integration rather than as you, so `gh pr create` opens the PR as that app identity; the MCP tools act with the user's own authorization and open it as them. Both succeed, both print a PR URL, and only one of them gets reviewed. Push with git as usual; it is the PR-opening call that decides the author.
This is about the opener only. Commit authorship and `Co-Authored-By` trailers change nothing here, and no trailer will make a reviewer look at a PR opened by an account it doesn't watch.
**Then check, against the account your GitHub tools authenticate as**, which is `get_me`, read once per session. That is the identity the MCP tools open PRs with, so a PR whose `user.login` differs was opened by some other path, which is the failure this catches. Nothing needs to be configured for this: the comparison is between two things you can read, and a plan carries no field naming the expected opener.
If they differ, say so in the phase report and on the PR before waiting on anything. Name the login that opened it, and name the affected `review.required` entries, the account-scoped ones, rather than asserting that review is broken in general. The fix is usually to close it and reopen from the right account, which is cheap now and expensive after five gate cycles.
Do not let this become a silent wait. A reviewer that was never asked looks exactly like a reviewer that is slow.
### UAT checklists
UAT is what a **human** does by hand. It is not the acceptance criteria, which you verified yourself. Never merge the two, and never tick a UAT box. You are not the one who performs it.
Start from the phase's `**UAT.**` items in Phase Details, then refine against what you actually built: you may have implemented it differently, and you know what's adjacent enough to have broken. Write steps someone can follow without reading the diff. What to open, what to do, what they should see. If a phase has no UAT section, write one.
Which checklist goes where depends on YOLO, because YOLO decides whether anyone is stopping to look:
- **YOLO off.** This PR carries its own phase's UAT, **plus** everything in `UAT-pending`, assembled into one script by the rules below. Then clear those ids. Someone is looking at this PR; give them everything nobody has verified yet.
- **YOLO on.** Omit UAT from the PR and append this phase's id to `UAT-pending` when it merges. The cumulative script. Everything in `UAT-pending`, plus the end-to-end flows that only make sense once every phase has landed. Goes on the **integration PR** at the end of the plan (§8), or on the **final phase**'s PR if no integration PR will exist because phases target the default branch directly. Clear the list when it lands somewhere.
Read the YOLO state **at the moment you open the PR**, not from memory. Toggling mid-plan is expected and `UAT-pending` is what makes it safe: turn YOLO off after three unattended merges and the next PR carries those three alongside its own.
§8 covers what happens if the plan finishes and the list never landed anywhere.
Where a repo sets `uat: false`, skip everything in this section, including the assembly rules below. Under YOLO an integration PR still opens at the end, it is worth having regardless, just without a checklist.
### Assembling more than one phase's UAT
**Write a test script, not a changelog with checkboxes.** Whoever runs it wants to sit down once and verify the feature; they do not care which phase produced which step, and phase numbers are an artifact of how the work was built rather than of how it is tested.
So when a PR carries more than one phase's worth:
1. **One list.** No per-phase sections. Group by theme or by user journey only where that genuinely helps someone execute it. Setup, then connect, then edit, then disconnect. If the feature is small, no headings at all.
2. **Ordered so it can be run top to bottom.** Anything that blocks everything else goes first, accounts, domains, migrations, env vars, and steps that depend on earlier state follow it. A script that has to be re-read to find a runnable order is a list, not a script.
3. **Merge duplicates.** Adjacent phases routinely specify the same click. One step, once.
4. **Reconcile contradictions, and say so in the step itself.** A later phase can invalidate an earlier phase's step, and grouping by phase hides that because both survive under their own heading. If phase 2 says *confirm the event comes back* and phase 4 changed that to *confirm it stays gone*, the script gets **one** step describing what the code does now, with a trailing note in that same step naming what it supersedes. *"This reverses what phase 2 specified; phase 4 changed a remote delete to unlink"*. The note belongs there and nowhere else: whoever runs the script may have read the plan, and without it a correct step looks like a bug they have just found. Fix the stale line in the plan's Phase Details too. A checklist that disagrees with the code is as harmful as a comment that does, and worse, because a human executes it and believes it.
5. **Say what has not been verified.** If nothing has been exercised against a live third party or a real device, put that at the top in a sentence. It is the single most useful line in the document, and it is invisible from the checkboxes alone.
Where each step came from stops mattering the moment the plan is done. What matters is whether someone can run the thing end to end and know the feature works.
## 4. Watch
Subscribe to PR activity for each open phase PR. Schedule a check-in 45–60 minutes out if you can. Webhooks don't reliably deliver CI success or new pushes, and a missed event otherwise stalls the plan silently.
Message the user what you started and where. Then **end your turn.** Sleeping is correct; there is nothing to poll.
## 5. On every wake
Reconcile first. Then handle what woke you, against the row it belongs to:
- **A check failed.** Read `references/gates.md` for how to get the failure text for that repo's `ci.logs` setting. Fix it and push. One re-run per head commit is allowed for a suspected flake, and say in the PR that you re-ran it. Beyond that, treat it as a real failure.
- **A review comment or thread.** See [Answering a review](#answering-a-review) below. Never resolve a thread you disagree with on the merits: reply explaining why, and leave it open for a human.
- **A merge conflict.** Update the branch from base; if that fails, resolve it in the working tree and push. After two failed attempts, mark `Blocked`.
- **The head moved.** Discard that row's gate result and re-evaluate from scratch. A gate result is only ever valid for the commit it was computed against.
- **The PR was merged by anyone.** Go to step 7. This is expected, not an anomaly.
- **A message from the user**, see `references/vocabulary.md`.
- **Nothing actionable**, re-arm the check-in and end the turn.
### Answering a review
A review round costs a push, a CI run, and another review. Observed on a real phase: four rounds, then twenty-eight more after the user approved continuing, on a PR that was never going to converge because each round was answering comments one at a time and taking every one of them as an order.
**Fix the whole shape, not the flagged line.** A reviewer comments where it happened to look. Before you change anything, read the surrounding code and ask what the finding actually is: the same mistake is usually in three other places in the same diff, and fixing only the line that was pointed at guarantees the next round finds its siblings. Fix the general case once, then say in the reply that you applied it everywhere rather than only where it was raised. This is the difference between one round and five.
**Answer every open thread in one push.** Read all of them first, work out which are the same underlying finding, then push once. A push per comment is a review round per comment.
**A review comment is evidence, not an order.** Judge each one against this phase's scope, exactly as you would a problem you noticed yourself:
- **In scope, and right.** Fix it, generally, per the rule above. Then reply saying what you changed and **resolve the thread**. Gate 5 wants every thread resolved, so a fix pushed without resolving leaves the phase blocked on a thread that no longer says anything true.
- **In scope, and wrong.** Reply with your reasoning and leave the thread open. This is a disagreement on the merits and a human settles it.
- **Out of scope.** Do not implement it. This is the same rule as §3: a phase does what its Phase Details say and notes the rest. Record it, reply naming why it falls outside this phase and where it went, and resolve the thread.
Out of scope is not the same as unimportant, so where it gets recorded depends on what it is:
| The finding | Goes to |
|---|---|
| Work that makes sense **for this plan**, just not for this phase | **Carried findings** in the plan file |
| A real defect that stands on its own, unrelated to what the plan delivers | A GitHub issue in that repo |
| A remark about this PR that changes nothing | The reply, and nothing else |
**Carried findings** is a section of the plan file, described in the plan format. Append one line saying what it is, the PR and thread that raised it, and why it was out of scope. Same write rules as any plan write: plan branch, the `sha` you just read, `[skip ci]`.
A plan starts without that section, so the first finding creates it, immediately **after Driver State**. Look for an existing one before you write: two `## Carried findings` headings in a file split the list in half and make the end-of-plan readout miss whichever one it doesn't reach.
**Append to it; never add a phase.** Recording is execution and belongs to you. Deciding that a finding is worth a phase means writing scope, acceptance criteria, UAT and dependencies for it, which is planning: it belongs to `phase-planner` and to the person whose plan it is. Tell the user what you recorded and let them ask for a phase. A driver that adds phases because a reviewer suggested something is re-planning a project mid-flight on the say-so of a tool that has read one diff.
Scope-declining resolves the thread, and disagreement does not. That difference is deliberate. An out-of-scope finding is not a claim that the PR is wrong, so holding gate 5 open for it would stall a phase over work that was never in it, while a thread that says the code is broken should block until a person agrees it isn't.
**Widening a phase because a reviewer suggested it is still widening it.** The scope came from a plan someone agreed to; a comment is a suggestion from a tool that has read this diff and nothing else.
#### Stop when the rounds stop converging
**Count the review rounds on this PR, and stop at `stuck.max_review_rounds`** (default `3`, `null` to disable).
**Count reviewed heads, not reviews.** A round is one head commit that a required reviewer evaluated, so the count is the number of distinct shas on this PR for which **any** `review.required` entry is satisfied by gate 6's own test: a review or inline comment at that sha by one of its `logins`, a check run named by its `check` at that sha passing the entry's `proof`, or a 👍 that belongs to that head.
**A reaction belongs to exactly one head.** It carries no sha, so bound it: a 👍 belongs to the head that was current when it was created, at or after that head's push and before the next head's push. Gate 6 asks only about the current head, where "at or after this head was pushed" is the whole test. Counting heads asks about all of them, and the same clause unbounded makes one reaction on the third head satisfy the first two as well, producing a count of three from a single 👍 and stopping the phase before one finding has been answered.
Counting submissions instead breaks in both directions. Three required reviewers all reviewing the opening head would exhaust a budget of three before the driver had answered anything once, though answering all three costs a single push and is plainly one round. And a `check:` entry posts no review at all, so a submission count sits at zero forever for managed Claude review and for review workflows, and the stop never fires for the setups most able to loop.
**Derive the count; never remember it.** Read it from the PR on the wake you are in. A session that keeps the tally in its head loses it to compaction and then sees round one again, which is how a PR reaches twenty-eight rounds with nobody able to say it had.
`stuck.max_cycles` does not cover this. That one counts a gate failing with an **unmoved head**, and a review round moves the head every time by construction, so this loop reads as progress on every pass.
At the threshold, **do not push again.** Mark the row `Blocked`, record the round count in `Note`, and post one comment on the PR containing:
1. What you changed in each round so far, one line each.
2. What is still flagged on the current head.
3. **Whether the newest findings are new ground or the same class resurfacing in new places.**
Line 3 is the one that earns the stop. "Round four, and the last three findings are the same retry-logic issue in three more files" says the phase needs reworking rather than another patch. "Round four, three unrelated real bugs" says carry on. Same count, opposite answers, and only the report tells them apart.
**Do not ask "shall I continue?"** A bare question gets a yes, because nothing in it gives anyone grounds to say no. That is the observed failure: a stop at four rounds was approved and ran twenty-eight more. Report the evidence and let the answer follow from it.
The user resumes with `continue`. Starting the budget again means **writing down where it restarted**: add this phase's current reviewed-head count to `Review-baselines` in Driver State, as `<phase id>:<count>`, and subtract that phase's baseline from every later count.
Without the baseline the stop is a trap rather than a checkpoint. The count is derived, so the next wake re-derives a number still at or above the threshold, blocks the row again, and no amount of saying `continue` moves it. The baseline is the one piece of this that has to be written down, for the same reason everything else in the plan file is: the session that agreed to continue will not be the session that carries on.
**It is keyed by phase, and it is not the `Note`.** Phases run concurrently across repos, so two rows can be stopped at once, and an unqualified baseline would be overwritten by whichever row was continued last, then subtracted from the wrong phase. `Note` is one plan-wide free-text field and cannot hold per-phase state that anything reads back. `Review-baselines` sits beside `UAT-pending`, which is per-phase runtime state for the same reason.
When a PR has no outstanding work, evaluate `references/gates.md` against it, using that repo's resolved config. Then:
- **YOLO on.** Re-read the head SHA as your *last* read before merging, with no other calls in between, and merge with that repo's `merge.method`. Then **delete the phase branch**, unless the repo sets `merge.delete_branch: false`.
- **YOLO off.** Do not merge. Tell the user it's ready, with the PR link and a per-gate summary, and set the row's `Note` to `ready to merge`. Re-ping only if the head moves and it passes again. Don't nag on a timer.
**Deleting the branch is yours to do because the API will not.** GitHub's merge endpoint has no equivalent of the UI's "delete branch after merge" checkbox, so a driver that merges and stops leaves a branch behind for every phase it shipped, while phases a human merged get cleaned up. Nobody notices until the branch list is half stale and it is no longer obvious which of them are finished.
Delete only a branch **you** created: it must match that repo's `branch_prefix`, and it must not be the plan branch or any repo's `target_branch`. Those three checks are the whole guard, and skipping them is how a driver deletes the branch its own plan lives on.
A failed delete is not a failed merge. Branch protection or a ruleset can forbid it, and so can the environment the session runs in: a sandboxed one may permit pushes and PR operations while refusing ref deletion at both the git and API layers. Say so once and carry on, because the phase is merged either way and nothing downstream depends on the branch being gone.
**A second failure for the same reason is not incidental, so stop reporting it as though it were.** Say once that deletions are refused here and recommend turning on GitHub's own *Automatically delete head branches* (Settings → General), which runs server-side on merge and needs nothing from the driver. Without that, the driver shrugs once per phase for the length of the plan and the user finds a fully stale branch list at the end, losing the signal the deletion exists to preserve: that a branch which still exists means work which has not landed.
`YOLO` lives in the Driver State block. A repo may also pin `yolo: false` in front matter, which wins, config can restrict, never enable.
**If a gate fails for the same reason on consecutive wakes with an unmoved head**, count it. At `stuck.max_cycles` (default 5), mark that row `Blocked` with the reason and tell the user. Count per row: one stuck platform must not stall the others.
The unmoved head is what this rule turns on, so it says nothing about a phase that is moving and getting nowhere. `stuck.max_review_rounds` in §5 covers that case.
## 7. After a merge
However the PR merged. You, the user from their phone, or anyone else:
1. Unsubscribe from that PR.
2. **Commit `Merged`** to the plan branch. If YOLO was on and the PR carried no UAT checklist, append that phase's id to `UAT-pending` in the same write.
3. Comment on the PR and tell the user.
4. **Start whatever rows that unblocks**, in the same turn, unless `Driver: paused`.
5. **If that was the last row**, meaning every row is now `Merged` or `Skipped`, go to §8, in this same turn.
A merge always advances the plan. YOLO changes who presses the button, never whether the plan moves.
Step 5 is the one that gets skipped, so it is worth saying why it exists. Every other transition here is chained: §5 sends a merge to §7, and step 4 sends an unblocked row back to §3. The last merge is the one where step 4 finds nothing to start and the list simply ends, which feels like a completed turn because the previous merges ended the same way. It is not one. Finishing is a transition like any other, and §8 is unreachable from here unless you take it.
## 8. Finishing the plan
When every row is `Merged` or `Skipped`, in this order: **open the integration PR if YOLO is on**, then set `Driver: idle`, clear `Active`, post a summary of what shipped, and stop.
**The summary names every open entry in Carried findings**, alongside outstanding UAT. Those are the things a reviewer raised that no phase did, and the end of the plan is the last moment anyone is looking. A plan that completes with a list nobody read is the same failure as UAT that reaches no one.
**Two actions here, and only one of them is withheld.** *Opening* the integration PR is yours and needs no permission: YOLO means nobody was watching, and this PR is the entire handoff. *Merging* it is never yours. Read "never merge it" below as the second of those, not as the PR being off-limits.
**Everything in this section is YOLO-only.** With YOLO off the developer merged every phase themselves and read every checklist as they went. They know exactly where the branch stands, and opening a PR to their default branch on their behalf presumes a decision they are already positioned to make. The handoff exists because YOLO means nobody was watching.
### The integration PR
Phases usually target a feature branch rather than the default one, so that nothing reaches `main` until the feature is whole. When the last phase merges, that branch is finished and nobody has been asked to look at it as a whole. Every review so far was of one phase against its siblings.
So, with YOLO on: **for each repo whose `target_branch` is not its default branch and which merged at least one phase, open a PR from `target_branch` to that repo's default branch.** Title it `<project>: <n> phases`. Body: what shipped, phase by phase with links; anything the plan recorded as deliberately out of scope; and any setup the feature needs that no PR could contain.
**Never merge it. YOLO opens this PR and does not merge it**, and that is not an inconsistency. YOLO is a judgement about *phase* PRs. Work the plan produced, scoped by the plan, verified by the driver, gated per phase. This one is the whole feature landing on the branch everything else builds on, and none of that reasoning reaches it. Open it, report it, go idle.
**A plan that calls this "the only human merge gate" is describing the merge.** Plans say that, in those words or close to them, because it is true and worth writing down. It is not an instruction to wait for permission before opening: the gate cannot gate anything until the PR exists, and a driver that reads it that way ends the plan by telling the user about a PR that isn't there. Front matter configures you; the plan's prose describes the work. Where the two appear to disagree about *opening* this PR, this section wins.
Don't wait on its CI either. Checks that were scoped to the default branch may report here for the first time. CodeQL and mobile build services commonly are, and a first red result on a check no phase ever saw is information for the user, not a gate for you to drive to green.
**The cumulative UAT checklist goes on the integration PR**, not on the final phase's PR, whenever an integration PR will exist. With YOLO on the final phase PR is merged by the driver within minutes of opening, so a checklist placed there is written into something nobody had to read. The integration PR is the first thing on the whole plan a human must act on, which is the only place a checklist has leverage. Clear `UAT-pending` when you open it.
With more than one repo, the **home repo's** integration PR carries the entire script across all repos, and the others link to it. UAT is end-to-end by nature; splitting it by repo produces two lists that each describe half a user journey, the same reason it is not split by phase.
Where `target_branch` *is* the default branch there is no integration PR to open, and the rules stand as they were: the final phase PR carries the cumulative checklist. Same with YOLO off, where every PR carried its own UAT as it went and there is no backlog to hand over.
### The fallback
If the plan completes with `UAT-pending` non-empty and no integration PR was opened. Every repo targets its default branch, or the final phase was skipped or merged before you assembled the list. Open a GitHub issue titled `UAT: <project>` with the outstanding items. Never drop them; unverified work that nobody knows is unverified is the thing this exists to prevent.
## Writing to the plan
`create_or_update_file` against `home_repo` on the plan branch, passing the `sha` you read at the start of the transition. Message: `chore(plan): <project> phase <N> -> <Status> [skip ci]`.
Re-read that `sha` at the start of **every** transition. When the plan branch is also a `target_branch`, merging a phase PR moves it, and a sha cached from before the merge is already stale.
On a sha conflict: re-read, **re-run the Driver-ID decision on what you just read**, and only then re-apply your change and retry. Three failures means another driver is active, so stop and report rather than forcing it.
Re-running that decision is what makes the sha a lock rather than a speed bump. A stale sha means someone else wrote between your read and your write, and the write that beat you may have been another driver claiming this plan. Re-applying your change blindly puts your own `Driver-ID` back over theirs, and now two drivers each believe they hold the claim and start the same phase. So if the re-read shows a foreign ID with a live heartbeat, you lost the race: report and stop, and do not retry.
If the plan branch rejects direct commits, switch to `plan_writes: plan-pr`. Each transition becomes a single-file PR with auto-merge, targeting the plan branch, and tell the user once that you've done so.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!