Skip to content

Commit eab0d75

Browse files
NagyViktNagyVikt
andauthored
feat(fleet): [SI-9] CODEX_FLEET_WORKER_CWD override at worker boot (#174)
Resolve worker cwd by precedence in claude-worker.sh before spawning the Claude CLI: CODEX_FLEET_WORKER_CWD > active-plan-meta writable_roots[0] > $REPO. Forward the env var through claude-spawn.sh, document the precedence in worker-prompt.md, add a yaml example in accounts.example.yml, and ship test/run-worker-cwd.sh covering the four precedence scenarios. Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
1 parent d574721 commit eab0d75

5 files changed

Lines changed: 201 additions & 1 deletion

File tree

scripts/codex-fleet/accounts.example.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
# specialty [list of plan-slug prefixes] (default [] = generalist)
2626
# Pane prefers tasks whose `plan_slug` starts with one of
2727
# these prefixes. Empty list = accept all plans.
28+
# worker_cwd (optional, SI-9) absolute path to cd into before the
29+
# worker REPL starts. Exported as CODEX_FLEET_WORKER_CWD.
30+
# Use when this account is dedicated to a project tree
31+
# outside the codex-fleet repo. Precedence:
32+
# worker_cwd > active-plan-meta writable_roots[0] > $REPO
2833

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

4452
- id: review
4553
email: admin@mite.hu

scripts/codex-fleet/claude-spawn.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,11 @@ build_pane_cmd() {
263263
local env_str
264264
env_str="CLAUDE_FLEET_AGENT_NAME='$agent' CLAUDE_FLEET_ACCOUNT_LABEL='$label' CLAUDE_FLEET_TIER='$TIER' CLAUDE_FLEET_SPECIALTY='$SPECIALTY' CLAUDE_FLEET_MODEL='$MODEL'"
265265
env_str="$env_str CODEX_HOME='$CODEX_HOME'"
266+
# SI-9: forward per-pane worker cwd override into the wrapper env so
267+
# claude-worker.sh's resolve_worker_cwd picks it up before the main loop.
268+
if [ -n "${CODEX_FLEET_WORKER_CWD:-}" ]; then
269+
env_str="$env_str CODEX_FLEET_WORKER_CWD='$CODEX_FLEET_WORKER_CWD'"
270+
fi
266271
if [ -n "$ACCOUNT_EMAIL" ]; then
267272
env_str="$env_str ACCOUNT_EMAIL='$ACCOUNT_EMAIL'"
268273
fi

scripts/codex-fleet/claude-worker.sh

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,45 @@
3838

3939
set -u
4040

41+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
42+
REPO="${CODEX_FLEET_REPO_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
43+
44+
# Resolve the worker's initial cwd by precedence:
45+
# 1. CODEX_FLEET_WORKER_CWD from env (e.g. set in accounts.yml per-pane)
46+
# 2. task.metadata.worker_cwd (only after a task is claimed — the wake
47+
# prompt is responsible for cd'ing there; out of scope for this fn)
48+
# 3. priority plan's metadata.writable_roots[0] read from
49+
# $REPO/.codex-fleet/active-plan-meta.json if present
50+
# 4. fallback: $REPO (back-compat with pre-SI-9 behavior)
51+
#
52+
# TODO(SI-3/SI-5/SI-11): the bringup hardening PR is the right place to
53+
# write $REPO/.codex-fleet/active-plan-meta.json from the priority plan's
54+
# metadata. Until then case (3) is a no-op for live fleets and we fall
55+
# through to (4) — which keeps things working exactly as before.
56+
resolve_worker_cwd() {
57+
if [ -n "${CODEX_FLEET_WORKER_CWD:-}" ]; then
58+
if [ -d "$CODEX_FLEET_WORKER_CWD" ] && [ -w "$CODEX_FLEET_WORKER_CWD" ]; then
59+
echo "$CODEX_FLEET_WORKER_CWD"; return
60+
fi
61+
fi
62+
local meta_file="$REPO/.codex-fleet/active-plan-meta.json"
63+
if [ -f "$meta_file" ] && command -v jq >/dev/null 2>&1; then
64+
local wr
65+
wr=$(jq -r '.metadata.writable_roots[0] // empty' "$meta_file" 2>/dev/null)
66+
if [ -n "$wr" ] && [ -d "$wr" ] && [ -w "$wr" ]; then
67+
echo "$wr"; return
68+
fi
69+
fi
70+
echo "$REPO"
71+
}
72+
73+
# When sourced by test/run-worker-cwd.sh we want resolve_worker_cwd
74+
# defined but the rest of the script (env validation, banner, main loop)
75+
# skipped. The test sets CLAUDE_WORKER_SOURCE_ONLY=1 before sourcing.
76+
if [ "${CLAUDE_WORKER_SOURCE_ONLY:-0}" = "1" ]; then
77+
return 0 2>/dev/null || exit 0
78+
fi
79+
4180
AGENT="${CLAUDE_FLEET_AGENT_NAME:-}"
4281
if [ -z "$AGENT" ]; then
4382
echo "[claude-worker] fatal: CLAUDE_FLEET_AGENT_NAME unset" >&2
@@ -56,13 +95,18 @@ mkdir -p "$LOG_DIR"
5695
LOG_FILE="$LOG_DIR/claude-worker-$AGENT.log"
5796
STOP_FILE="${STOP_FILE:-$LOG_DIR/claude-worker-$AGENT.stop}"
5897

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

104+
worker_cwd="$(resolve_worker_cwd)"
105+
cd "$worker_cwd" || {
106+
echo "[claude-worker] fatal: cannot cd to resolved worker cwd: $worker_cwd" >&2
107+
exit 2
108+
}
109+
66110
ADD_DIR_FLAGS=(
67111
--add-dir "/home/deadpool/Documents/recodee"
68112
--add-dir "/home/deadpool/Documents/codex-fleet"
@@ -97,6 +141,8 @@ fi
97141
"$AGENT" "$LABEL" "$TIER" "$SPECIALTY" "$MODEL"
98142
printf 'wake=%s log=%s\n' "$WAKE" "$LOG_FILE"
99143
printf 'stop=%s (touch this to break the loop)\n' "$STOP_FILE"
144+
printf 'cwd=%s (resolved via CODEX_FLEET_WORKER_CWD precedence; fallback $REPO=%s)\n' \
145+
"$worker_cwd" "$REPO"
100146
printf 'add-dir: %s\n' "${ADD_DIR_FLAGS[*]}"
101147
printf '========================================\n\n'
102148
} | tee -a "$LOG_FILE"
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#!/usr/bin/env bash
2+
#
3+
# run-worker-cwd.sh — SI-9 unit tests for claude-worker.sh's
4+
# resolve_worker_cwd precedence helper.
5+
#
6+
# Sources claude-worker.sh with CLAUDE_WORKER_SOURCE_ONLY=1 in a subshell
7+
# so the function is defined without firing the main loop, then exercises
8+
# four scenarios:
9+
#
10+
# 1. CODEX_FLEET_WORKER_CWD set + writable → echoes that path.
11+
# 2. CODEX_FLEET_WORKER_CWD unset, active-plan-meta.json points at a
12+
# writable dir → echoes that dir.
13+
# 3. Both unset → echoes $REPO.
14+
# 4. CODEX_FLEET_WORKER_CWD points at non-existent path → falls
15+
# through to plan-meta (if usable) or $REPO.
16+
17+
set -euo pipefail
18+
19+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
20+
WORKER_SH="$ROOT/scripts/codex-fleet/claude-worker.sh"
21+
22+
[ -f "$WORKER_SH" ] || { echo "FAIL: missing $WORKER_SH" >&2; exit 1; }
23+
24+
TMP="$(mktemp -d -t si9-worker-cwd-XXXXXX)"
25+
trap 'rm -rf "$TMP"' EXIT
26+
27+
PASS=0
28+
FAIL=0
29+
30+
fail() {
31+
printf 'FAIL %s\n' "$1" >&2
32+
FAIL=$((FAIL + 1))
33+
}
34+
35+
pass() {
36+
printf 'PASS %s\n' "$1"
37+
PASS=$((PASS + 1))
38+
}
39+
40+
assert_eq() {
41+
local label="$1" expected="$2" actual="$3"
42+
if [ "$expected" = "$actual" ]; then
43+
pass "$label"
44+
else
45+
fail "$label (expected='$expected' actual='$actual')"
46+
fi
47+
}
48+
49+
# Helper: invoke resolve_worker_cwd in a clean subshell with a forced
50+
# REPO and a chosen $HOME-like sandbox so each test is isolated.
51+
#
52+
# Args:
53+
# $1 fake REPO root (must exist; we control whether
54+
# .codex-fleet/active-plan-meta.json sits inside it)
55+
# env: CODEX_FLEET_WORKER_CWD optional pass-through
56+
run_resolver() {
57+
local fake_repo="$1"
58+
CLAUDE_WORKER_SOURCE_ONLY=1 \
59+
CODEX_FLEET_REPO_ROOT="$fake_repo" \
60+
CODEX_FLEET_WORKER_CWD="${CODEX_FLEET_WORKER_CWD:-}" \
61+
bash -c "
62+
set -u
63+
# shellcheck disable=SC1090
64+
source '$WORKER_SH'
65+
resolve_worker_cwd
66+
"
67+
}
68+
69+
# Scenario 1: CODEX_FLEET_WORKER_CWD set + writable → echoes it.
70+
foo="$TMP/foo"
71+
mkdir -p "$foo"
72+
fake_repo_1="$TMP/repo1"
73+
mkdir -p "$fake_repo_1"
74+
got="$(CODEX_FLEET_WORKER_CWD="$foo" run_resolver "$fake_repo_1")"
75+
assert_eq "1 env override returns explicit path" "$foo" "$got"
76+
77+
# Scenario 2: env unset, active-plan-meta points at /tmp/bar.
78+
bar="$TMP/bar"
79+
mkdir -p "$bar"
80+
fake_repo_2="$TMP/repo2"
81+
mkdir -p "$fake_repo_2/.codex-fleet"
82+
cat >"$fake_repo_2/.codex-fleet/active-plan-meta.json" <<JSON
83+
{ "metadata": { "writable_roots": ["$bar", "/should/not/use"] } }
84+
JSON
85+
unset CODEX_FLEET_WORKER_CWD
86+
if command -v jq >/dev/null 2>&1; then
87+
got="$(run_resolver "$fake_repo_2")"
88+
assert_eq "2 plan-meta writable_roots[0] used" "$bar" "$got"
89+
else
90+
printf 'SKIP 2 plan-meta test (jq not on PATH)\n'
91+
fi
92+
93+
# Scenario 3: both unset → echoes $REPO.
94+
fake_repo_3="$TMP/repo3"
95+
mkdir -p "$fake_repo_3"
96+
unset CODEX_FLEET_WORKER_CWD
97+
got="$(run_resolver "$fake_repo_3")"
98+
assert_eq "3 fallback to \$REPO" "$fake_repo_3" "$got"
99+
100+
# Scenario 4: CODEX_FLEET_WORKER_CWD points at non-existent path.
101+
# With no plan-meta, must fall through to $REPO. Confirms the env path
102+
# does NOT clobber the resolution when the target is unusable.
103+
fake_repo_4="$TMP/repo4"
104+
mkdir -p "$fake_repo_4"
105+
got="$(CODEX_FLEET_WORKER_CWD="$TMP/does-not-exist" run_resolver "$fake_repo_4")"
106+
assert_eq "4 non-existent env path falls through to \$REPO" "$fake_repo_4" "$got"
107+
108+
# Scenario 4b: non-existent env path WITH valid plan-meta → plan-meta wins.
109+
baz="$TMP/baz"
110+
mkdir -p "$baz"
111+
fake_repo_4b="$TMP/repo4b"
112+
mkdir -p "$fake_repo_4b/.codex-fleet"
113+
cat >"$fake_repo_4b/.codex-fleet/active-plan-meta.json" <<JSON
114+
{ "metadata": { "writable_roots": ["$baz"] } }
115+
JSON
116+
if command -v jq >/dev/null 2>&1; then
117+
got="$(CODEX_FLEET_WORKER_CWD="$TMP/still-missing" run_resolver "$fake_repo_4b")"
118+
assert_eq "4b non-existent env path falls through to plan-meta" "$baz" "$got"
119+
fi
120+
121+
printf '\n%d passed, %d failed\n' "$PASS" "$FAIL"
122+
[ "$FAIL" -eq 0 ]

scripts/codex-fleet/worker-prompt.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,25 @@ You are pane `$CODEX_FLEET_AGENT_NAME` (Colony agent id) under account
55
plus the `force-claim` + `claim-release-supervisor` daemons. Your job:
66
pull → preflight → execute → report. Do not propose tasks. Do not chat.
77

8+
## Worker cwd precedence (SI-9)
9+
10+
`claude-worker.sh` cd's into the resolved working directory *before*
11+
spawning the Claude CLI, so your first turn already lands in the right
12+
repo and you do not waste an action on `cd`. Precedence (highest wins):
13+
14+
1. `CODEX_FLEET_WORKER_CWD` (env, e.g. set per-account in `accounts.yml`)
15+
— used when this account is dedicated to a single project tree.
16+
2. `task.metadata.worker_cwd` (per-task override; honoured inside the
17+
wake loop after claim, not at boot).
18+
3. Priority plan's `metadata.writable_roots[0]` read from
19+
`$REPO/.codex-fleet/active-plan-meta.json` (written by the bringup
20+
hardening PR; absent today → falls through).
21+
4. `$REPO` — the codex-fleet repo root, back-compat fallback.
22+
23+
If your task's `touches_files` live elsewhere, cd into the appropriate
24+
worktree explicitly. Do NOT shell out to `cd "$REPO"` reflexively at
25+
the start of every turn.
26+
827
## Token discipline
928

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

0 commit comments

Comments
 (0)