Skip to content

feat(team-mode): Claude Code Agent Teams parity (OFF by default, 12 team_* tools)#3493

Open
code-yeongyu wants to merge 158 commits intodevfrom
feat/team-mode
Open

feat(team-mode): Claude Code Agent Teams parity (OFF by default, 12 team_* tools)#3493
code-yeongyu wants to merge 158 commits intodevfrom
feat/team-mode

Conversation

@code-yeongyu
Copy link
Copy Markdown
Owner

@code-yeongyu code-yeongyu commented Apr 17, 2026

Summary

Implements Team Mode for omo — Claude Code Agent Teams parity at the OpenCode plugin layer. OFF by default; enabled via JSONC config. Provides parallel multi-agent coordination through 12 team_* tools with role-based access control, deferred-ack mailbox semantics, durable runtime state, optional per-member git worktrees, and optional tmux visualization.

Plan: .sisyphus/plans/team-mode.md (Momus-approved, iteration 7). All 28 implementation tasks delivered across 5 waves + Final Wave Oracle audits.

Highlights

  • Config-gated: team_mode.enabled: false by default. Zero impact on existing users.
  • Full category + agent support: Members can be either a category routing through sisyphus-junior (kind: "category") or a direct subagent_type (kind: "subagent_type"). Eligible direct agents: sisyphus, atlas, sisyphus-junior, hephaestus. Read-only and orchestration-only agents (oracle, librarian, explore, multimodal-looker, metis, momus, prometheus) are rejected at parse time with verbatim §V.3 messages.
  • 12 team_* tools: Lifecycle (team_create/_delete/_shutdown_request/_approve_shutdown/_reject_shutdown), messaging (team_send_message), tasks (team_task_create/_list/_update/_get), query (team_status/_list).
  • At-least-once mailbox: poll records pendingInjectedMessageIds in durable RuntimeState; ack happens at session.idle (D-15). Crash before idle → message re-delivered next session.
  • Live-delivery reservation: team_send_message writes live-recipient messages directly under .delivering-<id>.json so the transform-hook fallback cannot double-inject the envelope while promptAsync is in flight. Atomic rename to processed/ on success, back to the unread slot on failure, reclaim-on-resume for stranded reservations.
  • Single-file 3-line locks per §III.7. Atomic writes via tmp + fsync + rename (D-05).
  • Project-scope wins on collision (D-23) with structured warning.
  • Optional tmux layout (focus + grid windows). Each pane runs opencode attach against the member session. Failures isolated — never blocks team creation (D-34).
  • Doctor check: bunx oh-my-opencode doctor reports team-mode status, dependencies, declared/runtime team counts.

Architecture

src/features/team-mode/
├── types.ts                    # TeamSpec, Member (discriminatedUnion), Message, Task, RuntimeState + parseMember
├── deps.ts                     # tmux + git availability probes
├── team-registry/              # paths, loader, validator (project>user scope)
├── team-state-store/           # locks, store (durable transitions), resume (post-reload recovery + stale reservation reclaim + worker liveness)
├── team-mailbox/               # send (pre-reserve for live), inbox, poll (deferred-ack + dedupe), ack, reservation (atomic rename primitives)
├── team-tasklist/              # store, claim (flock), update, dependencies, get, list (individual JSON files + .highwatermark)
├── team-worktree/              # manager, cleanup (optional git worktree per member)
├── team-runtime/               # create (with rollback), shutdown (2-phase), resolve-member (dual routing), status, resolve-caller-team-lead
├── team-layout-tmux/           # optional focus + grid layout via opencode attach
└── tools/                      # 12 team_* MCP tools

src/hooks/
├── team-mailbox-injector/      # transform-phase, prepends <peer_message ...> envelopes
├── team-tool-gating/           # role-based access (lead vs member vs neither)
└── team-session-events/        # lead-orphan, member-error, idle-wake-hint (with ack-on-idle)

src/cli/doctor/checks/
└── team-mode.ts                # doctor diagnostic

Storage layout

~/.omo/
├── teams/{name}/config.json                      # declared team specs (directory-style, Claude Code parity)
└── runtime/{teamRunId}/
    ├── state.json                                # durable runtime state machine
    ├── inboxes/{member}/{uuid}.json              # per-recipient atomic mailbox files
    ├── inboxes/{member}/.delivering-{uuid}.json  # transient live-delivery reservation (hidden from polls, counted for backpressure)
    ├── inboxes/{member}/processed/               # acked messages
    └── tasks/{id}.json                           # shared task list with .highwatermark counter

Key invariants

Invariant Why
Spawn path = BackgroundManager.launch() only Avoids double session creation
Ack deferred to session.idle At-least-once preservation on crash (D-15)
Lock file = single 3-line plain text at lockPath §III.7 Claude Code parity
parseMember emits §V.3 verbatim errors Plan compliance + Momus verification
Skill has NO mcpConfig Tools register via plugin ToolRegistry instead
team_create blocked for existing participants Prevents nested teams (D-21)
Member delegate-task budget = 0 Prevents nested delegation (D-13)
<peer_message ...> envelope literal Untrusted body never escaped/stripped (D-24)
Live recipients get .delivering-* reservation at write time No visible inbox window between send and live deliver (Oracle R21 round 2)
Reserved files count toward recipient backpressure Prevents unlimited queuing behind a slow live recipient (Oracle R21 round 3)

Tests

  • 4888 tests pass via bun run script/run-ci-tests.ts (full suite with proper isolation).
  • Per-module test files for every team-mode source file.
  • Integration test scaffold (src/features/team-mode/integration.test.ts) covering C-10 end-to-end scenarios (single-member echo, 2-member task pipeline, resume after restart, parallel bound enforcement).
  • bun run typecheck exit 0.

Documentation

  • User guide: docs/guide/team-mode.md (refreshed for opencode attach tmux flow + .delivering-* reservation file)
  • Module architecture: src/features/team-mode/AGENTS.md
  • Doctor check: bunx oh-my-opencode doctor

Reviewer notes

  • Branch contains atomic commits per task. Squash on merge if preferred.
  • Reference projects consulted: ../free-code (Claude Code experimental Agent Teams) and ../opencode (plugin API + session API).
  • All Oracle/Momus iteration findings (P-1 §V.3 messages, P-2 lock format, T12 worktree absence, status.ts gaps, R20 streamer race, R21 runtime bugs) addressed via dedicated fix commits.
  • No new npm dependencies added.
  • Existing OpenCode tool registry, hook registration, doctor check pipeline reused — minimal core changes.

Out of scope (per Momus iteration 5/6 lock-in)

  • No nested teams.
  • No synchronous reply waits.
  • No agentika integration.
  • No Dori/Watcher/Monitor/Escalation extensions.
  • No topic-based pub/sub.

Post-implementation verification (Oracle R20 — 2026-04-18)

After Oracle R5–R17 identified and the branch fixed a sequence of race conditions in the team-session-streamer hook (enqueue ordering, drain mutex, scheduler stop/dispose split, generation tokens, pre-mapping state, polling-based retry, etc.), Oracle verified the final code on ed7a816c against a fully interactive terminal QA of the whole lifecycle in one fresh run.

Oracle R20 verdict: VERIFY_PASS.

Evidence: a single fresh opencode run executed the entire lifecycle end-to-end in one continuous transcript: team_create → member spawn → team_status × 2 → team_shutdown_request + team_approve_shutdown for each member → team_delete. Post-delete invariants all passed.

The streamer-based visualization has since been removed (see R21 below); tmux visualization now attaches via opencode attach directly, which required no equivalent streaming gate.

Post-implementation runtime fixes (Oracle R21 — 2026-04-19)

Three full Oracle review rounds identified and fixed live-runtime bugs that persisted after R20. All findings land as atomic TDD commits with regression tests on feat/team-mode between 063e3c53..646344df.

Round 1 — 4 bugs in durable runtime behavior:

  1. poll.tspendingInjectedMessageIds accumulated duplicates of the same messageId every turn without ack landing (observed 8-12 duplicates per message in production state.json). Dedupe via Array.from(new Set([...])). → 88e9b34c
  2. messaging.tsdeliverLive() called promptAsync before ackMessages, so the transform-hook fallback could read the still-unread inbox file and inject the envelope a second time when promptAsync landed. Introduced team-mailbox/reservation.ts with atomic rename-based reservation. → c15c1e00
  3. messaging.ts — broadcast activeMembers filtered to members with a live sessionId AND included the sender itself; missed members that hadn't spawned yet and echoed lead broadcasts back into the lead's own inbox. Resolved to all members minus sender. → 737e6eb6
  4. resume.tsresumeAllTeams only verified the lead session; teams with every worker dead but an alive lead stayed active pointing to phantom sessions. Added worker-session inspection with orphan-on-all-dead semantics. → 70ed63f2

Round 2 — 3 follow-up bugs Oracle caught in the R1 fixes:

  1. Race still existed between sendMessage writing <id>.json and deliverLive renaming it. Moved reservation to write time via SendContext.reservedRecipients; sendMessage now writes live-recipient messages directly under .delivering-<id>.json. Made reserveMessageForDelivery idempotent (pre-reserved stat OR on-the-fly rename for the rare session-appears-after-send case). → d3c468c8
  2. Stranded .delivering-* files were invisible to listUnreadMessages (dotfile filter) and only logged release failures. Added reclaimStaleReservations(teamRunId, memberName, config, ttl), called per member during resume with a 10 minute TTL. → b2180928 + 290e189b
  3. inspectWorkerMembers counted every sessionId === undefined worker as alive. After one resume cleared a dead worker's sessionId, a later resume treated it as alive; if the last live worker then died the team stayed active with zero live workers. Fixed by checking member.status === "errored" first. → 65b49bf6

Round 3 — 1 regression Oracle caught in the R2 fixes:

  1. The R2-round-1 fix accidentally broke backpressure: getUnreadSizeBytes excluded all dotfiles, so pre-reserved .delivering-*.json messages did not count toward recipient_unread_max_bytes. Concurrent sends could stack arbitrary in-flight traffic behind a slow live recipient. Tightened the filter to include .delivering-*.json while still excluding lock/metadata dotfiles. → 646344df

Oracle R21 final verdict: <promise>LOOKS_GOOD</promise>.

Residual risk documented by Oracle: Same-process orphaned .delivering-* reservations now correctly consume recipient backpressure budget, so a stranded reservation can conservatively block new sends until release or restart-time reclaim (10 min TTL). Acceptable tradeoff versus the pre-fix unlimited-backpressure-bypass bug.

Post-fix verification:

  • bun run script/run-ci-tests.ts4888 pass / 0 fail
  • bun run typecheck → exit 0
  • LSP diagnostics clean on all touched files (poll.ts, send.ts, reservation.ts, messaging.ts, resume.ts)
  • User guide refreshed for the new reservation mechanic and opencode attach tmux flow → 052d1d1e

Summary by cubic

Adds Team Mode for parallel multi-agent coordination with 12 team_* tools, durable state, a shared mailbox with live‑delivery reservations, and an optional tmux layout that streams each member via opencode attach. Off by default; enable with team_mode.enabled to surface the team-mode skill and a new doctor check.

  • New Features

    • Root team_mode config and JSON schema (bounds, defaults); doctor check validates config, dirs, and tmux/git; builtin team-mode skill is config‑gated.
    • Durable runtime with resume-on-reload (resume deferred post-init to avoid startup deadlock); lead defaults to the calling agent and reuses the caller session.
    • Shared task list with file‑locked claims; mailbox has deferred-ack and live-delivery reservations via .delivering-*.json to prevent double‑inject; pending IDs deduped; reservations count toward backpressure and are reclaimed on resume.
    • Broadcast excludes sender and queues for not‑yet‑spawned members.
    • Optional tmux visualization creates focus + grid panes per member via opencode attach; helpers close panes, rebalance windows, and sweep stale omo-team-* sessions.
    • Team Mode guide added and linked from the docs.
  • Refactors

    • Replaced custom FIFO streamer with opencode attach; tmux runner hardened with retry/timeout and safer attach command.
    • Background manager treats session.error as transient if the session still exists.
    • Plugin loader recovers stale install paths and legacy plugin.json, with strict manifest name matching and deterministic version selection.
    • CI isolates new team-mode tests; .sisyphus/ fully ignored.

Written for commit d2f03ac. Summary will update on new commits.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 189 files

Confidence score: 3/5

  • There is a concrete user-facing behavior risk in src/cli/doctor/checks/index.ts: registering Team Mode causes doctor to run ensureBaseDirs(), which can create or re-permission ~/.omo during what should be a read-only health check.
  • This keeps merge risk in the moderate range because the issue is medium severity (5/10) with high confidence, while the docs issue in docs/guide/team-mode.md is low severity and mainly affects copy-paste success (subagent_type: explore fails validation).
  • Pay close attention to src/cli/doctor/checks/index.ts and docs/guide/team-mode.md - prevent side effects in doctor and fix the invalid Team Mode example so guidance matches parser rules.

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed. cubic prioritises the most important files to review.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="docs/guide/team-mode.md">

<violation number="1" location="docs/guide/team-mode.md:45">
P3: The example uses an ineligible `subagent_type` (`explore`), so copying the guide will fail parse-time validation.</violation>
</file>

<file name="src/cli/doctor/checks/index.ts">

<violation number="1" location="src/cli/doctor/checks/index.ts:39">
P2: Registering Team Mode here makes `doctor` perform filesystem writes via `ensureBaseDirs()`. Health checks should stay read-only, or diagnostics can unexpectedly create/re-permission `~/.omo` directories just by running the command.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

{
id: CHECK_IDS.TEAM_MODE,
name: CHECK_NAMES[CHECK_IDS.TEAM_MODE],
check: checkTeamMode,
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Registering Team Mode here makes doctor perform filesystem writes via ensureBaseDirs(). Health checks should stay read-only, or diagnostics can unexpectedly create/re-permission ~/.omo directories just by running the command.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/cli/doctor/checks/index.ts, line 39:

<comment>Registering Team Mode here makes `doctor` perform filesystem writes via `ensureBaseDirs()`. Health checks should stay read-only, or diagnostics can unexpectedly create/re-permission `~/.omo` directories just by running the command.</comment>

<file context>
@@ -32,5 +33,10 @@ export function getAllCheckDefinitions(): CheckDefinition[] {
+    {
+      id: CHECK_IDS.TEAM_MODE,
+      name: CHECK_NAMES[CHECK_IDS.TEAM_MODE],
+      check: checkTeamMode,
+    },
   ]
</file context>
Fix with Cubic

Comment thread docs/guide/team-mode.md Outdated
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Requires human review: Auto-approval blocked by 2 unresolved issues from previous reviews.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 76 files (changes from recent commits).

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed. cubic prioritises the most important files to review.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name=".gitignore">

<violation number="1" location=".gitignore:2">
P2: Ignoring the whole `.sisyphus/` tree removes the prior allowlist for `.sisyphus/rules/`, so rule files there can no longer be versioned normally.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread .gitignore
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 11 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/hooks/team-tool-gating/hook.ts">

<violation number="1" location="src/hooks/team-tool-gating/hook.ts:131">
P2: `team_list` is returned too late: it still re-loads participant state first, so a bad runtime can make this public tool fail before the allow-list branch runs.</violation>
</file>

<file name="src/plugin/event.ts">

<violation number="1" location="src/plugin/event.ts:285">
P2: Do not gate the idle wake hint on `promptAsync`; it also performs ack/cleanup work that should still run when promptAsync is unavailable.</violation>
</file>

<file name="src/features/team-mode/team-registry/loader.ts">

<violation number="1" location="src/features/team-mode/team-registry/loader.ts:125">
P2: Validating the normalized spec can produce member error paths that no longer match the authored JSON.</violation>
</file>

<file name="src/features/team-mode/team-registry/team-spec-input-normalizer.ts">

<violation number="1" location="src/features/team-mode/team-registry/team-spec-input-normalizer.ts:44">
P2: A top-level `lead` can be silently ignored when another member already uses the same name, so `leadAgentId` may point at the wrong member definition.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/hooks/team-tool-gating/hook.ts
Comment thread src/plugin/event.ts
Comment thread src/features/team-mode/team-registry/loader.ts
Comment thread src/features/team-mode/team-registry/team-spec-input-normalizer.ts
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

13 issues found across 30 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/plugin/event.ts">

<violation number="1" location="src/plugin/event.ts:275">
P2: `teamSessionStreamer` is executed twice for the same event, which can duplicate FIFO writes and tmux visualization updates.</violation>
</file>

<file name="src/hooks/team-session-streamer/fifo-writer.ts">

<violation number="1" location="src/hooks/team-session-streamer/fifo-writer.ts:9">
P1: Non-blocking FIFO writes can be partial, but this helper does a single write and ignores `bytesWritten`, so trailing data may be lost.</violation>
</file>

<file name="src/hooks/team-session-streamer/hook.ts">

<violation number="1" location="src/hooks/team-session-streamer/hook.ts:97">
P2: Advancing the per-part cursor before confirming a stream target can drop early text when team resolution is temporarily unavailable.</violation>

<violation number="2" location="src/hooks/team-session-streamer/hook.ts:183">
P2: Handle `message.part.delta` here as well; the current guard drops incremental text events, so tmux streaming can miss live output.</violation>
</file>

<file name="src/features/team-mode/tools/lifecycle-test-fixture.ts">

<violation number="1" location="src/features/team-mode/tools/lifecycle-test-fixture.ts:109">
P2: Deleting a team leaves the `teamRuns` reverse index stale, so recreating the same team/lead pair later can throw `missing runtime` instead of creating a fresh run.</violation>

<violation number="2" location="src/features/team-mode/tools/lifecycle-test-fixture.ts:120">
P2: Mock approval does not require an existing shutdown request, so lifecycle tests can miss invalid approve-before-request sequencing.</violation>
</file>

<file name="src/features/team-mode/team-layout-tmux/layout.ts">

<violation number="1" location="src/features/team-mode/team-layout-tmux/layout.ts:102">
P1: The same member FIFO is attached to both tmux windows, so focus and grid panes will race to consume output and each view can miss chunks of the stream.</violation>
</file>

<file name="src/features/team-mode/team-runtime/delete-team.ts">

<violation number="1" location="src/features/team-mode/team-runtime/delete-team.ts:26">
P1: Member deletability is checked on a stale snapshot before the locked state transition, so a concurrent state update can make a member active again and still have this deletion path remove its worktree/runtime artifacts.</violation>

<violation number="2" location="src/features/team-mode/team-runtime/delete-team.ts:45">
P1: Use the lead session ID when cancelling team background tasks; using `teamRunId` leaves member tasks running after deletion.</violation>
</file>

<file name="src/features/team-mode/team-runtime/activate-team-layout.ts">

<violation number="1" location="src/features/team-mode/team-runtime/activate-team-layout.ts:29">
P2: Clean up the tmux layout if persisting `tmuxPaneId` fails; otherwise a state-store error during team creation leaks the new tmux session.</violation>
</file>

<file name="src/features/team-mode/tools/lifecycle.test.ts">

<violation number="1" location="src/features/team-mode/tools/lifecycle.test.ts:101">
P2: Await the `rejects` assertion so the test actually verifies the promise rejection.</violation>
</file>

<file name="src/features/team-mode/team-runtime/cleanup-team-run-resources.ts">

<violation number="1" location="src/features/team-mode/team-runtime/cleanup-team-run-resources.ts:58">
P2: Rollback leaves tmux layout resources behind if layout activation fails partway through, and it never removes the layout FIFO directory.</violation>
</file>

<file name="src/features/team-mode/team-runtime/create.ts">

<violation number="1" location="src/features/team-mode/team-runtime/create.ts:168">
P2: Mark the layout as created before awaiting `activateTeamLayout()`. Otherwise a post-create failure can skip tmux cleanup and leave the session running.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/hooks/team-session-streamer/fifo-writer.ts Outdated
Comment thread src/features/team-mode/team-layout-tmux/layout.ts Outdated
Comment thread src/features/team-mode/team-runtime/delete-team.ts
Comment thread src/features/team-mode/team-runtime/delete-team.ts Outdated
Comment thread src/plugin/event.ts Outdated
Comment thread src/features/team-mode/team-runtime/activate-team-layout.ts
Comment thread src/features/team-mode/tools/lifecycle.test.ts
Comment thread src/features/team-mode/team-runtime/cleanup-team-run-resources.ts
Comment thread src/features/team-mode/team-runtime/create.ts
Comment thread src/hooks/team-session-streamer/hook.ts Outdated
code-yeongyu added a commit that referenced this pull request Apr 17, 2026
…t.ts

The teamSessionStreamer hook was registered inside dispatchToHooks() at
line 275 AND invoked again directly at line 390, causing every message
event to be processed twice. Remove the outer invocation - the one
inside dispatchToHooks is enough and runs for every event.

Oracle review blocker (reported by oracle session verifying #3493).
code-yeongyu added a commit that referenced this pull request Apr 17, 2026
…matched)

createTeamRun launches member tasks with parentSessionID=leadSessionId,
but deleteTeam was calling bgMgr.getTasksByParentSession(teamRunId).
The keys never matched, so team_delete could tear down tmux+FIFO while
leaving member background tasks alive as zombies.

Use runtimeState.leadSessionId (guarded by truthiness) to match the
key used at launch, and add regression tests that catch future drift
between the two call sites.

Oracle review blocker (reported by oracle session verifying #3493).
code-yeongyu added a commit that referenced this pull request Apr 17, 2026
SDK v1 Event union does not include EventMessagePartDelta, but
OpenCode >=1.2.0 emits message.part.delta for streaming reasoning /
text chunks (see background-agent/manager.ts line 1007).

Without this handling, long provider responses that arrive as
incremental deltas never reach the tmux panes, making live streaming
effectively broken for any non-trivial output.

Extend the hook with a narrow custom union covering the runtime shape
{ sessionID, partID?, field?, delta } and write deltas straight to the
member FIFO. Add unit tests that cover:
  - multiple deltas appending to the same FIFO
  - non-text fields (e.g., tool) being ignored

Oracle review blocker (reported by oracle session verifying #3493).
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/features/team-mode/team-runtime/delete-team-bg-cancel.test.ts">

<violation number="1" location="src/features/team-mode/team-runtime/delete-team-bg-cancel.test.ts:80">
P2: This test never creates the absent-`leadSessionId` state it claims to cover, so it does not validate the no-cancellation branch.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/features/team-mode/team-runtime/delete-team-bg-cancel.test.ts
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 4 files (changes from recent commits).

Requires human review: Auto-approval blocked by 14 unresolved issues from previous reviews.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 3 files (changes from recent commits).

Requires human review: Auto-approval blocked by 13 unresolved issues from previous reviews.

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai Bot commented Apr 17, 2026

You're iterating quickly on this pull request. To help protect your rate limits, cubic has paused automatic reviews on new pushes for now—when you're ready for another review, comment @cubic-dev-ai review.

code-yeongyu added a commit that referenced this pull request Apr 18, 2026
…t.ts

The teamSessionStreamer hook was registered inside dispatchToHooks() at
line 275 AND invoked again directly at line 390, causing every message
event to be processed twice. Remove the outer invocation - the one
inside dispatchToHooks is enough and runs for every event.

Oracle review blocker (reported by oracle session verifying #3493).
code-yeongyu added a commit that referenced this pull request Apr 18, 2026
…matched)

createTeamRun launches member tasks with parentSessionID=leadSessionId,
but deleteTeam was calling bgMgr.getTasksByParentSession(teamRunId).
The keys never matched, so team_delete could tear down tmux+FIFO while
leaving member background tasks alive as zombies.

Use runtimeState.leadSessionId (guarded by truthiness) to match the
key used at launch, and add regression tests that catch future drift
between the two call sites.

Oracle review blocker (reported by oracle session verifying #3493).
code-yeongyu added a commit that referenced this pull request Apr 18, 2026
SDK v1 Event union does not include EventMessagePartDelta, but
OpenCode >=1.2.0 emits message.part.delta for streaming reasoning /
text chunks (see background-agent/manager.ts line 1007).

Without this handling, long provider responses that arrive as
incremental deltas never reach the tmux panes, making live streaming
effectively broken for any non-trivial output.

Extend the hook with a narrow custom union covering the runtime shape
{ sessionID, partID?, field?, delta } and write deltas straight to the
member FIFO. Add unit tests that cover:
  - multiple deltas appending to the same FIFO
  - non-text fields (e.g., tool) being ignored

Oracle review blocker (reported by oracle session verifying #3493).
code-yeongyu added a commit that referenced this pull request Apr 18, 2026
…t.ts

The teamSessionStreamer hook was registered inside dispatchToHooks() at
line 275 AND invoked again directly at line 390, causing every message
event to be processed twice. Remove the outer invocation - the one
inside dispatchToHooks is enough and runs for every event.

Oracle review blocker (reported by oracle session verifying #3493).
code-yeongyu added a commit that referenced this pull request Apr 18, 2026
…matched)

createTeamRun launches member tasks with parentSessionID=leadSessionId,
but deleteTeam was calling bgMgr.getTasksByParentSession(teamRunId).
The keys never matched, so team_delete could tear down tmux+FIFO while
leaving member background tasks alive as zombies.

Use runtimeState.leadSessionId (guarded by truthiness) to match the
key used at launch, and add regression tests that catch future drift
between the two call sites.

Oracle review blocker (reported by oracle session verifying #3493).
code-yeongyu added a commit that referenced this pull request Apr 18, 2026
SDK v1 Event union does not include EventMessagePartDelta, but
OpenCode >=1.2.0 emits message.part.delta for streaming reasoning /
text chunks (see background-agent/manager.ts line 1007).

Without this handling, long provider responses that arrive as
incremental deltas never reach the tmux panes, making live streaming
effectively broken for any non-trivial output.

Extend the hook with a narrow custom union covering the runtime shape
{ sessionID, partID?, field?, delta } and write deltas straight to the
member FIFO. Add unit tests that cover:
  - multiple deltas appending to the same FIFO
  - non-text fields (e.g., tool) being ignored

Oracle review blocker (reported by oracle session verifying #3493).
code-yeongyu added a commit that referenced this pull request Apr 19, 2026
…t.ts

The teamSessionStreamer hook was registered inside dispatchToHooks() at
line 275 AND invoked again directly at line 390, causing every message
event to be processed twice. Remove the outer invocation - the one
inside dispatchToHooks is enough and runs for every event.

Oracle review blocker (reported by oracle session verifying #3493).
code-yeongyu added a commit that referenced this pull request Apr 19, 2026
…matched)

createTeamRun launches member tasks with parentSessionID=leadSessionId,
but deleteTeam was calling bgMgr.getTasksByParentSession(teamRunId).
The keys never matched, so team_delete could tear down tmux+FIFO while
leaving member background tasks alive as zombies.

Use runtimeState.leadSessionId (guarded by truthiness) to match the
key used at launch, and add regression tests that catch future drift
between the two call sites.

Oracle review blocker (reported by oracle session verifying #3493).
code-yeongyu added a commit that referenced this pull request Apr 19, 2026
SDK v1 Event union does not include EventMessagePartDelta, but
OpenCode >=1.2.0 emits message.part.delta for streaming reasoning /
text chunks (see background-agent/manager.ts line 1007).

Without this handling, long provider responses that arrive as
incremental deltas never reach the tmux panes, making live streaming
effectively broken for any non-trivial output.

Extend the hook with a narrow custom union covering the runtime shape
{ sessionID, partID?, field?, delta } and write deltas straight to the
member FIFO. Add unit tests that cover:
  - multiple deltas appending to the same FIFO
  - non-text fields (e.g., tool) being ignored

Oracle review blocker (reported by oracle session verifying #3493).
code-yeongyu added a commit that referenced this pull request Apr 20, 2026
…t.ts

The teamSessionStreamer hook was registered inside dispatchToHooks() at
line 275 AND invoked again directly at line 390, causing every message
event to be processed twice. Remove the outer invocation - the one
inside dispatchToHooks is enough and runs for every event.

Oracle review blocker (reported by oracle session verifying #3493).
code-yeongyu added a commit that referenced this pull request Apr 20, 2026
…matched)

createTeamRun launches member tasks with parentSessionID=leadSessionId,
but deleteTeam was calling bgMgr.getTasksByParentSession(teamRunId).
The keys never matched, so team_delete could tear down tmux+FIFO while
leaving member background tasks alive as zombies.

Use runtimeState.leadSessionId (guarded by truthiness) to match the
key used at launch, and add regression tests that catch future drift
between the two call sites.

Oracle review blocker (reported by oracle session verifying #3493).
code-yeongyu added a commit that referenced this pull request Apr 20, 2026
SDK v1 Event union does not include EventMessagePartDelta, but
OpenCode >=1.2.0 emits message.part.delta for streaming reasoning /
text chunks (see background-agent/manager.ts line 1007).

Without this handling, long provider responses that arrive as
incremental deltas never reach the tmux panes, making live streaming
effectively broken for any non-trivial output.

Extend the hook with a narrow custom union covering the runtime shape
{ sessionID, partID?, field?, delta } and write deltas straight to the
member FIFO. Add unit tests that cover:
  - multiple deltas appending to the same FIFO
  - non-text fields (e.g., tool) being ignored

Oracle review blocker (reported by oracle session verifying #3493).
code-yeongyu added a commit that referenced this pull request Apr 20, 2026
…t.ts

The teamSessionStreamer hook was registered inside dispatchToHooks() at
line 275 AND invoked again directly at line 390, causing every message
event to be processed twice. Remove the outer invocation - the one
inside dispatchToHooks is enough and runs for every event.

Oracle review blocker (reported by oracle session verifying #3493).
code-yeongyu added a commit that referenced this pull request Apr 20, 2026
…matched)

createTeamRun launches member tasks with parentSessionID=leadSessionId,
but deleteTeam was calling bgMgr.getTasksByParentSession(teamRunId).
The keys never matched, so team_delete could tear down tmux+FIFO while
leaving member background tasks alive as zombies.

Use runtimeState.leadSessionId (guarded by truthiness) to match the
key used at launch, and add regression tests that catch future drift
between the two call sites.

Oracle review blocker (reported by oracle session verifying #3493).
code-yeongyu and others added 20 commits April 22, 2026 12:27
…created to tmux

Follow-up to e70972e. Oracle review flagged that parentID is not the only subagent marker in the plugin; subagentSessions (a Set<string>) is already used elsewhere in event.ts to classify sessions. If a session.created event arrived without parentID but was already marked in subagentSessions, event.ts's tmux dispatch silently regressed to the primary-session path.

This commit extends the Path A guard to skip dispatch when either parentID is present or subagentSessions marks the session. Diagnosis confirmed the marker is defined as the module-scoped export in src/features/claude-code-session-state/state.ts. Path B is synchronized because BackgroundManager adds the child session to subagentSessions immediately after session.create resolves and before create-managers.ts invokes its tmux callback. Path C is synchronized because executeSyncTask adds the sync session to subagentSessions before tool-registry.ts invokes onSyncSessionCreated.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Follow-up to 418ed82. Oracle review flagged four surviving edge cases,
all addressed in one cohesive commit so the force path covers the full
emergency-teardown contract users expect.

- Force delete now accepts 'creating' and 'orphaned' as source statuses,
  not just the graceful-shutdown statuses. Stuck-creating teams and
  lead-crashed teams can now be torn down.
- Lead-session death no longer blocks cleanup: team_delete with
  force=true against an orphaned team may be called by any current
  participant of that team, while non-participants remain rejected.
  Default (force=false) still enforces lead-only.
- Worktree cleanup now includes the lead member's worktreePath when
  present. Previous code only cleaned non-lead worktrees, leaking lead
  dirs.
- When removeTeamLayout throws during a force delete, the error is
  logged via the shared logger and cleanup continues. Previously the
  team was left stuck in status='deleting'. Non-force mode keeps the
  fail-fast behavior to avoid hiding real bugs.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…session.idle

Follow-up to cc8a52e. Oracle review flagged two surviving gaps in the
tmux pane lifecycle:

- waitForSessionReady previously treated "session exists in map" as
  ready, regardless of status. A session in an error / aborted / unset
  state would pass readiness and get a pane attached to a dead session.
  Readiness now requires status in {idle, running}, matching the
  attachable-status whitelist used elsewhere in the plugin.
- A readiness timeout was terminal. If the opencode session booted
  slowly or transiently dropped and recovered, the subagent was left
  without a pane for the rest of its life. The manager now records
  failed-readiness sessionIDs and, on a subsequent session.idle event
  where the status has become attachable, re-enters the spawn flow
  exactly once. pendingSessions remains the idempotency gate so
  back-to-back idle events cannot double-spawn.

Diagnosis notes:
- The attachable whitelist is now canonicalized in
  src/features/tmux-subagent/attachable-session-status.ts, mirroring the
  live-session semantics already implied by
  src/plugin/session-status-normalizer.ts and
  src/features/tmux-subagent/polling-manager.ts, where idle and running
  are the non-terminal states that stay attachable.
- session.idle reaches TmuxSessionManager through src/plugin/event.ts,
  which forwards tmux activity events into TmuxSessionManager.onEvent();
  the manager now consumes that path for the one-time retry.
- pendingSessions is still cleared in both the queued spawn finally
  block and the outer onSessionCreated finally, so a timed-out session
  can re-enqueue cleanly without regressing the cc8a52e atomic dedup.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Follow-up to 91e39c5. Oracle flagged the post-create subagentSessions.add() writes in background-agent and delegate-task as a possible race if session.created arrives before the Set mutation.

Diagnosis: src/features/background-agent/manager.ts already passes parentID: input.parentSessionID into session.create(), and src/tools/delegate-task/sync-task.ts creates through src/tools/delegate-task/sync-session-creator.ts which already passes parentID: input.parentSessionID as well. grep for session.create( in src/ found 8 call sites; every subagent-intended creator already passes parentID (hooks/ralph-loop/session-reset-strategy.ts, features/background-agent/spawner.ts, features/background-agent/manager.ts, tools/look-at/look-at-session-runner.ts, tools/call-omo-agent/session-creator.ts, tools/call-omo-agent/subagent-session-creator.ts, tools/delegate-task/sync-session-creator.ts). The only root-session creator is src/cli/run/session-resolver.ts.

SDK check: node_modules/@opencode-ai/sdk/dist/gen/types.gen.d.ts exposes SessionCreateData.body.parentID?: string, and EventSessionCreated carries properties.info: Session where Session.parentID?: string. That makes parentID event-visible on the first session.created event.

Fix: Strategy A. Add event-level regression coverage proving Path A already skips tmux dispatch when parentID is present even if subagentSessions is still empty, then proving delayed Set population remains only a secondary marker on later events.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…ry path

Follow-up to 71cf0f1. Oracle review flagged that the new
failedReadinessSessions retry path in TmuxSessionManager.onEvent() was
never invoked in production: src/plugin/event.ts only forwarded message
activity events to the tmux manager, not session.idle. The retry tests
passed by calling manager.onEvent directly, so the integration was
dead code in production.

Forwarded session.idle events to managers.tmuxSessionManager.onEvent so
the retry-on-later-idle path actually runs end-to-end. Added an
event-handler-level integration test that locks this wiring in.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…l sources

Follow-up to a5e0f3a. Wiring session.idle forwarding into the tmux
manager exposed another churn path: opencode can fire two real
session.idle events in quick succession (observed during compaction
and retry-after-error), and the existing synthetic↔real dedup only
prevented collisions between the two sources, not repeats within the
same source.

Added recentAnyIdles<sessionID, timestamp>. Idle events for the same
sessionID within DEDUP_WINDOW_MS (500ms) now drop regardless of source.
Different sessionIDs are unaffected. Synthetic↔real semantics are
preserved — the new gate is strictly additive.
…timeout

SESSION_MISSING_GRACE_MS of 6s was too aggressive — transient status
query failures under load produced false "missing" detections that
closed live panes. Bumped to 30s.

SESSION_TIMEOUT_MS of 10min unconditionally closed sessions regardless
of recent activity. Bumped to 60min to stop interrupting legitimate
long-running work.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
failedReadinessSessions accumulates sessionIDs whose readiness wait
timed out. If session.idle never fires for those sessions, entries
stayed forever. Added 5 minute TTL with periodic sweep so the map
cannot grow unbounded across long-running plugin lifecycles.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
User-visible symptom: tmux subagent panes "open then immediately close" even after wait-before-attach + attachable-status + session.idle retry fixes. Team-layout panes do not exhibit this because they already pass --dir.

Root cause: pane-spawn.ts / window-spawn.ts / session-spawn.ts / pane-replace.ts construct `opencode attach <url> --session <id>` without --dir. opencode attach inherits the tmux pane's cwd rather than the session's directory, which can cause the TUI to exit at startup and the pane to die. Team layout has always passed --dir and works correctly.

This commit threads the subagent manager's working directory through the four spawn helpers, bringing them in line with the team-layout contract. Attach now receives the correct directory for every subagent pane.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
tryAttachDeferredSession bypassed beginPendingSession, so a deferred
session could race with the onSessionCreated spawn path and create two
panes for the same sessionID. Added beginPendingSession at the top of
tryAttachDeferredSession with proper release on both success and
failure finally blocks.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…y window

Stability close previously ran a status recheck after 3 stable-idle
polls, but ignored activityVersion for that recheck window. A message
arriving between the first stable observation and the recheck would
not cancel the close. Now activityVersion is captured at first stable
observation and the close aborts if any new activity landed before the
recheck confirms idle.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…a retry

The retryFailedReadinessSession path was designed to recover from
transient readiness failures when session.idle arrives later. But it
also resurrected sessions that the polling manager had intentionally
closed — a tight open/close/open loop visible to users as panes
"bouncing".

Added closedByPolling: Set<string>. PollingManager-initiated closes now
mark the sessionID; retryFailedReadinessSession and the deferred-attach
loop refuse to re-enqueue for marked IDs. The set is cleared when the
session is actually deleted (onSessionDeleted) so that a legitimate
fresh session with the same ID (unlikely) can still spawn.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…pending sessions

retryPendingCloses force-removed sessions from tracking after 3 close
retries, producing zombie tmux panes that remained on screen without
a tracked session. Now checks whether the pane still exists in tmux
before declaring force-remove necessary. If the pane is already gone,
mark as a normal close. If it still exists, log loudly and leave the
session in tracking for manual intervention (rather than silently
orphaning it).

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…eticIdles

Follow-up to 94cf139. The dedup commit added recentAnyIdles as a
required parameter to pruneRecentSyntheticIdles but the helper's test
file was not updated in the same commit. This completes the signature
migration so every test call passes the new map.
…sion is still alive

User report: team-member background task was reported as
[BACKGROUND TASK ERROR] Out of memory, but the actual opencode session
remained idle and alive in team_status — "fail이 아닌데 fail로 뜸".

Root cause: handleSessionErrorEvent() always marked the task terminal
once fallback retry declined, regardless of whether the underlying
session actually died. The opencode SDK can emit session.error with
transient payloads ("Out of memory" from a single overloaded tool call,
serialization failure, provider retry, etc.) without killing the
session. After terminal marking, the checks at manager.ts:1130 and
manager.ts:1654 blocked any recovery path, so subsequent session.idle
never completed the task, producing the "task errored but member still
idle" divergence.

Fix: before marking a task terminal on session.error, call
verifySessionExists() to check opencode's live session registry. When
the session is still alive, log a warning and let the normal
completion path (session.idle + output validation OR session.deleted)
take over. Only when the session is truly gone do we commit the
terminal error — preserving the existing terminal path for real
crashes.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Follow-up to 74c1942. Two defects in the subagent spawn helpers prevented panes from appearing at all for users whose project path or cwd had spaces/special characters, or whose ctx.directory was empty:

1. The --dir value was escaped via shellEscapeForDoubleQuotedCommand, which only escapes special chars and does NOT wrap the value in quotes. Paths with spaces split into multiple shell arguments, so    \ became two args and the CLI rejected it — the pane exited instantly.
2. No fallback: if ctx.directory was empty/undefined, the command ended with a bare    \ followed by nothing, which opencode also rejects.

team-layout-tmux/layout.ts already solved both by wrapping in single quotes and falling back to process.cwd(). This commit:
- Extracts shellSingleQuote() to src/shared/shell-env.ts.
- Threads it through pane-spawn.ts / window-spawn.ts / session-spawn.ts / pane-replace.ts for the --dir value.
- Adds a process.cwd() fallback in the helpers AND in TmuxSessionManager.projectDirectory (defense in depth).
- Removes the duplicate shellSingleQuote from layout.ts, importing the shared one instead.

Subagent panes now spawn correctly regardless of project path characters or empty directory input.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
…g promotion

Adds negative regression tests that load plugin config with canonical +
legacy basenames coexisting and prove team_mode.tmux_visualization stays
false unless the canonical config opts in explicitly.

Extends the migrator test to prove legacy team_mode.tmux_visualization=true
is not copied into an existing canonical config during the transition
window.

The observed runtime contradiction (tmuxPaneId present in state while
tmux_visualization defaulted false) traces to stale prior-run state, not
a loader leak. This lane locks the contract so any future regression
would be caught at the config-loading boundary.
…k grid panes for cleanup

Previous behavior: createTeamLayout unconditionally ran
  new-session -d -s omo-team-<teamRunId>
and created focus / grid windows inside that detached sibling session.
No code in src/ called switch-client, attach-session, move-window, or
join-pane, so team panes were invisible to a user already inside tmux,
even when tmux_visualization=true. Panes that died on opencode attach
failure took their windows with them, leaving only the default
new-session shell behind and the runtime state holding phantom pane ids.

New behavior: when TMUX_PANE resolves to a session id via
  tmux display -p -F '#{session_id}' -t $TMUX_PANE
team windows are created inside that session via
  new-window -t <session_id>
with -d so the caller's focus is not stolen. Window names become
focus-<shortTeamRunId> and grid-<shortTeamRunId>. When the caller
cannot be resolved, the legacy detached-session path is preserved as a
fallback and flagged via ownedSession=true.

TeamLayoutResult now carries focusPanesByMember, gridPanesByMember,
targetSessionId, and ownedSession so state and cleanup know which
windows they own. RuntimeStateMemberSchema gains optional
tmuxGridPaneId, and RuntimeStateSchema gains optional tmuxLayout
cleanup metadata. Both additions are idempotent and backward compatible.

activateTeamLayout now persists both pane ids per member plus the
tmuxLayout descriptor. removeTeamLayout is symmetric with topology:
ownedSession=false runs kill-window on the two known window ids
(tolerating individual failures so one stale id cannot block the
other); ownedSession=true falls back to legacy kill-session.
closeTeamMemberPane fans out to both panes and succeeds when either
close succeeds. delete-team and cleanup-team-run-resources pass the
persisted tmuxLayout descriptor into removeTeamLayout.

Tests cover: caller-session topology with -t $7, the new
TeamLayoutResult shape (distinct focus and grid pane maps), the
fallback path when TMUX_PANE cannot be resolved, dual-pane
closeTeamMemberPane behavior, and the new removeTeamLayout paths for
both ownedSession branches.
…fter caller-session rollout

Adds an opt-in live tmux smoke test gated by OMO_LIVE_TMUX=1. It creates
a real caller tmux session, runs createTeamLayout with two mock members,
asserts the two expected caller-session windows appear, runs
removeTeamLayout with the new ownedSession=false path, and asserts only
those windows are gone and the caller session survives. Skipped in CI
by default.

Extends the stale-session sweeper tests to cover two post-rollout
realities: empty candidate lists no-op safely (no kill-session calls),
and legacy omo-team-<uuid> orphans from pre-rollout runs are still
killed for backward-compat cleanup.

Verification:
  bun run script/run-ci-tests.ts -> 4967 pass / 1 skip / 0 fail
  bun run typecheck               -> clean
  OMO_LIVE_TMUX=1 bun test
    src/features/team-mode/team-layout-tmux/live-tmux-smoke.test.ts
                                  -> 1 pass (end-to-end verified)
…tach

The previous approach passed the opencode attach command as the
positional command argument to tmux new-window and split-window. tmux
executes that argument through the user's default shell, which varies
across environments (fish, zsh, bash, powershell). Shell-specific
quoting and argument parsing differences caused the command to fail
silently, killing the pane immediately and triggering tmux to reap the
empty window before the user could see it.

New approach: create windows and panes with no command (starts the
user's default shell), then inject the attach command via send-keys -l
(literal mode, no key-name interpretation) followed by send-keys Enter.
This is shell-agnostic because send-keys types raw characters into
whatever shell is running. If opencode attach exits, the shell remains
alive, keeping the pane visible for debugging.

Defense-in-depth: set remain-on-exit on at window level immediately
after creation, so even if the shell itself crashes, the pane and
window survive for inspection.
…iness

Replace command-arg approach with send-keys -l (literal) + Enter for
injecting the opencode attach command into team panes. This is shell
agnostic: works with fish, zsh, bash, powershell, or any shell tmux
supports.

Key changes:
- new-window and split-window no longer receive a command argument;
  panes start with the user's default shell
- buildAttachCommand returns a plain command string without shell
  wrappers; no quoting gymnastics needed since send-keys types raw
  characters
- waitForPaneShellReady polls pane_current_command via tmux display
  before sending keys, preventing the attach command from racing
  against shell initialization
- remain-on-exit set at window level as defense-in-depth

Verified: grid panes show live OpenCode TUI (Sisyphus-Junior GPT-5.4
mini + lead Opus 4.6) after team_create. team_delete removes windows
cleanly.
Users couldn't see team panes because focus/grid windows were created
with -d (detached). Now select-window switches to the grid window
(tiled layout showing all members at once) immediately after creation.
This also triggers a terminal resize signal that forces the attach
TUI to re-render.
… make task transitions idempotent

Members had no idea team_send_message, team_task_update, or
team_shutdown_request existed. buildMemberPrompt only injected team
name, member name, and the user-provided prompt. Appends a
TEAMMATE_COMMUNICATION_ADDENDUM matching Claude Code's
TEAMMATE_SYSTEM_PROMPT_ADDENDUM pattern: explains that text responses
are not visible to others, lists the team tools with usage, and
instructs the standard completion flow (send results, mark task
completed, request shutdown).

Also makes task status transitions idempotent: updating a task to its
current status is now a no-op instead of throwing
InvalidTaskTransitionError. This prevents the common case where a
member calls team_task_update(completed) twice (once after work, once
in the completion flow).

Layout.ts: split-window in current window instead of new-window, leader
30% left + teammates 70% right, pane creation directly in caller
window, kill-pane for cleanup.
…uxBackend pattern

Rewrites createTeamLayout to match Claude Code's TmuxBackend:
- Splits the current window instead of creating new windows
- Leader stays at 30% left, teammates get 70% right
- First teammate: split-window -h -l 70% from leader pane
- Additional teammates: alternating -v/-h splits from existing panes
- Pane creation lock prevents parallel split race conditions
- 200ms shell init delay before send-keys
- main-vertical layout with leader resize after each split
- Pane titles with cyan border color
- Cleanup kills individual panes then rebalances remaining layout
- No window switching needed, panes appear immediately in view
Rewrite team-mode tmux visualization to follow Claude Code's TmuxBackend pattern instead of creating detached focus/grid windows.

New behavior:
- split panes in the current leader window instead of opening separate windows
- keep the leader pane in place and add teammates beside it
- first teammate gets a horizontal split from the leader pane
- later teammates continue splitting the active teammate area
- send attach commands via send-keys after a short shell-init delay
- cleanup kills individual panes instead of tearing down whole windows

Also rebaseline the layout unit tests to assert the current-window split contract rather than the removed focus/grid window topology, and include the regenerated schema artifact from the verified build.

Verification:
- bun test src/features/team-mode/team-layout-tmux/layout.test.ts
- bun test src/features/team-mode/integration.test.ts
- bun run typecheck
- bun run build
- bun run script/run-ci-tests.ts
@code-yeongyu code-yeongyu force-pushed the feat/team-mode branch 2 times, most recently from 88782e3 to acdd515 Compare April 22, 2026 07:33
The test relied on registeredAgentAliases being populated by other
tests in the shared suite. When run in CI isolation, the registry was
empty so resolveRegisteredAgentName returned undefined instead of the
expected agent name. Fix by calling registerAgentName for the agents
used in test fixtures (sisyphus-junior, atlas) in beforeEach.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant