fix(run-hardening): workspace branch-collision + verb-runner None guard (#251)

* fix(gateway): guard the verb runner against a None task/agent

The runner's atomic steps dereference task.id / agent.id with no None-check, so a
verb invoked when the task or agent could not be resolved crashed with a cryptic
"'NoneType' object has no attribute 'id'" (observed on i_will_plan for a task
forced into an unexpected state out-of-band). Fail fast at run_intent's entry
with an actionable INVALID_STATE error instead.

* fix(git): reset the dev workspace before a fresh-claim branch checkout

A developer's persistent per-dev clone is shared across tasks, so a finished or
abandoned prior task can leave it dirty and on a sibling branch. create_branch's
checkouts then fail on the dirty tree — and because this git work runs as a
side-effect AFTER the claim's DB transition commits, the task is left marked
assigned while the workspace stays on the wrong branch, so the dev's next commit
is rejected BRANCH_MISMATCH (stalling then blocking the task).

reset --hard the tree before the base/feature checkouts. This runs only on a
fresh claim (resume short-circuits in _dev_reentry), so discarded changes are
abandoned cruft from a finished task — never commits (reset --hard keeps HEAD),
never the gitignored .venv. The branch-preservation test invariant is refined to
its real intent: a work-carrying branch must never be RE-POINTED
(reset --hard <base>); a bare tree-clean reset is allowed.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-06-24 03:56:36 +02:00
committed by GitHub
co-authored by Renn F
parent d8254300cd
commit 7094c3c171
5 changed files with 57 additions and 2 deletions
+9 -2
View File
@@ -629,8 +629,15 @@ async def test_create_branch_keeps_existing_branch_that_has_work() -> None:
calls = await _run_create_branch_with_existing_branch(
svc, "feature/frontend/abc12345--def67890", unique_commits="3"
)
assert not any(c[:2] == ["reset", "--hard"] for c in calls), (
"a branch with real work must never be reset"
# The fresh-claim tree-clean (a BARE `reset --hard`) is expected — it discards
# only uncommitted cruft from a prior task in the shared clone, never commits.
assert ["reset", "--hard"] in calls
# But the RE-POINT reset (`reset --hard <base>`, which throws commits away)
# must NEVER fire for a branch carrying its own work.
# `c[2:]` truthy == there is a ref arg after "reset --hard" → it re-points.
repoint_resets = [c for c in calls if c[:2] == ["reset", "--hard"] and c[2:]]
assert not repoint_resets, (
"a branch with real work must never be re-pointed onto base"
)