affaan-m/ECC · error
tmux split-window did not return a pane id for ${workerPlan.
Error message
tmux split-window did not return a pane id for ${workerPlan.workerName} What it means
Thrown after `tmux split-window -d -P -F #{pane_id} -t <session> -c <worktree>` returns exit 0 (so runCommand did not throw) but splitResult.stdout.trim() is empty, meaning tmux did not emit the requested pane id. The orchestrator needs that id to select-pane, title it, and send-keys into the new worker, so it cannot continue without it. A blank pane id almost always indicates an incompatibility between the flags given and the installed tmux version/config, or stdout being swallowed.
Source
Thrown at scripts/lib/tmux-worktree-orchestrator.js:550
'send-keys',
'-t',
plan.sessionName,
buildSessionBannerCommand(plan.sessionName, plan.coordinationDir),
'C-m'
],
{ cwd: plan.repoRoot }
);
for (const workerPlan of plan.workerPlans) {
const splitResult = runCommandImpl(
'tmux',
['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', plan.sessionName, '-c', workerPlan.worktreePath],
{ cwd: plan.repoRoot }
);
const paneId = splitResult.stdout.trim();
if (!paneId) {
throw new Error(`tmux split-window did not return a pane id for ${workerPlan.workerName}`);
}
runCommandImpl('tmux', ['select-layout', '-t', plan.sessionName, 'tiled'], { cwd: plan.repoRoot });
runCommandImpl('tmux', ['select-pane', '-t', paneId, '-T', workerPlan.workerSlug], {
cwd: plan.repoRoot
});
runCommandImpl(
'tmux',
[
'send-keys',
'-t',
paneId,
`cd ${shellQuote(workerPlan.worktreePath)} && ${workerPlan.launchCommand}`,
'C-m'
],
{ cwd: plan.repoRoot }
);
}View on GitHub (pinned to 01e15490f0)
Solutions
- Check the version: `tmux -V`. The orchestrator requires a modern tmux (>= 3.0 recommended); upgrade via your package manager if older.
- Test the flag combo directly: `tmux new -d -s t && tmux split-window -d -P -F '#{pane_id}' -t t` — if it prints nothing, the problem is your tmux build/config, not the script.
- Temporarily move ~/.tmux.conf aside and retry; if it works, a hook/option in your config is intercepting the split.
- Ensure the session named in plan.sessionName is still alive right before the worker loop (a dead session makes split-window produce no pane).
Example fix
// before
const splitResult = runCommandImpl('tmux',
['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', plan.sessionName, '-c', workerPlan.worktreePath],
{ cwd: plan.repoRoot });
const paneId = splitResult.stdout.trim();
if (!paneId) throw new Error(`tmux split-window did not return a pane id for ${workerPlan.workerName}`);
// after — fall back to listing panes of the session when -P/-F yields nothing
let paneId = splitResult.stdout.trim();
if (!paneId) {
const list = runCommandImpl('tmux',
['list-panes', '-t', plan.sessionName, '-F', '#{pane_id}:#{pane_current_path}'],
{ cwd: plan.repoRoot });
paneId = list.stdout.trim().split('\n')
.find(line => line.endsWith(`:${workerPlan.worktreePath}`))
?.split(':')[0] || '';
}
if (!paneId) throw new Error(`tmux split-window did not return a pane id for ${workerPlan.workerName}`); Defensive patterns
Strategy: try-catch
Validate before calling
// Validate the tmux build supports the split-window -P -F combination up front.
const { spawnSync } = require('child_process');
function tmuxSupportsSplitFormat() {
const v = spawnSync('tmux', ['-V'], { encoding: 'utf8' }).stdout?.trim();
const m = /tmux (\d+)\.(\d+)/.exec(v || '');
if (!m) return false;
return Number(m[1]) > 3 || (Number(m[1]) === 3 && Number(m[2]) >= 0) || Number(m[1]) >= 3;
}
// Call before executePlan; bail with a clear message if false. Type guard
// runtime.ensure the split result carries a pane id before relying on it.
/**
* @param {{ stdout?: string, status?: number|null }} r
* @returns {string|null}
*/
function paneIdFromSplit(r) {
const id = (r && typeof r.stdout === 'string' ? r.stdout : '').trim();
return /^%\d+$/.test(id) ? id : null;
} Try / catch
try {
for (const workerPlan of plan.workerPlans) {
const splitResult = runCommandImpl('tmux', /* split-window args */, { cwd: plan.repoRoot });
const paneId = paneIdFromSplit(splitResult) ?? fallbackListPanes(plan.sessionName, workerPlan.worktreePath);
if (!paneId) throw new Error(`tmux split-window did not return a pane id for ${workerPlan.workerName}`);
// ... use paneId
}
} catch (err) {
if (/did not return a pane id/.test(err.message)) {
throw new Error(`${err.message} — check tmux -V (>=3.0) and ~/.tmux.conf hooks.`);
}
throw err;
} Prevention
- Require tmux >= 3.0 in your orchestrator wrapper and fail fast with a clear message.
- Run executePlan with a clean HOME or isolated config to rule out ~/.tmux.conf hooks.
- Add a fallback that lists panes by worktree path when -P/-F yields nothing.
- Log the raw splitResult.stdout/stderr on failure to detect config interference.
When it happens
Trigger: Old tmux (< 2.6, where split-window -P / -F #{pane_id} is unsupported or behaves differently). A tmux config (e.g. a hook like 'after-split-window' or set -g default-command / aggressive output) writing to stdout and corrupting the -F stream. A patched/distro tmux that ignores -P. The target session dying between new-session and the split so the split silently no-ops on some builds.
Common situations: Dev box on an old Ubuntu/Debian with tmux 2.x. Custom ~/.tmux.conf with pane-border-status or hooks that emit text. Containers where tmux was installed via a minimal package that strips format support. CI image pinned to an ancient tmux.
Related errors
- tmux session already exists: ${plan.sessionName}
- launcherCommand must be a non-empty string
- Unknown template variable: ${key}
- buildOrchestrationPlan requires at least one worker
- ${program} ${args.join(' ')} failed${stderr ? `: ${stderr}`
AI-assisted analysis of affaan-m/ECC@01e15490f0 (2026-08-13).
Data as JSON: /api/errors/aadf84ed2cd584cc.
Report an issue: GitHub.