Stack workflows
Five workflows implement the xiv stack lifecycle: plan decides the stack, build
implements it locally, push publishes it as stacked PRs, review settles those PRs, and
amend edits the middle of the stack and re-flows everything above. They share one source of
truth — the stack map, a JSON file in SMITHERS_HOME linking each Linear issue to its repo,
branch, position, status, and PR (see
/concepts/stack-map-and-triage) — and one rebase engine, jj,
running colocated next to git. CLI reference: /commands/stack.
All five refuse to run without a stack map and say so: xiv stack plan first.
stack-plan (sp) — decide the stack
Triggered by xiv stack plan <source> --feature <slug>. Two tasks:
plan(agent): fetches the Linear project or parent issue named bysource, returns every sub-issue as a separate entry —{ issueId, issueTitle, repo }— assigned to one of the configured repos. The source itself is never an entry.persist(deterministic): orders the entries, derives each branch name (feat/<issue-key-lowercase>, the same conventionlinear-implementuses), chains each entry's base onto the previous same-repo entry's branch (the first in each repo sits on that repo's trunk), and writes the stack map. All entries startstatus: "pending".
No code changes — planning only reads Linear and writes the map. Passing
--skip-acceptance-review records "build without the local review step" as the feature-wide
default on the map.
This is the single highest-leverage call in the lifecycle: it runs once per feature and every
later command inherits its ordering — getting it wrong is a rebuild, not a bad paragraph. It
therefore runs Claude Fable 5 at xhigh effort, deliberately off the XIV_TIER ladder
(overridable via XIV_STACK_PLAN_MODEL / XIV_STACK_PLAN_EFFORT; see
/concepts/model-tiers). When issue→repo assignment needs a human in the
loop, the stack-plan skill resolves the plan interactively and persists it
with xiv stack plan --plan <file> — deterministic, no planner agent at all.
stack-build (sb) — implement every entry locally
Triggered by xiv stack build --feature <slug> (per repo; --all-repos fans out one detached
run per repo). For each entry in position order it renders a three-task sequence:
build:base:<issue>(deterministic) —git checkoutthe entry's base branch, so the work stacks on the entry below.build:impl:<issue>— a fulllinear-implementsubflow: fetch → gate → plan → implement/validate/review loop → finalize onfeat/<issue-key>.skipAcceptanceReviewandmaxIterationsare forwarded to every entry; the skip flag ORs with the map's plan-time default — a run flag can force skipping on, but never re-enables review for a map that opted out.build:record:<issue>(deterministic) — read-modify-write the live map: setstatus: "implemented", record the branch and head SHA, advance the repo's tip, and persist the subflow'sknownGaps— the findings its review raised that the loop shipped without fixing. Push runs later, in its own process, and would otherwise have to re-review the diff to rediscover them.
A final build:summary task reports { feature, repo, built, pending, tip }.
Resumability is structural: all three tasks carry skipIf on the entry's map status (anything
past pending/implementing counts as built), so a crash mid-stack resumes by skipping every
already-built entry. Everything is local — no push, no PRs.
stack-push (spush) — publish as stacked PRs
Triggered by xiv stack push --feature <slug> --count N (default count 5, --draft default
true). This is the only stack workflow that touches GitHub. Two phases:
Re-sync stale PRs first. An entry is stale when it has been published (status pushed or
pr-open) but its recorded headSha no longer matches its pushedSha — i.e. a later
stack-amend re-flowed the branch underneath the PR. For each: git push --force-with-lease, confirm the PR points at the new head,
record the SHA. No comments, no reviewer pings.
Then open the next batch. The lowest contiguous run of implemented-but-unpublished entries
(capped at count), bottom to top. Per entry:
push:describe:<issue>(agent,continueOnFail) reads that entry's own diff —base...branchwhere the base is the entry below — and writes the structured body (Summary/Test Plan/Known gaps). The recordedknownGapsfrom build are handed to it verbatim: they are the literal answer to "what's still wrong with this". If describing fails, the PR still opens with just the footer.push:open:<issue>(agent) opens the PR titled<ISSUE>: <title>, based on the previous entry's branch (or the trunk once that entry is merged). The footer — Linear issue, "entry k of n" stack position, base branch — is composed deterministically, never asked of the agent, which would only risk garbling facts the map already holds.push:open-record:<issue>(deterministic) setsstatus: "pr-open"with the PR number/URL and pushed SHA.
stack-review (sr) — settle the stack's PRs
Triggered by xiv stack review --feature <slug>. The stack-wide analogue of
pr-review-loop: a signals → address loop over every open PR in
the substack at once, capped at maxRounds (default 2, return-last on the cap), exiting
when a snapshot reports clean: true — green (or absent) checks on every PR and no unaddressed
human comment anywhere. If no entry has a PR yet, it short-circuits with "run xiv stack push
first".
review:signals— one read-only snapshot per round across all PRs: CI status per PR plus findings from failing checks and unresolved human comments (all four comment surfaces; bot comments ignored; "unresolved" judged against the code at each PR's head). Not a poll — a round that finds only pending checks simply ends.review:address— the jj cascade step, skipped when there is nothing actionable. Each finding is fixed in the branch that owns the code it refers to — normally the PR it was raised on, or lower if the fault was introduced lower; never downward into an unrelated PR. Mechanically:jj edit --ignore-immutable <branch>, fix the root cause (never loosen a check), let jj rebase the descendants, verify locally, then push the edited branch and every descendant the rebase rewrote (they get new SHAs even when their diffs did not change). Deliberate non-fixes go inskippedwith a defensible reason.review:report— final state: ready for a human to merge, or exactly which PRs are still open and why (escalating viasmithers ask-humanwhen not clean).
The cascade only ever moves the edited branch and the branches above it. Force-pushing a rewritten branch may dismiss prior approvals — expected, and honest: the fix shows up in the PR it belongs to.
stack-amend (sa) — edit the middle, re-flow the top
Triggered by xiv stack amend --feature <slug> -m "change" [--target <issue|branch>].
Local-only — it never pushes (the next stack-push re-syncs the now-stale PRs). Four tasks:
amend:locate(agent, skipped when--targetnames a valid entry directly): given the change description and the stack listing, pick the lowest entry whose scope covers the change, so the fix lives at its root and re-flows up to every descendant.amend:edit(agent):jj edit <branch>, make the change — edits amend that commit directly, nogit commit— then let jj auto-rebase every descendant, resolving any conflicts bottom-to-top until every descendant bookmark is conflict-free. Reports the new head SHA and jj change id for the target and each re-flowed branch, plusconflictsRemainingif a conflict could not be fully resolved.amend:record(deterministic): write the new SHAs/change ids back to the map. BecausepushedShais not updated, every re-flowed entry with an open PR now reads as stale — which is precisely what tells the nextstack-pushto re-sync it.amend:revalidate(agent): check out the substack's tip branch — which contains the whole feature — and run the repo's lint and tests, reporting pass/fail with a summary of every failure.
Inputs at a glance
| Workflow | Input fields (defaults) |
|---|---|
stack-plan | source, feature, repoSlug, repos{}, stackMapPath, skipAcceptanceReview (false) |
stack-build | stackMapPath, repo, skipAcceptanceReview (false), maxIterations (3) |
stack-push | stackMapPath, count (5), repo, draft (true) |
stack-review | stackMapPath, repo, maxRounds (2) |
stack-amend | stackMapPath, message, target, repo |
An omitted repo means "the whole map" for build/review/amend; the CLI resolves multi-repo
features to one run per repo. Entry status advances
pending → implementing → implemented → pushed → pr-open → merged.
Models and failure semantics
Beyond the planner (Fable 5 at xhigh) and the per-entry linear-implement subflows (the mixed
pipeline described on that page), the stack workflows' agent tasks
run on the autonomous pool — heavy Claude with git/gh/jj permissions and a hard never-merge
system prompt — with amend:locate on the plain heavy Claude provider. Everything moves to codex
under XIV_ENGINE=codex. See /concepts/model-tiers.
- Every map mutation is a read-modify-write of the live file, so concurrent entries (and
--all-reposfan-out) never clobber each other; results are recorded per entry, so any crash resumes without redoing finished work. - The only
continueOnFailtask ispush:describe— its fallback is the bare footer body. - Nothing in the lifecycle merges a PR, posts a comment, or tags a reviewer. Publishing and fixing are the whole job; merging is a human's.