Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,11 @@ push of the current commit is **not** a claim: when the branch already points at
that commit, git resolves the push as a no-op and exits 0, and the second
developer reads success on a task somebody else holds.

Run from the branch `task/<id>`, a won claim also makes `<remote>/task/<id>`
that branch's upstream. A branch started from the remote's default branch
tracks it otherwise, and a plain `git push` is refused, or, with
`push.default=upstream`, lands on the default branch.

Exit codes: `0` claim won, `2` already held, `1` usage or environment error.

## Derived status
Expand Down
4 changes: 3 additions & 1 deletion .agents/skills/delivery-orchestrator/scripts/launch-lanes.sh
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,9 @@ while IFS= read -r task_id <&3; do
base="$(base_for "$repo_dir")" ||
{ printf 'SKIP\t%s\tcannot fetch the default branch of %s; a lane must not start from a stale base\n' "$task_id" "$repo_name"; continue; }
worktree_abs="$(absolute "$worktree")"
git -C "$repo_dir" worktree add --quiet -b "$branch" "$worktree_abs" "$base" 2>/dev/null ||
# --no-track: the base is the remote's default branch, and a task branch that
# tracked it would push there. The claim sets its own branch as upstream.
git -C "$repo_dir" worktree add --quiet --no-track -b "$branch" "$worktree_abs" "$base" 2>/dev/null ||
{ printf 'SKIP\t%s\tcould not create worktree on %s\n' "$task_id" "$branch"; continue; }

# Claim from inside the worktree. Losing the claim is normal, not an error:
Expand Down
3 changes: 2 additions & 1 deletion .agents/skills/delivery-status/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ developer can ask rather than duplicate the work.

Run the claim from the lane's worktree, on the branch `task/<id>`. On success
the local branch is advanced to the claim commit so the lane builds on top of
it. A worktree with uncommitted changes is never reset; the script reports that
it, and its upstream is set to the claimed branch, so a plain `git push` goes
there rather than to the branch it was started from. A worktree with uncommitted changes is never reset; the script reports that
it left the branch alone.

Pass `--remote <name>` when the shared remote is not `origin`, and
Expand Down
10 changes: 10 additions & 0 deletions .agents/skills/delivery-status/scripts/claim.sh
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,16 @@ if [ "$push_rc" -eq 0 ]; then
printf 'COMMIT\t%s\n' "$claim_commit"
printf 'CLAIMANT\t%s\n' "$claimant"

# The branch was usually created from the default branch on the remote, which
# git then sets as its upstream: a plain push is refused, or with
# push.default=upstream it lands on the default branch. Its upstream is the
# branch just claimed. Set in config, it needs no remote-tracking ref.
if [ "$(git branch --show-current 2>/dev/null || true)" = "$branch" ]; then
git config "branch.$branch.remote" "$remote"
git config "branch.$branch.merge" "$ref"
printf 'UPSTREAM\t%s/%s\n' "$remote" "$branch"
fi

if [ "$advance" -eq 1 ] && [ "$(git branch --show-current 2>/dev/null || true)" = "$branch" ]; then
if [ -n "$(git status --porcelain)" ]; then
printf 'NOTE\t%s\n' "local branch left in place: worktree has uncommitted changes"
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ tagging it.

## [Unreleased]

- A lane's branch pushes to itself. The launcher no longer lets `task/<id>`
track the default branch it starts from, and a won claim sets
`<remote>/task/<id>` as the branch's upstream, so a plain `git push` from a
lane worktree is no longer refused or sent to the default branch.
- An install whose skills are split between `.claude/skills/` and
`.agents/skills/`, as `npx skills update` leaves after an install naming
only `claude-code`, is reported by `where.sh` as `NEXT repair-install`, and
Expand Down
21 changes: 21 additions & 0 deletions tests/unit/test_delivery_status_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,27 @@ def test_advances_the_local_task_branch_to_the_claim_commit(self, tmp_path: Path
assert first(rows, "ADVANCED") == "task/ingestion-003"
assert git(dev, "rev-parse", "HEAD").stdout.strip() == first(rows, "COMMIT")

def test_a_plain_push_goes_to_the_claimed_branch(self, tmp_path: Path, origin: Path) -> None:
"""A branch started from the remote default branch tracks it until the claim says otherwise."""
dev = developer(tmp_path, origin, "devA")
git(dev, "checkout", "-q", "-b", "task/ingestion-003", "--track", "origin/main")

result = run_claim(["ingestion-003"], cwd=dev)

assert first(rows_of(result.stdout), "UPSTREAM") == "origin/task/ingestion-003"
(dev / "work.txt").write_text("done\n", encoding="utf-8")
git(dev, "add", "work.txt")
git(dev, "commit", "-q", "-m", "feat: work")
git(dev, "push", "-q")
assert (
git(dev, "ls-remote", "origin", "refs/heads/task/ingestion-003").stdout.split()[0]
== git(dev, "rev-parse", "HEAD").stdout.strip()
)
assert (
git(dev, "ls-remote", "origin", "refs/heads/main").stdout.split()[0]
!= git(dev, "rev-parse", "HEAD").stdout.strip()
)

def test_no_advance_leaves_the_local_branch_alone(self, tmp_path: Path, origin: Path) -> None:
dev = self._on_task_branch(tmp_path, origin, "ingestion-003")
before = git(dev, "rev-parse", "HEAD").stdout
Expand Down
17 changes: 17 additions & 0 deletions tests/unit/test_launch_lanes_script.py
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,23 @@ def test_starts_the_agent_inside_the_lane_worktree(self, tmp_path: Path) -> None
assert recorded.strip(), "the harness was never invoked"
assert str(tmp_path / "lanes" / "a-001") in recorded

def test_the_lane_branch_pushes_to_itself(self, tmp_path: Path) -> None:
"""A lane started from the default branch must not track it, or a plain push goes astray."""
origin_path = make_shared_origin(tmp_path)
dev = clone_of(tmp_path, origin_path, "devA")
log = tmp_path / "agent.log"

result = launch_live(
dev,
["--claim-script", str(CLAIM), "--worktree-root", str(tmp_path / "lanes"), "a-001"],
fake_harness(tmp_path, log),
log,
)

assert result.returncode == OK
upstream = git(tmp_path / "lanes" / "a-001", "rev-parse", "--abbrev-ref", "task/a-001@{upstream}")
assert upstream.stdout.strip() == "origin/task/a-001"

@staticmethod
def one_task_remote(tmp_path: Path, tier: str = "build") -> Path:
origin_path = make_shared_origin(tmp_path)
Expand Down
Loading