fix(lanes): auto-detect and report stage for codex lanes (issue 11)
A lane on the 16-node `ship-feature` template showed an untouched pipeline
map and never declared a stage. Four independent causes, fixed at their
shared choke points:
1. Codex emits no hooks, so `touchLaneFromHook` — the only caller of
`stage-detect.detect()`, and correctly pinned to `claude` — could never
see a `codex` lane. New `server/lib/codex-ingest.js` tails Codex's own
rollout journals (`$CODEX_HOME/sessions/**/rollout-*.jsonl`), maps its
tool calls onto the hook-shaped `{tool_name, tool_input}` the matcher
already understands, and feeds the existing detect path plus an
idle/running status mirror under the same guard hooks use. Polled every
`DASHBOARD_CODEX_POLL_MS` ms (default 15000, `0` disables).
2. `resolveLaneArg` matched on cwd alone, so with a claude and a codex lane
at one directory (the composite `idx_lanes_cwd_tool`) a `ccam stage`
call could drive the sibling lane's card. It now resolves by
`--lane` → `CCAM_LANE_ID` → `--tool`/harness marker → owning lane, and
errors naming both candidates rather than picking one. Every pane the
dashboard starts carries `CCAM_LANE_ID` (`tmux new-session -e`, or an
assignment prefix when typing into a live shell pane).
3. `recordDetection`'s declared-wins veto had no exit: a lane left at the
pipeline's last node rejected every detection for the rest of its life.
A declaration on the LAST node no longer vetoes — the pipeline is over
there, so a later detection is new work. Nothing else is relaxed, and
detection still never writes `stage` nor renders `done`.
4. The run console pre-filled the pipeline's Claude slash command into
codex panes, which Codex reads as prose — so the skill that would have
called `ccam stage` never ran. Claude lanes only now.
A codex lane's *declared* stage still depends on the session calling
`ccam stage`; `AGENTS.md` is the only lever CCAM has, so it now says so.
Verified: server 1345/1345, client 441/441, and end-to-end against a real
codex lane row (detected_stage/detected_signal set, declared stage
untouched, no node rendered done).
This commit is contained in:
+24
-4
@@ -34,12 +34,24 @@ function hasSession(name) {
|
||||
}
|
||||
}
|
||||
|
||||
/** `-e NAME=value` pairs for tmux, in a stable order. */
|
||||
function envFlags(env) {
|
||||
const flags = [];
|
||||
for (const [key, value] of Object.entries(env || {})) flags.push("-e", `${key}=${value}`);
|
||||
return flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a detached tmux session running `argv` as the pane's command. Throws
|
||||
* if tmux itself fails to start (caller decides how to surface that).
|
||||
*
|
||||
* `env` becomes tmux `-e NAME=value` pairs, which tmux puts in the pane's own
|
||||
* environment (tmux >= 3.2). The dashboard uses it for CCAM_LANE_ID so a `ccam`
|
||||
* call from inside the pane knows exactly which lane it belongs to instead of
|
||||
* guessing from cwd — see resolveLaneArg in bin/ccam.js.
|
||||
*/
|
||||
function newSession({ name, cwd, argv }) {
|
||||
execImpl(["new-session", "-d", "-s", name, "-c", cwd, "--", ...argv]);
|
||||
function newSession({ name, cwd, argv, env }) {
|
||||
execImpl(["new-session", "-d", "-s", name, "-c", cwd, ...envFlags(env), "--", ...argv]);
|
||||
enableMouse(name);
|
||||
}
|
||||
|
||||
@@ -78,9 +90,17 @@ function shellQuote(arg) {
|
||||
* Type `argv` into an existing session's pane and press Enter. Only ever
|
||||
* called when the pane sits at a shell prompt (see `paneCommand`); `-l` sends
|
||||
* the string literally so no character is read as a tmux key name.
|
||||
*
|
||||
* `env` rides along as a shell assignment prefix on the SAME line — tmux's own
|
||||
* `-e` applies only at session creation, and a shell that is already running
|
||||
* takes its environment from the command line. The name is never quoted (bash
|
||||
* only recognizes an assignment when the `=` is unquoted); the value is.
|
||||
*/
|
||||
function sendCommand(name, argv) {
|
||||
execImpl(["send-keys", "-t", name, "-l", argv.map(shellQuote).join(" ")]);
|
||||
function sendCommand(name, argv, env) {
|
||||
const prefix = Object.entries(env || {})
|
||||
.map(([key, value]) => `${key}=${shellQuote(value)} `)
|
||||
.join("");
|
||||
execImpl(["send-keys", "-t", name, "-l", prefix + argv.map(shellQuote).join(" ")]);
|
||||
execImpl(["send-keys", "-t", name, "Enter"]);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user