Card 14: fresh work branches start from fetched origin/main

A fresh ▸ start work used to branch from whatever HEAD the board's
checkout happened to hold, so anything merged on GitHub or pushed by a
teammate was missing from the agent's world. Now, when an origin remote
exists, the launch fetches origin/main (bounded by BOARD_FETCH_TIMEOUT,
default 10s) and branches from it — without ever touching the main
checkout itself. No remote, a failed fetch or a timeout all fall back
to branching from HEAD exactly as before: launching is never blocked
by network weather.

The ticker narrates honestly either way: when origin/main is ahead of
the local checkout the launch line says by how much, and when the fetch
had to be skipped it says that too.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
istos
2026-07-30 07:25:49 +02:00
co-authored by Claude Fable 5
parent 12a7d1f1c5
commit 4a91e5a841
4 changed files with 184 additions and 5 deletions
+6
View File
@@ -33,6 +33,12 @@ BOARD_GH_BIN=gh
BOARD_GIT_REMOTE=
BOARD_PR_POLL_INTERVAL=60
# Seconds a work-agent launch waits for `git fetch origin main` before
# giving up and branching from local HEAD. Fresh launches branch from
# origin/main when the fetch succeeds; no remote or a dead network just
# means today's behaviour, never a blocked launch.
BOARD_FETCH_TIMEOUT=10
# Seconds between disk polls of the stage directories.
BOARD_WATCH_INTERVAL=2
+49 -5
View File
@@ -141,6 +141,40 @@ def _launch(mode: str, prompt: str, cwd: Path, agent_id: str, filename: str, log
return proc, log_file
def _fresh_branch_point() -> tuple[str | None, str | None]:
"""Where a brand-new task branch should start: the newest main that
exists. With an `origin` remote, fetch its main (bounded by
FETCH_TIMEOUT) and branch from origin/main — never touching the main
checkout itself, the fetched ref is only the branch point. No remote,
a failed fetch or a timeout all mean today's behaviour: branch from
HEAD, because launching must never be blocked by network weather.
Returns (start point, ticker note); (None, …) means HEAD. The note is
non-None whenever the branch point deserves a mention — origin/main
ahead of this checkout, or a fetch that had to be skipped.
"""
def _git(*args: str) -> subprocess.CompletedProcess:
return subprocess.run(["git", "-C", str(config.REPO), *args],
capture_output=True, text=True)
if "origin" not in _git("remote").stdout.split():
return None, None
try:
fetched = subprocess.run(
["git", "-C", str(config.REPO), "fetch", "origin", "main"],
capture_output=True, text=True, timeout=config.FETCH_TIMEOUT)
except subprocess.TimeoutExpired:
return None, "fetch of origin/main timed out; branched from local HEAD"
if fetched.returncode != 0 or \
_git("rev-parse", "--verify", "--quiet", "origin/main").returncode != 0:
return None, "fetch of origin/main failed; branched from local HEAD"
ahead = _git("rev-list", "--count", "main..origin/main").stdout.strip()
if ahead.isdigit() and int(ahead) > 0:
return "origin/main", (f"branched from origin/main, "
f"{ahead} ahead of this checkout")
return "origin/main", None
def start_agent(filename: str, stage: str) -> dict:
# Moving a card to in-progress is the commitment; only then does work start.
_validate(filename, stage, {"in-progress"},
@@ -156,6 +190,7 @@ def start_agent(filename: str, stage: str) -> dict:
branch_exists = _git("rev-parse", "--verify", "--quiet", branch).returncode == 0
continuing = worktree.exists()
base_note = None
if continuing:
# earlier work exists — the agent continues on it rather than refusing
current = subprocess.run(
@@ -172,8 +207,14 @@ def start_agent(filename: str, stage: str) -> dict:
base = _git("merge-base", "main", branch).stdout.strip()
result = _git("worktree", "add", str(worktree), branch)
else:
base = _git("rev-parse", "HEAD").stdout.strip()
result = _git("worktree", "add", "-b", branch, str(worktree))
point, base_note = _fresh_branch_point()
if point:
base = _git("rev-parse", point).stdout.strip()
result = _git("worktree", "add", "--no-track", "-b", branch,
str(worktree), point)
else:
base = _git("rev-parse", "HEAD").stdout.strip()
result = _git("worktree", "add", "-b", branch, str(worktree))
if result.returncode != 0:
raise ValueError(f"git worktree add failed: {result.stderr.strip()[:300]}")
@@ -197,11 +238,14 @@ def start_agent(filename: str, stage: str) -> dict:
}
with state.LOCK:
state.AGENTS[agent_id] = record
summary = (f"{name} is back on {filename} — continuing branch {branch}"
if continuing else
f"{name} started on {filename} (branch {branch})")
if base_note:
summary += f"{base_note}"
state.record_board_event({
"kind": "agent", "actor": "agent", "file": filename,
"summary": (f"{name} is back on {filename} — continuing branch {branch}"
if continuing else
f"{name} started on {filename} (branch {branch})"),
"summary": summary,
})
threading.Thread(target=_reap_agent, args=(agent_id, proc, log_file),
daemon=True).start()
+5
View File
@@ -101,6 +101,11 @@ GH_BIN = setting("BOARD_GH_BIN", "gh")
GIT_REMOTE = setting("BOARD_GIT_REMOTE", "")
PR_POLL_INTERVAL = float(setting("BOARD_PR_POLL_INTERVAL", "60"))
# How long a work-agent launch waits for `git fetch origin main` before
# branching from local HEAD instead. Launching must never be blocked by
# network weather; this bounds the whole delay.
FETCH_TIMEOUT = float(setting("BOARD_FETCH_TIMEOUT", "10"))
WATCH_INTERVAL = float(setting("BOARD_WATCH_INTERVAL", "2"))
EVENTS_CAP = int(setting("BOARD_EVENTS_CAP", "800"))
BOARD_EVENTS_CAP = int(setting("BOARD_HISTORY_CAP", "300"))
+124
View File
@@ -0,0 +1,124 @@
"""Where a fresh work launch branches from: origin/main when a fetch can
reach it, local HEAD when there is no origin or the network fails — and
never a blocked launch either way. Run with:
python3 -m unittest discover -s tests
"""
import subprocess
import sys
import tempfile
import time
import unittest
from pathlib import Path
REPO = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(REPO / "manager" / "core"))
import agents # noqa: E402
import config # noqa: E402
def _git(cwd: Path, *args: str) -> str:
return subprocess.check_output(
["git", "-C", str(cwd), *args], text=True,
stderr=subprocess.DEVNULL).strip()
def _commit(cwd: Path, msg: str) -> None:
subprocess.check_call(
["git", "-C", str(cwd), "-c", "user.email=t@t", "-c", "user.name=t",
"commit", "-q", "--allow-empty", "-m", msg])
class FreshBranchPoint(unittest.TestCase):
"""_fresh_branch_point decides the base of a brand-new task branch."""
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
root = Path(self._tmp.name)
self.upstream = root / "upstream"
self.upstream.mkdir()
_git(self.upstream, "init", "-q", "-b", "main")
_commit(self.upstream, "root")
self.local = root / "local"
subprocess.check_call(
["git", "clone", "-q", str(self.upstream), str(self.local)],
stderr=subprocess.DEVNULL)
self._saved = config.REPO, config.FETCH_TIMEOUT
config.REPO = self.local
config.FETCH_TIMEOUT = 5.0
def tearDown(self):
config.REPO, config.FETCH_TIMEOUT = self._saved
self._tmp.cleanup()
def _hang_remote(self, name: str) -> None:
"""Point a remote at a transport that never answers."""
_git(self.local, "config", "protocol.ext.allow", "always")
# ext:: runs the command as the remote helper; sleep never answers
# git's handshake, so the fetch blocks until the timeout fires.
_git(self.local, "remote", "set-url", name, "ext::sleep 30")
def test_no_remote_branches_from_head_silently(self):
_git(self.local, "remote", "remove", "origin")
self.assertEqual(agents._fresh_branch_point(), (None, None))
def test_only_origin_counts_and_is_never_fetched_when_absent(self):
# A hanging remote under another name: if anything fetched it, this
# test would stall; instead the launch path answers HEAD instantly.
_git(self.local, "remote", "rename", "origin", "upstream")
self._hang_remote("upstream")
started = time.monotonic()
self.assertEqual(agents._fresh_branch_point(), (None, None))
self.assertLess(time.monotonic() - started, 2)
def test_origin_ahead_branches_from_its_tip_and_says_so(self):
_commit(self.upstream, "landed elsewhere 1")
_commit(self.upstream, "landed elsewhere 2")
point, note = agents._fresh_branch_point()
self.assertEqual(point, "origin/main")
self.assertEqual(note, "branched from origin/main, "
"2 ahead of this checkout")
# The fetch refreshed the ref the branch will start from.
self.assertEqual(_git(self.local, "rev-parse", "origin/main"),
_git(self.upstream, "rev-parse", "main"))
def test_worktree_from_origin_main_has_its_tip_as_merge_base(self):
# The same commands start_agent runs for a fresh branch.
_commit(self.upstream, "landed elsewhere")
point, _ = agents._fresh_branch_point()
worktree = Path(self._tmp.name) / "wt"
_git(self.local, "worktree", "add", "--no-track", "-b", "task/x",
str(worktree), point)
self.assertEqual(_git(self.local, "merge-base", "task/x", "origin/main"),
_git(self.upstream, "rev-parse", "main"))
# --no-track: the task branch must not adopt origin/main as upstream.
upstream_cfg = subprocess.run(
["git", "-C", str(self.local), "config", "branch.task/x.merge"],
capture_output=True)
self.assertNotEqual(upstream_cfg.returncode, 0)
def test_origin_in_sync_is_used_without_narration(self):
self.assertEqual(agents._fresh_branch_point(), ("origin/main", None))
def test_unreachable_origin_falls_back_to_head_and_says_so(self):
_git(self.local, "remote", "set-url", "origin",
str(Path(self._tmp.name) / "gone"))
point, note = agents._fresh_branch_point()
self.assertIsNone(point)
self.assertEqual(note, "fetch of origin/main failed; "
"branched from local HEAD")
def test_hanging_fetch_times_out_within_bound_and_says_so(self):
self._hang_remote("origin")
config.FETCH_TIMEOUT = 0.5
started = time.monotonic()
point, note = agents._fresh_branch_point()
self.assertLess(time.monotonic() - started, 5)
self.assertIsNone(point)
self.assertEqual(note, "fetch of origin/main timed out; "
"branched from local HEAD")
if __name__ == "__main__":
unittest.main()