@a-t-h-i/bot-lobby 0.6.3 → 0.6.4
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 +102 -11
- package/package.json +1 -4
- package/prompts/master.md +47 -1
- package/prompts/researcher.md +8 -2
- package/prompts/reviewer.md +42 -0
- package/prompts/worker.md +7 -0
- package/src/ask/dialog.ts +167 -0
- package/src/ask/image.ts +202 -0
- package/src/ask/png.ts +179 -0
- package/src/ask/relay.ts +89 -0
- package/src/ask/state.ts +160 -0
- package/src/ask/tool.ts +126 -0
- package/src/ask/types.ts +55 -0
- package/src/ask/view.ts +159 -0
- package/src/execution/agent-runner.ts +103 -11
- package/src/execution/git.ts +111 -14
- package/src/execution/pi-runner.ts +164 -11
- package/src/index.ts +6 -0
- package/src/lobby/ask.ts +10 -120
- package/src/lobby/layout.ts +27 -8
- package/src/lobby/markdown.ts +36 -6
- package/src/lobby/planner.ts +1 -1
- package/src/lobby/quickfix.ts +21 -0
- package/src/lobby/runtime.ts +14 -36
- package/src/lobby/tabs/home.ts +112 -48
- package/src/lobby/tabs/issues.ts +4 -3
- package/src/lobby/tabs/plan.ts +4 -4
- package/src/lobby/tabs/quickfix.ts +7 -1
- package/src/lobby/tabs/tasks.ts +14 -4
- package/src/lobby/theme.ts +30 -0
- package/src/lobby/view.ts +27 -5
- package/src/master/decisions.ts +1 -1
- package/src/master/master.ts +41 -4
- package/src/master/research.ts +5 -2
- package/src/pi/commands.ts +94 -10
- package/src/pi/events.ts +47 -10
- package/src/pi/quiet.ts +22 -4
- package/src/pi/start-task.ts +8 -2
- package/src/pi/tools.ts +26 -8
- package/src/pi/ui.ts +6 -1
- package/src/pi/zen-metrics.ts +13 -3
- package/src/pi/zen.ts +16 -9
- package/src/roles/reviewer.ts +23 -4
- package/src/roles/worker.ts +18 -0
- package/src/schemas/configuration.ts +4 -0
- package/src/schemas/findings.ts +14 -0
- package/src/schemas/task.ts +21 -0
- package/src/state/budget.ts +274 -0
- package/src/state/changes.ts +231 -0
- package/src/text.ts +28 -2
- package/src/web/extract.ts +332 -0
- package/src/web/fetch.ts +232 -0
- package/src/web/html.ts +183 -0
- package/src/web/read.ts +113 -0
- package/src/web/search.ts +202 -0
- package/src/web/tools.ts +279 -0
- package/src/workflow/workflow.ts +592 -42
package/src/lobby/tabs/issues.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* becomes a task.
|
|
5
5
|
*/
|
|
6
6
|
import type { IssueDetail, IssueSummary } from "../issues.ts";
|
|
7
|
-
import { bold, since, columns, fill, paint, rule, selectRow, spinner, split, windowStart, wrap, type LobbyTheme } from "../layout.ts";
|
|
7
|
+
import { bold, since, columns, fill, markdownLines, paint, rule, selectRow, spinner, split, windowStart, wrap, type LobbyTheme } from "../layout.ts";
|
|
8
8
|
|
|
9
9
|
export interface IssuesTabInput {
|
|
10
10
|
issues: readonly IssueSummary[];
|
|
@@ -31,9 +31,10 @@ export function issueDetailLines(issue: IssueDetail, width: number, now: number,
|
|
|
31
31
|
].filter(Boolean);
|
|
32
32
|
const lines = [...wrap(bold(theme, `#${issue.number} ${issue.title}`), width), facts.join(paint(theme, "dim", " · "))];
|
|
33
33
|
if (issue.url) lines.push(paint(theme, "dim", issue.url));
|
|
34
|
-
|
|
34
|
+
// Issues and their comments are GitHub Markdown.
|
|
35
|
+
lines.push("", paint(theme, "dim", "p plans it with the planning panel, then save it as a task"), "", ...(issue.body.trim() ? markdownLines(issue.body.trim(), width, theme) : [paint(theme, "dim", "(no description)")]));
|
|
35
36
|
for (const comment of issue.comments) {
|
|
36
|
-
lines.push("", rule(width, comment.author ?? "comment", theme, comment.createdAt ? since(now - Date.parse(comment.createdAt)) : ""), ...
|
|
37
|
+
lines.push("", rule(width, comment.author ?? "comment", theme, comment.createdAt ? since(now - Date.parse(comment.createdAt)) : ""), ...markdownLines(comment.body.trim(), width, theme));
|
|
37
38
|
}
|
|
38
39
|
return lines;
|
|
39
40
|
}
|
package/src/lobby/tabs/plan.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { stripTerminalSequences } from "@earendil-works/pi-tui";
|
|
10
10
|
import { ORACLE_LABEL, type LineComment, type PanelNote, type PanelQuestion, type PlannerMessage, type PlannerReply, type PlannerSeed, type RoundMode } from "../planner.ts";
|
|
11
11
|
import type { PlannedTask } from "../../state/backlog.ts";
|
|
12
|
-
import { beside, bold, box, clock, fill, fit, italic, markdownLines, notePane, paint, selectRow, spinner, wrap, wrapHanging, type LobbyTheme, type PaneLayout } from "../layout.ts";
|
|
12
|
+
import { beside, bold, box, clock, fill, fit, italic, markdownHanging, markdownLines, notePane, paint, selectRow, spinner, wrap, wrapHanging, type LobbyTheme, type PaneLayout } from "../layout.ts";
|
|
13
13
|
import { sourceColor, speakerLine, tailWindow, youLines } from "./home.ts";
|
|
14
14
|
|
|
15
15
|
export interface SeatView {
|
|
@@ -152,7 +152,7 @@ function questionLines(questions: ReadonlyArray<{ question: PanelQuestion; numbe
|
|
|
152
152
|
const lead = `${paint(theme, "dim", `${String(number).padStart(2)}.`)} ${bold(theme, paint(theme, sourceColor(question.from), question.from.padEnd(LABEL_WIDTH)))} `;
|
|
153
153
|
const indent = " ".repeat(4 + LABEL_WIDTH);
|
|
154
154
|
const options = question.options.map((option) => wrapHanging(`${indent}${paint(theme, "dim", "○")} `, `${option.label}${option.description ? paint(theme, "dim", ` — ${option.description}`) : ""}`, width)).flat();
|
|
155
|
-
return [...
|
|
155
|
+
return [...markdownHanging(lead, question.text, width, theme), ...options];
|
|
156
156
|
});
|
|
157
157
|
}
|
|
158
158
|
|
|
@@ -178,7 +178,7 @@ export function conversationLines(view: PlanView, width: number, theme?: LobbyTh
|
|
|
178
178
|
continue;
|
|
179
179
|
}
|
|
180
180
|
lines.push(speakerLine("panel", width, theme, time));
|
|
181
|
-
lines.push(...(questions.length > 0 ? questionLines(questions, width, theme) :
|
|
181
|
+
lines.push(...(questions.length > 0 ? questionLines(questions, width, theme) : markdownLines(message.text, Math.max(1, width - 2), theme).map((line) => (line ? ` ${line}` : ""))));
|
|
182
182
|
for (const entry of message.decided ?? []) {
|
|
183
183
|
lines.push(...wrapHanging(` ${paint(theme, "success", "✓")} `, `${paint(theme, sourceColor(entry.from), `[${entry.from}]`)} ${entry.question} → ${bold(theme, entry.answer)} ${paint(theme, "dim", `· decided by the classifier (${entry.probability.toFixed(2)}); comment on the plan to overrule`)}`, width));
|
|
184
184
|
}
|
|
@@ -210,7 +210,7 @@ export function draftLines(view: PlanView, width: number, theme?: LobbyTheme): D
|
|
|
210
210
|
}
|
|
211
211
|
if (view.notes.length > 0) {
|
|
212
212
|
lines.push(line(""), line(bold(theme, paint(theme, "mdHeading", "What each seat needs"))));
|
|
213
|
-
for (const note of view.notes) for (const wrapped of
|
|
213
|
+
for (const note of view.notes) for (const wrapped of markdownHanging(`${paint(theme, sourceColor(note.from), note.from.padEnd(LABEL_WIDTH))} `, note.text, width, theme)) lines.push(line(wrapped));
|
|
214
214
|
}
|
|
215
215
|
return lines;
|
|
216
216
|
}
|
|
@@ -61,7 +61,9 @@ export function jobDetailLines(job: QuickFixJob, width: number, tick: number, no
|
|
|
61
61
|
job.tools ? `${job.tools} tool${job.tools === 1 ? "" : "s"}` : "",
|
|
62
62
|
job.usage?.cost ? `$${job.usage.cost.toFixed(2)}` : "",
|
|
63
63
|
].filter(Boolean);
|
|
64
|
-
|
|
64
|
+
// What you asked, in Markdown as your messages read in the conversation (a pasted snippet, a list of steps).
|
|
65
|
+
const asked = theme?.markdown ? markdownLines(job.prompt, width, theme, true, "you") : wrap(bold(theme, job.prompt), width);
|
|
66
|
+
const lines = [paint(theme, "dim", job.id), facts.join(paint(theme, "dim", " · ")), "", ...asked];
|
|
65
67
|
if (job.route) lines.push("", ...wrap(paint(theme, "dim", `↓ routed ${job.route}`), width));
|
|
66
68
|
if (job.note) lines.push("", ...wrap(paint(theme, "warning", job.status === "held" ? `‖ Held: ${job.note}. r runs it anyway · t starts it as a task in a new session.` : job.note), width));
|
|
67
69
|
lines.push("", rule(width, "Steps", theme));
|
|
@@ -70,6 +72,10 @@ export function jobDetailLines(job: QuickFixJob, width: number, tick: number, no
|
|
|
70
72
|
const lead = `${paint(theme, "dim", clock(step.at))} ${step.pending && job.status === "running" ? paint(theme, "accent", spinner(tick)) : paint(theme, "dim", "·")} `;
|
|
71
73
|
lines.push(...wrapHanging(lead, step.pending && job.status === "running" ? `${step.text}…` : paint(theme, "muted", step.text), width));
|
|
72
74
|
}
|
|
75
|
+
if (job.files?.length) {
|
|
76
|
+
lines.push("", rule(width, "Edited", theme));
|
|
77
|
+
for (const file of job.files) lines.push(...wrapHanging(paint(theme, "dim", "· "), paint(theme, "muted", file), width));
|
|
78
|
+
}
|
|
73
79
|
if (job.error) lines.push("", ...wrap(paint(theme, "error", `✗ ${job.error}`), width));
|
|
74
80
|
if (job.report) {
|
|
75
81
|
lines.push("", rule(width, "Report", theme), ...markdownLines(job.report, width, theme));
|
package/src/lobby/tabs/tasks.ts
CHANGED
|
@@ -11,7 +11,8 @@ import { planChecklist } from "../../pi/zen.ts";
|
|
|
11
11
|
import { persistedRuns } from "../../pi/ui.ts";
|
|
12
12
|
import { describeRun, runFromLog } from "../../pi/run-summary.ts";
|
|
13
13
|
import { pendingApprovals } from "../../workflow/approvals.ts";
|
|
14
|
-
import {
|
|
14
|
+
import { textWidth } from "../../width.ts";
|
|
15
|
+
import { ago, beside, bold, box, detailWindow, fill, markdownHanging, markdownLines, notePane, paint, position, rule, selectRow, since, spread, strike, windowStart, wrap, wrapHanging, type LobbyColor, type LobbyTheme, type PaneLayout } from "../layout.ts";
|
|
15
16
|
|
|
16
17
|
export type TaskSection = "mine" | "others" | "pending" | "recent" | "archived";
|
|
17
18
|
|
|
@@ -240,6 +241,13 @@ export function listCount(rows: readonly TaskRow[], query?: string): string {
|
|
|
240
241
|
const COMMENT_MARKS: Record<PlanComment["status"], string> = { open: "○", delivered: "◐", addressed: "✓" };
|
|
241
242
|
const COMMENT_WORDS: Record<PlanComment["status"], string> = { open: "waiting for the owning session", delivered: "sent to the oracle", addressed: "plan amended" };
|
|
242
243
|
|
|
244
|
+
/** `note` at the end of the last line when it fits there, else on a line of its own under it. */
|
|
245
|
+
function trailing(lines: string[], note: string, width: number): string[] {
|
|
246
|
+
const last = lines.at(-1) ?? "";
|
|
247
|
+
if (textWidth(last) + 1 + textWidth(note) <= width) return [...lines.slice(0, -1), `${last} ${note}`];
|
|
248
|
+
return [...lines, ...wrapHanging(" ", note, width)];
|
|
249
|
+
}
|
|
250
|
+
|
|
243
251
|
function section(title: string, width: number, theme?: LobbyTheme, right = ""): string[] {
|
|
244
252
|
return ["", rule(width, title, theme, right)];
|
|
245
253
|
}
|
|
@@ -268,7 +276,8 @@ export function taskDetailLines(task: Task, comments: readonly PlanComment[], se
|
|
|
268
276
|
lines.push("", ` ${pips(done, steps.length, cells, theme)} ${paint(theme, "muted", `${done} of ${steps.length} steps`)}`);
|
|
269
277
|
}
|
|
270
278
|
const request = taskRequest(task);
|
|
271
|
-
|
|
279
|
+
// Requests are often Markdown: a plan agreed in the planning panel, an issue, a pasted spec.
|
|
280
|
+
if (request && request !== task.title) lines.push(...section("Request", width, theme), ...markdownLines(request, width, theme));
|
|
272
281
|
if (task.plan) {
|
|
273
282
|
if (steps.length > 0) {
|
|
274
283
|
lines.push(...section("Progress", width, theme, `${done}/${steps.length} steps`));
|
|
@@ -291,11 +300,12 @@ export function taskDetailLines(task: Task, comments: readonly PlanComment[], se
|
|
|
291
300
|
if (comments.length === 0) lines.push(paint(theme, "dim", TERMINAL_STATES.includes(task.state) ? "No comments." : "No comments yet — press c to comment on the plan; the oracle amends it."));
|
|
292
301
|
for (const comment of comments) {
|
|
293
302
|
const color = comment.status === "addressed" ? "success" : comment.status === "delivered" ? "accent" : "warning";
|
|
294
|
-
|
|
303
|
+
const said = markdownHanging(`${paint(theme, color, COMMENT_MARKS[comment.status])} `, comment.text, width, theme);
|
|
304
|
+
lines.push(...trailing(said, paint(theme, "dim", `— ${COMMENT_WORDS[comment.status]}, ${since(now - Date.parse(comment.createdAt))}`), width));
|
|
295
305
|
}
|
|
296
306
|
if (task.amendments.length > 0) {
|
|
297
307
|
lines.push(...section("Amendments", width, theme));
|
|
298
|
-
for (const amendment of task.amendments) lines.push(...
|
|
308
|
+
for (const amendment of task.amendments) lines.push(...markdownHanging("- ", amendment, width, theme));
|
|
299
309
|
}
|
|
300
310
|
const approvals = pendingApprovals(task);
|
|
301
311
|
if (approvals.length > 0 || task.blockers.length > 0) {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi's theme as bot-lobby draws with it (the lobby, the questionnaire), plus
|
|
3
|
+
* Markdown rendering; one wrapper (and one Markdown renderer with its cache)
|
|
4
|
+
* per theme, so caches stay warm and a theme switch starts fresh.
|
|
5
|
+
*/
|
|
6
|
+
import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { LobbyTheme } from "./layout.ts";
|
|
8
|
+
import { createMarkdownRenderer } from "./markdown.ts";
|
|
9
|
+
|
|
10
|
+
const lobbyThemes = new WeakMap<Theme, LobbyTheme>();
|
|
11
|
+
|
|
12
|
+
export function lobbyTheme(theme: Theme): LobbyTheme {
|
|
13
|
+
let wrapped = lobbyThemes.get(theme);
|
|
14
|
+
if (!wrapped) {
|
|
15
|
+
wrapped = {
|
|
16
|
+
fg: (color, text) => theme.fg(color, text),
|
|
17
|
+
bold: (text) => theme.bold(text),
|
|
18
|
+
italic: (text) => theme.italic(text),
|
|
19
|
+
bg: (color, text) => theme.bg(color, text),
|
|
20
|
+
// The same looks pi gives your messages and the thinking it shows.
|
|
21
|
+
markdown: createMarkdownRenderer(getMarkdownTheme(), {
|
|
22
|
+
you: { color: (text) => theme.fg("accent", text) },
|
|
23
|
+
thought: { color: (text) => theme.fg("thinkingText", text), italic: true },
|
|
24
|
+
}),
|
|
25
|
+
strike: (text) => theme.strikethrough(text),
|
|
26
|
+
};
|
|
27
|
+
lobbyThemes.set(theme, wrapped);
|
|
28
|
+
}
|
|
29
|
+
return wrapped;
|
|
30
|
+
}
|
package/src/lobby/view.ts
CHANGED
|
@@ -23,7 +23,7 @@ import type { QuickFixQueue } from "./quickfix.ts";
|
|
|
23
23
|
import { MEMBER_LABELS, ORACLE_LABEL, type PlannerSeed, type PlanningSession } from "./planner.ts";
|
|
24
24
|
import { issueText, type IssuesState } from "./issues.ts";
|
|
25
25
|
import { actionFor, keyLabel, keyMap, LOBBY_ACTIONS, type KeyMap, type LobbyAction } from "./keys.ts";
|
|
26
|
-
import { beside, bold, box, fit, highlight, paint, rule, selectRow, spinner, spread, windowStart, wrap, wrapHanging, type LobbyTheme, type PaneLayout } from "./layout.ts";
|
|
26
|
+
import { beside, bold, box, fit, highlight, paint, rule, selectRow, spinner, spread, windowStart, wrap, wrapHanging, type LobbyTheme, type PaneBox, type PaneLayout, type PaneMark } from "./layout.ts";
|
|
27
27
|
import { chatTail, HOME_PANES, renderHome, type HomePane } from "./tabs/home.ts";
|
|
28
28
|
import { filterRows, pips, planDetailLines, renderTasks, taskDetailLines, taskProgress, taskRows, tasksWidths, type TaskRow } from "./tabs/tasks.ts";
|
|
29
29
|
import { renderPlan, type PlanLayout, type PlanView, type SeatView } from "./tabs/plan.ts";
|
|
@@ -320,6 +320,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
320
320
|
homeFocus: HomePane = "conversation";
|
|
321
321
|
/** How many lines each pane held last frame, so a pane scrolled back stays on what you are reading as lines arrive. */
|
|
322
322
|
private readonly seenTotals = new Map<string, number>();
|
|
323
|
+
/** Each Lobby pane's newest entry last frame: those panes measure what arrived below it, as their totals are estimates. */
|
|
324
|
+
private readonly marks = new Map<string, PaneMark>();
|
|
323
325
|
/** Where the current tab's scrollable panes landed in the last frame. */
|
|
324
326
|
private readonly panes: PaneLayout = new Map();
|
|
325
327
|
private tasksSelected = 0;
|
|
@@ -894,11 +896,10 @@ export class LobbyView implements Component, Focusable {
|
|
|
894
896
|
const limit = (name: string, offset: number, anchored: boolean): number => {
|
|
895
897
|
const pane = this.panes.get(name);
|
|
896
898
|
if (!pane) return offset;
|
|
897
|
-
const
|
|
898
|
-
this.seenTotals.set(name, pane.total);
|
|
899
|
+
const grew = this.grewBelow(name, pane);
|
|
899
900
|
let next = offset;
|
|
900
|
-
if (anchored && next > 0 &&
|
|
901
|
-
next +=
|
|
901
|
+
if (anchored && next > 0 && grew > 0) {
|
|
902
|
+
next += grew;
|
|
902
903
|
moved = true;
|
|
903
904
|
}
|
|
904
905
|
return Math.min(next, Math.max(0, pane.total - pane.rows));
|
|
@@ -922,6 +923,25 @@ export class LobbyView implements Component, Focusable {
|
|
|
922
923
|
return moved;
|
|
923
924
|
}
|
|
924
925
|
|
|
926
|
+
/**
|
|
927
|
+
* Lines that arrived below a pane since it was last looked at, counted once.
|
|
928
|
+
* A pane that measures them from its newest entry says so; for the others
|
|
929
|
+
* (their totals exact) the total's growth stands in. An estimated total also
|
|
930
|
+
* moves as scrolling draws more or fewer of the entries it estimates, and
|
|
931
|
+
* following that pinned a pane scrolled to its top there.
|
|
932
|
+
*/
|
|
933
|
+
private grewBelow(name: string, pane: PaneBox): number {
|
|
934
|
+
const seen = this.seenTotals.get(name);
|
|
935
|
+
this.seenTotals.set(name, pane.total);
|
|
936
|
+
const follow = pane.follow;
|
|
937
|
+
if (!follow) return seen === undefined ? 0 : pane.total - seen;
|
|
938
|
+
if (follow.mark) this.marks.set(name, follow.mark);
|
|
939
|
+
else this.marks.delete(name);
|
|
940
|
+
const grew = follow.grew;
|
|
941
|
+
follow.grew = 0;
|
|
942
|
+
return grew;
|
|
943
|
+
}
|
|
944
|
+
|
|
925
945
|
/** Move the draft cursor by `delta` lines, skipping blank ones, and keep it on screen. */
|
|
926
946
|
private moveCursor(delta: number): void {
|
|
927
947
|
const lines = this.planLayout.draftText;
|
|
@@ -2276,6 +2296,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2276
2296
|
others,
|
|
2277
2297
|
pending: this.data.plans.filter((plan) => plan.status === "pending").length,
|
|
2278
2298
|
offsets: this.homeOffsets,
|
|
2299
|
+
anchors: this.marks,
|
|
2279
2300
|
...(this.mode === "browse" ? { focus: this.homeFocus } : {}),
|
|
2280
2301
|
panes: this.panes,
|
|
2281
2302
|
tick: this.tick,
|
|
@@ -2306,6 +2327,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2306
2327
|
others: 0,
|
|
2307
2328
|
pending: 0,
|
|
2308
2329
|
offsets: this.homeOffsets,
|
|
2330
|
+
anchors: this.marks,
|
|
2309
2331
|
...(this.mode === "browse" ? { focus: this.homeFocus } : {}),
|
|
2310
2332
|
panes: this.panes,
|
|
2311
2333
|
tick: this.tick,
|
package/src/master/decisions.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { detectGaps, domainsInvolved } from "./synthesis.ts";
|
|
|
7
7
|
export function completionBlockers(task: Task, pendingCount: number): string[] {
|
|
8
8
|
const blockers: string[] = [];
|
|
9
9
|
if (!task.plan) blockers.push("no approved plan is recorded");
|
|
10
|
-
if (task.qaVerdict !== "pass") blockers.push(`QA gate is ${task.qaVerdict ?? "not run"}`);
|
|
10
|
+
if (task.qaVerdict !== "pass" && !task.qaWaiver) blockers.push(`QA gate is ${task.qaVerdict ?? "not run"}`);
|
|
11
11
|
if (pendingCount > 0) blockers.push(`${pendingCount} unresolved approval request(s)`);
|
|
12
12
|
if (task.blockers.length > 0) blockers.push(`${task.blockers.length} unresolved blocker(s)`);
|
|
13
13
|
return blockers;
|
package/src/master/master.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { profileFor, type BotLobbyConfig, type ProfileResolver } from "../schema
|
|
|
4
4
|
import type { Domain } from "../schemas/agent.ts";
|
|
5
5
|
import type { AgentRun, ReviewResult, ScoutResult, WorkerResult } from "../schemas/findings.ts";
|
|
6
6
|
import { domainSpec } from "../agents/registry.ts";
|
|
7
|
-
import { runAgent, runParallel, watchdogOptions, type AgentRequest } from "../execution/agent-runner.ts";
|
|
8
|
-
import { spawnPiProcess, type ProcessRunner } from "../execution/pi-runner.ts";
|
|
7
|
+
import { runAgent, runParallel, watchdogOptions, type AgentRequest, type AgentTime } from "../execution/agent-runner.ts";
|
|
8
|
+
import { spawnPiProcess, type ProcessRunner, type RelayAsk } from "../execution/pi-runner.ts";
|
|
9
9
|
import { readAgentKnowledge, writeFileEnsured } from "../knowledge/store.ts";
|
|
10
10
|
import { selectKnowledge, type KnowledgeSelection } from "../knowledge/selector.ts";
|
|
11
11
|
import { summarizeOutcomes } from "./synthesis.ts";
|
|
@@ -24,6 +24,8 @@ export interface ScoutOutcome {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export interface ScoutRequest {
|
|
27
|
+
/** Under a task time budget: the batch's time (every scout runs within it). */
|
|
28
|
+
time?: AgentTime;
|
|
27
29
|
taskId: string;
|
|
28
30
|
/** Requirements/objective text used for context selection. */
|
|
29
31
|
taskText: string;
|
|
@@ -65,7 +67,7 @@ function scoutContext(request: ScoutRequest, domain: Domain, likely = ""): Agent
|
|
|
65
67
|
task: request.taskText,
|
|
66
68
|
...selectKnowledge(`${request.taskText} ${domainSpec(domain).scoutFocus}`, slices),
|
|
67
69
|
instructions: request.config.agents[domain].instructions,
|
|
68
|
-
workflowContext: [`Task state: scouting. Domain: ${domain}. Read-only reconnaissance; no implementation.`, likely].filter(Boolean).join("\n\n"),
|
|
70
|
+
workflowContext: [`Task state: scouting. Domain: ${domain}. Read-only reconnaissance; no implementation.`, likely, request.time?.note ?? ""].filter(Boolean).join("\n\n"),
|
|
69
71
|
};
|
|
70
72
|
}
|
|
71
73
|
|
|
@@ -120,6 +122,7 @@ export async function runScouts(request: ScoutRequest, run: ProcessRunner = spaw
|
|
|
120
122
|
context: scoutContext(request, domain, likely[index]),
|
|
121
123
|
...(extraTools.length > 0 ? { extraTools } : {}),
|
|
122
124
|
...profileFields(request.config, request.profile, domain, "scout"),
|
|
125
|
+
...(request.time ? { time: { ...request.time } } : {}),
|
|
123
126
|
cwd: request.cwd,
|
|
124
127
|
signal: request.signal,
|
|
125
128
|
onUpdate: request.onUpdate,
|
|
@@ -203,6 +206,21 @@ export interface WorkerRequest {
|
|
|
203
206
|
hints?: FileHinter;
|
|
204
207
|
/** Lowers thinking (or the model) for a step the classifier judges simple or trivial. */
|
|
205
208
|
effort?: EffortRouter;
|
|
209
|
+
/** Under a task time budget: the step's time, and who decides on more when it runs out. */
|
|
210
|
+
time?: AgentTime;
|
|
211
|
+
/** The worker may ask the user (the designer): its questions are relayed, and its images saved in `previews`. */
|
|
212
|
+
ask?: { onAsk: RelayAsk; previews: string };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** What a worker that may ask the user is told about it. */
|
|
216
|
+
function askGuidance(previews: string): string {
|
|
217
|
+
return [
|
|
218
|
+
"You can ask the user with ask_user_question: the oracle relays it, and your clock stops while they answer.",
|
|
219
|
+
"Ask when a design decision is genuinely theirs (a visual direction, a layout, a style), once, with all such questions together, before you build it; decide everything else yourself.",
|
|
220
|
+
"Show each option. Give it a `preview`: a Markdown wireframe (a fenced block drawn with box characters) or a short snippet of the component.",
|
|
221
|
+
`When you can render an option (a screenshot from the project's own tooling, a rendered mockup), save it as a PNG under ${previews} (never in the repository) and give its path as the option's \`image\`.`,
|
|
222
|
+
"Put the option you recommend first. The answers are recorded as the task's decisions: build what the user chose.",
|
|
223
|
+
].join(" ");
|
|
206
224
|
}
|
|
207
225
|
|
|
208
226
|
function workerWorkflowContext(request: WorkerRequest, likely = ""): string {
|
|
@@ -218,6 +236,8 @@ function workerWorkflowContext(request: WorkerRequest, likely = ""): string {
|
|
|
218
236
|
? `Scout findings for your domain:\n${summarizeOutcomes(own, 1500)}`
|
|
219
237
|
: "No scout findings were collected for your domain; verify the repository yourself.",
|
|
220
238
|
likely,
|
|
239
|
+
request.time?.note ?? "",
|
|
240
|
+
request.ask ? askGuidance(request.ask.previews) : "",
|
|
221
241
|
].filter(Boolean).join("\n\n");
|
|
222
242
|
}
|
|
223
243
|
|
|
@@ -253,11 +273,18 @@ export async function runWorker(
|
|
|
253
273
|
...watchdogOptions(request.config.workflow),
|
|
254
274
|
...request.agent,
|
|
255
275
|
...(extraTools.length > 0 ? { extraTools } : {}),
|
|
276
|
+
// One allotment for the step: a routed attempt that falls short re-runs on what is left of it.
|
|
277
|
+
...(request.time ? { time: request.time } : {}),
|
|
278
|
+
...(request.ask ? { onAsk: request.ask.onAsk } : {}),
|
|
256
279
|
};
|
|
257
280
|
let outcome = workerOutcome(request.domain, await runAgent(route ? { ...base, ...routedFields(route) } : base, run));
|
|
258
281
|
// A routed step that fell short runs again at the configured model and thinking.
|
|
259
282
|
if (route && fellShort(outcome.run, outcome.issues) && !request.signal?.aborted) {
|
|
283
|
+
const routed = outcome.run.edited ?? [];
|
|
260
284
|
outcome = workerOutcome(request.domain, await runAgent(base, run));
|
|
285
|
+
// The step owns what the routed attempt edited too.
|
|
286
|
+
const edited = [...new Set([...routed, ...(outcome.run.edited ?? [])])];
|
|
287
|
+
if (edited.length > 0) outcome = { ...outcome, run: { ...outcome.run, edited } };
|
|
261
288
|
}
|
|
262
289
|
return outcome;
|
|
263
290
|
}
|
|
@@ -284,6 +311,12 @@ export interface ReviewerRequest {
|
|
|
284
311
|
workerSummary: string;
|
|
285
312
|
scoutOutcomes: ScoutOutcome[];
|
|
286
313
|
diff: string;
|
|
314
|
+
/** Who changed each changed file (planned, quick fix, pre-existing, …), one line each. */
|
|
315
|
+
provenance?: string;
|
|
316
|
+
/** What the previous QA round asked for, when there was one: this round verifies it. */
|
|
317
|
+
previousRound?: string;
|
|
318
|
+
/** Under a task time budget: the gate's time. */
|
|
319
|
+
time?: AgentTime;
|
|
287
320
|
instruction?: string;
|
|
288
321
|
cwd: string;
|
|
289
322
|
dataRoots: readonly string[];
|
|
@@ -301,8 +334,11 @@ function reviewerContext(request: ReviewerRequest): string {
|
|
|
301
334
|
return [
|
|
302
335
|
`Task state: reviewing. Domain: ${request.domain}.`,
|
|
303
336
|
"You may not modify implementation. Report required changes instead.",
|
|
304
|
-
request.
|
|
337
|
+
request.previousRound ?? "",
|
|
338
|
+
request.workerSummary ? `Worker summary (each domain's newest entries):\n${truncate(request.workerSummary, 4800)}` : "No worker summary available.",
|
|
305
339
|
owns.length > 0 ? `Scout findings:\n${summarizeOutcomes(owns, 1200)}` : "",
|
|
340
|
+
request.provenance ? `Who changed each file (bot-lobby's record of every agent's edit and write calls; judge each as your role's Change provenance says):\n${request.provenance}` : "",
|
|
341
|
+
request.time?.note ?? "",
|
|
306
342
|
`Repository changes:\n${request.diff}`,
|
|
307
343
|
]
|
|
308
344
|
.filter((line) => line.length > 0)
|
|
@@ -334,6 +370,7 @@ function reviewerAgentRequest(request: ReviewerRequest, selected: KnowledgeSelec
|
|
|
334
370
|
workflowContext: reviewerContext(request),
|
|
335
371
|
},
|
|
336
372
|
...profileFields(request.config, request.profile, request.domain, "reviewer"),
|
|
373
|
+
...(request.time ? { time: request.time } : {}),
|
|
337
374
|
cwd: request.cwd,
|
|
338
375
|
signal: request.signal,
|
|
339
376
|
onUpdate: request.onUpdate,
|
package/src/master/research.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import type { Domain } from "../schemas/agent.ts";
|
|
4
4
|
import { profileFor, type BotLobbyConfig, type ProfileResolver } from "../schemas/configuration.ts";
|
|
5
5
|
import type { AgentRun, ResearchResult } from "../schemas/findings.ts";
|
|
6
|
-
import { runAgent, watchdogOptions, type AgentRequest } from "../execution/agent-runner.ts";
|
|
6
|
+
import { runAgent, watchdogOptions, type AgentRequest, type AgentTime } from "../execution/agent-runner.ts";
|
|
7
7
|
import { spawnPiProcess, type ProcessRunner } from "../execution/pi-runner.ts";
|
|
8
8
|
import { writeFileEnsured } from "../knowledge/store.ts";
|
|
9
9
|
import { isResearchResultUsable, parseResearchResult, validateResearchResult } from "../roles/researcher.ts";
|
|
@@ -27,6 +27,8 @@ export interface ResearchRequest {
|
|
|
27
27
|
profile?: ProfileResolver;
|
|
28
28
|
signal?: AbortSignal;
|
|
29
29
|
onUpdate?: (run: AgentRun) => void;
|
|
30
|
+
/** Under a task time budget: the researcher's time. */
|
|
31
|
+
time?: AgentTime;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export function researchResultPath(taskDir: string, domain: Domain): string {
|
|
@@ -56,7 +58,7 @@ function researchContext(request: ResearchRequest): AgentRequest["context"] {
|
|
|
56
58
|
return {
|
|
57
59
|
task: request.instruction,
|
|
58
60
|
instructions: request.config.agents[request.domain].instructions,
|
|
59
|
-
workflowContext: `Task state: research requested by the Master. Domain: ${request.domain}. Read-only; report cited findings only.`,
|
|
61
|
+
workflowContext: [`Task state: research requested by the Master. Domain: ${request.domain}. Read-only; report cited findings only.`, request.time?.note ?? ""].filter(Boolean).join("\n\n"),
|
|
60
62
|
};
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -86,6 +88,7 @@ export async function runResearch(request: ResearchRequest, run: ProcessRunner =
|
|
|
86
88
|
model: profile.model,
|
|
87
89
|
thinking: profile.thinking,
|
|
88
90
|
timeoutMs: profile.timeoutMs,
|
|
91
|
+
...(request.time ? { time: request.time } : {}),
|
|
89
92
|
cwd: request.cwd,
|
|
90
93
|
signal: request.signal,
|
|
91
94
|
onUpdate: request.onUpdate,
|
package/src/pi/commands.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { transition } from "../state/task-state.ts";
|
|
|
17
17
|
import { AGENT_DIR_NAMES, KNOWLEDGE_FILES, knowledgeDir, type KnowledgeAgent } from "../knowledge/paths.ts";
|
|
18
18
|
import { readFirstExisting } from "../knowledge/store.ts";
|
|
19
19
|
import { overThreshold } from "../knowledge/compactor.ts";
|
|
20
|
-
import { applyApprovalChoice, describeTask, describeOversizedKnowledge, type ApprovalChoice } from "../workflow/workflow.ts";
|
|
20
|
+
import { applyApprovalChoice, describeTask, describeOversizedKnowledge, lastQaAsks, waiveQa, type ApprovalChoice } from "../workflow/workflow.ts";
|
|
21
21
|
import { applyStatus, registerRevealShortcut, setMinimized } from "./ui.ts";
|
|
22
22
|
import { classifierSummary, openSettings } from "./settings-ui.ts";
|
|
23
23
|
import { keyStatus } from "../classifier/instance.ts";
|
|
@@ -27,16 +27,20 @@ import { autoOpenLobby, showLobby } from "../lobby/runtime.ts";
|
|
|
27
27
|
import { modelRef, thinkingMismatches } from "./model-support.ts";
|
|
28
28
|
import { describeRun, runFromLog } from "./run-summary.ts";
|
|
29
29
|
import { modelLookup } from "./tools.ts";
|
|
30
|
+
import { budgetLine, budgetState, parseMinutes, readBudget, setBudget, startClock } from "../state/budget.ts";
|
|
30
31
|
|
|
31
32
|
const HELP = [
|
|
32
33
|
"/bot-lobby Open the lobby: tasks, plan, quick fix, metrics (alt+l)",
|
|
33
34
|
"/bot-lobby <request> Start a task through the workflow",
|
|
34
35
|
"/bot-lobby --task [--auto] <request> Start a task even when the request begins with a subcommand word",
|
|
36
|
+
"/bot-lobby --budget 90m <request> Start a task with a time budget the oracle divides between its agents",
|
|
37
|
+
"/bot-lobby budget [90m|off] Show or set this session's task time budget",
|
|
35
38
|
"/bot-lobby status [taskId] Show the active task",
|
|
36
39
|
"/bot-lobby tasks List tasks",
|
|
37
40
|
"/bot-lobby pause|resume Pause or resume the active task",
|
|
38
41
|
"/bot-lobby cancel [taskId] Abandon a task",
|
|
39
42
|
"/bot-lobby approve|amend <text>|decline Answer the current proposal",
|
|
43
|
+
"/bot-lobby accept [taskId] Accept a task's work as it is, without a QA pass; the oracle then completes it",
|
|
40
44
|
"/bot-lobby knowledge Show persistent knowledge files",
|
|
41
45
|
"/bot-lobby runs [taskId] Recent subagent runs: time, turns, tokens, cost, model",
|
|
42
46
|
"/bot-lobby settings Edit per-agent model/thinking/instructions",
|
|
@@ -50,21 +54,48 @@ const HELP = [
|
|
|
50
54
|
].join("\n");
|
|
51
55
|
|
|
52
56
|
/** Subcommands only win when no free-form text follows (so tasks still start). */
|
|
53
|
-
const SUBCOMMANDS = new Set(["lobby", "help", "status", "runs", "tasks", "pause", "resume", "cancel", "approve", "amend", "decline", "knowledge", "config", "settings", "minimize", "restore", "claim", "auto", "start-plan", "switch"]);
|
|
57
|
+
const SUBCOMMANDS = new Set(["lobby", "help", "status", "runs", "tasks", "pause", "resume", "cancel", "approve", "amend", "decline", "accept", "budget", "knowledge", "config", "settings", "minimize", "restore", "claim", "auto", "start-plan", "switch"]);
|
|
54
58
|
|
|
55
59
|
function isTaskId(value: string | undefined): boolean {
|
|
56
60
|
return Boolean(value && /^TASK-/.test(value));
|
|
57
61
|
}
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
/** A task start's leading flags: `--task` (always a task), `--auto`, `--budget <time>` (or `--budget=<time>`). */
|
|
64
|
+
const START_FLAG = /^--(task|auto)(?=\s|$)\s*|^--budget(?:=|\s+)(\S+)\s*/;
|
|
65
|
+
|
|
66
|
+
export interface ParsedCommand {
|
|
67
|
+
sub: string | undefined;
|
|
68
|
+
rest: string[];
|
|
69
|
+
restText: string;
|
|
70
|
+
auto?: boolean;
|
|
71
|
+
/** Minutes from `--budget`. */
|
|
72
|
+
budget?: number;
|
|
73
|
+
/** `--budget` with something that is not a time. */
|
|
74
|
+
budgetError?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function parseCommand(args: string): ParsedCommand {
|
|
78
|
+
let trimmed = args.trim();
|
|
79
|
+
// Leading flags start a task: what follows is its request, even when it begins with a subcommand word
|
|
80
|
+
// (a background session started from the lobby sends `--task [--auto] <request>`).
|
|
81
|
+
let flagged = false;
|
|
82
|
+
const extras: Pick<ParsedCommand, "auto" | "budget" | "budgetError"> = {};
|
|
83
|
+
for (let match = START_FLAG.exec(trimmed); match; match = START_FLAG.exec(trimmed)) {
|
|
84
|
+
flagged = true;
|
|
85
|
+
if (match[1] === "auto") extras.auto = true;
|
|
86
|
+
else if (match[2] !== undefined) {
|
|
87
|
+
const minutes = parseMinutes(match[2]);
|
|
88
|
+
if (minutes) extras.budget = minutes;
|
|
89
|
+
else extras.budgetError = `"${match[2]}" is not a time budget (try 90m, 1h or 1h30m)`;
|
|
90
|
+
}
|
|
91
|
+
trimmed = trimmed.slice(match[0].length);
|
|
92
|
+
}
|
|
93
|
+
if (flagged) return { sub: undefined, rest: [], restText: trimmed.trim(), ...extras };
|
|
64
94
|
const [sub, ...rest] = trimmed.split(/\s+/).filter(Boolean);
|
|
65
95
|
if (!sub || !SUBCOMMANDS.has(sub)) return { sub: undefined, rest: [], restText: trimmed };
|
|
66
96
|
const takesArgs = sub === "amend" || sub === "claim"
|
|
67
97
|
|| (sub === "auto" && rest.length === 1 && /^(on|off)$/i.test(rest[0]!))
|
|
98
|
+
|| (sub === "budget" && rest.length >= 1 && rest.length <= 2 && (/^off$/i.test(rest[0]!) || parseMinutes(rest.join("")) !== undefined))
|
|
68
99
|
|| (sub === "start-plan" && rest.length >= 1 && rest.length <= 2 && /^PLAN-/.test(rest[0]!) && (rest.length === 1 || rest[1] === "auto"))
|
|
69
100
|
|| (sub === "switch" && rest.length >= 1 && /\.jsonl$/.test(rest.join(" ")));
|
|
70
101
|
if (!takesArgs && rest.length > 0 && !(rest.length === 1 && isTaskId(rest[0]))) {
|
|
@@ -87,7 +118,9 @@ function showStatus(ctx: ExtensionCommandContext, configDir: string, taskId?: st
|
|
|
87
118
|
broken.length > 0 ? `Unreadable task state: ${broken.join(", ")}` : "",
|
|
88
119
|
].filter(Boolean).join("\n");
|
|
89
120
|
const footer = knowledge ? `\n${knowledge}` : "";
|
|
90
|
-
|
|
121
|
+
const budget = task ? readBudget(root, configDir, task.id) : undefined;
|
|
122
|
+
const time = task && budget ? `\n${budgetLine(budget, budgetState(task.id, budget, task.qaVerdict === "pass"))}` : "";
|
|
123
|
+
ctx.ui.notify(task ? `${describeTask(task)}${time}${footer}` : `No bot-lobby task found in ${root}.${footer}`, task ? "info" : "warning");
|
|
91
124
|
}
|
|
92
125
|
|
|
93
126
|
/** Recent runs of the active (or named) task, newest last, so slow models are easy to spot. */
|
|
@@ -175,6 +208,52 @@ function answerProposal(
|
|
|
175
208
|
}
|
|
176
209
|
}
|
|
177
210
|
|
|
211
|
+
/**
|
|
212
|
+
* `/bot-lobby accept [taskId]`: the user accepts a task's work as it stands,
|
|
213
|
+
* without a QA pass (QA keeps asking for more, and the user has seen enough).
|
|
214
|
+
* The gate is waived and blockers cleared; the oracle is asked to complete it.
|
|
215
|
+
*/
|
|
216
|
+
function acceptWork(pi: ExtensionAPI, ctx: ExtensionCommandContext, configDir: string, taskId?: string): void {
|
|
217
|
+
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
218
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
219
|
+
const task = selectTask(root, configDir, taskId, sessionId);
|
|
220
|
+
if (!task || TERMINAL_STATES.includes(task.state)) return ctx.ui.notify("bot-lobby: no active task to accept.", "warning");
|
|
221
|
+
if (task.ownerSessionId && task.ownerSessionId !== sessionId) {
|
|
222
|
+
return ctx.ui.notify(`${task.id} is owned by another pi session; accept it there, or /bot-lobby claim ${task.id} first.`, "warning");
|
|
223
|
+
}
|
|
224
|
+
if (!["implementing", "reviewing", "blocked"].includes(task.state)) {
|
|
225
|
+
return ctx.ui.notify(`bot-lobby: ${task.id} is still ${task.state}; there is no work to accept yet.`, "warning");
|
|
226
|
+
}
|
|
227
|
+
if (task.qaVerdict !== "pass" && !task.qaWaiver) waiveQa(task, lastQaAsks(task), "with /bot-lobby accept");
|
|
228
|
+
saveTask(root, configDir, task);
|
|
229
|
+
applyStatus(ctx, root, configDir);
|
|
230
|
+
ctx.ui.notify(`bot-lobby: ${task.id} accepted as it is; the oracle will complete it.`, "info");
|
|
231
|
+
const message = `The user accepted ${task.id}'s work as it is (the QA gate is waived). Call orchestrate action=complete now with a short summary that names anything QA still asked for.`;
|
|
232
|
+
pi.sendUserMessage(message, ctx.isIdle() ? undefined : { deliverAs: "followUp" });
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* `/bot-lobby budget [time|off]`: show, set or remove the time budget of this
|
|
237
|
+
* session's task. Time already spent is kept; the oracle sees the change on
|
|
238
|
+
* its next step.
|
|
239
|
+
*/
|
|
240
|
+
function budgetCommand(ctx: ExtensionCommandContext, configDir: string, value: string): void {
|
|
241
|
+
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
242
|
+
const task = activeTask(root, configDir, ctx.sessionManager.getSessionId());
|
|
243
|
+
if (!task) return ctx.ui.notify("bot-lobby: no active task in this session — a time budget applies to a task (start one with /bot-lobby --budget 90m <request>)", "warning");
|
|
244
|
+
if (!value) {
|
|
245
|
+
const budget = readBudget(root, configDir, task.id);
|
|
246
|
+
return ctx.ui.notify(budget ? `${task.id} — ${budgetLine(budget, budgetState(task.id, budget, task.qaVerdict === "pass"))}` : `${task.id} has no time budget. Set one with /bot-lobby budget 90m.`, "info");
|
|
247
|
+
}
|
|
248
|
+
const minutes = /^off$/i.test(value) ? 0 : parseMinutes(value);
|
|
249
|
+
if (minutes === undefined) return ctx.ui.notify(`bot-lobby: "${value}" is not a time budget (try 90m, 1h or 1h30m)`, "warning");
|
|
250
|
+
const budget = setBudget(root, configDir, task.id, minutes);
|
|
251
|
+
// Set while the oracle works: its clock runs from now, not from its next turn.
|
|
252
|
+
if (budget && !ctx.isIdle()) startClock(root, configDir, task.id);
|
|
253
|
+
applyStatus(ctx, root, configDir);
|
|
254
|
+
ctx.ui.notify(budget ? `bot-lobby: ${task.id} — ${budgetLine(budget, budgetState(task.id, budget, task.qaVerdict === "pass"))}` : `bot-lobby: ${task.id} has no time budget now.`, "info");
|
|
255
|
+
}
|
|
256
|
+
|
|
178
257
|
function showKnowledge(ctx: ExtensionCommandContext, configDir: string): void {
|
|
179
258
|
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
180
259
|
const cfg = loadConfig();
|
|
@@ -236,13 +315,14 @@ export function registerCommands(pi: ExtensionAPI, configDir: string): void {
|
|
|
236
315
|
return filtered.length > 0 ? filtered : null;
|
|
237
316
|
},
|
|
238
317
|
handler: async (args, ctx) => {
|
|
239
|
-
const { sub, rest, restText, auto } = parseCommand(args ?? "");
|
|
318
|
+
const { sub, rest, restText, auto, budget, budgetError } = parseCommand(args ?? "");
|
|
319
|
+
if (budgetError) return ctx.ui.notify(`bot-lobby: ${budgetError}`, "warning");
|
|
240
320
|
if (!sub) {
|
|
241
321
|
if (!restText) {
|
|
242
322
|
if (!showLobby()) ctx.ui.notify(HELP, "info");
|
|
243
323
|
return;
|
|
244
324
|
}
|
|
245
|
-
if (await startTask(pi, ctx, configDir, restText, auto ? { auto } : {})) autoOpenLobby();
|
|
325
|
+
if (await startTask(pi, ctx, configDir, restText, { ...(auto ? { auto } : {}), ...(budget ? { budget } : {}) })) autoOpenLobby();
|
|
246
326
|
return;
|
|
247
327
|
}
|
|
248
328
|
switch (sub) {
|
|
@@ -269,6 +349,10 @@ export function registerCommands(pi: ExtensionAPI, configDir: string): void {
|
|
|
269
349
|
return answerProposal(ctx, configDir, "amend", restText);
|
|
270
350
|
case "decline":
|
|
271
351
|
return answerProposal(ctx, configDir, "decline");
|
|
352
|
+
case "accept":
|
|
353
|
+
return acceptWork(pi, ctx, configDir, rest[0]);
|
|
354
|
+
case "budget":
|
|
355
|
+
return budgetCommand(ctx, configDir, rest.join(""));
|
|
272
356
|
case "knowledge":
|
|
273
357
|
return showKnowledge(ctx, configDir);
|
|
274
358
|
case "settings":
|