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
+27 -3
View File
@@ -3,7 +3,7 @@
`mosaic launch <seat>` starts a seat through its existing launch script,
unchanged, and leaves one registration record that the control board reads
instead of guessing. `mosaic seat task <seat> <text>` changes the task on
that record. Nothing else. No provider or auth registry, no roster schema
that record and notes who set it. Nothing else. No provider or auth registry, no roster schema
change, no stopping or killing of seats, one record per seat.
Issue #1504. Plain ESM, no dependencies, Node 24 or newer.
@@ -17,7 +17,7 @@ or `seat` command; run it by path.
```
scripts/mosaic launch <seat|seat-dir> [--task TEXT] [--project NAME] [--workspace PATH]
[--harness NAME] [--repo PATH] [--config PATH] [-- args...]
scripts/mosaic seat task <seat> <text> [--layout repo|fleet|unknown] [--config PATH]
scripts/mosaic seat task <seat> <text> [--by NAME] [--layout repo|fleet|unknown] [--config PATH]
```
- `<seat>` is a name under `<repo>/agents/` (`--repo` defaults to the
@@ -36,6 +36,17 @@ scripts/mosaic seat task <seat> <text> [--layout repo|fleet|unknown] [--config P
- The launch script receives `MOSAIC_LAUNCH_REGISTERED=<record path>`. A
launch script that sees this variable is already registered and must not
call `mosaic launch` again; `mosaic launch` refuses to run when it is set.
- `seat task` records who set the task in `taskSetBy` (#1511): `--by NAME`
if given, else `$MOSAIC_AGENT_NAME` if set and non-empty, else the word
`unknown`. An explicit value must be one token of 1 to 64 characters
(letters, digits, `.` `_` `@` `:` `-`, starting with a letter or digit);
anything else refuses with exit 4 before the record is touched. A set but
malformed `MOSAIC_AGENT_NAME` also refuses rather than being reported as
`unknown`; `--by` is the explicit way past it. The value is never echoed
back in a refusal. **This is what the caller said, not a verified
identity.** Nothing checks it, nothing grants on it; the board displays
it next to the task and does no more with it. `launch --task` does not
set the field.
The four repository seats (`agents/darkwing`, `agents/dewey`,
`agents/filbert`, `agents/rocko`) register themselves: their `launch.sh`
@@ -81,6 +92,10 @@ scan never changes it.
}
```
After `scripts/mosaic seat task darkwing "Row 6 (#1511)" --by jason` the
same record also carries `"taskSetBy": "jason"` and a non-null
`updatedAt`; `startedAt` and every other field are unchanged.
Fields asked for in the brief: `seat`, `project`, `task` (empty unless
`--task` or a later `seat task`), `workspace`, `tmux` (session name and
socket, from the `TMUX` variable of the pane the launch ran in; null outside
@@ -100,6 +115,11 @@ Fields added, and why:
the record is still written but the board cannot match it).
- `updatedAt`: set only by `seat task`, so a task change is distinguishable
from a relaunch.
- `taskSetBy`: set only by `seat task` (see above). Optional in the shape:
a record written before the field existed loads unchanged, the board shows
`unknown` for it, and nothing rewrites it until the next `seat task` or
launch. The record version stays 1; a value that is not one bounded token
makes the record unreadable (refused, not misread).
- `version`: so a later shape change can be refused rather than misread.
`project` and `workspace` are derived only for the repo layout (the
@@ -119,7 +139,11 @@ registration are unchanged. A malformed record is reported in
`registrationErrors` on the index and skipped; it never takes the board down
and it is never treated as absent silently. A record whose pid is no
longer running is stale: still shown on the Registered line, but it does
not override anything.
not override anything. The row's `taskSetBy` is the record's setter only
while the task shown is the registered one (`taskSource` `registration`);
otherwise it is null, so a stale or empty registration never attributes a
transcript-derived task, and a Discord connector's fixed task never borrows
a native record's setter.
The repository launch scripts are the only launchers that register.
Fleet seats under `~/.mosaic` stay on their own launchers (Jason's ruling,
+14 -7
View File
@@ -2,7 +2,11 @@
// Usage:
// mosaic launch <seat|seat-dir> [--task TEXT] [--project NAME] [--workspace PATH]
// [--harness NAME] [--repo PATH] [--config PATH] [-- args...]
// mosaic seat task <seat> <text> [--layout repo|fleet|unknown] [--config PATH]
// mosaic seat task <seat> <text> [--by NAME] [--layout repo|fleet|unknown] [--config PATH]
//
// `seat task` records who set the task as taskSetBy: `--by NAME`, else
// $MOSAIC_AGENT_NAME, else "unknown". This is what the caller claimed; it is
// not verified and it authorizes nothing.
//
// `launch` writes <dataRoot>/seats/<layout>/<seat>/registration.json, then replaces
// itself with the seat's launch.sh, unchanged, with everything after `--` as
@@ -15,12 +19,12 @@
import { rmSync } from "node:fs";
import {
SeatError, defaultConfigPath, loadDataRoot, seatsDir, resolveSeat, tmuxContext,
makeRegistration, writeRegistration, updateTask,
makeRegistration, writeRegistration, updateTask, resolveSetBy,
} from "./seat.mjs";
const USAGE = [
"usage: mosaic launch <seat|seat-dir> [--task TEXT] [--project NAME] [--workspace PATH] [--harness NAME] [--repo PATH] [--config PATH] [-- args...]",
" mosaic seat task <seat> <text> [--layout repo|fleet|unknown] [--config PATH]",
" mosaic seat task <seat> <text> [--by NAME] [--layout repo|fleet|unknown] [--config PATH]",
].join("\n");
export const REGISTERED_ENV = "MOSAIC_LAUNCH_REGISTERED";
@@ -49,10 +53,10 @@ function parseLaunch(argv) {
}
function parseSeatTask(argv) {
const opts = { seat: null, task: null, layout: null, config: defaultConfigPath() };
const opts = { seat: null, task: null, layout: null, by: null, config: defaultConfigPath() };
for (let i = 0; i < argv.length; i++) {
const a = argv[i];
if (a === "--config" || a === "--layout") {
if (a === "--config" || a === "--layout" || a === "--by") {
if (i + 1 >= argv.length) throw new SeatError(`missing value for ${a}`, 4);
opts[a.slice(2)] = argv[++i];
} else if (a.startsWith("--")) throw new SeatError(`unknown argument: ${a}\n${USAGE}`, 4);
@@ -90,9 +94,12 @@ function launch(argv) {
function seatTask(argv) {
const opts = parseSeatTask(argv);
// Attribution is resolved (and an invalid value refused) before any read
// or write, so a refusal leaves the record exactly as it was.
const setBy = resolveSetBy({ by: opts.by, env: process.env });
const seats = seatsDir(loadDataRoot(opts.config));
const record = updateTask(seats, opts.seat, opts.task, { layout: opts.layout });
process.stdout.write(`mosaic seat task: ${opts.seat} (${record.layout} layout) task set (${opts.task.length} characters)\n`);
const record = updateTask(seats, opts.seat, opts.task, { layout: opts.layout, setBy });
process.stdout.write(`mosaic seat task: ${opts.seat} (${record.layout} layout) task set (${opts.task.length} characters, set by ${setBy})\n`);
return 0;
}
+38 -5
View File
@@ -23,6 +23,15 @@ import { spawnSync } from "node:child_process";
export const REGISTRATION_VERSION = 1;
export const SEAT_NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;
export const TASK_LIMIT = 2000;
// Who set the task (#1511): caller-supplied attribution, bounded to one
// short token so it cannot carry markup or whitespace. The bound is a syntax
// limit, not a privacy filter: a numeric ID or an email-like name still fits,
// so callers choose what they put here. It is what the caller claimed, not
// an authenticated identity, and it grants nothing: the board displays it
// and does no more with it.
export const SET_BY_NAME = /^[A-Za-z0-9][A-Za-z0-9._@:-]{0,63}$/;
export const SET_BY_UNKNOWN = "unknown";
export const SET_BY_ENV = "MOSAIC_AGENT_NAME";
// exitCode follows docs/TOOLS.md: 1 operation failed, 2 invalid data or
// configuration, 4 usage.
@@ -121,6 +130,7 @@ export function tmuxContext({ env = process.env, exec = spawnSync } = {}) {
const FIELDS = Object.freeze([
"version", "seat", "project", "task", "workspace", "tmux", "harness",
"startedAt", "pid", "sessionsDir", "seatDir", "launchScript", "layout", "updatedAt",
"taskSetBy",
]);
const isNullableString = (v) => v === null || typeof v === "string";
@@ -134,6 +144,9 @@ export function validateRegistration(record) {
if (record.version !== REGISTRATION_VERSION) throw new SeatError("registration has an unsupported version");
if (typeof record.seat !== "string" || !SEAT_NAME.test(record.seat)) throw new SeatError("registration.seat is invalid");
if (typeof record.task !== "string" || record.task.length > TASK_LIMIT) throw new SeatError("registration.task must be a string");
// Optional: records written before #1511 have no taskSetBy and still load
// (the board shows "unknown"); no version change, no migration on read.
if (record.taskSetBy !== undefined && !(typeof record.taskSetBy === "string" && SET_BY_NAME.test(record.taskSetBy))) throw new SeatError("registration.taskSetBy is invalid");
for (const key of ["project", "workspace", "harness", "sessionsDir", "seatDir", "launchScript"]) {
if (!isNullableString(record[key])) throw new SeatError(`registration.${key} must be a string or null`);
}
@@ -208,17 +221,37 @@ export function findRegistrations(seats, seat) {
return LAYOUTS.map((layout) => readRegistration(seats, seat, layout)).filter(Boolean);
}
// Change the task field only. Refuses when the seat was never launched
// through `mosaic launch`, because there is nothing to attach the task to.
// A name that exists in more than one layout must be qualified with layout.
export function updateTask(seats, seat, task, { layout = null, now = () => new Date() } = {}) {
// Who a `seat task` is attributed to: an explicit `--by NAME`, else the
// MOSAIC_AGENT_NAME variable, else "unknown". An explicit value must match
// SET_BY_NAME; so must a non-empty environment value, because a malformed
// or unexpected value there is not the same as no value and is refused
// rather than silently reported as "unknown". Neither value is echoed back.
// The result is a claim by the caller, nothing more.
export function resolveSetBy({ by = null, env = process.env } = {}) {
if (by !== null && by !== undefined) {
if (typeof by !== "string" || !SET_BY_NAME.test(by)) throw new SeatError(`--by must be 1-64 characters of letters, digits, . _ @ : or -, starting with a letter or digit`, 4);
return by;
}
const fromEnv = env[SET_BY_ENV];
if (fromEnv === undefined || fromEnv === "") return SET_BY_UNKNOWN;
if (typeof fromEnv !== "string" || !SET_BY_NAME.test(fromEnv)) throw new SeatError(`${SET_BY_ENV} is set but is not a valid name (1-64 characters of letters, digits, . _ @ : or -); pass --by NAME or unset it`, 4);
return fromEnv;
}
// Change the task field, and record who set it (taskSetBy), and updatedAt.
// Every other field, startedAt included, is carried over unchanged. Refuses
// when the seat was never launched through `mosaic launch`, because there is
// nothing to attach the task to. A name that exists in more than one layout
// must be qualified with layout.
export function updateTask(seats, seat, task, { layout = null, now = () => new Date(), setBy = SET_BY_UNKNOWN } = {}) {
if (typeof task !== "string") throw new SeatError("task must be a string", 4);
if (task.length > TASK_LIMIT) throw new SeatError(`task is longer than ${TASK_LIMIT} characters`, 4);
if (typeof setBy !== "string" || !SET_BY_NAME.test(setBy)) throw new SeatError("setBy must be a valid name", 4);
const found = layout ? [readRegistration(seats, seat, layout)].filter(Boolean) : findRegistrations(seats, seat);
if (found.length === 0) throw new SeatError(`no registration for seat ${seat}; launch it through mosaic launch first`, 1);
if (found.length > 1) throw new SeatError(`seat ${seat} is registered in more than one layout (${found.map((r) => r.layout).join(", ")}); pass --layout`, 4);
const record = found[0];
const updated = { ...record, task, updatedAt: now().toISOString() };
const updated = { ...record, task, taskSetBy: setBy, updatedAt: now().toISOString() };
writeRegistration(seats, updated);
return updated;
}
+172 -1
View File
@@ -29,7 +29,10 @@ import {
updateTask,
findRegistrations,
samePath,
resolveSetBy,
TASK_LIMIT,
SET_BY_NAME,
SET_BY_UNKNOWN,
} from "../src/seat.mjs";
const pkgRoot = resolve(import.meta.dirname, "..");
@@ -91,6 +94,9 @@ function buildConfig(root, dataRoot = join(root, "data")) {
function cliEnv(overrides = {}) {
const env = { ...process.env, TMUX: "", TMUX_PANE: "" };
delete env.MOSAIC_LAUNCH_REGISTERED;
// The developer's shell may carry a seat name (agents/rocko/launch.sh
// exports one); attribution tests set it explicitly or not at all.
delete env.MOSAIC_AGENT_NAME;
return { ...env, ...overrides };
}
@@ -290,7 +296,7 @@ test("writeRegistration/readRegistration: round trip, permissions, absence, and
// 7. updateTask
// ---------------------------------------------------------------------------
test("updateTask: changes task and updatedAt only, and refuses appropriately", () => {
test("updateTask: changes task, taskSetBy and updatedAt only, and refuses appropriately", () => {
const root = makeRoot();
const seatDir = buildRepoSeat(root, "myseat");
const resolved = resolveSeat("myseat", { repo: root });
@@ -300,11 +306,14 @@ test("updateTask: changes task and updatedAt only, and refuses appropriately", (
const updated = updateTask(seats, "myseat", "revised");
assert.equal(updated.task, "revised");
assert.equal(updated.taskSetBy, SET_BY_UNKNOWN, "no setBy option records the literal unknown");
assert.notEqual(updated.updatedAt, null);
for (const key of Object.keys(record)) {
if (key === "task" || key === "updatedAt") continue;
assert.deepEqual(updated[key], record[key], `field ${key} changed unexpectedly`);
}
assert.equal(updated.startedAt, record.startedAt, "startedAt is the launch time and survives a task update");
assert.deepEqual(Object.keys(updated).sort(), [...Object.keys(record), "taskSetBy"].sort(), "exactly one field is added");
// Refuses when there is no registration to attach to.
assert.throws(() => updateTask(seats, "ghost", "x"), (err) => {
@@ -560,3 +569,165 @@ test("samePath: equal paths, symlinked dirs, distinct dirs, and non-strings", ()
assert.equal(samePath(1, "x"), false);
assert.equal(samePath(null, target), false);
});
// ---------------------------------------------------------------------------
// 14. Task attribution (#1511): taskSetBy on the record, resolveSetBy, and the
// --by / MOSAIC_AGENT_NAME precedence at the CLI. Attribution is a claim by
// the caller: bounded, escaped by the presentations, never authority.
// ---------------------------------------------------------------------------
test("resolveSetBy: explicit --by wins over the environment; absent or empty environment gives unknown; invalid explicit or environment values refuse with exit 4", () => {
assert.equal(resolveSetBy({ by: "darkwing", env: { MOSAIC_AGENT_NAME: "rocko" } }), "darkwing");
assert.equal(resolveSetBy({ by: null, env: { MOSAIC_AGENT_NAME: "rocko" } }), "rocko");
assert.equal(resolveSetBy({ env: {} }), SET_BY_UNKNOWN);
assert.equal(resolveSetBy({ env: { MOSAIC_AGENT_NAME: "" } }), SET_BY_UNKNOWN);
assert.equal(resolveSetBy({ by: "jason@host:1", env: {} }), "jason@host:1");
assert.equal(resolveSetBy({ by: "a".repeat(64), env: {} }), "a".repeat(64), "64 characters is the bound");
const refused = (fn, pattern) => assert.throws(fn, (err) => {
assert.ok(err instanceof SeatError);
assert.equal(err.exitCode, 4);
assert.match(err.message, pattern);
return true;
});
for (const bad of ["", " ", "a".repeat(65), "<b>x</b>", "two words", "-leading", ".leading", "tab\tname", "new\nline", "ünïcode", "a/b", "a;b", "$(x)"]) {
refused(() => resolveSetBy({ by: bad, env: { MOSAIC_AGENT_NAME: "valid" } }), /--by/);
assert.equal(SET_BY_NAME.test(bad), false);
// The offending value is never echoed back.
if (bad.trim().length > 1) assert.throws(() => resolveSetBy({ by: bad, env: {} }), (err) => !err.message.includes(bad));
}
refused(() => resolveSetBy({ by: 42, env: {} }), /--by/);
// A set but malformed environment value is refused, not read as unknown:
// silently reporting "unknown" would hide a misconfigured seat, and --by
// is the explicit way past it.
refused(() => resolveSetBy({ env: { MOSAIC_AGENT_NAME: "<script>" } }), /MOSAIC_AGENT_NAME/);
refused(() => resolveSetBy({ env: { MOSAIC_AGENT_NAME: "a".repeat(65) } }), /MOSAIC_AGENT_NAME/);
assert.throws(() => resolveSetBy({ env: { MOSAIC_AGENT_NAME: "<script>" } }), (err) => !err.message.includes("<script>"));
assert.equal(resolveSetBy({ by: "explicit", env: { MOSAIC_AGENT_NAME: "<script>" } }), "explicit", "an explicit value does not consult the environment at all");
});
test("validateRegistration/readRegistration: taskSetBy is optional; a record without it (written before #1511) still loads unchanged; an invalid one is refused; the version does not change", () => {
const root = makeRoot();
const seatDir = buildRepoSeat(root, "old");
const resolved = resolveSeat("old", { repo: root });
const seats = seatsDir(join(root, "data"));
const record = makeRegistration({ resolved, task: "launched task", pid: 1 });
assert.equal("taskSetBy" in record, false, "launch does not attribute; only seat task does");
assert.equal(record.version, 1);
// An old on-disk record, byte for byte, without the field.
const path = registrationPath(seats, "old", "repo");
mkdirSync(dirname(path), { recursive: true });
const oldBytes = JSON.stringify(record, null, 2) + "\n";
writeFileSync(path, oldBytes);
const loaded = readRegistration(seats, "old", "repo");
assert.deepEqual(loaded, record);
assert.equal(loaded.taskSetBy, undefined);
assert.equal(readFileSync(path, "utf8"), oldBytes, "reading never rewrites (no implicit migration)");
assert.equal(findRegistrations(seats, "old").length, 1);
// Valid values.
for (const ok of ["unknown", "darkwing", "jason@host:1", "a".repeat(64)]) validateRegistration({ ...record, taskSetBy: ok });
// Invalid values, never echoed.
for (const bad of [null, 7, "", "<b>", "two words", "a".repeat(65), {}]) {
assert.throws(() => validateRegistration({ ...record, taskSetBy: bad }), (err) => {
assert.ok(err instanceof SeatError);
assert.match(err.message, /taskSetBy/);
if (typeof bad === "string" && bad.length > 1) assert.equal(err.message.includes(bad), false);
return true;
});
}
writeFileSync(path, JSON.stringify({ ...record, taskSetBy: "<b>" }));
assert.throws(() => readRegistration(seats, "old", "repo"), /taskSetBy/);
});
test("updateTask: records setBy, preserves startedAt and every unrelated field, upgrades an old record in place only when the task is set, and replaces a previous attribution", () => {
const root = makeRoot();
buildRepoSeat(root, "myseat");
const resolved = resolveSeat("myseat", { repo: root });
const seats = seatsDir(join(root, "data"));
const started = new Date("2026-09-10T10:00:00Z");
const record = makeRegistration({ resolved, task: "original", harness: "pi", tmux: { socket: "mosaic-fleet", session: "myseat" }, pid: 1, workspace: "/w", project: "proj", now: () => started });
writeRegistration(seats, record);
const first = updateTask(seats, "myseat", "by darkwing", { setBy: "darkwing", now: () => new Date("2026-09-14T10:00:00Z") });
assert.equal(first.taskSetBy, "darkwing");
assert.equal(first.startedAt, started.toISOString());
assert.equal(first.updatedAt, "2026-09-14T10:00:00.000Z");
for (const key of ["seat", "project", "workspace", "tmux", "harness", "pid", "sessionsDir", "seatDir", "launchScript", "layout", "version"]) {
assert.deepEqual(first[key], record[key], `field ${key} changed unexpectedly`);
}
assert.deepEqual(readRegistration(seats, "myseat", "repo"), first);
const second = updateTask(seats, "myseat", "by jason", { setBy: "jason", now: () => new Date("2026-09-14T11:00:00Z") });
assert.equal(second.taskSetBy, "jason", "a later set replaces the attribution; it is not appended");
assert.equal(second.startedAt, started.toISOString());
const third = updateTask(seats, "myseat", "anonymous");
assert.equal(third.taskSetBy, SET_BY_UNKNOWN, "no setBy means unknown, never the previous name");
// Invalid setBy refuses with exit 4 before anything is written.
const before = readFileSync(registrationPath(seats, "myseat", "repo"), "utf8");
assert.throws(() => updateTask(seats, "myseat", "x", { setBy: "<b>" }), (err) => err instanceof SeatError && err.exitCode === 4);
assert.throws(() => updateTask(seats, "myseat", "x", { setBy: "" }), (err) => err instanceof SeatError && err.exitCode === 4);
assert.equal(readFileSync(registrationPath(seats, "myseat", "repo"), "utf8"), before);
});
test("CLI seat task: --by beats MOSAIC_AGENT_NAME, the environment beats nothing, empty environment is unknown, invalid --by or environment refuses with exit 4 and leaves the record byte for byte", () => {
const root = makeRoot();
buildRepoSeat(root, "myseat");
const { configPath, dataRoot } = buildConfig(root);
const path = registrationPath(seatsDir(dataRoot), "myseat", "repo");
const launchResult = runCli(["launch", "myseat", "--task", "before", "--config", configPath], { cwd: root, env: cliEnv({ MOSAIC_CONFIG: configPath, MOSAIC_AGENT_NAME: "launcher" }) });
assert.equal(launchResult.status, 0, launchResult.stderr);
const launched = JSON.parse(readFileSync(path, "utf8"));
assert.equal("taskSetBy" in launched, false, "launch --task does not attribute, even with the variable set");
const run = (args, env) => runCli(["seat", "task", "myseat", ...args, "--config", configPath], { cwd: root, env: cliEnv({ MOSAIC_CONFIG: configPath, ...env }) });
const read = () => JSON.parse(readFileSync(path, "utf8"));
let r = run(["one", "--by", "darkwing"], { MOSAIC_AGENT_NAME: "rocko" });
assert.equal(r.status, 0, r.stderr);
assert.match(r.stdout, /set by darkwing/);
assert.equal(read().taskSetBy, "darkwing");
assert.equal(read().task, "one");
assert.equal(read().startedAt, launched.startedAt);
r = run(["two"], { MOSAIC_AGENT_NAME: "rocko" });
assert.equal(r.status, 0, r.stderr);
assert.match(r.stdout, /set by rocko/);
assert.equal(read().taskSetBy, "rocko");
r = run(["three"], {});
assert.equal(r.status, 0, r.stderr);
assert.match(r.stdout, /set by unknown/);
assert.equal(read().taskSetBy, "unknown");
r = run(["four"], { MOSAIC_AGENT_NAME: "" });
assert.equal(r.status, 0, r.stderr);
assert.equal(read().taskSetBy, "unknown");
for (const key of Object.keys(launched)) {
if (key === "task" || key === "updatedAt") continue;
assert.deepEqual(read()[key], launched[key], `field ${key} changed unexpectedly`);
}
const frozen = readFileSync(path, "utf8");
r = run(["five", "--by", "<script>alert(1)</script>"], { MOSAIC_AGENT_NAME: "rocko" });
assert.equal(r.status, 4);
assert.match(r.stderr, /refused: --by/);
assert.equal(r.stderr.includes("<script>"), false, "the refusal does not echo the value");
assert.equal(readFileSync(path, "utf8"), frozen, "an invalid --by writes nothing");
r = run(["five", "--by", ""], {});
assert.equal(r.status, 4);
assert.equal(readFileSync(path, "utf8"), frozen);
r = runCli(["seat", "task", "myseat", "five", "--config", configPath, "--by"], { cwd: root, env: cliEnv({ MOSAIC_CONFIG: configPath }) });
assert.equal(r.status, 4);
assert.match(r.stderr, /missing value for --by/);
assert.equal(readFileSync(path, "utf8"), frozen);
r = run(["five"], { MOSAIC_AGENT_NAME: "not a name" });
assert.equal(r.status, 4);
assert.match(r.stderr, /MOSAIC_AGENT_NAME/);
assert.equal(r.stderr.includes("not a name"), false);
assert.equal(readFileSync(path, "utf8"), frozen, "an invalid environment value writes nothing");
r = run(["six", "--by", "explicit"], { MOSAIC_AGENT_NAME: "not a name" });
assert.equal(r.status, 0, "an explicit --by is the way past a bad environment value");
assert.equal(read().taskSetBy, "explicit");
assert.equal(read().task, "six");
});