Files
stack/packages/cli/src/format.mjs
T
jason.woltjeandClaude Opus 5.5 2f5303c1c7 feat(cli): the mosaic CLI, broker host and decision notifier (row 39, S4, rocko)
packages/cli adds mosaic inbox, decide, tasks, agents and trail over the
human-cli transport, and mosaic bus start, stop and status as the trusted
host (unit mosaic-bus@<business>, scripts/bus-service.sh). The host boots
packages/bus/src/process.mjs, passes config.trackers from the tracker.*
variables (lead decision 70), and runs a notifier child. The notifier DMs
each open blocking decision once and sends an 08:00 America/Chicago
digest, journaled in notify/<business>/sent.jsonl at 0600 with no Discord
ids. A torn journal tail is copied aside and truncated; a malformed line,
a directory looser than 0700 or a symlinked journal refuses (lead
decision 71). packages/discord gains dmRecipient, createDm and notify.mjs.

Candidate agents/rocko/work/slice1-s4, base b9b6cf00, build.patch
b52f7d68, manifest e858504e (29 files). Darkwing approved round 2 on
#1521 (comment 26855), Filbert approved round 2 (comment 26856). The
packet's mutant table lists M28 as killed; it survived, and BUILD-LOG
records the correction.

Integration gate in a worktree on 2557e29d with the patch applied:
bus 67, business 60, cli 49, control-board 124, discord 178, ledger 78,
mosaic 69, queue 148, seat 19, tasks 51 and webui 14, all with no
failures. Conversation is 149/3, the same K1, K3 and K10 cases that fail
on the base; S4 doesn't touch the package. Every scripts/test-*.sh is
green, with test-release 14/14 and test-task 98/98 on the existing gate2
compose network. A scratch test, not in this commit, booted the real
host with trackers against S3's fake Vikunja: the adapter went ready and
a task.close on a missing task answered task-not-found after a Vikunja
read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 07:49:38 -05:00

98 lines
3.9 KiB
JavaScript

// Plain-text views of broker results. Every function takes what the broker
// returned and keeps its order: the broker sorts, this file never re-sorts.
const one = (s, max = 160) => {
const flat = String(s ?? "").replace(/\s+/g, " ").trim();
return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
};
export const shortId = (id) => String(id).slice(0, 8);
export function optionsLine(d) {
return d.options.map((o) => `${o.key} = ${one(o.text, 80)}`).join("; ");
}
// What approving means, when the decision carries authorization context.
export function authorizationLines(d) {
const a = d.authorization;
if (!a) return [];
const lines = [`action: ${a.action}${a.target ? ` on ${a.target}` : ""}`];
lines.push(`choosing "${a.approvalChoice}" authorizes it; any other choice declines`);
return lines;
}
export function formatInbox(list) {
if (list.length === 0) return "inbox: empty\n";
const out = [`inbox: ${list.length} open decision(s)`];
for (const d of list) {
const flags = [d.class, d.blocking ? "blocking" : null].filter(Boolean).join(", ");
out.push("", `${shortId(d.id)} ${d.action} (${flags}) raised ${d.at} by ${d.raised_by_role}`);
out.push(` ${one(d.question, 400)}`);
out.push(` options: ${optionsLine(d)} (recommended: ${d.recommendation})`);
for (const l of authorizationLines(d)) out.push(` ${l}`);
if (d.task_ref) out.push(` task: ${d.task_ref}`);
out.push(` decide: mosaic decide ${shortId(d.id)} <option>`);
}
return `${out.join("\n")}\n`;
}
export function formatDecision(d) {
const out = [
`decision ${d.id}`,
` ${d.class}${d.blocking ? ", blocking" : ""}, raised ${d.at} by ${d.raised_by_role} (${d.raised_by_run})`,
` question: ${d.question}`,
` options: ${optionsLine(d)}`,
` recommended: ${d.recommendation}`,
];
for (const l of authorizationLines(d)) out.push(` ${l}`);
if (d.task_ref) out.push(` task: ${d.task_ref}`);
return `${out.join("\n")}\n`;
}
export function formatAgents(list) {
if (list.length === 0) return "agents: no role is claimed\n";
return `${list.map((c) => `${c.role} ${c.holder_run} ${c.harness ?? "-"} since ${c.at}`).join("\n")}\n`;
}
export function formatTasks(list) {
if (list.length === 0) return "tasks: none\n";
return `${list
.map((t) => {
const f = t.fields ?? {};
const title = f.title ?? f.name ?? "";
const state = f.status ?? f.state ?? (f.done === true ? "done" : f.done === false ? "open" : "");
return [t.task_ref, state, one(title, 100)].filter(Boolean).join(" ");
})
.join("\n")}\n`;
}
function trailSummary(r) {
switch (r.table) {
case "events":
return [r.kind, r.actor_role ?? null, r.body?.operation ?? null, r.body?.decision ? `decision ${shortId(r.body.decision)}` : null].filter(Boolean).join(" ");
case "decisions":
return `${r.class} ${r.action} → ${r.route_to}${r.blocking ? " (blocking)" : ""}: ${one(r.question, 120)}`;
case "decision_events":
return [r.op, r.choice ? `choice ${r.choice}` : null, `by ${r.by}`, r.via ? `via ${r.via}` : null].filter(Boolean).join(" ");
case "messages":
return `${r.from_role} → ${r.to_role}: ${one(r.body, 120)}`;
case "deliveries":
return [r.op, r.transport, r.holder_run].filter(Boolean).join(" ");
case "role_claims":
return `${r.op} ${r.role} by ${r.holder_run}`;
case "task_snapshots":
return `snapshot ${r.task_ref ?? ""}`.trim();
default:
return "";
}
}
// Rows print in the order the broker returned them (at, table, seq).
export function formatTrail(subject, rows) {
const out = [`trail ${subject}: ${rows.length} row(s)`];
for (const r of rows) out.push(`${r.at} ${r.table}#${r.seq} ${trailSummary(r)}`.trimEnd());
const decision = rows.find((r) => r.table === "decisions" && r.id === subject);
if (decision?.task_ref) out.push("", `task: ${decision.task_ref}`, `follow with: mosaic trail ${decision.task_ref}`);
return `${out.join("\n")}\n`;
}