Git + Worktree Reference (AEP)
/aep-launch, /aep-build, and /aep-wrap operate on a plain git repository plus git worktree for parallel agent isolation — when these skills say "commit", they mean git commit. This skill is the canonical home for AEP's git conventions: every other skill resolves $BASE, creates/removes worktrees, and opens PRs by pointing here, never by re-inlining these blocks. Rationale for plain git (agent training data, universal tooling): docs/decisions/migrate-from-jj-to-git.md.
Integration Branch
Every AEP git operation targets the integration branch — the branch feature worktrees are
created from, rebased onto, PR'd into, merged into, and where control-plane commits (/aep-dispatch,
/aep-design, the /aep-wrap archive) land. Throughout this skill and the bash blocks in /aep-launch,
/aep-build, /aep-wrap, and the product-context skills, this branch is referred to as $BASE.
AEP auto-detects one of two modes — no configuration required for the common cases:
| Mode | Condition | Integration ($BASE) |
Production |
|---|---|---|---|
| single-branch (default) | no develop branch |
main |
main (same branch) |
| two-branch | develop exists |
develop (staging) |
main (promote-only) |
A project grows from single- to two-branch mode simply by creating a develop branch — no
reconfiguration. In two-branch mode AEP never touches the production branch (main): promotion
from integration to production (develop → main) is the user's concern, exactly like deployment,
which is already outside AEP's scope.
Resolving $BASE
Every git-touching skill resolves the integration branch at the top of its first bash block:
# Resolve AEP integration branch ($BASE): override → auto-detect develop → main
BASE=$(git config --get aep.integration-branch 2>/dev/null || true)
if [ -z "$BASE" ]; then
if git show-ref --verify --quiet refs/heads/develop \
|| git show-ref --verify --quiet refs/remotes/origin/develop; then
BASE=develop
else
BASE=main
fi
fi
Resolution order:
- Explicit override —
git config --get aep.integration-branch. This is an override only, for a non-standard integration branch name (e.g.staging,integration). You do not set it for the standardmain/developcases — those are auto-detected./aep-onboardreports the detected mode and only writes this key when you use a non-standard name (git config aep.integration-branch <name>; unset withgit config --unset aep.integration-branch). - Auto-detect —
developif it exists locally or onorigin. - Default —
main(single-branch mode).
Because the standard cases are auto-detected (not pinned), a project grows from single- to
two-branch mode with no reconfiguration: create develop and every skill resolves $BASE to it
on the next run. (If you had pinned aep.integration-branch=main, that override would win and
suppress the upgrade — which is why setup does not pin the default.)
git config --get aep.integration-branch is repo-local and shared across all worktrees via the
common .git/config, so $BASE resolves identically in the main session and inside any
.feature-workspaces/<name> worktree.
Worktree Lifecycle
Create
# Resolve $BASE first — see "Resolving $BASE" above
mkdir -p .feature-workspaces
git worktree add -b feat/<name> .feature-workspaces/<name> "$BASE"
- Path is always under
.feature-workspaces/<name>(kept gitignored). - Branch is always
feat/<name>— corresponding to the OpenSpec change name or story id. - Base is always the integration branch
$BASE.
The worktree shares .git/objects with the main repo, so creating it is fast and history is not duplicated. Only the working tree files are duplicated on disk.
Inspect
git worktree list # all worktrees, branches, paths
git -C .feature-workspaces/<name> status # status inside a specific worktree
git -C .feature-workspaces/<name> log --oneline "$BASE"..HEAD # commits unique to the feature branch
Remove (/aep-wrap step 6)
git worktree remove .feature-workspaces/<name>
git branch -d feat/<name>
If git branch -d warns the branch isn't fully merged (likely because the PR was squash-merged so commit SHAs differ), force with git branch -D feat/<name> after confirming via gh pr view <number> --json state that the PR is MERGED.
Recover from a corrupt worktree directory
If .feature-workspaces/<name>/ was deleted manually:
git worktree prune # forget the orphaned registration
git branch -D feat/<name> # delete the orphan branch (if needed)
If .git/worktrees/<name>/ still references a missing path (e.g. you moved the parent directory):
git worktree repair .feature-workspaces/<name>
Branch Naming
| Branch | Pattern | Created by |
|---|---|---|
| Feature work | feat/<short-name> |
/aep-launch (one per story) |
| Migration / chore | chore/<short-name> |
manually, when applicable |
| Hotfix off main | fix/<short-name> |
manually |
| Migration project | migration/<topic> |
manually |
Rules:
- Use kebab-case after the slash. No spaces, no underscores.
- Keep names short (≤ 30 chars) — they appear in tab labels, signal files, and PR titles.
- Reuse a deleted branch name once its worktree is fully removed (
git worktree pruneclears the dangling refs).
The One-Commit-per-Task Pattern (Phase 4 of /aep-build)
What
tasks.md lists N tasks. The feature branch ends up with N commits — one per task — in the same order. Conventional-commit format. Workspace agents implement linearly, committing after each task, so the commit count matches the task count:
# Implement task 1
# ... edit files ...
git add -A
git commit -m "feat(auth): extract auth service"
# Implement task 2
# ... edit files ...
git add -A
git commit -m "feat(auth): add token refresh flow"
# ... etc.
After each commit, record the short SHA in .dev-workflow/feature-verification.json against the matching task entry's commit_sha field.
Why this shape
- The PR's commit list reads like a table of contents matching
tasks.md. - Each commit is a self-contained unit that the evaluator (in Phase 5) and reviewers (in Phase 11) can
git show <sha>independently. - We squash-merge at PR-merge time, so per-commit hygiene only matters for review readability — the integration branch's history stays clean automatically.
- It removes any need to rewrite history mid-stream, which is where agents most often go off the rails.
What to do when something goes wrong
| Situation | Action |
|---|---|
| Forgot a file in the just-committed task | git add <file> && git commit --amend --no-edit (only safe before the next task's commit) |
| Realized the previous task is broken, several commits ago | Add a new commit: fix(<scope>): correct <issue from task N> — the history stays append-only |
| Review feedback or eval-loop FAIL | Add a follow-up commit: fix(<scope>): address review on <topic> |
Need to update against new origin/$BASE |
git fetch origin && git rebase origin/"$BASE" && git push --force-with-lease origin feat/<name> |
| Conflicts during rebase | Resolve in working tree, git add <files> && git rebase --continue. If hopelessly tangled, git rebase --abort and surface to the orchestrator via signal file. |
Push a rewritten branch with --force-with-lease rather than a bare --force: the lease variant fails safely when someone else has pushed since your last fetch.
Publishing & PR Conventions
Push the feature branch
# First push (after Phase 9 cleanup):
git push -u origin feat/<name>
# Subsequent pushes (review fixes, rebases):
git push origin feat/<name>
# or after a rebase:
git push --force-with-lease origin feat/<name>
Open the PR — always set base explicitly
# $BASE is the integration branch — resolve it first (see "Integration Branch" above)
gh pr create --base "$BASE" --title "<title>" --body "<body>"
The --base "$BASE" flag is mandatory: without it gh infers the base from local branch state (a dispatch commit can look like a recent base), the PR merges into a non-integration branch, and the code never lands on $BASE. In two-branch mode an inferred base could be production main, which AEP never merges feature work into.
Merge the PR
gh pr merge <number> --squash --delete-branch
We always squash-merge. The feature branch's per-task commits collapse into one commit on the integration branch ($BASE), which keeps its log readable while preserving the per-task review trail in the PR's "Commits" tab.
Control-Plane Commits (on the integration branch)
/aep-design, /aep-dispatch, /aep-envision, /aep-map, /aep-calibrate, /aep-validate, /aep-reflect, and the /aep-wrap archive step all commit directly to the integration branch ($BASE). The pattern is identical:
# Resolve $BASE first — see "Resolving $BASE" above
git pull --ff-only origin "$BASE" # fail-fast if the integration branch has diverged
git add <specific-files> # never -A on the integration branch; be explicit
git commit -m "<conventional message>"
git push origin "$BASE"
The --ff-only is intentional — it refuses a non-fast-forward pull, which means concurrent pushes get caught instead of silently merged. If the pull fails because someone else pushed first, fetch, rebase your work, and try again.
Recovery
Lost a commit (committed to wrong branch, or amend'd away)
git reflog # find the orphaned commit's SHA
git cherry-pick <sha> # bring it onto the current branch
# or
git reset --hard <sha> # if you want to rewind to that point
Lost an entire branch (deleted with -D)
git reflog # the deleted branch's tip is in here
git branch <name> <sha> # recreate the branch at that tip
Lost work in a worktree directory you removed
If you ran rm -rf .feature-workspaces/<name> without committing first, the work is gone — git worktree only tracks committed history. .dev-workflow/ files (signals, lessons, progress) are also gone since they're gitignored. Use git worktree remove instead of rm -rf next time; it refuses if the worktree has uncommitted changes.
OpenSpec files missing after rebase
If the dispatch commit's OpenSpec files disappeared during a rebase or merge:
git log --oneline -n 30 # find the dispatch commit SHA
git restore --source=<sha> -- openspec/
This restores openspec/ to the state of the dispatch commit without touching anything else.
Disk Budget
Each worktree adds approximately one full working-tree copy on disk. .git/objects is shared across all worktrees, so commit history isn't duplicated. For a typical AEP repo (~100 MB working tree, multi-GB .git/objects after years of history), three concurrent agents adds ~300 MB on top of one shared .git/.
Monitor with:
du -sh .feature-workspaces/ # working-tree footprint
du -sh .git/objects # shared history
If disk pressure hits before agents finish, prefer pausing new /aep-launch invocations over forcibly removing in-flight worktrees.
Cheat Sheet
| You want to… | Run |
|---|---|
| Create a feature worktree | git worktree add -b feat/<n> .feature-workspaces/<n> "$BASE" |
| List all worktrees | git worktree list |
| See feature-branch commits | git log --oneline "$BASE"..HEAD |
Sync against latest $BASE |
git fetch origin && git rebase origin/"$BASE" |
| Push after rebase | git push --force-with-lease origin feat/<n> |
| Open PR | gh pr create --base "$BASE" |
| Merge PR | gh pr merge <#> --squash --delete-branch |
| Remove worktree post-merge | git worktree remove .feature-workspaces/<n> && git branch -d feat/<n> |
| Recover deleted commit | git reflog then git cherry-pick <sha> |
| Restore lost openspec/ files | git restore --source=<sha> -- openspec/ |