feat(lanes): auto-advance a codex lane's stage from its rollout detection
A codex session emits no hooks and rarely calls `ccam stage` on its own, so its card sat at `idle` forever while the amber "detected" badge was the only thing that ever moved. `codex-ingest.js` now advances the declared `stage` from its own written detection — the one place in CCAM where an inference moves the blue `current` ring, and only for `codex` lanes. It stays honest: gated on `recordDetection` having written (so forward-only, declared-wins and the detection TTL are inherited unchanged), passes NO evidence (the node renders `passed-no-evidence` amber, never `done`), and no template's last node carries detect rules, so a lane can never be auto-declared `done`. `setStage` grows a `keepDetection` option — its sole caller — so the detection columns survive the advance and the card keeps showing which command moved it instead of looking like an agent's own claim. Two levers then get the evidence auto-advance cannot supply: - `codex-nudge.js` now nudges on a stage held 2+ min with no evidence, instead of on a detection sitting ahead of the declared stage — after auto-advance the old condition can never fire, since the two now match. - `ccam lanes agents install` on a codex lane writes the marker-delimited `<!-- ccam:stage:start -->` block into the lane's own AGENTS.md — the one instruction channel Codex reads — instead of copying agent templates into `.claude/agents/`, which it never loads. Appended, user prose preserved, reinstall replaces exactly that block. Verified with a real `codex exec` run: stage moved to `watching-pr` with `evidence: null` and the detection signal intact. 1364 server tests pass.
This commit is contained in:
@@ -158,6 +158,39 @@ describe("codex ingest pass", () => {
|
||||
lanes.deleteLane(lane.id);
|
||||
});
|
||||
|
||||
it("advances the declared stage from the detection, without evidence", () => {
|
||||
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-lane-"));
|
||||
const lane = lanes.createLane({ title: "cx", cwd, tool: "codex" });
|
||||
writeRollout("rollout-2026-08-27T10-00-10-adv.jsonl", cwd);
|
||||
codex.__resetOffsets();
|
||||
|
||||
codex.ingestCodexOnce({});
|
||||
const after = lanes.getLane(lane.id);
|
||||
// The ring moves — a codex session has nothing else that would move it…
|
||||
assert.equal(after.stage, "tests");
|
||||
// …but the node stays amber: an inference sees the tool call, not its result.
|
||||
assert.equal(after.stages.tests.evidence, null);
|
||||
// …and the badge survives, so the card still says WHY it moved.
|
||||
assert.equal(after.detected_stage, "tests");
|
||||
assert.match(after.detected_signal, /test:server/);
|
||||
lanes.deleteLane(lane.id);
|
||||
});
|
||||
|
||||
it("never drags an auto-advanced stage backwards", () => {
|
||||
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-lane-"));
|
||||
const lane = lanes.createLane({ title: "cx", cwd, tool: "codex" });
|
||||
lanes.setStage(lane.id, { stage: "review", evidence: "declared by the session" });
|
||||
writeRollout("rollout-2026-08-27T10-00-11-back.jsonl", cwd); // detects `tests`, behind `review`
|
||||
codex.__resetOffsets();
|
||||
|
||||
codex.ingestCodexOnce({});
|
||||
const after = lanes.getLane(lane.id);
|
||||
assert.equal(after.stage, "review"); // declared-wins: recordDetection refused, so no advance
|
||||
assert.equal(after.detected_stage, null);
|
||||
assert.equal(after.stages.review.evidence, "declared by the session");
|
||||
lanes.deleteLane(lane.id);
|
||||
});
|
||||
|
||||
it("a second pass with no appended bytes changes nothing", () => {
|
||||
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), "codex-lane-"));
|
||||
const lane = lanes.createLane({ title: "cx", cwd, tool: "codex" });
|
||||
|
||||
Reference in New Issue
Block a user