The lead currently leaves teams alive after the task list drains because
none of the prompt surfaces tell it WHEN to close or HOW. omx-style
'self-closing' behavior was missing for four reasons (diagnosed via
prompt-engineering A/B/C: wrong / misframed / missing):
1. builtin team-mode skill 'Lifecycle' (B+C): 'phase ends / shape
outgrown' is qualitative, so the model maps it to 'wait for user'.
Step 6 jumped to team_delete without the request/approve pair the
tool contract requires. Replaced with a 'Closure Contract' (a
computable predicate over team_task_list + team_status) and an
explicit 'Closure Sequence' (request -> approve -> delete, with
force=true reserved for unrecoverable paths only).
2. TEAM_MESSAGE keyword injection (C): spent 100%% of its one-shot
budget on routing ('do not substitute delegate_task'), 0%% on
closure. Added the same closure rule in compressed form. Kept the
'NEVER substitute with delegate_task' literal that
keyword-detector/index.test.ts depends on.
3. team-mode-status-injector body (C): the only per-session injection
for team mode had no closure obligation. Replaced the optional
'load the team-mode skill ... otherwise use the team_* tools'
sentence with a 'Closure invariant' clause that ties the check to
every team_task_update.
4. member-guidance Wrap-up (A+B): step 3 said 'so the lead can decide
whether to request shutdown', but team_shutdown_request is
lead-only - members cannot initiate it. Step ordering also placed
the completion message before team_task_update, so the lead's
closable check would see stale data. Reordered to
task_update -> check task_list for new work -> if nothing left,
send a single 'closure-ready' message and idle. Test assertion
updated to match the new accurate contract.
Also: stripped Korean alternation from TEAM_PATTERN per directive
('절대로 코드 내에 한국어 적지 마라'). Pattern is now
/\\bteam[\\s_-]?mode\\b/i. Removed 4 Korean test cases
(2 positive triggers + 2 false-positive guards) that the pattern no
longer needs to defend, and updated the keyword-detector AGENTS.md
row.
Net: -71 lines across prompt surfaces. The Closure Contract is the
only addition; everything else tightened.
Tests: 428/428 pass across src/features/team-mode/,
src/features/builtin-skills/, src/hooks/keyword-detector/,
src/hooks/team-mode-status-injector/, src/hooks/team-mailbox-injector/,
src/hooks/team-tool-gating/, src/hooks/team-session-events/.
LSP: no errors introduced (one pre-existing error in
keyword-detector/index.test.ts confirmed pre-existing on dev).
- prometheus-prompt.test.ts: close missing }) on the OpenSpec expanded
commands describe block (introduced by d66b6bcbf, parse error).
- agent-sort-shim/agent-config-integration/continuation-injection/
unstable-agent-babysitter/subagent-resolver/sync-executor/
resolve-caller-team-lead tests: expect 'Sisyphus - ultraworker'
(lowercase) to match production after cd39f8858, which lowercased the
display name to dodge a TUI ZWSP rendering glitch. Legacy uppercase
inputs that exercise the normalization path are preserved.
- sync-executor.ts + resolve-caller-team-lead.ts: route legacy display
name inputs through normalizeAgentForPrompt so prompt agent names and
caller team lead lookups produce the canonical lowercase form.
ensureBaseDirs unconditionally called chmod(baseDir, 0o700) on every startup
and on every team_create. On filesystems where the OS rejects chmod for the
directory (network mounts, SIP-protected locations, non-owner cases on macOS
shared by multiple GUI users), the call raises EPERM and the entire team-mode
init aborts:
[team-mode] init failed: EPERM: operation not permitted, chmod '/Users/<u>/.omo'
Wrap chmod through a small safeChmod helper that converts EPERM, ENOTSUP, and
EINVAL into a single warning log and continues. mkdir already creates new
directories with mode 0o700, and the existing post-creation stat-guard remains
in place for the case where the directory pre-exists with a different mode and
chmod is permitted, so the security envelope on supported filesystems is
unchanged. All other error codes (ENOENT, EACCES, etc.) still propagate.
Regression test mocks node:fs/promises.chmod to throw EPERM and asserts that
ensureBaseDirs completes successfully and emits exactly the documented warning.
When oh-my-opencode created a delegated tmux pane, terminal
capability/color probe replies emitted by tmux or the freshly
attaching opencode session could end up in the caller pane's
input buffer instead of being consumed by the delegated pane,
appearing as literal text in the main OpenCode chat (e.g.
"414/21212a2/...").
Root cause: buildSplitArgs in team-layout-tmux/layout.ts called
split-window without the -d (detached/don't-switch-focus) flag.
Without -d, tmux briefly grants focus to the new pane during
creation; the outer terminal then sends DA1/DA2 and OSC color
probe replies into what it believes is the active pane, but the
focus handoff races and those bytes land in the caller pane's
stdin buffer instead.
Fix: add -d to every split-window call in buildSplitArgs, matching
the same flag already used in pane-spawn.ts for inline subagent
panes. This keeps the caller pane focused throughout the delegated
pane lifecycle so probe replies are consumed by the correct target.
Existing tests pass; one new test asserts -d is present on every
split-window call to guard this invariant.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a hard-reject agent (e.g. prometheus) called team_create with an
explicit `lead` in the spec, the eligibility check that runs in the
no-lead branch of shouldReuseCallerLeadSession was bypassed. The
caller session was never registered in the team, the spawned lead
ran as a detached child, and replies routed to the spawned lead
never reached the caller — the caller became an orphan that could
send but never receive.
Move the caller eligibility guard to the top of team_create.execute
so it runs unconditionally before any team-run state mutates. Throw
an actionable error naming the agent and explaining hard-reject
agents cannot lead teams regardless of an explicit `lead` in the
spec.
When ctx.serverUrl had a port string of "0", TmuxSessionManager
silently replaced it with the localhost:4096 fallback and
createTeamLayout subsequently skipped pane creation without any
user-visible signal. The two-step silent failure made team_mode
tmux_visualization look broken in default TUI mode.
Surface the failure path:
- TmuxSessionManager now retains ctx.serverUrl on the instance and
exposes it via getCtxServerUrl(), and emits a structured warning
log on the port-0 fallback branch naming both the discarded URL
and the fallback it landed on.
- createTeamLayout's "opencode server not reachable" log is
upgraded to a structured warning including ctxServerUrl and a
hint to launch with --port N + OPENCODE_PORT=N.
No behavior change to the fallback resolution itself - only the
silence. Existing port-0 fallback tests still pass; two new tests
assert the warning fires on port 0 and is absent for real ports.
Keep prompt reservations briefly after successful dispatch so rapid idle/message/error transitions cannot inject the same follow-up twice.
Route all production session prompt calls through the shared gate, restore skipped background resume state, release holds after abort/recovery paths, and preserve Ralph/ULW loop state when a dispatch is deferred.
Add regression coverage for session routing, static prompt route auditing, team-mode live messaging, model suggestion retries, call-omo-agent reuse, background parent wakes, runtime fallback, compaction recovery, Atlas, and Ralph/ULW loops.
Eliminates 19 unguarded `Bun.*` runtime call sites in the plugin bundle
that crashed with `ReferenceError: Bun is not defined` under Electron.
Per-tool-call hot paths (executed on every Read/Edit):
- src/tools/hashline-edit/hash-computation.ts: Bun.hash.xxHash32 → bunHashXxh32
- src/tools/hashline-edit/hashline-edit-executor.ts: 8 sites via bunFile/bunWrite
- src/hooks/hashline-read-enhancer/hook.ts: Bun.file → bunFile
- src/hooks/hashline-edit-diff-enhancer/hook.ts: 2 sites via bunFile
Plugin-load paths:
- src/hooks/claude-code-hooks/config.ts and config-loader.ts: Bun.file → bunFile
- src/features/claude-code-mcp-loader/loader.ts: Bun.file → bunFile
- src/features/claude-code-plugin-loader/mcp-server-loader.ts: Bun.file → bunFile
- src/features/team-mode/deps.ts: Bun.spawn → spawn shim
- src/hooks/session-notification-utils.ts: Bun.which → bunWhich, also drops
the bare `declare const Bun` ambient declaration
- src/shared/binary-downloader.ts: Bun.write → bunWrite
Pure mechanical API swaps. No control-flow or signature changes.
Replaces direct fileHandle.sync() calls in acquireLock and atomicWrite
with tolerantFsync. Users on iCloud Drive / OneDrive / Desktop sync
folders were hitting 'EPERM: operation not permitted, fsync' during
team_create, which propagated up and aborted the entire team_create
flow even though the actual write+rename had succeeded.
Reported on Discord (omo 4.0.0, opencode desktop 1.14.41, project on
synced Desktop). atomicity is preserved by the temp-file rename; only
the durability hint is now best-effort on filesystems that disallow
fsync.
Source code change:
- src/shared/model-requirements.ts: prepend claude-sonnet-4-6 to metis fallback
chain so Sonnet becomes the default. Opus 4.7 max remains as the immediate
fallback for callers who want extra reasoning.
- src/shared/model-requirements.test.ts: update assertion to expect Sonnet
primary + Opus secondary.
AGENTS.md accuracy fixes (verified against source):
- Agent modes: Sisyphus/Hephaestus are 'primary' (not 'all'); Sisyphus-Junior
is 'subagent' (not 'all'). Confirmed via 'const MODE: AgentMode = ...' in
each agent file. Also clarified Prometheus has no agentSources factory and
is built via buildPrometheusAgentConfig.
- Sisyphus fallback chain: corrected order to kimi-k2.6 → k2p5 → kimi-k2.5
→ gpt-5.5 medium → glm-5 → big-pickle (was missing kimi-k2.5).
- Librarian/Explore: added missing minimax-m2.7 step between -highspeed and
claude-haiku-4-5.
- Metis chain: removed fictitious gemini-3.1-pro entry.
- Sisyphus-Junior chain: spelled out the actual fallback (was 'user-configurable').
- Temperatures: Sisyphus/Hephaestus do not set explicit temperature (model
default); Sisyphus-Junior is 0.1 via SISYPHUS_JUNIOR_DEFAULTS.
- Quick category default: gpt-5.4-mini (not gpt-5.4-mini-fast).
Team-mode corrections:
- Eligibility registry has 3 verdicts: eligible (sisyphus, atlas, sisyphus-junior),
conditional (hephaestus — needs D-36 teammate permission), hard-reject
(oracle, librarian, explore, multimodal-looker, metis, momus, prometheus).
- Schema has 11 fields, not 4: added max_messages_per_run, max_wall_clock_minutes,
max_member_turns, base_dir, message_payload_max_bytes, recipient_unread_max_bytes,
mailbox_poll_interval_ms.
- Hooks: 'team-session-events' is 4 sub-handlers in src/plugin/event.ts
(team-idle-wake-hint, team-lead-orphan-handler, team-member-error-handler,
team-member-status-handler), not a single Continuation-tier hook.
- Tier counts now show base + team-mode: ToolGuard 14/15, Transform 5/7.
- Total: 52 base hooks, 59 with team-mode.
Doc cascade for the Metis change:
- docs/guide/orchestration.md, agent-model-matching.md, installation.md
- docs/reference/configuration.md, features.md