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:
@@ -133,6 +133,15 @@ function archiveActiveFeature(laneId) {
|
||||
* the live row already IS that feature's state, so there's nothing to
|
||||
* restore and nothing to archive.
|
||||
*
|
||||
* A real switch also clears `detected_stage`/`detected_signal`/`detected_at`,
|
||||
* for the same reason `setStage` and `clearLane` do: a detection belongs to the
|
||||
* feature that produced it. Carried across a switch it would both paint the new
|
||||
* feature's map from the old one's activity (a fresh slug has `stage='idle'` and
|
||||
* `stages={}`, so `inferredDisplayStage` renders that detection verbatim) AND —
|
||||
* because `recordDetection` is forward-only — reject every real detection for
|
||||
* the new feature until the old one ages past DETECTION_TTL_MS. Detection is
|
||||
* live-only state, so it is deliberately NOT archived onto the outgoing row.
|
||||
*
|
||||
* @param {number} laneId
|
||||
* @param {string} rawSlug - Canonicalized internally; the caller's raw input is never stored.
|
||||
* @param {{title?: string}} [options]
|
||||
@@ -171,7 +180,8 @@ function activateFeature(laneId, rawSlug, options = {}) {
|
||||
db.prepare(
|
||||
`UPDATE lanes SET
|
||||
stage = ?, stage_since = ?, status = ?, gate_decision = ?, ci_status = ?,
|
||||
stages = ?, links = ?, notes = ?, active_feature_id = ?, updated_at = ?
|
||||
stages = ?, links = ?, notes = ?, active_feature_id = ?, updated_at = ?,
|
||||
detected_stage = NULL, detected_signal = NULL, detected_at = NULL
|
||||
WHERE id = ?`
|
||||
).run(
|
||||
target.stage,
|
||||
|
||||
Reference in New Issue
Block a user