Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions scripts/codex-fleet/accounts.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
# specialty [list of plan-slug prefixes] (default [] = generalist)
# Pane prefers tasks whose `plan_slug` starts with one of
# these prefixes. Empty list = accept all plans.
# worker_cwd (optional, SI-9) absolute path to cd into before the
# worker REPL starts. Exported as CODEX_FLEET_WORKER_CWD.
# Use when this account is dedicated to a project tree
# outside the codex-fleet repo. Precedence:
# worker_cwd > active-plan-meta writable_roots[0] > $REPO

accounts:
- id: research
Expand All @@ -40,6 +45,9 @@ accounts:
rate_limit_tier: standard
tier: medium
specialty: ["codex-fleet", "recodee"]
# Pin this pane to the polymarket-cli tree so its first action is not
# `cd /home/deadpool/Documents/polymarket-cli` every wake. See SI-9.
worker_cwd: /home/deadpool/Documents/polymarket-cli

- id: review
email: admin@mite.hu
Expand Down
5 changes: 5 additions & 0 deletions scripts/codex-fleet/claude-spawn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,11 @@ build_pane_cmd() {
local env_str
env_str="CLAUDE_FLEET_AGENT_NAME='$agent' CLAUDE_FLEET_ACCOUNT_LABEL='$label' CLAUDE_FLEET_TIER='$TIER' CLAUDE_FLEET_SPECIALTY='$SPECIALTY' CLAUDE_FLEET_MODEL='$MODEL'"
env_str="$env_str CODEX_HOME='$CODEX_HOME'"
# SI-9: forward per-pane worker cwd override into the wrapper env so
# claude-worker.sh's resolve_worker_cwd picks it up before the main loop.
if [ -n "${CODEX_FLEET_WORKER_CWD:-}" ]; then
env_str="$env_str CODEX_FLEET_WORKER_CWD='$CODEX_FLEET_WORKER_CWD'"
fi
if [ -n "$ACCOUNT_EMAIL" ]; then
env_str="$env_str ACCOUNT_EMAIL='$ACCOUNT_EMAIL'"
fi
Expand Down
48 changes: 47 additions & 1 deletion scripts/codex-fleet/claude-worker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,45 @@

set -u

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO="${CODEX_FLEET_REPO_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"

# Resolve the worker's initial cwd by precedence:
# 1. CODEX_FLEET_WORKER_CWD from env (e.g. set in accounts.yml per-pane)
# 2. task.metadata.worker_cwd (only after a task is claimed — the wake
# prompt is responsible for cd'ing there; out of scope for this fn)
# 3. priority plan's metadata.writable_roots[0] read from
# $REPO/.codex-fleet/active-plan-meta.json if present
# 4. fallback: $REPO (back-compat with pre-SI-9 behavior)
#
# TODO(SI-3/SI-5/SI-11): the bringup hardening PR is the right place to
# write $REPO/.codex-fleet/active-plan-meta.json from the priority plan's
# metadata. Until then case (3) is a no-op for live fleets and we fall
# through to (4) — which keeps things working exactly as before.
resolve_worker_cwd() {
if [ -n "${CODEX_FLEET_WORKER_CWD:-}" ]; then
if [ -d "$CODEX_FLEET_WORKER_CWD" ] && [ -w "$CODEX_FLEET_WORKER_CWD" ]; then
echo "$CODEX_FLEET_WORKER_CWD"; return
fi
fi
local meta_file="$REPO/.codex-fleet/active-plan-meta.json"
if [ -f "$meta_file" ] && command -v jq >/dev/null 2>&1; then
local wr
wr=$(jq -r '.metadata.writable_roots[0] // empty' "$meta_file" 2>/dev/null)
if [ -n "$wr" ] && [ -d "$wr" ] && [ -w "$wr" ]; then
echo "$wr"; return
fi
fi
echo "$REPO"
}

# When sourced by test/run-worker-cwd.sh we want resolve_worker_cwd
# defined but the rest of the script (env validation, banner, main loop)
# skipped. The test sets CLAUDE_WORKER_SOURCE_ONLY=1 before sourcing.
if [ "${CLAUDE_WORKER_SOURCE_ONLY:-0}" = "1" ]; then
return 0 2>/dev/null || exit 0
fi

AGENT="${CLAUDE_FLEET_AGENT_NAME:-}"
if [ -z "$AGENT" ]; then
echo "[claude-worker] fatal: CLAUDE_FLEET_AGENT_NAME unset" >&2
Expand All @@ -56,13 +95,18 @@ mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/claude-worker-$AGENT.log"
STOP_FILE="${STOP_FILE:-$LOG_DIR/claude-worker-$AGENT.stop}"

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WAKE="$SCRIPT_DIR/claude-wake-prompt.md"
if [ ! -f "$WAKE" ]; then
echo "[claude-worker] fatal: wake-prompt missing at $WAKE" >&2
exit 2
fi

worker_cwd="$(resolve_worker_cwd)"
cd "$worker_cwd" || {
echo "[claude-worker] fatal: cannot cd to resolved worker cwd: $worker_cwd" >&2
exit 2
}

ADD_DIR_FLAGS=(
--add-dir "/home/deadpool/Documents/recodee"
--add-dir "/home/deadpool/Documents/codex-fleet"
Expand Down Expand Up @@ -97,6 +141,8 @@ fi
"$AGENT" "$LABEL" "$TIER" "$SPECIALTY" "$MODEL"
printf 'wake=%s log=%s\n' "$WAKE" "$LOG_FILE"
printf 'stop=%s (touch this to break the loop)\n' "$STOP_FILE"
printf 'cwd=%s (resolved via CODEX_FLEET_WORKER_CWD precedence; fallback $REPO=%s)\n' \
"$worker_cwd" "$REPO"
printf 'add-dir: %s\n' "${ADD_DIR_FLAGS[*]}"
printf '========================================\n\n'
} | tee -a "$LOG_FILE"
Expand Down
122 changes: 122 additions & 0 deletions scripts/codex-fleet/test/run-worker-cwd.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#!/usr/bin/env bash
#
# run-worker-cwd.sh — SI-9 unit tests for claude-worker.sh's
# resolve_worker_cwd precedence helper.
#
# Sources claude-worker.sh with CLAUDE_WORKER_SOURCE_ONLY=1 in a subshell
# so the function is defined without firing the main loop, then exercises
# four scenarios:
#
# 1. CODEX_FLEET_WORKER_CWD set + writable → echoes that path.
# 2. CODEX_FLEET_WORKER_CWD unset, active-plan-meta.json points at a
# writable dir → echoes that dir.
# 3. Both unset → echoes $REPO.
# 4. CODEX_FLEET_WORKER_CWD points at non-existent path → falls
# through to plan-meta (if usable) or $REPO.

set -euo pipefail

ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
WORKER_SH="$ROOT/scripts/codex-fleet/claude-worker.sh"

[ -f "$WORKER_SH" ] || { echo "FAIL: missing $WORKER_SH" >&2; exit 1; }

TMP="$(mktemp -d -t si9-worker-cwd-XXXXXX)"
trap 'rm -rf "$TMP"' EXIT

PASS=0
FAIL=0

fail() {
printf 'FAIL %s\n' "$1" >&2
FAIL=$((FAIL + 1))
}

pass() {
printf 'PASS %s\n' "$1"
PASS=$((PASS + 1))
}

assert_eq() {
local label="$1" expected="$2" actual="$3"
if [ "$expected" = "$actual" ]; then
pass "$label"
else
fail "$label (expected='$expected' actual='$actual')"
fi
}

# Helper: invoke resolve_worker_cwd in a clean subshell with a forced
# REPO and a chosen $HOME-like sandbox so each test is isolated.
#
# Args:
# $1 fake REPO root (must exist; we control whether
# .codex-fleet/active-plan-meta.json sits inside it)
# env: CODEX_FLEET_WORKER_CWD optional pass-through
run_resolver() {
local fake_repo="$1"
CLAUDE_WORKER_SOURCE_ONLY=1 \
CODEX_FLEET_REPO_ROOT="$fake_repo" \
CODEX_FLEET_WORKER_CWD="${CODEX_FLEET_WORKER_CWD:-}" \
bash -c "
set -u
# shellcheck disable=SC1090
source '$WORKER_SH'
resolve_worker_cwd
"
}

# Scenario 1: CODEX_FLEET_WORKER_CWD set + writable → echoes it.
foo="$TMP/foo"
mkdir -p "$foo"
fake_repo_1="$TMP/repo1"
mkdir -p "$fake_repo_1"
got="$(CODEX_FLEET_WORKER_CWD="$foo" run_resolver "$fake_repo_1")"
assert_eq "1 env override returns explicit path" "$foo" "$got"

# Scenario 2: env unset, active-plan-meta points at /tmp/bar.
bar="$TMP/bar"
mkdir -p "$bar"
fake_repo_2="$TMP/repo2"
mkdir -p "$fake_repo_2/.codex-fleet"
cat >"$fake_repo_2/.codex-fleet/active-plan-meta.json" <<JSON
{ "metadata": { "writable_roots": ["$bar", "/should/not/use"] } }
JSON
unset CODEX_FLEET_WORKER_CWD
if command -v jq >/dev/null 2>&1; then
got="$(run_resolver "$fake_repo_2")"
assert_eq "2 plan-meta writable_roots[0] used" "$bar" "$got"
else
printf 'SKIP 2 plan-meta test (jq not on PATH)\n'
fi

# Scenario 3: both unset → echoes $REPO.
fake_repo_3="$TMP/repo3"
mkdir -p "$fake_repo_3"
unset CODEX_FLEET_WORKER_CWD
got="$(run_resolver "$fake_repo_3")"
assert_eq "3 fallback to \$REPO" "$fake_repo_3" "$got"

# Scenario 4: CODEX_FLEET_WORKER_CWD points at non-existent path.
# With no plan-meta, must fall through to $REPO. Confirms the env path
# does NOT clobber the resolution when the target is unusable.
fake_repo_4="$TMP/repo4"
mkdir -p "$fake_repo_4"
got="$(CODEX_FLEET_WORKER_CWD="$TMP/does-not-exist" run_resolver "$fake_repo_4")"
assert_eq "4 non-existent env path falls through to \$REPO" "$fake_repo_4" "$got"

# Scenario 4b: non-existent env path WITH valid plan-meta → plan-meta wins.
baz="$TMP/baz"
mkdir -p "$baz"
fake_repo_4b="$TMP/repo4b"
mkdir -p "$fake_repo_4b/.codex-fleet"
cat >"$fake_repo_4b/.codex-fleet/active-plan-meta.json" <<JSON
{ "metadata": { "writable_roots": ["$baz"] } }
JSON
if command -v jq >/dev/null 2>&1; then
got="$(CODEX_FLEET_WORKER_CWD="$TMP/still-missing" run_resolver "$fake_repo_4b")"
assert_eq "4b non-existent env path falls through to plan-meta" "$baz" "$got"
fi

printf '\n%d passed, %d failed\n' "$PASS" "$FAIL"
[ "$FAIL" -eq 0 ]
19 changes: 19 additions & 0 deletions scripts/codex-fleet/worker-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ You are pane `$CODEX_FLEET_AGENT_NAME` (Colony agent id) under account
plus the `force-claim` + `claim-release-supervisor` daemons. Your job:
pull → preflight → execute → report. Do not propose tasks. Do not chat.

## Worker cwd precedence (SI-9)

`claude-worker.sh` cd's into the resolved working directory *before*
spawning the Claude CLI, so your first turn already lands in the right
repo and you do not waste an action on `cd`. Precedence (highest wins):

1. `CODEX_FLEET_WORKER_CWD` (env, e.g. set per-account in `accounts.yml`)
— used when this account is dedicated to a single project tree.
2. `task.metadata.worker_cwd` (per-task override; honoured inside the
wake loop after claim, not at boot).
3. Priority plan's `metadata.writable_roots[0]` read from
`$REPO/.codex-fleet/active-plan-meta.json` (written by the bringup
hardening PR; absent today → falls through).
4. `$REPO` — the codex-fleet repo root, back-compat fallback.

If your task's `touches_files` live elsewhere, cd into the appropriate
worktree explicitly. Do NOT shell out to `cd "$REPO"` reflexively at
the start of every turn.

## Token discipline

- Less word, same proof. No commentary, no narration of your reasoning.
Expand Down