Back to Oh My Openagent

Teammode

packages/omo-codex/plugin/components/teammode/skills/teammode/SKILL.md

4.16.114.2 KB
Original Source

Teammode

Run a named team of cooperating Codex threads under one leader, with durable state on disk. This is a Codex-only workflow. It is inspired by the lifecycle concerns in the Yeachan-Heo/oh-my-codex team skill, but it does not copy that runtime model and never depends on an external terminal runner - it coordinates through Codex's own thread tools plus a bundled state script.

When to use a team (and when to use plain subagents instead)

Use a TEAM when EITHER holds:

  • the work does NOT split into perfectly isolated pieces, but doing it in parallel is clearly more convenient - members will need to see and react to each other's findings; or
  • one task still needs exploration, yet its GOAL is already clear - parallel investigation under a fixed objective.

Use plain subagents ($ulw / multi_agent_v1.spawn_agent) - NOT a team - when EITHER holds:

  • the work IS perfectly isolated, so there is no coordination cost worth paying; or
  • the GOAL is still ambiguous, where one mind should resolve direction before any fan-out.

A team buys cross-member coordination at a real overhead cost; only spend it when coordination is the thing you actually need.

You are the leader - orchestrate, do not implement

The main session is ALWAYS the team leader; you orchestrate directly and never spin up a separate leader thread. Your job is orchestration, NOT writing product code: split the work and assign each slice, hold live situational awareness of every member, verify and QA what they deliver, relay findings between members, instruct and unblock, and synthesize the result. DELEGATE every code edit to a member - if you catch yourself editing product files while the team runs, that work was a member's slice you should have handed off. You own direction, verification, and integration (the merge), not the keystrokes.

Compose by part, ownership, or perspective - not by job title

A team is ALWAYS two or more members - never a single-member team. One worker on an isolated job is a subagent (multi_agent_v1.spawn_agent), not a team; if you end up with a single member, either split off a second distinct slice or drop the team and use a subagent.

Compose the team from what you actually KNOW about the work. Ground the split in real knowledge of the problem, then divide it into clear, non-overlapping responsibilities - one per aspect of the work - and give each member exactly one. No two members may own the same thing. Define each member by a concrete slice: a specific part of the codebase, an ownership area, or a distinct perspective/lens. Assigning a vague role ("backend dev", "release analyst", "the tester") is an anti-pattern - it gives the member no real boundary and invites overlap. Each member's focus names what they own concretely; the lens is one of area, ownership, or perspective. Give each member a short, distinct --name too - its role or what it watches (e.g. app-server-lifecycle, mailbox-delivery) - because that name titles its thread; never reuse one name for two members.

Run the script - never hand-write team state

A bundled, dependency-free Node script owns all team state so you never author team.json or the member manual by hand. Run it with node (or bun); it works on macOS, Linux, and Windows. Replace <skill-root> with this skill's own directory.

node "<skill-root>/scripts/team.mjs" init        --name "<team>" --session-name "<session>" [--session <leader_session_id>] [--worktree] [--base-branch dev]
node "<skill-root>/scripts/team.mjs" add-member  --team <session_id> --id A --name "<short role>" --focus "<part/ownership/perspective>" --lens area|ownership|perspective --deliverable "<...>" [--branch <branch>]
node "<skill-root>/scripts/team.mjs" bind-thread  --team <session_id> --id A --thread <thread_id> [--cwd <path>]
node "<skill-root>/scripts/team.mjs" member-prompt --team <session_id> --id A
node "<skill-root>/scripts/team.mjs" set-status   --team <session_id> --id A --status reported|blocked|active|archived [--note "<...>"]
node "<skill-root>/scripts/team.mjs" worktree-add    --team <session_id> --id A [--base-branch <branch>]
node "<skill-root>/scripts/team.mjs" worktree-remove --team <session_id> --id A [--force]
node "<skill-root>/scripts/team.mjs" integrate       --team <session_id> [--id A]
node "<skill-root>/scripts/team.mjs" archive      --team <session_id> [--id A] [--note "<...>"]
node "<skill-root>/scripts/team.mjs" delete       --team <session_id> [--force]
node "<skill-root>/scripts/team.mjs" status       --team <session_id>

init creates .omo/teams/{session_id}/ containing team.json (the single durable state file: team id, the main-session leader, the member roster, status, worktree config, and a lifecycle log), guide.md (the auto-generated member field manual), and artifacts/ (a shared exchange space). {session_id} is the leader's Codex session id when you can pass it via --session; otherwise the script generates a stable handle. Re-running init is a safe no-op. Every mutating subcommand rewrites guide.md, so the manual always matches the current team.

Mutating subcommands take a per-team state lock before reading and rewriting team.json. It is safe to run independent add-member, bind-thread, set-status, archive, delete, guide, and worktree mutation commands concurrently against the same team: they serialize and each command reads the latest committed state before writing. If a command reports that team state is locked, do not treat the intended mutation as complete; retry after the named command finishes, or inspect .omo/teams/{session_id}/.team.lock/owner.json if the previous command crashed.

Create the team and its threads

  1. init the team, then add-member once per member.
  2. Create a durable thread per member with codex_app.create_thread - ALWAYS this tool for every member, never a spawned agent - titled [team name] <member name>, using THAT member's own name (its role / what it watches), so no two threads share a title. add-member prints the exact title to use. If codex_app.create_thread accepts a working directory / cwd argument, set it to that member's worktree; otherwise the member's manual tells it to cd there first. Use codex_app.set_thread_title if the title did not land at creation. If Codex returns only pendingWorktreeId, the worktree-backed thread is not ready yet: do not bind-thread and do not send the member bootstrap. Wait until Codex surfaces a real threadId or the thread appears in the thread list, then set the title, bind that real id with the cwd, and only then send the bootstrap.
  3. bind-thread to record each thread id (and --cwd), then send that member's bootstrap trigger (printed by add-member / member-prompt) as the thread's first message. The trigger is short on purpose: it tells the new thread to READ its guide.md and team.json rather than carrying the whole protocol inline.
  4. Whenever you report, audit, reopen, or hand off a member thread, include the app deep link codex://threads/<thread_id> next to the raw id. For example: codex://threads/019ef350-ee78-72a3-bd5e-e40cebc3d814. Worktree-backed threads are easy to lose in the sidebar without this link.

Every team member is a real Codex thread created with codex_app.create_thread - this is strict, not a preference. NEVER substitute multi_agent_v1.spawn_agent, or any other in-process subagent, for a team member: a spawned agent is an ephemeral helper that does not show up as a team thread, cannot carry the [team name] <member name> title, and cannot be inspected, titled, archived, or re-opened with the codex_app.* thread tools - which defeats the entire point of a durable team. A member only counts once you have bind-thread-ed it to a real codex_app.create_thread thread id. If the thread-creation tool is unavailable, STOP and say so (see Stop rules); do not quietly fall back to a spawned agent.

Communication

Members push to you and to one another with codex_app.send_message_to_thread; you inspect their state with codex_app.read_thread. So members can actually reach you, run init with --session <your own thread id> - that makes leader.sessionId in team.json a real, messageable thread; without it members cannot report to you and you are stuck polling. The generated manual binds members to the hard rules, so you mainly keep the channel open: expect frequent small inbound updates from each member - findings, WORKING:/BLOCKED: markers, peer digests - rather than one final dump, and act on them as they arrive. All member-to-member and member-to-leader traffic is in English; when the END user addresses a member, that member replies in the user's own language. Members hand off files and memos through the team artifacts/ directory and reference them by path.

Let members work - do not rush them

Members heartbeat every few tool calls and message you on every finding, blocker, and finished slice (their manual binds them to this). So a member that is quiet between heartbeats is working, not stalled - a stretch of silence is the normal sound of focused work, not a problem to chase. Re-reading codex_app.read_thread to check on a calm member, or sending "any update?" / "are you done?" / "hurry up" pings, interrupts that member and slows the whole team. Trust the heartbeat and let them cook.

Message a member only when one of these is true:

  • you have new information, context, or a correction it needs to do its slice right;
  • you are reassigning, narrowing, or unblocking its scope;
  • a peer's result changes what it should do; or
  • it has gone fully silent well past its heartbeat cadence AND that stall is blocking the team - then send one specific question, not a barrage.

Otherwise stay calm and keep the channel open: read inbound updates as they arrive and act on them. A long-running member is alive; a heartbeat you have not received yet is not a failure. Wait for every required member's final report before you declare the team done - rushing toward "done" while members are still mid-slice just produces half-built work you will have to redo.

Worktrees - isolate members who would touch the same files

The moment two members' slices would edit the same files, give each its own git worktree so they cannot clobber each other. Decide this whenever you see the collision - at team creation OR mid-run, not only up front. For each colliding member run worktree-add --team <id> --id <member>: it creates the worktree off the base branch on a derived branch, flips the team into worktree mode, records it in team.json, and prints the cd path to hand that member. The member works and commits only inside its own worktree. To land the work, integrate --team <id> merges every member branch into your current branch with a merge commit (never a squash or rebase); resolve any conflict it reports, then worktree-remove each worktree at cleanup.

After worktree-add, immediately send the member a follow-up that includes both its assigned worktree path and its codex://threads/<thread_id> link. Use bind-thread --cwd <worktree> or --worktree-path <worktree> after the thread exists so team.json, guide.md, status, and member-prompt all point at the same worktree-backed thread.

When the member starts inside a worktree, it must verify the assigned cwd exists and contains the repository checkout before editing. If the directory is missing, empty, or does not look like a git worktree/repository yet, the member reports BLOCKED: worktree not ready to the leader and waits instead of editing a parent checkout or an empty directory.

Run a ulw-plan in parallel

When a decision-complete plan already exists at .omo/plans/<slug>.md (from ulw-plan), execute its parallel waves as a team instead of one todo at a time. Map it directly:

  • one wave's independent todos -> one member each; the todo's scope/files become that member's focus, and its acceptance criteria + QA become the member's deliverable.
  • the plan's dependency matrix sets the shape: todos with no unmet dependency inside a wave run as concurrent members; a todo that depends on another waits, so launch the next wave only after the blocking members report.
  • todos in the same wave that touch overlapping files -> give those members worktrees (see above).

Keep the plan file as the shared spec: point each member at its todo by path, and verify the member's result against that todo's acceptance criteria before you integrate.

Archive, delete, and cleanup

DISBAND the team the moment it is no longer needed. A team exists only to do its work; once that work is done, or the user no longer wants it, do not leave it lying around - archive every member, then delete the team state only after archival evidence is clean or preserved. A finished team that is never disbanded is a leak.

  • archive closes the team: notify each active member, copy anything useful into artifacts/, then try to archive each member thread with codex_app.set_thread_archived. Treat Codex App failures such as "Ambiguous Codex thread id" or a thread id that is ambiguous across hosts as an app-thread archival blocker, not as a team-state blocker: record the failure in the team log, tell the user which member thread was not proven archived, and continue the team-state archive with archive --note "<blocker>". Never pretend a member thread was archived. Do not delete the team state after an app-thread archival blocker unless the evidence has been copied elsewhere or the user explicitly accepts that evidence loss.
  • delete removes .omo/teams/{session_id} and refuses while the team is unarchived or any member is still active unless --force.
  • When the work wraps up, land it the way the user asked: integrate --team <id> for a direct merge commit, or push each member branch and open a PR. Then worktree-remove each worktree, archive, and delete. Cleanup is real work; respect the user's instruction on how to land it.

Stop rules

  • Stop and ask before deleting an unarchived team while any member is still active.
  • Member communication stays English unless the user explicitly requests otherwise; user-facing replies follow the user's language.
  • Stop if the requested operation needs Codex thread tools (create/read/send/title/archive) and they are unavailable; say so instead of faking it.