feat(team-mode): Claude Code Agent Teams parity (OFF by default, 12 team_* tools)#3493
feat(team-mode): Claude Code Agent Teams parity (OFF by default, 12 team_* tools)#3493code-yeongyu wants to merge 158 commits intodevfrom
Conversation
There was a problem hiding this comment.
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 causesdoctorto runensureBaseDirs(), which can create or re-permission~/.omoduring 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.mdis low severity and mainly affects copy-paste success (subagent_type: explorefails validation). - Pay close attention to
src/cli/doctor/checks/index.tsanddocs/guide/team-mode.md- prevent side effects indoctorand 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, |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
…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).
…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).
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).
There was a problem hiding this comment.
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.
|
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 |
…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).
…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).
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).
38d66d8 to
0c97702
Compare
…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).
…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).
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).
07b6164 to
82a60ca
Compare
…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).
…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).
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).
d3127a0 to
031b885
Compare
…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).
…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).
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).
646344d to
371250b
Compare
…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).
…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).
…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.
22fa6fb to
2457284
Compare
…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
88782e3 to
acdd515
Compare
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.
acdd515 to
d2f03ac
Compare
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
team_mode.enabled: falseby default. Zero impact on existing users.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.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).pendingInjectedMessageIdsin durable RuntimeState; ack happens atsession.idle(D-15). Crash before idle → message re-delivered next session.team_send_messagewrites live-recipient messages directly under.delivering-<id>.jsonso the transform-hook fallback cannot double-inject the envelope whilepromptAsyncis in flight. Atomic rename toprocessed/on success, back to the unread slot on failure, reclaim-on-resume for stranded reservations.opencode attachagainst the member session. Failures isolated — never blocks team creation (D-34).bunx oh-my-opencode doctorreports team-mode status, dependencies, declared/runtime team counts.Architecture
Storage layout
Key invariants
BackgroundManager.launch()onlysession.idlelockPathparseMemberemits §V.3 verbatim errorsmcpConfigToolRegistryinsteadteam_createblocked for existing participantsdelegate-taskbudget = 0<peer_message ...>envelope literal.delivering-*reservation at write timeTests
bun run script/run-ci-tests.ts(full suite with proper isolation).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 typecheckexit 0.Documentation
docs/guide/team-mode.md(refreshed foropencode attachtmux flow +.delivering-*reservation file)src/features/team-mode/AGENTS.mdbunx oh-my-opencode doctorReviewer notes
../free-code(Claude Code experimental Agent Teams) and../opencode(plugin API + session API).Out of scope (per Momus iteration 5/6 lock-in)
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-streamerhook (enqueue ordering, drain mutex, scheduler stop/dispose split, generation tokens, pre-mapping state, polling-based retry, etc.), Oracle verified the final code oned7a816cagainst a fully interactive terminal QA of the whole lifecycle in one fresh run.Oracle R20 verdict: VERIFY_PASS.
Evidence: a single fresh
opencode runexecuted the entire lifecycle end-to-end in one continuous transcript:team_create→ member spawn →team_status× 2 →team_shutdown_request+team_approve_shutdownfor 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 attachdirectly, 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-modebetween063e3c53..646344df.Round 1 — 4 bugs in durable runtime behavior:
poll.ts—pendingInjectedMessageIdsaccumulated duplicates of the same messageId every turn without ack landing (observed 8-12 duplicates per message in production state.json). Dedupe viaArray.from(new Set([...])). →88e9b34cmessaging.ts—deliverLive()calledpromptAsyncbeforeackMessages, so the transform-hook fallback could read the still-unread inbox file and inject the envelope a second time when promptAsync landed. Introducedteam-mailbox/reservation.tswith atomic rename-based reservation. →c15c1e00messaging.ts— broadcastactiveMembersfiltered 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. →737e6eb6resume.ts—resumeAllTeamsonly verified the lead session; teams with every worker dead but an alive lead stayedactivepointing to phantom sessions. Added worker-session inspection with orphan-on-all-dead semantics. →70ed63f2Round 2 — 3 follow-up bugs Oracle caught in the R1 fixes:
sendMessagewriting<id>.jsonanddeliverLiverenaming it. Moved reservation to write time viaSendContext.reservedRecipients;sendMessagenow writes live-recipient messages directly under.delivering-<id>.json. MadereserveMessageForDeliveryidempotent (pre-reserved stat OR on-the-fly rename for the rare session-appears-after-send case). →d3c468c8.delivering-*files were invisible tolistUnreadMessages(dotfile filter) and only logged release failures. AddedreclaimStaleReservations(teamRunId, memberName, config, ttl), called per member during resume with a 10 minute TTL. →b2180928+290e189binspectWorkerMemberscounted everysessionId === undefinedworker 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 stayedactivewith zero live workers. Fixed by checkingmember.status === "errored"first. →65b49bf6Round 3 — 1 regression Oracle caught in the R2 fixes:
getUnreadSizeBytesexcluded all dotfiles, so pre-reserved.delivering-*.jsonmessages did not count towardrecipient_unread_max_bytes. Concurrent sends could stack arbitrary in-flight traffic behind a slow live recipient. Tightened the filter to include.delivering-*.jsonwhile still excluding lock/metadata dotfiles. →646344dfOracle 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.ts→ 4888 pass / 0 failbun run typecheck→ exit 0poll.ts,send.ts,reservation.ts,messaging.ts,resume.ts)opencode attachtmux flow →052d1d1eSummary 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 viaopencode attach. Off by default; enable withteam_mode.enabledto surface theteam-modeskill and a new doctor check.New Features
team_modeconfig and JSON schema (bounds, defaults); doctor check validates config, dirs, and tmux/git; builtinteam-modeskill is config‑gated..delivering-*.jsonto prevent double‑inject; pending IDs deduped; reservations count toward backpressure and are reclaimed on resume.opencode attach; helpers close panes, rebalance windows, and sweep staleomo-team-*sessions.Refactors
opencode attach; tmux runner hardened with retry/timeout and safer attach command.session.erroras transient if the session still exists.plugin.json, with strict manifest name matching and deterministic version selection..sisyphus/fully ignored.Written for commit d2f03ac. Summary will update on new commits.