GitLab Assignment Locking
Two developers working the same issue simultaneously = merge hell. Spexso prevents this with a GitLab assignment lock enforced at the /spexso-start level.
Rules
- On
/spexso-start <id>: Check issue assignment vianpx spexso issue view <id> - Unassigned: Proceed — assign issue to current user via
npx spexso issue update <id> --assignee $(npx spexso whoami) - Assigned to me: Proceed — resume work (worktree may or may not exist)
- Assigned to someone else: BLOCK — "Issue #42 is assigned to @dev-b. Pick a different issue or coordinate with them."
Lock Check in /spexso-start
# Check assignment (issue view prints the normalized Issue shape)
assignee=$(npx spexso issue view $ISSUE_ID | jq -r '.assignee // empty')
me=$(npx spexso whoami --output json | jq -r '.username')
if [ -n "$assignee" ] && [ "$assignee" != "$me" ]; then
echo "Issue #$ISSUE_ID is assigned to @$assignee."
echo "Pick a different issue or ask @$assignee to unassign."
exit 1
fi
# Claim the issue
npx spexso issue update $ISSUE_ID --assignee $me
# Also check if remote branch already exists (belt + suspenders)
if git ls-remote --heads origin "spexso/issue-$ISSUE_ID" | grep -q .; then
echo "Branch spexso/issue-$ISSUE_ID exists on remote."
echo "Someone may be working on this. Check with the team."
# Warning, not a hard block — branch could be from a previous attempt
fiOn /spexso-finish
The assignment stays through merge — /spexso-finish moves the issue to stage::ship + needs::manual-test, but does not unassign it. Unassignment isn't automated: the developer clears needs::manual-test after manual QA and moves the issue to stage::operate themselves. This keeps a clear ownership trail visible on the GitLab board until a human confirms the work is live.
Auto-pick runner
The auto-pick runner acts as its own bot user, so the assignment lock works in both directions: the marathon skips issues assigned to a developer, and /spexso-start blocks on an issue assigned to the bot.
On the runner path (/spexso-start --autopick) the soft "branch exists" warning is replaced by an atomic claim, so a developer and the bot cannot both win the same issue:
git push origin HEAD:refs/heads/spexso/issue-$ISSUE_ID --force-with-lease=refs/heads/spexso/issue-$ISSUE_ID:The empty expected value means "the branch must not exist yet" and the server does the compare-and-swap. The flag goes after the refspec on purpose: the permission policy denies git push --force* as a prefix match. If the push is rejected, the bot hands the issue off: it unassigns itself, removes auto-pick, adds needs::attention and notes that the branch already exists, so the issue is not picked again on the next run.
Right before the push and auto-merge, /spexso-finish --train-merge --autopick re-reads the assignee. If a developer took the issue over in the meantime, the bot removes auto-pick, posts a note and leaves the MR open without auto-merge.
A human /spexso-start on an auto-pick issue drops the auto-pick label, so the issue leaves the bot's queue.