@a-t-h-i/bot-lobby 0.4.0 → 0.5.1

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 (41) hide show
  1. package/README.md +238 -10
  2. package/package.json +5 -2
  3. package/prompts/master.md +12 -0
  4. package/prompts/panel.md +44 -0
  5. package/prompts/planner.md +69 -0
  6. package/prompts/quickfix.md +41 -0
  7. package/src/execution/agent-runner.ts +7 -2
  8. package/src/execution/pi-runner.ts +25 -0
  9. package/src/index.ts +3 -0
  10. package/src/lobby/ask.ts +226 -0
  11. package/src/lobby/feed.ts +253 -0
  12. package/src/lobby/issues.ts +227 -0
  13. package/src/lobby/keys.ts +51 -0
  14. package/src/lobby/layout.ts +363 -0
  15. package/src/lobby/markdown.ts +48 -0
  16. package/src/lobby/planner.ts +581 -0
  17. package/src/lobby/quickfix.ts +227 -0
  18. package/src/lobby/runtime.ts +591 -0
  19. package/src/lobby/tabs/home.ts +242 -0
  20. package/src/lobby/tabs/issues.ts +72 -0
  21. package/src/lobby/tabs/metrics.ts +275 -0
  22. package/src/lobby/tabs/plan.ts +242 -0
  23. package/src/lobby/tabs/quickfix.ts +129 -0
  24. package/src/lobby/tabs/tasks.ts +243 -0
  25. package/src/lobby/view.ts +1610 -0
  26. package/src/pi/activity.ts +120 -0
  27. package/src/pi/commands.ts +19 -57
  28. package/src/pi/events.ts +5 -2
  29. package/src/pi/model-support.ts +34 -0
  30. package/src/pi/run-summary.ts +2 -0
  31. package/src/pi/settings-ui.ts +69 -1
  32. package/src/pi/start-task.ts +63 -0
  33. package/src/pi/tools.ts +2 -2
  34. package/src/pi/ui.ts +116 -55
  35. package/src/schemas/configuration.ts +102 -3
  36. package/src/schemas/findings.ts +6 -0
  37. package/src/schemas/task.ts +1 -0
  38. package/src/state/backlog.ts +106 -0
  39. package/src/state/comments.ts +136 -0
  40. package/src/state/metrics.ts +305 -0
  41. package/src/workflow/workflow.ts +39 -7
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Quick fixes: a direct prompt from the lobby goes straight to one coding
3
+ * subagent — no scouting, proposal, plan or QA gate — while any bot-lobby task
4
+ * keeps running. Jobs run one at a time in submission order so two quick fixes
5
+ * never edit the tree at once; each streams plain-words steps into its own log
6
+ * and the lobby feed, and every finished job lands in the metrics log.
7
+ */
8
+ import { loadPrompt } from "../prompts/loader.ts";
9
+ import { runPiAgent, spawnPiProcess, type PiStreamEvent, type ProcessRunner } from "../execution/pi-runner.ts";
10
+ import { describeToolCall } from "../pi/activity.ts";
11
+ import { appendMetrics, type MetricRecord } from "../state/metrics.ts";
12
+ import type { LobbyFeed } from "./feed.ts";
13
+
14
+ /** A quick fix edits code, so it gets the full coding tool set. */
15
+ export const QUICK_FIX_TOOLS: readonly string[] = ["read", "bash", "edit", "write", "grep", "find", "ls"];
16
+
17
+ export const QUICK_FIX_SOURCE = "QUICK FIX";
18
+
19
+ export type QuickFixStatus = "queued" | "running" | "success" | "failed" | "cancelled" | "timeout";
20
+
21
+ export interface QuickFixStep {
22
+ at: number;
23
+ text: string;
24
+ pending: boolean;
25
+ }
26
+
27
+ export interface QuickFixJob {
28
+ id: string;
29
+ prompt: string;
30
+ status: QuickFixStatus;
31
+ createdAt: number;
32
+ startedAt?: number;
33
+ finishedAt?: number;
34
+ model?: string;
35
+ thinking?: string;
36
+ steps: QuickFixStep[];
37
+ tools: number;
38
+ turns: number;
39
+ report?: string;
40
+ error?: string;
41
+ usage?: { input: number; output: number; cost: number; turns: number };
42
+ }
43
+
44
+ export interface QuickFixProfile {
45
+ model?: string;
46
+ thinking: string;
47
+ timeoutMs: number;
48
+ instructions?: string;
49
+ }
50
+
51
+ export interface QuickFixDeps {
52
+ cwd: string;
53
+ root: string;
54
+ configDir: string;
55
+ /** Model, thinking and time limit for the next job, read from settings at start time. */
56
+ profile: () => QuickFixProfile;
57
+ stallTimeoutMs?: number;
58
+ toolStallTimeoutMs?: number;
59
+ feed?: LobbyFeed;
60
+ runProcess?: ProcessRunner;
61
+ /** Called after every change so the lobby can repaint. */
62
+ onChange?: () => void;
63
+ notify?: (message: string, level: "info" | "warning" | "error") => void;
64
+ }
65
+
66
+ export const MAX_JOBS = 30;
67
+ export const MAX_STEPS = 200;
68
+
69
+ function isActive(job: QuickFixJob): boolean {
70
+ return job.status === "queued" || job.status === "running";
71
+ }
72
+
73
+ /** The system prompt a quick fix runs with: the quick-fix role plus the user's custom instructions. */
74
+ export function quickFixPrompt(instructions?: string): string {
75
+ const custom = instructions?.trim();
76
+ return custom ? `${loadPrompt("quickfix.md")}\n\n## Custom Instructions\n\n${custom}` : loadPrompt("quickfix.md");
77
+ }
78
+
79
+ /** First line of a prompt, for lists. */
80
+ export function jobTitle(job: Pick<QuickFixJob, "prompt">): string {
81
+ return job.prompt.split("\n").find((line) => line.trim())?.trim() ?? "(empty)";
82
+ }
83
+
84
+ export class QuickFixQueue {
85
+ jobs: QuickFixJob[] = [];
86
+ private readonly deps: QuickFixDeps;
87
+ private readonly controllers = new Map<string, AbortController>();
88
+ private counter = 0;
89
+
90
+ constructor(deps: QuickFixDeps) {
91
+ this.deps = deps;
92
+ }
93
+
94
+ get running(): QuickFixJob | undefined {
95
+ return this.jobs.find((job) => job.status === "running");
96
+ }
97
+
98
+ /** Queue a quick fix; it starts at once when nothing else is running. */
99
+ submit(prompt: string, now = Date.now()): QuickFixJob {
100
+ const text = prompt.trim();
101
+ if (!text) throw new Error("a quick fix needs a prompt");
102
+ const job: QuickFixJob = { id: `QF-${++this.counter}`, prompt: text, status: "queued", createdAt: now, steps: [], tools: 0, turns: 0 };
103
+ this.jobs = [...this.jobs, job].filter((entry, index, all) => isActive(entry) || index >= all.length - MAX_JOBS);
104
+ this.changed();
105
+ this.pump();
106
+ return job;
107
+ }
108
+
109
+ /** Cancel a queued or running job; finished jobs are left alone. */
110
+ cancel(id: string): boolean {
111
+ const job = this.jobs.find((entry) => entry.id === id);
112
+ if (!job || !isActive(job)) return false;
113
+ if (job.status === "queued") {
114
+ job.status = "cancelled";
115
+ job.finishedAt = Date.now();
116
+ this.changed();
117
+ return true;
118
+ }
119
+ this.controllers.get(id)?.abort();
120
+ return true;
121
+ }
122
+
123
+ /** Abort everything (session shutdown). */
124
+ cancelAll(): void {
125
+ for (const job of this.jobs) if (job.status === "queued") job.status = "cancelled";
126
+ for (const controller of this.controllers.values()) controller.abort();
127
+ }
128
+
129
+ private changed(): void {
130
+ this.deps.onChange?.();
131
+ }
132
+
133
+ private pump(): void {
134
+ if (this.running) return;
135
+ const next = this.jobs.find((job) => job.status === "queued");
136
+ if (next) void this.run(next);
137
+ }
138
+
139
+ private addStep(job: QuickFixJob, text: string, at: number): void {
140
+ for (const step of job.steps) step.pending = false;
141
+ job.steps = [...job.steps, { at, text, pending: true }].slice(-MAX_STEPS);
142
+ this.deps.feed?.step(QUICK_FIX_SOURCE, text, job.id, at);
143
+ }
144
+
145
+ private onEvent(job: QuickFixJob, event: PiStreamEvent): void {
146
+ const now = Date.now();
147
+ if (event.type === "tool_execution_start") {
148
+ job.tools += 1;
149
+ this.addStep(job, describeToolCall(event.toolName, event.args), now);
150
+ } else if (event.type === "turn_start") job.turns += 1;
151
+ else if (event.type === "thought") this.deps.feed?.thought(QUICK_FIX_SOURCE, event.text, now);
152
+ else if (event.type === "retry") this.addStep(job, `provider retry ${event.attempt}/${event.maxAttempts}`, now);
153
+ else return;
154
+ this.changed();
155
+ }
156
+
157
+ private async run(job: QuickFixJob): Promise<void> {
158
+ const profile = this.deps.profile();
159
+ const controller = new AbortController();
160
+ this.controllers.set(job.id, controller);
161
+ job.status = "running";
162
+ job.startedAt = Date.now();
163
+ job.thinking = profile.thinking;
164
+ if (profile.model) job.model = profile.model;
165
+ this.deps.feed?.log(QUICK_FIX_SOURCE, `started: ${jobTitle(job)}`, "info", job.startedAt);
166
+ this.changed();
167
+ try {
168
+ const result = await runPiAgent(
169
+ {
170
+ cwd: this.deps.cwd,
171
+ task: job.prompt,
172
+ systemPrompt: quickFixPrompt(profile.instructions),
173
+ tools: QUICK_FIX_TOOLS,
174
+ model: profile.model,
175
+ thinking: profile.thinking,
176
+ timeoutMs: profile.timeoutMs,
177
+ signal: controller.signal,
178
+ stallTimeoutMs: this.deps.stallTimeoutMs,
179
+ toolStallTimeoutMs: this.deps.toolStallTimeoutMs,
180
+ onEvent: (event) => this.onEvent(job, event),
181
+ },
182
+ this.deps.runProcess ?? spawnPiProcess,
183
+ );
184
+ job.status = result.status;
185
+ job.report = result.output.trim() || undefined;
186
+ job.error = result.error;
187
+ job.usage = result.usage;
188
+ if (result.model) job.model = result.model;
189
+ } catch (error) {
190
+ job.status = "failed";
191
+ job.error = (error as Error).message;
192
+ } finally {
193
+ this.controllers.delete(job.id);
194
+ this.finish(job);
195
+ }
196
+ }
197
+
198
+ private finish(job: QuickFixJob): void {
199
+ job.finishedAt = Date.now();
200
+ for (const step of job.steps) step.pending = false;
201
+ this.deps.feed?.end(job.id, job.status !== "success");
202
+ const ok = job.status === "success";
203
+ const outcome = ok ? "done" : `${job.status}${job.error ? ` — ${job.error.split("\n")[0]}` : ""}`;
204
+ this.deps.feed?.log(QUICK_FIX_SOURCE, `${outcome}: ${jobTitle(job)}`, ok ? "success" : job.status === "cancelled" ? "warning" : "error", job.finishedAt);
205
+ appendMetrics(this.deps.root, this.deps.configDir, [quickFixMetric(job)]);
206
+ this.deps.notify?.(`bot-lobby quick fix ${outcome}: ${jobTitle(job)}`, ok ? "info" : "warning");
207
+ this.changed();
208
+ this.pump();
209
+ }
210
+ }
211
+
212
+ export function quickFixMetric(job: QuickFixJob): MetricRecord {
213
+ const started = job.startedAt ?? job.createdAt;
214
+ return {
215
+ id: `${job.id}-${started}`,
216
+ kind: "quickfix",
217
+ agent: QUICK_FIX_SOURCE,
218
+ ...(job.model ? { model: job.model } : {}),
219
+ ...(job.thinking ? { thinking: job.thinking } : {}),
220
+ status: job.status === "queued" || job.status === "running" ? "failed" : job.status,
221
+ startedAt: new Date(started).toISOString(),
222
+ durationMs: Math.max(0, (job.finishedAt ?? started) - started),
223
+ turns: job.usage?.turns || job.turns || undefined,
224
+ tools: job.tools,
225
+ ...(job.usage ? { input: job.usage.input, output: job.usage.output, cost: job.usage.cost } : {}),
226
+ };
227
+ }