feat(board): session attention, Discord rows, task attribution and relaunch activity (rows 18, 22, #1511, #1512)
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]>
This commit is contained in:
@@ -176,8 +176,8 @@ test("startServer: serves page, healthz, and a rescanning /api/board", async ()
|
||||
assert.equal(first.headers.get("content-type"), "application/json");
|
||||
assert.equal(first.headers.get("cache-control"), "no-store");
|
||||
const firstBody = await first.json();
|
||||
assert.equal(firstBody.sessions[0].state, "waiting");
|
||||
assert.ok(firstBody.waitingOnYou.includes("proj/agent1"));
|
||||
assert.equal(firstBody.sessions[0].state, "idle");
|
||||
assert.ok(!firstBody.waitingOnYou.includes("proj/agent1"));
|
||||
|
||||
assert.ok(existsSync(join(boardDir, "index.json")), "index.json must be written by the scan");
|
||||
assert.ok(existsSync(join(boardDir, "sessions", "proj", "agent1.json")), "per-agent file must be written by the scan");
|
||||
@@ -397,7 +397,7 @@ test("POST /api/seen marks a row; GET /api/board still shows it seen; seen:false
|
||||
const sessionsDir = join(root, "sessions");
|
||||
writeSessionFile(sessionsDir, "s.jsonl", [
|
||||
sessionLine({ id: "s1", timestamp: "2026-09-01T00:00:00Z", cwd: "/w" }),
|
||||
messageLine({ timestamp: "2026-09-01T00:00:01Z", role: "assistant", stopReason: "stop", texts: ["done"] }),
|
||||
messageLine({ timestamp: "2026-09-01T00:00:01Z", role: "assistant", stopReason: "stop", texts: ["Input needed: Confirm the fixture."] }),
|
||||
]);
|
||||
const boardDir = join(root, "board");
|
||||
const specs = [{ agent: "agent1", project: "proj", sessionsDir, tmux: {} }];
|
||||
@@ -557,7 +557,7 @@ test("CLI: scan --print marks a seen row with 's' and the summary line ends with
|
||||
join(sessionsDir, "s.jsonl"),
|
||||
[
|
||||
sessionLine({ id: "s1", timestamp: "2026-09-01T00:00:00Z", cwd: "/w" }),
|
||||
messageLine({ timestamp: lastActivity, role: "assistant", stopReason: "stop", texts: ["done"] }),
|
||||
messageLine({ timestamp: lastActivity, role: "assistant", stopReason: "stop", texts: ["Input needed: Confirm the fixture."] }),
|
||||
].join("\n") + "\n"
|
||||
);
|
||||
const boardDir = join(dataRoot, "board");
|
||||
@@ -881,3 +881,64 @@ test("page.html: the reply box appears only where canReply() holds (live registr
|
||||
assert.match(html, /function withReplyFocus\(render\)/, "a refresh gives the caret back to the reply box");
|
||||
assert.doesNotMatch(html, /send-keys/, "the page never talks tmux");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Task attribution (#1511): serialized by the server, rendered by page.html.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("startServer: /api/board carries taskSetBy from a live registration and null for the derived rows", async () => {
|
||||
const root = makeRoot();
|
||||
const sessionsDir = join(root, "sessions");
|
||||
const derivedDir = join(root, "derived-sessions");
|
||||
for (const dir of [sessionsDir, derivedDir]) {
|
||||
writeSessionFile(dir, "s.jsonl", [
|
||||
sessionLine({ id: "s1", timestamp: "2026-09-01T00:00:00Z", cwd: "/w" }),
|
||||
messageLine({ timestamp: "2026-09-01T00:00:01Z", role: "user", texts: ["derived task"] }),
|
||||
]);
|
||||
}
|
||||
const seatsDir = join(root, "seats");
|
||||
const reg = makeRegistration({
|
||||
resolved: { seat: "agent1", project: "proj", sessionsDir, seatDir: join(root, "agent1"), launchScript: join(root, "agent1", "launch.sh"), layout: "repo", defaultWorkspace: null },
|
||||
task: "registered task",
|
||||
});
|
||||
writeRegistration(seatsDir, { ...reg, taskSetBy: "darkwing" });
|
||||
const specs = [
|
||||
{ agent: "agent1", project: "proj", sessionsDir, tmux: {} },
|
||||
{ agent: "agent2", project: "proj", sessionsDir: derivedDir, tmux: {} },
|
||||
];
|
||||
const server = await startServer({ host: "127.0.0.1", port: 0, specs, boardDir: join(root, "board"), seatsDir, isAlive: () => ({ alive: true, workspace: null }), page: "<html></html>" });
|
||||
try {
|
||||
const body = await (await fetch(`http://127.0.0.1:${server.address().port}/api/board`)).json();
|
||||
const byAgent = Object.fromEntries(body.sessions.map((r) => [r.agent, r]));
|
||||
assert.equal(byAgent.agent1.taskSource, "registration");
|
||||
assert.equal(byAgent.agent1.taskSetBy, "darkwing");
|
||||
assert.equal(byAgent.agent2.taskSource, "first-user-message");
|
||||
assert.equal(byAgent.agent2.taskSetBy, null);
|
||||
assert.deepEqual(body.registrationErrors, []);
|
||||
} finally {
|
||||
await new Promise((r) => server.close(r));
|
||||
}
|
||||
});
|
||||
|
||||
test("page.html: the task cell and detail show who set a registered task via setByTag()/setByText(), both escaped, only from rec.taskSetBy; the reply gate does not read it", () => {
|
||||
const html = readFileSync(join(pkgRoot, "src", "page.html"), "utf8");
|
||||
const lines = html.split("\n");
|
||||
const rowPair = html.match(/function buildRowPair\(rec, showProject\) \{[\s\S]*?\n \}/);
|
||||
assert.ok(rowPair);
|
||||
assert.match(rowPair[0], /sourceTag\(rec\.taskSource\) \+ setByTag\(rec\)/, "the set-by tag follows the task's source tag on the row");
|
||||
assert.match(rowPair[0], /<dt>Task set by<\/dt><dd>" \+ setByText\(rec\) \+ "<\/dd>/, "the detail has a Task set by row");
|
||||
const tagLine = lines.find((l) => l.includes("function setByTag(rec)"));
|
||||
assert.ok(tagLine);
|
||||
assert.match(tagLine, /rec\.taskSetBy \?/, "nothing is shown without a value");
|
||||
assert.equal((tagLine.match(/esc\(rec\.taskSetBy\)/g) || []).length, 2, "the title and the text are both escaped");
|
||||
assert.match(tagLine, /not verified/);
|
||||
const textLine = lines.find((l) => l.includes("function setByText(rec)"));
|
||||
assert.ok(textLine);
|
||||
assert.match(textLine, /esc\(rec\.taskSetBy\)/);
|
||||
assert.match(textLine, /not verified/);
|
||||
// Attribution never enables a reply: canReply() reads the registration only.
|
||||
const canReplyFn = html.match(/function canReply\(rec\) \{[\s\S]*?\n \}/);
|
||||
assert.ok(canReplyFn);
|
||||
assert.equal(canReplyFn[0].includes("taskSetBy"), false);
|
||||
assert.equal(html.match(/function replyControl\(rec\) \{[\s\S]*?\n \}/)[0].includes("taskSetBy"), false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user