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 previewchecks each repo out at its tip.engine— the rebase engine;jjis 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 byxiv stack status, never built, never lost.
Entries
One entry per issue — one reviewable unit of the stack:
| Field | Meaning |
|---|---|
position | index in the overall stack order (0 = bottom) |
issueId / issueTitle | the Linear issue |
repo | key into repos — which substack this entry belongs to |
branchName | the git branch / jj bookmark, deterministically feat/<issue-id-lowercased> |
changeId | the jj change ID — stable across restacks |
baseBranch | what this entry stacks on: the previous same-repo entry's branch, or that repo's trunk |
headSha | current head of the branch |
status | lifecycle state (table below) |
prNumber / prUrl | set once pushed as a PR |
pushedSha | branch head at last push — drift from headSha means the open PR is stale |
dependsOn | logical (often cross-repo) dependencies; used to flag, not to auto-rebase |
knownGaps | review findings the build shipped without fixing; feeds the PR's "Known gaps" section |
Statuses
| Status | Meaning | Group |
|---|---|---|
pending | not started | unbuilt |
implementing | a build run is (or was) working on it | unbuilt |
implemented | built and committed locally, not on GitHub | built-unpublished |
pushed | branch pushed, PR opening in progress | published |
pr-open | PR open on GitHub | published |
merged | PR merged | published |
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 Ntakes the lowest contiguous run ofimplementedentries 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 agentTriage 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).
| Action | Meaning | What the operator does |
|---|---|---|
plan | no stack map exists yet | xiv stack plan <source> --feature <name> |
build | some entries are pending/implementing | xiv stack build --all-repos (resumable, parallel per repo) |
push | built-unpublished entries exist, or an open PR is stale after a re-flow | xiv stack push --all-repos (or --repo <key> --count N) |
wait | everything is published as open PRs | nothing — humans review and merge |
done | every entry in every repo is merged | the 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 publishing → wait. 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.
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.