fix(missions): every phase of a mission shared one branch
ci / gates (push) Failing after 7s
ci / rust (push) Skipped
ci / frontend (push) Skipped
ci / e2e (push) Skipped
ci / publish (push) Skipped

`branch_name` took `[..8]` of both the mission and the phase id. Both are
UUIDv7, which leads with a 48-bit timestamp, so ids minted in the same
millisecond — which is exactly what happens when a mission inserts its phases
in one transaction — share their leading hex. Production produced:

    clawmates/mission-019fc40e-019fc40e

for both the research and the coding phase. Each phase's commit moved the ref
the previous one had just set, so a two-phase mission ended with one branch
and the earlier phase's work reachable only by sha.

The segments now come from opposite ends: the mission keeps its time-ordered
prefix so branches group and sort usefully, and the phase contributes its
random tail so siblings cannot collide.

The existing test missed this because it compared iteration 0 against
iteration 1 of the *same* phase, where the `-i2` suffix guaranteed a
difference. The new test asserts the precondition explicitly — two v7 ids
minted together do share leading hex — and then that their branches differ
anyway.

Also adds the `commit_policy` gate, which three workflow recipes have declared
since they were written with nothing reading it. Two properties it must have:
a failed gate redirects work to `<branch>-wip` rather than discarding it, and
an unrunnable or undiscoverable test suite counts as unproven, never as green.
`discover_test_command` returns None for a `package.json` with no test script,
because `npm test` exits non-zero for a missing script and would read as a red
suite rather than an absent one. Not yet wired to publishing.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-02 13:08:08 -07:00
co-authored by Claude Opus 5
parent ca1fd46e08
commit 3ea288dbb5
2 changed files with 190 additions and 4 deletions
+26
View File
@@ -386,6 +386,32 @@ async fn excluded_paths_are_not_committed() {
);
}
/// Sibling phases of one mission must not share a branch.
///
/// They did. Both ids are UUIDv7, which leads with a timestamp, so two phases
/// created in the same millisecond had identical leading hex and the name
/// collapsed to one branch per mission — each phase quietly moving the ref the
/// previous one had set. Production showed
/// `clawmates/mission-019fc40e-019fc40e` for both phases of a mission.
#[test]
fn sibling_phases_get_distinct_branches() {
let mission = Uuid::now_v7();
// Minted back to back, so they share a timestamp prefix exactly as they do
// when a mission inserts its phases in one transaction.
let research = Uuid::now_v7();
let coding = Uuid::now_v7();
assert_eq!(
research.simple().to_string()[..8],
coding.simple().to_string()[..8],
"precondition: v7 ids minted together share their leading hex"
);
let a = mission_delivery::branch_name(mission, research, 0);
let b = mission_delivery::branch_name(mission, coding, 0);
assert_ne!(a, b, "each phase needs its own ref: {a} vs {b}");
assert!(a.starts_with("clawmates/mission-"));
}
/// A re-run must not collide with the pass before it.
#[test]
fn a_rerun_lands_on_its_own_branch() {