@a-t-h-i/bot-lobby 0.6.44 → 0.6.45

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
@@ -33,14 +33,25 @@ files the task changed, after the project's linter and a QA pass sized to the ch
33
33
  - **Plan**: plan with a panel of agents. **View plan** (`V`) opens the draft in a slide-out, where
34
34
  any line takes a comment.
35
35
  - **Tasks**: every task across sessions as a list or a board, with its steps, timings and **Resume**.
36
- - **Issues** and **Git**: plan from an open issue, or have an agent review a pull request read-only.
37
- Both read GitHub through `gh` and post nothing.
36
+ - **Git**: use the **Pull requests / Issues** switch to review pull requests or browse open issues
37
+ through `gh`. Issues retain descriptions, existing comments, **File issue** and **Plan it**;
38
+ pull-request reviews remain read-only and post nothing.
38
39
  - **Knowledge**: what each agent has learned about the project, which you can edit, annotate or ask
39
40
  about.
40
41
 
41
42
  The rail on the left holds settings, the key list, the theme and sounds. The project switcher
42
- (`P`) sits in the message box and opens any folder on the machine running bot-lobby. A task running
43
- in a background session opens its conversation from **Tasks**.
43
+ (`P`) opens any folder on the machine running bot-lobby. Background sessions still run tasks;
44
+ follow their progress and use **Resume** in **Tasks**, without a separate Sessions conversation page.
45
+
46
+ Git opens pull requests at `#/git` and `#/git/<number>`, and issues at `#/git/issues` and
47
+ `#/git/issues/<number>`. Old `#/issues` bookmarks redirect to Git. Former Sessions and Excalidraw
48
+ addresses fall back to Lobby; Excalidraw integration is removed without deleting saved room links
49
+ or external boards.
50
+
51
+ The composer appears only on **Lobby**, **Tasks**, **Plan**, **Quick fix** and **Knowledge**, including
52
+ supported detail routes and Knowledge's Docs/Chat views. On Git, Metrics and Settings it stays mounted
53
+ but hidden and inert, preserving drafts and attachments (including uploads that finish while hidden)
54
+ without taking space. Phone navigation remains available.
44
55
 
45
56
  | Agent | Does |
46
57
  | --- | --- |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a-t-h-i/bot-lobby",
3
- "version": "0.6.44",
3
+ "version": "0.6.45",
4
4
  "description": "Structured multi-agent software engineering orchestrator for Pi",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -72,16 +72,11 @@
72
72
  "react-dom": "19.3.0",
73
73
  "react-markdown": "10.1.0",
74
74
  "remark-gfm": "4.0.1",
75
- "socket.io": "4.8.4",
76
75
  "tailwind-merge": "3.7.0",
77
76
  "tailwindcss": "4.3.3",
78
77
  "tw-animate-css": "1.4.0",
79
78
  "typebox": "1.3.27",
80
79
  "typescript": "^5.7.0",
81
80
  "vite": "8.3.2"
82
- },
83
- "dependencies": {
84
- "https-proxy-agent": "^9.1.0",
85
- "socket.io-client": "4.8.4"
86
81
  }
87
82
  }
@@ -11,7 +11,6 @@ import { ASK_ENV } from "../ask/relay.ts";
11
11
  import { STEP_LIMIT_MS } from "../state/step-limits.ts";
12
12
  import type { EffortRouter, RunProfileLike } from "../classifier/effort.ts";
13
13
  import { ASK_TOOL } from "../ask/types.ts";
14
- import { agentOfRun, grantOption } from "../excalidraw/sessions.ts";
15
14
 
16
15
  export interface AgentContext {
17
16
  task: string;
@@ -289,7 +288,6 @@ async function runAgentOnce(request: AgentRequest, run: ProcessRunner, attempt:
289
288
  } : {}),
290
289
  signal,
291
290
  ...(request.onAsk ? { onAsk: request.onAsk } : {}),
292
- ...grantOption(agentOfRun(request.domain, request.role)),
293
291
  env: request.onAsk ? { ...request.env, [ASK_ENV]: "1" } : request.env,
294
292
  stallTimeoutMs: request.stallTimeoutMs,
295
293
  toolStallTimeoutMs: request.toolStallTimeoutMs,
@@ -1,4 +1,3 @@
1
- import { toolsOf } from "../excalidraw/sessions.ts";
2
1
  import { appendMetrics, metricFromRun } from "../state/metrics.ts";
3
2
  import { randomUUID } from "node:crypto";
4
3
  import { mkdirSync, writeFileSync } from "node:fs";
@@ -73,7 +72,7 @@ export async function delegationHost(options: PiRunOptions, run: ProcessRunner)
73
72
  const agent = options.agent ?? { runId: id, taskId: id, domain: "backend" as const, role: editable ? "worker" as const : "reviewer" as const };
74
73
  const depth = agent.depth ?? 1;
75
74
  const quota = options.delegationQuota ?? { used: 0 };
76
- const tools = [...new Set([...baseTools, ...(options.excalidraw ? toolsOf(options.excalidraw) : []), "codemode", "tool_search", ...(canDelegate(agent, quota.used) ? [DELEGATE_TOOL] : [])])];
75
+ const tools = [...new Set([...baseTools, "codemode", "tool_search", ...(canDelegate(agent, quota.used) ? [DELEGATE_TOOL] : [])])];
77
76
  const mcpTools = options.mcpTools ?? agentProfile(config, agent.domain, agent.role).mcpTools ?? [];
78
77
  const policy: AgentPolicy = { tools, mcpTools, readOnly: !editable, depth };
79
78
  if (depth > 1) return { tools, env: { ...options.env, [AGENT_ENV]: JSON.stringify(policy), [DELEGATION_ENV]: "" }, note: "You are a child agent. Finish only your assigned brief and report evidence to your parent; you cannot delegate further.", close: async () => {}, onStart: options.onStart, children: [] as AgentRun[] };
@@ -10,7 +10,6 @@ import { activityWord } from "../pi/activity.ts";
10
10
  import { shortDuration } from "../text.ts";
11
11
  import { readRelayRequest } from "../ask/relay.ts";
12
12
  import type { AskQuestion, AskResult } from "../ask/types.ts";
13
- import { grantEnv, grantNote, type Grant } from "../excalidraw/sessions.ts";
14
13
  import type { EffortRouter, RunProfileLike } from "../classifier/effort.ts";
15
14
 
16
15
  /** Live control over one running subagent: queue a steering message for its next turn. */
@@ -57,8 +56,6 @@ export interface PiRunOptions {
57
56
  time?: TimeLimit;
58
57
  /** Answers the agent's relayed questions; without it they are cancelled like any other dialog. */
59
58
  onAsk?: RelayAsk;
60
- /** The Excalidraw sessions assigned to this agent: its tools, the room links in its environment, and a note on them in its task. */
61
- excalidraw?: Grant;
62
59
  }
63
60
 
64
61
  /**
@@ -771,7 +768,6 @@ function writePromptFile(prompt: string): { file: string; cleanup: () => void }
771
768
  export async function runPiAgent(options: PiRunOptions, run: ProcessRunner = spawnPiProcess): Promise<PiRunResult> {
772
769
  const host = await delegationHost(options, run);
773
770
  let prompt: ReturnType<typeof writePromptFile> | undefined;
774
- const grant = options.excalidraw;
775
771
  try {
776
772
  prompt = writePromptFile([options.systemPrompt, host.note].filter(Boolean).join("\n\n"));
777
773
  if (options.signal?.aborted) return { status: "cancelled", output: "", usage: { input: 0, output: 0, cost: 0, turns: 0 } };
@@ -780,8 +776,8 @@ export async function runPiAgent(options: PiRunOptions, run: ProcessRunner = spa
780
776
  cwd: options.cwd,
781
777
  signal: options.signal,
782
778
  timeoutMs: options.timeoutMs,
783
- prompt: `Task: ${options.task}${grant ? `\n\n${grantNote(grant)}` : ""}`,
784
- env: grant ? { ...host.env, ...grantEnv(grant) } : host.env,
779
+ prompt: `Task: ${options.task}`,
780
+ env: host.env,
785
781
  stallTimeoutMs: options.stallTimeoutMs,
786
782
  toolStallTimeoutMs: options.toolStallTimeoutMs,
787
783
  wrapUpAtMs: options.wrapUpAtMs,
package/src/index.ts CHANGED
@@ -19,7 +19,6 @@ import { registerClassifierTools } from "./classifier/tools.ts";
19
19
  import { registerFreshContext } from "./pi/fresh-context.ts";
20
20
  import { registerAskTool } from "./ask/tool.ts";
21
21
  import { registerWebTools } from "./web/tools.ts";
22
- import { registerExcalidrawTools } from "./excalidraw/tools.ts";
23
22
  import { registerToolBudget } from "./pi/tool-budget.ts";
24
23
 
25
24
  export default function (pi: ExtensionAPI): void {
@@ -49,8 +48,6 @@ export default function (pi: ExtensionAPI): void {
49
48
  registerAskTool(pi, undefined, CONFIG_DIR_NAME);
50
49
  // The web tools, for the researcher and for pi without a task (the oracle leaves them to the researcher).
51
50
  registerWebTools(pi);
52
- // The shared Excalidraw sessions the user assigned to agents: the oracle here, every other agent through its own grant.
53
- registerExcalidrawTools(pi, CONFIG_DIR_NAME);
54
51
  // Parallel workers share files through the master's file desk.
55
52
  if (isSubagentProcess()) registerDeskClient(pi);
56
53
  // Subagents look files up with the classifier (the engine allows the tool only while file hints are on).
package/src/lobby/host.ts CHANGED
@@ -18,18 +18,11 @@ import type { IssuesState } from "./issues.ts";
18
18
  import type { PullsState } from "./pulls.ts";
19
19
  import type { PullReviews } from "./pr-review.ts";
20
20
  import type { KnowledgeBook } from "./knowledge.ts";
21
- import type { ExcalidrawBook, ExcalidrawSession } from "../excalidraw/sessions.ts";
22
21
  import type { WorkspaceInfo } from "../execution/workspace.ts";
23
22
 
24
23
  export { TAB_IDS, TAB_LABELS, visibleTabs } from "./prompts.ts";
25
24
  export type { TabId } from "./prompts.ts";
26
25
 
27
- /** What a checked Excalidraw session reported. */
28
- export interface SessionCheck {
29
- ok: boolean;
30
- text: string;
31
- }
32
-
33
26
  /**
34
27
  * Which session the Lobby tab shows and talks to: this window, one it started
35
28
  * in the background, one running in another terminal, or a task whose
@@ -147,10 +140,6 @@ export interface LobbyService {
147
140
  reviews: PullReviews;
148
141
  /** Every agent's knowledge files: read, edited, commented on. */
149
142
  knowledge: KnowledgeBook;
150
- /** The shared Excalidraw sessions (five at most) and which agents each is assigned to. */
151
- excalidraw: ExcalidrawBook;
152
- /** Join a session's room for a moment and report what is there. */
153
- checkExcalidraw(session: ExcalidrawSession): Promise<SessionCheck>;
154
143
  /** The effective config, never a secret; the web settings page reads it. */
155
144
  config?(): BotLobbyConfig;
156
145
  /** Save a whole config, the way the settings menu does; the web settings page writes it. */
@@ -163,7 +152,7 @@ export interface LobbyService {
163
152
  classifierModels?(): Array<{ id: string; label: string }>;
164
153
  /** The model this window's session runs on (`provider/id`), when it has one. */
165
154
  sessionModel?(): string | undefined;
166
- /** The Issues tab is switched on (`lobby.issues`). */
155
+ /** Git's Issues view is switched on (`lobby.issues`). */
167
156
  issuesEnabled(): boolean;
168
157
  /** What this window carried on from one that stopped unexpectedly, when it did. */
169
158
  recovered?(): { at: number; text: string } | undefined;
@@ -22,7 +22,6 @@ import { randomUUID } from "node:crypto";
22
22
  import { loadPrompt } from "../prompts/loader.ts";
23
23
  import { compilePrompt } from "../prompts/compiler.ts";
24
24
  import { runPiAgent, spawnPiProcess, type PiStreamEvent, type ProcessRunner } from "../execution/pi-runner.ts";
25
- import { grantOption, type Grant } from "../excalidraw/sessions.ts";
26
25
  import { describeToolCall } from "../pi/activity.ts";
27
26
  import { appendMetrics, type MetricRecord } from "../state/metrics.ts";
28
27
  import { savePlannedTask, type IssueRef, type PlannedTask } from "../state/backlog.ts";
@@ -904,7 +903,6 @@ export class PlanningSession {
904
903
  task: splitRequest({ title, plan, steps, ...(feedback ? { revision: feedback } : {}), ...(rejected.length > 0 ? { rejected } : {}) }),
905
904
  systemPrompt: splitPrompt(profile.instructions),
906
905
  tools: this.tools(PLANNER_TOOLS),
907
- ...grantOption("planner"),
908
906
  }, signal, (step) => (this.step = step));
909
907
  if (signal.aborted) throw new Error("stopped");
910
908
  if (lead.status !== "success") return lead.error ?? lead.status;
@@ -997,7 +995,7 @@ export class PlanningSession {
997
995
  this.deps.onChange?.();
998
996
  }
999
997
 
1000
- private async run(label: string, kind: "planner" | "panel", profile: QuickFixProfile, request: { task: string; systemPrompt: string; tools: readonly string[]; excalidraw?: Grant }, signal: AbortSignal, setStep: (step: string) => void, routedFrom?: string): Promise<RunOutcome> {
998
+ private async run(label: string, kind: "planner" | "panel", profile: QuickFixProfile, request: { task: string; systemPrompt: string; tools: readonly string[] }, signal: AbortSignal, setStep: (step: string) => void, routedFrom?: string): Promise<RunOutcome> {
1001
999
  const startedAt = Date.now();
1002
1000
  const runId = randomUUID();
1003
1001
  let outcome: RunOutcome;
@@ -1069,8 +1067,6 @@ export class PlanningSession {
1069
1067
  task: `${transcript}\n\nYou are ${label} on the planning panel${roundNote(this.turns, this.limit)}: ask your seat's open questions, or declare READY.`,
1070
1068
  systemPrompt: memberPrompt(member, profile.instructions),
1071
1069
  tools: this.tools(member === "researcher" ? RESEARCH_PANEL_TOOLS : PLANNER_TOOLS),
1072
- // A panel seat is the agent of its own kind: DEV is Backend, DESIGN the Designer.
1073
- ...grantOption(member),
1074
1070
  };
1075
1071
  const route = effort ? this.deps.effort?.plan(effort, { ...(profile.model ? { model: profile.model } : {}), thinking: profile.thinking }) : undefined;
1076
1072
  let outcome = await this.run(label, "panel", route ? { ...profile, model: route.model, thinking: route.thinking } : profile, request, signal, (step) => (state.step = step), route ? profileLabel(route.from) : undefined);
@@ -1128,7 +1124,6 @@ export class PlanningSession {
1128
1124
  task: `${transcript}\n\n${panelSection(outcomes)}\n\n${oracleClosing(mode, this.turns, limit)}`,
1129
1125
  systemPrompt: plannerPrompt(profile.instructions),
1130
1126
  tools: this.tools(PLANNER_TOOLS),
1131
- ...grantOption("planner"),
1132
1127
  }, controller.signal, (step) => (this.step = step));
1133
1128
  if (controller.signal.aborted) throw new Error("stopped");
1134
1129
  this.finishRound(outcomes, lead, mode);
@@ -4,7 +4,7 @@
4
4
  * `node:*`): the web page imports this file directly.
5
5
  */
6
6
 
7
- export const TAB_IDS = ["lobby", "tasks", "plan", "quickfix", "issues", "excalidraw", "git", "knowledge", "metrics"] as const;
7
+ export const TAB_IDS = ["lobby", "tasks", "plan", "quickfix", "git", "knowledge", "metrics"] as const;
8
8
  export type TabId = (typeof TAB_IDS)[number];
9
9
 
10
10
  export const TAB_LABELS: Record<TabId, string> = {
@@ -12,16 +12,14 @@ export const TAB_LABELS: Record<TabId, string> = {
12
12
  tasks: "Tasks",
13
13
  plan: "Plan",
14
14
  quickfix: "Quick fix",
15
- issues: "Issues",
16
15
  metrics: "Metrics",
17
16
  git: "Git",
18
17
  knowledge: "Knowledge",
19
- excalidraw: "Excalidraw",
20
18
  };
21
19
 
22
- /** The tabs on show: Issues only while `lobby.issues` switches it on. */
23
- export function visibleTabs(issues: boolean): TabId[] {
24
- return TAB_IDS.filter((tab) => issues || tab !== "issues");
20
+ /** The tabs on show; issues are available inside Git. */
21
+ export function visibleTabs(_issues: boolean): TabId[] {
22
+ return [...TAB_IDS];
25
23
  }
26
24
 
27
25
  /** A question answered in another window of the page (or put away) before this one's answer arrived. */
@@ -9,7 +9,6 @@ import type { AgentRun } from "../schemas/findings.ts";
9
9
  import { loadPrompt } from "../prompts/loader.ts";
10
10
  import { runPiAgent, spawnPiProcess, type PiStreamEvent, type ProcessRunner } from "../execution/pi-runner.ts";
11
11
  import { MAX_CHILDREN } from "../execution/delegation.ts";
12
- import { grantOption } from "../excalidraw/sessions.ts";
13
12
  import { describeToolCall } from "../pi/activity.ts";
14
13
  import { appendMetrics, type MetricRecord } from "../state/metrics.ts";
15
14
  import { appendChange, EditLog } from "../state/changes.ts";
@@ -338,7 +337,6 @@ export class QuickFixQueue {
338
337
  task: [job.prompt, job.interrupted ? INTERRUPTED_NOTE : "", likely, scripts ? `## Scripts\n\n${scripts}` : ""].filter(Boolean).join("\n\n"),
339
338
  systemPrompt: quickFixPrompt(profile.instructions),
340
339
  tools: [...QUICK_FIX_TOOLS, ...(this.deps.hints?.tools() ?? [])],
341
- ...grantOption("quickfix"),
342
340
  model,
343
341
  thinking,
344
342
  mcpTools: profile.mcpTools,
@@ -26,7 +26,6 @@ import { execCommand, IssuesState } from "./issues.ts";
26
26
  import { PullsState } from "./pulls.ts";
27
27
  import { PullReviews } from "./pr-review.ts";
28
28
  import { KnowledgeBook } from "./knowledge.ts";
29
- import { ExcalidrawBook } from "../excalidraw/sessions.ts";
30
29
  import type { LobbyService } from "./host.ts";
31
30
  import { backgroundSessions, createLobbyService, lobbyNotify, lobbyProfile, plannerDeps, resumeSession, reviewProfile, seatProfile, setServiceState, savePlan as serviceSavePlan, answerPanel as serviceAnswerPanel } from "./service.ts";
32
31
  import { lobbyTopics } from "./topics.ts";
@@ -49,7 +48,6 @@ export interface Runtime {
49
48
  pulls: PullsState;
50
49
  reviews: PullReviews;
51
50
  knowledge: KnowledgeBook;
52
- excalidraw: ExcalidrawBook;
53
51
  unsubscribeFeed?: () => void;
54
52
  /** Puts the panel's questions to the user: the web page's questionnaire unless a test sets another. */
55
53
  asker?: Asker;
@@ -163,7 +161,6 @@ export function startLobbyService(pi: ExtensionAPI, ctx: ExtensionContext, confi
163
161
  backups: () => loadConfig().knowledge.backupCount,
164
162
  sessionId: () => ctx.sessionManager.getSessionId(),
165
163
  }),
166
- excalidraw: new ExcalidrawBook({ root }),
167
164
  };
168
165
  state.reviews = new PullReviews({
169
166
  cwd: ctx.cwd,
@@ -204,8 +201,7 @@ export function startLobbyService(pi: ExtensionAPI, ctx: ExtensionContext, confi
204
201
  // changes and the web page rereads it. lobbyFeed (lobby), run updates (tasks),
205
202
  // quick fix (quickfix), planner (planner), background sessions (sessions),
206
203
  // metrics (metrics), pulls/reviews (git), issues (issues), the prompt hub
207
- // (prompts), owner/notify events (notices), busy/workspace (status), link
208
- // checks (excalidraw).
204
+ // (prompts), owner/notify events (notices), busy/workspace (status).
209
205
  setQuickFixHandoff((request, builder, reason) => (runtime === state ? handToQuickFix(state, request, builder, reason) : undefined));
210
206
  lobbyFeed.clear();
211
207
  // Only the newest messages are kept; the feed learns whether earlier ones exist, and loads them when scrolled to.
@@ -45,7 +45,6 @@ import { carryOnMessage, resumeRoute, RESUMED_DECISION, type Drivers } from "./r
45
45
  import { archivePlan as archivePlanOnDisk, deletePlan as deletePlanOnDisk, isPlanId, previousPlans, readPlan, type PlanRecord, type PlanSummary } from "../state/plan-history.ts";
46
46
  import { applyMasterModel } from "../pi/model-settings.ts";
47
47
  import { lobbyTopics } from "./topics.ts";
48
- import { checkSession } from "../excalidraw/check.ts";
49
48
  import type { LobbyAgentKind, PanelMember } from "../schemas/configuration.ts";
50
49
  import type { LiveSession, LobbyService, SwitchTarget } from "./host.ts";
51
50
  import { pushNotice, type NoticeLevel } from "../webui/notices.ts";
@@ -814,11 +813,6 @@ export function createLobbyService(state: Runtime): LobbyService {
814
813
  pulls: state.pulls,
815
814
  reviews: state.reviews,
816
815
  knowledge: state.knowledge,
817
- excalidraw: state.excalidraw,
818
- checkExcalidraw: (session) => {
819
- lobbyTopics.bump("excalidraw");
820
- return checkSession(session.link, session.name);
821
- },
822
816
  profileLabel: (kind) => {
823
817
  const profile = lobbyProfile(state, kind);
824
818
  return `${profile.model ?? "session model"} · ${profile.thinking}`;
@@ -16,7 +16,6 @@ export const LOBBY_TOPICS = [
16
16
  "git",
17
17
  "issues",
18
18
  "knowledge",
19
- "excalidraw",
20
19
  "prompts",
21
20
  "notices",
22
21
  "status",
package/src/pi/tools.ts CHANGED
@@ -62,7 +62,7 @@ const OrchestrateSchema = Type.Object({
62
62
  text: Type.Optional(Type.String({ description: "decide/complete/knowledge/script: the decision, completion summary, knowledge text, or the script itself" })),
63
63
  message: Type.Optional(Type.String({ description: "complete: the commit message for the agents' changes, which the engine commits (only their files): a first line of 72 characters or fewer saying what changed, at most three short lines below it" })),
64
64
  file: Type.Optional(Type.String({ description: "compact: the knowledge file to rewrite, e.g. knowledge.md; script: a script a worker drafted, to save instead of text" })),
65
- name: Type.Optional(Type.String({ description: "whiteboard: name for the new Excalidraw session; script: the file name, what it does in snake_case with its language's extension (seed_public_holidays.py, run_changed_tests.sh)" })),
65
+ name: Type.Optional(Type.String({ description: "script: the file name, what it does in snake_case with its language's extension (seed_public_holidays.py, run_changed_tests.sh)" })),
66
66
  purpose: Type.Optional(Type.String({ description: "script: one line on what the script does" })),
67
67
  usage: Type.Optional(Type.String({ description: "script: the command that runs it, with its arguments" })),
68
68
  kind: Type.Optional(
@@ -79,7 +79,7 @@ const DESCRIPTION = [
79
79
  "dependency), propose (record the proposal and request approval), plan (record the internal",
80
80
  "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",
81
81
  "review), knowledge (record approved project knowledge or a decision; works without a task),",
82
- "compact (replace a knowledge file with a rewritten version, archiving the old one; works without a task; disperse domain-relevant facts to action=knowledge (domain=designer|backend|qa) first), script (save a script to the shared scripts library, or replace one, for a job that comes back; works without a task), whiteboard (create your own Excalidraw session and assign it to yourself),",
82
+ "compact (replace a knowledge file with a rewritten version, archiving the old one; works without a task; disperse domain-relevant facts to action=knowledge (domain=designer|backend|qa) first), script (save a script to the shared scripts library, or replace one, for a job that comes back; works without a task),",
83
83
  "resolve_approval (approve or reject a request), complete (declare the task done after the gates",
84
84
  "pass, then ask the user to confirm merging and pushing main and removing its worktree), block/resume (escalate or continue), budget (under a time budget: where it stands, or ask the",
85
85
  "user for more minutes with a reason), track (the task's path and who takes part: show it, or correct it with",
@@ -18,7 +18,6 @@ import { quickfixCancel, quickfixList, quickfixMovedToTask, quickfixRunAnyway, q
18
18
  import { tasksArchive, tasksArchived, tasksAuto, tasksComment, tasksComments, tasksDelete, tasksEditComment, tasksGet, tasksList, tasksMessage, tasksRestore, tasksOpen, tasksResume, tasksDeliveryReview, tasksDeliveryDefer, tasksDeliver } from "./tasks.ts";
19
19
  import { metricsGet } from "./metrics.ts";
20
20
  import { knowledgeAdd, knowledgeAsk, knowledgeComment, knowledgeEdit, knowledgeFiles, knowledgeOpen, knowledgeRemove, knowledgeReplaceFile, knowledgeUnnote } from "./knowledge.ts";
21
- import { excalidrawAdd, excalidrawCheck, excalidrawCreate, excalidrawList, excalidrawRemove, excalidrawRename, excalidrawReveal, excalidrawToggleAgent, excalidrawToggleAll, excalidrawToggleContribute } from "./excalidraw.ts";
22
21
  import { plansDiscard, plansGet, plansStart } from "./plans.ts";
23
22
  import { gitCancelReview, gitJev, gitPull, gitPulls, gitReview } from "./git.ts";
24
23
  import { issuesCreate, issuesGet, issuesList } from "./issues.ts";
@@ -93,7 +92,6 @@ const KnowledgeAgent = Type.Union([Type.Literal("master"), Type.Literal("designe
93
92
  const KnowledgeFile = Type.String({ minLength: 1, maxLength: 200 });
94
93
  const KnowledgeText = Type.String({ minLength: 1, maxLength: 100_000 });
95
94
  const EntryRef = Type.Object({ text: Type.String({ minLength: 1, maxLength: 100_000 }), occurrence: Type.Integer({ minimum: 0, maximum: 100000 }) }, { additionalProperties: false });
96
- const ExcalidrawAgent = Type.Union([Type.Literal("master"), Type.Literal("designer"), Type.Literal("backend"), Type.Literal("qa"), Type.Literal("scout"), Type.Literal("researcher"), Type.Literal("quickfix"), Type.Literal("planner")]);
97
95
  const PlannerSeed = Type.Object({
98
96
  issue: Type.Object({ number: Type.Integer({ minimum: 1 }), title: Type.String({ minLength: 1, maxLength: 500 }), url: Type.Optional(Type.String({ maxLength: 2000 })) }, { additionalProperties: false }),
99
97
  body: Type.String({ maxLength: 20_000 }),
@@ -307,43 +305,6 @@ function buildRoutes(): Record<string, Route> {
307
305
  schema: Type.Object({ id: TaskId }, { additionalProperties: false }),
308
306
  run: (body, ctx) => knowledgeUnnote(body as { id: string }, ctx),
309
307
  };
310
- routes["excalidraw.list"] = { schema: Empty, run: (_body, ctx) => excalidrawList(ctx) };
311
- routes["excalidraw.add"] = {
312
- schema: Type.Object({ link: Type.String({ minLength: 1, maxLength: 2000 }), name: Type.Optional(Type.String({ maxLength: 40 })) }, { additionalProperties: false }),
313
- run: (body, ctx) => excalidrawAdd(body as { link: string; name?: string }, ctx),
314
- };
315
- routes["excalidraw.create"] = {
316
- schema: Type.Object({ name: Type.Optional(Type.String({ maxLength: 40 })) }, { additionalProperties: false }),
317
- run: (body, ctx) => excalidrawCreate(body as { name?: string }, ctx),
318
- };
319
- routes["excalidraw.remove"] = {
320
- schema: Type.Object({ id: TaskId }, { additionalProperties: false }),
321
- run: (body, ctx) => excalidrawRemove(body as { id: string }, ctx),
322
- };
323
- routes["excalidraw.rename"] = {
324
- schema: Type.Object({ id: TaskId, name: Type.String({ maxLength: 40 }) }, { additionalProperties: false }),
325
- run: (body, ctx) => excalidrawRename(body as { id: string; name: string }, ctx),
326
- };
327
- routes["excalidraw.toggleAgent"] = {
328
- schema: Type.Object({ id: TaskId, agent: ExcalidrawAgent }, { additionalProperties: false }),
329
- run: (body, ctx) => excalidrawToggleAgent(body as { id: string; agent: "master" | "designer" | "backend" | "qa" | "scout" | "researcher" | "quickfix" | "planner" }, ctx),
330
- };
331
- routes["excalidraw.toggleAll"] = {
332
- schema: Type.Object({ id: TaskId }, { additionalProperties: false }),
333
- run: (body, ctx) => excalidrawToggleAll(body as { id: string }, ctx),
334
- };
335
- routes["excalidraw.toggleContribute"] = {
336
- schema: Type.Object({ id: TaskId }, { additionalProperties: false }),
337
- run: (body, ctx) => excalidrawToggleContribute(body as { id: string }, ctx),
338
- };
339
- routes["excalidraw.check"] = {
340
- schema: Type.Object({ id: TaskId }, { additionalProperties: false }),
341
- run: (body, ctx) => excalidrawCheck(body as { id: string }, ctx),
342
- };
343
- routes["excalidraw.reveal"] = {
344
- schema: Type.Object({ id: TaskId }, { additionalProperties: false }),
345
- run: (body, ctx) => excalidrawReveal(body as { id: string }, ctx),
346
- };
347
308
  routes["git.pulls"] = {
348
309
  schema: Type.Object({ refresh: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
349
310
  run: (body, ctx) => gitPulls(body as { refresh?: boolean }, ctx),
@@ -342,10 +342,6 @@ export function createFixtureService(name: string, feed = new LobbyFeed()): Lobb
342
342
  const classifierMetrics = (fixture.mockClassifierMetrics ?? []).map((record) => ({ ...record }));
343
343
  const knowledgeFiles = (fixture.mockKnowledge ?? []).map((file) => ({ ...file }));
344
344
  const knowledgeView = fixture.mockKnowledgeView ? { ...fixture.mockKnowledgeView, entries: [...fixture.mockKnowledgeView.entries], attached: [...fixture.mockKnowledgeView.attached], detached: [...fixture.mockKnowledgeView.detached] } : undefined;
345
- const xSessions = (fixture.mockExcalidraw ?? []).map((session) => ({ ...session, agents: [...session.agents] }));
346
- const xLinks = new Map<string, string>();
347
- if (xSessions[0] && fixture.mockExcalidrawLink) xLinks.set(xSessions[0].id, fixture.mockExcalidrawLink);
348
- const xLinkOf = (id: string): string => xLinks.get(id) ?? `https://whiteboard.example/#room=${id},AAAAAAAAAAAAAAAAAAAAAA`;
349
345
  const knowledge = {
350
346
  files: () => knowledgeFiles.map((file) => ({ ...file })),
351
347
  open: (agent: string, file: string) => {
@@ -359,57 +355,6 @@ export function createFixtureService(name: string, feed = new LobbyFeed()): Lobb
359
355
  comment: () => "note saved — every agent reads it under this entry",
360
356
  unnote: () => "note removed",
361
357
  };
362
- const excalidraw = {
363
- list: () => xSessions.map((session) => ({ id: session.id, name: session.name, link: xLinkOf(session.id), agents: [...session.agents], contribute: session.contribute, addedAt: session.addedAt })),
364
- add: (link: string, name?: string) => {
365
- if (!link.includes("#room=")) return { notice: "that is not an Excalidraw room link — it looks like https://excalidraw.com/#room=<id>,<key>" };
366
- const session = { id: `x${xSessions.length + 1}`, name: name || `Session ${xSessions.length + 1}`, link, agents: [] as string[], contribute: true, addedAt: new Date().toISOString() };
367
- xSessions.push({ id: session.id, name: session.name, masked: "", agents: [], contribute: true, addedAt: session.addedAt });
368
- xLinks.set(session.id, link);
369
- return { notice: `added “${session.name}” — assign it to agents with enter`, session };
370
- },
371
- create: (name?: string) => {
372
- const id = `x${xSessions.length + 1}`;
373
- const link = `https://whiteboard.example/#room=mock${xSessions.length + 1},AAAAAAAAAAAAAAAAAAAAAA`;
374
- const session = { id, name: name || `Session ${xSessions.length + 1}`, link, agents: [] as string[], contribute: true, addedAt: new Date().toISOString() };
375
- xSessions.push({ id, name: session.name, masked: "", agents: [], contribute: true, addedAt: session.addedAt });
376
- xLinks.set(id, link);
377
- return { notice: `added “${session.name}” — assign it to agents with enter`, session };
378
- },
379
- remove: (id: string) => {
380
- const index = xSessions.findIndex((session) => session.id === id);
381
- if (index < 0) return "no such session";
382
- const [gone] = xSessions.splice(index, 1);
383
- return `removed “${gone!.name}”`;
384
- },
385
- rename: (id: string, name: string) => {
386
- const session = xSessions.find((entry) => entry.id === id);
387
- if (!session) return "no such session";
388
- if (!name.trim()) return "a session needs a name";
389
- session.name = name.trim().slice(0, 40);
390
- return `renamed to “${session.name}”`;
391
- },
392
- toggleAgent: (id: string, agent: string) => {
393
- const session = xSessions.find((entry) => entry.id === id);
394
- if (!session) return "no such session";
395
- const at = session.agents.indexOf(agent);
396
- if (at >= 0) session.agents.splice(at, 1);
397
- else session.agents.push(agent);
398
- return at >= 0 ? `${agent} no longer has this session` : `${agent} has this session now`;
399
- },
400
- toggleAll: (id: string) => {
401
- const session = xSessions.find((entry) => entry.id === id);
402
- if (!session) return "no such session";
403
- session.agents = session.agents.length > 0 ? [] : ["master"];
404
- return session.agents.length > 0 ? "assigned to every agent" : "taken back from every agent";
405
- },
406
- toggleContribute: (id: string) => {
407
- const session = xSessions.find((entry) => entry.id === id);
408
- if (!session) return "no such session";
409
- session.contribute = !session.contribute;
410
- return session.contribute ? "agents may draw in this session" : "agents may only look at this session";
411
- },
412
- };
413
358
  const qfJobs = fakeQuickFixJobs(fixture.mockQuickfix ?? []);
414
359
  let qfCounter = qfJobs.length;
415
360
  const quickfix = {
@@ -485,8 +430,6 @@ export function createFixtureService(name: string, feed = new LobbyFeed()): Lobb
485
430
  metrics: () => metrics.map((record) => ({ ...record })),
486
431
  classifierMetrics: () => classifierMetrics.map((record) => ({ ...record })),
487
432
  knowledge,
488
- excalidraw,
489
- checkExcalidraw: async () => ({ ok: true, text: "reached the server; nobody has this session open yet — open the link in Excalidraw, and agents can read and draw" }),
490
433
  comments: (taskId: string) => [...(comments.get(taskId) ?? [])],
491
434
  comment: (taskId: string, text: string) => {
492
435
  const entry = { id: `C-mock-${(commentSeq += 1)}`, taskId, text, by: fixture.status.sessionId, createdAt: new Date().toISOString(), status: "open" };
@@ -101,10 +101,6 @@ export interface ScenarioFixture {
101
101
  mockKnowledge?: Array<{ agent: string; file: string; label: string; chars: number; over: boolean; notes: number }>;
102
102
  /** One open knowledge file for `knowledge.open` (absent means an empty file). */
103
103
  mockKnowledgeView?: { agent: string; file: string; label: string; chars: number; over: boolean; notes: number; content: string; entries: Array<Record<string, unknown>>; attached: Array<Record<string, unknown>>; detached: Array<Record<string, unknown>> };
104
- /** Excalidraw sessions for `excalidraw.list` (masked links only; absent means none). */
105
- mockExcalidraw?: Array<{ id: string; name: string; masked: string; agents: string[]; contribute: boolean; addedAt: string }>;
106
- /** The full link `excalidraw.reveal` returns (absent means none). */
107
- mockExcalidrawLink?: string;
108
104
  /** Open pull requests for the `git.*` calls, each with its detail fields (absent means none). */
109
105
  mockPulls?: Array<Record<string, unknown>>;
110
106
  /** Saved reviews of those pull requests, by number. */
@@ -173,8 +169,6 @@ export function loadScenario(name: string | undefined): ScenarioFixture {
173
169
  mockClassifierMetrics: Array.isArray(parsed.mockClassifierMetrics) ? (parsed.mockClassifierMetrics as ScenarioFixture["mockClassifierMetrics"]) : [],
174
170
  mockKnowledge: Array.isArray(parsed.mockKnowledge) ? (parsed.mockKnowledge as ScenarioFixture["mockKnowledge"]) : [],
175
171
  mockKnowledgeView: isRecord(parsed.mockKnowledgeView) ? (parsed.mockKnowledgeView as unknown as ScenarioFixture["mockKnowledgeView"]) : undefined,
176
- mockExcalidraw: Array.isArray(parsed.mockExcalidraw) ? (parsed.mockExcalidraw as ScenarioFixture["mockExcalidraw"]) : [],
177
- mockExcalidrawLink: typeof parsed.mockExcalidrawLink === "string" ? (parsed.mockExcalidrawLink as string) : undefined,
178
172
  mockPulls: Array.isArray(parsed.mockPulls) ? (parsed.mockPulls as ScenarioFixture["mockPulls"]) : [],
179
173
  mockReviews: Array.isArray(parsed.mockReviews) ? (parsed.mockReviews as ScenarioFixture["mockReviews"]) : [],
180
174
  mockReads: Array.isArray(parsed.mockReads) ? (parsed.mockReads as ScenarioFixture["mockReads"]) : [],
@@ -393,25 +393,6 @@ export interface EntryRefInfo {
393
393
  occurrence: number;
394
394
  }
395
395
 
396
- /** Who an Excalidraw session can be assigned to. */
397
- export type ExcalidrawAgentName = "master" | "designer" | "backend" | "qa" | "scout" | "researcher" | "quickfix" | "planner";
398
-
399
- /** One shared session with its key hidden (`room <id>`). */
400
- export interface ExcalidrawSessionInfo {
401
- id: string;
402
- name: string;
403
- masked: string;
404
- agents: ExcalidrawAgentName[];
405
- contribute: boolean;
406
- addedAt: string;
407
- }
408
-
409
- /** What the last check of a session found. */
410
- export interface ExcalidrawCheck {
411
- ok: boolean;
412
- text: string;
413
- }
414
-
415
396
  /** `tasks.get`: one task read whole, in the terminal's wording. */
416
397
  export interface TaskDetail {
417
398
  timing?: TimingProjection;
@@ -655,16 +636,6 @@ export interface Api {
655
636
  "knowledge.replaceFile": { request: { agent: KnowledgeAgentName; file: string; text: string }; result: { notice: string } };
656
637
  "knowledge.comment": { request: { agent: KnowledgeAgentName; file: string; ref: EntryRefInfo; text: string }; result: { notice: string } };
657
638
  "knowledge.unnote": { request: { id: string }; result: { notice: string } };
658
- "excalidraw.list": { request: Record<string, never>; result: { sessions: ExcalidrawSessionInfo[] } };
659
- "excalidraw.add": { request: { link: string; name?: string }; result: { notice: string; id?: string } };
660
- "excalidraw.create": { request: { name?: string }; result: { notice: string; id?: string } };
661
- "excalidraw.remove": { request: { id: string }; result: { notice: string } };
662
- "excalidraw.rename": { request: { id: string; name: string }; result: { notice: string } };
663
- "excalidraw.toggleAgent": { request: { id: string; agent: ExcalidrawAgentName }; result: { notice: string } };
664
- "excalidraw.toggleAll": { request: { id: string }; result: { notice: string } };
665
- "excalidraw.toggleContribute": { request: { id: string }; result: { notice: string } };
666
- "excalidraw.check": { request: { id: string }; result: ExcalidrawCheck };
667
- "excalidraw.reveal": { request: { id: string }; result: { link: string } };
668
639
  "git.pulls": { request: { refresh?: boolean }; result: GitPulls };
669
640
  "git.pull": { request: { number: number }; result: GitPull };
670
641
  "git.review": { request: { number: number; focus?: string }; result: { notice: string } };
@@ -20,7 +20,6 @@ import { loadTask, ownerlessTask, readTaskArtifact, removeTaskScratchpads, saveT
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";
24
23
  import { knowledgeDir, type KnowledgeAgent } from "../knowledge/paths.ts";
25
24
  import { lobbyTopics } from "../lobby/topics.ts";
26
25
  import { writeScratchpad } from "../state/persistence.ts";
@@ -92,7 +91,6 @@ export const ORCHESTRATE_ACTIONS = [
92
91
  "knowledge",
93
92
  "compact",
94
93
  "script",
95
- "whiteboard",
96
94
  "complete",
97
95
  "block",
98
96
  "resume",
@@ -130,7 +128,7 @@ export interface OrchestrateParams {
130
128
  kind?: KnowledgeKind;
131
129
  /** compact: the knowledge file being rewritten; script: a drafted script to take instead of `text`. */
132
130
  file?: string;
133
- /** whiteboard: name for the new Excalidraw session; script: the script's file name (`seed_public_holidays.py`). */
131
+ /** script: the script's file name (`seed_public_holidays.py`). */
134
132
  name?: string;
135
133
  /** script: one line on what the script does. */
136
134
  purpose?: string;
@@ -1542,19 +1540,6 @@ function handleCompact(task: Task | undefined, params: OrchestrateParams, deps:
1542
1540
  return `Compacted ${agent}/${file}: ${outcome.before} -> ${outcome.after} chars. Previous version archived at ${outcome.archive}.`;
1543
1541
  }
1544
1542
 
1545
- /**
1546
- * The Master creates its own whiteboard: a room on the configured server,
1547
- * assigned to itself so its seat keeps the room alive for this pi session.
1548
- */
1549
- function handleWhiteboard(task: Task, params: OrchestrateParams, deps: WorkflowDeps): string {
1550
- const book = new ExcalidrawBook({ root: deps.root });
1551
- const made = book.create(params.name?.trim() || undefined);
1552
- if (!made.session) throw new Error(made.notice);
1553
- book.toggleAgent(made.session.id, "master");
1554
- recordDecision(task, `Created Excalidraw session "${made.session.name}" and assigned it to Master.`);
1555
- 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.`;
1556
- }
1557
-
1558
1543
  /** §63: record history, drop scratchpads, then mark the task completed. */
1559
1544
  async function handleComplete(task: Task, params: OrchestrateParams, deps: WorkflowDeps): Promise<string> {
1560
1545
  // The fast track (and a plan QA sat out) completes straight from its work; the full workflow from review.
@@ -2068,7 +2053,6 @@ const HANDLERS: Record<OrchestrateAction, (task: Task, params: OrchestrateParams
2068
2053
  knowledge: handleKnowledge,
2069
2054
  script: handleScript,
2070
2055
  compact: handleCompact,
2071
- whiteboard: handleWhiteboard,
2072
2056
  complete: handleComplete,
2073
2057
  block: handleBlock,
2074
2058
  resume: handleResume,