fix(lanes): keep stage detection honest across rollbacks and feature switches
Three defects let a lane's pipeline map claim progress that was not this
pass's work, all of them on the "going back" path a new request takes:
- `setStage` cleared the detection columns on a real transition but never
pruned the `stages` records AHEAD of the new stage. A lane that ran through
`done` and was then declared back at `plan` rendered `plan` as `current`
while `tests`/`review`/`done` still read `done`, carrying the previous
pass's evidence, and contradicting `progress` (computed from the declared
index alone). A backwards declaration now drops every record it can prove
is ahead; a key matching no node is unplaceable and left alone.
- `activateFeature` rewrote the live lane row for the incoming feature but
carried the outgoing one's `detected_*` across. A never-seen slug starts at
`stage='idle'`/`stages={}`, exactly the shape `inferredDisplayStage` renders
from detection, so the new feature opened painted with the old one's
progress — and forward-only rejected every real detection for it until the
carried-over one aged past DETECTION_TTL_MS. Now cleared, like every other
real transition; deliberately not archived, since detection is live-only.
- `codex-ingest` classified a rollout journal by raw `now - mtime`, which goes
negative for a future mtime (clock stepped back, NTP correction, a restore
that preserved timestamps) and read as "written milliseconds ago". A
finished thread's journal could replay as live work; a file claiming to be
from the future is now idle.
Also fixes `check-headers.sh`, which audited via `find` and so reported local
gitignored runtime state (a lane's own `.ccam/profile/hooks/*.sh`) that is not
this repo's source and can never be made compliant here. It now lists files
through git (tracked + untracked-but-not-ignored), so a file an agent just
wrote is still caught before it is staged while ignored state is not.
Tests: 1359 pass (4 new). Header audit exits 0.
This commit is contained in:
@@ -286,6 +286,17 @@ function reservedPorts(excludeLaneId = null) {
|
||||
* paint stale progress in the UI AND — because recordDetection is forward-only
|
||||
* — silently reject every real detection for the new stage until the old one
|
||||
* ages past DETECTION_TTL_MS.
|
||||
*
|
||||
* A transition BACKWARDS additionally drops the `stages` records of every node
|
||||
* ahead of the new one, for the same reason and with the same consequence in
|
||||
* the UI. Those records are the previous pass's — a lane that ran through
|
||||
* `done` and is then declared back at `plan` for a new request would otherwise
|
||||
* render `plan` as `current` while `tests`/`review`/`done` still read `done`,
|
||||
* carrying the *old* work's evidence, and `progress` (computed from the
|
||||
* declared index alone) would contradict the map it sits next to. A gate ahead
|
||||
* of where the agent now says it is has not been passed for this pass; it has
|
||||
* to be re-earned. Records under a key matching no node in the pipeline are
|
||||
* left untouched — their position is unknown, so they cannot be proven ahead.
|
||||
*/
|
||||
function setStage(id, { stage, status, evidence, note, result } = {}) {
|
||||
const lane = getLane(id);
|
||||
@@ -293,6 +304,15 @@ function setStage(id, { stage, status, evidence, note, result } = {}) {
|
||||
const next = stage || lane.stage;
|
||||
const changed = next !== lane.stage;
|
||||
const stages = { ...lane.stages };
|
||||
const pipeline = getPipeline(lane.pipeline);
|
||||
const nextIdx = phaseIdx(pipeline, next);
|
||||
const wentBack = changed && nextIdx > -1 && nextIdx < phaseIdx(pipeline, lane.stage);
|
||||
if (wentBack) {
|
||||
for (const key of Object.keys(stages)) {
|
||||
const idx = phaseIdx(pipeline, key);
|
||||
if (idx > nextIdx) delete stages[key];
|
||||
}
|
||||
}
|
||||
const prev = stages[next] || {};
|
||||
stages[next] = {
|
||||
enteredAt: !changed && prev.enteredAt ? prev.enteredAt : nowIso(),
|
||||
|
||||
Reference in New Issue
Block a user