huddle-cli 0.1.0 → 0.1.3
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 +69 -0
- package/QUICKSTART.md +135 -0
- package/README.md +26 -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 +9 -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/app/live.js +142 -0
- package/dist/app/page.js +701 -0
- package/dist/app/server.js +262 -0
- package/dist/args.js +18 -0
- package/dist/attach/connect.js +7 -4
- package/dist/attach/guard.js +21 -2
- package/dist/attach/live.js +5 -1
- package/dist/attach/plug.js +21 -3
- package/dist/attach/screen.js +150 -0
- package/dist/attach/session.js +7 -2
- package/dist/cli.js +134 -42
- package/dist/mcp/context-tools.js +79 -0
- package/dist/mcp/server.js +57 -11
- package/dist/orchestrator/ask.js +13 -10
- package/dist/orchestrator/brain.js +16 -4
- package/dist/orchestrator/budget.js +283 -0
- package/dist/orchestrator/busy.js +106 -0
- package/dist/orchestrator/council.js +434 -87
- package/dist/orchestrator/goal.js +196 -0
- package/dist/orchestrator/handoff.js +2 -2
- package/dist/orchestrator/launcher.js +31 -13
- package/dist/orchestrator/learn.js +101 -0
- package/dist/orchestrator/planner.js +54 -9
- package/dist/orchestrator/qa.js +71 -15
- package/dist/orchestrator/radar.js +719 -0
- package/dist/orchestrator/scheduler.js +575 -50
- package/dist/orchestrator/takeover.js +155 -0
- package/dist/platform.js +377 -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 +225 -0
- package/dist/room/progress.js +40 -1
- package/dist/room/server.js +220 -39
- package/dist/room/tunnel.js +6 -5
- package/dist/room/user.js +54 -0
- package/dist/sandbox/sync.js +4 -4
- package/dist/sandbox/worktree.js +145 -14
- package/dist/ui/app.js +963 -159
- package/dist/ui/brief.js +494 -0
- package/dist/ui/catalog.js +62 -24
- package/dist/ui/dashboard.js +269 -55
- package/dist/ui/doctor.js +15 -11
- package/dist/ui/export.js +1194 -0
- package/dist/ui/lessons.js +81 -0
- package/dist/ui/narrator.js +56 -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 +716 -94
- package/dist/ui/talk.js +176 -0
- package/dist/ui/term.js +182 -16
- package/dist/ui/transcript.js +434 -0
- package/dist/ui/watch.js +149 -48
- package/package.json +6 -3
package/dist/ui/story.js
CHANGED
|
@@ -13,11 +13,11 @@
|
|
|
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). */
|
|
20
|
-
const CORE_RUN = new Set(["start", "declined", "tokens", "review", "reviewed", "merged", "failed", "stuck", "conflict", "resolved", "handoff"]);
|
|
20
|
+
export const CORE_RUN = new Set(["start", "declined", "tokens", "review", "reviewed", "merged", "failed", "stuck", "conflict", "resolved", "handoff"]);
|
|
21
21
|
const NOT_AGENTS = new Set(["huddle", "room", "all", "you", "council", ""]);
|
|
22
22
|
function parse(e) {
|
|
23
23
|
let p = {};
|
|
@@ -40,9 +40,9 @@ function parse(e) {
|
|
|
40
40
|
delete p.type;
|
|
41
41
|
return { ts: e.ts, type, agent: e.agent_id, task: e.task_id, p };
|
|
42
42
|
}
|
|
43
|
-
const str = (v) => (v === undefined || v === null ? "" : typeof v === "string" ? v : typeof v === "object" ? JSON.stringify(v) : String(v));
|
|
43
|
+
export const str = (v) => (v === undefined || v === null ? "" : typeof v === "string" ? v : typeof v === "object" ? JSON.stringify(v) : String(v));
|
|
44
44
|
/** First non-empty field among `keys`. */
|
|
45
|
-
const pick = (p, ...keys) => {
|
|
45
|
+
export const pick = (p, ...keys) => {
|
|
46
46
|
for (const k of keys) {
|
|
47
47
|
const v = p[k];
|
|
48
48
|
if (v !== undefined && v !== null && str(v).trim())
|
|
@@ -50,7 +50,7 @@ const pick = (p, ...keys) => {
|
|
|
50
50
|
}
|
|
51
51
|
return "";
|
|
52
52
|
};
|
|
53
|
-
const oneLine = (s, n = 140) => {
|
|
53
|
+
export const oneLine = (s, n = 140) => {
|
|
54
54
|
const l = s.replace(/\s+/g, " ").trim();
|
|
55
55
|
return l.length <= n ? l : l.slice(0, n - 1) + "…";
|
|
56
56
|
};
|
|
@@ -74,7 +74,7 @@ function qaEyes(checkers, authors) {
|
|
|
74
74
|
return "(didn't write it)";
|
|
75
75
|
return mine.length === checkers.length ? "(wrote some of it too — checking anyway)" : `(${mine.join(", ")} wrote some of it too)`;
|
|
76
76
|
}
|
|
77
|
-
const list = (v) => {
|
|
77
|
+
export const list = (v) => {
|
|
78
78
|
if (Array.isArray(v))
|
|
79
79
|
return v.map(str);
|
|
80
80
|
if (typeof v === "string") {
|
|
@@ -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;
|
|
@@ -329,6 +373,8 @@ export function buildStory(run, runs = 1) {
|
|
|
329
373
|
case "memory.added": {
|
|
330
374
|
const author = pick(p, "author") || e.agent || "";
|
|
331
375
|
const body = pick(p, "body");
|
|
376
|
+
if (author === "you")
|
|
377
|
+
break; // your words: already their message or answer
|
|
332
378
|
if (author === "huddle") {
|
|
333
379
|
// Mostly the scheduler restating what run.* says; the red gate is news.
|
|
334
380
|
if (/red test gate/.test(body))
|
|
@@ -358,6 +404,18 @@ export function buildStory(run, runs = 1) {
|
|
|
358
404
|
// Huddle's automatic help offer, sent in a free agent's name: a board event,
|
|
359
405
|
// not a message the agent wrote. Found live (capstone round 2): 6 offers
|
|
360
406
|
// and no real message, yet the table said claude sent 3 messages.
|
|
407
|
+
// The radar's mail: the run.radar item already tells it, once per finding.
|
|
408
|
+
if (from === "huddle" && /^Huddle radar:/.test(pick(p, "body")))
|
|
409
|
+
break;
|
|
410
|
+
// Buddy notes and contract broadcasts: run.checkpoint / contract.amended tell them once.
|
|
411
|
+
if (from === "huddle" && /^buddy check from /.test(pick(p, "body")))
|
|
412
|
+
break;
|
|
413
|
+
if (/^contract v\d+ by /.test(pick(p, "body")))
|
|
414
|
+
break;
|
|
415
|
+
if (from === "huddle" && / is taking “.*” \(files .*\) in \d+s unless you reply/.test(pick(p, "body"))) {
|
|
416
|
+
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)` });
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
361
419
|
if (isHelpOffer(from, pick(p, "body"))) {
|
|
362
420
|
const t = /your task "(.*)" \(/.exec(pick(p, "body"))?.[1];
|
|
363
421
|
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)` });
|
|
@@ -396,6 +454,19 @@ export function buildStory(run, runs = 1) {
|
|
|
396
454
|
st(to).received++;
|
|
397
455
|
break;
|
|
398
456
|
}
|
|
457
|
+
// An agent's question for the person at the keyboard (huddle_ask_user), and what came back.
|
|
458
|
+
case "user.ask": {
|
|
459
|
+
const from = pick(p, "from") || e.agent || "?";
|
|
460
|
+
const opts = list(p.options);
|
|
461
|
+
add({ ts, kind: "message", type: e.type, agent: from, to: "you", text: oneLine(pick(p, "question"), 400), detail: `question for you · ${opts.map((o, i) => `${i + 1}. ${o}`).join(" · ")}` });
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
case "user.answer": {
|
|
465
|
+
const to = pick(p, "from") || e.agent || "?";
|
|
466
|
+
const mine = pick(p, "by") === "you";
|
|
467
|
+
add({ ts, kind: "message", type: e.type, agent: mine ? "you" : "huddle", to, text: oneLine(pick(p, "answer"), 400), detail: mine ? "your answer" : "no answer in time · went with option 1" });
|
|
468
|
+
break;
|
|
469
|
+
}
|
|
399
470
|
case "handoff": {
|
|
400
471
|
const from = pick(p, "from") || e.agent;
|
|
401
472
|
const to = pick(p, "to");
|
|
@@ -486,6 +557,7 @@ export function buildStory(run, runs = 1) {
|
|
|
486
557
|
merged++;
|
|
487
558
|
if (/^Fix: /.test(title(e.task, p)))
|
|
488
559
|
s.bugsFixed++; // a QA bug's fix task landed
|
|
560
|
+
mergedTitles.push({ title: title(e.task, p), agent: a });
|
|
489
561
|
close(a, ts, "ok");
|
|
490
562
|
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
563
|
break;
|
|
@@ -505,6 +577,14 @@ export function buildStory(run, runs = 1) {
|
|
|
505
577
|
case "run.stuck":
|
|
506
578
|
add({ ts, kind: "fail", type: e.type, agent: null, text: `! blocked by failures: ${list(p.blocked).join(" · ")}` });
|
|
507
579
|
break;
|
|
580
|
+
case "busy": // quiet past the silence window, but working: not taken over (busy.ts)
|
|
581
|
+
add({ ts, kind: "progress", type: e.type, agent: pick(p, "agent") || e.agent, text: `⧗ running a long command: quiet ${Math.round((Number(p.quietMs) || 0) / 60_000) || "<1"} min, but busy — not taken over`, detail: oneLine(pick(p, "why"), 200) || undefined });
|
|
582
|
+
break;
|
|
583
|
+
case "budget": {
|
|
584
|
+
const left = list(p.left);
|
|
585
|
+
add({ ts, kind: "fail", type: e.type, agent: null, text: `! token budget ${p.level === "hard" ? "ran over: agents stopped" : "nearly spent: no new work started"} (${Math.round(((Number(p.tokens) || 0) / (Number(p.limit) || 1)) * 100)}% of ${fmtTokens(Number(p.limit) || 0)})`, detail: left.length ? `left undone: ${left.join(" · ")}` : undefined });
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
508
588
|
case "run.conflict": {
|
|
509
589
|
const a = pick(p, "agent") || e.agent || "?";
|
|
510
590
|
const author = pick(p, "author");
|
|
@@ -530,7 +610,11 @@ export function buildStory(run, runs = 1) {
|
|
|
530
610
|
const a = pick(p, "agent", "from", "by", "critic") || e.agent;
|
|
531
611
|
const of = pick(p, "of", "target", "to", "author");
|
|
532
612
|
st(a).critiques++;
|
|
533
|
-
|
|
613
|
+
// Council v2: the ADOPT / FLAW lines get their own items (council.adopt / .flaw); here, just where it landed.
|
|
614
|
+
const revised = Array.isArray(p.adopts) || Array.isArray(p.flaws) ? /REVISED\W*:?\s*([\s\S]*)$/i.exec(pick(p, "text"))?.[1] : undefined;
|
|
615
|
+
const it = revised
|
|
616
|
+
? { ts, kind: "design", type: e.type, agent: a, to: null, text: `revised position: ${oneLine(revised, 300)}` }
|
|
617
|
+
: { 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
618
|
add(it);
|
|
535
619
|
talked(a, of);
|
|
536
620
|
thread(it);
|
|
@@ -541,7 +625,19 @@ export function buildStory(run, runs = 1) {
|
|
|
541
625
|
plan.by = plan.by ?? (by ? `${by} (council)` : "council");
|
|
542
626
|
const tasks = Array.isArray(p.tasks) ? p.tasks : [];
|
|
543
627
|
councilTasks.push(...tasks);
|
|
544
|
-
|
|
628
|
+
const sy = p.synthesizer && typeof p.synthesizer === "object" ? p.synthesizer : null;
|
|
629
|
+
if (sy)
|
|
630
|
+
collab.synthesizer = { id: str(sy.id), kind: str(sy.kind), why: str(sy.why) };
|
|
631
|
+
for (const o of Array.isArray(p.objections) ? p.objections : [])
|
|
632
|
+
collab.objections.push({ by: str(o?.by), objection: str(o?.objection), taken: o?.taken === true });
|
|
633
|
+
add({
|
|
634
|
+
ts,
|
|
635
|
+
kind: "design",
|
|
636
|
+
type: e.type,
|
|
637
|
+
agent: by,
|
|
638
|
+
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) : ""}`,
|
|
639
|
+
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,
|
|
640
|
+
});
|
|
545
641
|
break;
|
|
546
642
|
}
|
|
547
643
|
case "task.takeover": {
|
|
@@ -564,6 +660,19 @@ export function buildStory(run, runs = 1) {
|
|
|
564
660
|
break;
|
|
565
661
|
case "offer": // the help offer itself is an agent.message in the busy agent's inbox
|
|
566
662
|
break;
|
|
663
|
+
case "radar.finding": // the Room's copy of run.radar
|
|
664
|
+
break;
|
|
665
|
+
case "radar.stats": {
|
|
666
|
+
const prev = collab.radarLooks ?? { ticks: 0, looks: 0, pairs: 0 };
|
|
667
|
+
collab.radarLooks = { ticks: prev.ticks + (Number(p.ticks) || 0), looks: prev.looks + (Number(p.looks) || 0), pairs: prev.pairs + (Number(p.pairs) || 0) };
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
case "radar": {
|
|
671
|
+
const w = pick(p, "with");
|
|
672
|
+
collab.radar.push({ agent: e.agent ?? pick(p, "agent"), with: w, kind: pick(p, "kind"), detail: pick(p, "detail") });
|
|
673
|
+
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 });
|
|
674
|
+
break;
|
|
675
|
+
}
|
|
567
676
|
case "takeover": {
|
|
568
677
|
if (roomTakeovers) {
|
|
569
678
|
// The first agent's time and tokens on the task it handed over.
|
|
@@ -591,6 +700,95 @@ export function buildStory(run, runs = 1) {
|
|
|
591
700
|
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
701
|
break;
|
|
593
702
|
}
|
|
703
|
+
case "checkpoint": {
|
|
704
|
+
// A buddy check: another agent read the author's work in progress.
|
|
705
|
+
const a = pick(p, "agent") || e.agent;
|
|
706
|
+
const author = pick(p, "author");
|
|
707
|
+
const notes = pick(p, "notes");
|
|
708
|
+
const ok = !notes || /^LOOKS GOOD/i.test(notes.trim());
|
|
709
|
+
const late = !ok && !!p.late;
|
|
710
|
+
collab.buddy.push({ agent: a ?? "", author, pct: Number(p.pct) || 0, notes: ok ? "" : notes, ...(late ? { late } : {}) });
|
|
711
|
+
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) };
|
|
712
|
+
add(it);
|
|
713
|
+
if (!ok) {
|
|
714
|
+
talked(a, author);
|
|
715
|
+
thread(it);
|
|
716
|
+
}
|
|
717
|
+
break;
|
|
718
|
+
}
|
|
719
|
+
// A note that needed an answer, settled when its task finished (src/room/followup.ts).
|
|
720
|
+
case "note.closed": {
|
|
721
|
+
const to = pick(p, "to") || e.agent || "";
|
|
722
|
+
const by = pick(p, "by");
|
|
723
|
+
const how = pick(p, "how") || "ignored";
|
|
724
|
+
const kind = pick(p, "kind") || "buddy";
|
|
725
|
+
collab.followups.push({ to, by, kind, how, note: pick(p, "body"), ...(title(e.task) ? { task: title(e.task) } : {}) });
|
|
726
|
+
const what = kind === "radar" ? "the radar's finding" : `${by}'s buddy note`;
|
|
727
|
+
const verb = how === "replied" ? "answers" : "addresses (in code)";
|
|
728
|
+
add({
|
|
729
|
+
ts,
|
|
730
|
+
kind: how === "ignored" ? "fail" : "review",
|
|
731
|
+
type: e.type,
|
|
732
|
+
agent: to || null,
|
|
733
|
+
to: kind === "radar" ? null : by || null,
|
|
734
|
+
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))}`,
|
|
735
|
+
detail: oneLine(pick(p, "body"), 300) || undefined,
|
|
736
|
+
});
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
739
|
+
case "followup": // the scheduler's copy of note.closed
|
|
740
|
+
case "note.open": // the note itself is an agent.message; run.checkpoint / run.radar tell it
|
|
741
|
+
case "buddy.start":
|
|
742
|
+
case "buddy.end":
|
|
743
|
+
break;
|
|
744
|
+
case "done.refused": {
|
|
745
|
+
const a = e.agent;
|
|
746
|
+
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` });
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
case "pair.dropped": {
|
|
750
|
+
const w = pick(p, "with");
|
|
751
|
+
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 });
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
case "pair": {
|
|
755
|
+
const a = pick(p, "agent") || e.agent;
|
|
756
|
+
const w = pick(p, "with");
|
|
757
|
+
collab.pairs.push({ agent: a ?? "", with: w, part: pick(p, "part") });
|
|
758
|
+
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 });
|
|
759
|
+
talked(a, w);
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
case "council.adopt":
|
|
763
|
+
case "council.flaw": {
|
|
764
|
+
// Cross-review: what one agent took from (or found wrong in) another's proposal.
|
|
765
|
+
const a = pick(p, "agent") || e.agent;
|
|
766
|
+
const of = pick(p, "of");
|
|
767
|
+
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)}` };
|
|
768
|
+
add(it);
|
|
769
|
+
talked(a, of);
|
|
770
|
+
(e.type === "council.adopt" ? collab.adopts : collab.flaws).push({ by: a ?? "", of, text: pick(p, "body", "text") });
|
|
771
|
+
break;
|
|
772
|
+
}
|
|
773
|
+
case "council.ratify": {
|
|
774
|
+
const a = pick(p, "agent") || e.agent;
|
|
775
|
+
const obj = list(p.objections);
|
|
776
|
+
collab.ratify.push({ agent: a ?? "", verdict: pick(p, "verdict") || (obj.length ? "object" : "ok"), objections: obj });
|
|
777
|
+
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)" : ""}` });
|
|
778
|
+
break;
|
|
779
|
+
}
|
|
780
|
+
case "contract.set":
|
|
781
|
+
if (pick(p, "text"))
|
|
782
|
+
collab.contract = { by: pick(p, "by") || e.agent || "council", chars: str(p.text).length };
|
|
783
|
+
if (pick(p, "text"))
|
|
784
|
+
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` });
|
|
785
|
+
break;
|
|
786
|
+
case "contract.amended":
|
|
787
|
+
collab.amendments.push({ by: pick(p, "by") || e.agent || "?", version: Number(p.version) || 0, change: `${pick(p, "section")}: ${pick(p, "change")}` });
|
|
788
|
+
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 });
|
|
789
|
+
break;
|
|
790
|
+
case "run.contract": // the Room's contract.amended says the same
|
|
791
|
+
break;
|
|
594
792
|
case "council.skipped": {
|
|
595
793
|
const a = pick(p, "agent") || e.agent;
|
|
596
794
|
add({ ts, kind: "design", type: e.type, agent: a, text: `${pick(p, "round") || "council"} skipped (${pick(p, "reason") || "no answer"})` });
|
|
@@ -616,7 +814,7 @@ export function buildStory(run, runs = 1) {
|
|
|
616
814
|
const a = pick(p, "agent", "by", "checker") || list(p.agents)[0] || e.agent;
|
|
617
815
|
if (isAgent(a))
|
|
618
816
|
begin({ agent: a, from: ts, kind: "qa", label: "QA" });
|
|
619
|
-
add({ ts, kind: "qa", type: e.type, agent: a, text:
|
|
817
|
+
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
818
|
break;
|
|
621
819
|
}
|
|
622
820
|
case "qa.bug": {
|
|
@@ -625,10 +823,14 @@ export function buildStory(run, runs = 1) {
|
|
|
625
823
|
if (isAgent(a))
|
|
626
824
|
close(a, ts, "warn");
|
|
627
825
|
const owner2 = pick(p, "to", "owner", "assigned_to", "author");
|
|
628
|
-
|
|
826
|
+
collab.bugs.push({ by: a ?? "", to: owner2, title: pick(p, "title", "summary", "bug", "body") });
|
|
827
|
+
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
828
|
talked(a, owner2);
|
|
630
829
|
break;
|
|
631
830
|
}
|
|
831
|
+
case "qa.suspect":
|
|
832
|
+
add({ ts, kind: "qa", type: e.type, agent: null, text: `? QA round ${str(p.round)} clean, but quick · ${str(p.reason)}` });
|
|
833
|
+
break;
|
|
632
834
|
case "qa.unchecked":
|
|
633
835
|
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
836
|
break;
|
|
@@ -643,7 +845,10 @@ export function buildStory(run, runs = 1) {
|
|
|
643
845
|
case "qa.fix": {
|
|
644
846
|
const a = pick(p, "agent", "by", "fixed_by") || e.agent;
|
|
645
847
|
st(a).bugsFixed++;
|
|
646
|
-
|
|
848
|
+
const fixed = collab.bugs.find((b) => !b.fixedBy && b.title === (pick(p, "title", "summary", "bug", "body") || title(e.task)));
|
|
849
|
+
if (fixed && a)
|
|
850
|
+
fixed.fixedBy = a;
|
|
851
|
+
add({ ts, kind: "qa", type: e.type, agent: a, text: `✓ fixed: ${oneLine(pick(p, "title", "summary", "bug", "body") || title(e.task), 200)}` });
|
|
647
852
|
break;
|
|
648
853
|
}
|
|
649
854
|
default: {
|
|
@@ -663,28 +868,183 @@ export function buildStory(run, runs = 1) {
|
|
|
663
868
|
}
|
|
664
869
|
for (const a of [...open.keys()])
|
|
665
870
|
close(a, run.end);
|
|
871
|
+
// A QA bug is fixed when its "Fix: <title>" task merged (the title may be cut on the board).
|
|
872
|
+
for (const b of collab.bugs) {
|
|
873
|
+
const want = oneLine(b.title, 60).replace(/…$/, "");
|
|
874
|
+
const hit = mergedTitles.find((m) => m.title.startsWith("Fix: ") && m.title.slice(5).startsWith(want.slice(0, 40)));
|
|
875
|
+
if (hit)
|
|
876
|
+
b.fixedBy = hit.agent;
|
|
877
|
+
}
|
|
666
878
|
// Who proposed what: a council plan's tasks may say whose idea each one was.
|
|
667
879
|
for (const t of councilTasks) {
|
|
668
880
|
const pt = plan.tasks.find((x) => x.title === pick(t ?? {}, "title"));
|
|
669
|
-
const
|
|
881
|
+
const from = list((t ?? {}).proposed_by ?? (t ?? {}).by ?? (t ?? {}).from ?? (t ?? {}).author);
|
|
882
|
+
const primary = pick(t ?? {}, "primary") || from[0] || "";
|
|
883
|
+
if (primary)
|
|
884
|
+
collab.credit.set(primary, (collab.credit.get(primary) ?? 0) + 1);
|
|
885
|
+
const rest = from.filter((x) => x !== primary);
|
|
886
|
+
const who = primary ? `${primary}${rest.length ? ` (with ${rest.join(", ")})` : ""}` : "";
|
|
670
887
|
if (pt && who && !pt.by)
|
|
671
888
|
pt.by = who;
|
|
672
889
|
}
|
|
673
|
-
return { run, runs, agents, plan, design, items, spans, stats, talk, threads, merged, failed };
|
|
890
|
+
return { run, runs, agents, plan, design, items, spans, stats, talk, threads, screen: [...screen.values()], merged, failed, collab };
|
|
891
|
+
}
|
|
892
|
+
// ---- teamwork summary (terminal + HTML) -------------------------------------------
|
|
893
|
+
/** The run's collaboration as labelled lines; [] when nobody worked with anybody. */
|
|
894
|
+
export function collabLines(s) {
|
|
895
|
+
const c = s.collab;
|
|
896
|
+
const out = [];
|
|
897
|
+
const n = (k, one, many = one + "s") => `${k} ${k === 1 ? one : many}`;
|
|
898
|
+
for (const a of c.adopts.slice(0, 6))
|
|
899
|
+
out.push({ label: "adopted", text: `${a.by} took ${a.of}'s idea: ${oneLine(a.text, 110)}` });
|
|
900
|
+
if (c.adopts.length > 6)
|
|
901
|
+
out.push({ label: "adopted", text: `…and ${c.adopts.length - 6} more` });
|
|
902
|
+
for (const f of c.flaws.slice(0, 6))
|
|
903
|
+
out.push({ label: "flaw", text: `${f.by} on ${f.of}'s idea: ${oneLine(f.text, 110)}` });
|
|
904
|
+
if (c.flaws.length > 6)
|
|
905
|
+
out.push({ label: "flaw", text: `…and ${c.flaws.length - 6} more` });
|
|
906
|
+
if (c.synthesizer)
|
|
907
|
+
out.push({ label: "plan by", text: `${c.synthesizer.id}${c.synthesizer.why ? ` — ${oneLine(c.synthesizer.why, 110)}` : ""}` });
|
|
908
|
+
if (c.ratify.length) {
|
|
909
|
+
const ok = c.ratify.filter((r) => r.verdict !== "object").map((r) => r.agent);
|
|
910
|
+
const obj = c.ratify.filter((r) => r.verdict === "object");
|
|
911
|
+
out.push({ label: "ratify", text: [ok.length ? `${ok.join(", ")} ok` : "", ...obj.map((r) => `${r.agent} objects: ${oneLine(r.objections.join(" · "), 90)}`)].filter(Boolean).join(" · ") });
|
|
912
|
+
}
|
|
913
|
+
for (const o of c.objections.slice(0, 4))
|
|
914
|
+
out.push({ label: "revision", text: `${o.taken ? "took" : "kept the plan over"} ${o.by}'s objection: ${oneLine(o.objection, 100)}` });
|
|
915
|
+
if (c.contract || c.amendments.length) {
|
|
916
|
+
const v = c.amendments.length ? Math.max(...c.amendments.map((a) => a.version)) : 1;
|
|
917
|
+
out.push({ label: "contract", text: `v${v}${c.contract ? ` · written by ${c.contract.by}` : " · started mid-run"} · ${n(c.amendments.length, "amendment")}` });
|
|
918
|
+
for (const a of c.amendments.slice(-4))
|
|
919
|
+
out.push({ label: "", text: `v${a.version} ${a.by}: ${oneLine(a.change, 110)}` });
|
|
920
|
+
}
|
|
921
|
+
if (!c.radar.length && c.radarLooks?.looks)
|
|
922
|
+
out.push({ label: "radar", text: `no catches · compared 2+ agents' work in progress ${n(c.radarLooks.looks, "time")} (${n(c.radarLooks.pairs, "pair")})` });
|
|
923
|
+
if (c.radar.length) {
|
|
924
|
+
out.push({ label: "radar", text: `${n(c.radar.length, "catch", "catches")} while agents worked` });
|
|
925
|
+
for (const r of c.radar.slice(0, 4))
|
|
926
|
+
out.push({ label: "", text: `${r.kind === "overlap" ? "overlap" : "mismatch"} ${r.agent} ↔ ${r.with}: ${oneLine(r.detail, 100)}` });
|
|
927
|
+
}
|
|
928
|
+
if (c.buddy.length) {
|
|
929
|
+
const sent = c.buddy.filter((b) => b.notes);
|
|
930
|
+
const late = sent.filter((b) => b.late).length;
|
|
931
|
+
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)` : ""}` });
|
|
932
|
+
for (const b of sent.slice(0, 4))
|
|
933
|
+
out.push({ label: "", text: `${b.agent} → ${b.author} at ${b.pct}%: ${oneLine(b.notes, 100)}` });
|
|
934
|
+
}
|
|
935
|
+
if (c.followups.length) {
|
|
936
|
+
const k = (h) => c.followups.filter((f) => f.how === h).length;
|
|
937
|
+
const ignored = c.followups.filter((f) => f.how === "ignored");
|
|
938
|
+
out.push({ label: "answered", text: `${n(c.followups.length - ignored.length, "note")} addressed (${k("fixed")} fixed, ${k("replied")} replied) · ${ignored.length} ignored` });
|
|
939
|
+
for (const f of ignored.slice(0, 4))
|
|
940
|
+
out.push({ label: "", text: `ignored by ${f.to}: ${f.kind === "radar" ? "radar" : f.by}: ${oneLine(f.note.replace(/^buddy check from [^:]*:\s*/i, ""), 100)}` });
|
|
941
|
+
}
|
|
942
|
+
for (const p of c.pairs.slice(0, 4))
|
|
943
|
+
out.push({ label: "pair", text: `${p.agent} helped ${p.with}: ${oneLine(p.part, 110)}` });
|
|
944
|
+
if (c.credit.size && (c.adopts.length || c.credit.size > 1))
|
|
945
|
+
out.push({ label: "credit", text: [...c.credit.entries()].sort((a, b) => b[1] - a[1]).map(([a, k]) => `${a} ${n(k, "task")}`).join(" · ") });
|
|
946
|
+
return out;
|
|
947
|
+
}
|
|
948
|
+
export function crewRows(s) {
|
|
949
|
+
const done = s.items.filter((it) => it.type === "qa.done");
|
|
950
|
+
return s.agents.map((a) => {
|
|
951
|
+
const x = s.stats.get(a);
|
|
952
|
+
const qa = done.length ? done.filter((it) => it.agent === a).length : s.items.filter((it) => it.type === "qa.round" && it.agent === a).length;
|
|
953
|
+
return { agent: a, merged: x.merged, failed: x.failed, reviews: x.reviews, qa, ms: x.ms, tokens: x.tokens, cached: x.cached };
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
export const plural = (k, one, many = one + "s") => `${k} ${k === 1 ? one : many}`;
|
|
957
|
+
/** "5 merged · 3 reviews · 1 QA" — what an agent did, nothing for what it didn't. */
|
|
958
|
+
export function didLine(c) {
|
|
959
|
+
const parts = [`${c.merged} merged`, c.failed ? `${c.failed} failed` : "", c.reviews ? plural(c.reviews, "review") : "", c.qa ? `${c.qa} QA` : ""];
|
|
960
|
+
return parts.filter(Boolean).join(" · ");
|
|
961
|
+
}
|
|
962
|
+
/** The run cut into its phases: the build, then QA (from the first QA round on). */
|
|
963
|
+
export function phases(s) {
|
|
964
|
+
// The council's decisions and risks, written to shared memory as the build starts: part of the plan.
|
|
965
|
+
const agreed = s.items.filter((it) => it.agent === "council" && it.type === "memory.added");
|
|
966
|
+
const rest = s.items.filter((it) => !agreed.includes(it));
|
|
967
|
+
const i = rest.findIndex((it) => it.type === "qa.round");
|
|
968
|
+
return i < 0 ? { agreed, build: rest, qa: [] } : { agreed, build: rest.slice(0, i), qa: rest.slice(i) };
|
|
969
|
+
}
|
|
970
|
+
/** A council decision or risk from shared memory, as "decided" / "risk (by)". */
|
|
971
|
+
export function agreement(it) {
|
|
972
|
+
const risk = /^gotcha: council risk:/.test(it.text);
|
|
973
|
+
return { risk, by: risk ? /flagged by (\S+)/.exec(it.detail ?? "")?.[1] : undefined, body: it.text.replace(/^\w+: council( risk)?:\s*/, "") };
|
|
974
|
+
}
|
|
975
|
+
/** The council is recorded in one go when it's done: its items share one instant, so they get no clock. */
|
|
976
|
+
const councilInstant = (s) => s.design.length > 1 && s.design.at(-1).ts - s.design[0].ts < 2000;
|
|
977
|
+
/** How the run ended: totals, merges gated on tests, QA's last word. */
|
|
978
|
+
export function outcome(s) {
|
|
979
|
+
const rows = crewRows(s);
|
|
980
|
+
const tokens = rows.reduce((n, c) => n + c.tokens, 0);
|
|
981
|
+
const cached = rows.reduce((n, c) => n + c.cached, 0);
|
|
982
|
+
const merges = s.items.filter((it) => it.type === "run.merged");
|
|
983
|
+
const green = merges.filter((it) => /tests? (green|pass)/i.test(it.detail ?? "")).length;
|
|
984
|
+
const last = [...s.items].reverse().find((it) => ["qa.clean", "qa.capped", "qa.unchecked"].includes(it.type));
|
|
985
|
+
const qa = last ? { text: last.text.replace(/^[✓!]\s*/, ""), tone: last.type === "qa.clean" ? "ok" : "warn" } : null;
|
|
986
|
+
const rounds = s.items.filter((it) => it.type === "qa.round").length;
|
|
987
|
+
return { tokens, cached, merges: merges.length, green, qa, rounds, ms: s.run.end - s.run.start };
|
|
988
|
+
}
|
|
989
|
+
/** The teamwork as numbers, for the summary line and the HTML tiles. Only what happened (or was looked for). */
|
|
990
|
+
export function tally(s) {
|
|
991
|
+
const c = s.collab;
|
|
992
|
+
const out = [];
|
|
993
|
+
if (c.adopts.length || c.flaws.length) {
|
|
994
|
+
out.push({ n: String(c.adopts.length), label: c.adopts.length === 1 ? "idea adopted" : "ideas adopted", tone: "ok" });
|
|
995
|
+
out.push({ n: String(c.flaws.length), label: c.flaws.length === 1 ? "flaw flagged" : "flaws flagged" });
|
|
996
|
+
}
|
|
997
|
+
if (c.objections.length)
|
|
998
|
+
out.push({ n: `${c.objections.filter((o) => o.taken).length}/${c.objections.length}`, label: "objections taken", tone: "ok" });
|
|
999
|
+
if (c.contract || c.amendments.length) {
|
|
1000
|
+
const v = c.amendments.length ? Math.max(...c.amendments.map((a) => a.version)) : 1;
|
|
1001
|
+
out.push({ n: `v${v}`, label: `contract, ${plural(c.amendments.length, "amendment")}`, text: `contract v${v} (${plural(c.amendments.length, "amendment")})` });
|
|
1002
|
+
}
|
|
1003
|
+
if (c.buddy.length)
|
|
1004
|
+
out.push({ n: String(c.buddy.length), label: `buddy checks, ${plural(c.buddy.filter((b) => b.notes).length, "note")}` });
|
|
1005
|
+
if (c.followups.length) {
|
|
1006
|
+
const ignored = c.followups.filter((f) => f.how === "ignored").length;
|
|
1007
|
+
out.push({ n: `${c.followups.length - ignored}/${c.followups.length}`, label: "notes answered", tone: ignored ? "warn" : "ok" });
|
|
1008
|
+
}
|
|
1009
|
+
if (c.radar.length || c.radarLooks?.looks)
|
|
1010
|
+
out.push({ n: String(c.radar.length), label: c.radar.length === 1 ? "radar catch" : "radar catches", tone: c.radar.length ? "warn" : undefined });
|
|
1011
|
+
if (c.pairs.length)
|
|
1012
|
+
out.push({ n: String(c.pairs.length), label: c.pairs.length === 1 ? "pair" : "pairs" });
|
|
1013
|
+
if (c.bugs.length) {
|
|
1014
|
+
const fixed = c.bugs.filter((b) => b.fixedBy).length;
|
|
1015
|
+
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` });
|
|
1016
|
+
}
|
|
1017
|
+
const msgs = [...s.stats.values()].reduce((n, x) => n + x.sent, 0);
|
|
1018
|
+
if (msgs)
|
|
1019
|
+
out.push({ n: String(msgs), label: msgs === 1 ? "message between agents" : "messages between agents" });
|
|
1020
|
+
return out;
|
|
1021
|
+
}
|
|
1022
|
+
/** Each buddy note with how it ended: fixed in code, answered, ignored — or just sent. */
|
|
1023
|
+
export function buddyNotes(s) {
|
|
1024
|
+
const left = s.collab.followups.filter((f) => f.kind !== "radar");
|
|
1025
|
+
return s.collab.buddy
|
|
1026
|
+
.filter((b) => b.notes)
|
|
1027
|
+
.map((b) => {
|
|
1028
|
+
const i = left.findIndex((f) => f.by === b.agent && f.to === b.author);
|
|
1029
|
+
const f = i >= 0 ? left.splice(i, 1)[0] : undefined;
|
|
1030
|
+
const how = f ? (f.how === "ignored" ? "ignored" : f.how === "replied" ? "answered" : "fixed in code") : b.late ? "came back late" : "sent";
|
|
1031
|
+
const tone = f ? (f.how === "ignored" ? "bad" : "ok") : b.late ? "warn" : "";
|
|
1032
|
+
return { ...b, how, tone };
|
|
1033
|
+
});
|
|
674
1034
|
}
|
|
675
1035
|
// ---- terminal -----------------------------------------------------------------
|
|
676
|
-
const clock = (ms) => {
|
|
1036
|
+
export const clock = (ms) => {
|
|
677
1037
|
const s = Math.max(0, Math.round(ms / 1000));
|
|
678
1038
|
return `+${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
|
|
679
1039
|
};
|
|
680
1040
|
const kindMark = {
|
|
681
|
-
design:
|
|
1041
|
+
design: (s) => s,
|
|
682
1042
|
plan: C.violet,
|
|
683
1043
|
task: (s) => s,
|
|
684
1044
|
progress: C.faint,
|
|
685
1045
|
announce: C.faint,
|
|
686
1046
|
message: C.bold,
|
|
687
|
-
memory: C.
|
|
1047
|
+
memory: C.faint,
|
|
688
1048
|
handoff: C.violet,
|
|
689
1049
|
review: C.violet,
|
|
690
1050
|
merge: C.green,
|
|
@@ -695,24 +1055,83 @@ const kindMark = {
|
|
|
695
1055
|
team: C.faint,
|
|
696
1056
|
other: C.faint,
|
|
697
1057
|
};
|
|
698
|
-
|
|
1058
|
+
/** Parts joined with " · ", wrapped onto as many lines as `width` needs. */
|
|
1059
|
+
function wrapParts(parts, width, indent = " ") {
|
|
1060
|
+
const out = [];
|
|
1061
|
+
let cur = "";
|
|
1062
|
+
for (const p of parts) {
|
|
1063
|
+
const next = cur ? `${cur} · ${p}` : p;
|
|
1064
|
+
if (cur && indent.length + next.length > width) {
|
|
1065
|
+
out.push(indent + cur);
|
|
1066
|
+
cur = p;
|
|
1067
|
+
}
|
|
1068
|
+
else
|
|
1069
|
+
cur = next;
|
|
1070
|
+
}
|
|
1071
|
+
if (cur)
|
|
1072
|
+
out.push(indent + cur);
|
|
1073
|
+
return out;
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* The story in the terminal, in the live run's words: COUNCIL → PLAN → BUILD →
|
|
1077
|
+
* QA → RESULT. `width` (the terminal's columns) cuts every line to fit; without
|
|
1078
|
+
* it (a file, a pipe) lines are kept whole.
|
|
1079
|
+
*/
|
|
1080
|
+
export function renderText(s, opts = {}) {
|
|
699
1081
|
const out = [];
|
|
700
1082
|
const r = s.run;
|
|
1083
|
+
const W = opts.width ?? 120;
|
|
1084
|
+
const nameW = Math.max(6, ...s.agents.map((a) => a.length));
|
|
701
1085
|
const who = (a) => (a ? `${dot(a)} ${C.bold(a)}` : `${C.violet("◆")} ${C.faint("huddle")}`);
|
|
1086
|
+
const whoPad = (a) => (a ? `${dot(a)} ${C.bold(pad(a, nameW))}` : `${C.violet("◆")} ${C.faint(pad("huddle", nameW))}`);
|
|
1087
|
+
const indent = " ".repeat(2 + 6 + 1 + 2 + nameW + 1);
|
|
1088
|
+
const instant = councilInstant(s);
|
|
702
1089
|
const line = (it) => {
|
|
703
|
-
const head = it.to && it.kind === "message" ? `${who(it.agent)} ${C.faint("→")} ${who(it.to)}${C.faint(":")}` :
|
|
1090
|
+
const head = it.to && it.kind === "message" ? `${who(it.agent)} ${C.faint("→")} ${who(it.to)}${C.faint(":")}` : whoPad(it.agent);
|
|
704
1091
|
const tag = it.kind === "message" && it.detail ? C.faint(` (${it.detail})`) : "";
|
|
705
|
-
|
|
706
|
-
|
|
1092
|
+
const t = it.kind === "design" && instant ? "" : clock(it.ts - r.start);
|
|
1093
|
+
out.push(` ${C.faint(pad(t, 6))} ${head} ${kindMark[it.kind](it.text)}${tag}`);
|
|
1094
|
+
// Details that only repeat the task's title or a why, and specs of new tasks, stay in the HTML.
|
|
1095
|
+
const quiet = it.kind === "message" || it.kind === "progress" || it.type === "memory.added" || it.type === "task.added";
|
|
1096
|
+
if (it.detail && !quiet)
|
|
707
1097
|
for (const l of it.detail.split("\n").slice(0, 6))
|
|
708
|
-
out.push(C.faint(
|
|
1098
|
+
out.push(C.faint(`${indent}${l}`));
|
|
709
1099
|
};
|
|
1100
|
+
const head = (name, sub = "") => out.push("", `${C.violet(name)}${sub ? C.faint(" · " + sub) : ""}`);
|
|
1101
|
+
const o = outcome(s);
|
|
1102
|
+
const crew = crewRows(s);
|
|
710
1103
|
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
|
-
|
|
1104
|
+
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")}`));
|
|
1105
|
+
const mark = s.failed ? C.yellow("◎") : C.green("✓");
|
|
1106
|
+
out.push(` ${mark} ${s.merged} merged, ${s.failed} failed` +
|
|
1107
|
+
C.faint(`${o.merges ? ` · ${o.green}/${o.merges} merged on green tests` : ""}${o.tokens ? ` · ${fmtTokens(o.tokens)} tok` : ""}`));
|
|
1108
|
+
if (o.qa)
|
|
1109
|
+
out.push(` ${o.qa.tone === "ok" ? C.green("✓") : C.yellow("!")} ${o.qa.text}`);
|
|
1110
|
+
for (const c of crew)
|
|
1111
|
+
out.push(` ${dot(c.agent)} ${pad(c.agent, nameW)} ${didLine(c)}${C.faint(`${c.ms ? " · " + fmtDuration(c.ms) : ""}${c.tokens ? " · " + fmtTokens(c.tokens) + " tok" : ""}`)}`);
|
|
1112
|
+
const team = collabLines(s);
|
|
1113
|
+
const nums = tally(s);
|
|
1114
|
+
if (team.length || nums.length) {
|
|
1115
|
+
head("TEAMWORK", "who built on whom");
|
|
1116
|
+
out.push(...wrapParts(nums.map((t) => t.text ?? `${t.n} ${t.label}`), W).map((l) => C.bold(l)));
|
|
1117
|
+
// The terminal shows the first few ideas and flaws; the HTML has them all.
|
|
1118
|
+
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);
|
|
1119
|
+
const shown = new Map();
|
|
1120
|
+
for (const l of team) {
|
|
1121
|
+
const capped = l.label === "adopted" || l.label === "flaw";
|
|
1122
|
+
const k = (shown.get(l.label) ?? 0) + 1;
|
|
1123
|
+
shown.set(l.label, k);
|
|
1124
|
+
if (capped && k > 3) {
|
|
1125
|
+
if (k === 4)
|
|
1126
|
+
out.push(` ${C.faint(pad(l.label, 9))} ${C.faint(`…and ${total(l.label) - 3} more`)}`);
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
out.push(` ${C.faint(pad(l.label, 9))} ${l.text}`);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
714
1132
|
if (s.design.length) {
|
|
715
|
-
|
|
1133
|
+
const members = [...new Set(s.design.map((it) => it.agent).filter(Boolean))];
|
|
1134
|
+
head("COUNCIL", `${members.length ? members.join(", ") + " " : ""}designed it before anyone coded`);
|
|
716
1135
|
s.design.forEach(line);
|
|
717
1136
|
}
|
|
718
1137
|
if (s.plan.tasks.length) {
|
|
@@ -721,21 +1140,60 @@ export function renderText(s) {
|
|
|
721
1140
|
s.plan.tasks.forEach((t, i) => {
|
|
722
1141
|
const deps = t.deps.map((d) => "#" + (num.get(d) ?? d));
|
|
723
1142
|
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 ? `
|
|
1143
|
+
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
1144
|
.filter(Boolean)
|
|
726
1145
|
.map((x) => " · " + x)
|
|
727
1146
|
.join("")));
|
|
728
1147
|
});
|
|
1148
|
+
const c = s.collab;
|
|
1149
|
+
for (const it of phases(s).agreed) {
|
|
1150
|
+
const a = agreement(it);
|
|
1151
|
+
out.push(a.risk ? ` ${C.yellow("!")} ${C.faint(`risk${a.by ? ` (${a.by})` : ""}:`)} ${a.body}` : ` ${C.violet("◆")} ${C.faint("decided:")} ${a.body}`);
|
|
1152
|
+
}
|
|
1153
|
+
if (c.contract) {
|
|
1154
|
+
const taken = c.objections.filter((x) => x.taken).length;
|
|
1155
|
+
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`)}`);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
if (s.screen.length) {
|
|
1159
|
+
head("SHARED SCREEN", "one copy every agent reads");
|
|
1160
|
+
for (const x of s.screen) {
|
|
1161
|
+
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(""))}`);
|
|
1162
|
+
out.push(C.faint(` read by: ${x.readers.length ? x.readers.join(", ") : "nobody"}`));
|
|
1163
|
+
for (const n of x.notes)
|
|
1164
|
+
out.push(` 📌 ${C.bold(n.by)} ${oneLine(n.body, 160)}`);
|
|
1165
|
+
}
|
|
729
1166
|
}
|
|
730
|
-
|
|
1167
|
+
const ph = phases(s);
|
|
1168
|
+
head("BUILD", "agents code, check each other, merge");
|
|
731
1169
|
if (!s.items.length)
|
|
732
1170
|
out.push(C.faint(" (nothing happened in this run)"));
|
|
733
|
-
|
|
1171
|
+
ph.build.forEach(line);
|
|
1172
|
+
if (ph.qa.length) {
|
|
1173
|
+
head("QA", "agents re-check the merged build; bugs become fix tasks");
|
|
1174
|
+
ph.qa.forEach(line);
|
|
1175
|
+
}
|
|
1176
|
+
head("RESULT");
|
|
1177
|
+
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)` : ""}`)}`);
|
|
1178
|
+
if (o.merges)
|
|
1179
|
+
out.push(` ${o.green === o.merges ? C.green("✓") : C.yellow("!")} ${o.green}/${o.merges} merges passed the tests first`);
|
|
1180
|
+
if (o.qa)
|
|
1181
|
+
out.push(` ${o.qa.tone === "ok" ? C.green("✓") : C.yellow("!")} ${plural(o.rounds, "QA round")} · ${o.qa.text}`);
|
|
734
1182
|
if (s.agents.length) {
|
|
735
|
-
out.push("", C.violet("WHO DID WHAT"));
|
|
736
|
-
const cols = [
|
|
737
|
-
|
|
738
|
-
|
|
1183
|
+
out.push("", `${C.violet("WHO DID WHAT")} ${C.faint("· msgs sent/got · bugs found/fixed · takeovers in/out")}`);
|
|
1184
|
+
const cols = [
|
|
1185
|
+
["merged", 7],
|
|
1186
|
+
["failed", 7],
|
|
1187
|
+
["reviews", 8],
|
|
1188
|
+
["msgs", 7],
|
|
1189
|
+
["bugs", 6],
|
|
1190
|
+
["takeover", 9],
|
|
1191
|
+
["tools", 6],
|
|
1192
|
+
["time", 8],
|
|
1193
|
+
["tokens", 7],
|
|
1194
|
+
];
|
|
1195
|
+
const w = nameW + 4; // "● name" + a gap
|
|
1196
|
+
out.push(C.faint(" " + pad("", w) + cols.map(([c, n]) => pad(c, n)).join("")));
|
|
739
1197
|
for (const a of s.agents) {
|
|
740
1198
|
const x = s.stats.get(a);
|
|
741
1199
|
const vals = [
|
|
@@ -743,16 +1201,15 @@ export function renderText(s) {
|
|
|
743
1201
|
x.failed,
|
|
744
1202
|
x.reviews + (x.changesAsked ? ` (${x.changesAsked}✎)` : ""),
|
|
745
1203
|
`${x.sent}/${x.received}`,
|
|
746
|
-
x.proposals || x.critiques ? `${x.proposals}p ${x.critiques}c` : "0",
|
|
747
1204
|
`${x.bugsFound}/${x.bugsFixed}`,
|
|
748
1205
|
`${x.takeoversIn}/${x.takeoversOut}`,
|
|
749
1206
|
x.toolCalls,
|
|
750
1207
|
x.ms ? fmtDuration(x.ms) : "—",
|
|
751
1208
|
x.tokens ? fmtTokens(x.tokens) : "—",
|
|
752
1209
|
].map(String);
|
|
753
|
-
out.push(` ${pad(`${dot(a)} ${a}`, w)}${vals.map((v, i) => pad(v,
|
|
1210
|
+
out.push(` ${pad(`${dot(a)} ${a}`, w)}${vals.map((v, i) => pad(v, cols[i][1])).join("")}`);
|
|
754
1211
|
}
|
|
755
|
-
out.push("", `${C.violet("WHO TALKED TO WHOM")} ${C.faint("
|
|
1212
|
+
out.push("", `${C.violet("WHO TALKED TO WHOM")} ${C.faint("· row → column: messages, reviews, critiques, handoffs")}`);
|
|
756
1213
|
const cw = Math.max(8, ...s.agents.map((a) => a.length + 2));
|
|
757
1214
|
out.push(" " + pad("", w) + s.agents.map((a) => pad(a, cw)).join(""));
|
|
758
1215
|
for (const a of s.agents) {
|
|
@@ -760,7 +1217,7 @@ export function renderText(s) {
|
|
|
760
1217
|
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
1218
|
}
|
|
762
1219
|
}
|
|
763
|
-
return out.join("\n");
|
|
1220
|
+
return (opts.width ? out.map((l) => truncate(l, Math.max(20, opts.width - 1))) : out).join("\n");
|
|
764
1221
|
}
|
|
765
1222
|
export function renderRunList(runs) {
|
|
766
1223
|
if (!runs.length)
|
|
@@ -774,14 +1231,175 @@ export function renderRunList(runs) {
|
|
|
774
1231
|
.join("\n");
|
|
775
1232
|
}
|
|
776
1233
|
// ---- HTML (a static, self-contained report file) --------------------------------
|
|
777
|
-
const esc = (s) => str(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
778
|
-
const colorOf = (a) => (a ? AGENT_COLOR[a.split("#")[0]] ?? "#8A857C" : "#7B6CF6");
|
|
1234
|
+
export const esc = (s) => str(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
1235
|
+
export const colorOf = (a) => (a ? AGENT_COLOR[a.split("#")[0]] ?? "#8A857C" : "#7B6CF6");
|
|
1236
|
+
/** Lines that are the agents thinking out loud, hidden behind a toggle so the story reads first. */
|
|
1237
|
+
const NOISE = (it) => it.kind === "progress" || it.kind === "announce" || it.kind === "memory" || it.type === "memory.added";
|
|
1238
|
+
/** The story page's look — shared with `huddle export --format html` (src/ui/export.ts). */
|
|
1239
|
+
export const STORY_CSS = `:root{--bg:#f6f5f1;--fg:#1d1b18;--mute:#6f6a62;--line:#e4e0d8;--card:#fff;--soft:#efede7;--ok:#23864c;--bad:#c93f30;--warn:#a86f05;--brand:#6a5af0;color-scheme:light}
|
|
1240
|
+
@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}}
|
|
1241
|
+
: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}
|
|
1242
|
+
*{box-sizing:border-box}
|
|
1243
|
+
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%}
|
|
1244
|
+
main{max-width:1100px;margin:0 auto;padding:24px 16px 60px}
|
|
1245
|
+
h1{font-size:26px;line-height:1.25;margin:6px 0 4px;word-break:break-word}
|
|
1246
|
+
h2{font-size:13px;text-transform:uppercase;letter-spacing:.08em;color:var(--brand);margin:36px 0 12px}
|
|
1247
|
+
h2 small{text-transform:none;letter-spacing:0}
|
|
1248
|
+
h3{font-size:14px;margin:0 0 10px}
|
|
1249
|
+
.brand{font-weight:700;color:var(--brand);font-size:13px;letter-spacing:.04em}
|
|
1250
|
+
.sub,small,.detail,time,.meta,.none,.legend{color:var(--mute)}small{font-size:12px;margin-left:6px;font-weight:400}
|
|
1251
|
+
.none{font-size:13px;margin:6px 0}
|
|
1252
|
+
.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}
|
|
1253
|
+
.hero{background:var(--card);border:1px solid var(--line);border-radius:16px;padding:20px}
|
|
1254
|
+
.hero details{margin:8px 0}
|
|
1255
|
+
.kpis,.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px;margin-top:16px}
|
|
1256
|
+
.kpi,.tile{background:var(--soft);border-radius:12px;padding:10px 12px;min-width:0}
|
|
1257
|
+
.kpi b,.tile b{display:block;font-size:24px;line-height:1.2;font-variant-numeric:tabular-nums}
|
|
1258
|
+
.kpi span,.tile span{font-size:12px;color:var(--mute)}
|
|
1259
|
+
.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)}
|
|
1260
|
+
.verdict{margin:14px 0 0;font-weight:600}.verdict.ok{color:var(--ok)}.verdict.warn{color:var(--warn)}
|
|
1261
|
+
.crew{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:10px;margin-top:12px}
|
|
1262
|
+
.agent{background:var(--card);border:1px solid var(--line);border-top:4px solid var(--c);border-radius:12px;padding:10px 14px;min-width:0}
|
|
1263
|
+
.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)}
|
|
1264
|
+
.agent .as{font-size:14px}.agent .am{font-size:12px;color:var(--mute);font-variant-numeric:tabular-nums}
|
|
1265
|
+
.cards{columns:320px 3;column-gap:12px;margin-top:12px}.cards .card{break-inside:avoid;margin-bottom:12px}
|
|
1266
|
+
.card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:14px 16px;min-width:0}
|
|
1267
|
+
.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}
|
|
1268
|
+
.list li.mute{color:var(--mute)}
|
|
1269
|
+
.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}
|
|
1270
|
+
.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)}
|
|
1271
|
+
.grid td,.grid th{text-align:center}.grid th:first-child{text-align:left}
|
|
1272
|
+
.ad,.fl{display:inline-block;min-width:22px;padding:1px 6px;margin:1px;border-radius:6px;font-weight:700;font-size:12px}
|
|
1273
|
+
.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)}
|
|
1274
|
+
.legend{font-size:12px;margin:8px 0}
|
|
1275
|
+
.quote{font-size:13px;padding:6px 0 6px 10px;border-left:3px solid var(--line);margin-top:6px;word-break:break-word}
|
|
1276
|
+
.scroll{overflow-x:auto;-webkit-overflow-scrolling:touch}
|
|
1277
|
+
.lanesbox{border:1px solid var(--line);border-radius:12px;background:var(--card)}
|
|
1278
|
+
.lanes{min-width:640px;padding:8px 12px 12px}
|
|
1279
|
+
.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%)}
|
|
1280
|
+
.lane{display:flex;align-items:center;height:38px;border-top:1px solid var(--line)}
|
|
1281
|
+
.lane .name{width:120px;flex:none;overflow:hidden;text-overflow:ellipsis;font-size:13px}
|
|
1282
|
+
.track{position:relative;flex:1;height:26px}
|
|
1283
|
+
.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}
|
|
1284
|
+
.bar.review{border-style:dashed}.bar.conflict{border-color:var(--warn)}.bar.qa{border-style:dotted}
|
|
1285
|
+
.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)}
|
|
1286
|
+
.mark{position:absolute;top:-3px;width:7px;height:7px;margin-left:-3px;border-radius:50%;background:var(--brand)}
|
|
1287
|
+
.mark.merge{background:var(--ok)}.mark.fail{background:var(--bad)}.mark.qa,.mark.takeover{background:var(--warn)}
|
|
1288
|
+
ol,ul{padding:0;margin:0;list-style:none}
|
|
1289
|
+
.phase{margin-top:22px}
|
|
1290
|
+
.phase h3{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;font-weight:400;color:var(--mute);font-size:13px}
|
|
1291
|
+
.tag{font-weight:800;letter-spacing:.08em;color:#fff;background:var(--brand);border-radius:6px;padding:2px 8px;font-size:12px}
|
|
1292
|
+
.timeline li{display:flex;gap:10px;padding:6px 0;font-size:14px;border-top:1px solid var(--line);min-width:0}
|
|
1293
|
+
.timeline li>div{min-width:0;word-break:break-word}
|
|
1294
|
+
.timeline time{flex:none;width:46px;font-variant-numeric:tabular-nums;font-size:13px}
|
|
1295
|
+
.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)}
|
|
1296
|
+
.timeline .progress .text,.timeline .announce .text,.timeline .team .text,.timeline .other .text,.timeline .memory .text{color:var(--mute)}
|
|
1297
|
+
#noise{position:absolute;opacity:0;pointer-events:none}
|
|
1298
|
+
.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}
|
|
1299
|
+
.toggle::before{content:"○ "}#noise:checked~.toggle::before{content:"● "}
|
|
1300
|
+
#noise:focus-visible~.toggle{outline:2px solid var(--brand)}
|
|
1301
|
+
.timeline li.noise{display:none}#noise:checked~.phases .timeline li.noise{display:flex}
|
|
1302
|
+
.arrow{color:var(--mute)}.detail{font-size:13px;white-space:pre-wrap;word-break:break-word}
|
|
1303
|
+
.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)}
|
|
1304
|
+
.contract{margin:10px 0 0}
|
|
1305
|
+
.result li{font-size:15px}.result li.ok{color:var(--ok)}.result li.warn{color:var(--warn)}
|
|
1306
|
+
details{border:1px solid var(--line);border-radius:12px;background:var(--card);margin:10px 0;padding:8px 12px}summary{cursor:pointer;font-weight:600}
|
|
1307
|
+
.thread{display:flex;flex-direction:column;gap:8px;margin-top:10px}
|
|
1308
|
+
.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}
|
|
1309
|
+
.thread li.r{align-self:flex-end;border-left:1px solid var(--line);border-right:4px solid var(--c)}.thread .meta{font-size:12px}
|
|
1310
|
+
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}
|
|
1311
|
+
table.team td{white-space:normal;word-break:break-word}table.team th{color:var(--mute);font-weight:600;vertical-align:top}
|
|
1312
|
+
td.hot{font-weight:700;color:var(--brand)}td.self{color:var(--mute)}
|
|
1313
|
+
footer{margin-top:40px;font-size:12px;color:var(--mute);text-align:center}
|
|
1314
|
+
@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))}}
|
|
1315
|
+
`;
|
|
779
1316
|
export function renderHtml(s) {
|
|
780
1317
|
const r = s.run;
|
|
1318
|
+
const c = s.collab;
|
|
1319
|
+
const o = outcome(s);
|
|
781
1320
|
const span = Math.max(1, r.end - r.start);
|
|
782
1321
|
const pct = (ts) => (((ts - r.start) / span) * 100).toFixed(3);
|
|
783
1322
|
const chip = (a) => `<span class="who"><i style="background:${colorOf(a)}"></i>${esc(a ?? "huddle")}</span>`;
|
|
784
|
-
|
|
1323
|
+
const badge = (text, tone = "") => `<span class="badge ${tone}">${esc(text)}</span>`;
|
|
1324
|
+
const card = (title, body, sub = "") => `<div class="card"><h3>${esc(title)}${sub ? ` <small>${esc(sub)}</small>` : ""}</h3>${body}</div>`;
|
|
1325
|
+
const empty = (t) => `<p class="none">${esc(t)}</p>`;
|
|
1326
|
+
// ---- header + crew
|
|
1327
|
+
const kpis = [
|
|
1328
|
+
{ n: String(s.merged), l: "merged", tone: "ok" },
|
|
1329
|
+
{ n: String(s.failed), l: "failed", tone: s.failed ? "bad" : "" },
|
|
1330
|
+
...(o.merges ? [{ n: `${o.green}/${o.merges}`, l: "merged on green tests", tone: o.green === o.merges ? "ok" : "warn" }] : []),
|
|
1331
|
+
{ n: fmtDuration(o.ms), l: "start to finish", tone: "" },
|
|
1332
|
+
...(o.tokens ? [{ n: fmtTokens(o.tokens), l: "tokens", tone: "" }] : []),
|
|
1333
|
+
...(c.bugs.length ? [{ n: `${c.bugs.length}→${c.bugs.filter((b) => b.fixedBy).length}`, l: "QA bugs → fixed", tone: "" }] : []),
|
|
1334
|
+
]
|
|
1335
|
+
.map((k) => `<div class="kpi ${k.tone}"><b>${esc(k.n)}</b><span>${esc(k.l)}</span></div>`)
|
|
1336
|
+
.join("");
|
|
1337
|
+
const crew = crewRows(s)
|
|
1338
|
+
.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>`)
|
|
1339
|
+
.join("");
|
|
1340
|
+
// ---- teamwork
|
|
1341
|
+
const tiles = tally(s)
|
|
1342
|
+
.map((t) => `<div class="tile ${t.tone ?? ""}"><b>${esc(t.n)}</b><span>${esc(t.label)}</span></div>`)
|
|
1343
|
+
.join("");
|
|
1344
|
+
const ideaAgents = s.agents.filter((a) => [...c.adopts, ...c.flaws].some((x) => x.by === a || x.of === a));
|
|
1345
|
+
const cell = (by, of) => {
|
|
1346
|
+
if (by === of)
|
|
1347
|
+
return `<td class="self">·</td>`;
|
|
1348
|
+
const ad = c.adopts.filter((x) => x.by === by && x.of === of).length;
|
|
1349
|
+
const fl = c.flaws.filter((x) => x.by === by && x.of === of).length;
|
|
1350
|
+
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>`;
|
|
1351
|
+
};
|
|
1352
|
+
const ideas = ideaAgents.length
|
|
1353
|
+
? `<div class="scroll"><table class="grid"><tr><th><small>took ↓ · from →</small></th>${ideaAgents.map((a) => `<th>${chip(a)}</th>`).join("")}</tr>${ideaAgents
|
|
1354
|
+
.map((by) => `<tr><th>${chip(by)}</th>${ideaAgents.map((of) => cell(by, of)).join("")}</tr>`)
|
|
1355
|
+
.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
|
|
1356
|
+
.slice(0, 3)
|
|
1357
|
+
.map((a) => `<div class="quote">${chip(a.by)} took ${chip(a.of)}'s idea: ${esc(oneLine(a.text, 160))}</div>`)
|
|
1358
|
+
.join("")}`
|
|
1359
|
+
: "";
|
|
1360
|
+
const contract = [
|
|
1361
|
+
c.synthesizer ? `<li>${chip(c.synthesizer.id)} wrote the plan${c.synthesizer.why ? ` <small>${esc(oneLine(c.synthesizer.why, 120))}</small>` : ""}</li>` : "",
|
|
1362
|
+
// Each objection is listed with whether it was taken; the ratify rows add only who signed off.
|
|
1363
|
+
...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>`),
|
|
1364
|
+
...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>`),
|
|
1365
|
+
c.contract ? `<li>◆ contract v1 by ${chip(c.contract.by)} <small>${esc(c.contract.chars)} chars · the names every task codes to</small></li>` : "",
|
|
1366
|
+
...c.amendments.map((a) => `<li>◆ v${esc(a.version)} by ${chip(a.by)} <span class="detail">${esc(oneLine(a.change, 160))}</span></li>`),
|
|
1367
|
+
]
|
|
1368
|
+
.filter(Boolean)
|
|
1369
|
+
.join("");
|
|
1370
|
+
const notes = buddyNotes(s);
|
|
1371
|
+
const good = c.buddy.filter((b) => !b.notes).length;
|
|
1372
|
+
const buddy = c.buddy.length
|
|
1373
|
+
? `<ul class="list">${notes
|
|
1374
|
+
.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>`)
|
|
1375
|
+
.join("")}${good ? `<li class="mute">✓ ${plural(good, "check")} looked good</li>` : ""}</ul>`
|
|
1376
|
+
: "";
|
|
1377
|
+
const bugs = c.bugs.length
|
|
1378
|
+
? `<ul class="list">${c.bugs
|
|
1379
|
+
.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>`)
|
|
1380
|
+
.join("")}</ul>`
|
|
1381
|
+
: "";
|
|
1382
|
+
const radarFix = c.followups.filter((f) => f.kind === "radar");
|
|
1383
|
+
const radar = [
|
|
1384
|
+
...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>`),
|
|
1385
|
+
...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>`),
|
|
1386
|
+
...c.pairs.map((p) => `<li>⇉ ${chip(p.agent)} paired with ${chip(p.with)}<div class="detail">took: ${esc(oneLine(p.part, 200))}</div></li>`),
|
|
1387
|
+
].join("");
|
|
1388
|
+
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>` : "";
|
|
1389
|
+
const team = collabLines(s);
|
|
1390
|
+
const cards = [
|
|
1391
|
+
ideas ? card("Ideas between agents", ideas, "council cross-review") : "",
|
|
1392
|
+
contract ? card("Council → contract", `<ul class="list">${contract}</ul>`) : "",
|
|
1393
|
+
buddy ? card("Buddy notes mid-task", buddy, "and what came of them") : "",
|
|
1394
|
+
bugs ? card("QA bugs → fixes", bugs) : "",
|
|
1395
|
+
radar || radarNote ? card("Radar & pairs", (radar ? `<ul class="list">${radar}</ul>` : "") + radarNote) : "",
|
|
1396
|
+
]
|
|
1397
|
+
.filter(Boolean)
|
|
1398
|
+
.join("");
|
|
1399
|
+
const teamwork = tiles || cards || team.length
|
|
1400
|
+
? `<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>` : ""}`
|
|
1401
|
+
: "";
|
|
1402
|
+
// ---- lanes: one per agent; spans are its tasks/reviews/conflicts/QA; dots are its messages.
|
|
785
1403
|
const ticks = Array.from({ length: 5 }, (_, i) => `<span style="left:${i * 25}%">${esc(clock((span * i) / 4))}</span>`).join("");
|
|
786
1404
|
const lanes = s.agents
|
|
787
1405
|
.map((a) => {
|
|
@@ -790,19 +1408,50 @@ export function renderHtml(s) {
|
|
|
790
1408
|
.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
1409
|
.join("");
|
|
792
1410
|
const marks = [...s.items, ...s.design]
|
|
793
|
-
.filter((it) => it.agent === a && ["message", "design", "qa", "takeover", "merge", "fail"
|
|
1411
|
+
.filter((it) => it.agent === a && ["message", "design", "qa", "takeover", "merge", "fail"].includes(it.kind))
|
|
794
1412
|
.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
1413
|
.join("");
|
|
796
1414
|
return `<div class="lane" data-lane="${esc(a)}"><div class="name">${chip(a)}</div><div class="track">${bars}${marks}</div></div>`;
|
|
797
1415
|
})
|
|
798
1416
|
.join("");
|
|
799
|
-
|
|
1417
|
+
// ---- the timeline, by phase
|
|
1418
|
+
const instant = councilInstant(s);
|
|
1419
|
+
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
1420
|
const num = new Map(s.plan.tasks.map((t, i) => [t.id, i + 1]));
|
|
801
1421
|
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>
|
|
1422
|
+
.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
1423
|
.join("");
|
|
1424
|
+
const ph = phases(s);
|
|
1425
|
+
const noise = s.items.filter(NOISE).length;
|
|
1426
|
+
const phase = (tag, sub, body) => `<section class="phase"><h3><span class="tag">${esc(tag)}</span> ${esc(sub)}</h3>${body}</section>`;
|
|
1427
|
+
const members = [...new Set(s.design.map((it) => it.agent).filter(Boolean))];
|
|
1428
|
+
const result = [
|
|
1429
|
+
`<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>`,
|
|
1430
|
+
o.merges ? `<li>${o.green === o.merges ? "✓" : "!"} ${o.green}/${o.merges} merges passed the tests first</li>` : "",
|
|
1431
|
+
o.qa ? `<li class="${o.qa.tone}">${o.qa.tone === "ok" ? "✓" : "!"} ${esc(plural(o.rounds, "QA round"))} · ${esc(o.qa.text)}</li>` : "",
|
|
1432
|
+
].join("");
|
|
1433
|
+
const timeline = [
|
|
1434
|
+
s.design.length ? phase("COUNCIL", `${members.join(", ")} designed it before anyone coded`, `<ol class="timeline">${s.design.map(row).join("")}</ol>`) : "",
|
|
1435
|
+
s.plan.tasks.length
|
|
1436
|
+
? phase("PLAN", `${plural(s.plan.tasks.length, "task")}${s.plan.by ? " · by " + s.plan.by : ""}`, `<ol class="plan">${plan}</ol>${ph.agreed.length
|
|
1437
|
+
? `<ul class="list agreed">${ph.agreed
|
|
1438
|
+
.map((it) => agreement(it))
|
|
1439
|
+
.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>`)
|
|
1440
|
+
.join("")}</ul>`
|
|
1441
|
+
: ""}${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>` : ""}`)
|
|
1442
|
+
: "",
|
|
1443
|
+
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>`),
|
|
1444
|
+
ph.qa.length ? phase("QA", "agents re-check the merged build; bugs become fix tasks", `<ol class="timeline">${ph.qa.map(row).join("")}</ol>`) : "",
|
|
1445
|
+
phase("RESULT", "", `<ul class="list result">${result}</ul>`),
|
|
1446
|
+
].join("");
|
|
1447
|
+
// ---- shared screen, conversations, tables
|
|
1448
|
+
const screen = s.screen.length
|
|
1449
|
+
? `<h2>Shared screen <small>one copy every agent reads</small></h2><ul class="list card">${s.screen
|
|
1450
|
+
.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>`)
|
|
1451
|
+
.join("")}</ul>`
|
|
1452
|
+
: "";
|
|
804
1453
|
const threads = [...s.threads.entries()]
|
|
805
|
-
.map(([pair, msgs]) => `<details
|
|
1454
|
+
.map(([pair, msgs]) => `<details><summary>${esc(pair)} <small>${msgs.length} message${msgs.length > 1 ? "s" : ""}</small></summary><ol class="thread">${msgs
|
|
806
1455
|
.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
1456
|
.join("")}</ol></details>`)
|
|
808
1457
|
.join("");
|
|
@@ -822,59 +1471,31 @@ export function renderHtml(s) {
|
|
|
822
1471
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
823
1472
|
<title>Huddle story</title>
|
|
824
1473
|
<style>
|
|
825
|
-
|
|
826
|
-
@media (prefers-color-scheme: dark){:root:not([data-theme="light"]){--bg:#15141a;--fg:#ece9e3;--mute:#9a948b;--line:#2c2a33;--card:#1d1c23}}
|
|
827
|
-
:root[data-theme="dark"]{--bg:#15141a;--fg:#ece9e3;--mute:#9a948b;--line:#2c2a33;--card:#1d1c23}
|
|
828
|
-
*{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:20px 16px 60px}
|
|
831
|
-
h1{font-size:22px;margin:0 0 4px}h2{font-size:15px;text-transform:uppercase;letter-spacing:.06em;color:var(--brand);margin:32px 0 10px}
|
|
832
|
-
.sub,small,.detail,time,.meta{color:var(--mute)}small{font-size:12px;margin-left:6px}
|
|
833
|
-
.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}
|
|
834
|
-
.scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;border:1px solid var(--line);border-radius:10px;background:var(--card)}
|
|
835
|
-
.lanes{min-width:640px;padding:8px 12px 12px}
|
|
836
|
-
.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
|
-
.lane{display:flex;align-items:center;height:38px;border-top:1px solid var(--line)}
|
|
838
|
-
.lane .name{width:120px;flex:none;overflow:hidden;text-overflow:ellipsis;font-size:13px}
|
|
839
|
-
.track{position:relative;flex:1;height:26px}
|
|
840
|
-
.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 span{opacity:.9}.bar.review{border-style:dashed}.bar.conflict{border-color:var(--warn)}.bar.qa{border-style:dotted}
|
|
842
|
-
.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
|
-
.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)}.mark.announce{background:var(--mute)}
|
|
845
|
-
ol,ul{padding:0;margin:0;list-style:none}
|
|
846
|
-
.timeline li{display:flex;gap:10px;padding:7px 0;border-top:1px solid var(--line)}
|
|
847
|
-
.timeline time{flex:none;width:48px;font-variant-numeric:tabular-nums;font-size:13px}
|
|
848
|
-
.timeline .merge .text{color:var(--ok)}.timeline .fail .text{color:var(--bad)}.timeline .conflict .text,.timeline .qa .text,.timeline .takeover .text{color:var(--warn)}
|
|
849
|
-
.timeline .progress .text,.timeline .announce .text,.timeline .team .text,.timeline .other .text{color:var(--mute)}
|
|
850
|
-
.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)}.plan{counter-reset:n}.plan li::before{counter-increment:n;content:counter(n) ". ";color:var(--mute)}
|
|
852
|
-
details{border:1px solid var(--line);border-radius:10px;background:var(--card);margin:10px 0;padding:8px 12px}summary{cursor:pointer;font-weight:600}
|
|
853
|
-
.thread{display:flex;flex-direction:column;gap:8px;margin-top:10px}
|
|
854
|
-
.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
|
-
.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
|
-
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}
|
|
857
|
-
td.hot{font-weight:700;color:var(--brand)}td.self{color:var(--mute)}
|
|
858
|
-
</style>
|
|
1474
|
+
${STORY_CSS}</style>
|
|
859
1475
|
</head>
|
|
860
1476
|
<body>
|
|
861
1477
|
<main>
|
|
862
|
-
<
|
|
863
|
-
<div class="
|
|
864
|
-
|
|
865
|
-
<
|
|
866
|
-
<div class="
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
${
|
|
870
|
-
|
|
871
|
-
|
|
1478
|
+
<header class="hero">
|
|
1479
|
+
<div class="brand">⬢ HUDDLE · STORY</div>
|
|
1480
|
+
<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>` : ""}
|
|
1481
|
+
<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>
|
|
1482
|
+
<div class="kpis">${kpis}</div>
|
|
1483
|
+
${o.qa ? `<p class="verdict ${o.qa.tone}">${o.qa.tone === "ok" ? "✓" : "!"} ${esc(plural(o.rounds, "QA round"))} · ${esc(o.qa.text)}</p>` : ""}
|
|
1484
|
+
</header>
|
|
1485
|
+
${crew ? `<div class="crew">${crew}</div>` : ""}
|
|
1486
|
+
${teamwork}
|
|
1487
|
+
<h2>Timeline <small>who was doing what, when</small></h2>
|
|
1488
|
+
<div class="lanesbox scroll"><div class="lanes"><div class="axis">${ticks}</div>${lanes || '<p class="none">no agents in this run</p>'}</div></div>
|
|
1489
|
+
<p class="legend">bars: tasks (solid) · reviews (dashed) · conflict fixes · QA (dotted); right edge green = merged/approved, red = failed/changes, amber = skipped/bug.</p>
|
|
1490
|
+
<input type="checkbox" id="noise"><label class="toggle" for="noise">show progress notes & memory (${noise})</label>
|
|
1491
|
+
<div class="phases">${timeline}</div>
|
|
1492
|
+
${screen}
|
|
1493
|
+
${threads ? `<h2>Conversations <small>agent to agent</small></h2>${threads}` : ""}
|
|
872
1494
|
<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>
|
|
1495
|
+
<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
1496
|
<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>
|
|
1497
|
+
<div class="scroll card"><table>${matrix}</table></div>
|
|
1498
|
+
<footer>made by huddle story · one file, no network</footer>
|
|
878
1499
|
</main>
|
|
879
1500
|
</body>
|
|
880
1501
|
</html>
|
|
@@ -927,6 +1548,7 @@ export async function runStory(args) {
|
|
|
927
1548
|
console.log(`${C.green("✓")} wrote ${out} ${C.faint(`· one file, no network · ${story.agents.length} lanes, ${story.items.length} events`)}`);
|
|
928
1549
|
return;
|
|
929
1550
|
}
|
|
930
|
-
|
|
1551
|
+
// A terminal gets lines cut to its width; a file or a pipe gets them whole.
|
|
1552
|
+
console.log(renderText(story, { width: process.stdout.isTTY ? process.stdout.columns || 80 : undefined }));
|
|
931
1553
|
}
|
|
932
1554
|
//# sourceMappingURL=story.js.map
|