Skip to content
xiv

Stack map & triage

The stack map is the persisted source of truth for a stacked feature: which Linear issues are in it, in what order, on which branch, in which repo, and how far each has progressed toward merge. Every xiv stack command reads it, and the ones that change state write it back. The triage oracle derives "what should happen next" from the map deterministically, so even a low-cost operator agent can drive a feature by matching one field against a fixed decision table.

Where the map lives

<SMITHERS_HOME>/stacks/<feature>.json      # e.g. ~/.smithers/stacks/checkout.json

The path is keyed by feature alone, deliberately: every xiv stack command can find the map from anywhere, because the repos a feature spans are recorded inside the map, not implied by where you stand. Feature names are slugified for the filename (lowercased, runs of non-alphanumerics collapsed to a dash).

The data model

A map (schema version 1) looks like this:

{
  "version": 1,
  "feature": "checkout",
  "repoSlug": "shop-api-3f9c2a1b",
  "repos": {
    "api":    { "path": "/code/api",    "baseBranch": "main" },
    "ledger": { "path": "/code/ledger", "baseBranch": "main" }
  },
  "tips": { "api": "feat/eng-403" },
  "engine": "jj",
  "skipAcceptanceReview": true,
  "source": {
    "parentIssueId": "ENG-400",
    "issueIds": ["ENG-401", "ENG-402", "ENG-403"],
    "excluded": [{ "issueId": "ENG-404", "reason": "infra/manual: set GCP secrets" }]
  },
  "entries": [ /* see below */ ],
  "createdAt": "…", "updatedAt": "…"
}
  • repos — every repo the feature spans, each with its local path and trunk branch. A single-repo feature is just the degenerate case with one entry.
  • tips — per repo, the branch at the top of its substack (empty until that repo has built at least once). xiv stack preview checks each repo out at its tip.
  • engine — the rebase engine; jj is the only supported value.
  • skipAcceptanceReview — the plan-time feature default for building without the local review step (see the review model); only materialized when set, so existing maps round-trip byte-identically.
  • source.excluded — issues that belong to the feature but were deliberately not built (non-code work like "set GCP secrets"), with the reason. Shown by xiv stack status, never built, never lost.

Entries

One entry per issue — one reviewable unit of the stack:

FieldMeaning
positionindex in the overall stack order (0 = bottom)
issueId / issueTitlethe Linear issue
repokey into repos — which substack this entry belongs to
branchNamethe git branch / jj bookmark, deterministically feat/<issue-id-lowercased>
changeIdthe jj change ID — stable across restacks
baseBranchwhat this entry stacks on: the previous same-repo entry's branch, or that repo's trunk
headShacurrent head of the branch
statuslifecycle state (table below)
prNumber / prUrlset once pushed as a PR
pushedShabranch head at last push — drift from headSha means the open PR is stale
dependsOnlogical (often cross-repo) dependencies; used to flag, not to auto-rebase
knownGapsreview findings the build shipped without fixing; feeds the PR's "Known gaps" section

Statuses

StatusMeaningGroup
pendingnot startedunbuilt
implementinga build run is (or was) working on itunbuilt
implementedbuilt and committed locally, not on GitHubbuilt-unpublished
pushedbranch pushed, PR opening in progresspublished
pr-openPR open on GitHubpublished
mergedPR mergedpublished

xiv stack build resumes by skipping anything past the unbuilt group; xiv stack push publishes from the built-unpublished group.

Per-repo substacks

Stacking is within a repo only. Each repo's entries form an independent chain: an entry's base is the previous same-repo entry's branch, or the repo's trunk for the bottom entry. Two rules keep publishing coherent:

  • Push is contiguous from the bottom. xiv stack push --count N takes the lowest contiguous run of implemented entries in a repo and stops at the first unbuilt one — so a PR never opens against an unpushed base.
  • PR bases retarget on merge. A new PR targets the previous entry's branch — unless that entry is already merged (or absent), in which case it targets the repo trunk.

Cross-repo dependencies are recorded (dependsOn) but not auto-propagated; contract breaks between repos surface at integration-test time.

The triage oracle

xiv stack triage --feature checkout          # human-readable
xiv stack triage --feature checkout --json   # for an operator agent

Triage is a pure function of the map — deterministic, the same answer every run — plus a dump of active Smithers runs appended for context. It reports one overall action and a per-repo breakdown (phase, counts, in-flight issue, stale branches, tip).

ActionMeaningWhat the operator does
planno stack map exists yetxiv stack plan <source> --feature <name>
buildsome entries are pending/implementingxiv stack build --all-repos (resumable, parallel per repo)
pushbuilt-unpublished entries exist, or an open PR is stale after a re-flowxiv stack push --all-repos (or --repo <key> --count N)
waiteverything is published as open PRsnothing — humans review and merge
doneevery entry in every repo is mergedthe feature is complete

Per repo, the phase is decided in priority order: stale branches → push; unbuilt entries → build; built-unpublished → push; all merged → done; otherwise publishingwait. The overall action is the most urgent across repos (build > push > wait > done). The xiv-operator skill is built around polling exactly this report.

How jj powers the stack

The rebase engine is jj (Jujutsu), run colocated: xiv stack init runs jj git init --colocate, which puts .jj/ next to .git/ (reversible with rm -rf .jj). GitHub, CI, and reviewers see a normal git repo with normal branches and PRs. The working rule: read with git, edit/restack through jj or xiv stack.

Every stack command that touches a repo runs a jj preflight first and fails with a specific fix when jj is missing, the directory is not a jj repo, or — the subtle one — a stray jj workspace in an ancestor directory is shadowing the repo (e.g. someone ran jj git init in a parent folder holding many repos).

The payoff is xiv stack amend: edit one entry anywhere in the stack and jj automatically rebases every descendant on top of the change — no manual cascade of rebases.

Loading diagram...

amend is local-only; push is the only command that touches GitHub — it opens PRs for the next batch and re-syncs any open PR that a later amend re-flowed (detected by the headSha / pushedSha drift above).

Related: stack workflows, the stack-plan skill, and the xiv stack command reference.