Command identity and discovery
- The command, project, module, repository, formula and this skill are all
g2g. The tool was once calledgt2gh, and that name survives only where it is a historical fact: tags and release assets published under it, and the tap'sformula_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, thengt2gh --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 runbrew install shhac/tap/g2gor 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|commentandg2g graphite adopt|mirror. Keep a new command on that side of the line its dependencies put it;push,submitandlandare top level because publishing is part of managing a stack. - The names moved in one step and no aliases exist:
graphisstatus, the old pull requeststatusisgithub status,syncispull,track --stackisadopt,importisgraphite adopt/github adopt,mirrorisgraphite mirror, and the sourcepull-requestisgithub. Do not add an alias back; an old name is an unknown command. --jsonoperationis the command's path (status,pull,github link,graphite adopt), which is whyschemaVersionis 3.- Exit status:
0success or nothing to do,2failure,3stopped part-way, and1only fromdoctor, meaning it found something.
Work in this repository
- Read
README.mdand the design doc for the area before changing behavior;design-docs/initial-scope.mdis the historical starting point, not the current contract. For anything touching the g2g-owned branch forest, readdesign-docs/g2g-owned-graphs.mdfirst. - 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.mdbefore changing how a command selects a stack, and never reintroduce the assumption that Graphite decides. github linkpreviews by default. Its optional--branchtarget must work without checkout;--applyis the only path that may invokegh stack link. A bare namespace (g2g github,g2g graphite) prints its help.--take publishedmeans each branch as the git remote--remotenames holds it (agit remotename,originby default) — never GitHub, and never a branch name.--takeonly ever changes the outcome for a branch that has genuinely diverged from its own published version; every other classificationcollectmakes 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 publishedreaches every diverged branch in the selection, including ones the user was not thinking about.pullhas 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 callsgh. 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>, readdesign-docs/sync-points.md) record where this clone and the remote last agreed. Against one,pulldrops a commit the remote dropped and leaves one dropped here forpush, which publishes it; a commit the remote dropped that is still here makespushrefuse. Every such commit is listed by branch, id and subject (and in--jsoncommits);pull --keep <commit>keeps one;--strictonpull,push,submitandlandrefuses any drop, and any branch differing with no sync point. Membership in a sync point is by commit id, never content. adopt --scope pathandgithub adopt --scope pathrecord one chain — the trunk up to--branch— for taking up one line of somebody else's stack.pull --trunk-onlyadvances 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,pullandlandmove 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 byg2g restack(org2g pull --trunk-onlyfor a trunk) run in that worktree.--worktrees skipleaves every such branch;--worktrees refuserefuses instead.landcannot 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. landretries 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.pruneforgets 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-branchesopts into local deletion;--forget-missingalso 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 exits3with 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 pointtrackuses; that is recording where it already sits, not reparenting around it. Anything else, including a child outside the selection, still refuses, offeringg2g pull --pruneand then ag2g track --branch <child> --parent <branch>per child. Do not widen the rehome to a guess.pull --prunecomposes 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/--porcelainrefuse--prune, since it is two reports and those formats are one document.--delete-branchesand--forget-missingrequire--pruneonpulland carry the same choices through to prune after the pull.statusis 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>andrefs/g2g/remotes/<remote>/<branch>, taking whichever descends from the other and the remote-tracking ref when they are not in order (git.Client.KnownTips, throughpush.Known). It never fetches and never callsgh; that is what makes it safe to run before deciding whether to. Marks areorigin✓,N ahead,N behind,replayed since pushed,diverged · N here, M there,on a commit not here,not on origin, counted by content likepush. The default remote missing draws no marks; a--remotenamed on purpose that does not exist is an error. It also reads local PR observations, labelled "last seen" and dated, fromg2g/pull-requests.jsonin the Git common directory. No observation means "PR history unknown", never "never submitted". Readdesign-docs/pr-observations.mdbefore changing this cache: it is separate from graph authority and can never justify branch deletion.submit,landand 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.doctorreads 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 exits1when it finds something.statusis the full overview;doctoris only the unexpected — keep that split.pushis 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 butall), leaves a landed branch out rather than recreating it, must never submit or restack, and must never callgh; only--applymay run exactly onegit push --atomic --set-upstream --force-with-lease <remote> <branches>call (--no-set-upstreamdrops--set-upstream;submitandlandcarry 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— not0, which told a script the work had finished, and not the failure status, because what it achieved is not coming back.pullstopping mid-replay,pull --prunewhose prune refused after the pull,landstopping after something merged or was tidied,github commentstopping after writing some comments,github retargetstopping after moving some bases,submitfailing after its push or after opening a pull request,create -mwhose commit failed after the record, and adelete/fold/renamewhose rollback could not finish are all this; a descent that changed nothing is an ordinary failure.stoppedPartWaymarks it and nothing further is printed, because the report is already on stdout. - A trunk can be declared:
track --as-trunknames a second one (stagingbesidemain), and--into <branch> --by squash|merge|rebasesays where it lands when finished. It has no parent edge — so it is never replayed and every stack above it stops there — andland --branch <trunk>lands it into that branch by the declared method.track --parentoruntrackends a declaration;adopt,graphite adoptandgithub adoptrefuse to overwrite one. Seedesign-docs/declared-trunks.md. landtakes a finished stack down onto its trunk, bottom branch first. Readdesign-docs/land.mdbefore 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 throughpush, advances and replays throughpull, and asks Git by content whether a branch has landed through the same checkpruneuses. 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, andrestackstays the only resumable operation.- How much of the structure a command means is
--scope, and it means the same thing whichever record answered. Readdesign-docs/stack-scope.mdbefore changing it.--frompins 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 anerror— stderr is undecorated and would print them verbatim — and strip them for--jsonand--porcelain.Presentation.stylere-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 itsBlockedsentence from it.internal/clirenders 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, sorefusingtakes the sentence too and still says why. - Linking has two halves and they must stay apart:
Presentation.hyperlinkis the capability (may this output carry a link), andinternal/cli/links.gois 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. --debugis 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 owngt --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--trunkmay only confirm it and must refuse any other value rather than ignoring it. - Read
design-docs/graphite-cli-contract.mdbefore changing discovery. Do not read Graphite internal metadata/configuration or usegt --debug: supported production discovery is strict, compatibility-gated noninteractive CLI parsing. graphite mirroris the only command that writes Graphite, through exactlygt track <branch> --parent <p> --no-interactiveandgt untrack <branch> --force --no-interactive, both behind the same version gate as discovery. Every other command's Graphite use stays read-only. Readdesign-docs/source-alignment.mdbefore touchinginternal/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 mirrorandgraphite adoptcheckgraphite.Configuredand 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, anduntrackoperate 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 (
g2gorgraphite), never per graph. A whole-graph rule cannot survive two components becoming connected by an action g2g never observed. trackmust 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.adoptis 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.Adoptowns both halves of that invariant; do not pairTrackwith a hand-rolled promotion step, and never take the trunk list from the graph as it was before the edge was recorded. untrackmust never reparent the children it strands. Report them.delete,foldandrename(internal/reshape, which reaches Git and the graph and nothing else) act only on branches the g2g graph records and refuse the rest namingg2g track.deleteis 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 tountrackorprune. 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, perlanded.IntoandCherry; on no remote-tracking ref — a local read). None of the three replays a commit:foldonly fast-forwards the parent under a lease, and refuses a trunk (namingg2g land) and a parent that moved on (namingg2g restack). A ref moved under the checkout brings the tree with it throughSwitchTree; a branch another worktree holds is refused for delete and fold, and allowed for rename becausegit branch -mmoves that worktree's HEAD. Steps are ordered so all but the last can be put back, and a rollback that cannot finish exits3. There is nosplit: choosing which commit goes where is interactive and a guess.createtakes 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 throughPlanTrackandApplyTrack, 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 exits3.up/down/top/bottomare the only commands that act without--apply, and that exception is deliberate: moving the checkout changes no ref, record or remote, andgit switchrefuses to clobber local changes. Do not add a preview, and do not extend the exception to anything that writes. They resolve through the samestack.Resolveras 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 mirrorand the two namespacedadopts must never remove a branch from the g2g graph. Alignment keeps the two records in step; it does not transfer ownership.graphite mirrorwrites Graphite only,graphite adoptandgithub adoptwrite 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 adoptadopts a published stack through the same planning (planAdoptions: additive, conflict refusal, parents first) asgraphite adopt; only the record read and the fork point differ. The namespace fixes the source, so neither takes--from.github adopttakes--branchand--scope stack|trunk;graphite adopttakes neither, because Graphite is read whole. It selects through the GitHub source's ownSelectand is the only adopt that invokesgh. It never creates a branch: anything the pull requests place that is not local (Snapshot.Absent, or a base not here) refuses the plan withgit fetch && git switch <branch>/git branch <branch> origin/<branch>. The stack's base must already be recorded or beDefaultBranch— evidence permitting a root, as increate, 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 --parentrequires a tracked parent, and prunes go deepest first — refusing any stranger with a surviving child — becausegt untrackcascades 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.
restackis the only code permitted to rewrite history, and only throughinternal/git's two engines.git replaypreviews exact object ids without moving a ref and applies cleanly without touching the checkout;git rebase --update-refsis 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--detachworkaround silently splits the stack in two.- The replay range is
forkPoint..branch, neverbase..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,
--continuerecomputes from the refs rather than resuming a stored queue, and--abortrestores 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.storeSchemaVersionis separate from the--jsonschemaVersion; an unrecognised store version fails closed. --scope branch|path|subtree|stack|trunk|allis selection, not projection policy. Displaying a subtree does not imply a subtree can be linked on GitHub. The type and the traversal live ininternal/shape, which depends on nothing, because every record answers them andinternal/graphneeds them.- A command must refuse any scope it did not offer, and name its own
default. They genuinely differ:
statusandgithub statusdefault tostackbecause reading is free,restacktosubtreebecause rewriting is not, andallis offered only where nothing is rewritten: the read-only commands, andprune, which forgets landed records and, with--forget-missing, absent selected records.ParseScopetakes 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,landandgithub retargettakestack|pathand refuse a forked selection throughSnapshot.RequireLinear, which names the remedy instead of choosing a line. - Selected from a trunk,
stackis 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. allis deliberately absent fromRewriteScopes: it spans trunks, and a rewrite acts on one.trunkis 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 refuserefuses 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
Describesis answered from the repository's own configuration andSelectis the only call that runsgt. 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;
restackrefuses a branch it has no fork point for and namesg2g 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.
-
githubis the third source and answers only via--from github(ongithub status), never by precedence: reading a base invokesgh, andpushmust 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 linkcovers both creating and repairing the GitHub relationship; there is no separate reconcile command.pullmeans fetch, advance the base, replay. It forgets nothing unless asked with--prune:gt syncprunes as its tail and this does not by default, because forgetting a landed branch is a different question on the same boundary and belongs toprune. -
A diverged base is reported, never merged or reset. Pruning edits the graph; local branch deletion requires explicit
--delete-branches. -
github commentkeeps one marked comment per pull request listing its stack. Readdesign-docs/stack-comment.mdbefore 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 therev=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 linev=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 nov=is the bare list 0.43.0 and earlier wrote. Writes areaddComment/updateIssueCommentby node id with the body as a raw-ffield, 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, exiting3when some were. Local PR observations are supplemental history, never authority for rewriting these comments.submitandlandkeep the comments as their last act throughcomment.Service.Keepunless--no-comment—submitafter opening them,landon the branches above what it landed (land.Plan.Above, the last recipe step) — and a failure there exits3with the command's own work standing.pushnever keeps them (it must never callgh), nor dogithub retargetandgithub link. -
github retargetis the only command a user runs to change what a merge will do, andlandreaches 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 throughgithubstack.Client.Retarget—updatePullRequestmutations by the node idInspectreads, every change in one aliased document,landsending 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 printsgh pr edit, as advice to a person. Do not fold it intosubmitor run it as the tail ofrestack. -
Writes once per pull request go through
githubstack.Client.mutate: one aliased document per twenty, values as raw-fvariables, the answer read per alias even when gh exits non-zero, and only an answer with no data a*CommandErrorfor the whole document. A new per-pull-request write belongs there and inschema_test.go.gh pr createandgh pr mergeare deliberately not batched.
Develop and test
- Keep external process calls behind
internal/subprocess.Runner. Tests must use fakegtandghexecutables onPATH, 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|fishinterface. Dynamic--branchand--trunkcompletion 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 -won changed Go files andgo test ./.... Usego vet ./...when changing Go code or preparing a release. - Use
git hunkfor any staging. Do not commit, tag, push, or invoke realgt/ghmutations unless the user explicitly asks.
Use safely
- For a person who wants an editor workflow,
g2g submit --editopens one temporary JSON document, not a buffer per PR. It retains the document on all failures and after preview; successful--edit --applycleans it up unless--keep-specis present.
Submitting pull requests
-
submitis a preview-first PR creation recovery path. It must never invokegt submit, restack Graphite, or retarget an existing PR. Its--applyboundary validates/revalidates first, publishes throughpush(and so refuses a remote holding work this checkout lacks), creates only missing draft PRs, links the eligible stack only when--linkasks, then keeps the stack comments unless--no-comment. Linking is opt-in because GitHub will not merge a linked pull request throughgh pr merge;landrefuses a linked stack in its preview and namesg2g 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>, completesubmission.json, validate withg2g 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) andrepair(the ways out, each with its command separate from the prose). Readrepairrather than parsing the sentence, and treat a way with nocommandas 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, andschemaVersionsignals breaking changes. Never scrape the pretty graph. -
A blocked preview names the repairing command, decided once in
link.Plan.Repair(arepair.Note) and rendered from it for both readers: merged pull requests point atg2g pullfor a g2g-recorded stack andgt syncfor a Graphite one, landed branches atg2g prune/gt sync, missing or closed ones atg2g submit, and a wrong base atg2g github retarget— neverpull, 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.markedreplaces marks,withMarksenriches them, andlabeledsupplies 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 ispr✓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:
Inspectis a singlegh api graphqlnaming the repository through{owner}/{repo}, and revisions resolve throughgit.Client.ResolveAllin 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, neverDivergence) 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 isCurrency.Rewritten: nothing missing, needs pushing, and not a divergence. -
"Landed" is a content question Git answers and GitHub cannot, and it needs both halves:
Cherryper commit, thenAbsorbedfor the squash merge. A landed branch islink.IssueLanded, neverIssueMissing— the advice for missing isg2g submit, and submitting work already in the trunk is the bug this prevents. What forgets it depends on the source (g2g prunefor g2g's graph,gt syncfor Graphite), so do not hardcode one. -
github statusis 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 statusmay 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 unlinkis the deliberate inverse ofgithub 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-numberto override, previews first, and only--applyinvokesgh stack unstack. It must never alter Graphite, branches, PR content, reviewers, or PR lifecycle. -
After command discovery, use the resolved command's
--helpto inspect the current interface (for example,g2g github --helpafter a Homebrew install). -
Start with
g2g status(offline, one stack) org2g doctor(offline, every stack, only what is wrong); reach forg2g github statuswhen the question is about pull requests. -
Read the preview before any
--apply.pullnever changes GitHub; the commands that do aresubmit,landand everything underg2g githubexceptstatusandadopt.