SuperAPI AI Directory

g2g

General & other

View repository

Updated 10-16 03:38

About this skill

g2g

Command identity and discovery

  • The command, project, module, repository, formula and this skill are all g2g. The tool was once called gt2gh, and that name survives only where it is a historical fact: tags and release assets published under it, and the tap's formula_renames.json, which is what migrates installs made under the old name. Do not reintroduce it anywhere else.
  • At the start of a task, reuse a usable command already resolved in the task context. Otherwise discover it once locally: try g2g --version, then gt2gh --version, which is worth one attempt only for an install predating the rename. Select the first that succeeds. If neither works, say the user must run brew install shhac/tap/g2g or provide a built binary; do not assume either exists.
  • Record the selected command and use it for every later invocation. Do not re-detect unless it fails or the environment changes.

Command surface

  • The top level is your own stack and git-like verbs: status, doctor, create, adopt, track, untrack, delete, fold, rename, up/down/top/bottom, restack, pull, prune, push, submit, land. Anything that reaches past Git into another tool lives under that tool's name: g2g github status|adopt|link|unlink|retarget|comment and g2g graphite adopt|mirror. Keep a new command on that side of the line its dependencies put it; push, submit and land are top level because publishing is part of managing a stack.
  • The names moved in one step and no aliases exist: graph is status, the old pull request status is github status, sync is pull, track --stack is adopt, import is graphite adopt / github adopt, mirror is graphite mirror, and the source pull-request is github. Do not add an alias back; an old name is an unknown command.
  • --json operation is the command's path (status, pull, github link, graphite adopt), which is why schemaVersion is 3.
  • Exit status: 0 success or nothing to do, 2 failure, 3 stopped part-way, and 1 only from doctor, meaning it found something.

Work in this repository

  • Read README.md and the design doc for the area before changing behavior; design-docs/initial-scope.md is the historical starting point, not the current contract. For anything touching the g2g-owned branch forest, read design-docs/g2g-owned-graphs.md first.
  • Graphite is not authoritative. Structure is resolved per invocation, and a branch the g2g graph records wins over anything Graphite declares; Graphite answers for whatever g2g has not adopted. Read design-docs/source-resolution.md before changing how a command selects a stack, and never reintroduce the assumption that Graphite decides.
  • github link previews by default. Its optional --branch target must work without checkout; --apply is the only path that may invoke gh stack link. A bare namespace (g2g github, g2g graphite) prints its help.
  • --take published means each branch as the git remote --remote names holds it (a git remote name, origin by default) — never GitHub, and never a branch name. --take only ever changes the outcome for a branch that has genuinely diverged from its own published version; every other classification collect makes is take-independent. --through <branch> bounds it to that branch and what it is stacked on — its ancestry, never a position in a flattened list, so a sibling fork is outside the boundary — and refuses the divergence elsewhere, which is a narrowing rather than an enabler: unbounded, --take published reaches every diverged branch in the selection, including ones the user was not thinking about.
  • pull has nothing to do with pull requests. It brings a stack up to date with its remote: fetch into g2g's own ref namespace, fast-forward the base or refuse if it has diverged, and replay. It never calls gh. A published branch is measured from its parent as the remote holds it, so two people moving different branches of one stack reconcile in either order.
  • Sync points (refs/g2g/synced/<remote>/<branch>, read design-docs/sync-points.md) record where this clone and the remote last agreed. Against one, pull drops a commit the remote dropped and leaves one dropped here for push, which publishes it; a commit the remote dropped that is still here makes push refuse. Every such commit is listed by branch, id and subject (and in --json commits); pull --keep <commit> keeps one; --strict on pull, push, submit and land refuses any drop, and any branch differing with no sync point. Membership in a sync point is by commit id, never content.
  • adopt --scope path and github adopt --scope path record one chain — the trunk up to --branch — for taking up one line of somebody else's stack.
  • pull --trunk-only advances only the selected branch's evidenced trunk, using the existing base-only path. It leaves stack branches and graph records alone, works on default or declared trunks before a stack is recorded, and refuses unknown feature branches. It is mutually exclusive with --scope, --take, --through, and --prune.
  • A branch another worktree has checked out (the trunk in the main checkout, a stack branch in an agent's worktree): restack, pull and land move it when that worktree is clean and bring its files along, and leave it where it is when the worktree has changes — the preview names both, and a left branch is finished later by g2g restack (or g2g pull --trunk-only for a trunk) run in that worktree. --worktrees skip leaves every such branch; --worktrees refuse refuses instead. land cannot leave part of a descent, so a branch it would leave refuses it — except a lone branch with nothing above it, which lands while the trunk stays — and a branch it deletes is refused while another worktree has it.
  • land retries GitHub's explicit base-change refusal at most twice, checking readiness again and pinning every merge with --match-head-commit. Other errors stop. A single branch with no children can land while the trunk is checked out elsewhere: it checks the merge in the fetched trunk and leaves the local trunk untouched. Deleting the current branch detaches onto the fetched merge, as previewed; survivors still require the ordinary replay and refuse a held trunk.
  • prune forgets branches whose work has landed. It is its own command rather than pull's tail because it answers a different question on the same boundary. By default it edits the recorded graph and deletes no branch. --delete-branches opts into local deletion; --forget-missing also removes selected records whose refs are gone. Preview lists both acts separately. Deletion is content-based, refuses every checked-out branch, rechecks heads and bases, and removes refs under an expected-tip lease. Partial cleanup exits 3 with completed work and a scoped retry. Remote branches are untouched. A child that forgetting would strand is recorded on the branch below only where Git shows that branch is an ancestor of the child — what a pull leaves — with the same check and fork point track uses; that is recording where it already sits, not reparenting around it. Anything else, including a child outside the selection, still refuses, offering g2g pull --prune and then a g2g track --branch <child> --parent <branch> per child. Do not widen the rehome to a guess.
  • pull --prune composes the two over one selection: the pull, then the prune. The preview can only say it will prune, because what has landed is known once the base moves. A prune that refuses after the pull happened is a stop part-way (3), not a failure. --json/--porcelain refuse --prune, since it is two reports and those formats are one document. --delete-branches and --forget-missing require --prune on pull and carry the same choices through to prune after the pull.
  • status is offline. It draws the stack from g2g's graph (or --from graphite) and marks each branch against what the remote last held here, from local refs alone — refs/remotes/<remote>/<branch> and refs/g2g/remotes/<remote>/<branch>, taking whichever descends from the other and the remote-tracking ref when they are not in order (git.Client.KnownTips, through push.Known). It never fetches and never calls gh; that is what makes it safe to run before deciding whether to. Marks are origin✓, N ahead, N behind, replayed since pushed, diverged · N here, M there, on a commit not here, not on origin, counted by content like push. The default remote missing draws no marks; a --remote named on purpose that does not exist is an error. It also reads local PR observations, labelled "last seen" and dated, from g2g/pull-requests.json in the Git common directory. No observation means "PR history unknown", never "never submitted". Read design-docs/pr-observations.md before changing this cache: it is separate from graph authority and can never justify branch deletion. submit, land and GitHub reads share the client's observer without extra network calls; a merge request is not a confirmed merge. Cache write failure warns without turning a successful remote action into failure.
  • doctor reads every recorded stack offline and reports only what is not as it should be, each with its command: restack --continue, restack --branch, track --branch (moved off, parent gone, no tracked parent), track --branch --parent (fork point gone), untrack --branch, prune --branch, pull --branch (diverged from the remote). It exits 1 when it finds something. status is the full overview; doctor is only the unexpected — keep that split.
  • push is a preview-first publication escape hatch. It selects through source resolution, forks included (one atomic push has no order to keep, so it takes every scope but all), leaves a landed branch out rather than recreating it, must never submit or restack, and must never call gh; only --apply may run exactly one git push --atomic --set-upstream --force-with-lease <remote> <branches> call (--no-set-upstream drops --set-upstream; submit and land carry the same flag through to it). Keep the remote default explicit (origin), validate it, and never fall back to a weaker push mode.
  • A command that did part of what it was asked and stopped exits 3 — not 0, which told a script the work had finished, and not the failure status, because what it achieved is not coming back. pull stopping mid-replay, pull --prune whose prune refused after the pull, land stopping after something merged or was tidied, github comment stopping after writing some comments, github retarget stopping after moving some bases, submit failing after its push or after opening a pull request, create -m whose commit failed after the record, and a delete/fold/rename whose rollback could not finish are all this; a descent that changed nothing is an ordinary failure. stoppedPartWay marks it and nothing further is printed, because the report is already on stdout.
  • A trunk can be declared: track --as-trunk names a second one (staging beside main), and --into <branch> --by squash|merge|rebase says where it lands when finished. It has no parent edge — so it is never replayed and every stack above it stops there — and land --branch <trunk> lands it into that branch by the declared method. track --parent or untrack ends a declaration; adopt, graphite adopt and github adopt refuse to overwrite one. See design-docs/declared-trunks.md.
  • land takes a finished stack down onto its trunk, bottom branch first. Read design-docs/land.md before changing it. It refuses a stack g2g has not adopted: it resolves through whichever source describes the branch but replays and forgets in g2g's own graph, so on a Graphite-described stack it would merge every pull request and then restack and forget nothing. It owns no rules of its own: it publishes through push, advances and replays through pull, and asks Git by content whether a branch has landed through the same check prune uses. Do not give it its own copies of those refusals — a lease built from tips it read itself always matches, so a direct push would overwrite a reviewer's commit and then merge it. It aims every pull request at the trunk rather than at the branch below it, which is where each correctly sits now and which will not exist by the time its turn comes. It waits for GitHub twice, on the push and on the merge, and for neither does it wait on CI; the merge wait is answered by ancestry from the merge commit, never by watching a tip change, because a colleague's push changes that too. It merges nothing until every branch has been found landable. Uncommitted work is refused only where the descent would touch it: up front when the checkout is a branch it moves, and before a replay whose preview conflicts (restack.Plan.NeedsWorkingTree), which stops part-way rather than rebasing into somebody's changes. Cleanup never fails a descent. It is not journaled and must not become so: re-entrancy comes from recomputation, and restack stays the only resumable operation.
  • How much of the structure a command means is --scope, and it means the same thing whichever record answered. Read design-docs/stack-scope.md before changing it. --from pins which source answers for one invocation.
  • A command named inside a hint is marked at the point the sentence is written (runnable("g2g github link")) and drawn when the sentence is styled. Do not add a highlighter that finds commands by pattern: what is drawn as runnable must be exactly what can be copied and run. Marks are control characters, never content, so keep them out of anything that becomes an error — stderr is undecorated and would print them verbatim — and strip them for --json and --porcelain. Presentation.style re-opens the enclosing style after each command, because ANSI ends a style by returning to the default and a subdued hint would otherwise come back bright from its first command onwards. The chip's padding column is painted, never written into the sentence, so plain output is unchanged.
  • A package that refuses says why and what to do as a repair.Note, and derives its Blocked sentence from it. internal/cli renders the sentence for a machine and lays the same values out for a person — reason on its own line, one way out per line, each command drawn as a command. Do not hand-write a refusal sentence beside the structure: the pair drifts, and the sentence is the half a machine reads. A refusal delegated from another package carries no structure here, so refusing takes the sentence too and still says why.
  • Linking has two halves and they must stay apart: Presentation.hyperlink is the capability (may this output carry a link), and internal/cli/links.go is the policy (what does a thing point at, and which service wins). A render site never builds a URL. Add a destination by adding a resolver to an ordered list; add a linkable thing by adding a subject type and its own list. GitHub outranks Graphite for a pull request because its address was reported rather than assembled.
  • --debug is a persistent, stderr-only diagnostic flag on every command. It is safe for local investigation but must not alter command behavior or cause agents to enable Graphite's own gt --debug.
  • Never guess a Graphite trunk from its name. The selected ancestry determines the inferred trunk; multiple valid declared trunks require --trunk, whose value must itself be declared and ancestral. A g2g-owned path has one root, so --trunk may only confirm it and must refuse any other value rather than ignoring it.
  • Read design-docs/graphite-cli-contract.md before changing discovery. Do not read Graphite internal metadata/configuration or use gt --debug: supported production discovery is strict, compatibility-gated noninteractive CLI parsing.
  • graphite mirror is the only command that writes Graphite, through exactly gt track <branch> --parent <p> --no-interactive and gt untrack <branch> --force --no-interactive, both behind the same version gate as discovery. Every other command's Graphite use stays read-only. Read design-docs/source-alignment.md before touching internal/align.
  • No g2g command may enrol a repository into Graphite — including the ones that write it. Reading Graphite's forest is what creates state, so graphite mirror and graphite adopt check graphite.Configured and refuse before reading. A repository with no Graphite has no trunk and could not be mirrored into anyway, so nothing is lost by refusing first.

g2g-owned graphs

  • status, doctor, adopt, track, and untrack operate on a branch forest g2g owns itself. They read Git only: never call Graphite or GitHub from these paths, and never make them require a network. That independence is the feature.
  • The model is a forest: at most one parent per branch, many children per parent, several roots. Do not reintroduce a linear assumption. Graph identity is derived from the edges, never stored; do not add graph IDs.
  • Authority is per branch (g2g or graphite), never per graph. A whole-graph rule cannot survive two components becoming connected by an action g2g never observed.
  • track must never choose a parent. Preview the ordered candidates and block. Recording a structure every later command trusts is not a place for a good guess. adopt is not an exception: the user asserts the trunk (--trunk) and ancestry supplies the rest, and it refuses wherever ancestry cannot order two branches. It records a forest, never a chain — a branch whose only selected ancestor is the trunk is a separate stack and must be left alone.
  • A trunk is a branch nothing sits under. Graph.Adopt owns both halves of that invariant; do not pair Track with a hand-rolled promotion step, and never take the trunk list from the graph as it was before the edge was recorded.
  • untrack must never reparent the children it strands. Report them.
  • delete, fold and rename (internal/reshape, which reaches Git and the graph and nothing else) act only on branches the g2g graph records and refuse the rest naming g2g track. delete is the one place children are reparented, onto the deleted branch's parent, because the user asked for the branch to go; do not extend that to untrack or prune. The children keep their fork points, so the next restack drops the deleted branch's commits from them, and the preview must say so and name each of those commits that exists nowhere else (not in the parent by content, per landed.Into and Cherry; on no remote-tracking ref — a local read). None of the three replays a commit: fold only fast-forwards the parent under a lease, and refuses a trunk (naming g2g land) and a parent that moved on (naming g2g restack). A ref moved under the checkout brings the tree with it through SwitchTree; a branch another worktree holds is refused for delete and fold, and allowed for rename because git branch -m moves that worktree's HEAD. Steps are ordered so all but the last can be put back, and a rollback that cannot finish exits 3. There is no split: choosing which commit goes where is interactive and a guess.
  • create takes its parent from the branch you stand on or --parent, which is the user stating it, so it offers no candidates. It refuses a parent the graph does not record (tracked, or a trunk something sits under) unless it is the repository's default branch, because recording a child under an unknown branch silently makes that branch a trunk. It records through PlanTrack and ApplyTrack, never its own write. Order is switch, record, commit: a failed record is rolled back completely (switch back, delete the branch, which has nothing on it), and a failed commit after the record keeps both and exits 3.
  • up/down/top/bottom are the only commands that act without --apply, and that exception is deliberate: moving the checkout changes no ref, record or remote, and git switch refuses to clobber local changes. Do not add a preview, and do not extend the exception to anything that writes. They resolve through the same stack.Resolver as every stack command, refuse at a fork naming the children, switch with --no-guess, honour the restack guard, and offer --dry-run. A trunk is undescribed by every source, so from one they ask the g2g graph for its recorded children and resolve onward from the single child.
  • graphite mirror and the two namespaced adopts must never remove a branch from the g2g graph. Alignment keeps the two records in step; it does not transfer ownership. graphite mirror writes Graphite only, graphite adopt and github adopt write the g2g graph only, and both adopts refuse a branch the g2g graph already records under a different parent rather than resolving the disagreement.
  • github adopt adopts a published stack through the same planning (planAdoptions: additive, conflict refusal, parents first) as graphite adopt; only the record read and the fork point differ. The namespace fixes the source, so neither takes --from. github adopt takes --branch and --scope stack|trunk; graphite adopt takes neither, because Graphite is read whole. It selects through the GitHub source's own Select and is the only adopt that invokes gh. It never creates a branch: anything the pull requests place that is not local (Snapshot.Absent, or a base not here) refuses the plan with git fetch && git switch <branch> / git branch <branch> origin/<branch>. The stack's base must already be recorded or be DefaultBranch — evidence permitting a root, as in create, never choosing a parent. The fork point is the merge base with the base, never the base's tip, because the base may have moved since the pull request was opened. Its revalidation re-reads GitHub, so it is wired with a selector that has no memo.
  • Mirror ordering is dictated by Graphite's CLI, not by taste: writes go parents before children because gt track --parent requires a tracked parent, and prunes go deepest first — refusing any stranger with a surviving child — because gt untrack cascades to the subtree.
  • Do not record commit SHAs in the store: commits and force-pushes are content movement, not structural drift. Validate against Git at read time instead.
  • restack is the only code permitted to rewrite history, and only through internal/git's two engines. git replay previews exact object ids without moving a ref and applies cleanly without touching the checkout; git rebase --update-refs is used only once a preview has established the rewrite conflicts, and it runs in the user's own working tree because resolving a conflict needs a tree they can edit. Do not move it to a private worktree: git refuses to check out a branch already checked out elsewhere, and the --detach workaround silently splits the stack in two.
  • The replay range is forkPoint..branch, never base..branch. Before any rewrite, the fork point must be an ancestor of the branch — a branch someone rebased by hand fails that, and replaying anyway pulls the base's own commits into the range. Refuse and tell the user to retrack.
  • A branch whose parent is being rewritten must be rewritten too, even when it still sits exactly where its fork point says. Each independent root is its own replay, and every range passed to an engine starts at that root's fork point, because an engine replays the union onto one base.
  • restack is the only resumable operation. Every other mutating command must refuse while its journal exists, --continue recomputes from the refs rather than resuming a stored queue, and --abort restores tips the journal recorded because git only rolls back the invocation it was running.
  • The store lives under the Git common directory and is located with git rev-parse --path-format=absolute --git-common-dir. The bare form is relative to the working directory and silently wrong from a subdirectory. Writes are temp-file plus rename. storeSchemaVersion is separate from the --json schemaVersion; an unrecognised store version fails closed.
  • --scope branch|path|subtree|stack|trunk|all is selection, not projection policy. Displaying a subtree does not imply a subtree can be linked on GitHub. The type and the traversal live in internal/shape, which depends on nothing, because every record answers them and internal/graph needs them.
  • A command must refuse any scope it did not offer, and name its own default. They genuinely differ: status and github status default to stack because reading is free, restack to subtree because rewriting is not, and all is offered only where nothing is rewritten: the read-only commands, and prune, which forgets landed records and, with --forget-missing, absent selected records. ParseScope takes both the accepted set and the fallback; there is no global default left to inherit.
  • Projection is a capability, not a scope. github link, submit, land and github retarget take stack|path and refuse a forked selection through Snapshot.RequireLinear, which names the remedy instead of choosing a line.
  • Selected from a trunk, stack is the whole tree under it — a trunk's path is itself. That is how a rewrite asks for an entire shape without being handed a scope that could reach another trunk.
  • all is deliberately absent from RewriteScopes: it spans trunks, and a rewrite acts on one. trunk is offered, and a rewrite that wide is the likeliest to reach a branch checked out in another worktree, which the rewrite then brings along if that worktree is clean and leaves where it is if not; --worktrees refuse refuses it by name instead.

Source resolution

  • Every stack-selecting command resolves which source describes the branch: g2g's own store first (adoption is the claim), then Graphite. The answer is derived per branch on every run and never stored — there is no owner field, and adding one reintroduces state that goes stale through actions g2g never observes.

  • Never run Graphite in a repository that does not already use it. Its discovery command creates state, so Describes is answered from the repository's own configuration and Select is the only call that runs gt. Checking for that file is the single deliberate exception to reading none of Graphite's paths, and only its existence is ever read.

  • Authority governs mutation, not description. Reading composes across sources; restack refuses a branch it has no fork point for and names g2g track.

  • GitHub's native stack is not a source. It is written from the others, and is only ever read to report membership or to find a stack to unlink.

  • github is the third source and answers only via --from github (on github status), never by precedence: reading a base invokes gh, and push must never do that. It describes published branches only, and GitHub retargets a child when its base is deleted on merge, so it reports what a merge will do rather than what the stack was.

  • github link covers both creating and repairing the GitHub relationship; there is no separate reconcile command. pull means fetch, advance the base, replay. It forgets nothing unless asked with --prune: gt sync prunes as its tail and this does not by default, because forgetting a landed branch is a different question on the same boundary and belongs to prune.

  • A diverged base is reported, never merged or reset. Pruning edits the graph; local branch deletion requires explicit --delete-branches.

  • github comment keeps one marked comment per pull request listing its stack. Read design-docs/stack-comment.md before changing it. It always keeps the whole stack the branch belongs to — each comment draws the stack from its own pull request (the path from the base flat, what forks off it counted beside the branch it grew from, everything above it in full), so a partial run would leave comments disagreeing — and has no --scope. Every line names its branch. A comment is current when the rev= on its marker line — a hash of the stack and data line, never the footer — matches what this run would write; do not go back to comparing text, which depends on GitHub returning the body byte for byte. Merged history lives in the comments' own data line because another clone cannot read local PR observations; keep only what GitHub says merged, never create a comment on a merged pull request, never edit a comment without the marker, and leave alone one the viewer cannot edit, a pull request carrying two, or one whose data line v= this g2g does not know, and believe records only from comments the viewer can edit. Marker and data line fields are read by name; a data line with no v= is the bare list 0.43.0 and earlier wrote. Writes are addComment/updateIssueComment by node id with the body as a raw -f field, all of a run's in one aliased mutation document (githubstack.Client.WriteComments, twenty to a document); errors must not echo the body. A refused write fails alone and the rest are written, so a run reports every comment written and every one not, exiting 3 when some were. Local PR observations are supplemental history, never authority for rewriting these comments. submit and land keep the comments as their last act through comment.Service.Keep unless --no-comment — submit after opening them, land on the branches above what it landed (land.Plan.Above, the last recipe step) — and a failure there exits 3 with the command's own work standing. push never keeps them (it must never call gh), nor do github retarget and github link.

  • github retarget is the only command a user runs to change what a merge will do, and land reaches for the same client method rather than growing its own — a child's base only goes stale during a descent, once the branch below it has merged, and every move it makes appears in the preview. It writes through githubstack.Client.Retarget — updatePullRequest mutations by the node id Inspect reads, every change in one aliased document, land sending one at a time — moves only the bases that disagree with the resolved stack, reports every base it could not move rather than stopping at the first, and refuses a branch with more than one open pull request rather than choosing between them. land's recipe still prints gh pr edit, as advice to a person. Do not fold it into submit or run it as the tail of restack.

  • Writes once per pull request go through githubstack.Client.mutate: one aliased document per twenty, values as raw -f variables, the answer read per alias even when gh exits non-zero, and only an answer with no data a *CommandError for the whole document. A new per-pull-request write belongs there and in schema_test.go. gh pr create and gh pr merge are deliberately not batched.

Develop and test

  • Keep external process calls behind internal/subprocess.Runner. Tests must use fake gt and gh executables on PATH, including captured supported Graphite text fixtures; never require credentials, network access, or real CLI installations. Graph ancestry is the one exception where a PATH fake proves nothing — it answers whatever it is asked, and the question is what Git considers reachable — so those cases build a throwaway local repository with synthetic branch names and no remote.
  • Preserve the completion bash|zsh|fish interface. Dynamic --branch and --trunk completion must remain deterministic, read-only, and checkout-free — and must reach no source the command itself would not reach. Completing a flag must never be what enrols a repository into Graphite, and must keep working with no Graphite installed.
  • Run gofmt -w on changed Go files and go test ./.... Use go vet ./... when changing Go code or preparing a release.
  • Use git hunk for any staging. Do not commit, tag, push, or invoke real gt/gh mutations unless the user explicitly asks.

Use safely

  • For a person who wants an editor workflow, g2g submit --edit opens one temporary JSON document, not a buffer per PR. It retains the document on all failures and after preview; successful --edit --apply cleans it up unless --keep-spec is present.

Submitting pull requests

  • submit is a preview-first PR creation recovery path. It must never invoke gt submit, restack Graphite, or retarget an existing PR. Its --apply boundary validates/revalidates first, publishes through push (and so refuses a remote holding work this checkout lacks), creates only missing draft PRs, links the eligible stack only when --link asks, then keeps the stack comments unless --no-comment. Linking is opt-in because GitHub will not merge a linked pull request through gh pr merge; land refuses a linked stack in its preview and names g2g github unlink --branch <target> --stack-number <n>. Do not make linking the default again.

  • For non-interactive use, create a private temporary directory with g2g submit --write-spec <dir>, complete submission.json, validate with g2g submit --spec <dir>/submission.json, then add --apply. Keep the spec on failure and state exact repair/validation/retry commands. Multiple PR templates require --template <name> or --no-template; never guess.

  • A refusal reaches a machine as blocked (the reason, no label) and repair (the ways out, each with its command separate from the prose). Read repair rather than parsing the sentence, and treat a way with no command as a real answer that is not a thing to run.

  • Prefer --json (or --porcelain) over parsing the human preview. Both are renderers over the same validated view, they suppress colour and every human-facing line, and schemaVersion signals breaking changes. Never scrape the pretty graph.

  • A blocked preview names the repairing command, decided once in link.Plan.Repair (a repair.Note) and rendered from it for both readers: merged pull requests point at g2g pull for a g2g-recorded stack and gt sync for a Graphite one, landed branches at g2g prune/gt sync, missing or closed ones at g2g submit, and a wrong base at g2g github retarget — never pull, which does not touch pull requests. A structure read from pull request bases gets no command, because nothing here records it. Two open pull requests for one branch is deliberately unadvised — a person must choose.

  • A branch's annotation is a list of stackMark — one axis each, one severity each: base✓/base✗, head✗, pr✗, and a subject-less mark for what is about no axis. stackNode.marked replaces marks, withMarks enriches them, and labeled supplies a plain label. Marks are the sole annotation record; machine state and worst severity are derived when rendered, never cached alongside them. Do not fold two axes into one mark. A merged pull request is pr✓ and neutral, never grouped with a missing or closed one: it succeeded, and only the leftover branch is a problem.

  • Batch before parallelising, and do not reintroduce a call whose only purpose is to feed the next one: Inspect is a single gh api graphql naming the repository through {owner}/{repo}, and revisions resolve through git.Client.ResolveAll in one process.

  • Per-branch Git reads go through link.eachBranch, which runs them several at a time and cancels the rest on the first failure. Write results into a slice index the read was given, never a shared map, and keep any fake it can reach safe to call concurrently. go test -race ./internal/link ./internal/cli.

  • Currency is counted by content (Cherry, never Divergence) and bounded to a branch's own commits — above its parent, not above the trunk. Counting commit ids reported every commit the trunk had gained as unpushed work of the reader's own. A branch replayed since it was pushed is Currency.Rewritten: nothing missing, needs pushing, and not a divergence.

  • "Landed" is a content question Git answers and GitHub cannot, and it needs both halves: Cherry per commit, then Absorbed for the squash merge. A landed branch is link.IssueLanded, never IssueMissing — the advice for missing is g2g submit, and submitting work already in the trunk is the bug this prevents. What forgets it depends on the source (g2g prune for g2g's graph, gt sync for Graphite), so do not hardcode one.

  • github status is the read-only triage entry point for pull requests. It renders one selected path from the resolved g2g or Graphite structure and reports each selected PR's native GitHub stack membership from the same batched PR query; keep the healthy case to one compact summary line and annotate only missing/conflicting nodes. github status may select missing g2g-recorded branches to check their PRs online, and skips Git content/currency questions for absent refs. Every mutation retains the default missing-branch refusal. github unlink is the deliberate inverse of github link: it discovers the GitHub stack number from the selected path and refuses rather than guesses when that path is unlinked or spans several stacks, accepts --stack-number to override, previews first, and only --apply invokes gh stack unstack. It must never alter Graphite, branches, PR content, reviewers, or PR lifecycle.

  • After command discovery, use the resolved command's --help to inspect the current interface (for example, g2g github --help after a Homebrew install).

  • Start with g2g status (offline, one stack) or g2g doctor (offline, every stack, only what is wrong); reach for g2g github status when the question is about pull requests.

  • Read the preview before any --apply. pull never changes GitHub; the commands that do are submit, land and everything under g2g github except status and adopt.