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
62 changes: 29 additions & 33 deletions .claude/rules/claim-acquire-before-worktree-work.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ Carved sentence:
> share git + bus on one machine, **`--from` must differ** (e.g., `otto-cli`
> vs `otto-desktop`) for the claim-coordinator (`tools/bus/claim.ts`,
> B-0400 slice 3) to prevent split-brain — identical `--from` values both
> exit 0 (same-sender idempotent re-acquire). Until the sender-ID schema is
> extended, use **lane-based convention** as the only real split-brain
> prevention; branch-prefix is NOT a workaround because `claim acquire`
> only filters by `from`, not by `branch`. Before starting work on any
> backlog row, `claim acquire` first. If already claimed by another
> agent, pick a different row.
> exit 0 (same-sender idempotent re-acquire). As of PR #3037 (2026-05-13)
> `SENDER_IDS` includes surface-tagged variants — opt in to `otto-cli` /
> `otto-desktop` / `alexa-cli` / `alexa-kiro` / etc. for correct
> distinction. Identity-level names (`otto`, `alexa`, etc.) remain
> valid for back-compat but do NOT distinguish surfaces. Branch-prefix
> is NOT a workaround because `claim acquire` only filters by `from`,
> not by `branch`. Before starting work on any backlog row,
> `claim acquire` first with your surface-tagged sender ID. If already
> claimed by another agent, pick a different row.

## Operational content

Expand Down Expand Up @@ -71,39 +74,31 @@ $ echo $?
# Proceeded with worktree creation + impl
```

### Example 2: Otto-CLI and Otto-Desktop race — KNOWN GAP
### Example 2: Otto-CLI and Otto-Desktop race — FIXED (PR #3037)

```bash
# Otto-CLI publishes claim first:
$ bun tools/bus/claim.ts acquire --from otto --item B-0444
# Otto-CLI publishes claim first (using surface-tagged sender):
$ bun tools/bus/claim.ts acquire --from otto-cli --item B-0444
$ echo $?
0

# Otto-Desktop tries to claim same row with the SAME --from value:
$ bun tools/bus/claim.ts acquire --from otto --item B-0444
# Otto-Desktop tries to claim the same row with its OWN surface ID:
$ bun tools/bus/claim.ts acquire --from otto-desktop --item B-0444
$ echo $?
0 # <-- ALSO succeeds! claim.ts treats same-from as idempotent self-re-acquire
1 # otto-desktop sees otto-cli's claim, exits 1 — split-brain prevented
```

**Known architectural gap (caught by Vera 2026-05-13 in review of this
rule, PR #3032):** `tools/bus/claim.ts` line ~270 filters existing
claims by `c.from !== sender`, so two callers passing the same
`--from otto` are indistinguishable to claim.ts. The canonical
`SENDER_IDS` (`otto`, `alexa`, `riven`, `vera`, `lior`) does NOT
distinguish multi-surface instances of the same agent.

**Workarounds** (until the schema supports multi-surface sender IDs):

1. **Lane-based convention** (zero-code; the ONLY real split-brain
prevention available today): Otto-CLI takes backlog grinding +
slice impl; Otto-Desktop takes substrate + cowork. Different
scopes, no claim collision possible because the scopes don't
overlap.
2. **Schema extension** (substrate-level fix; future work): add
`otto-cli` and `otto-desktop` (and analogous `alexa-cli`/
`alexa-kiro`, etc.) to `SENDER_IDS` in `tools/bus/types.ts`. Then
`--from otto-desktop` becomes a distinct claim from `--from
otto-cli`. THIS is the substrate-level mechanization.
**Schema fix landed (PR #3037, 2026-05-13):** `otto-cli`, `otto-desktop`,
`alexa-cli`, `alexa-kiro`, `riven-cli`, `riven-cursor`, `lior-antigravity`,
`lior-gemini`, and `vera-codex` are now valid `SENDER_IDS`. The prior
architectural gap (two callers passing `--from otto` were indistinguishable)
is resolved — use surface-tagged variants for correct multi-surface claim
distinction. Identity-level names (`otto`, `alexa`, etc.) remain valid for
back-compat.

**Lane-based convention** (zero-code; still useful as defense-in-depth):
Otto-CLI takes backlog grinding + slice impl; Otto-Desktop takes substrate +
cowork. Even with the schema fix, different scopes reduce collision risk further.

**Branch-prefix is NOT a workaround**: `claim acquire` filters
existing claims by `c.from !== sender` only, NOT by branch. Two
Expand All @@ -127,8 +122,9 @@ $ echo $?
# Otto-Desktop picks B-0445 instead.
```

This is the target behavior. Today (Example 2) it doesn't work
because `otto-cli` and `otto-desktop` aren't in `SENDER_IDS` yet.
This is the operational behavior as of PR #3037 (2026-05-13). Use surface-tagged
`--from otto-cli` / `--from otto-desktop` (not `--from otto`) for correct
multi-surface split-brain prevention.

### Example 4: Otto-CLI crashes mid-work

Expand Down
68 changes: 68 additions & 0 deletions tools/bus/claim.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,74 @@ describe("claim.ts — acquire", () => {
expect(r.exitCode).toBe(1);
expect(r.stderr).toContain("required");
});

// PR #3037 — multi-surface sender ID coverage
test("acquire accepts otto-cli surface-tagged sender", () => {
const r = run("acquire", "--from", "otto-cli", "--item", "B-0444");
expect(r.exitCode).toBe(0);
});

test("acquire accepts otto-desktop surface-tagged sender", () => {
const r = run("acquire", "--from", "otto-desktop", "--item", "B-0445");
expect(r.exitCode).toBe(0);
});

test("otto-cli and otto-desktop are DISTINCT senders — same-item claim by second surface is rejected", () => {
const r1 = run("acquire", "--from", "otto-cli", "--item", "B-0500");
expect(r1.exitCode).toBe(0);
// otto-desktop on the SAME item should be REJECTED (otto-cli holds it)
const r2 = run("acquire", "--from", "otto-desktop", "--item", "B-0500");
expect(r2.exitCode).toBe(1);
expect(r2.stderr).toContain("otto-cli");
});

test("acquire accepts alexa-cli surface-tagged sender", () => {
const r = run("acquire", "--from", "alexa-cli", "--item", "B-0501");
expect(r.exitCode).toBe(0);
});

test("acquire accepts alexa-kiro surface-tagged sender", () => {
const r = run("acquire", "--from", "alexa-kiro", "--item", "B-0506");
expect(r.exitCode).toBe(0);
});

test("alexa-cli and alexa-kiro are DISTINCT senders — same-item claim by second surface is rejected", () => {
const r1 = run("acquire", "--from", "alexa-cli", "--item", "B-0507");
expect(r1.exitCode).toBe(0);
const r2 = run("acquire", "--from", "alexa-kiro", "--item", "B-0507");
expect(r2.exitCode).toBe(1);
expect(r2.stderr).toContain("alexa-cli");
});

test("acquire accepts riven-cli surface-tagged sender", () => {
const r = run("acquire", "--from", "riven-cli", "--item", "B-0502");
expect(r.exitCode).toBe(0);
});

test("acquire accepts riven-cursor surface-tagged sender", () => {
const r = run("acquire", "--from", "riven-cursor", "--item", "B-0508");
expect(r.exitCode).toBe(0);
});

test("acquire accepts lior-antigravity surface-tagged sender", () => {
const r = run("acquire", "--from", "lior-antigravity", "--item", "B-0503");
expect(r.exitCode).toBe(0);
});

test("acquire accepts lior-gemini surface-tagged sender", () => {
const r = run("acquire", "--from", "lior-gemini", "--item", "B-0509");
expect(r.exitCode).toBe(0);
});

test("acquire accepts vera-codex surface-tagged sender", () => {
const r = run("acquire", "--from", "vera-codex", "--item", "B-0504");
expect(r.exitCode).toBe(0);
});

test("identity-level otto still accepted (back-compat)", () => {
const r = run("acquire", "--from", "otto", "--item", "B-0505");
expect(r.exitCode).toBe(0);
});
});

describe("claim.ts — release", () => {
Expand Down
39 changes: 38 additions & 1 deletion tools/bus/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,40 @@
// work-assignment — B-0441: proactive assignment of a ready-to-grind backlog row
// missed-substrate-cascade — B-0442: branch-vs-merged-PR drift detected; recovery needed

/**
* Multi-foreground-surface agent identifiers.
*
* Each AI agent in the factory may operate across multiple surfaces (CLI +
* IDE + Desktop). The unsuffixed name (e.g., "otto") is the identity-level
* reference. The surface-tagged variants (e.g., "otto-cli", "otto-desktop")
* are distinct sender IDs for the SAME identity operating on different
* surfaces — required for the claim-coordinator to prevent split-brain
* (per `.claude/rules/claim-acquire-before-worktree-work.md` 2026-05-13).
Comment thread
AceHack marked this conversation as resolved.
*
* Identity ≠ instance. Same Otto, different process. Coordination at the
* bus-protocol layer, identity preserved at the substrate layer.
*/
export type AgentId =
// Identity-level (back-compat; unsuffixed)
| "otto"
| "alexa"
| "riven"
| "vera"
| "lior"
// Otto multi-surface (added 2026-05-13 — multi-foreground-surface activation)
| "otto-cli"
| "otto-desktop"
// Alexa multi-surface (Kiro IDE + CLI)
| "alexa-cli"
| "alexa-kiro"
// Riven multi-surface (Cursor IDE + CLI)
| "riven-cli"
| "riven-cursor"
// Lior multi-surface (Antigravity IDE + Gemini CLI)
| "lior-antigravity"
| "lior-gemini"
// Vera (single primary surface currently; reserved for future)
| "vera-codex"
| "*"; // broadcast

/** Sender identity — excludes broadcast target "*" which is not a valid origin. */
Expand Down Expand Up @@ -109,7 +137,16 @@ export type MessageEnvelope = BusMessage & {

// ── canonical agent lists (single source of truth for both CLIs) ─────────────

export const SENDER_IDS: readonly SenderAgentId[] = ["otto", "alexa", "riven", "vera", "lior"];
export const SENDER_IDS: readonly SenderAgentId[] = [
// Identity-level (back-compat; unsuffixed)
"otto", "alexa", "riven", "vera", "lior",
// Multi-surface variants (added 2026-05-13 — multi-foreground-surface activation)
"otto-cli", "otto-desktop",
"alexa-cli", "alexa-kiro",
Comment thread
AceHack marked this conversation as resolved.
"riven-cli", "riven-cursor",
"lior-antigravity", "lior-gemini",
Comment thread
AceHack marked this conversation as resolved.
"vera-codex",
];
export const AGENT_IDS: readonly AgentId[] = [...SENDER_IDS, "*"];

// ── TTL defaults (milliseconds) ───────────────────────────────────────────────
Expand Down
Loading