Skip to content

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.

The publish pipeline is engine-owned end to end, in this order:

  1. 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).
  2. Agent works and commits only — the agent edits files, runs tests and commits with git commit. Any git push is 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).
  3. 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 a workflow-patch.diff artifact (the agent token has no workflow scope, 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.

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.

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> and git 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>).

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_TOKEN credentials (GitSessionGuard proves 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_pr picks up where it stopped.
  • 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 git pre-push hook and credential isolation are recorded as future hardening.
  • gh pr create and branch creation by agents are discouraged by instructions but not hard-blocked; the hard block covers git push only, 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_pr fallback handles the rest.