Git flow: agents commit, the engine publishes
Agents never touch a remote. They work and commit on a branch the engine prepared; after the agent’s run the engine pushes the branch, delivers workflow-file changes as a patch, and opens (or reuses) the pull request linked to the issue.
How it works
Section titled “How it works”The publish pipeline is engine-owned end to end, in this order:
- Branch scaffold — before the run, the engine creates or rebases the
feature branch
feature/issue-<n>-<slug>on the base branch and hands it to the agent as the working branch (BranchScaffold). - Agent works and commits only — the agent edits files, runs tests and
commits with
git commit. Anygit pushis denied by the engine’s terminal deny-list before it reaches the shell: the statement is not executed and the agent receives an explanation instead (see below). - Engine finalize — after the run,
ensure_pr(via the state phase’s_pr_finalize):- commits any files the agent left uncommitted,
- pushes the branch,
- delivers changes under
.github/workflows/as aworkflow-patch.diffartifact (the agent token has noworkflowscope, so they can never be pushed directly), - opens a pull request if none exists for the branch and links it to the
issue with
Closes #N.
ensure_pr is idempotent: re-running it finds the existing PR, pushes only
what is missing, and refreshes the patch — it is safe on every re-run.
Automatic issue linking
Section titled “Automatic issue linking”What happens automatically. When the engine opens a pull request for the
run’s branch, the PR body carries a Closes #<issue> line, so merging the PR
closes the issue. This also covers PRs the engine reuses: if a PR already
exists for the branch (or was opened by the agent via gh pr create) and its
body lacks the closing reference, ensure_pr fetches the body and appends
Closes #<issue> with gh pr edit — nothing else in the body is touched, and
a body that already links the issue is left alone (the step is idempotent).
When it applies / limitations. Linking needs the target issue number
(TARGET_NUMBER); on workflow_call re-runs it is only present when the
caller sets it. nebuko init setup pull requests are never linked — they are
not issue-driven. If the PR’s base is not the repository’s default branch,
GitHub shows the Closes #N reference on the issue timeline but may not
auto-close the issue on merge.
How to verify. Open the PR body and check for the closing line, or check the issue’s timeline for the linked pull-request reference.
Branch switching is denied for agents
Section titled “Branch switching is denied for agents”The terminal deny-list also blocks every statement that would move the
session off the scaffolded branch: git checkout (with or without -b),
git switch, git branch <name> and the mutating git branch options
(-d/-D, -m/-M, -c/-C, -f, --set-upstream-to, …). Read-only
forms stay allowed (issue #1268): git branch alone, --show-current,
-a/-r/-vv, --list <pattern>, --contains/--merged <commit>, and
git checkout [<tree-ish>] -- <path>…, which restores files without moving
HEAD. The engine owns branch lifecycle —
it scaffolds the branch before the run and publishes it after finalize —
so a mid-session switch strands the agent’s commit on a branch finalize
does not know about (run 41354010528).
How to use it:
- For agents: stay on the branch the prompt gives you. To inspect another
branch you do not need to check it out —
git log <ref>,git show <ref>,git diff <ref>andgit ls-tree <ref>all work from the scaffolded branch. - If a statement is denied you see: “the session must stay on the
scaffolded branch; the engine owns branch lifecycle and publishing.
Inspect other branches read-only with
git log/show/diff/ls-tree <ref>— no checkout needed.”
The guard is a regex on each submitted statement, same as the git push
rule: obfuscated invocations (a script or alias that itself switches
branch) are not caught. Finalize’s branch check remains the last line of
defense and still fails the session loudly if the working branch was
somehow changed.
The engine holds up its own end before the agent starts (issue #1268): the
scaffold checks out the feature branch on every path — created, merged, or
continued as-is when the base is already contained or merges cleanly — and
fails the run if HEAD is not on it afterwards. ensure_pr refuses to
publish from the base branch, and its failure detail always names the
exception type (error: <ExcType>: <message>).
How to use it
Section titled “How to use it”For agents:
- Work on the branch the prompt gives you. Do not create branches.
- Commit your work locally with clear messages. Stage only the files you changed — never generated files, caches, or secrets.
- Never run
git push, and never open or comment on pull requests. The engine pushes and opens the PR after your run.
For operators:
- A denied statement appears in the agent’s tool output as a failed entry with the message: “git push is disabled for agents: the engine commits leftovers, pushes the branch and opens the pull request after your run. Commit your work locally instead.” The batch continues with the remaining entries.
- Push and PR creation are driven by the engine’s
GH_TOKEN/GITHUB_TOKENcredentials (GitSessionGuardproves the auth before the session and re-proves it before each push). - If finalize reports push-auth problems, check that the token is present,
has
repo(and, for workflow files,workflow) scope, and is not expired; then re-run —ensure_prpicks up where it stopped.
Limitations & troubleshooting
Section titled “Limitations & troubleshooting”- The deny-list is a regex check on each submitted statement, scoped to
git push. Obfuscated invocations (e.g. through a script or an alias that itself pushes) are not caught; a gitpre-pushhook and credential isolation are recorded as future hardening. gh pr createand branch creation by agents are discouraged by instructions but not hard-blocked; the hard block coversgit pushonly, per the issue’s scope.- If the agent sees deny messages but the branch still ends up unpushed,
the publication guard recalls it for uncommitted changes only; anything
push-related is the engine’s job, and finalize’s
ensure_prfallback handles the rest.