@a-t-h-i/bot-lobby 0.6.57 → 0.6.59

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.
Files changed (118) hide show
  1. package/README.md +17 -11
  2. package/THIRD_PARTY_NOTICES.md +17 -3
  3. package/package.json +6 -1
  4. package/prompts/lobby-chat.md +39 -0
  5. package/prompts/master.md +7 -7
  6. package/prompts/reviewer.md +6 -6
  7. package/src/classifier/instance.ts +1 -1
  8. package/src/classifier/lobby.ts +37 -0
  9. package/src/classifier/triage.ts +2 -24
  10. package/src/execution/delegation.ts +2 -2
  11. package/src/index.ts +0 -3
  12. package/src/lobby/chat-agents.ts +293 -0
  13. package/src/lobby/feed.ts +73 -17
  14. package/src/lobby/host.ts +8 -4
  15. package/src/lobby/keys.ts +1 -0
  16. package/src/lobby/planner.ts +5 -5
  17. package/src/lobby/pr-review.ts +2 -2
  18. package/src/lobby/profile.ts +16 -0
  19. package/src/lobby/prompts.ts +56 -4
  20. package/src/lobby/recovery.ts +10 -42
  21. package/src/lobby/runtime.ts +27 -45
  22. package/src/lobby/service.ts +142 -13
  23. package/src/lobby/session-files.ts +4 -1
  24. package/src/lobby/topics.ts +0 -1
  25. package/src/master/master.ts +1 -1
  26. package/src/pi/commands.ts +6 -8
  27. package/src/pi/events.ts +11 -3
  28. package/src/pi/fresh-context.ts +1 -1
  29. package/src/pi/model-settings.ts +0 -2
  30. package/src/pi/model-support.ts +11 -2
  31. package/src/pi/owner.ts +35 -4
  32. package/src/pi/start-task.ts +3 -9
  33. package/src/pi/switch.ts +1 -1
  34. package/src/schemas/configuration.ts +14 -24
  35. package/src/state/changes.ts +25 -23
  36. package/src/state/metrics.ts +5 -8
  37. package/src/state/recovery.ts +3 -3
  38. package/src/webui/api/index.ts +12 -18
  39. package/src/webui/api/lobby.ts +9 -6
  40. package/src/webui/code.ts +202 -0
  41. package/src/webui/dev/fake-service.ts +28 -62
  42. package/src/webui/dev/fixtures.ts +1 -4
  43. package/src/webui/protocol.ts +15 -9
  44. package/src/webui/static.ts +5 -2
  45. package/src/workflow/track.ts +1 -52
  46. package/src/workflow/workflow.ts +2 -2
  47. package/webui/dist/assets/CodeSurface-BtzJOVyH.js +448 -0
  48. package/webui/dist/assets/{TerminalView-C8VC7KXy.js → TerminalView-Cz-KNWsq.js} +2 -2
  49. package/webui/dist/assets/astro-p42oEArs.js +1 -0
  50. package/webui/dist/assets/c-DaO8xguW.js +1 -0
  51. package/webui/dist/assets/ccount-DQxSH8e6.js +1 -0
  52. package/webui/dist/assets/csharp-dmMxLVLW.js +1 -0
  53. package/webui/dist/assets/css-C8fQr_0Q.js +1 -0
  54. package/webui/dist/assets/diff-woXpYk--.js +1 -0
  55. package/webui/dist/assets/dockerfile-IyjqRm3v.js +1 -0
  56. package/webui/dist/assets/dotenv-CuH5my32.js +1 -0
  57. package/webui/dist/assets/fish-BJitypiv.js +1 -0
  58. package/webui/dist/assets/go-rLFTqkRN.js +1 -0
  59. package/webui/dist/assets/graphql-AVu4aKQX.js +1 -0
  60. package/webui/dist/assets/html-wSbaAAK3.js +1 -0
  61. package/webui/dist/assets/index-D5fvY8f8.js +112 -0
  62. package/webui/dist/assets/index-DFvk8ADr.css +2 -0
  63. package/webui/dist/assets/ini-B5eOa1yu.js +1 -0
  64. package/webui/dist/assets/java-DdHNQ9hk.js +1 -0
  65. package/webui/dist/assets/javascript-BgS3c2Ky.js +1 -0
  66. package/webui/dist/assets/json-qhed-kSA.js +1 -0
  67. package/webui/dist/assets/json5-BR5RXkoi.js +1 -0
  68. package/webui/dist/assets/jsonc-CYpm1nAK.js +1 -0
  69. package/webui/dist/assets/jsonl-CmCQp5Yx.js +1 -0
  70. package/webui/dist/assets/jsx-gTgmArGx.js +1 -0
  71. package/webui/dist/assets/kotlin-DhhofPvG.js +1 -0
  72. package/webui/dist/assets/kts-DFAlwc6n.js +1 -0
  73. package/webui/dist/assets/less-DVTAwKKz.js +1 -0
  74. package/webui/dist/assets/lua-xZcbXUE9.js +1 -0
  75. package/webui/dist/assets/makefile-Dixweg8N.js +1 -0
  76. package/webui/dist/assets/markdown-BYOwaDjH.js +1 -0
  77. package/webui/dist/assets/php-C6PDnbTe.js +1 -0
  78. package/webui/dist/assets/pierre-dark-CpLgRqie.js +1 -0
  79. package/webui/dist/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
  80. package/webui/dist/assets/pierre-dark-soft-kZQmAZld.js +1 -0
  81. package/webui/dist/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
  82. package/webui/dist/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
  83. package/webui/dist/assets/pierre-light-CoaEpmwp.js +1 -0
  84. package/webui/dist/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
  85. package/webui/dist/assets/pierre-light-soft-lWLdNTOI.js +1 -0
  86. package/webui/dist/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
  87. package/webui/dist/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
  88. package/webui/dist/assets/postcss-D_Wz94rJ.js +1 -0
  89. package/webui/dist/assets/powershell-QtSnmXcu.js +1 -0
  90. package/webui/dist/assets/prisma-DMgNhnYS.js +1 -0
  91. package/webui/dist/assets/properties-CxAuzHS9.js +1 -0
  92. package/webui/dist/assets/python-gzcpVVnB.js +1 -0
  93. package/webui/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
  94. package/webui/dist/assets/ruby-CPL7DL0w.js +1 -0
  95. package/webui/dist/assets/rust-DpiIf5JO.js +1 -0
  96. package/webui/dist/assets/sass-DXrisJhu.js +1 -0
  97. package/webui/dist/assets/scss-CHVQqdPx.js +1 -0
  98. package/webui/dist/assets/shellscript-CLZ0U2zV.js +1 -0
  99. package/webui/dist/assets/sql-CEbI0hMM.js +1 -0
  100. package/webui/dist/assets/svelte-DKhxH1PN.js +1 -0
  101. package/webui/dist/assets/swift-CyEgAFGc.js +1 -0
  102. package/webui/dist/assets/toml-CcmNWLt0.js +1 -0
  103. package/webui/dist/assets/tsx-CUOciP0b.js +1 -0
  104. package/webui/dist/assets/typescript-BydbNFcO.js +1 -0
  105. package/webui/dist/assets/vue-kBwcnTZb.js +1 -0
  106. package/webui/dist/assets/wasm-BnjxR4X6.js +1 -0
  107. package/webui/dist/assets/xml-CeXJrFlG.js +1 -0
  108. package/webui/dist/assets/yaml-rwi0_p6S.js +1 -0
  109. package/webui/dist/assets/yml-s9iVJXCw.js +1 -0
  110. package/webui/dist/assets/zsh-B7CDPTg5.js +1 -0
  111. package/webui/dist/build.json +61 -45
  112. package/webui/dist/index.html +4 -2
  113. package/prompts/quickfix.md +0 -67
  114. package/src/lobby/quickfix.ts +0 -546
  115. package/src/pi/route.ts +0 -180
  116. package/src/webui/api/quickfix.ts +0 -49
  117. package/webui/dist/assets/index-Bs1_77_1.js +0 -114
  118. package/webui/dist/assets/index-DOLQ4FDd.css +0 -2
package/src/lobby/feed.ts CHANGED
@@ -14,7 +14,7 @@ export type ActivityKind = "info" | "success" | "warning" | "error";
14
14
  export interface ActivityEntry {
15
15
  id: number;
16
16
  at: number;
17
- /** Who acted: MASTER, DEV, DESIGN, QA, RESEARCH, QUICK FIX, PLANNER, LOBBY. */
17
+ /** Who acted: MASTER, DEV, DESIGN, QA, RESEARCH, SCOUT, PLANNER, LOBBY. */
18
18
  source: string;
19
19
  text: string;
20
20
  kind: ActivityKind;
@@ -35,13 +35,24 @@ export interface ThoughtEntry {
35
35
 
36
36
  export type ChatRole = "you" | "oracle" | "note";
37
37
 
38
- export interface ChatEntry {
38
+ /** Who a message was for (yours) or who wrote it (a reply): `DESIGN`, `DEV`, …; absent is the oracle. */
39
+ export interface ChatSpeaker {
40
+ /** The agent that wrote a reply. */
41
+ agent?: string;
42
+ /** The agent your message went to. */
43
+ to?: string;
44
+ }
45
+
46
+ export interface ChatEntry extends ChatSpeaker {
39
47
  id: number;
40
48
  at: number;
41
49
  role: ChatRole;
42
50
  text: string;
43
51
  }
44
52
 
53
+ /** A conversation line before it is numbered: what the session's entries read as. */
54
+ export type ChatLine = { role: ChatRole; text: string; at?: number } & ChatSpeaker;
55
+
45
56
  export const MAX_ACTIVITY = 400;
46
57
  export const MAX_THOUGHTS = 40;
47
58
  /** Messages the conversation keeps in memory; earlier ones are loaded from the session when scrolled to. */
@@ -71,7 +82,7 @@ export class LobbyFeed {
71
82
  /** Last step reported per run, so repeated run updates log each step once. */
72
83
  private readonly runSteps = new Map<string, string>();
73
84
  private readonly runThoughts = new Map<string, string>();
74
- /** The live whole thought of each piece of work still under way (a run, a quick fix, a review), by its key. */
85
+ /** The live whole thought of each piece of work still under way (a run, a chat agent's reply, a review), by its key. */
75
86
  private readonly liveThoughts = new Map<string, number>();
76
87
  private readonly runStatus = new Map<string, AgentRun["status"]>();
77
88
  /** Runs already reported as switched to their fallback model. */
@@ -122,6 +133,11 @@ export class LobbyFeed {
122
133
  if (this.settle(undefined, key, isError ? "error" : undefined)) this.touch();
123
134
  }
124
135
 
136
+ /** Settle every step `source` still has open (a turn that ended without closing its tool calls). */
137
+ endAll(source: string): void {
138
+ if (this.settle(source, undefined)) this.touch();
139
+ }
140
+
125
141
  /** The next step of an agent that reports one step at a time: the previous one settles. */
126
142
  step(source: string, text: string, key: string, at = Date.now()): void {
127
143
  this.settle(undefined, key);
@@ -165,7 +181,7 @@ export class LobbyFeed {
165
181
  }
166
182
 
167
183
  /**
168
- * A whole thought from work still under way (`key`: its run, quick fix or
184
+ * A whole thought from work still under way (`key`: its run, chat reply or
169
185
  * review): live, so the Thinking orb takes its agent's colour, until the
170
186
  * same work's next thought or `settleThought(key)` when it ends.
171
187
  */
@@ -214,6 +230,15 @@ export class LobbyFeed {
214
230
  this.say("you", body, at);
215
231
  }
216
232
 
233
+ /** Mark the newest message of yours with this text as one that went to `to` (an agent, not the oracle). */
234
+ addressed(text: string, to: string): void {
235
+ const body = text.trim();
236
+ const entry = [...this.chat].reverse().find((line) => line.role === "you" && line.text === body);
237
+ if (!entry || entry.to === to) return;
238
+ entry.to = to;
239
+ this.touch();
240
+ }
241
+
217
242
  /** Whether `text` is a queued message already on show; it is forgotten once delivered. */
218
243
  takeQueued(text: string): boolean {
219
244
  const index = this.queued.indexOf(text.trim());
@@ -222,10 +247,10 @@ export class LobbyFeed {
222
247
  return true;
223
248
  }
224
249
 
225
- say(role: ChatRole, text: string, at = Date.now()): void {
250
+ say(role: ChatRole, text: string, at = Date.now(), speaker: ChatSpeaker = {}): void {
226
251
  const body = text.trim();
227
252
  if (!body) return;
228
- this.chat.push({ id: this.nextId++, at, role, text: body });
253
+ this.chat.push({ id: this.nextId++, at, role, text: body, ...speaker });
229
254
  if (this.chat.length > MAX_CHAT) {
230
255
  this.chat.splice(0, this.chat.length - MAX_CHAT);
231
256
  this.chatOlder = true;
@@ -234,10 +259,10 @@ export class LobbyFeed {
234
259
  }
235
260
 
236
261
  /** Replace the conversation (seeding from the session when the lobby first opens); only the newest `MAX_CHAT` are kept. */
237
- seedChat(entries: ReadonlyArray<{ role: ChatRole; text: string; at?: number }>): void {
262
+ seedChat(entries: readonly ChatLine[]): void {
238
263
  const kept = entries.filter((entry) => entry.text.trim());
239
264
  this.chatOlder = kept.length > MAX_CHAT;
240
- this.chat = kept.slice(-MAX_CHAT).map((entry) => ({ id: this.nextId++, at: entry.at ?? 0, role: entry.role, text: entry.text.trim() }));
265
+ this.chat = kept.slice(-MAX_CHAT).map((entry) => ({ id: this.nextId++, at: entry.at ?? 0, role: entry.role, text: entry.text.trim(), ...(entry.agent ? { agent: entry.agent } : {}), ...(entry.to ? { to: entry.to } : {}) }));
241
266
  this.touch();
242
267
  }
243
268
 
@@ -343,18 +368,18 @@ export function chatText(role: "user" | "assistant", text: string): Array<{ role
343
368
  const kickoff = /^A bot-lobby task is active: (\S+)\nTitle: (.*)/.exec(body);
344
369
  if (kickoff) {
345
370
  const request = /\nRequest: ([\s\S]*?)(?:\nState: |$)/.exec(body)?.[1]?.trim();
346
- // A request the oracle routed to the team is already in the conversation, under the routing note.
347
- const shown = /\nRouted: /.test(body);
348
- return [{ role: "note", text: `task started · ${kickoff[2]}` }, ...(request && !shown ? [{ role: "you" as const, text: request }] : [])];
371
+ return [{ role: "note", text: `task started · ${kickoff[2]}` }, ...(request ? [{ role: "you" as const, text: request }] : [])];
349
372
  }
350
- // A new request the classifier read as a quick fix, before the oracle confirms where it goes.
351
- const routing = /^bot-lobby: a new request, not a task yet\.\nRequest: ([\s\S]*?)\nRead: /.exec(body);
352
- if (routing) return [{ role: "you", text: routing[1]!.trim() }, { role: "note", text: "reads as a quick fix · the oracle confirms where it goes" }];
373
+ // Auto mode's nudge to keep going (pi/owner.ts autoNudge) shows as one short line.
374
+ if (/^Auto mode: \S+ is \S+\. Keep driving it to completion/.test(body)) return [{ role: "note", text: AUTO_NOTE }];
353
375
  const comment = /^The user left (?:a comment|\d+ comments) on (the approved plan|the proposal) of (\S+) from the lobby:/.exec(body);
354
376
  if (comment) return [{ role: "note", text: `your comment on ${comment[1]} went to the oracle` }];
355
377
  return [{ role: "you", text: body }];
356
378
  }
357
379
 
380
+ /** What the conversation shows where auto mode kept the oracle going. */
381
+ export const AUTO_NOTE = "auto mode";
382
+
358
383
  /** The session entry bot-lobby leaves where the oracle's context starts over (see pi/fresh-context.ts). */
359
384
  export const CONTEXT_MARK = "bot-lobby-context";
360
385
 
@@ -367,9 +392,34 @@ export function isClearedMark(entry: unknown): boolean {
367
392
  return record?.type === "custom" && record.customType === CONTEXT_MARK && record.data?.kind === "end";
368
393
  }
369
394
 
370
- /** Session entries (`{ type: "message", message }`) as conversation entries, oldest first. */
371
- export function chatFromEntries(entries: readonly unknown[], max = MAX_CHAT): Array<{ role: ChatRole; text: string; at?: number }> {
372
- const chat: Array<{ role: ChatRole; text: string; at?: number }> = [];
395
+ /** The session entry that keeps one line of the lobby chat an agent other than the oracle took part in. */
396
+ export const CHAT_MARK = "bot-lobby-chat";
397
+
398
+ /** What a `CHAT_MARK` entry carries: your message to an agent, or its reply. */
399
+ export interface ChatMarkDetails {
400
+ role: "you" | "agent";
401
+ /** The agent: who it went to, or who replied. */
402
+ agent: string;
403
+ text: string;
404
+ }
405
+
406
+ /** What the Master reads of a lobby line it did not take part in (the entry's content). */
407
+ export function chatMarkContent(details: ChatMarkDetails): string {
408
+ return details.role === "you" ? `The user asked ${details.agent} in the lobby chat:\n\n${details.text}` : `${details.agent} answered in the lobby chat:\n\n${details.text}`;
409
+ }
410
+
411
+ /** A `CHAT_MARK` session entry as its conversation line; undefined for any other entry. */
412
+ export function chatMarkLine(entry: unknown): ChatLine | undefined {
413
+ const record = entry as { type?: string; customType?: string; details?: Partial<ChatMarkDetails> } | undefined;
414
+ if (record?.type !== "custom_message" || record.customType !== CHAT_MARK) return undefined;
415
+ const details = record.details;
416
+ if (!details || typeof details.text !== "string" || typeof details.agent !== "string") return undefined;
417
+ return details.role === "you" ? { role: "you", text: details.text, to: details.agent } : { role: "oracle", text: details.text, agent: details.agent };
418
+ }
419
+
420
+ /** Session entries (`{ type: "message", message }`, and the chat's own marks) as conversation entries, oldest first. */
421
+ export function chatFromEntries(entries: readonly unknown[], max = MAX_CHAT): ChatLine[] {
422
+ const chat: ChatLine[] = [];
373
423
  for (const entry of entries) {
374
424
  const record = entry as { type?: string; timestamp?: string; message?: { role?: string; content?: unknown } };
375
425
  if (isClearedMark(entry)) {
@@ -377,6 +427,12 @@ export function chatFromEntries(entries: readonly unknown[], max = MAX_CHAT): Ar
377
427
  chat.push({ role: "note", text: CLEARED_NOTE, ...(Number.isFinite(at) ? { at } : {}) });
378
428
  continue;
379
429
  }
430
+ const marked = chatMarkLine(entry);
431
+ if (marked) {
432
+ const at = record.timestamp ? Date.parse(record.timestamp) : Number.NaN;
433
+ chat.push({ ...marked, ...(Number.isFinite(at) ? { at } : {}) });
434
+ continue;
435
+ }
380
436
  if (record?.type !== "message") continue;
381
437
  const role = record.message?.role;
382
438
  if (role !== "user" && role !== "assistant") continue;
package/src/lobby/host.ts CHANGED
@@ -11,7 +11,6 @@ import type { MetricRecord } from "../state/metrics.ts";
11
11
  import type { BotLobbyConfig, LobbyAgentKind, PanelMember } from "../schemas/configuration.ts";
12
12
  import type { ChatEntry, LobbyFeed } from "./feed.ts";
13
13
  import type { BackgroundSession } from "./sessions.ts";
14
- import type { QuickFixQueue } from "./quickfix.ts";
15
14
  import type { PlannerSeed, PlanningSession } from "./planner.ts";
16
15
  import type { PlanRecord, PlanSummary } from "../state/plan-history.ts";
17
16
  import type { IssuesState } from "./issues.ts";
@@ -88,6 +87,8 @@ export interface LobbyService {
88
87
  zen(): { task?: Task; runs: readonly AgentRun[] };
89
88
  feed: LobbyFeed;
90
89
  masterBusy(): boolean;
90
+ /** The agent other than the oracle answering in the chat now (`DESIGN`, …), if any. */
91
+ answering?(): string | undefined;
91
92
  tasks(): Task[];
92
93
  /** How long a task's agents have worked on it, and the worker runs under way (`work.json`); undefined without one. */
93
94
  work?(taskId: string): import("../state/work-time.ts").WorkProjection | undefined;
@@ -99,8 +100,12 @@ export interface LobbyService {
99
100
  metrics(): MetricRecord[];
100
101
  /** Classifier calls (kind `classifier`), kept out of the agent tables. */
101
102
  classifierMetrics?(): MetricRecord[];
102
- /** Send text to the oracle, or start a task when none is active; returns a notice. */
103
- toOracle(text: string): string | undefined;
103
+ /**
104
+ * A message in the lobby chat: to the oracle, the agent it names or the
105
+ * classifier picks, or a `/plan` or `/task`; returns a notice, and the tab
106
+ * to open when it moved somewhere else.
107
+ */
108
+ toOracle(text: string): string | { notice: string; open: "plan" } | undefined;
104
109
  /** Answer a project question and learn from the codebase, without creating a task. */
105
110
  askKnowledge(text: string): void;
106
111
  comment(taskId: string, text: string): string;
@@ -109,7 +114,6 @@ export interface LobbyService {
109
114
  startPlanned(plan: PlannedTask): string;
110
115
  discardPlan(id: string): void;
111
116
  abortMaster(): void;
112
- quickfix: QuickFixQueue;
113
117
  planner(): PlanningSession | undefined;
114
118
  /** The project's previous plans (never saved as a task), newest first; the archived ones when asked. */
115
119
  previousPlans(archived: boolean): PlanSummary[];
package/src/lobby/keys.ts CHANGED
@@ -13,6 +13,7 @@ export const LOBBY_ACTIONS = {
13
13
  activity: { key: "alt+a", help: "open or fold every step in the Lobby conversation" },
14
14
  thinking: { key: "alt+t", help: "show or hide Thinking on the Lobby" },
15
15
  terminal: { key: "ctrl+`", help: "show or hide the terminal" },
16
+ code: { key: "alt+e", help: "open or close the code viewer: changed files, diffs and editing" },
16
17
  nextTab: { key: "alt+]", help: "next tab" },
17
18
  prevTab: { key: "alt+[", help: "previous tab" },
18
19
  } as const;
@@ -31,7 +31,7 @@ import type { AskQuestion, AskResult } from "../ask/types.ts";
31
31
  import { PANEL_MEMBERS, type PanelMember } from "../schemas/configuration.ts";
32
32
  import { truncate } from "../text.ts";
33
33
  import type { LobbyFeed } from "./feed.ts";
34
- import type { QuickFixProfile } from "./quickfix.ts";
34
+ import type { LobbyRunProfile } from "./profile.ts";
35
35
  import { MAX_QUESTIONS } from "./ask.ts";
36
36
  import type { Classifier } from "../classifier/classifier.ts";
37
37
  import { chooseSeats } from "../classifier/seats.ts";
@@ -138,9 +138,9 @@ export interface PlannerDeps {
138
138
  root: string;
139
139
  configDir: string;
140
140
  /** The oracle chairing the panel (the Planner settings entry). */
141
- profile: () => QuickFixProfile;
141
+ profile: () => LobbyRunProfile;
142
142
  /** A seat's model, thinking and grants; the oracle's profile when absent. */
143
- memberProfile?: (member: PanelMember) => QuickFixProfile;
143
+ memberProfile?: (member: PanelMember) => LobbyRunProfile;
144
144
  /** Seats on the panel when the session starts; every seat when absent. */
145
145
  panel?: readonly PanelMember[];
146
146
  stallTimeoutMs?: number;
@@ -995,7 +995,7 @@ export class PlanningSession {
995
995
  this.deps.onChange?.();
996
996
  }
997
997
 
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> {
998
+ private async run(label: string, kind: "planner" | "panel", profile: LobbyRunProfile, request: { task: string; systemPrompt: string; tools: readonly string[] }, signal: AbortSignal, setStep: (step: string) => void, routedFrom?: string): Promise<RunOutcome> {
999
999
  const startedAt = Date.now();
1000
1000
  const runId = randomUUID();
1001
1001
  let outcome: RunOutcome;
@@ -1271,7 +1271,7 @@ export function planningMetric(
1271
1271
  turn: number,
1272
1272
  startedAt: number,
1273
1273
  status: MetricRecord["status"],
1274
- profile: QuickFixProfile,
1274
+ profile: LobbyRunProfile,
1275
1275
  model: string | undefined,
1276
1276
  usage: { input: number; output: number; cost: number; turns: number },
1277
1277
  now = Date.now(),
@@ -19,7 +19,7 @@ import { dataRoot } from "../state/project.ts";
19
19
  import type { Classifier } from "../classifier/classifier.ts";
20
20
  import { readLine, readPull, type PullRead } from "../classifier/review.ts";
21
21
  import type { LobbyFeed } from "./feed.ts";
22
- import type { QuickFixProfile } from "./quickfix.ts";
22
+ import type { LobbyRunProfile } from "./profile.ts";
23
23
  import { pullDiff, viewPull, type PullDetail } from "./pulls.ts";
24
24
  import type { Exec } from "./issues.ts";
25
25
 
@@ -75,7 +75,7 @@ export interface PullReviewDeps {
75
75
  configDir: string;
76
76
  exec: Exec;
77
77
  /** QA's model, thinking and grants, read as a review starts. */
78
- profile: () => QuickFixProfile;
78
+ profile: () => LobbyRunProfile;
79
79
  stallTimeoutMs?: number;
80
80
  toolStallTimeoutMs?: number;
81
81
  feed?: LobbyFeed;
@@ -0,0 +1,16 @@
1
+ import type { HarnessRun } from "../schemas/harness.ts";
2
+
3
+ /** The model, thinking and grants a lobby agent (the planner, a panel seat, a pull request review) runs with, read from settings at start time. */
4
+ export interface LobbyRunProfile {
5
+ model?: string;
6
+ thinking: string;
7
+ instructions?: string;
8
+ mcpTools?: string[];
9
+ /** Skill names it may load; absent keeps every skill pi finds. */
10
+ skills?: string[];
11
+ /** False when its settings turn the agent's auto-compaction off. */
12
+ autoCompact?: boolean;
13
+ /** The third-party harness it runs on; absent runs it on pi. */
14
+ harness?: HarnessRun;
15
+ compactAt?: number;
16
+ }
@@ -1,17 +1,17 @@
1
1
  /**
2
- * Strings the server and the page share: tab metadata and the short line
3
- * shown when a question was answered elsewhere. Pure on purpose (no
2
+ * Strings the server and the page share: tab metadata, the short line shown
3
+ * when a question was answered elsewhere, and the lobby chat's agents, @
4
+ * mentions and slash commands. Pure on purpose (no
4
5
  * `node:*`): the web page imports this file directly.
5
6
  */
6
7
 
7
- export const TAB_IDS = ["lobby", "tasks", "plan", "quickfix", "git", "knowledge"] as const;
8
+ export const TAB_IDS = ["lobby", "tasks", "plan", "git", "knowledge"] as const;
8
9
  export type TabId = (typeof TAB_IDS)[number];
9
10
 
10
11
  export const TAB_LABELS: Record<TabId, string> = {
11
12
  lobby: "Lobby",
12
13
  tasks: "Tasks",
13
14
  plan: "Plan",
14
- quickfix: "Quick fix",
15
15
  git: "Git",
16
16
  knowledge: "Knowledge",
17
17
  };
@@ -26,3 +26,55 @@ export const ANSWERED_ELSEWHERE = "already answered in another window";
26
26
 
27
27
  /** Opens the list of files a message carries (see src/webui/uploads.ts); the page shows them as thumbnails and chips. */
28
28
  export const ATTACHMENTS_MARK = "Attached files:";
29
+
30
+ /* ------------------------------------------------------------ the lobby chat */
31
+
32
+ /** Who can answer in the lobby chat: the oracle (MASTER) or one of the agents, on its own settings. */
33
+ export const CHAT_AGENTS = ["master", "designer", "backend", "qa", "researcher", "scout"] as const;
34
+ export type ChatAgent = (typeof CHAT_AGENTS)[number];
35
+
36
+ /** Each one's name as the feeds and the chat show it. */
37
+ export const CHAT_AGENT_SOURCES: Record<ChatAgent, string> = {
38
+ master: "MASTER",
39
+ designer: "DESIGN",
40
+ backend: "DEV",
41
+ qa: "QA",
42
+ researcher: "RESEARCH",
43
+ scout: "SCOUT",
44
+ };
45
+
46
+ /** The @ names the composer offers, in its menu's order, with what each one is for. */
47
+ export const CHAT_MENTIONS: ReadonlyArray<{ name: string; agent: ChatAgent; hint: string }> = [
48
+ { name: "master", agent: "master", hint: "The oracle: anything, and changes across the project" },
49
+ { name: "design", agent: "designer", hint: "Frontend: pages, components, styling and UX" },
50
+ { name: "dev", agent: "backend", hint: "Backend: APIs, data and server code" },
51
+ { name: "qa", agent: "qa", hint: "Tests, and how to check a change" },
52
+ { name: "research", agent: "researcher", hint: "Facts from the web: libraries, versions, docs" },
53
+ { name: "scout", agent: "scout", hint: "Reads the code and explains how something works" },
54
+ ];
55
+
56
+ /** Other spellings an @ mention may use. */
57
+ const MENTION_ALIASES: Record<string, ChatAgent> = { oracle: "master", designer: "designer", frontend: "designer", backend: "backend", researcher: "researcher" };
58
+
59
+ /** A message that opens with `@name`: who it is for and the rest of it; undefined for any other message. */
60
+ export function parseMention(text: string): { agent: ChatAgent; rest: string } | undefined {
61
+ const match = /^\s*@([a-z]+)(?=\s|$)\s*/i.exec(text);
62
+ if (!match) return undefined;
63
+ const name = match[1]!.toLowerCase();
64
+ const agent = CHAT_MENTIONS.find((entry) => entry.name === name)?.agent ?? MENTION_ALIASES[name];
65
+ return agent ? { agent, rest: text.slice(match[0].length) } : undefined;
66
+ }
67
+
68
+ /** The lobby's slash commands: the chat does the work until you ask for a plan or a task. */
69
+ export const LOBBY_COMMANDS = [
70
+ { name: "plan", args: "what to plan", hint: "Plan it with the panel on the Plan tab" },
71
+ { name: "task", args: "what to do", hint: "Start it as a task for the team" },
72
+ ] as const;
73
+ export type LobbyCommand = (typeof LOBBY_COMMANDS)[number]["name"];
74
+
75
+ /** A message that opens with one of the lobby's commands: which, and the rest of it; undefined for anything else. */
76
+ export function parseLobbyCommand(text: string): { command: LobbyCommand; rest: string } | undefined {
77
+ const match = /^\s*\/([a-z]+)(?=\s|$)\s*/i.exec(text);
78
+ const command = LOBBY_COMMANDS.find((entry) => entry.name === match?.[1]?.toLowerCase())?.name;
79
+ return match && command ? { command, rest: text.slice(match[0].length) } : undefined;
80
+ }
@@ -1,14 +1,13 @@
1
1
  /**
2
2
  * Carrying on after pi stops unexpectedly. A window running the lobby keeps
3
- * what it is doing on disk as it changes: its planning panel and quick-fix
4
- * queue (by session), and a record of the sessions it drives and whether each
3
+ * what it is doing on disk as it changes: its planning panel (by session),
4
+ * and a record of the sessions it drives and whether each
5
5
  * was mid-turn (src/state/recovery.ts). Quitting on purpose removes the
6
6
  * record; a crash, a kill or a closed terminal leaves it.
7
7
  *
8
8
  * The next window to start the lobby in the project takes the stopped
9
9
  * windows over: the planning panel comes back where it was (a round that was
10
- * cut off runs again), queued quick fixes run (the one cut off runs again),
11
- * and every session that drove a task still under way is started again from
10
+ * cut off runs again), and every session that drove a task still under way is started again from
12
11
  * its saved file, its oracle told to pick up where it stopped when it was
13
12
  * mid-turn. A window that is that same session again (`pi -c`) carries its
14
13
  * own task on itself. Nothing here runs in a subagent or a background session.
@@ -32,7 +31,6 @@ import {
32
31
  } from "../state/recovery.ts";
33
32
  import { isPlanningSnapshot, type PlanningSession, type PlanningSnapshot } from "./planner.ts";
34
33
  import { keepPlan } from "../state/plan-history.ts";
35
- import type { QuickFixJob } from "./quickfix.ts";
36
34
  import type { BackgroundSession } from "./sessions.ts";
37
35
 
38
36
  /** Writes after a change wait this long, so a burst of changes is one write. */
@@ -87,7 +85,6 @@ export interface RecoveryPlan {
87
85
  windows: WindowRecord[];
88
86
  /** Kept state to hand to this session, from the newest stopped window that has it. */
89
87
  planner?: { from: string };
90
- quickfix: string[];
91
88
  relaunch: Relaunch[];
92
89
  /** This window is the stopped one's session again, and its task is under way. */
93
90
  own?: { taskId: string; message?: string };
@@ -107,7 +104,7 @@ export function planRecovery(input: {
107
104
  running: ReadonlySet<string>;
108
105
  }): RecoveryPlan {
109
106
  const { root, configDir, sessionId } = input;
110
- const plan: RecoveryPlan = { windows: [...input.windows], quickfix: [], relaunch: [] };
107
+ const plan: RecoveryPlan = { windows: [...input.windows], relaunch: [] };
111
108
  const hasOwnPlanner = isPlanningSnapshot(readSessionState(root, configDir, "planner", sessionId));
112
109
  const seen = new Set<string>();
113
110
  const consider = (session: { sessionId: string; sessionFile?: string; name?: string; working: boolean }) => {
@@ -127,7 +124,6 @@ export function planRecovery(input: {
127
124
  if (!hasOwnPlanner && !plan.planner && window.sessionId !== sessionId && isPlanningSnapshot(readSessionState(root, configDir, "planner", window.sessionId))) {
128
125
  plan.planner = { from: window.sessionId };
129
126
  }
130
- if (window.sessionId !== sessionId && Array.isArray(readSessionState(root, configDir, "quickfix", window.sessionId))) plan.quickfix.push(window.sessionId);
131
127
  consider({ sessionId: window.sessionId, ...(window.sessionFile ? { sessionFile: window.sessionFile } : {}), working: window.working });
132
128
  for (const background of window.background) consider(background);
133
129
  }
@@ -143,17 +139,14 @@ export interface RecoveryHost {
143
139
  relaunch(start: Relaunch): BackgroundSession | string;
144
140
  /** Restore the planning panel from a snapshot (and run again a round it cut off). */
145
141
  restorePlanner(snapshot: PlanningSnapshot): void;
146
- /** Take up quick-fix jobs a stopped process left; returns how many will run. */
147
- restoreQuickFixes(jobs: readonly QuickFixJob[]): number;
148
142
  /** Send this window's oracle the nudge, once it is idle. */
149
143
  nudge(message: string): void;
150
144
  }
151
145
 
152
146
  /** What carried on, in words, or undefined when nothing did. */
153
- export function recoverySummary(parts: { planner: boolean; quickfixes: number; tasks: string[] }): string | undefined {
147
+ export function recoverySummary(parts: { planner: boolean; tasks: string[] }): string | undefined {
154
148
  const done = [
155
149
  parts.planner ? "the planning session" : "",
156
- parts.quickfixes > 0 ? `${parts.quickfixes} quick fix${parts.quickfixes === 1 ? "" : "es"}` : "",
157
150
  parts.tasks.length > 0 ? `${parts.tasks.length === 1 ? "task" : "tasks"} ${parts.tasks.join(", ")}` : "",
158
151
  ].filter(Boolean);
159
152
  if (done.length === 0) return undefined;
@@ -163,7 +156,7 @@ export function recoverySummary(parts: { planner: boolean; quickfixes: number; t
163
156
 
164
157
  /**
165
158
  * Carry on what the stopped windows were doing, and this session's own kept
166
- * planning panel and quick-fix queue. Returns the summary to show, if any.
159
+ * planning panel. Returns the summary to show, if any.
167
160
  */
168
161
  export function recover(host: RecoveryHost): string | undefined {
169
162
  const { root, configDir, sessionId } = host;
@@ -182,14 +175,6 @@ export function recover(host: RecoveryHost): string | undefined {
182
175
  const planner = isPlanningSnapshot(snapshot);
183
176
  if (planner) host.restorePlanner(snapshot);
184
177
 
185
- const jobs: QuickFixJob[] = [];
186
- for (const from of [sessionId, ...plan.quickfix]) {
187
- const kept = readSessionState(root, configDir, "quickfix", from);
188
- if (Array.isArray(kept)) jobs.push(...(kept as QuickFixJob[]));
189
- if (from !== sessionId) removeSessionState(root, configDir, "quickfix", from);
190
- }
191
- const quickfixes = jobs.length > 0 ? host.restoreQuickFixes(jobs) : 0;
192
-
193
178
  const tasks: string[] = [];
194
179
  for (const start of plan.relaunch) {
195
180
  settleInterrupted(root, configDir, start.taskId);
@@ -202,7 +187,7 @@ export function recover(host: RecoveryHost): string | undefined {
202
187
  tasks.push(plan.own.taskId);
203
188
  }
204
189
  // A restart after a quit restores the panel quietly; only carrying on after a stop is announced.
205
- return windows.length > 0 ? recoverySummary({ planner, quickfixes, tasks }) : undefined;
190
+ return windows.length > 0 ? recoverySummary({ planner, tasks }) : undefined;
206
191
  }
207
192
 
208
193
  /** Whether a window in this project stopped without quitting: the lobby then starts on, to carry its work on. */
@@ -271,22 +256,20 @@ export interface KeeperSource {
271
256
  /** This window's oracle is mid-turn. */
272
257
  working: () => boolean;
273
258
  planner: () => PlanningSession | undefined;
274
- quickfix: () => readonly QuickFixJob[];
275
259
  background: () => readonly BackgroundSession[];
276
260
  }
277
261
 
278
262
  /**
279
- * Keeps this window's record, planning panel and quick-fix queue on disk as
263
+ * Keeps this window's record and planning panel on disk as
280
264
  * they change, for the next window to carry on after a stop.
281
265
  */
282
266
  export class WindowKeeper {
283
267
  private readonly startedAt = new Date().toISOString();
284
268
  readonly window: Debounced;
285
269
  readonly planner: Debounced;
286
- readonly quickfix: Debounced;
287
270
  /** pi going down on an uncaught error skips the shutdown: what is waiting is written as the process exits. */
288
271
  private readonly onExit = () => {
289
- for (const writer of [this.planner, this.quickfix, this.window]) writer.close();
272
+ for (const writer of [this.planner, this.window]) writer.close();
290
273
  };
291
274
  private readonly source: KeeperSource;
292
275
  constructor(source: KeeperSource) {
@@ -297,11 +280,6 @@ export class WindowKeeper {
297
280
  const session = source.planner();
298
281
  if (session) this.keepPlanner(session, session.snapshot());
299
282
  });
300
- this.quickfix = new Debounced(() => {
301
- const jobs = source.quickfix();
302
- if (jobs.length > 0) writeSessionState(source.root, source.configDir, "quickfix", source.sessionId(), jobs);
303
- else removeSessionState(source.root, source.configDir, "quickfix", source.sessionId());
304
- });
305
283
  }
306
284
 
307
285
  /** The panel as this window's (to carry on) and among the project's plans (to read again later). */
@@ -331,7 +309,6 @@ export class WindowKeeper {
331
309
  process.off("exit", this.onExit);
332
310
  if (carryOn) {
333
311
  this.planner.close();
334
- this.quickfix.close();
335
312
  this.window.close();
336
313
  return;
337
314
  }
@@ -340,14 +317,11 @@ export class WindowKeeper {
340
317
  try {
341
318
  const session = this.source.planner();
342
319
  if (session) this.keepPlanner(session, stoppedSnapshot(session.snapshot()));
343
- const jobs = this.source.quickfix().map(stoppedJob);
344
- if (jobs.length > 0) writeSessionState(root, configDir, "quickfix", sessionId, jobs);
345
- else removeSessionState(root, configDir, "quickfix", sessionId);
346
320
  removeWindow(root, configDir);
347
321
  } catch {
348
322
  // A read-only project keeps nothing.
349
323
  }
350
- for (const writer of [this.planner, this.quickfix, this.window]) writer.cancel();
324
+ for (const writer of [this.planner, this.window]) writer.cancel();
351
325
  }
352
326
  }
353
327
 
@@ -357,12 +331,6 @@ export function stoppedSnapshot(snapshot: PlanningSnapshot): PlanningSnapshot {
357
331
  return { ...snapshot, running: false, members: snapshot.members.map((member) => member.status === "thinking" ? { ...member, status: "failed", error: "stopped" } : member), error: STOPPED_ROUND };
358
332
  }
359
333
 
360
- /** What a stop on purpose leaves of a quick fix that was queued or running: cancelled. */
361
- export function stoppedJob(job: QuickFixJob): QuickFixJob {
362
- if (job.status !== "queued" && job.status !== "running") return job;
363
- return { ...job, status: "cancelled", note: "stopped when the lobby stopped", ...(job.finishedAt ? {} : { finishedAt: Date.now() }) };
364
- }
365
-
366
334
  /** A round stopped by a quit, as the Plan tab shows it. */
367
335
  export const STOPPED_ROUND = "stopped when the lobby stopped — retry runs the round again";
368
336