huddle-cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/QUICKSTART.md +102 -0
- package/README.md +23 -4
- package/dist/adapters/amp.js +1 -1
- package/dist/adapters/antigravity.js +2 -0
- package/dist/adapters/claude.js +19 -3
- package/dist/adapters/codex.js +6 -1
- package/dist/adapters/cursor.js +12 -5
- package/dist/adapters/custom.js +4 -2
- package/dist/adapters/droid.js +1 -0
- package/dist/adapters/goose.js +2 -0
- package/dist/adapters/index.js +44 -18
- package/dist/adapters/kiro.js +1 -1
- package/dist/adapters/opencode.js +1 -1
- package/dist/attach/connect.js +4 -1
- package/dist/attach/guard.js +21 -2
- package/dist/attach/live.js +5 -1
- package/dist/attach/plug.js +14 -3
- package/dist/attach/screen.js +150 -0
- package/dist/attach/session.js +7 -2
- package/dist/cli.js +108 -40
- package/dist/mcp/context-tools.js +79 -0
- package/dist/mcp/server.js +38 -11
- package/dist/orchestrator/ask.js +13 -10
- package/dist/orchestrator/brain.js +13 -3
- package/dist/orchestrator/council.js +427 -86
- package/dist/orchestrator/goal.js +196 -0
- package/dist/orchestrator/handoff.js +2 -2
- package/dist/orchestrator/launcher.js +29 -13
- package/dist/orchestrator/learn.js +105 -0
- package/dist/orchestrator/planner.js +25 -7
- package/dist/orchestrator/qa.js +62 -14
- package/dist/orchestrator/radar.js +719 -0
- package/dist/orchestrator/scheduler.js +498 -39
- package/dist/orchestrator/takeover.js +155 -0
- package/dist/platform.js +273 -0
- package/dist/room/context.js +1285 -0
- package/dist/room/contract.js +132 -0
- package/dist/room/db.js +79 -16
- package/dist/room/followup.js +233 -0
- package/dist/room/identity.js +2 -2
- package/dist/room/lessons.js +275 -0
- package/dist/room/live.js +75 -20
- package/dist/room/mail.js +212 -0
- package/dist/room/progress.js +40 -1
- package/dist/room/server.js +169 -39
- package/dist/room/tunnel.js +6 -5
- package/dist/sandbox/sync.js +4 -4
- package/dist/sandbox/worktree.js +145 -14
- package/dist/ui/app.js +554 -133
- package/dist/ui/catalog.js +62 -24
- package/dist/ui/dashboard.js +194 -51
- package/dist/ui/doctor.js +15 -11
- package/dist/ui/lessons.js +81 -0
- package/dist/ui/paste.js +515 -0
- package/dist/ui/remote.js +11 -5
- package/dist/ui/setup.js +152 -0
- package/dist/ui/story.js +666 -69
- package/dist/ui/term.js +151 -16
- package/dist/ui/watch.js +145 -48
- package/package.json +6 -3
package/dist/ui/story.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// huddle story --html out.html a self-contained static report (lanes, threads)
|
|
14
14
|
import fs from "node:fs";
|
|
15
15
|
import path from "node:path";
|
|
16
|
-
import { C, dot, pad, AGENT_COLOR } from "./term.js";
|
|
16
|
+
import { C, dot, pad, truncate, AGENT_COLOR } from "./term.js";
|
|
17
17
|
import { fmtDuration, fmtTokens } from "../adapters/usage.js";
|
|
18
18
|
import { isHelpOffer } from "../room/progress.js";
|
|
19
19
|
/** The scheduler's own event types, persisted as "run.<type>" (scheduler.ts). */
|
|
@@ -118,7 +118,8 @@ export function splitRuns(raw) {
|
|
|
118
118
|
});
|
|
119
119
|
const chunks = starts.map((s, k) => evs.slice(s, k + 1 < starts.length ? starts[k + 1] : evs.length));
|
|
120
120
|
// The design talk happens before the board is cleared: move it forward.
|
|
121
|
-
|
|
121
|
+
// So is what the user put on the shared screen just before typing the goal.
|
|
122
|
+
const isDesign = (e) => e.type.startsWith("council.") || e.type === "context.added";
|
|
122
123
|
const before = evs.slice(0, starts[0]).filter(isDesign);
|
|
123
124
|
chunks[0] = [...before, ...chunks[0]];
|
|
124
125
|
for (let k = 1; k < chunks.length; k++) {
|
|
@@ -173,9 +174,23 @@ export function buildStory(run, runs = 1) {
|
|
|
173
174
|
const plan = { tasks: [] };
|
|
174
175
|
const open = new Map(); // agent -> its open span
|
|
175
176
|
const councilTasks = [];
|
|
177
|
+
const collab = { adopts: [], flaws: [], ratify: [], objections: [], amendments: [], radar: [], buddy: [], followups: [], bugs: [], pairs: [], credit: new Map() };
|
|
178
|
+
const mergedTitles = [];
|
|
179
|
+
const screen = new Map();
|
|
180
|
+
const onScreen = (p) => {
|
|
181
|
+
const id = pick(p, "id");
|
|
182
|
+
if (!screen.has(id))
|
|
183
|
+
screen.set(id, { id, kind: pick(p, "kind"), title: pick(p, "title"), by: "", size: "", readers: [], notes: [] });
|
|
184
|
+
const e = screen.get(id);
|
|
185
|
+
if (!e.title)
|
|
186
|
+
e.title = pick(p, "title");
|
|
187
|
+
if (!e.kind)
|
|
188
|
+
e.kind = pick(p, "kind");
|
|
189
|
+
return e;
|
|
190
|
+
};
|
|
176
191
|
let merged = 0;
|
|
177
192
|
let failed = 0;
|
|
178
|
-
// The brain that settles a council plan (a label like "claude ·
|
|
193
|
+
// The brain that settles a council plan (a label like "claude · opus") is not an agent on the crew.
|
|
179
194
|
const working = new Set(run.events.filter((e) => e.type === "run.start" || e.type === "agent.joined").map((e) => e.agent));
|
|
180
195
|
const brains = new Set(run.events.filter((e) => e.type === "council.plan").map((e) => pick(e.p, "by")).filter((b) => b && !working.has(b)));
|
|
181
196
|
const isAgent = (a) => !!a && !NOT_AGENTS.has(a) && !brains.has(a);
|
|
@@ -258,6 +273,35 @@ export function buildStory(run, runs = 1) {
|
|
|
258
273
|
case "plug.call":
|
|
259
274
|
st(e.agent).toolCalls++;
|
|
260
275
|
break;
|
|
276
|
+
case "context.added": {
|
|
277
|
+
const x = onScreen(p);
|
|
278
|
+
x.by = pick(p, "by") || "you";
|
|
279
|
+
x.size = Number(p.lines) ? `${p.lines} lines` : Number(p.bytes) ? `${Math.max(1, Math.round(Number(p.bytes) / 1024))} KB` : "";
|
|
280
|
+
add({ ts, kind: "team", type: e.type, agent: isAgent(x.by) ? x.by : null, text: `📎 ${isAgent(x.by) ? "" : x.by + " "}put ${x.id} ${q(x.title)} on the shared screen${x.size ? " (" + x.size + ")" : ""}` });
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
case "context.read": {
|
|
284
|
+
const x = onScreen(p);
|
|
285
|
+
const who = e.agent ?? pick(p, "by");
|
|
286
|
+
if (who && !x.readers.includes(who)) {
|
|
287
|
+
x.readers.push(who);
|
|
288
|
+
// First read of each item by each agent: the proof that they looked at the screen.
|
|
289
|
+
add({ ts, kind: "progress", type: e.type, agent: who, text: `📖 read ${x.id} ${q(x.title)}${p.from ? ` (lines ${p.from}-${p.to})` : ""}` });
|
|
290
|
+
}
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
case "context.note": {
|
|
294
|
+
const x = onScreen(p);
|
|
295
|
+
const who = pick(p, "by") || e.agent || "you";
|
|
296
|
+
x.notes.push({ by: who, body: pick(p, "body") });
|
|
297
|
+
add({ ts, kind: "message", type: e.type, agent: isAgent(who) ? who : null, text: `📌 on ${x.id} ${q(x.title)}: ${oneLine(pick(p, "body"), 300)}${isAgent(who) ? "" : ` (${who})`}` });
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
case "context.removed":
|
|
301
|
+
onScreen(p).removed = true;
|
|
302
|
+
break;
|
|
303
|
+
case "context.search":
|
|
304
|
+
break;
|
|
261
305
|
case "plan.made":
|
|
262
306
|
plan.by = pick(p, "by") || plan.by;
|
|
263
307
|
plan.note = pick(p, "note") || plan.note;
|
|
@@ -358,6 +402,18 @@ export function buildStory(run, runs = 1) {
|
|
|
358
402
|
// Huddle's automatic help offer, sent in a free agent's name: a board event,
|
|
359
403
|
// not a message the agent wrote. Found live (capstone round 2): 6 offers
|
|
360
404
|
// and no real message, yet the table said claude sent 3 messages.
|
|
405
|
+
// The radar's mail: the run.radar item already tells it, once per finding.
|
|
406
|
+
if (from === "huddle" && /^Huddle radar:/.test(pick(p, "body")))
|
|
407
|
+
break;
|
|
408
|
+
// Buddy notes and contract broadcasts: run.checkpoint / contract.amended tell them once.
|
|
409
|
+
if (from === "huddle" && /^buddy check from /.test(pick(p, "body")))
|
|
410
|
+
break;
|
|
411
|
+
if (/^contract v\d+ by /.test(pick(p, "body")))
|
|
412
|
+
break;
|
|
413
|
+
if (from === "huddle" && / is taking “.*” \(files .*\) in \d+s unless you reply/.test(pick(p, "body"))) {
|
|
414
|
+
add({ ts, kind: "takeover", type: "pair.proposed", agent: null, to, text: `proposes: ${oneLine(pick(p, "body").split(" unless you reply")[0], 200)} (${to} may say no)` });
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
361
417
|
if (isHelpOffer(from, pick(p, "body"))) {
|
|
362
418
|
const t = /your task "(.*)" \(/.exec(pick(p, "body"))?.[1];
|
|
363
419
|
add({ ts, kind: "task", type: "offer", agent: from, to, text: `is free: Huddle offers ${to} help${t ? ` with ${q(t)}` : ""} (huddle_split to hand off a part)` });
|
|
@@ -486,6 +542,7 @@ export function buildStory(run, runs = 1) {
|
|
|
486
542
|
merged++;
|
|
487
543
|
if (/^Fix: /.test(title(e.task, p)))
|
|
488
544
|
s.bugsFixed++; // a QA bug's fix task landed
|
|
545
|
+
mergedTitles.push({ title: title(e.task, p), agent: a });
|
|
489
546
|
close(a, ts, "ok");
|
|
490
547
|
add({ ts, kind: "merge", type: e.type, agent: a, text: `✓ merged ${q(title(e.task, p))}`, detail: [pick(p, "reason"), p.ms ? fmtDuration(Number(p.ms)) : "", p.tokens ? fmtTokens(Number(p.tokens)) + " tok" : ""].filter(Boolean).join(" · ") });
|
|
491
548
|
break;
|
|
@@ -530,7 +587,11 @@ export function buildStory(run, runs = 1) {
|
|
|
530
587
|
const a = pick(p, "agent", "from", "by", "critic") || e.agent;
|
|
531
588
|
const of = pick(p, "of", "target", "to", "author");
|
|
532
589
|
st(a).critiques++;
|
|
533
|
-
|
|
590
|
+
// Council v2: the ADOPT / FLAW lines get their own items (council.adopt / .flaw); here, just where it landed.
|
|
591
|
+
const revised = Array.isArray(p.adopts) || Array.isArray(p.flaws) ? /REVISED\W*:?\s*([\s\S]*)$/i.exec(pick(p, "text"))?.[1] : undefined;
|
|
592
|
+
const it = revised
|
|
593
|
+
? { ts, kind: "design", type: e.type, agent: a, to: null, text: `revised position: ${oneLine(revised, 300)}` }
|
|
594
|
+
: { ts, kind: "design", type: e.type, agent: a, to: of || null, text: `critiques ${of ? of + "'s proposal" : "the proposals"}: ${oneLine(pick(p, "body", "critique", "text", "summary", "flaws"), 300)}` };
|
|
534
595
|
add(it);
|
|
535
596
|
talked(a, of);
|
|
536
597
|
thread(it);
|
|
@@ -541,7 +602,19 @@ export function buildStory(run, runs = 1) {
|
|
|
541
602
|
plan.by = plan.by ?? (by ? `${by} (council)` : "council");
|
|
542
603
|
const tasks = Array.isArray(p.tasks) ? p.tasks : [];
|
|
543
604
|
councilTasks.push(...tasks);
|
|
544
|
-
|
|
605
|
+
const sy = p.synthesizer && typeof p.synthesizer === "object" ? p.synthesizer : null;
|
|
606
|
+
if (sy)
|
|
607
|
+
collab.synthesizer = { id: str(sy.id), kind: str(sy.kind), why: str(sy.why) };
|
|
608
|
+
for (const o of Array.isArray(p.objections) ? p.objections : [])
|
|
609
|
+
collab.objections.push({ by: str(o?.by), objection: str(o?.objection), taken: o?.taken === true });
|
|
610
|
+
add({
|
|
611
|
+
ts,
|
|
612
|
+
kind: "design",
|
|
613
|
+
type: e.type,
|
|
614
|
+
agent: by,
|
|
615
|
+
text: `writes the plan${tasks.length || p.tasks ? `: ${tasks.length || str(p.tasks)} task(s)` : ""}${sy?.why ? ` · ${sy.kind === "agent" ? "chosen" : "brain"}: ${oneLine(str(sy.why), 160)}` : ""}${pick(p, "summary", "note") ? " · " + oneLine(pick(p, "summary", "note"), 200) : ""}`,
|
|
616
|
+
detail: collab.objections.length ? collab.objections.map((o) => `${o.taken ? "✓ took" : "✗ kept as is"} ${o.by}'s objection: ${oneLine(o.objection, 120)}`).join("\n") : undefined,
|
|
617
|
+
});
|
|
545
618
|
break;
|
|
546
619
|
}
|
|
547
620
|
case "task.takeover": {
|
|
@@ -564,6 +637,19 @@ export function buildStory(run, runs = 1) {
|
|
|
564
637
|
break;
|
|
565
638
|
case "offer": // the help offer itself is an agent.message in the busy agent's inbox
|
|
566
639
|
break;
|
|
640
|
+
case "radar.finding": // the Room's copy of run.radar
|
|
641
|
+
break;
|
|
642
|
+
case "radar.stats": {
|
|
643
|
+
const prev = collab.radarLooks ?? { ticks: 0, looks: 0, pairs: 0 };
|
|
644
|
+
collab.radarLooks = { ticks: prev.ticks + (Number(p.ticks) || 0), looks: prev.looks + (Number(p.looks) || 0), pairs: prev.pairs + (Number(p.pairs) || 0) };
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
case "radar": {
|
|
648
|
+
const w = pick(p, "with");
|
|
649
|
+
collab.radar.push({ agent: e.agent ?? pick(p, "agent"), with: w, kind: pick(p, "kind"), detail: pick(p, "detail") });
|
|
650
|
+
add({ ts, kind: "conflict", type: e.type, agent: e.agent, to: w && w !== "contract" ? w : null, text: `⚠ radar: ${pick(p, "kind") === "overlap" ? "overlap" : "name mismatch"}${w ? ` with ${w}` : ""} · told mid-task`, detail: oneLine(pick(p, "detail"), 300) || undefined });
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
567
653
|
case "takeover": {
|
|
568
654
|
if (roomTakeovers) {
|
|
569
655
|
// The first agent's time and tokens on the task it handed over.
|
|
@@ -591,6 +677,95 @@ export function buildStory(run, runs = 1) {
|
|
|
591
677
|
add({ ts, kind: "takeover", type: e.type, agent: from, text: `✂ splits ${q(title(e.task, p))} into ${names.length || "?"} part(s)${to.length ? " → " + to.join(", ") : ""}`, detail: names.join(" · ") || oneLine(pick(p, "reason"), 200) || undefined });
|
|
592
678
|
break;
|
|
593
679
|
}
|
|
680
|
+
case "checkpoint": {
|
|
681
|
+
// A buddy check: another agent read the author's work in progress.
|
|
682
|
+
const a = pick(p, "agent") || e.agent;
|
|
683
|
+
const author = pick(p, "author");
|
|
684
|
+
const notes = pick(p, "notes");
|
|
685
|
+
const ok = !notes || /^LOOKS GOOD/i.test(notes.trim());
|
|
686
|
+
const late = !ok && !!p.late;
|
|
687
|
+
collab.buddy.push({ agent: a ?? "", author, pct: Number(p.pct) || 0, notes: ok ? "" : notes, ...(late ? { late } : {}) });
|
|
688
|
+
const it = { ts, kind: "review", type: e.type, agent: a, to: author || null, text: `◎ buddy check on ${author}'s ${q(title(e.task, p))} ${Number(p.pct) ? `at ${str(p.pct)}%` : "mid-task"}: ${ok ? "looks good" : late ? "notes came back after it landed (kept in shared memory)" : "notes sent mid-task"}`, detail: ok ? undefined : oneLine(notes, 400) };
|
|
689
|
+
add(it);
|
|
690
|
+
if (!ok) {
|
|
691
|
+
talked(a, author);
|
|
692
|
+
thread(it);
|
|
693
|
+
}
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
// A note that needed an answer, settled when its task finished (src/room/followup.ts).
|
|
697
|
+
case "note.closed": {
|
|
698
|
+
const to = pick(p, "to") || e.agent || "";
|
|
699
|
+
const by = pick(p, "by");
|
|
700
|
+
const how = pick(p, "how") || "ignored";
|
|
701
|
+
const kind = pick(p, "kind") || "buddy";
|
|
702
|
+
collab.followups.push({ to, by, kind, how, note: pick(p, "body"), ...(title(e.task) ? { task: title(e.task) } : {}) });
|
|
703
|
+
const what = kind === "radar" ? "the radar's finding" : `${by}'s buddy note`;
|
|
704
|
+
const verb = how === "replied" ? "answers" : "addresses (in code)";
|
|
705
|
+
add({
|
|
706
|
+
ts,
|
|
707
|
+
kind: how === "ignored" ? "fail" : "review",
|
|
708
|
+
type: e.type,
|
|
709
|
+
agent: to || null,
|
|
710
|
+
to: kind === "radar" ? null : by || null,
|
|
711
|
+
text: how === "ignored" ? `${kind === "radar" ? "radar finding" : "buddy note"} ignored: ${kind === "radar" ? "" : `${by}'s note `}on ${q(title(e.task))} got no answer` : `${verb} ${what} on ${q(title(e.task))}`,
|
|
712
|
+
detail: oneLine(pick(p, "body"), 300) || undefined,
|
|
713
|
+
});
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
case "followup": // the scheduler's copy of note.closed
|
|
717
|
+
case "note.open": // the note itself is an agent.message; run.checkpoint / run.radar tell it
|
|
718
|
+
case "buddy.start":
|
|
719
|
+
case "buddy.end":
|
|
720
|
+
break;
|
|
721
|
+
case "done.refused": {
|
|
722
|
+
const a = e.agent;
|
|
723
|
+
add({ ts, kind: "review", type: e.type, agent: a, text: `huddle_done refused once: ${list(p.notes).length || "some"} teammate note(s) on ${q(title(e.task))} had no answer yet` });
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
case "pair.dropped": {
|
|
727
|
+
const w = pick(p, "with");
|
|
728
|
+
add({ ts, kind: "takeover", type: e.type, agent: e.agent, to: w || null, text: `⇉ offered to pair with ${w} on ${q(title(e.task, p))} · dropped`, detail: oneLine(pick(p, "reason"), 200) || undefined });
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
case "pair": {
|
|
732
|
+
const a = pick(p, "agent") || e.agent;
|
|
733
|
+
const w = pick(p, "with");
|
|
734
|
+
collab.pairs.push({ agent: a ?? "", with: w, part: pick(p, "part") });
|
|
735
|
+
add({ ts, kind: "takeover", type: e.type, agent: a, to: w || null, text: `⇉ pairs with ${w} on ${q(title(e.task, p))}: takes ${oneLine(pick(p, "part"), 160)}`, detail: oneLine(pick(p, "reason"), 200) || undefined });
|
|
736
|
+
talked(a, w);
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
739
|
+
case "council.adopt":
|
|
740
|
+
case "council.flaw": {
|
|
741
|
+
// Cross-review: what one agent took from (or found wrong in) another's proposal.
|
|
742
|
+
const a = pick(p, "agent") || e.agent;
|
|
743
|
+
const of = pick(p, "of");
|
|
744
|
+
const it = { ts, kind: "design", type: e.type, agent: a, to: of || null, text: `${e.type === "council.adopt" ? "adopts" : "finds a flaw in"} ${of}'s idea: ${oneLine(pick(p, "body", "text"), 300)}` };
|
|
745
|
+
add(it);
|
|
746
|
+
talked(a, of);
|
|
747
|
+
(e.type === "council.adopt" ? collab.adopts : collab.flaws).push({ by: a ?? "", of, text: pick(p, "body", "text") });
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
case "council.ratify": {
|
|
751
|
+
const a = pick(p, "agent") || e.agent;
|
|
752
|
+
const obj = list(p.objections);
|
|
753
|
+
collab.ratify.push({ agent: a ?? "", verdict: pick(p, "verdict") || (obj.length ? "object" : "ok"), objections: obj });
|
|
754
|
+
add({ ts, kind: "design", type: e.type, agent: a, text: obj.length ? `objects to the contract: ${oneLine(obj.join(" · "), 300)}` : `ratifies the contract${pick(p, "verdict") === "unclear" ? " (no clear verdict)" : ""}` });
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
case "contract.set":
|
|
758
|
+
if (pick(p, "text"))
|
|
759
|
+
collab.contract = { by: pick(p, "by") || e.agent || "council", chars: str(p.text).length };
|
|
760
|
+
if (pick(p, "text"))
|
|
761
|
+
add({ ts, kind: "design", type: e.type, agent: pick(p, "by") || e.agent, text: `writes the contract (${str(p.text).length} chars): the names every task codes to` });
|
|
762
|
+
break;
|
|
763
|
+
case "contract.amended":
|
|
764
|
+
collab.amendments.push({ by: pick(p, "by") || e.agent || "?", version: Number(p.version) || 0, change: `${pick(p, "section")}: ${pick(p, "change")}` });
|
|
765
|
+
add({ ts, kind: "other", type: e.type, agent: pick(p, "by") || e.agent, text: `amends the contract (v${str(p.version)}): ${oneLine(`${pick(p, "section")}: ${pick(p, "change")}`, 200)}`, detail: oneLine(pick(p, "why"), 200) || undefined });
|
|
766
|
+
break;
|
|
767
|
+
case "run.contract": // the Room's contract.amended says the same
|
|
768
|
+
break;
|
|
594
769
|
case "council.skipped": {
|
|
595
770
|
const a = pick(p, "agent") || e.agent;
|
|
596
771
|
add({ ts, kind: "design", type: e.type, agent: a, text: `${pick(p, "round") || "council"} skipped (${pick(p, "reason") || "no answer"})` });
|
|
@@ -616,7 +791,7 @@ export function buildStory(run, runs = 1) {
|
|
|
616
791
|
const a = pick(p, "agent", "by", "checker") || list(p.agents)[0] || e.agent;
|
|
617
792
|
if (isAgent(a))
|
|
618
793
|
begin({ agent: a, from: ts, kind: "qa", label: "QA" });
|
|
619
|
-
add({ ts, kind: "qa", type: e.type, agent: a, text:
|
|
794
|
+
add({ ts, kind: "qa", type: e.type, agent: a, text: `◎ QA round${p.round !== undefined ? " " + str(p.round) : ""} on the integrated main${list(p.agents).length ? ` · checked by ${list(p.agents).join(", ")} ${qaEyes(list(p.agents), p.authors)}` : ""}`, detail: oneLine(pick(p, "summary", "checks", "note"), 200) || undefined });
|
|
620
795
|
break;
|
|
621
796
|
}
|
|
622
797
|
case "qa.bug": {
|
|
@@ -625,10 +800,14 @@ export function buildStory(run, runs = 1) {
|
|
|
625
800
|
if (isAgent(a))
|
|
626
801
|
close(a, ts, "warn");
|
|
627
802
|
const owner2 = pick(p, "to", "owner", "assigned_to", "author");
|
|
628
|
-
|
|
803
|
+
collab.bugs.push({ by: a ?? "", to: owner2, title: pick(p, "title", "summary", "bug", "body") });
|
|
804
|
+
add({ ts, kind: "qa", type: e.type, agent: a, to: owner2 || null, text: `✗ QA found: ${oneLine(pick(p, "title", "summary", "bug", "body"), 200)}${owner2 ? ` → fix by ${owner2}` : ""}`, detail: oneLine(pick(p, "details", "repro", "spec", "body"), 300) || undefined });
|
|
629
805
|
talked(a, owner2);
|
|
630
806
|
break;
|
|
631
807
|
}
|
|
808
|
+
case "qa.suspect":
|
|
809
|
+
add({ ts, kind: "qa", type: e.type, agent: null, text: `? QA round ${str(p.round)} clean, but quick · ${str(p.reason)}` });
|
|
810
|
+
break;
|
|
632
811
|
case "qa.unchecked":
|
|
633
812
|
add({ ts, kind: "qa", type: e.type, agent: null, text: `! QA round ${str(p.round)}: the build was NOT checked · ${str(p.reason) || "no check finished"}` });
|
|
634
813
|
break;
|
|
@@ -643,7 +822,10 @@ export function buildStory(run, runs = 1) {
|
|
|
643
822
|
case "qa.fix": {
|
|
644
823
|
const a = pick(p, "agent", "by", "fixed_by") || e.agent;
|
|
645
824
|
st(a).bugsFixed++;
|
|
646
|
-
|
|
825
|
+
const fixed = collab.bugs.find((b) => !b.fixedBy && b.title === (pick(p, "title", "summary", "bug", "body") || title(e.task)));
|
|
826
|
+
if (fixed && a)
|
|
827
|
+
fixed.fixedBy = a;
|
|
828
|
+
add({ ts, kind: "qa", type: e.type, agent: a, text: `✓ fixed: ${oneLine(pick(p, "title", "summary", "bug", "body") || title(e.task), 200)}` });
|
|
647
829
|
break;
|
|
648
830
|
}
|
|
649
831
|
default: {
|
|
@@ -663,14 +845,169 @@ export function buildStory(run, runs = 1) {
|
|
|
663
845
|
}
|
|
664
846
|
for (const a of [...open.keys()])
|
|
665
847
|
close(a, run.end);
|
|
848
|
+
// A QA bug is fixed when its "Fix: <title>" task merged (the title may be cut on the board).
|
|
849
|
+
for (const b of collab.bugs) {
|
|
850
|
+
const want = oneLine(b.title, 60).replace(/…$/, "");
|
|
851
|
+
const hit = mergedTitles.find((m) => m.title.startsWith("Fix: ") && m.title.slice(5).startsWith(want.slice(0, 40)));
|
|
852
|
+
if (hit)
|
|
853
|
+
b.fixedBy = hit.agent;
|
|
854
|
+
}
|
|
666
855
|
// Who proposed what: a council plan's tasks may say whose idea each one was.
|
|
667
856
|
for (const t of councilTasks) {
|
|
668
857
|
const pt = plan.tasks.find((x) => x.title === pick(t ?? {}, "title"));
|
|
669
|
-
const
|
|
858
|
+
const from = list((t ?? {}).proposed_by ?? (t ?? {}).by ?? (t ?? {}).from ?? (t ?? {}).author);
|
|
859
|
+
const primary = pick(t ?? {}, "primary") || from[0] || "";
|
|
860
|
+
if (primary)
|
|
861
|
+
collab.credit.set(primary, (collab.credit.get(primary) ?? 0) + 1);
|
|
862
|
+
const rest = from.filter((x) => x !== primary);
|
|
863
|
+
const who = primary ? `${primary}${rest.length ? ` (with ${rest.join(", ")})` : ""}` : "";
|
|
670
864
|
if (pt && who && !pt.by)
|
|
671
865
|
pt.by = who;
|
|
672
866
|
}
|
|
673
|
-
return { run, runs, agents, plan, design, items, spans, stats, talk, threads, merged, failed };
|
|
867
|
+
return { run, runs, agents, plan, design, items, spans, stats, talk, threads, screen: [...screen.values()], merged, failed, collab };
|
|
868
|
+
}
|
|
869
|
+
// ---- teamwork summary (terminal + HTML) -------------------------------------------
|
|
870
|
+
/** The run's collaboration as labelled lines; [] when nobody worked with anybody. */
|
|
871
|
+
export function collabLines(s) {
|
|
872
|
+
const c = s.collab;
|
|
873
|
+
const out = [];
|
|
874
|
+
const n = (k, one, many = one + "s") => `${k} ${k === 1 ? one : many}`;
|
|
875
|
+
for (const a of c.adopts.slice(0, 6))
|
|
876
|
+
out.push({ label: "adopted", text: `${a.by} took ${a.of}'s idea: ${oneLine(a.text, 110)}` });
|
|
877
|
+
if (c.adopts.length > 6)
|
|
878
|
+
out.push({ label: "adopted", text: `…and ${c.adopts.length - 6} more` });
|
|
879
|
+
for (const f of c.flaws.slice(0, 6))
|
|
880
|
+
out.push({ label: "flaw", text: `${f.by} on ${f.of}'s idea: ${oneLine(f.text, 110)}` });
|
|
881
|
+
if (c.flaws.length > 6)
|
|
882
|
+
out.push({ label: "flaw", text: `…and ${c.flaws.length - 6} more` });
|
|
883
|
+
if (c.synthesizer)
|
|
884
|
+
out.push({ label: "plan by", text: `${c.synthesizer.id}${c.synthesizer.why ? ` — ${oneLine(c.synthesizer.why, 110)}` : ""}` });
|
|
885
|
+
if (c.ratify.length) {
|
|
886
|
+
const ok = c.ratify.filter((r) => r.verdict !== "object").map((r) => r.agent);
|
|
887
|
+
const obj = c.ratify.filter((r) => r.verdict === "object");
|
|
888
|
+
out.push({ label: "ratify", text: [ok.length ? `${ok.join(", ")} ok` : "", ...obj.map((r) => `${r.agent} objects: ${oneLine(r.objections.join(" · "), 90)}`)].filter(Boolean).join(" · ") });
|
|
889
|
+
}
|
|
890
|
+
for (const o of c.objections.slice(0, 4))
|
|
891
|
+
out.push({ label: "revision", text: `${o.taken ? "took" : "kept the plan over"} ${o.by}'s objection: ${oneLine(o.objection, 100)}` });
|
|
892
|
+
if (c.contract || c.amendments.length) {
|
|
893
|
+
const v = c.amendments.length ? Math.max(...c.amendments.map((a) => a.version)) : 1;
|
|
894
|
+
out.push({ label: "contract", text: `v${v}${c.contract ? ` · written by ${c.contract.by}` : " · started mid-run"} · ${n(c.amendments.length, "amendment")}` });
|
|
895
|
+
for (const a of c.amendments.slice(-4))
|
|
896
|
+
out.push({ label: "", text: `v${a.version} ${a.by}: ${oneLine(a.change, 110)}` });
|
|
897
|
+
}
|
|
898
|
+
if (!c.radar.length && c.radarLooks?.looks)
|
|
899
|
+
out.push({ label: "radar", text: `no catches · compared 2+ agents' work in progress ${n(c.radarLooks.looks, "time")} (${n(c.radarLooks.pairs, "pair")})` });
|
|
900
|
+
if (c.radar.length) {
|
|
901
|
+
out.push({ label: "radar", text: `${n(c.radar.length, "catch", "catches")} while agents worked` });
|
|
902
|
+
for (const r of c.radar.slice(0, 4))
|
|
903
|
+
out.push({ label: "", text: `${r.kind === "overlap" ? "overlap" : "mismatch"} ${r.agent} ↔ ${r.with}: ${oneLine(r.detail, 100)}` });
|
|
904
|
+
}
|
|
905
|
+
if (c.buddy.length) {
|
|
906
|
+
const sent = c.buddy.filter((b) => b.notes);
|
|
907
|
+
const late = sent.filter((b) => b.late).length;
|
|
908
|
+
out.push({ label: "buddy", text: `${n(c.buddy.length, "check")} mid-task · ${n(sent.length, "note")} sent${late ? ` (${late} came back after the task landed)` : ""}` });
|
|
909
|
+
for (const b of sent.slice(0, 4))
|
|
910
|
+
out.push({ label: "", text: `${b.agent} → ${b.author} at ${b.pct}%: ${oneLine(b.notes, 100)}` });
|
|
911
|
+
}
|
|
912
|
+
if (c.followups.length) {
|
|
913
|
+
const k = (h) => c.followups.filter((f) => f.how === h).length;
|
|
914
|
+
const ignored = c.followups.filter((f) => f.how === "ignored");
|
|
915
|
+
out.push({ label: "answered", text: `${n(c.followups.length - ignored.length, "note")} addressed (${k("fixed")} fixed, ${k("replied")} replied) · ${ignored.length} ignored` });
|
|
916
|
+
for (const f of ignored.slice(0, 4))
|
|
917
|
+
out.push({ label: "", text: `ignored by ${f.to}: ${f.kind === "radar" ? "radar" : f.by}: ${oneLine(f.note.replace(/^buddy check from [^:]*:\s*/i, ""), 100)}` });
|
|
918
|
+
}
|
|
919
|
+
for (const p of c.pairs.slice(0, 4))
|
|
920
|
+
out.push({ label: "pair", text: `${p.agent} helped ${p.with}: ${oneLine(p.part, 110)}` });
|
|
921
|
+
if (c.credit.size && (c.adopts.length || c.credit.size > 1))
|
|
922
|
+
out.push({ label: "credit", text: [...c.credit.entries()].sort((a, b) => b[1] - a[1]).map(([a, k]) => `${a} ${n(k, "task")}`).join(" · ") });
|
|
923
|
+
return out;
|
|
924
|
+
}
|
|
925
|
+
function crewRows(s) {
|
|
926
|
+
const done = s.items.filter((it) => it.type === "qa.done");
|
|
927
|
+
return s.agents.map((a) => {
|
|
928
|
+
const x = s.stats.get(a);
|
|
929
|
+
const qa = done.length ? done.filter((it) => it.agent === a).length : s.items.filter((it) => it.type === "qa.round" && it.agent === a).length;
|
|
930
|
+
return { agent: a, merged: x.merged, failed: x.failed, reviews: x.reviews, qa, ms: x.ms, tokens: x.tokens, cached: x.cached };
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
const plural = (k, one, many = one + "s") => `${k} ${k === 1 ? one : many}`;
|
|
934
|
+
/** "5 merged · 3 reviews · 1 QA" — what an agent did, nothing for what it didn't. */
|
|
935
|
+
function didLine(c) {
|
|
936
|
+
const parts = [`${c.merged} merged`, c.failed ? `${c.failed} failed` : "", c.reviews ? plural(c.reviews, "review") : "", c.qa ? `${c.qa} QA` : ""];
|
|
937
|
+
return parts.filter(Boolean).join(" · ");
|
|
938
|
+
}
|
|
939
|
+
/** The run cut into its phases: the build, then QA (from the first QA round on). */
|
|
940
|
+
function phases(s) {
|
|
941
|
+
// The council's decisions and risks, written to shared memory as the build starts: part of the plan.
|
|
942
|
+
const agreed = s.items.filter((it) => it.agent === "council" && it.type === "memory.added");
|
|
943
|
+
const rest = s.items.filter((it) => !agreed.includes(it));
|
|
944
|
+
const i = rest.findIndex((it) => it.type === "qa.round");
|
|
945
|
+
return i < 0 ? { agreed, build: rest, qa: [] } : { agreed, build: rest.slice(0, i), qa: rest.slice(i) };
|
|
946
|
+
}
|
|
947
|
+
/** A council decision or risk from shared memory, as "decided" / "risk (by)". */
|
|
948
|
+
function agreement(it) {
|
|
949
|
+
const risk = /^gotcha: council risk:/.test(it.text);
|
|
950
|
+
return { risk, by: risk ? /flagged by (\S+)/.exec(it.detail ?? "")?.[1] : undefined, body: it.text.replace(/^\w+: council( risk)?:\s*/, "") };
|
|
951
|
+
}
|
|
952
|
+
/** The council is recorded in one go when it's done: its items share one instant, so they get no clock. */
|
|
953
|
+
const councilInstant = (s) => s.design.length > 1 && s.design.at(-1).ts - s.design[0].ts < 2000;
|
|
954
|
+
/** How the run ended: totals, merges gated on tests, QA's last word. */
|
|
955
|
+
function outcome(s) {
|
|
956
|
+
const rows = crewRows(s);
|
|
957
|
+
const tokens = rows.reduce((n, c) => n + c.tokens, 0);
|
|
958
|
+
const cached = rows.reduce((n, c) => n + c.cached, 0);
|
|
959
|
+
const merges = s.items.filter((it) => it.type === "run.merged");
|
|
960
|
+
const green = merges.filter((it) => /tests? (green|pass)/i.test(it.detail ?? "")).length;
|
|
961
|
+
const last = [...s.items].reverse().find((it) => ["qa.clean", "qa.capped", "qa.unchecked"].includes(it.type));
|
|
962
|
+
const qa = last ? { text: last.text.replace(/^[✓!]\s*/, ""), tone: last.type === "qa.clean" ? "ok" : "warn" } : null;
|
|
963
|
+
const rounds = s.items.filter((it) => it.type === "qa.round").length;
|
|
964
|
+
return { tokens, cached, merges: merges.length, green, qa, rounds, ms: s.run.end - s.run.start };
|
|
965
|
+
}
|
|
966
|
+
/** The teamwork as numbers, for the summary line and the HTML tiles. Only what happened (or was looked for). */
|
|
967
|
+
function tally(s) {
|
|
968
|
+
const c = s.collab;
|
|
969
|
+
const out = [];
|
|
970
|
+
if (c.adopts.length || c.flaws.length) {
|
|
971
|
+
out.push({ n: String(c.adopts.length), label: c.adopts.length === 1 ? "idea adopted" : "ideas adopted", tone: "ok" });
|
|
972
|
+
out.push({ n: String(c.flaws.length), label: c.flaws.length === 1 ? "flaw flagged" : "flaws flagged" });
|
|
973
|
+
}
|
|
974
|
+
if (c.objections.length)
|
|
975
|
+
out.push({ n: `${c.objections.filter((o) => o.taken).length}/${c.objections.length}`, label: "objections taken", tone: "ok" });
|
|
976
|
+
if (c.contract || c.amendments.length) {
|
|
977
|
+
const v = c.amendments.length ? Math.max(...c.amendments.map((a) => a.version)) : 1;
|
|
978
|
+
out.push({ n: `v${v}`, label: `contract, ${plural(c.amendments.length, "amendment")}`, text: `contract v${v} (${plural(c.amendments.length, "amendment")})` });
|
|
979
|
+
}
|
|
980
|
+
if (c.buddy.length)
|
|
981
|
+
out.push({ n: String(c.buddy.length), label: `buddy checks, ${plural(c.buddy.filter((b) => b.notes).length, "note")}` });
|
|
982
|
+
if (c.followups.length) {
|
|
983
|
+
const ignored = c.followups.filter((f) => f.how === "ignored").length;
|
|
984
|
+
out.push({ n: `${c.followups.length - ignored}/${c.followups.length}`, label: "notes answered", tone: ignored ? "warn" : "ok" });
|
|
985
|
+
}
|
|
986
|
+
if (c.radar.length || c.radarLooks?.looks)
|
|
987
|
+
out.push({ n: String(c.radar.length), label: c.radar.length === 1 ? "radar catch" : "radar catches", tone: c.radar.length ? "warn" : undefined });
|
|
988
|
+
if (c.pairs.length)
|
|
989
|
+
out.push({ n: String(c.pairs.length), label: c.pairs.length === 1 ? "pair" : "pairs" });
|
|
990
|
+
if (c.bugs.length) {
|
|
991
|
+
const fixed = c.bugs.filter((b) => b.fixedBy).length;
|
|
992
|
+
out.push({ n: `${c.bugs.length}→${fixed}`, label: "QA bugs → fixed", tone: fixed === c.bugs.length ? "ok" : "warn", text: `${plural(c.bugs.length, "QA bug")}, ${fixed} fixed` });
|
|
993
|
+
}
|
|
994
|
+
const msgs = [...s.stats.values()].reduce((n, x) => n + x.sent, 0);
|
|
995
|
+
if (msgs)
|
|
996
|
+
out.push({ n: String(msgs), label: msgs === 1 ? "message between agents" : "messages between agents" });
|
|
997
|
+
return out;
|
|
998
|
+
}
|
|
999
|
+
/** Each buddy note with how it ended: fixed in code, answered, ignored — or just sent. */
|
|
1000
|
+
function buddyNotes(s) {
|
|
1001
|
+
const left = s.collab.followups.filter((f) => f.kind !== "radar");
|
|
1002
|
+
return s.collab.buddy
|
|
1003
|
+
.filter((b) => b.notes)
|
|
1004
|
+
.map((b) => {
|
|
1005
|
+
const i = left.findIndex((f) => f.by === b.agent && f.to === b.author);
|
|
1006
|
+
const f = i >= 0 ? left.splice(i, 1)[0] : undefined;
|
|
1007
|
+
const how = f ? (f.how === "ignored" ? "ignored" : f.how === "replied" ? "answered" : "fixed in code") : b.late ? "came back late" : "sent";
|
|
1008
|
+
const tone = f ? (f.how === "ignored" ? "bad" : "ok") : b.late ? "warn" : "";
|
|
1009
|
+
return { ...b, how, tone };
|
|
1010
|
+
});
|
|
674
1011
|
}
|
|
675
1012
|
// ---- terminal -----------------------------------------------------------------
|
|
676
1013
|
const clock = (ms) => {
|
|
@@ -678,13 +1015,13 @@ const clock = (ms) => {
|
|
|
678
1015
|
return `+${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
|
|
679
1016
|
};
|
|
680
1017
|
const kindMark = {
|
|
681
|
-
design:
|
|
1018
|
+
design: (s) => s,
|
|
682
1019
|
plan: C.violet,
|
|
683
1020
|
task: (s) => s,
|
|
684
1021
|
progress: C.faint,
|
|
685
1022
|
announce: C.faint,
|
|
686
1023
|
message: C.bold,
|
|
687
|
-
memory: C.
|
|
1024
|
+
memory: C.faint,
|
|
688
1025
|
handoff: C.violet,
|
|
689
1026
|
review: C.violet,
|
|
690
1027
|
merge: C.green,
|
|
@@ -695,24 +1032,83 @@ const kindMark = {
|
|
|
695
1032
|
team: C.faint,
|
|
696
1033
|
other: C.faint,
|
|
697
1034
|
};
|
|
698
|
-
|
|
1035
|
+
/** Parts joined with " · ", wrapped onto as many lines as `width` needs. */
|
|
1036
|
+
function wrapParts(parts, width, indent = " ") {
|
|
1037
|
+
const out = [];
|
|
1038
|
+
let cur = "";
|
|
1039
|
+
for (const p of parts) {
|
|
1040
|
+
const next = cur ? `${cur} · ${p}` : p;
|
|
1041
|
+
if (cur && indent.length + next.length > width) {
|
|
1042
|
+
out.push(indent + cur);
|
|
1043
|
+
cur = p;
|
|
1044
|
+
}
|
|
1045
|
+
else
|
|
1046
|
+
cur = next;
|
|
1047
|
+
}
|
|
1048
|
+
if (cur)
|
|
1049
|
+
out.push(indent + cur);
|
|
1050
|
+
return out;
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* The story in the terminal, in the live run's words: COUNCIL → PLAN → BUILD →
|
|
1054
|
+
* QA → RESULT. `width` (the terminal's columns) cuts every line to fit; without
|
|
1055
|
+
* it (a file, a pipe) lines are kept whole.
|
|
1056
|
+
*/
|
|
1057
|
+
export function renderText(s, opts = {}) {
|
|
699
1058
|
const out = [];
|
|
700
1059
|
const r = s.run;
|
|
1060
|
+
const W = opts.width ?? 120;
|
|
1061
|
+
const nameW = Math.max(6, ...s.agents.map((a) => a.length));
|
|
701
1062
|
const who = (a) => (a ? `${dot(a)} ${C.bold(a)}` : `${C.violet("◆")} ${C.faint("huddle")}`);
|
|
1063
|
+
const whoPad = (a) => (a ? `${dot(a)} ${C.bold(pad(a, nameW))}` : `${C.violet("◆")} ${C.faint(pad("huddle", nameW))}`);
|
|
1064
|
+
const indent = " ".repeat(2 + 6 + 1 + 2 + nameW + 1);
|
|
1065
|
+
const instant = councilInstant(s);
|
|
702
1066
|
const line = (it) => {
|
|
703
|
-
const head = it.to && it.kind === "message" ? `${who(it.agent)} ${C.faint("→")} ${who(it.to)}${C.faint(":")}` :
|
|
1067
|
+
const head = it.to && it.kind === "message" ? `${who(it.agent)} ${C.faint("→")} ${who(it.to)}${C.faint(":")}` : whoPad(it.agent);
|
|
704
1068
|
const tag = it.kind === "message" && it.detail ? C.faint(` (${it.detail})`) : "";
|
|
705
|
-
|
|
706
|
-
|
|
1069
|
+
const t = it.kind === "design" && instant ? "" : clock(it.ts - r.start);
|
|
1070
|
+
out.push(` ${C.faint(pad(t, 6))} ${head} ${kindMark[it.kind](it.text)}${tag}`);
|
|
1071
|
+
// Details that only repeat the task's title or a why, and specs of new tasks, stay in the HTML.
|
|
1072
|
+
const quiet = it.kind === "message" || it.kind === "progress" || it.type === "memory.added" || it.type === "task.added";
|
|
1073
|
+
if (it.detail && !quiet)
|
|
707
1074
|
for (const l of it.detail.split("\n").slice(0, 6))
|
|
708
|
-
out.push(C.faint(
|
|
1075
|
+
out.push(C.faint(`${indent}${l}`));
|
|
709
1076
|
};
|
|
1077
|
+
const head = (name, sub = "") => out.push("", `${C.violet(name)}${sub ? C.faint(" · " + sub) : ""}`);
|
|
1078
|
+
const o = outcome(s);
|
|
1079
|
+
const crew = crewRows(s);
|
|
710
1080
|
out.push(`${C.violet("STORY")} ${C.bold(JSON.stringify(goalTitle(r.goal)))}`);
|
|
711
|
-
out.push(C.faint(` run ${r.index} of ${s.runs}${r.id ? " · " + r.id : ""} · ${new Date(r.start).toLocaleString()} · ${fmtDuration(
|
|
712
|
-
|
|
713
|
-
|
|
1081
|
+
out.push(C.faint(` run ${r.index} of ${s.runs}${r.id ? " · " + r.id : ""} · ${new Date(r.start).toLocaleString()} · ${fmtDuration(o.ms)} · ${plural(s.agents.length, "agent")}`));
|
|
1082
|
+
const mark = s.failed ? C.yellow("◎") : C.green("✓");
|
|
1083
|
+
out.push(` ${mark} ${s.merged} merged, ${s.failed} failed` +
|
|
1084
|
+
C.faint(`${o.merges ? ` · ${o.green}/${o.merges} merged on green tests` : ""}${o.tokens ? ` · ${fmtTokens(o.tokens)} tok` : ""}`));
|
|
1085
|
+
if (o.qa)
|
|
1086
|
+
out.push(` ${o.qa.tone === "ok" ? C.green("✓") : C.yellow("!")} ${o.qa.text}`);
|
|
1087
|
+
for (const c of crew)
|
|
1088
|
+
out.push(` ${dot(c.agent)} ${pad(c.agent, nameW)} ${didLine(c)}${C.faint(`${c.ms ? " · " + fmtDuration(c.ms) : ""}${c.tokens ? " · " + fmtTokens(c.tokens) + " tok" : ""}`)}`);
|
|
1089
|
+
const team = collabLines(s);
|
|
1090
|
+
const nums = tally(s);
|
|
1091
|
+
if (team.length || nums.length) {
|
|
1092
|
+
head("TEAMWORK", "who built on whom");
|
|
1093
|
+
out.push(...wrapParts(nums.map((t) => t.text ?? `${t.n} ${t.label}`), W).map((l) => C.bold(l)));
|
|
1094
|
+
// The terminal shows the first few ideas and flaws; the HTML has them all.
|
|
1095
|
+
const total = (label) => team.filter((x) => x.label === label).reduce((n, x) => n + (/^…and (\d+) more$/.exec(x.text) ? Number(/\d+/.exec(x.text)[0]) : 1), 0);
|
|
1096
|
+
const shown = new Map();
|
|
1097
|
+
for (const l of team) {
|
|
1098
|
+
const capped = l.label === "adopted" || l.label === "flaw";
|
|
1099
|
+
const k = (shown.get(l.label) ?? 0) + 1;
|
|
1100
|
+
shown.set(l.label, k);
|
|
1101
|
+
if (capped && k > 3) {
|
|
1102
|
+
if (k === 4)
|
|
1103
|
+
out.push(` ${C.faint(pad(l.label, 9))} ${C.faint(`…and ${total(l.label) - 3} more`)}`);
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
out.push(` ${C.faint(pad(l.label, 9))} ${l.text}`);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
714
1109
|
if (s.design.length) {
|
|
715
|
-
|
|
1110
|
+
const members = [...new Set(s.design.map((it) => it.agent).filter(Boolean))];
|
|
1111
|
+
head("COUNCIL", `${members.length ? members.join(", ") + " " : ""}designed it before anyone coded`);
|
|
716
1112
|
s.design.forEach(line);
|
|
717
1113
|
}
|
|
718
1114
|
if (s.plan.tasks.length) {
|
|
@@ -721,21 +1117,60 @@ export function renderText(s) {
|
|
|
721
1117
|
s.plan.tasks.forEach((t, i) => {
|
|
722
1118
|
const deps = t.deps.map((d) => "#" + (num.get(d) ?? d));
|
|
723
1119
|
out.push(` ${C.faint(String(i + 1).padStart(2) + ".")} ${t.title}` +
|
|
724
|
-
C.faint([deps.length ? `after ${deps.join(", ")}` : "", t.prefers.length ? `suits ${t.prefers.join("/")}` : "", t.files.length ? t.files.slice(0, 4).join(", ") : "", t.by ? `
|
|
1120
|
+
C.faint([deps.length ? `after ${deps.join(", ")}` : "", t.prefers.length ? `suits ${t.prefers.join("/")}` : "", t.files.length ? t.files.slice(0, 4).join(", ") : "", t.by ? `idea from ${t.by}` : ""]
|
|
725
1121
|
.filter(Boolean)
|
|
726
1122
|
.map((x) => " · " + x)
|
|
727
1123
|
.join("")));
|
|
728
1124
|
});
|
|
1125
|
+
const c = s.collab;
|
|
1126
|
+
for (const it of phases(s).agreed) {
|
|
1127
|
+
const a = agreement(it);
|
|
1128
|
+
out.push(a.risk ? ` ${C.yellow("!")} ${C.faint(`risk${a.by ? ` (${a.by})` : ""}:`)} ${a.body}` : ` ${C.violet("◆")} ${C.faint("decided:")} ${a.body}`);
|
|
1129
|
+
}
|
|
1130
|
+
if (c.contract) {
|
|
1131
|
+
const taken = c.objections.filter((x) => x.taken).length;
|
|
1132
|
+
out.push(` ${C.violet("◆")} contract by ${c.contract.by}${C.faint(`${c.objections.length ? ` · ${taken}/${c.objections.length} objections taken` : ""} · ${plural(c.amendments.length, "amendment")} during the build`)}`);
|
|
1133
|
+
}
|
|
729
1134
|
}
|
|
730
|
-
|
|
1135
|
+
if (s.screen.length) {
|
|
1136
|
+
head("SHARED SCREEN", "one copy every agent reads");
|
|
1137
|
+
for (const x of s.screen) {
|
|
1138
|
+
out.push(` ${C.bold(x.id)} ${x.kind} “${x.title}”${C.faint([x.size, x.by ? "by " + x.by : "", x.removed ? "removed" : ""].filter(Boolean).map((v) => " · " + v).join(""))}`);
|
|
1139
|
+
out.push(C.faint(` read by: ${x.readers.length ? x.readers.join(", ") : "nobody"}`));
|
|
1140
|
+
for (const n of x.notes)
|
|
1141
|
+
out.push(` 📌 ${C.bold(n.by)} ${oneLine(n.body, 160)}`);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
const ph = phases(s);
|
|
1145
|
+
head("BUILD", "agents code, check each other, merge");
|
|
731
1146
|
if (!s.items.length)
|
|
732
1147
|
out.push(C.faint(" (nothing happened in this run)"));
|
|
733
|
-
|
|
1148
|
+
ph.build.forEach(line);
|
|
1149
|
+
if (ph.qa.length) {
|
|
1150
|
+
head("QA", "agents re-check the merged build; bugs become fix tasks");
|
|
1151
|
+
ph.qa.forEach(line);
|
|
1152
|
+
}
|
|
1153
|
+
head("RESULT");
|
|
1154
|
+
out.push(` ${mark} ${s.merged} merged, ${s.failed} failed${C.faint(` · ${fmtDuration(o.ms)}${o.tokens ? ` · ${fmtTokens(o.tokens)} tok` : ""}${o.cached > o.tokens / 2 ? ` (${fmtTokens(o.cached)} cached)` : ""}`)}`);
|
|
1155
|
+
if (o.merges)
|
|
1156
|
+
out.push(` ${o.green === o.merges ? C.green("✓") : C.yellow("!")} ${o.green}/${o.merges} merges passed the tests first`);
|
|
1157
|
+
if (o.qa)
|
|
1158
|
+
out.push(` ${o.qa.tone === "ok" ? C.green("✓") : C.yellow("!")} ${plural(o.rounds, "QA round")} · ${o.qa.text}`);
|
|
734
1159
|
if (s.agents.length) {
|
|
735
|
-
out.push("", C.violet("WHO DID WHAT"));
|
|
736
|
-
const cols = [
|
|
737
|
-
|
|
738
|
-
|
|
1160
|
+
out.push("", `${C.violet("WHO DID WHAT")} ${C.faint("· msgs sent/got · bugs found/fixed · takeovers in/out")}`);
|
|
1161
|
+
const cols = [
|
|
1162
|
+
["merged", 7],
|
|
1163
|
+
["failed", 7],
|
|
1164
|
+
["reviews", 8],
|
|
1165
|
+
["msgs", 7],
|
|
1166
|
+
["bugs", 6],
|
|
1167
|
+
["takeover", 9],
|
|
1168
|
+
["tools", 6],
|
|
1169
|
+
["time", 8],
|
|
1170
|
+
["tokens", 7],
|
|
1171
|
+
];
|
|
1172
|
+
const w = nameW + 4; // "● name" + a gap
|
|
1173
|
+
out.push(C.faint(" " + pad("", w) + cols.map(([c, n]) => pad(c, n)).join("")));
|
|
739
1174
|
for (const a of s.agents) {
|
|
740
1175
|
const x = s.stats.get(a);
|
|
741
1176
|
const vals = [
|
|
@@ -743,16 +1178,15 @@ export function renderText(s) {
|
|
|
743
1178
|
x.failed,
|
|
744
1179
|
x.reviews + (x.changesAsked ? ` (${x.changesAsked}✎)` : ""),
|
|
745
1180
|
`${x.sent}/${x.received}`,
|
|
746
|
-
x.proposals || x.critiques ? `${x.proposals}p ${x.critiques}c` : "0",
|
|
747
1181
|
`${x.bugsFound}/${x.bugsFixed}`,
|
|
748
1182
|
`${x.takeoversIn}/${x.takeoversOut}`,
|
|
749
1183
|
x.toolCalls,
|
|
750
1184
|
x.ms ? fmtDuration(x.ms) : "—",
|
|
751
1185
|
x.tokens ? fmtTokens(x.tokens) : "—",
|
|
752
1186
|
].map(String);
|
|
753
|
-
out.push(` ${pad(`${dot(a)} ${a}`, w)}${vals.map((v, i) => pad(v,
|
|
1187
|
+
out.push(` ${pad(`${dot(a)} ${a}`, w)}${vals.map((v, i) => pad(v, cols[i][1])).join("")}`);
|
|
754
1188
|
}
|
|
755
|
-
out.push("", `${C.violet("WHO TALKED TO WHOM")} ${C.faint("
|
|
1189
|
+
out.push("", `${C.violet("WHO TALKED TO WHOM")} ${C.faint("· row → column: messages, reviews, critiques, handoffs")}`);
|
|
756
1190
|
const cw = Math.max(8, ...s.agents.map((a) => a.length + 2));
|
|
757
1191
|
out.push(" " + pad("", w) + s.agents.map((a) => pad(a, cw)).join(""));
|
|
758
1192
|
for (const a of s.agents) {
|
|
@@ -760,7 +1194,7 @@ export function renderText(s) {
|
|
|
760
1194
|
out.push(` ${pad(`${dot(a)} ${a}`, w)}${s.agents.map((b) => pad(a === b ? C.faint("·") : row?.get(b) ? C.bold(String(row.get(b))) : C.faint("0"), cw)).join("")}`);
|
|
761
1195
|
}
|
|
762
1196
|
}
|
|
763
|
-
return out.join("\n");
|
|
1197
|
+
return (opts.width ? out.map((l) => truncate(l, Math.max(20, opts.width - 1))) : out).join("\n");
|
|
764
1198
|
}
|
|
765
1199
|
export function renderRunList(runs) {
|
|
766
1200
|
if (!runs.length)
|
|
@@ -776,12 +1210,95 @@ export function renderRunList(runs) {
|
|
|
776
1210
|
// ---- HTML (a static, self-contained report file) --------------------------------
|
|
777
1211
|
const esc = (s) => str(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
778
1212
|
const colorOf = (a) => (a ? AGENT_COLOR[a.split("#")[0]] ?? "#8A857C" : "#7B6CF6");
|
|
1213
|
+
/** Lines that are the agents thinking out loud, hidden behind a toggle so the story reads first. */
|
|
1214
|
+
const NOISE = (it) => it.kind === "progress" || it.kind === "announce" || it.kind === "memory" || it.type === "memory.added";
|
|
779
1215
|
export function renderHtml(s) {
|
|
780
1216
|
const r = s.run;
|
|
1217
|
+
const c = s.collab;
|
|
1218
|
+
const o = outcome(s);
|
|
781
1219
|
const span = Math.max(1, r.end - r.start);
|
|
782
1220
|
const pct = (ts) => (((ts - r.start) / span) * 100).toFixed(3);
|
|
783
1221
|
const chip = (a) => `<span class="who"><i style="background:${colorOf(a)}"></i>${esc(a ?? "huddle")}</span>`;
|
|
784
|
-
|
|
1222
|
+
const badge = (text, tone = "") => `<span class="badge ${tone}">${esc(text)}</span>`;
|
|
1223
|
+
const card = (title, body, sub = "") => `<div class="card"><h3>${esc(title)}${sub ? ` <small>${esc(sub)}</small>` : ""}</h3>${body}</div>`;
|
|
1224
|
+
const empty = (t) => `<p class="none">${esc(t)}</p>`;
|
|
1225
|
+
// ---- header + crew
|
|
1226
|
+
const kpis = [
|
|
1227
|
+
{ n: String(s.merged), l: "merged", tone: "ok" },
|
|
1228
|
+
{ n: String(s.failed), l: "failed", tone: s.failed ? "bad" : "" },
|
|
1229
|
+
...(o.merges ? [{ n: `${o.green}/${o.merges}`, l: "merged on green tests", tone: o.green === o.merges ? "ok" : "warn" }] : []),
|
|
1230
|
+
{ n: fmtDuration(o.ms), l: "start to finish", tone: "" },
|
|
1231
|
+
...(o.tokens ? [{ n: fmtTokens(o.tokens), l: "tokens", tone: "" }] : []),
|
|
1232
|
+
...(c.bugs.length ? [{ n: `${c.bugs.length}→${c.bugs.filter((b) => b.fixedBy).length}`, l: "QA bugs → fixed", tone: "" }] : []),
|
|
1233
|
+
]
|
|
1234
|
+
.map((k) => `<div class="kpi ${k.tone}"><b>${esc(k.n)}</b><span>${esc(k.l)}</span></div>`)
|
|
1235
|
+
.join("");
|
|
1236
|
+
const crew = crewRows(s)
|
|
1237
|
+
.map((x) => `<div class="agent" style="--c:${colorOf(x.agent)}"><div class="an"><i></i>${esc(x.agent)}</div><div class="as">${esc(didLine(x))}</div><div class="am">${esc([x.ms ? fmtDuration(x.ms) : "", x.tokens ? fmtTokens(x.tokens) + " tok" : ""].filter(Boolean).join(" · ") || "—")}</div></div>`)
|
|
1238
|
+
.join("");
|
|
1239
|
+
// ---- teamwork
|
|
1240
|
+
const tiles = tally(s)
|
|
1241
|
+
.map((t) => `<div class="tile ${t.tone ?? ""}"><b>${esc(t.n)}</b><span>${esc(t.label)}</span></div>`)
|
|
1242
|
+
.join("");
|
|
1243
|
+
const ideaAgents = s.agents.filter((a) => [...c.adopts, ...c.flaws].some((x) => x.by === a || x.of === a));
|
|
1244
|
+
const cell = (by, of) => {
|
|
1245
|
+
if (by === of)
|
|
1246
|
+
return `<td class="self">·</td>`;
|
|
1247
|
+
const ad = c.adopts.filter((x) => x.by === by && x.of === of).length;
|
|
1248
|
+
const fl = c.flaws.filter((x) => x.by === by && x.of === of).length;
|
|
1249
|
+
return `<td>${ad ? `<span class="ad" title="${ad} adopted">${ad}</span>` : ""}${fl ? `<span class="fl" title="${fl} flaw(s)">${fl}</span>` : ""}${!ad && !fl ? `<span class="z">0</span>` : ""}</td>`;
|
|
1250
|
+
};
|
|
1251
|
+
const ideas = ideaAgents.length
|
|
1252
|
+
? `<div class="scroll"><table class="grid"><tr><th><small>took ↓ · from →</small></th>${ideaAgents.map((a) => `<th>${chip(a)}</th>`).join("")}</tr>${ideaAgents
|
|
1253
|
+
.map((by) => `<tr><th>${chip(by)}</th>${ideaAgents.map((of) => cell(by, of)).join("")}</tr>`)
|
|
1254
|
+
.join("")}</table></div><p class="legend"><span class="ad">n</span> ideas adopted · <span class="fl">n</span> flaws found · row took from / flagged the column's proposal</p>${c.adopts
|
|
1255
|
+
.slice(0, 3)
|
|
1256
|
+
.map((a) => `<div class="quote">${chip(a.by)} took ${chip(a.of)}'s idea: ${esc(oneLine(a.text, 160))}</div>`)
|
|
1257
|
+
.join("")}`
|
|
1258
|
+
: "";
|
|
1259
|
+
const contract = [
|
|
1260
|
+
c.synthesizer ? `<li>${chip(c.synthesizer.id)} wrote the plan${c.synthesizer.why ? ` <small>${esc(oneLine(c.synthesizer.why, 120))}</small>` : ""}</li>` : "",
|
|
1261
|
+
// Each objection is listed with whether it was taken; the ratify rows add only who signed off.
|
|
1262
|
+
...c.ratify.filter((x) => x.verdict !== "object" || !c.objections.length).map((x) => `<li>${chip(x.agent)} ${x.verdict === "object" ? badge("objected", "warn") + ` ${esc(oneLine(x.objections.join(" · "), 160))}` : badge("ratified", "ok")}</li>`),
|
|
1263
|
+
...c.objections.map((x) => `<li>${chip(x.by)}'s objection ${x.taken ? badge("taken", "ok") : badge("kept as is")} <span class="detail">${esc(oneLine(x.objection, 160))}</span></li>`),
|
|
1264
|
+
c.contract ? `<li>◆ contract v1 by ${chip(c.contract.by)} <small>${esc(c.contract.chars)} chars · the names every task codes to</small></li>` : "",
|
|
1265
|
+
...c.amendments.map((a) => `<li>◆ v${esc(a.version)} by ${chip(a.by)} <span class="detail">${esc(oneLine(a.change, 160))}</span></li>`),
|
|
1266
|
+
]
|
|
1267
|
+
.filter(Boolean)
|
|
1268
|
+
.join("");
|
|
1269
|
+
const notes = buddyNotes(s);
|
|
1270
|
+
const good = c.buddy.filter((b) => !b.notes).length;
|
|
1271
|
+
const buddy = c.buddy.length
|
|
1272
|
+
? `<ul class="list">${notes
|
|
1273
|
+
.map((b) => `<li>${chip(b.agent)} → ${chip(b.author)} <small>at ${esc(b.pct)}%</small> ${badge(b.how, b.tone)}<div class="detail">${esc(oneLine(b.notes, 240))}</div></li>`)
|
|
1274
|
+
.join("")}${good ? `<li class="mute">✓ ${plural(good, "check")} looked good</li>` : ""}</ul>`
|
|
1275
|
+
: "";
|
|
1276
|
+
const bugs = c.bugs.length
|
|
1277
|
+
? `<ul class="list">${c.bugs
|
|
1278
|
+
.map((b) => `<li>${chip(b.by)} found ${b.to ? `→ fix for ${chip(b.to)}` : ""} ${b.fixedBy ? badge(`fixed by ${b.fixedBy}`, "ok") : badge("open", "warn")}<div class="detail">${esc(oneLine(b.title, 200))}</div></li>`)
|
|
1279
|
+
.join("")}</ul>`
|
|
1280
|
+
: "";
|
|
1281
|
+
const radarFix = c.followups.filter((f) => f.kind === "radar");
|
|
1282
|
+
const radar = [
|
|
1283
|
+
...c.radar.map((x) => `<li>⚠ ${chip(x.agent)} ↔ ${x.with && x.with !== "contract" ? chip(x.with) : esc(x.with || "?")} ${badge(x.kind === "overlap" ? "overlap" : "name mismatch", "warn")}<div class="detail">${esc(oneLine(x.detail, 200))}</div></li>`),
|
|
1284
|
+
...radarFix.map((f) => `<li>${chip(f.to)} ${badge(f.how === "ignored" ? "ignored" : f.how === "replied" ? "answered" : "fixed in code", f.how === "ignored" ? "bad" : "ok")} the radar's finding<div class="detail">${esc(oneLine(f.note, 200))}</div></li>`),
|
|
1285
|
+
...c.pairs.map((p) => `<li>⇉ ${chip(p.agent)} paired with ${chip(p.with)}<div class="detail">took: ${esc(oneLine(p.part, 200))}</div></li>`),
|
|
1286
|
+
].join("");
|
|
1287
|
+
const radarNote = !c.radar.length && c.radarLooks?.looks ? `<p class="none">no radar catches · it compared work in progress ${plural(c.radarLooks.looks, "time")}</p>` : "";
|
|
1288
|
+
const team = collabLines(s);
|
|
1289
|
+
const cards = [
|
|
1290
|
+
ideas ? card("Ideas between agents", ideas, "council cross-review") : "",
|
|
1291
|
+
contract ? card("Council → contract", `<ul class="list">${contract}</ul>`) : "",
|
|
1292
|
+
buddy ? card("Buddy notes mid-task", buddy, "and what came of them") : "",
|
|
1293
|
+
bugs ? card("QA bugs → fixes", bugs) : "",
|
|
1294
|
+
radar || radarNote ? card("Radar & pairs", (radar ? `<ul class="list">${radar}</ul>` : "") + radarNote) : "",
|
|
1295
|
+
]
|
|
1296
|
+
.filter(Boolean)
|
|
1297
|
+
.join("");
|
|
1298
|
+
const teamwork = tiles || cards || team.length
|
|
1299
|
+
? `<h2>Teamwork <small>who built on whom</small></h2>${tiles ? `<div class="tiles">${tiles}</div>` : ""}${cards ? `<div class="cards">${cards}</div>` : ""}${team.length ? `<details><summary>Every teamwork line (${team.length})</summary><table class="team">${team.map((l) => `<tr><th>${esc(l.label)}</th><td>${esc(l.text)}</td></tr>`).join("")}</table></details>` : ""}`
|
|
1300
|
+
: "";
|
|
1301
|
+
// ---- lanes: one per agent; spans are its tasks/reviews/conflicts/QA; dots are its messages.
|
|
785
1302
|
const ticks = Array.from({ length: 5 }, (_, i) => `<span style="left:${i * 25}%">${esc(clock((span * i) / 4))}</span>`).join("");
|
|
786
1303
|
const lanes = s.agents
|
|
787
1304
|
.map((a) => {
|
|
@@ -790,19 +1307,50 @@ export function renderHtml(s) {
|
|
|
790
1307
|
.map((x) => `<div class="bar ${x.kind} ${x.outcome ?? ""}" style="left:${pct(x.from)}%;width:max(4px,${(((x.to - x.from) / span) * 100).toFixed(3)}%);--c:${colorOf(a)}" title="${esc(`${x.kind}: ${x.label} · ${clock(x.from - r.start)}–${clock(x.to - r.start)}`)}"><span>${esc(x.label)}</span></div>`)
|
|
791
1308
|
.join("");
|
|
792
1309
|
const marks = [...s.items, ...s.design]
|
|
793
|
-
.filter((it) => it.agent === a && ["message", "design", "qa", "takeover", "merge", "fail"
|
|
1310
|
+
.filter((it) => it.agent === a && ["message", "design", "qa", "takeover", "merge", "fail"].includes(it.kind))
|
|
794
1311
|
.map((it) => `<b class="mark ${it.kind}" style="left:${pct(it.ts)}%" title="${esc(`${clock(it.ts - r.start)} ${it.to ? "→ " + it.to + ": " : ""}${it.text}`)}"></b>`)
|
|
795
1312
|
.join("");
|
|
796
1313
|
return `<div class="lane" data-lane="${esc(a)}"><div class="name">${chip(a)}</div><div class="track">${bars}${marks}</div></div>`;
|
|
797
1314
|
})
|
|
798
1315
|
.join("");
|
|
799
|
-
|
|
1316
|
+
// ---- the timeline, by phase
|
|
1317
|
+
const instant = councilInstant(s);
|
|
1318
|
+
const row = (it) => `<li class="${it.kind}${NOISE(it) ? " noise" : ""}"><time>${it.kind === "design" && instant ? "" : esc(clock(it.ts - r.start))}</time><div>${chip(it.agent)}${it.to && it.kind !== "design" ? ` <span class="arrow">→</span> ${chip(it.to)}` : ""} <span class="text">${esc(it.text)}</span>${it.detail ? `<div class="detail">${esc(it.detail)}</div>` : ""}</div></li>`;
|
|
800
1319
|
const num = new Map(s.plan.tasks.map((t, i) => [t.id, i + 1]));
|
|
801
1320
|
const plan = s.plan.tasks
|
|
802
|
-
.map((t) => `<li><b>${esc(t.title)}</b>${t.deps.length ? ` <small>after ${esc(t.deps.map((d) => "#" + (num.get(d) ?? d)).join(", "))}</small>` : ""}${t.prefers.length ? ` <small>suits ${esc(t.prefers.join("/"))}</small>` : ""}${t.by ? ` <small>
|
|
1321
|
+
.map((t) => `<li><b>${esc(t.title)}</b>${t.deps.length ? ` <small>after ${esc(t.deps.map((d) => "#" + (num.get(d) ?? d)).join(", "))}</small>` : ""}${t.prefers.length ? ` <small>suits ${esc(t.prefers.join("/"))}</small>` : ""}${t.by ? ` <small>idea from ${esc(t.by)}</small>` : ""}${t.files.length ? `<div class="detail">${esc(t.files.join(", "))}</div>` : ""}</li>`)
|
|
803
1322
|
.join("");
|
|
1323
|
+
const ph = phases(s);
|
|
1324
|
+
const noise = s.items.filter(NOISE).length;
|
|
1325
|
+
const phase = (tag, sub, body) => `<section class="phase"><h3><span class="tag">${esc(tag)}</span> ${esc(sub)}</h3>${body}</section>`;
|
|
1326
|
+
const members = [...new Set(s.design.map((it) => it.agent).filter(Boolean))];
|
|
1327
|
+
const result = [
|
|
1328
|
+
`<li>${s.failed ? "◎" : "✓"} <b>${s.merged} merged, ${s.failed} failed</b> <small>${esc(fmtDuration(o.ms))}${o.tokens ? " · " + esc(fmtTokens(o.tokens)) + " tok" : ""}</small></li>`,
|
|
1329
|
+
o.merges ? `<li>${o.green === o.merges ? "✓" : "!"} ${o.green}/${o.merges} merges passed the tests first</li>` : "",
|
|
1330
|
+
o.qa ? `<li class="${o.qa.tone}">${o.qa.tone === "ok" ? "✓" : "!"} ${esc(plural(o.rounds, "QA round"))} · ${esc(o.qa.text)}</li>` : "",
|
|
1331
|
+
].join("");
|
|
1332
|
+
const timeline = [
|
|
1333
|
+
s.design.length ? phase("COUNCIL", `${members.join(", ")} designed it before anyone coded`, `<ol class="timeline">${s.design.map(row).join("")}</ol>`) : "",
|
|
1334
|
+
s.plan.tasks.length
|
|
1335
|
+
? phase("PLAN", `${plural(s.plan.tasks.length, "task")}${s.plan.by ? " · by " + s.plan.by : ""}`, `<ol class="plan">${plan}</ol>${ph.agreed.length
|
|
1336
|
+
? `<ul class="list agreed">${ph.agreed
|
|
1337
|
+
.map((it) => agreement(it))
|
|
1338
|
+
.map((a) => `<li>${a.risk ? `<span class="badge warn">risk${a.by ? " · " + esc(a.by) : ""}</span>` : `<span class="badge ok">decided</span>`} ${esc(a.body)}</li>`)
|
|
1339
|
+
.join("")}</ul>`
|
|
1340
|
+
: ""}${c.contract ? `<p class="contract">◆ contract by ${chip(c.contract.by)}${c.objections.length ? ` · ${c.objections.filter((x) => x.taken).length}/${c.objections.length} objections taken` : ""} · ${plural(c.amendments.length, "amendment")} during the build</p>` : ""}`)
|
|
1341
|
+
: "",
|
|
1342
|
+
phase("BUILD", "agents code, check each other, merge", `<ol class="timeline">${ph.build.map(row).join("") || '<li class="none">nothing happened in this run</li>'}</ol>`),
|
|
1343
|
+
ph.qa.length ? phase("QA", "agents re-check the merged build; bugs become fix tasks", `<ol class="timeline">${ph.qa.map(row).join("")}</ol>`) : "",
|
|
1344
|
+
phase("RESULT", "", `<ul class="list result">${result}</ul>`),
|
|
1345
|
+
].join("");
|
|
1346
|
+
// ---- shared screen, conversations, tables
|
|
1347
|
+
const screen = s.screen.length
|
|
1348
|
+
? `<h2>Shared screen <small>one copy every agent reads</small></h2><ul class="list card">${s.screen
|
|
1349
|
+
.map((x) => `<li><b>${esc(x.id)}</b> ${esc(x.kind)} “${esc(x.title)}”${x.size ? ` <small>${esc(x.size)}</small>` : ""}${x.by ? ` <small>by ${esc(x.by)}</small>` : ""}${x.removed ? ` <small>removed</small>` : ""}<div class="detail">read by: ${x.readers.length ? x.readers.map((a) => chip(a)).join(" ") : "nobody"}</div>${x.notes.map((n) => `<div class="detail">📌 ${chip(n.by)} ${esc(n.body)}</div>`).join("")}</li>`)
|
|
1350
|
+
.join("")}</ul>`
|
|
1351
|
+
: "";
|
|
804
1352
|
const threads = [...s.threads.entries()]
|
|
805
|
-
.map(([pair, msgs]) => `<details
|
|
1353
|
+
.map(([pair, msgs]) => `<details><summary>${esc(pair)} <small>${msgs.length} message${msgs.length > 1 ? "s" : ""}</small></summary><ol class="thread">${msgs
|
|
806
1354
|
.map((m) => `<li class="${m.agent === pair.split(" ↔ ")[0] ? "l" : "r"}" style="--c:${colorOf(m.agent)}"><div class="meta">${esc(m.agent)} → ${esc(m.to)} · ${esc(clock(m.ts - r.start))}${m.detail && m.kind === "message" ? " · " + esc(m.detail) : ""}</div>${esc(m.text)}${m.detail && m.kind !== "message" ? `<div class="detail">${esc(m.detail)}</div>` : ""}</li>`)
|
|
807
1355
|
.join("")}</ol></details>`)
|
|
808
1356
|
.join("");
|
|
@@ -822,59 +1370,107 @@ export function renderHtml(s) {
|
|
|
822
1370
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
823
1371
|
<title>Huddle story</title>
|
|
824
1372
|
<style>
|
|
825
|
-
:root{--bg:#
|
|
826
|
-
@media (prefers-color-scheme: dark){:root:not([data-theme="light"]){--bg:#
|
|
827
|
-
:root[data-theme="dark"]{--bg:#
|
|
1373
|
+
:root{--bg:#f6f5f1;--fg:#1d1b18;--mute:#6f6a62;--line:#e4e0d8;--card:#fff;--soft:#efede7;--ok:#23864c;--bad:#c93f30;--warn:#a86f05;--brand:#6a5af0;color-scheme:light}
|
|
1374
|
+
@media (prefers-color-scheme: dark){:root:not([data-theme="light"]){--bg:#121117;--fg:#ece9e3;--mute:#9a948b;--line:#2b2932;--card:#1b1a21;--soft:#23222a;--ok:#4cc27c;--bad:#f06a5b;--warn:#e0a53a;--brand:#9488ff;color-scheme:dark}}
|
|
1375
|
+
:root[data-theme="dark"]{--bg:#121117;--fg:#ece9e3;--mute:#9a948b;--line:#2b2932;--card:#1b1a21;--soft:#23222a;--ok:#4cc27c;--bad:#f06a5b;--warn:#e0a53a;--brand:#9488ff;color-scheme:dark}
|
|
828
1376
|
*{box-sizing:border-box}
|
|
829
|
-
body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif}
|
|
830
|
-
main{max-width:1100px;margin:0 auto;padding:
|
|
831
|
-
h1{font-size:
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1377
|
+
body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.5 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;-webkit-text-size-adjust:100%}
|
|
1378
|
+
main{max-width:1100px;margin:0 auto;padding:24px 16px 60px}
|
|
1379
|
+
h1{font-size:26px;line-height:1.25;margin:6px 0 4px;word-break:break-word}
|
|
1380
|
+
h2{font-size:13px;text-transform:uppercase;letter-spacing:.08em;color:var(--brand);margin:36px 0 12px}
|
|
1381
|
+
h2 small{text-transform:none;letter-spacing:0}
|
|
1382
|
+
h3{font-size:14px;margin:0 0 10px}
|
|
1383
|
+
.brand{font-weight:700;color:var(--brand);font-size:13px;letter-spacing:.04em}
|
|
1384
|
+
.sub,small,.detail,time,.meta,.none,.legend{color:var(--mute)}small{font-size:12px;margin-left:6px;font-weight:400}
|
|
1385
|
+
.none{font-size:13px;margin:6px 0}
|
|
1386
|
+
.who{display:inline-flex;align-items:center;gap:5px;font-weight:600;white-space:nowrap}.who i{width:9px;height:9px;border-radius:50%;display:inline-block;flex:none}
|
|
1387
|
+
.hero{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:20px}
|
|
1388
|
+
.hero details{margin:8px 0}
|
|
1389
|
+
.kpis,.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px;margin-top:16px}
|
|
1390
|
+
.kpi,.tile{background:var(--soft);border-radius:12px;padding:10px 12px;min-width:0}
|
|
1391
|
+
.kpi b,.tile b{display:block;font-size:24px;line-height:1.2;font-variant-numeric:tabular-nums}
|
|
1392
|
+
.kpi span,.tile span{font-size:12px;color:var(--mute)}
|
|
1393
|
+
.kpi.ok b,.tile.ok b{color:var(--ok)}.kpi.bad b,.tile.bad b{color:var(--bad)}.kpi.warn b,.tile.warn b{color:var(--warn)}
|
|
1394
|
+
.verdict{margin:14px 0 0;font-weight:600}.verdict.ok{color:var(--ok)}.verdict.warn{color:var(--warn)}
|
|
1395
|
+
.crew{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px;margin-top:12px}
|
|
1396
|
+
.agent{background:var(--card);border:1px solid var(--line);border-top:4px solid var(--c);border-radius:12px;padding:10px 14px;min-width:0}
|
|
1397
|
+
.agent .an{display:flex;align-items:center;gap:7px;font-weight:700;font-size:16px}.agent .an i{width:11px;height:11px;border-radius:50%;background:var(--c)}
|
|
1398
|
+
.agent .as{font-size:14px}.agent .am{font-size:12px;color:var(--mute);font-variant-numeric:tabular-nums}
|
|
1399
|
+
.cards{columns:320px 3;column-gap:12px;margin-top:12px}.cards .card{break-inside:avoid;margin-bottom:12px}
|
|
1400
|
+
.card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px 16px;min-width:0}
|
|
1401
|
+
.list li{font-size:14px;padding:7px 0;border-top:1px solid var(--line);word-break:break-word}.list li:first-child{border-top:0}
|
|
1402
|
+
.list li.mute{color:var(--mute)}
|
|
1403
|
+
.badge{display:inline-block;font-size:11px;font-weight:700;padding:1px 8px;border-radius:999px;background:var(--soft);color:var(--mute);white-space:nowrap}
|
|
1404
|
+
.badge.ok{color:var(--ok);background:color-mix(in srgb,var(--ok) 14%,transparent)}.badge.bad{color:var(--bad);background:color-mix(in srgb,var(--bad) 14%,transparent)}.badge.warn{color:var(--warn);background:color-mix(in srgb,var(--warn) 16%,transparent)}
|
|
1405
|
+
.grid td,.grid th{text-align:center}.grid th:first-child{text-align:left}
|
|
1406
|
+
.ad,.fl{display:inline-block;min-width:22px;padding:1px 6px;margin:1px;border-radius:6px;font-weight:700;font-size:12px}
|
|
1407
|
+
.ad{color:var(--ok);background:color-mix(in srgb,var(--ok) 15%,transparent)}.fl{color:var(--warn);background:color-mix(in srgb,var(--warn) 17%,transparent)}.z{color:var(--line)}
|
|
1408
|
+
.legend{font-size:12px;margin:8px 0}
|
|
1409
|
+
.quote{font-size:13px;padding:6px 0 6px 10px;border-left:3px solid var(--line);margin-top:6px;word-break:break-word}
|
|
1410
|
+
.scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}
|
|
1411
|
+
.lanesbox{border:1px solid var(--line);border-radius:12px;background:var(--card)}
|
|
835
1412
|
.lanes{min-width:640px;padding:8px 12px 12px}
|
|
836
1413
|
.axis{position:relative;height:18px;margin-left:120px;font-size:11px;color:var(--mute)}.axis span{position:absolute;transform:translateX(-50%)}.axis span:first-child{transform:none}.axis span:last-child{transform:translateX(-100%)}
|
|
837
1414
|
.lane{display:flex;align-items:center;height:38px;border-top:1px solid var(--line)}
|
|
838
1415
|
.lane .name{width:120px;flex:none;overflow:hidden;text-overflow:ellipsis;font-size:13px}
|
|
839
1416
|
.track{position:relative;flex:1;height:26px}
|
|
840
1417
|
.bar{position:absolute;top:3px;height:20px;border-radius:5px;background:color-mix(in srgb,var(--c) 30%,transparent);border:1px solid var(--c);overflow:hidden;font-size:11px;line-height:18px;padding:0 4px;white-space:nowrap}
|
|
841
|
-
.bar
|
|
1418
|
+
.bar.review{border-style:dashed}.bar.conflict{border-color:var(--warn)}.bar.qa{border-style:dotted}
|
|
842
1419
|
.bar.ok{box-shadow:inset -3px 0 0 var(--ok)}.bar.bad{box-shadow:inset -3px 0 0 var(--bad)}.bar.warn{box-shadow:inset -3px 0 0 var(--warn)}
|
|
843
1420
|
.mark{position:absolute;top:-3px;width:7px;height:7px;margin-left:-3px;border-radius:50%;background:var(--brand)}
|
|
844
|
-
.mark.merge{background:var(--ok)}.mark.fail{background:var(--bad)}.mark.qa,.mark.takeover{background:var(--warn)}
|
|
1421
|
+
.mark.merge{background:var(--ok)}.mark.fail{background:var(--bad)}.mark.qa,.mark.takeover{background:var(--warn)}
|
|
845
1422
|
ol,ul{padding:0;margin:0;list-style:none}
|
|
846
|
-
.
|
|
847
|
-
.
|
|
848
|
-
.
|
|
849
|
-
.timeline
|
|
1423
|
+
.phase{margin-top:22px}
|
|
1424
|
+
.phase h3{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;font-weight:400;color:var(--mute);font-size:13px}
|
|
1425
|
+
.tag{font-weight:800;letter-spacing:.08em;color:#fff;background:var(--brand);border-radius:6px;padding:2px 8px;font-size:12px}
|
|
1426
|
+
.timeline li{display:flex;gap:10px;padding:6px 0;font-size:14px;border-top:1px solid var(--line);min-width:0}
|
|
1427
|
+
.timeline li>div{min-width:0;word-break:break-word}
|
|
1428
|
+
.timeline time{flex:none;width:46px;font-variant-numeric:tabular-nums;font-size:13px}
|
|
1429
|
+
.timeline .merge .text{color:var(--ok);font-weight:600}.timeline .fail .text{color:var(--bad)}.timeline .conflict .text,.timeline .qa .text,.timeline .takeover .text{color:var(--warn)}
|
|
1430
|
+
.timeline .progress .text,.timeline .announce .text,.timeline .team .text,.timeline .other .text,.timeline .memory .text{color:var(--mute)}
|
|
1431
|
+
#noise{position:absolute;opacity:0;pointer-events:none}
|
|
1432
|
+
.toggle{display:inline-block;font-size:13px;color:var(--mute);border:1px solid var(--line);border-radius:999px;padding:3px 12px;cursor:pointer;user-select:none;margin-top:4px}
|
|
1433
|
+
.toggle::before{content:"○ "}#noise:checked~.toggle::before{content:"● "}
|
|
1434
|
+
#noise:focus-visible~.toggle{outline:2px solid var(--brand)}
|
|
1435
|
+
.timeline li.noise{display:none}#noise:checked~.phases .timeline li.noise{display:flex}
|
|
850
1436
|
.arrow{color:var(--mute)}.detail{font-size:13px;white-space:pre-wrap;word-break:break-word}
|
|
851
|
-
.plan li{padding:6px 0;border-top:1px solid var(--line)
|
|
852
|
-
|
|
1437
|
+
.plan{counter-reset:n}.plan li{padding:6px 0;border-top:1px solid var(--line);word-break:break-word}.plan li::before{counter-increment:n;content:counter(n) ". ";color:var(--mute)}
|
|
1438
|
+
.contract{margin:10px 0 0}
|
|
1439
|
+
.result li{font-size:15px}.result li.ok{color:var(--ok)}.result li.warn{color:var(--warn)}
|
|
1440
|
+
details{border:1px solid var(--line);border-radius:12px;background:var(--card);margin:10px 0;padding:8px 12px}summary{cursor:pointer;font-weight:600}
|
|
853
1441
|
.thread{display:flex;flex-direction:column;gap:8px;margin-top:10px}
|
|
854
1442
|
.thread li{max-width:85%;padding:8px 10px;border-radius:12px;border:1px solid var(--line);border-left:4px solid var(--c);word-break:break-word}
|
|
855
1443
|
.thread li.r{align-self:flex-end;border-left:1px solid var(--line);border-right:4px solid var(--c)}.thread .meta{font-size:12px}
|
|
856
1444
|
table{border-collapse:collapse;width:100%;font-size:13px;font-variant-numeric:tabular-nums}th,td{padding:6px 8px;border-bottom:1px solid var(--line);text-align:left;white-space:nowrap}
|
|
1445
|
+
table.team td{white-space:normal;word-break:break-word}table.team th{color:var(--mute);font-weight:600;vertical-align:top}
|
|
857
1446
|
td.hot{font-weight:700;color:var(--brand)}td.self{color:var(--mute)}
|
|
1447
|
+
footer{margin-top:40px;font-size:12px;color:var(--mute);text-align:center}
|
|
1448
|
+
@media (max-width:520px){h1{font-size:21px}.hero{padding:16px}.kpi b,.tile b{font-size:20px}.kpis,.tiles{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
858
1449
|
</style>
|
|
859
1450
|
</head>
|
|
860
1451
|
<body>
|
|
861
1452
|
<main>
|
|
862
|
-
<
|
|
863
|
-
<div class="
|
|
864
|
-
|
|
865
|
-
<
|
|
866
|
-
<div class="
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
${
|
|
870
|
-
|
|
871
|
-
|
|
1453
|
+
<header class="hero">
|
|
1454
|
+
<div class="brand">⬢ HUDDLE · STORY</div>
|
|
1455
|
+
<h1>${esc(goalTitle(r.goal, 110))}</h1>${goalTitle(r.goal, 110) !== r.goal.trim() ? `<details><summary>The goal</summary><pre style="white-space:pre-wrap;word-break:break-word">${esc(r.goal)}</pre></details>` : ""}
|
|
1456
|
+
<div class="sub">run ${r.index} of ${s.runs}${r.id ? " · " + esc(r.id) : ""} · ${esc(new Date(r.start).toLocaleString())} · ${s.agents.length} agent${s.agents.length === 1 ? "" : "s"}</div>
|
|
1457
|
+
<div class="kpis">${kpis}</div>
|
|
1458
|
+
${o.qa ? `<p class="verdict ${o.qa.tone}">${o.qa.tone === "ok" ? "✓" : "!"} ${esc(plural(o.rounds, "QA round"))} · ${esc(o.qa.text)}</p>` : ""}
|
|
1459
|
+
</header>
|
|
1460
|
+
${crew ? `<div class="crew">${crew}</div>` : ""}
|
|
1461
|
+
${teamwork}
|
|
1462
|
+
<h2>Timeline <small>who was doing what, when</small></h2>
|
|
1463
|
+
<div class="lanesbox scroll"><div class="lanes"><div class="axis">${ticks}</div>${lanes || '<p class="none">no agents in this run</p>'}</div></div>
|
|
1464
|
+
<p class="legend">bars: tasks (solid) · reviews (dashed) · conflict fixes · QA (dotted); right edge green = merged/approved, red = failed/changes, amber = skipped/bug.</p>
|
|
1465
|
+
<input type="checkbox" id="noise"><label class="toggle" for="noise">show progress notes & memory (${noise})</label>
|
|
1466
|
+
<div class="phases">${timeline}</div>
|
|
1467
|
+
${screen}
|
|
1468
|
+
${threads ? `<h2>Conversations <small>agent to agent</small></h2>${threads}` : ""}
|
|
872
1469
|
<h2>Who did what</h2>
|
|
873
|
-
<div class="scroll"><table><tr><th>agent</th><th>merged</th><th>failed</th><th>reviews</th><th>msgs out/in</th><th>proposals/critiques</th><th>bugs found/fixed</th><th>takeovers in/out</th><th>tool calls</th><th>time</th><th>tokens</th></tr>${statRows}</table></div>
|
|
1470
|
+
<div class="scroll card"><table><tr><th>agent</th><th>merged</th><th>failed</th><th>reviews</th><th>msgs out/in</th><th>proposals/critiques</th><th>bugs found/fixed</th><th>takeovers in/out</th><th>tool calls</th><th>time</th><th>tokens</th></tr>${statRows}</table></div>
|
|
874
1471
|
<h2>Who talked to whom <small>row → column</small></h2>
|
|
875
|
-
<div class="scroll"><table>${matrix}</table></div>
|
|
876
|
-
<
|
|
877
|
-
<ol class="timeline">${s.items.map(row).join("") || '<li class="sub">nothing happened in this run</li>'}</ol>
|
|
1472
|
+
<div class="scroll card"><table>${matrix}</table></div>
|
|
1473
|
+
<footer>made by huddle story · one file, no network</footer>
|
|
878
1474
|
</main>
|
|
879
1475
|
</body>
|
|
880
1476
|
</html>
|
|
@@ -927,6 +1523,7 @@ export async function runStory(args) {
|
|
|
927
1523
|
console.log(`${C.green("✓")} wrote ${out} ${C.faint(`· one file, no network · ${story.agents.length} lanes, ${story.items.length} events`)}`);
|
|
928
1524
|
return;
|
|
929
1525
|
}
|
|
930
|
-
|
|
1526
|
+
// A terminal gets lines cut to its width; a file or a pipe gets them whole.
|
|
1527
|
+
console.log(renderText(story, { width: process.stdout.isTTY ? process.stdout.columns || 80 : undefined }));
|
|
931
1528
|
}
|
|
932
1529
|
//# sourceMappingURL=story.js.map
|