@a-t-h-i/bot-lobby 0.6.10 → 0.6.12

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/README.md CHANGED
@@ -325,6 +325,14 @@ knowledge, standards, decisions and completed tasks — files on the left, the
325
325
  open file's entries on the right (a heading, a bullet, a paragraph), one of
326
326
  them picked. Files past the compaction threshold are marked.
327
327
 
328
+ Completion is gated on compaction: the engine refuses `complete` while any
329
+ knowledge file is over the threshold (default 20,000 chars,
330
+ `knowledge.compactionThreshold`). The Master first disperses domain-relevant
331
+ facts where they belong with `action=knowledge (domain=designer|backend|qa)`
332
+ — per-agent files fill up through that dispersal — then rewrites each
333
+ oversized file with `action=compact` (which archives the previous version)
334
+ before the task closes.
335
+
328
336
  - `e` edits the picked entry: it comes into the prompt (Shift+Enter for a new
329
337
  line, Enter saves). `n` adds an entry after it, `d d` deletes it, `E` edits
330
338
  the whole file in pi's editor. Every write archives the version before
@@ -361,6 +369,12 @@ planner), and those agents can look at what you drew and add to it.
361
369
  sessions are all look-only gets no drawing tool.
362
370
  - `t` **checks** the session: joins the room for a moment and says whether the
363
371
  server can be reached, who is in it, and how much is on the board. `r` renames it.
372
+ - `orchestrate action=whiteboard [name=...]` lets the Master create its own
373
+ session: a room on the configured collaboration server, assigned to itself,
374
+ with the join link returned. Its seat (opened by `excalidraw_read` /
375
+ `excalidraw_draw`) holds the room alive for the pi session's lifetime, so it
376
+ needs no one else in the room. The room is not guaranteed to persist after
377
+ the last connection leaves.
364
378
 
365
379
  **What an assigned agent can do.** It gets two tools and the room link:
366
380
  `excalidraw_read` describes the board in words (shapes with their labels, arrows
@@ -647,6 +661,7 @@ unavailable, it runs again on the fallback instead of failing the task.
647
661
  | Scouts and the QA gate can't edit code | Scouts get read-only tools; the QA gate adds only `bash` for tests |
648
662
  | New dependencies and architecture changes need approval | Parsed from worker reports; the domain is blocked until resolved |
649
663
  | Only the Master writes knowledge | Agents can only propose it |
664
+ | Knowledge is compacted before done | `complete` is refused while any knowledge file is over the threshold; disperse with `action=knowledge (domain=designer\|backend\|qa)`, then `action=compact` |
650
665
  | "Done" is earned | Needs a plan, a passing QA gate that ran checks (or your explicit acceptance), and no open blockers; on the fast track, a finished worker step, and QA's part only when the change needs tests |
651
666
  | Parallel workers don't clobber files | Edits need a file-desk claim |
652
667
  | A crash doesn't corrupt a task | State is on disk; tasks resume from their state |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a-t-h-i/bot-lobby",
3
- "version": "0.6.10",
3
+ "version": "0.6.12",
4
4
  "description": "Structured multi-agent software engineering orchestrator for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import { HttpsProxyAgent } from "https-proxy-agent";
15
15
  import { io, type Socket } from "socket.io-client";
16
- import { DEFAULT_SERVER, seal, unseal, type RoomLink } from "./room.ts";
16
+ import { DEFAULT_ORIGIN, DEFAULT_SERVER, seal, unseal, type RoomLink } from "./room.ts";
17
17
  import { describeScene, mergeElements, plainText, planDraw, sceneBounds, visible, type DrawOutcome, type DrawRequest, type Scene, type SceneElement } from "./scene.ts";
18
18
 
19
19
  /** The part of a socket.io client a seat uses; tests and other transports can stand in for it. */
@@ -56,7 +56,16 @@ function defaultConnect(server: string): SocketLike {
56
56
  // Where a network lets plain HTTPS through but not a websocket, the seat falls back to long-polling, as a browser does.
57
57
  // socket.io types `agent` for browsers (string | boolean); in Node it is passed on to the http and ws requests as an http.Agent.
58
58
  const agent = proxy ? { agent: new HttpsProxyAgent(proxy) as unknown as string } : {};
59
- return io(server, { transports: ["websocket", "polling"], tryAllTransports: true, autoUnref: true, timeout: 8000, ...agent });
59
+ // Excalidraw's collaboration server refuses handshakes whose Origin is not on its allowlist, so a seat presents the site a browser would come from.
60
+ let site = DEFAULT_ORIGIN;
61
+ if (server !== DEFAULT_SERVER) {
62
+ try {
63
+ site = new URL(server).origin;
64
+ } catch {
65
+ /* fall back to DEFAULT_ORIGIN */
66
+ }
67
+ }
68
+ return io(server, { transports: ["websocket", "polling"], tryAllTransports: true, autoUnref: true, timeout: 8000, extraHeaders: { Origin: site }, ...agent });
60
69
  }
61
70
 
62
71
  /**
package/src/pi/tools.ts CHANGED
@@ -58,6 +58,7 @@ const OrchestrateSchema = Type.Object({
58
58
  roster: Type.Optional(Type.Array(Type.String(), { description: "track: who takes part — any of designer, backend, qa, researcher" })),
59
59
  text: Type.Optional(Type.String({ description: "decide/complete/knowledge: the decision, completion summary, or knowledge text" })),
60
60
  file: Type.Optional(Type.String({ description: "compact: the knowledge file to rewrite, e.g. knowledge.md" })),
61
+ name: Type.Optional(Type.String({ description: "whiteboard: name for the new Excalidraw session" })),
61
62
  kind: Type.Optional(
62
63
  StringEnum(["knowledge", "standard", "decision", "completed"] as const, {
63
64
  description: "knowledge: which persistent file the text belongs to",
@@ -72,7 +73,7 @@ const DESCRIPTION = [
72
73
  "dependency), propose (record the proposal and request approval), plan (record the internal",
73
74
  "plan, or amend it later with the full revised plan), implement (delegate a step to a domain worker, or several domains in parallel with assignments), qa (final quality gate and the only",
74
75
  "review), knowledge (record approved knowledge or a decision),",
75
- "compact (replace a knowledge file with a rewritten version, archiving the old one),",
76
+ "compact (replace a knowledge file with a rewritten version, archiving the old one, dispersing domain-relevant facts to action=knowledge (domain=designer|backend|qa) first), whiteboard (create your own Excalidraw session and assign it to yourself),",
76
77
  "resolve_approval (approve or reject a request), complete (declare the task done after the gates",
77
78
  "pass), block/resume (escalate or continue), budget (under a time budget: where it stands, or ask the",
78
79
  "user for more minutes with a reason), track (the task's path and who takes part: show it, or correct it with",
@@ -30,6 +30,14 @@ function parseVerdict(text: string | undefined): Verdict {
30
30
  return "blocked";
31
31
  }
32
32
 
33
+ /** The verdict token may sit in the heading itself (`## Verdict PASS`), leaving an empty body. */
34
+ function verdictHeading(sections: Map<string, string>): string | undefined {
35
+ for (const heading of sections.keys()) {
36
+ if (heading.includes("verdict")) return heading;
37
+ }
38
+ return undefined;
39
+ }
40
+
33
41
  /** The severity a finding was tagged with, or undefined when it carries none. */
34
42
  function taggedSeverity(text: string): Severity | undefined {
35
43
  const match = /^\[([^\]]+)\]/.exec(text);
@@ -58,7 +66,7 @@ export function parseReviewResult(domain: Domain, raw: string): ReviewResult {
58
66
  const verification = findSection(sections, "verification") ?? "";
59
67
  const requiredChanges = bullets(findSection(sections, "required changes"));
60
68
  const entries = bullets(findSection(sections, "findings"));
61
- let verdict = parseVerdict(findSection(sections, "verdict"));
69
+ let verdict = parseVerdict(`${verdictHeading(sections) ?? ""}\n${findSection(sections, "verdict") ?? ""}`);
62
70
  let downgraded: string | undefined;
63
71
  let relaxed: string | undefined;
64
72
  const checked = executedChecks(verification).length > 0;
@@ -20,6 +20,7 @@ import { ownerlessTask, readTaskArtifact, removeTaskScratchpads, saveTask, selec
20
20
  import { dataRoot, readDataRoots } from "../state/project.ts";
21
21
  import { appendCompletedTask, appendDecision, applyKnowledge, readFileOr, writeFileEnsured, type KnowledgeKind } from "../knowledge/store.ts";
22
22
  import { compactKnowledgeFile, overThreshold } from "../knowledge/compactor.ts";
23
+ import { ExcalidrawBook } from "../excalidraw/sessions.ts";
23
24
  import { knowledgeDir, type KnowledgeAgent } from "../knowledge/paths.ts";
24
25
  import { writeScratchpad } from "../state/persistence.ts";
25
26
  import { spawnPiProcess, type ProcessRunner, type RelayAsk } from "../execution/pi-runner.ts";
@@ -75,6 +76,7 @@ export const ORCHESTRATE_ACTIONS = [
75
76
  "qa",
76
77
  "knowledge",
77
78
  "compact",
79
+ "whiteboard",
78
80
  "complete",
79
81
  "block",
80
82
  "resume",
@@ -112,6 +114,8 @@ export interface OrchestrateParams {
112
114
  kind?: KnowledgeKind;
113
115
  /** compact: the knowledge file being rewritten. */
114
116
  file?: string;
117
+ /** whiteboard: name for the new Excalidraw session. */
118
+ name?: string;
115
119
  approvalId?: string;
116
120
  decision?: "approved" | "rejected";
117
121
  note?: string;
@@ -1270,6 +1274,19 @@ function handleCompact(task: Task, params: OrchestrateParams, deps: WorkflowDeps
1270
1274
  return `Compacted ${agent}/${file}: ${outcome.before} -> ${outcome.after} chars. Previous version archived at ${outcome.archive}.`;
1271
1275
  }
1272
1276
 
1277
+ /**
1278
+ * The Master creates its own whiteboard: a room on the configured server,
1279
+ * assigned to itself so its seat keeps the room alive for this pi session.
1280
+ */
1281
+ function handleWhiteboard(task: Task, params: OrchestrateParams, deps: WorkflowDeps): string {
1282
+ const book = new ExcalidrawBook({ root: deps.root });
1283
+ const made = book.create(params.name?.trim() || undefined);
1284
+ if (!made.session) throw new Error(made.notice);
1285
+ book.toggleAgent(made.session.id, "master");
1286
+ recordDecision(task, `Created Excalidraw session "${made.session.name}" and assigned it to Master.`);
1287
+ return `Created Excalidraw session "${made.session.name}" and assigned it to you. Join link: ${made.session.link}. Call excalidraw_read to open your seat; the connection stays open for this pi session and keeps the room alive without anyone else in it. It ends when the pi session ends.`;
1288
+ }
1289
+
1273
1290
  /** §63: record history, drop scratchpads, then mark the task completed. */
1274
1291
  async function handleComplete(task: Task, params: OrchestrateParams, deps: WorkflowDeps): Promise<string> {
1275
1292
  // The fast track completes straight from its work; the full workflow from review.
@@ -1279,6 +1296,9 @@ async function handleComplete(task: Task, params: OrchestrateParams, deps: Workf
1279
1296
  if (task.state === "blocked") throw new Error("cannot complete: the task is blocked, and the user did not accept its work as it is");
1280
1297
  const blockers = completionBlockers(task, pendingApprovals(task).length);
1281
1298
  if (blockers.length > 0) throw new Error(`cannot complete: ${blockers.join("; ")}`);
1299
+ // The gate refuses before any side effect, so a refusal leaves the task untouched.
1300
+ const oversized = overThreshold(readDataRoots(deps.root, deps.configDir), deps.config.knowledge.compactionThreshold);
1301
+ if (oversized.length > 0) throw new Error(`cannot complete: knowledge files over the compaction threshold: ${oversized.map((entry) => `${entry.agent}/${entry.file} (${entry.chars})`).join(", ")}. First record domain-relevant facts where they belong with action=knowledge (domain=designer|backend|qa), then rewrite each oversized file with action=compact, then call complete again.`);
1282
1302
  const summary = params.text?.trim() || task.proposal || task.title;
1283
1303
  flushDecisions(deps, task);
1284
1304
  recordCompletion(deps, task, summary);
@@ -1286,12 +1306,7 @@ async function handleComplete(task: Task, params: OrchestrateParams, deps: Workf
1286
1306
  task.blockers = [];
1287
1307
  if (task.state === "implementing") transition(task, "reviewing");
1288
1308
  transition(task, "completed");
1289
- const oversized = overThreshold(readDataRoots(deps.root, deps.configDir), deps.config.knowledge.compactionThreshold);
1290
- const advice =
1291
- oversized.length > 0
1292
- ? `\nKnowledge files over the compaction threshold: ${oversized.map((entry) => `${entry.agent}/${entry.file} (${entry.chars})`).join(", ")}. Compact them with action=compact when convenient.`
1293
- : "";
1294
- return `Task ${task.id} completed. History recorded and temporary scratchpads removed.${advice}`;
1309
+ return `Task ${task.id} completed. History recorded and temporary scratchpads removed.`;
1295
1310
  }
1296
1311
 
1297
1312
  /** Asked when the oracle completes a task QA has not passed (the user told it to finish, say). */
@@ -1653,6 +1668,7 @@ const HANDLERS: Record<OrchestrateAction, (task: Task, params: OrchestrateParams
1653
1668
  qa: handleQa,
1654
1669
  knowledge: handleKnowledge,
1655
1670
  compact: handleCompact,
1671
+ whiteboard: handleWhiteboard,
1656
1672
  complete: handleComplete,
1657
1673
  block: handleBlock,
1658
1674
  resume: handleResume,