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:
2026-09-26 14:54:18 -05:00
co-authored by Claude Opus 5.5
parent 21e3e908b6
commit af4203ca92
67 changed files with 2417 additions and 78 deletions
+65 -4
View File
@@ -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);
});