fix(lanes): stop the stuck "waiting for your input" banner and idle status #1
+2
-2
@@ -7,8 +7,8 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "vitest run",
|
"test": "NODE_OPTIONS=--no-experimental-webstorage vitest run",
|
||||||
"test:watch": "vitest"
|
"test:watch": "NODE_OPTIONS=--no-experimental-webstorage vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "^5.2.8",
|
"@fontsource/inter": "^5.2.8",
|
||||||
|
|||||||
@@ -587,6 +587,39 @@ describe("stage detection", () => {
|
|||||||
lanes.deleteLane(l.id);
|
lanes.deleteLane(l.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("a real stage transition clears a stale detection and unblocks a fresh one behind it", () => {
|
||||||
|
const l = lanes.createLane({ cwd: "/tmp/wt-detect-stale-cross-task" });
|
||||||
|
// A prior, unrelated task's tool calls left `tests` as the detected stage.
|
||||||
|
lanes.recordDetection(l.id, { nodeId: "tests", signal: "`npm test`" });
|
||||||
|
assert.equal(lanes.getLane(l.id).detected_stage, "tests");
|
||||||
|
|
||||||
|
// A new task starts and the agent declares an EARLIER stage. Without the
|
||||||
|
// fix, the leftover `tests` detection both misrepresents the new task's
|
||||||
|
// progress and (via forward-only) rejects every real detection for it
|
||||||
|
// until `tests` ages past DETECTION_TTL_MS.
|
||||||
|
const after = lanes.setStage(l.id, { stage: "plan" });
|
||||||
|
assert.equal(after.detected_stage, null);
|
||||||
|
assert.equal(after.detected_signal, null);
|
||||||
|
assert.equal(after.detected_at, null);
|
||||||
|
|
||||||
|
assert.deepEqual(lanes.recordDetection(l.id, { nodeId: "implement", signal: "`Edit`" }), {
|
||||||
|
written: true,
|
||||||
|
});
|
||||||
|
assert.equal(lanes.getLane(l.id).detected_stage, "implement");
|
||||||
|
lanes.deleteLane(l.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("re-declaring the SAME stage (a heartbeat) leaves a live detection alone", () => {
|
||||||
|
const l = lanes.createLane({ cwd: "/tmp/wt-detect-heartbeat-noop" });
|
||||||
|
lanes.setStage(l.id, { stage: "plan" });
|
||||||
|
assert.deepEqual(lanes.recordDetection(l.id, { nodeId: "implement", signal: "`Edit`" }), {
|
||||||
|
written: true,
|
||||||
|
});
|
||||||
|
lanes.setStage(l.id, { stage: "plan", note: "still planning" });
|
||||||
|
assert.equal(lanes.getLane(l.id).detected_stage, "implement");
|
||||||
|
lanes.deleteLane(l.id);
|
||||||
|
});
|
||||||
|
|
||||||
it("migration: detection columns are added to a database holding an old-schema lanes row", () => {
|
it("migration: detection columns are added to a database holding an old-schema lanes row", () => {
|
||||||
const tmpPath = pathMod.join(
|
const tmpPath = pathMod.join(
|
||||||
os.tmpdir(),
|
os.tmpdir(),
|
||||||
@@ -744,12 +777,18 @@ describe("stage detection", () => {
|
|||||||
it("detection expiry: stale detection behind declared stage still rejects", () => {
|
it("detection expiry: stale detection behind declared stage still rejects", () => {
|
||||||
const { db } = require("../db");
|
const { db } = require("../db");
|
||||||
const l = lanes.createLane({ cwd: "/tmp/wt-detect-expiry-declared" });
|
const l = lanes.createLane({ cwd: "/tmp/wt-detect-expiry-declared" });
|
||||||
// Stand up the detection BEFORE declaring, otherwise declared-wins refuses
|
|
||||||
// it and there is no standing detection left to age.
|
|
||||||
lanes.recordDetection(l.id, { nodeId: "tests" });
|
|
||||||
lanes.setStage(l.id, { stage: "review" });
|
lanes.setStage(l.id, { stage: "review" });
|
||||||
|
// setStage now clears detected_* on every real transition (a fresh
|
||||||
|
// declaration supersedes older inference), so a standing detection is
|
||||||
|
// written directly here, bypassing that, to isolate the rule this test
|
||||||
|
// is actually about: declared-wins is never relaxed by staleness, no
|
||||||
|
// matter how the stale detection got there.
|
||||||
const oldTime = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString();
|
const oldTime = new Date(Date.now() - 2 * 60 * 60 * 1000).toISOString();
|
||||||
db.prepare("UPDATE lanes SET detected_at = ? WHERE id = ?").run(oldTime, l.id);
|
db.prepare("UPDATE lanes SET detected_stage = ?, detected_at = ? WHERE id = ?").run(
|
||||||
|
"tests",
|
||||||
|
oldTime,
|
||||||
|
l.id
|
||||||
|
);
|
||||||
|
|
||||||
// Try backward detection to implement - should still be rejected because declared stage is ahead
|
// Try backward detection to implement - should still be rejected because declared stage is ahead
|
||||||
const result = lanes.recordDetection(l.id, { nodeId: "implement" });
|
const result = lanes.recordDetection(l.id, { nodeId: "implement" });
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tool": "Write",
|
"tool": "Write",
|
||||||
"match": "^(?!.*(?:^|/)docs/)"
|
"match": "^(?!.*(?:^|/)(?:docs|\\.superpowers)/)"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
+15
-3
@@ -189,24 +189,36 @@ function deleteLane(id) {
|
|||||||
* Record a stage transition. `stage_since` moves ONLY when the stage value
|
* Record a stage transition. `stage_since` moves ONLY when the stage value
|
||||||
* actually changes, so the UI's time-on-phase is real; a re-report of the same
|
* actually changes, so the UI's time-on-phase is real; a re-report of the same
|
||||||
* stage (a heartbeat, an added note) leaves it alone.
|
* stage (a heartbeat, an added note) leaves it alone.
|
||||||
|
*
|
||||||
|
* A real transition also clears `detected_stage`/`detected_signal`/`detected_at`.
|
||||||
|
* Inference tracks progress relative to whatever the agent last declared; once
|
||||||
|
* the agent declares again, any older detection is either stale (a prior task's
|
||||||
|
* leftover, e.g. `tests` from earlier work bleeding into a fresh `plan`) or
|
||||||
|
* redundant (the agent's own claim now covers it). Left in place it would both
|
||||||
|
* 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.
|
||||||
*/
|
*/
|
||||||
function setStage(id, { stage, status, evidence, note, result } = {}) {
|
function setStage(id, { stage, status, evidence, note, result } = {}) {
|
||||||
const lane = getLane(id);
|
const lane = getLane(id);
|
||||||
if (!lane) throw Object.assign(new Error(`no lane ${id}`), { code: "ENOLANE" });
|
if (!lane) throw Object.assign(new Error(`no lane ${id}`), { code: "ENOLANE" });
|
||||||
const next = stage || lane.stage;
|
const next = stage || lane.stage;
|
||||||
|
const changed = next !== lane.stage;
|
||||||
const stages = { ...lane.stages };
|
const stages = { ...lane.stages };
|
||||||
const prev = stages[next] || {};
|
const prev = stages[next] || {};
|
||||||
stages[next] = {
|
stages[next] = {
|
||||||
enteredAt: next === lane.stage && prev.enteredAt ? prev.enteredAt : nowIso(),
|
enteredAt: !changed && prev.enteredAt ? prev.enteredAt : nowIso(),
|
||||||
evidence: evidence !== undefined ? evidence : prev.evidence || null,
|
evidence: evidence !== undefined ? evidence : prev.evidence || null,
|
||||||
result: result !== undefined ? result : prev.result || null,
|
result: result !== undefined ? result : prev.result || null,
|
||||||
};
|
};
|
||||||
db.prepare(
|
db.prepare(
|
||||||
`UPDATE lanes SET stage = ?, stage_since = ?, status = ?, stages = ?, notes = ?, updated_at = ?
|
`UPDATE lanes SET stage = ?, stage_since = ?, status = ?, stages = ?, notes = ?, updated_at = ?${
|
||||||
|
changed ? ", detected_stage = NULL, detected_signal = NULL, detected_at = NULL" : ""
|
||||||
|
}
|
||||||
WHERE id = ?`
|
WHERE id = ?`
|
||||||
).run(
|
).run(
|
||||||
next,
|
next,
|
||||||
next === lane.stage ? lane.stage_since || nowIso() : nowIso(),
|
changed ? nowIso() : lane.stage_since || nowIso(),
|
||||||
status || lane.status,
|
status || lane.status,
|
||||||
JSON.stringify(stages),
|
JSON.stringify(stages),
|
||||||
note !== undefined ? note : lane.notes,
|
note !== undefined ? note : lane.notes,
|
||||||
|
|||||||
Reference in New Issue
Block a user