$ cat node-template.py
S
Sync with base
// Bring a branch up to date by merging another branch into it on GitHub — one merge commit, like GitHub's "Update branch" button. Never rebases or force-pushes. A conflict is reported as a result, with nothing written. Use it on a Lane's branch after another Lane was merged into the working branch, or on the working branch when its base moved. Refuses the default branch and protected branches. Do not run it while a Lane is working on the branch.
Process
Integration
#github#branch#merge#sync#repo-write#vibe
template.py
1"""Sync with base — bring a branch up to date by merging its base into it.23Two moments in a plan need it:45 - after one Lane's pull request is merged into the working branch, the6 other Lanes' branches are behind it, and a Lane whose pull request now7 conflicts has to take in what landed before it can merge;8 - when the base (develop) moves while the plan runs, the working branch9 should take those commits before it ships.1011It makes ONE merge commit on GitHub (`POST /repos/.../merges`) — the same12thing GitHub's "Update branch" button does. It never rebases and never13force-pushes: history is only added to (the Lane's rule, D18). A conflict is14an OUTCOME, not a failure: nothing is written, `result` says `conflict`, and15resolving it is work for the Lane, whose agent can merge and resolve in its16clone.1718## Where it refuses to write1920The repository's default branch and any protected branch: what lands there is21a person's merge. And `expected_head_sha`, when set, pins the write to the22commit the caller last saw — a Lane pushing in between must not have its work23merged over without anyone looking.2425Run it only while no Lane is running on `branch`: a Lane pushes with a plain26`git push`, which GitHub rejects if this node moved the branch under it.27"""2829from gais import github_repo as gh30from gais._errors import RemoteError313233def run(inputs: dict, client: gh.Client) -> dict:34 owner, repo, repo_branch = gh.parse_repo(inputs.get("repo"))35 branch = str(inputs.get("branch") or "").strip() or repo_branch36 base = str(inputs.get("base") or "").strip()37 expected = str(inputs.get("expected_head_sha") or "").strip()38 if not branch:39 raise gh.RepoError("Set the branch to update (`branch`), or wire `repo` from \"Working branch\".repo.")40 if not base:41 raise gh.RepoError("Set `base` — the branch whose commits to bring in, e.g. \"Working branch\".base_branch "42 "for the working branch, or the working branch for a Lane's branch.")43 for name in (branch, base):44 problem = gh.branch_problem(name)45 if problem:46 raise gh.RepoError(problem)47 if branch == base:48 raise gh.RepoError(f"'{branch}' cannot take in itself — set `base` to the branch it came from.")4950 info = gh.repository(client, owner, repo)51 refusal = gh.write_refusal(client, owner, repo, branch, info)52 if refusal:53 raise gh.RepoError(refusal)54 before = gh.branch_sha(client, owner, repo, branch)55 if not before:56 raise gh.RepoError(f"The branch '{branch}' does not exist in {owner}/{repo}.")57 if expected and not before.startswith(expected) and not expected.startswith(before):58 raise gh.RepoError(f"'{branch}' moved since it was checked (it is at {before[:8]}, expected "59 f"{expected[:8]}) — read it again before syncing.")6061 message = f"Merge {base} into {branch}"62 try:63 merged = client.request("POST", f"/repos/{owner}/{repo}/merges",64 {"base": branch, "head": base, "commit_message": message})65 except RemoteError as e:66 if e.http_status == 409:67 return {68 "result": "conflict", "branch": branch, "base": base, "head_sha": before, "merge_sha": "",69 "summary": (f"**{base}** does not merge cleanly into **{branch}**; nothing was changed. "70 f"The conflict has to be resolved on '{branch}' — by its Lane, merging "71 f"'{base}' in its clone, or by a person."),72 "summary_line": f"{owner}/{repo} {base} → {branch}: conflict, nothing written",73 }74 if e.http_status == 404:75 raise gh.RepoError(f"'{base}' was not found in {owner}/{repo}.") from None76 raise gh.explain(e, f"to merge '{base}' into '{branch}'", gh.NEED["contents_write"]) from None7778 sha = (merged or {}).get("sha") or ""79 if not sha: # 204: nothing to merge80 return {81 "result": "up_to_date", "branch": branch, "base": base, "head_sha": before, "merge_sha": "",82 "summary": f"**{branch}** already has everything on **{base}**.",83 "summary_line": f"{owner}/{repo} {branch} already up to date with {base}",84 }85 return {86 "result": "merged", "branch": branch, "base": base, "head_sha": sha, "merge_sha": sha,87 "summary": f"Merged **{base}** into **{branch}** ({sha[:7]}).",88 "summary_line": f"{owner}/{repo} merged {base} into {branch} at {sha[:8]}",89 }909192def main() -> None:93 gh.run_node("sync-with-base", run)949596if __name__ == "__main__":97 main()98$ git log --oneline
v1.0.0
HEAD
2026-10-14First marketplace publish: vibe-coding lanes and the gstack plan flow (PR #1313).