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]>
74 lines
3.5 KiB
JavaScript
74 lines
3.5 KiB
JavaScript
// Read-only connector discovery. Never load binding policy or token resources.
|
|
import { lstatSync, readdirSync, openSync, closeSync, fstatSync, readFileSync, constants } from 'node:fs';
|
|
import { join, resolve, parse } from 'node:path';
|
|
import { readPid, ownerState } from '../../discord/src/journal.mjs';
|
|
|
|
const NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
// Check every existing ancestor, not just the final component.
|
|
export function noLinks(path) {
|
|
const absolute = resolve(path);
|
|
let current = parse(absolute).root;
|
|
for (const part of absolute.slice(current.length).split('/').filter(Boolean)) {
|
|
current = join(current, part);
|
|
try { if (lstatSync(current).isSymbolicLink()) return false; }
|
|
catch (e) { if (e.code === 'ENOENT') return true; return false; }
|
|
}
|
|
return true;
|
|
}
|
|
function stat(path) { try { return lstatSync(path); } catch { return null; } }
|
|
export function discoverDiscordAgents(dataRoot) {
|
|
const specs = [], errors = [];
|
|
const dir = join(dataRoot, 'discord');
|
|
if (!noLinks(dir)) return { specs, errors: ['discord discovery: unsafe directory'] };
|
|
const ds = stat(dir);
|
|
if (!ds) return { specs, errors };
|
|
if (!ds.isDirectory()) return { specs, errors: ['discord discovery: invalid directory'] };
|
|
let names;
|
|
try { names = readdirSync(dir).sort(); } catch { return { specs, errors: ['discord discovery: unreadable directory'] }; }
|
|
for (const file of names.filter(n => n.endsWith('.json'))) {
|
|
let fd;
|
|
try {
|
|
const name = file.slice(0, -5);
|
|
if (!NAME.test(name)) throw new Error();
|
|
fd = openSync(join(dir, file), constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
const s = fstatSync(fd);
|
|
if (!s.isFile() || (s.mode & 0o7777) !== 0o600 || s.size > 1024 * 1024) throw new Error();
|
|
const raw = JSON.parse(readFileSync(fd, 'utf8'));
|
|
if (!raw || raw.name !== name || typeof raw.seat !== 'string' || !NAME.test(raw.seat)) throw new Error();
|
|
// Do not retain the binding object, even in internal scan specs.
|
|
specs.push({ agent: `${raw.seat} (discord: ${name})`, project: 'fleet', sessionsDir: join(dataRoot, 'sessions', `discord-${name}`), tmux: null, connector: { binding: name, journalDir: join(dir, name) } });
|
|
} catch { errors.push('discord discovery: skipped invalid binding'); }
|
|
finally { if (fd !== undefined) closeSync(fd); }
|
|
}
|
|
return { specs, errors };
|
|
}
|
|
export function inspectDiscord(connector, { identity } = {}) {
|
|
const dir = connector.journalDir, owner = join(dir, 'run.lock', 'owner.json');
|
|
let braked = null;
|
|
if (noLinks(dir)) {
|
|
try { lstatSync(join(dir, 'STOP')); braked = true; }
|
|
catch (e) { if (e.code === 'ENOENT') braked = false; }
|
|
}
|
|
let ownerStatus = 'invalid';
|
|
if (noLinks(owner)) {
|
|
const s = stat(owner);
|
|
if (!s || s.isFile()) {
|
|
try { ownerStatus = ownerState(readPid(dir), identity ? { identity } : {}); } catch { ownerStatus = 'invalid'; }
|
|
}
|
|
}
|
|
return { binding: connector.binding, braked, ownerState: ownerStatus, alive: ownerStatus === 'live' };
|
|
}
|
|
// Connector session trees must not follow links, including ancestor links.
|
|
export function newestDiscordSession(dir) {
|
|
if (!noLinks(dir)) return null;
|
|
let best = null;
|
|
const walk = path => {
|
|
const s = stat(path);
|
|
if (!s || s.isSymbolicLink()) return;
|
|
if (s.isDirectory()) { for (const name of readdirSync(path)) walk(join(path, name)); }
|
|
else if (s.isFile() && path.endsWith('.jsonl') && (!best || s.mtimeMs > best.time)) best = { path, time: s.mtimeMs };
|
|
};
|
|
walk(dir);
|
|
return best?.path ?? null;
|
|
}
|