$ cat node-template.py
W
Working branch
// Create the plan's working branch in a GitHub repository, off a base branch, and hand every downstream node the repository to work in. Lanes branch off it and open their pull requests back into it; the Codebase map reads it. Re-running reuses an existing branch and never moves it. Needs a GitHub connection that can write to the repository.
Process
Integration
#github#branch#repository#lane#vibe
template.py
1"""Working branch — the plan's branch, created first, used by everything after.23A vibe-coding plan works on ONE branch off the base: every Lane branches off it4and opens its pull request back into it (the Lane passes its `repo.base_branch`5to both `git clone --branch` and `gh pr create --base`), and the Codebase map6reads the repository at it. When the lanes are merged, the working branch ships7to the base as one pull request. That is why it is created before anything8else: every downstream node needs a concrete repository and ref.910## It never moves a branch1112If the working branch already exists it is REUSED, at whatever commit it is on.13It is never reset to the base and never force-updated — it may already hold14merged lane work, and a rerun of this node must not be able to erase that.15This is the same rule the lanes follow (D18: no force-push, ever).1617## Credentials1819`GITHUB_TOKEN` comes from the user's GitHub connection, injected by the20executor for every canvas node the user runs, and is reached ONLY through21`gais.connectors.github` — the sandbox blocks network imports in template22code, and the connector keeps the token out of every error message.23"""2425import json26import re27import sys28import traceback2930from gais import connectors31from gais._errors import ConfigError, RemoteError3233# Git's own rules for a ref name, reduced to what a working branch needs.34# Validated here rather than left to GitHub so the error says what is wrong35# with the NAME instead of an opaque 422.36BRANCH_OK = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._/-]*$")373839class BranchError(Exception):40 """A failure the user can act on. The message is shown as-is."""414243def branch_problem(branch: str, base: str | None = None) -> str | None:44 if not branch:45 return "The working branch name is empty."46 if not BRANCH_OK.match(branch):47 return f"'{branch}' is not a valid branch name: use letters, digits, '.', '_', '-' and '/'."48 if ".." in branch or "//" in branch or branch.endswith("/") or branch.endswith(".lock"):49 return f"'{branch}' is not a valid branch name."50 if base and branch == base:51 return (f"The working branch cannot be the base branch '{base}' itself — the plan's work "52 "needs its own branch so it can ship as one pull request.")53 return None545556def call(method: str, path: str, body: dict | None = None):57 """(status, body) — the only GitHub call, so tests replace it.5859 A non-2xx is RETURNED as its status rather than raised: this node's logic60 branches on 404 / 422 / 403, and each means something different to say.61 """62 try:63 return 200, connectors.github.request(method, path, json_body=body)64 except RemoteError as e:65 return e.http_status, {}666768def ref_path(owner, repo, branch):69 # No URL-encoding needed: `branch_problem` admits only letters, digits,70 # '.', '_', '-' and '/', all safe in a path — and GitHub's ref API wants71 # the slashes of `feature/x` literal.72 return f"/repos/{owner}/{repo}/git/ref/heads/{branch}"737475def ensure_branch(owner: str, repo: str, base: str, branch: str, call=call) -> dict:76 """Create `branch` off `base`, or reuse it. Returns the node's outputs."""77 status, info = call("GET", f"/repos/{owner}/{repo}")78 if status == 404:79 raise BranchError(f"Repository {owner}/{repo} was not found, or your GitHub connection cannot see it.")80 if status >= 400:81 raise BranchError(f"GitHub answered {status} for {owner}/{repo}.")82 base = base or info.get("default_branch") or ""83 if not base:84 raise BranchError(f"{owner}/{repo} has no default branch; set the base branch explicitly.")85 if not BRANCH_OK.match(base) or ".." in base:86 raise BranchError(f"'{base}' is not a base branch this node can use.")8788 problem = branch_problem(branch, base)89 if problem:90 raise BranchError(problem)9192 status, ref = call("GET", ref_path(owner, repo, base))93 if status == 404:94 raise BranchError(f"The base branch '{base}' does not exist in {owner}/{repo}.")95 if status >= 400:96 raise BranchError(f"GitHub answered {status} reading the base branch '{base}'.")97 base_sha = (ref.get("object") or {}).get("sha", "")9899 status, created = call("POST", f"/repos/{owner}/{repo}/git/refs",100 {"ref": f"refs/heads/{branch}", "sha": base_sha})101 if status == 200:102 head_sha, was_created = (created.get("object") or {}).get("sha", base_sha), True103 elif status == 422:104 # Already exists. Reuse it exactly where it is — never reset it to the105 # base: it may hold lane work that has already been merged into it.106 status, existing = call("GET", ref_path(owner, repo, branch))107 if status >= 400:108 raise BranchError(f"'{branch}' exists but could not be read (GitHub answered {status}).")109 head_sha, was_created = (existing.get("object") or {}).get("sha", ""), False110 elif status in (403, 404):111 raise BranchError(f"Your GitHub connection cannot create branches in {owner}/{repo}.")112 else:113 raise BranchError(f"GitHub answered {status} creating '{branch}'.")114115 return {116 "repo": {"owner": owner, "repo": repo, "base_branch": branch},117 "branch": branch,118 "base_branch": base,119 "head_sha": head_sha,120 "created": was_created,121 "url": f"https://github.com/{owner}/{repo}/tree/{branch}",122 }123124125def main():126 try:127 envelope = json.loads(sys.stdin.read() or "{}")128 inputs = envelope.get("inputs", {}) if isinstance(envelope, dict) else {}129 owner = (inputs.get("owner") or "").strip()130 repo = (inputs.get("repo") or "").strip()131 base = (inputs.get("base_branch") or "").strip()132 branch = (inputs.get("branch") or "").strip()133 if not owner or not repo:134 raise BranchError("Set the repository owner and name.")135 connectors.github.require()136 out = ensure_branch(owner, repo, base, branch)137 print(f"[working-branch] {owner}/{repo} {out['branch']} off {out['base_branch']} "138 f"{'created' if out['created'] else 'reused'} at {out['head_sha'][:8]}", file=sys.stderr)139 json.dump(out, sys.stdout)140 except (BranchError, ConfigError) as e:141 # ConfigError is "no GitHub connection reached this node" — already142 # phrased for a user by the connector, naming where to add one.143 print(json.dumps({"error": str(e), "errorType": type(e).__name__}), file=sys.stderr)144 sys.exit(1)145 except Exception as e:146 print(json.dumps({"error": str(e), "errorType": type(e).__name__,147 "traceback": traceback.format_exc()}), file=sys.stderr)148 sys.exit(1)149150151if __name__ == "__main__":152 main()153$ git log --oneline
v1.0.0
HEAD
2026-10-14First marketplace publish: vibe-coding lanes and the gstack plan flow (PR #1313).