Design and rules
For contributors changing the skills or scripts, and for agents that need the why behind a rule. The rules are deliberate, and most were settled by a test; How it was validated has the evidence.
The shape: one flow over a set of repos
The skills have no per-layout sections. They say "each repo in the set", and a single repo is a set of one. Everything
layout-specific lives behind one front door, repos.sh (twin: win/repos.ps1), which picks a strategy and dispatches
each verb to it:
skills (judgement: what to change, scope, review, recovery)
│ "run repos.sh <verb>, act on its rows"
▼
_shared/scripts/posix/repos.sh → strategies/single.sh today's behaviour
→ strategies/submodules.sh these rules
→ strategies/<future>.sh a new layout is a new file, not a skill edit
- The strategy is explicit:
JST_MEMBERSset → submodules, else single. It's never detected from.gitmodules, because an explicit list fails safe: upgrading the plugin can't change a project that vendors libraries as submodules, and a newly added vendored submodule can't silently become a member..gitmodulesvalidates the list instead. - Single-repo projects go through the scripts too, guarded by a regression test that compares statuscheck's whole table against the base commit's.
- Scripts stop, the model recovers. A verb never resolves a conflict, guesses a base or retries a failed push: it prints the row and exits non-zero.
- A command the model must run sits where it's used. In testing, a reviewer twice took the PR from statuscheck's
current_prrow instead of runninggate, two sections down; it only rangateonce the command was inside each phase check.
The model
- The workspace holds
.jst/, the project docs, and one gitlink per member. Its gitlinks mean "the last set of member commits integrated together", so a workspace commit is a freeze point. - The issue branch has one name, in the workspace and in every member the issue touches.
- During the work, members follow their issue branches, not the workspace's gitlinks. The gitlinks move once, at landing.
- The project root is the outermost repo, up the submodule chain, that has
.jst/. Every script finds.jst/that way (_jst_root.sh), so a skill run from inside a member still configures itself from the workspace.
Rules
Checkouts
- An issue checkout is a git worktree, not a clone. Worktrees work with submodules. A clone would lose what the
plugin relies on: worktree detection, shared
parentbranchconfig, and git's refusal to check one branch out twice. - Removing a worktree needs
--force, which also skips git's own safety check and deletes the members' copies, unpushed commits included. Sorepos.sh remove-checkcomes first, and no skill removes a worktree itself. - A worktree's members have their own git directories (
.git/worktrees/<wt>/modules/<name>). Config set in the main checkout's member doesn't reach them, soprovisionrecordsbranch.<b>.parentbranchinside the worktree's copy. - A credentials copy never lands in a member. A submodule's
.gitis a file, like a linked worktree's, so "is.gita file" alone misreads a member as a worktree. The scripts check where it points (…/worktrees/…vs.…/modules/…).
Scope and the member step
- A member is in scope when its issue branch exists on its origin. The assigner creates the branches eagerly, and any later run, or a fresh clone, re-derives the same scope from GitHub. Nothing is stored in a comment or a file.
- The member step (
provision):submodule update --init --recursive(everything at its recorded commit, nested submodules too), then each in-scope member is fetched and switched to its issue branch, then its own submodules are updated. - Nothing is ever committed to
.gitmodulesfor an issue. Freezing a member's branch in.gitmoduleswas tried, and amerge=oursrule to keep it out of the base doesn't work: a merge driver only runs when both sides changed the file, and GitHub's merge button doesn't run custom drivers. - Never combine
--remotewith--recursive.--remoteis passed down, and moves pinned nested submodules to their branch tip. - Never run a bare
git submodule updatein an issue worktree. It puts every member back on the workspace's gitlink, off its issue branch. Re-runprovisioninstead, and don't setsubmodule.recurse=true.
Commits, pushes and PRs
- Members commit and push on the issue branch. The workspace gets commits during the work only for its own files. It never moves a gitlink before landing.
push.recurseSubmodules=checkmakes git refuse a workspace push that points at a member commit no remote has.- One PR per member with commits; none for the workspace before landing. Its own commits ride the landing PR. Every
PR's body carries a marked PR set section, kept current by
open-prs.
Landing, and why squash merges matter
A gitlink is a commit SHA. If a member PR is squash-merged (or rebase-merged), GitHub writes a new commit on the base, and the PR's own commit is on no branch once its branch is deleted. A workspace pointing at that commit breaks: a fresh clone can fetch it only until the server garbage-collects it ("upload-pack: not our ref").
- The workspace's gitlinks only ever point at commits on each member's base, never at issue-branch commits. This makes the squash problem impossible, whatever merge method the members use.
- Landing (
land): for each member PR, take its merge commit, check it's onorigin/<base>, move the gitlink there, commit, push, and open the landing PR. Everything is checked before anything changes. - The reviewer's gate on the landing PR (
land --check): every gitlink is on its member's base, and.gitmodulesis unchanged. - Done is the landing PR's merge, which is why merge → Done automation must be scoped to the workspace repo.
- A squash-merged branch is still safe to remove:
remove-checkaccepts a commit on no remote when it's the head of a merged PR, or an ancestor of that head.
The reviewer and the executor
- PRs are read through
repos.sh, never a hand-typedgh pr list.ghanswers a filter that misses with[], not an error, so a narrowed query reads an open PR as none. The script asks the one exact query (bare--head, every state, no--base) twice before it saysnone, and aghfailure iserror: unknown, never none. - Every
gh prcommand takes-R <slug>, in single-repo mode too, so the prose has no layout branch. - The reviewer never sources a project's secrets to run its tests. It reports what it couldn't run.
Limits and what's next
- Multistep issues across members need per-member parent branches and PRs before landing, a third level. Every skill stops on that combination instead of guessing.
- Hotfixes across members need each member's production branch as the PR base, which isn't modelled (members have a base only).
- Other layouts (git-ignored clones, manifests) would each be one more
strategies/file (Other layouts).
How it was validated
Git behaviour, tested on git 2.43 on throwaway repos with local remotes:
| Check | Result |
|---|---|
submodule update --init in a linked worktree | Works; member gitdirs under .git/worktrees/<wt>/modules/ |
git worktree remove with submodules | Refused; --force works and removes the member copies |
git config inside a worktree | Shared with the main checkout and every other worktree |
submodule.<name>.branch in config vs. .gitmodules | Config wins |
branch = . with a member lacking the branch | update --remote fails |
.gitmodules merge=ours, only the feature changed it | The feature's version is taken; the driver doesn't run |
| A squash-merged commit, its branch deleted, before / after gc | Fetchable / "not our ref" |
push.recurseSubmodules=check with an unpushed member commit | Push refused |
update --init --recursive --remote with a pinned nested submodule | The nested one moves off its pin |
The scripts: scripts/check-repos.sh --ports runs 208 checks in both ports. They cover:
- the project-root rule from 11 locations
- the single-repo regression against the base commit
- every
repos.shverb, against a fakeghand real local git (including a real squash merge thenland) - the healthcheck rows
scripts/check-ensure-local-env.sh --ports covers credentials never landing in a member.
The skills, run headlessly against a demo workspace (an app and its server) and a single-repo project:
- a cross-repo issue reviewed, landed after squash merges, and merged
- a member pushed with no PR, which stops the run
- a broken contract, rejected under dimension 7
- a rejected issue fixed by the executor, then approved and landed
- a plain task description taken through assigner → executor → reviewer → landing → Done with no hand steps but the merges
- single-repo single-step and multistep runs, unchanged