@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.
- package/README.md +17 -11
- package/THIRD_PARTY_NOTICES.md +17 -3
- package/package.json +6 -1
- package/prompts/lobby-chat.md +39 -0
- package/prompts/master.md +7 -7
- package/prompts/reviewer.md +6 -6
- package/src/classifier/instance.ts +1 -1
- package/src/classifier/lobby.ts +37 -0
- package/src/classifier/triage.ts +2 -24
- package/src/execution/delegation.ts +2 -2
- package/src/index.ts +0 -3
- package/src/lobby/chat-agents.ts +293 -0
- package/src/lobby/feed.ts +73 -17
- package/src/lobby/host.ts +8 -4
- package/src/lobby/keys.ts +1 -0
- package/src/lobby/planner.ts +5 -5
- package/src/lobby/pr-review.ts +2 -2
- package/src/lobby/profile.ts +16 -0
- package/src/lobby/prompts.ts +56 -4
- package/src/lobby/recovery.ts +10 -42
- package/src/lobby/runtime.ts +27 -45
- package/src/lobby/service.ts +142 -13
- package/src/lobby/session-files.ts +4 -1
- package/src/lobby/topics.ts +0 -1
- package/src/master/master.ts +1 -1
- package/src/pi/commands.ts +6 -8
- package/src/pi/events.ts +11 -3
- package/src/pi/fresh-context.ts +1 -1
- package/src/pi/model-settings.ts +0 -2
- package/src/pi/model-support.ts +11 -2
- package/src/pi/owner.ts +35 -4
- package/src/pi/start-task.ts +3 -9
- package/src/pi/switch.ts +1 -1
- package/src/schemas/configuration.ts +14 -24
- package/src/state/changes.ts +25 -23
- package/src/state/metrics.ts +5 -8
- package/src/state/recovery.ts +3 -3
- package/src/webui/api/index.ts +12 -18
- package/src/webui/api/lobby.ts +9 -6
- package/src/webui/code.ts +202 -0
- package/src/webui/dev/fake-service.ts +28 -62
- package/src/webui/dev/fixtures.ts +1 -4
- package/src/webui/protocol.ts +15 -9
- package/src/webui/static.ts +5 -2
- package/src/workflow/track.ts +1 -52
- package/src/workflow/workflow.ts +2 -2
- package/webui/dist/assets/CodeSurface-BtzJOVyH.js +448 -0
- package/webui/dist/assets/{TerminalView-C8VC7KXy.js → TerminalView-Cz-KNWsq.js} +2 -2
- package/webui/dist/assets/astro-p42oEArs.js +1 -0
- package/webui/dist/assets/c-DaO8xguW.js +1 -0
- package/webui/dist/assets/ccount-DQxSH8e6.js +1 -0
- package/webui/dist/assets/csharp-dmMxLVLW.js +1 -0
- package/webui/dist/assets/css-C8fQr_0Q.js +1 -0
- package/webui/dist/assets/diff-woXpYk--.js +1 -0
- package/webui/dist/assets/dockerfile-IyjqRm3v.js +1 -0
- package/webui/dist/assets/dotenv-CuH5my32.js +1 -0
- package/webui/dist/assets/fish-BJitypiv.js +1 -0
- package/webui/dist/assets/go-rLFTqkRN.js +1 -0
- package/webui/dist/assets/graphql-AVu4aKQX.js +1 -0
- package/webui/dist/assets/html-wSbaAAK3.js +1 -0
- package/webui/dist/assets/index-D5fvY8f8.js +112 -0
- package/webui/dist/assets/index-DFvk8ADr.css +2 -0
- package/webui/dist/assets/ini-B5eOa1yu.js +1 -0
- package/webui/dist/assets/java-DdHNQ9hk.js +1 -0
- package/webui/dist/assets/javascript-BgS3c2Ky.js +1 -0
- package/webui/dist/assets/json-qhed-kSA.js +1 -0
- package/webui/dist/assets/json5-BR5RXkoi.js +1 -0
- package/webui/dist/assets/jsonc-CYpm1nAK.js +1 -0
- package/webui/dist/assets/jsonl-CmCQp5Yx.js +1 -0
- package/webui/dist/assets/jsx-gTgmArGx.js +1 -0
- package/webui/dist/assets/kotlin-DhhofPvG.js +1 -0
- package/webui/dist/assets/kts-DFAlwc6n.js +1 -0
- package/webui/dist/assets/less-DVTAwKKz.js +1 -0
- package/webui/dist/assets/lua-xZcbXUE9.js +1 -0
- package/webui/dist/assets/makefile-Dixweg8N.js +1 -0
- package/webui/dist/assets/markdown-BYOwaDjH.js +1 -0
- package/webui/dist/assets/php-C6PDnbTe.js +1 -0
- package/webui/dist/assets/pierre-dark-CpLgRqie.js +1 -0
- package/webui/dist/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
- package/webui/dist/assets/pierre-dark-soft-kZQmAZld.js +1 -0
- package/webui/dist/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
- package/webui/dist/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
- package/webui/dist/assets/pierre-light-CoaEpmwp.js +1 -0
- package/webui/dist/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
- package/webui/dist/assets/pierre-light-soft-lWLdNTOI.js +1 -0
- package/webui/dist/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
- package/webui/dist/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
- package/webui/dist/assets/postcss-D_Wz94rJ.js +1 -0
- package/webui/dist/assets/powershell-QtSnmXcu.js +1 -0
- package/webui/dist/assets/prisma-DMgNhnYS.js +1 -0
- package/webui/dist/assets/properties-CxAuzHS9.js +1 -0
- package/webui/dist/assets/python-gzcpVVnB.js +1 -0
- package/webui/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/webui/dist/assets/ruby-CPL7DL0w.js +1 -0
- package/webui/dist/assets/rust-DpiIf5JO.js +1 -0
- package/webui/dist/assets/sass-DXrisJhu.js +1 -0
- package/webui/dist/assets/scss-CHVQqdPx.js +1 -0
- package/webui/dist/assets/shellscript-CLZ0U2zV.js +1 -0
- package/webui/dist/assets/sql-CEbI0hMM.js +1 -0
- package/webui/dist/assets/svelte-DKhxH1PN.js +1 -0
- package/webui/dist/assets/swift-CyEgAFGc.js +1 -0
- package/webui/dist/assets/toml-CcmNWLt0.js +1 -0
- package/webui/dist/assets/tsx-CUOciP0b.js +1 -0
- package/webui/dist/assets/typescript-BydbNFcO.js +1 -0
- package/webui/dist/assets/vue-kBwcnTZb.js +1 -0
- package/webui/dist/assets/wasm-BnjxR4X6.js +1 -0
- package/webui/dist/assets/xml-CeXJrFlG.js +1 -0
- package/webui/dist/assets/yaml-rwi0_p6S.js +1 -0
- package/webui/dist/assets/yml-s9iVJXCw.js +1 -0
- package/webui/dist/assets/zsh-B7CDPTg5.js +1 -0
- package/webui/dist/build.json +61 -45
- package/webui/dist/index.html +4 -2
- package/prompts/quickfix.md +0 -67
- package/src/lobby/quickfix.ts +0 -546
- package/src/pi/route.ts +0 -180
- package/src/webui/api/quickfix.ts +0 -49
- package/webui/dist/assets/index-Bs1_77_1.js +0 -114
- 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,
|
|
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
|
-
|
|
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
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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
|
-
//
|
|
351
|
-
|
|
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
|
-
/**
|
|
371
|
-
export
|
|
372
|
-
|
|
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
|
-
/**
|
|
103
|
-
|
|
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;
|
package/src/lobby/planner.ts
CHANGED
|
@@ -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 {
|
|
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: () =>
|
|
141
|
+
profile: () => LobbyRunProfile;
|
|
142
142
|
/** A seat's model, thinking and grants; the oracle's profile when absent. */
|
|
143
|
-
memberProfile?: (member: PanelMember) =>
|
|
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:
|
|
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:
|
|
1274
|
+
profile: LobbyRunProfile,
|
|
1275
1275
|
model: string | undefined,
|
|
1276
1276
|
usage: { input: number; output: number; cost: number; turns: number },
|
|
1277
1277
|
now = Date.now(),
|
package/src/lobby/pr-review.ts
CHANGED
|
@@ -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 {
|
|
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: () =>
|
|
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
|
+
}
|
package/src/lobby/prompts.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Strings the server and the page share: tab metadata
|
|
3
|
-
*
|
|
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", "
|
|
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
|
+
}
|
package/src/lobby/recovery.ts
CHANGED
|
@@ -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
|
|
4
|
-
*
|
|
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),
|
|
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],
|
|
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;
|
|
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
|
|
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,
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|