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.
prependRoomRoleHeader was called BEFORE /compact detection, turning
"/compact" into "[PAIRED ROOM: owner]\n/compact" which failed the
exact match check. Now session commands are detected first.
- Remove UNIFIED_MODE legacy: single service manages all 3 Discord bots
- Add OWNER_AGENT_TYPE / REVIEWER_AGENT_TYPE env vars for configurable agent selection
- Fix Discord channel routing: reviewer output goes through correct bot (discord/discord-review)
- Fix channel name assignment: explicit names prevent agentTypeFilter collision
- Remove silent output suppression system: harness-level protections replace prompt workarounds
- Add reviewer approval detection: DONE marker on first line stops ping-pong
- Include user's original message in reviewer prompt for context
- Fix round_trip_count auto-reset on new user message
- Fix session ID conflict: reviewer doesn't reuse owner's session
- Fix pending progress text flush in runner on close sentinel
- Promote buffered intermediate text to final result when result event has no text
- Remove legacy files: .env.codex.example, .env.codex-review.example, migrate-unify.cjs
- Update CLAUDE.md: server-side build deployment, unified architecture docs
In unified mode, SERVICE_SESSION_SCOPE doesn't match the lease's
owner/reviewer service IDs. Determine the effective role from the
paired task status: review_ready/in_review → reviewer, otherwise → owner.
This restores the owner↔reviewer ping-pong in unified single-process mode.
- Owner finishes → auto mark review_ready → reviewer executes
- Reviewer finishes → task back to active → owner can respond
- Automatic ping-pong until consensus or round trip limit
- PAIRED_MAX_ROUND_TRIPS env var (default 10, 0 = unlimited)
- round_trip_count tracked per task in DB
When /stop sends SIGTERM, the runner now calls AbortController.abort()
on the Claude SDK query, allowing graceful cleanup of in-flight API
requests before the process exits. Falls back to SIGKILL after 5s.
- Add UNIFIED_MODE flag (default on, disable with UNIFIED_MODE=0)
- Register all 3 Discord bots in one process (claude, codex, review)
- Load all registered groups regardless of agent_type (codex/owner priority)
- Start credential proxy and container cleanup at unified startup
- shouldServiceProcessChat returns true in unified mode
- Add findChannelByName for role-based response routing
- Backward compatible: UNIFIED_MODE=0 restores per-service behavior