Drive gitq with agents
gitq is a CLI you can drive by hand, and it is a CLI built to be driven by an agent. Four Claude skills wrap it: gitq:sync, gitq:publish, gitq:absorb, and gitq:restructure. Each owns exactly one action the board can launch, and each one splits the work the same way: gitq does the git mechanics, deterministically, and the agent supplies the judgment gitq deliberately does not have an opinion about.
What each skill owns
| Skill | CLI mechanics | Judgment the skill adds |
|---|---|---|
gitq:sync | gitq sync, gitq continue, gitq abort | Reading a conflict's both sides and merging them for real, not picking one side mechanically. |
gitq:publish | gitq publish --mr-meta <path> | Writing MR titles and descriptions from the actual diff, deciding which already-published MRs should have their prose overwritten (an --mr-meta entry for one replaces what is on the forge), and holding the gate before anything reaches it. |
gitq:absorb | gitq absorb --preview, gitq absorb | Sanity-checking the file→branch attribution before committing to it; gitq's attribution is mechanical (git diff --name-only <parent> <branch>) and can be wrong on a surprising file. |
gitq:restructure | gitq split, fold, reparent, rename, reset | Turning a plain-language instruction ("split the UI work out of feat-core") into the right sequence of surgery commands, and gating the whole plan before executing any of it. |
None of them mutate git directly. All four run the real gitq CLI for every git operation and only exercise judgment in the parts gitq's own conflict/attribution/mapping logic hands back for a human (or an agent standing in for one) to decide.
Installing them
mattstack.app ships the skills alongside the bundled gitq CLI, and its setup links them into ~/.claude/skills (the "Link bundled skills" step), so /gitq:sync and the others are available as slash commands in any Claude Code session, not just ones launched by the board. With the app installed there is nothing to run, and an app update brings skills that match the CLI it ships.
From a checkout
Working on the skills themselves, link them to your checkout instead:
bun run scripts/install-skills.ts
This symlinks every skills/<dir> whose SKILL.md has a name: frontmatter field into ~/.claude/skills/<name>. It is checkout-only, and it is idempotent: re-running it after a git pull that changed the skills just re-links. It replaces any existing symlink at the destination, the app's links included, and the app's setup leaves a link it did not make alone, so the checkout links stay until you remove them. It refuses (with a message, not a crash) if something other than a symlink already occupies the destination path. Pass a different destination directory as the first argument if you don't want ~/.claude/skills.
The flag contract
Every skill accepts the same shape:
/gitq:<action> <repoPath> <stackName> [--state <path>] [--status-bin <path>]
<repoPath> and <stackName> are positional and required. --state and --status-bin are both optional, and they travel together: given either, both are expected. gitq:restructure has one extra positional that the other three don't: an optional [instruction] between <stackName> and the flags, the plain-language description of what to restructure.
gitq:absorb's <repoPath> is not necessarily the primary checkout. It can be any worktree of the repo, because absorb sources its uncommitted changes from wherever you point it. The board's absorb menu passes whichever dirty worktree you picked; run it by hand against a sibling worktree the same way.
What --state and --status-bin do
--status-bin <path> is the absolute path to the gitq executable itself, whose job-status verb writes the job status. From a checkout that is bin/gitq; from the compiled binary it is that binary's own path, so a mattstack.app update that replaces the bundle leaves later panes reaching the new one. --state <path> is a per-job JSON file the board reads to render its job chips. When both are given, a skill writes its progress by running:
<status-bin> job-status <state> <status> [detail]
with <status> one of five lifecycle values: starting, working, conflict, done, error. starting is special: the board itself writes it, twice: once to seed the job file before the pane even spawns (so a crash-on-launch still shows up as a job, not silence), and again once the herdr tab exists to record tabId/workspaceId. Every status after that is the skill's own: working right after it starts, conflict if it hits one, and always a terminal done or error before it finishes (every skill's Rules section calls out ending on a terminal status so a board badge can never get stuck mid-run), except while genuinely parked at a human gate with no answer yet.
When --state/--status-bin are absent, a skill skips every status write and just talks to you. That's what happens invoking any of the four by hand from an interactive Claude Code session: /gitq:sync /Users/you/repos/myrepo demo, no flags, and it runs the exact same steps, it just narrates them in the conversation instead of updating a file. Nothing else about the skill's behavior changes.
How the board wires it up
Right-clicking a stack action on the board does three things, in order (src/server/server.ts's /action handler, src/server/herdr.ts's actionPrompt):
- Seeds the job file at a deterministic path derived from
(repoPath, stack, action), statusstarting, before anything is spawned. - Builds the slash command:
/gitq:<action> <repoPath> <stack> --state <statePath> --status-bin <path/to/gitq>, and launches a herdr tab runningclaudewith that as the initial prompt. - Updates the job file with the new tab's id once the pane exists.
From there the skill takes over and the board just polls: /data.json reads every job file under <app root>/state/jobs/, so as the skill runs working → maybe conflict → done, the badge on the board updates without anything else in the loop. Relaunching an action that's already live re-focuses that tab instead of spawning a duplicate: the dedup key is the same (repoPath, stack, action) triple.
Job files older than 24 hours are pruned, but only once they've reached a terminal status; one parked at conflict (or sitting at a gate) keeps its file indefinitely, exactly because that's the state a human still needs to see. See The board and Where state lives.
A worked example: gitq:restructure by hand
Say a branch has grown one commit of pure cleanup that really belongs one level down. Invoked directly, with the instruction as the third positional and no board flags:
/gitq:restructure /Users/you/repos/myrepo demo "fold feat-core-docs into feat-core, it's just cleanup"
What the skill does, matching its own steps:
- Reads the intent straight from the instruction positional: no ambiguity here, so no clarifying question.
- Learns the current shape:
gitq -C /Users/you/repos/myrepo stacks --jsonandgitq -C /Users/you/repos/myrepo diagnose --json, plusgit log --oneline <parent>..<branch>on the branch in question, to see there really is nothing but cleanup on it. - Maps the instruction to one operation:
gitq fold feat-core-docs --stack demo. - Gates. Before running anything it shows you the plan (
fold feat-core-docs into feat-core; feat-core-docs's children get reparented onto feat-core) and the caveat that matters:foldis not reversible bygitq undo(onlyreparentis), so this is a one-way door. It waits for your yes. No--state/--status-binhere, so there's no board badge sitting at this gate: the pane itself is the gate, and if you never answer, nothing executes and the conversation just sits there. - Executes only after approval: runs
gitq fold feat-core-docs --stack demo --json, checks the result. - Verifies: a final
gitq diagnose --jsonto confirm the tree is healthy, then reports the new shape back to you in the conversation (since there's no status file to write to).
The same gate exists identically when the board launches this skill; the only difference is where the plan and the yes/no happen: in a herdr pane either way, just one the board spawned versus one you opened yourself.
Where the docs are occasionally wrong
These skill files are the prompts actually driving the agents, so a detail in one can fall behind the code underneath it. Where that has happened, the mismatch is called out here rather than left to mislead you from inside the skill:
skills/restructure/SKILL.md on checkout neutralityIts step 3 says "surgery never moves the launch worktree's checkout." True for split, reparent, rename, and reset, but there is one exception, and this skill drives it.
fold deletes the branch it folds, so if your launch worktree has that branch checked out (clean), gitq switches it to the parent before deleting (src/core/branch-fold.ts). Nothing else in the surgery set moves a checkout: reset, which used to check the branch out and leave you there, is a compare-and-swap on the ref now (src/core/branch-reset.ts), so gitq reset feat-b while you are on feat-a leaves you on feat-a. See Restructure a stack.
Next
- The board: where these skills get launched from, and what the badges mean.
- Resolve a conflict: the conflict loop
gitq:syncandgitq:restructure's reparent path both follow. - Publish a stack: what
gitq:publishgates before it runs. - Absorb uncommitted changes and Restructure a stack: the CLI mechanics behind the other two skills.