One cumulative control-board, webui and seat state. The four rows edit the
same files (scan.mjs, page.html, README.md, app.js), so they land together,
each on its own receipt:
- Row 18, Discord connector rows on the board (#1509): R3 approved by
Darkwing and Dewey, Gitea comment 26257, manifest 254403b8. Jason
accepted the visual test.
- Row 22, board attention status (#1503): Filbert approved R1, comment
26248, manifest e40b58ec; restart receipt 26249.
- #1511, task attribution (row 6 code phase): R2 approved by Filbert and
Dewey, manifest d4c96395. docs/TOOLS.md carries the approved --by usage
line (tools-usage.patch 86bcba3c).
- #1512, relaunch activity (row 6 pilot): R1 approved by Darkwing and
Dewey, candidate manifest 47769fad. All seven source files match it.
Row 16, internal development bootstrap (#1510): the seven files outside
shared records match Filbert's R1 pins, receipt 26204 (agents/researcher/*,
scripts/test-darkwing-launch.mjs, the bootstrap plan).
packages/webui/src/public/app.js is committed at its #1512 R1 pin ce7d79a4.
The working copy holds Dewey's unreviewed return-flow candidate on top of
that, and it stays uncommitted.
Also: the four row briefs and Darkwing's evidence records under
agents/darkwing/work, including the 2026-09-26 tree manifest and the #1512
re-run against 21e3e908. Serial acceptance command: 397/397, three runs.
The failures that only show when tests run concurrently are in #1509 engine
tests, and they reproduce on clean HEAD.
Suites on the exact staged tree: config 24, task 90, foundation 43,
conductor 17, release 14, auth 15, discord 63; package union 397/397
(serial); test-darkwing-launch 5/5.
Shared records (BUILD-LOG, QUEUE, CURRENT, DEFERRED, SESSIONS, AGENTS.md,
agents/README.md) follow in Sage's records commit.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
56 lines
2.9 KiB
JavaScript
56 lines
2.9 KiB
JavaScript
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, mkdirSync, writeFileSync, appendFileSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { scan, markSeen, deriveState } from '../src/scan.mjs';
|
|
|
|
test('explicit request, Seen, ordinary completion and a new request have distinct attention states', () => {
|
|
const root = mkdtempSync(join(tmpdir(), 'board-attention-flow-'));
|
|
try {
|
|
const sessionsDir = join(root, 'sessions');
|
|
mkdirSync(sessionsDir);
|
|
const file = join(sessionsDir, 's.jsonl');
|
|
writeFileSync(file, JSON.stringify({ type: 'session', id: 'fixture', cwd: root, timestamp: '2026-09-13T00:00:00Z' }) + '\n');
|
|
const specs = [{ agent: 'researcher', project: 'fixture', sessionsDir, tmux: {} }];
|
|
const boardDir = join(root, 'board');
|
|
const get = () => scan(specs, { boardDir, isAlive: () => true }).sessions[0];
|
|
const append = (second, text) => appendFileSync(file, JSON.stringify({
|
|
type: 'message', timestamp: `2026-09-13T00:00:0${second}Z`,
|
|
message: { role: 'assistant', stopReason: 'stop', content: [{ type: 'text', text }] },
|
|
}) + '\n');
|
|
append(1, 'Input needed: Choose A or B.');
|
|
let r = get();
|
|
assert.equal(r.state, 'waiting');
|
|
assert.equal(r.waitingOnYou, true);
|
|
markSeen(boardDir, { project: 'fixture', agent: 'researcher', lastActivity: r.lastActivity, seen: true });
|
|
r = get();
|
|
assert.equal(r.state, 'waiting', 'acknowledgment does not resolve an input request');
|
|
assert.equal(r.seen, true);
|
|
assert.equal(r.waitingOnYou, false);
|
|
append(2, 'BOARD_REPLY_OK');
|
|
r = get();
|
|
assert.equal(r.state, 'idle');
|
|
assert.equal(r.waitingOnYou, false);
|
|
assert.equal(r.seen, false, 'ordinary completion needs no Seen action');
|
|
append(3, 'Input needed: Confirm the revised choice.');
|
|
r = get();
|
|
assert.equal(r.state, 'waiting');
|
|
assert.equal(r.waitingOnYou, true, 'old Seen does not suppress a new request');
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('attention convention ignores reasoning/quoted examples and permits leading blank lines', () => {
|
|
const derive = (content, alive = true) => deriveState({ alive, session: { lastMessage: { role: 'assistant', stopReason: 'stop', content } } });
|
|
const text = (value) => [{ type: 'text', text: value }];
|
|
for (const value of ['> Input needed: Choose A.', ' Input needed: Choose A.', '`Input needed: Choose A.`', 'Input needed:\tChoose A.']) {
|
|
assert.equal(derive(text(value)), 'idle', value);
|
|
}
|
|
assert.equal(derive([{ type: 'thinking', thinking: 'Input needed: Choose A.' }, ...text('Done.')]), 'idle');
|
|
assert.equal(derive(text('\n\r\nInput needed: Choose A.')), 'waiting');
|
|
assert.equal(derive(text('Input needed: Choose A.'), false), 'offline');
|
|
assert.equal(derive(text('Input needed: Choose A.'), null), 'unknown');
|
|
});
|