Git worktrees reference the parent repo's .git directory via absolute
path. Without mounting it, all git commands fail inside the container.
Now reads the .git file, resolves the parent .git path, and mounts it
read-only at the same host path so references resolve naturally.
CLAUDE_CONFIG_DIR included .claude suffix, but the container mounts
the parent directory at /home/node/.claude. This caused double nesting:
/home/node/.claude/.claude/CLAUDE.md — SDK never found the prompts.
All reviewers (claude, codex-review, codex-review-failover) now use
claude-paired-room.md. All owners use owner-common-paired-room.md.
Removed: codex-paired-room.md, codex-review-paired-room.md,
codex-review-failover-paired-room.md, codex-review-platform.md
10 prompt files → 6.
The anti-pattern guardrails ("It should work now" means run it, etc.)
and stagnation pattern names (Spinning/Oscillation/Diminishing/No progress)
are too useful to compress out — they prevent common LLM failure modes.
Container mounted data/sessions/{folder}/ but CLAUDE.md was written
to data/sessions/{folder}-reviewer/.claude/ by paired-execution-context.
The reviewer never saw its prompts (verdict protocol, paired room rules).
Now mounts {folder}-reviewer/ to match, so the CLAUDE.md with platform
and paired room prompts is visible inside the container.
The alreadyVisible check compared progress text with final text and
skipped deliverFinalText when they matched. This caused legitimate
final outputs to vanish when the progress message happened to
accumulate the same text through edits.
When the reviewer approves (merge_ready) and the owner finalizes,
the system previously only checked for code changes. If the owner
raised concerns (DONE_WITH_CONCERNS, BLOCKED, NEEDS_CONTEXT) during
finalize — e.g. push failed due to detached HEAD — the system
ignored it and auto-completed the task.
Now the owner's verdict is checked first. If the owner raises
concerns, the task goes back to active and re-triggers the reviewer
so the issue can be addressed collaboratively.
In paired rooms, cron output was posted by the owner bot. Since the
owner can't respond to its own messages, the cron report just sat
there with no follow-up action.
Now cron output in paired rooms is posted via the reviewer bot. The
owner picks it up as a peer request, analyzes the report, and acts
on it (e.g., fixing Sentry errors). The normal paired review loop
then kicks in to verify the fix.
Container reviewer was running without prompts because the container
path in agent-runner.ts skipped prepareGroupEnvironment() entirely.
This meant no CLAUDE.md (platform + paired room prompts + memory
briefing) and no roomRoleContext were passed to the container.
- Add prepareContainerSessionEnvironment() to write CLAUDE.md, sync
skills, and ensure settings.json for the reviewer session directory
- Pass roomRoleContext through ReviewerContainerInput so the runner
can prepend the [ROOM_ROLE] header
- Add roomRoleContext field to ReviewerContainerInput interface
When reviewer was triggered via the normal message dispatch path (not
the review_ready check path), output was sent through the owner channel.
Now checks paired task status to determine the correct output channel.
This was the second code path that still called
refreshReviewerSnapshotForPairedTask (file copy). Now returns owner
workspace directly like markPairedTaskReviewReady does.
Instead of docker run --rm per turn (~300ms overhead), containers are
now created once per channel with docker run -d and reused across turns.
Each turn runs via docker exec inside the warm container.
- ensurePersistentContainer: creates or reuses container per channel
- stopReviewerContainer: explicit stop for /clear or shutdown
- Container stays alive with sleep infinity, turns exec node runner
- cleanupOrphans handles stale containers on restart
Reviewer always runs in Docker container. No more snapshot file copy —
reviewer mounts the owner workspace directly as read-only. Removes
REVIEWER_CONTAINER_ENABLED toggle and host-mode fallback code.
When git ls-files returns directory entries (nested git repos),
cpSync fails trying to overwrite a file placeholder with a directory.
Now removes the conflicting target file before copying.
git ls-files can return directory entries (e.g. submodules). readFileSync
on these causes EISDIR error, blocking reviewer trigger for all non-brain
paired channels.
Codex CLI reads skills from ~/.agents/skills/ (user-level) and
{workDir}/.agents/skills/ (project-level), not from .codex/skills/.
Skills were being copied to the wrong path and never discovered.
When intermediate text was displayed as a progress message and the final
result had the same content, both the progress message and a separate
final message were sent to Discord. Now detects when the progress message
already shows the final text and finalizes in-place without sending a
duplicate.
Container runner was calling onOutput with fire-and-forget (void),
so work items were still in 'produced' status when the drain loop
checked after container exit. This caused the same message to be
delivered through both reviewer and owner channels.
Now chains onOutput calls and waits for completion before resolving,
matching the host-process runner's outputChain pattern.
Proxy was binding to 127.0.0.1 which is unreachable from Docker
containers. Now uses PROXY_BIND_HOST (detects docker0 IP on Linux)
so containers can reach the proxy via host.docker.internal.
Host paths passed via envOverrides don't exist inside the container,
causing ENOENT when the SDK tries to spawn with a non-existent CWD.
Now remaps EJCLAW_WORK_DIR to /workspace/project and CLAUDE_CONFIG_DIR
to /home/node/.claude (matching the container mount points).
Owner worktree path was per-task (data/workspaces/{folder}/tasks/{taskId}/owner/),
causing Claude SDK to treat each new task as a different project and lose session
context. Now uses a stable per-channel path (data/workspaces/{folder}/owner/).
- Channel isolation preserved (different folders get different worktrees)
- Session persists across tasks (same path = same Claude SDK project)
- Reviewer snapshot still reads from owner worktree correctly
- Fix stale approvedHead reference from cherry-pick conflict
- Replace commit hash comparison with git tree hash (HEAD^{tree}) to
avoid false re-review triggers on commit-only operations
- Use git diff --quiet for actual file change detection
- Update source_ref on failed reviewer execution with done verdict
- Add tests for empty-commit finalize and code-change re-review
- Document Codex reviewer bash mutation gap in reviewer-runtime
Reviewer was picking up owner's cached session from the shared
.claude directory, causing "No conversation found" errors. Now uses
data/sessions/{folder}-reviewer/.claude/ as a separate config dir.
- Compare git HEAD at reviewer approval vs owner finalize to detect
new code changes after DONE verdict. Re-trigger reviewer if changed,
complete if unchanged (commit-only is fine).
- Record approved HEAD in source_ref when reviewer says DONE
- Add pnpm store mount, pre-flight checks, idle timeout for containers
- Clean up orphaned containers on shutdown
- Detect pnpm projects and mount global store read-only so hardlinks resolve
- npm/yarn/bun need no extra mounts (node_modules is self-contained)
- Pre-flight check: verify Docker running + image exists (cached after first call)
- Add activity-based idle timeout (reset on stdout/stderr like host runner)
- Clean up orphaned containers on shutdown (not just startup)
Reviewer was starting with undefined sessionId every turn, losing all
conversation context. Now uses `group.folder:reviewer` as the session key
so owner and reviewer maintain independent session histories.
Also updates README to reflect unified service architecture and paired
review workflow.