@a-t-h-i/bot-lobby 0.6.2 → 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 +112 -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 +9 -0
- package/src/lobby/ask.ts +10 -120
- package/src/lobby/feed.ts +76 -8
- package/src/lobby/layout.ts +38 -18
- package/src/lobby/markdown.ts +92 -19
- package/src/lobby/planner.ts +1 -1
- package/src/lobby/quickfix.ts +21 -0
- package/src/lobby/runtime.ts +51 -37
- package/src/lobby/session-files.ts +162 -25
- package/src/lobby/sessions.ts +21 -7
- package/src/lobby/tabs/home.ts +289 -67
- 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 +141 -25
- 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 +54 -12
- package/src/pi/fresh-context.ts +134 -0
- package/src/pi/owner.ts +19 -10
- package/src/pi/quiet.ts +22 -4
- package/src/pi/start-task.ts +11 -2
- package/src/pi/tools.ts +26 -8
- package/src/pi/ui.ts +9 -3
- package/src/pi/zen-large.ts +10 -10
- package/src/pi/zen-metrics.ts +13 -3
- package/src/pi/zen.ts +22 -15
- package/src/roles/reviewer.ts +23 -4
- package/src/roles/worker.ts +18 -0
- package/src/schemas/configuration.ts +8 -0
- package/src/schemas/findings.ts +14 -0
- package/src/schemas/task.ts +21 -0
- package/src/state/archive.ts +12 -3
- package/src/state/backlog.ts +13 -3
- package/src/state/budget.ts +274 -0
- package/src/state/changes.ts +231 -0
- package/src/state/file-cache.ts +62 -0
- package/src/state/metrics.ts +63 -13
- package/src/state/persistence.ts +36 -2
- 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/width.ts +102 -0
- package/src/workflow/workflow.ts +592 -42
package/src/lobby/view.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* pane toggles, paging) work in both. Everything bot-lobby-specific arrives
|
|
10
10
|
* through `LobbyHost`, so the view renders and reacts the same in tests.
|
|
11
11
|
*/
|
|
12
|
-
import { decodeKittyPrintable, Editor, Input, Key, matchesKey,
|
|
12
|
+
import { decodeKittyPrintable, Editor, Input, Key, matchesKey, type Component, type EditorTheme, type Focusable, type TUI, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
13
|
+
import { textWidth } from "../width.ts";
|
|
13
14
|
import { TERMINAL_STATES, type Task } from "../schemas/task.ts";
|
|
14
15
|
import type { AgentRun } from "../schemas/findings.ts";
|
|
15
16
|
import type { PlannedTask } from "../state/backlog.ts";
|
|
@@ -22,8 +23,8 @@ import type { QuickFixQueue } from "./quickfix.ts";
|
|
|
22
23
|
import { MEMBER_LABELS, ORACLE_LABEL, type PlannerSeed, type PlanningSession } from "./planner.ts";
|
|
23
24
|
import { issueText, type IssuesState } from "./issues.ts";
|
|
24
25
|
import { actionFor, keyLabel, keyMap, LOBBY_ACTIONS, type KeyMap, type LobbyAction } from "./keys.ts";
|
|
25
|
-
import { beside, bold, box, fit, highlight, paint, rule, selectRow, spinner, spread, windowStart, wrap, wrapHanging, type LobbyTheme, type PaneLayout } from "./layout.ts";
|
|
26
|
-
import {
|
|
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
|
+
import { chatTail, HOME_PANES, renderHome, type HomePane } from "./tabs/home.ts";
|
|
27
28
|
import { filterRows, pips, planDetailLines, renderTasks, taskDetailLines, taskProgress, taskRows, tasksWidths, type TaskRow } from "./tabs/tasks.ts";
|
|
28
29
|
import { renderPlan, type PlanLayout, type PlanView, type SeatView } from "./tabs/plan.ts";
|
|
29
30
|
import { filterJobs, newestFirst, renderQuickFix } from "./tabs/quickfix.ts";
|
|
@@ -113,6 +114,9 @@ function sameView(a: SessionView, b: SessionView): boolean {
|
|
|
113
114
|
return true;
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
/** Tops a conversation whose earlier messages are not in memory. */
|
|
118
|
+
export const OLDER_NOTE = "↑ earlier messages load as you scroll up";
|
|
119
|
+
|
|
116
120
|
const SECTION_OF: Record<SessionWhere, string> = { "this window": "THIS WINDOW", background: "BACKGROUND", "other terminal": "OTHER TERMINALS", "not running": "NOT RUNNING" };
|
|
117
121
|
const WHERE_MARKS: Record<SessionWhere, string> = { "this window": "●", background: "◆", "other terminal": "◇", "not running": "○" };
|
|
118
122
|
|
|
@@ -189,8 +193,12 @@ export interface LobbyHost {
|
|
|
189
193
|
archiveTask(taskId: string): string;
|
|
190
194
|
restoreTask(taskId: string): string;
|
|
191
195
|
deleteTask(taskId: string, where: "list" | "archive"): string;
|
|
192
|
-
/** Another session's conversation, read from its saved session file. */
|
|
196
|
+
/** Another session's conversation, read from its saved session file (its newest messages). */
|
|
193
197
|
sessionChat(sessionId: string): ChatEntry[];
|
|
198
|
+
/** Whether another session has messages earlier than `sessionChat` returns. */
|
|
199
|
+
hasOlderChat(sessionId: string): boolean;
|
|
200
|
+
/** A session's whole conversation, oldest first: this window's (no id) or another's; loaded only while scrolled back to it. */
|
|
201
|
+
chatHistory(sessionId?: string): readonly ChatEntry[];
|
|
194
202
|
/** A task's status without animations, for a session other than this window's. */
|
|
195
203
|
taskScene(task: Task, width: number, height: number): string[];
|
|
196
204
|
profileLabel(kind: LobbyAgentKind): string;
|
|
@@ -216,7 +224,7 @@ class LobbyEditor extends Editor {
|
|
|
216
224
|
if (!this.label) return super.renderTopBorder(width, hiddenLineCount);
|
|
217
225
|
const label = ` ${this.label} `;
|
|
218
226
|
const more = hiddenLineCount > 0 ? ` ↑${hiddenLineCount} ` : "";
|
|
219
|
-
const rest = width - 2 -
|
|
227
|
+
const rest = width - 2 - textWidth(label) - textWidth(more);
|
|
220
228
|
if (rest < 1) return super.renderTopBorder(width, hiddenLineCount);
|
|
221
229
|
return `${this.borderColor("──")}${this.paintLabel(label)}${this.borderColor("─".repeat(rest))}${more ? this.borderColor(more) : ""}`;
|
|
222
230
|
}
|
|
@@ -312,6 +320,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
312
320
|
homeFocus: HomePane = "conversation";
|
|
313
321
|
/** How many lines each pane held last frame, so a pane scrolled back stays on what you are reading as lines arrive. */
|
|
314
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>();
|
|
315
325
|
/** Where the current tab's scrollable panes landed in the last frame. */
|
|
316
326
|
private readonly panes: PaneLayout = new Map();
|
|
317
327
|
private tasksSelected = 0;
|
|
@@ -430,6 +440,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
430
440
|
refreshData(force: boolean): void {
|
|
431
441
|
const now = this.now();
|
|
432
442
|
if (!force && now - this.data.at < DATA_REFRESH_MS) return;
|
|
443
|
+
this.dataVersion += 1;
|
|
433
444
|
this.data = {
|
|
434
445
|
tasks: this.host.tasks(),
|
|
435
446
|
plans: this.host.plans(),
|
|
@@ -838,6 +849,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
838
849
|
break;
|
|
839
850
|
}
|
|
840
851
|
this.clampScroll();
|
|
852
|
+
if (pane === "conversation") this.followHistory();
|
|
841
853
|
}
|
|
842
854
|
|
|
843
855
|
/** Home and End: the oldest or first line of the focused pane, or back to its newest or last. */
|
|
@@ -861,6 +873,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
861
873
|
return;
|
|
862
874
|
}
|
|
863
875
|
this.scrollPane(pane, far);
|
|
876
|
+
// Home loads earlier messages at the top; go on to the very first of them.
|
|
877
|
+
if (start && pane === "conversation" && this.history) this.homeOffsets.conversation = Number.MAX_SAFE_INTEGER;
|
|
864
878
|
}
|
|
865
879
|
|
|
866
880
|
/** The wheel over the draft scrolls it; the cursor follows only when it would leave the screen. */
|
|
@@ -882,11 +896,10 @@ export class LobbyView implements Component, Focusable {
|
|
|
882
896
|
const limit = (name: string, offset: number, anchored: boolean): number => {
|
|
883
897
|
const pane = this.panes.get(name);
|
|
884
898
|
if (!pane) return offset;
|
|
885
|
-
const
|
|
886
|
-
this.seenTotals.set(name, pane.total);
|
|
899
|
+
const grew = this.grewBelow(name, pane);
|
|
887
900
|
let next = offset;
|
|
888
|
-
if (anchored && next > 0 &&
|
|
889
|
-
next +=
|
|
901
|
+
if (anchored && next > 0 && grew > 0) {
|
|
902
|
+
next += grew;
|
|
890
903
|
moved = true;
|
|
891
904
|
}
|
|
892
905
|
return Math.min(next, Math.max(0, pane.total - pane.rows));
|
|
@@ -910,6 +923,25 @@ export class LobbyView implements Component, Focusable {
|
|
|
910
923
|
return moved;
|
|
911
924
|
}
|
|
912
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
|
+
|
|
913
945
|
/** Move the draft cursor by `delta` lines, skipping blank ones, and keep it on screen. */
|
|
914
946
|
private moveCursor(delta: number): void {
|
|
915
947
|
const lines = this.planLayout.draftText;
|
|
@@ -954,7 +986,9 @@ export class LobbyView implements Component, Focusable {
|
|
|
954
986
|
const rows = this.taskRowList();
|
|
955
987
|
this.tasksSelected = Math.max(0, Math.min(rows.length - 1, index));
|
|
956
988
|
this.tasksDetailOffset = 0;
|
|
957
|
-
|
|
989
|
+
// Only the newly selected task's comments: moving through the list must not reread every task from disk.
|
|
990
|
+
const row = rows[this.tasksSelected];
|
|
991
|
+
if (row?.kind === "task" && !this.data.comments.has(row.id)) this.data.comments.set(row.id, this.host.comments(row.id));
|
|
958
992
|
}
|
|
959
993
|
|
|
960
994
|
private async loadIssueDetail(): Promise<void> {
|
|
@@ -1205,6 +1239,50 @@ export class LobbyView implements Component, Focusable {
|
|
|
1205
1239
|
void this.host.answerDialog(session).then(() => this.host.requestRender());
|
|
1206
1240
|
}
|
|
1207
1241
|
|
|
1242
|
+
/* -------------------------------------------------------------- history */
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* The whole conversation of the session in view, loaded when the
|
|
1246
|
+
* conversation pane is scrolled back past the messages kept in memory, and
|
|
1247
|
+
* let go once it is back at the newest.
|
|
1248
|
+
*/
|
|
1249
|
+
private history?: { view: SessionView; entries: readonly ChatEntry[] };
|
|
1250
|
+
|
|
1251
|
+
/** The conversation to draw for the session in view, and whether earlier messages wait to be loaded. */
|
|
1252
|
+
private conversationFor(entry: SessionEntry): { chat: readonly ChatEntry[]; older: boolean } {
|
|
1253
|
+
if (this.history && sameView(this.history.view, entry.view)) return { chat: this.history.entries, older: false };
|
|
1254
|
+
if (entry.view.kind === "here") return { chat: this.host.feed.chat, older: this.host.feed.chatOlder };
|
|
1255
|
+
if (entry.view.kind === "background") {
|
|
1256
|
+
const session = this.viewedSessionFor(entry.view.key);
|
|
1257
|
+
return { chat: session?.feed.chat ?? [], older: Boolean(session?.feed.chatOlder && session.sessionId) };
|
|
1258
|
+
}
|
|
1259
|
+
return entry.sessionId ? { chat: this.host.sessionChat(entry.sessionId), older: this.host.hasOlderChat(entry.sessionId) } : { chat: [], older: false };
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/**
|
|
1263
|
+
* After the conversation pane scrolls: at its top, with earlier messages
|
|
1264
|
+
* waiting, load the whole conversation; back at its newest, let it go.
|
|
1265
|
+
* True when history was loaded now.
|
|
1266
|
+
*/
|
|
1267
|
+
private followHistory(): boolean {
|
|
1268
|
+
if (this.tab !== "lobby") return false;
|
|
1269
|
+
if (this.homeOffsets.conversation === 0) {
|
|
1270
|
+
this.history = undefined;
|
|
1271
|
+
return false;
|
|
1272
|
+
}
|
|
1273
|
+
if (this.history && sameView(this.history.view, this.viewing)) return false;
|
|
1274
|
+
const pane = this.panes.get("conversation");
|
|
1275
|
+
if (!pane || this.homeOffsets.conversation < pane.total - pane.rows) return false;
|
|
1276
|
+
const entry = this.viewedEntry();
|
|
1277
|
+
if (!this.conversationFor(entry).older) return false;
|
|
1278
|
+
const sessionId = entry.view.kind === "here" ? undefined : entry.view.kind === "background" ? this.viewedSessionFor(entry.view.key)?.sessionId : entry.sessionId;
|
|
1279
|
+
if (entry.view.kind !== "here" && !sessionId) return false;
|
|
1280
|
+
const entries = this.host.chatHistory(sessionId);
|
|
1281
|
+
if (entries.length === 0) return false;
|
|
1282
|
+
this.history = { view: entry.view, entries };
|
|
1283
|
+
return true;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1208
1286
|
/* ------------------------------------------------------------ sessions */
|
|
1209
1287
|
|
|
1210
1288
|
/** Every session the lobby can show: this window, the background sessions it started, then tasks other terminals drive. */
|
|
@@ -1270,6 +1348,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
1270
1348
|
this.viewing = target;
|
|
1271
1349
|
this.picking = false;
|
|
1272
1350
|
this.newSession = false;
|
|
1351
|
+
this.history = undefined;
|
|
1273
1352
|
for (const pane of HOME_PANES) this.homeOffsets[pane] = 0;
|
|
1274
1353
|
if (this.tab !== "lobby") this.setTab("lobby");
|
|
1275
1354
|
this.setMode("type");
|
|
@@ -1657,8 +1736,41 @@ export class LobbyView implements Component, Focusable {
|
|
|
1657
1736
|
|
|
1658
1737
|
/* -------------------------------------------------------------- render */
|
|
1659
1738
|
|
|
1739
|
+
/** Bumped each time the data is reread. */
|
|
1740
|
+
private dataVersion = 0;
|
|
1741
|
+
/** Task rows as last built: several parts of a frame ask, and they only change with the data, the search or the clock. */
|
|
1742
|
+
private rowsMemo?: { key: string; rows: TaskRow[] };
|
|
1743
|
+
|
|
1744
|
+
/** The Metrics tab's figures as last computed: they change only with the data, the grouping, the order or the search. */
|
|
1745
|
+
private metricsMemo?: { key: string; figures: ReturnType<LobbyView["computeMetrics"]> };
|
|
1746
|
+
|
|
1747
|
+
private metricsFigures(query: string | undefined) {
|
|
1748
|
+
const key = `${this.dataVersion}|${this.metricsBy}|${this.metricsSort}|${query ?? ""}`;
|
|
1749
|
+
if (this.metricsMemo?.key !== key) this.metricsMemo = { key, figures: this.computeMetrics(query) };
|
|
1750
|
+
return this.metricsMemo.figures;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
private computeMetrics(query: string | undefined) {
|
|
1754
|
+
const records = filterRecords(collectMetrics(this.data.metrics, this.data.tasks), query);
|
|
1755
|
+
return {
|
|
1756
|
+
records,
|
|
1757
|
+
groups: sortGroups(aggregateMetrics(records, this.metricsBy), this.metricsSort),
|
|
1758
|
+
taskTimes: taskTimesByModel(this.data.tasks, records),
|
|
1759
|
+
stats: taskStats(this.data.tasks),
|
|
1760
|
+
jev: summarizeClassifier(this.data.classifierMetrics, records),
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1660
1764
|
/** Task rows, narrowed by the Tasks tab's search. */
|
|
1661
1765
|
private taskRowList(): TaskRow[] {
|
|
1766
|
+
const key = `${this.dataVersion}|${this.tick}|${this.showArchived}|${this.queries.tasks ?? ""}`;
|
|
1767
|
+
if (this.rowsMemo?.key === key) return this.rowsMemo.rows;
|
|
1768
|
+
const rows = this.buildTaskRows();
|
|
1769
|
+
this.rowsMemo = { key, rows };
|
|
1770
|
+
return rows;
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
private buildTaskRows(): TaskRow[] {
|
|
1662
1774
|
const names = new Map<string, string>();
|
|
1663
1775
|
for (const session of this.host.sessions()) if (session.sessionId && session.alive) names.set(session.sessionId, session.name);
|
|
1664
1776
|
const auto = new Set(this.data.tasks.filter((task) => !TERMINAL_STATES.includes(task.state) && this.host.isAuto(task.id)).map((task) => task.id));
|
|
@@ -1691,12 +1803,12 @@ export class LobbyView implements Component, Focusable {
|
|
|
1691
1803
|
const badge = badges[tab] ? ` ${paint(theme, tab === "plan" && session?.awaitingAnswers ? "warning" : "accent", badges[tab]!)}` : "";
|
|
1692
1804
|
let cell = ` ${number} ${name}${badge} `;
|
|
1693
1805
|
if (tab === this.tab) cell = theme.bg ? theme.bg("selectedBg", cell) : `[${cell}]`;
|
|
1694
|
-
const from =
|
|
1806
|
+
const from = textWidth(left);
|
|
1695
1807
|
left += cell;
|
|
1696
|
-
spans.push({ tab, from, to:
|
|
1808
|
+
spans.push({ tab, from, to: textWidth(left) });
|
|
1697
1809
|
}
|
|
1698
1810
|
this.tabSpans = spans;
|
|
1699
|
-
const room = width -
|
|
1811
|
+
const room = width - textWidth(left) - 1;
|
|
1700
1812
|
const entry = this.viewedEntry();
|
|
1701
1813
|
const help = paint(theme, "dim", `${keyLabel(this.keys.help)} keys `);
|
|
1702
1814
|
const auto = entry.auto ? ` ${bold(theme, paint(theme, "success", "⟳ AUTO"))}` : "";
|
|
@@ -1717,8 +1829,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
1717
1829
|
? [`${dot} ${zen.task.id} ${state}${auto}${asking} ${help}`, `${dot} ${zen.task.id} ${state}${auto}${asking} `, `${dot} ${state}${auto} `, `${dot}${auto} `]
|
|
1718
1830
|
: [`${paint(theme, "dim", "no task in this session")}${asking} ${help}`, `${paint(theme, "dim", "no task in this session")}${asking} `, help, ""];
|
|
1719
1831
|
}
|
|
1720
|
-
const status = choices.find((choice) =>
|
|
1721
|
-
const gap = width -
|
|
1832
|
+
const status = choices.find((choice) => textWidth(choice) <= room) ?? "";
|
|
1833
|
+
const gap = width - textWidth(left) - textWidth(status);
|
|
1722
1834
|
return gap >= 1 ? `${left}${" ".repeat(gap)}${status}` : fit(left, width);
|
|
1723
1835
|
}
|
|
1724
1836
|
|
|
@@ -1780,7 +1892,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
1780
1892
|
const tail = this.searching
|
|
1781
1893
|
? paint(theme, "dim", " enter keep · esc clear · ↑↓ browse results ")
|
|
1782
1894
|
: paint(theme, "dim", ` ${keyLabel(this.keys.search)} or / edits · esc clears `);
|
|
1783
|
-
const room = Math.max(4, width -
|
|
1895
|
+
const room = Math.max(4, width - textWidth(lead) - textWidth(tail));
|
|
1784
1896
|
const field = this.searching ? this.search.render(room)[0] ?? "" : bold(theme, fit(query ?? "", room));
|
|
1785
1897
|
return [fit(`${lead}${fit(field, room)}${tail}`, width)];
|
|
1786
1898
|
}
|
|
@@ -2018,12 +2130,9 @@ export class LobbyView implements Component, Focusable {
|
|
|
2018
2130
|
}, width, height, theme);
|
|
2019
2131
|
}
|
|
2020
2132
|
case "metrics": {
|
|
2021
|
-
const records
|
|
2022
|
-
const groups = sortGroups(aggregateMetrics(records, this.metricsBy), this.metricsSort);
|
|
2133
|
+
const { records, groups, taskTimes, stats, jev } = this.metricsFigures(query);
|
|
2023
2134
|
this.metricsSelected = Math.min(this.metricsSelected, Math.max(0, groups.length - 1));
|
|
2024
|
-
|
|
2025
|
-
const jev = summarizeClassifier(this.data.classifierMetrics, records);
|
|
2026
|
-
return renderMetrics({ groups, taskTimes, records, stats: taskStats(this.data.tasks), by: this.metricsBy, sort: this.metricsSort, selected: this.metricsSelected, ...(query ? { query } : {}), panes: this.panes, ...(jev ? { classifier: jev } : {}) }, width, height, theme);
|
|
2135
|
+
return renderMetrics({ groups, taskTimes, records, stats, by: this.metricsBy, sort: this.metricsSort, selected: this.metricsSelected, ...(query ? { query } : {}), panes: this.panes, ...(jev ? { classifier: jev } : {}) }, width, height, theme);
|
|
2027
2136
|
}
|
|
2028
2137
|
}
|
|
2029
2138
|
}
|
|
@@ -2150,7 +2259,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2150
2259
|
const room = height - lines.length - 2;
|
|
2151
2260
|
if (room > 2) {
|
|
2152
2261
|
lines.push("", rule(width, "Conversation", theme));
|
|
2153
|
-
const talk = chat.length > 0 ?
|
|
2262
|
+
const talk = chat.length > 0 ? chatTail(chat, width, theme, room - 1).lines : [paint(theme, "dim", "Nothing said yet.")];
|
|
2154
2263
|
lines.push(...talk.slice(-(room - 1)));
|
|
2155
2264
|
}
|
|
2156
2265
|
return lines;
|
|
@@ -2175,9 +2284,11 @@ export class LobbyView implements Component, Focusable {
|
|
|
2175
2284
|
const query = this.query();
|
|
2176
2285
|
const entry = this.viewedEntry();
|
|
2177
2286
|
if (entry.view.kind !== "here") return this.otherSessionBody(entry, width, height, theme, now);
|
|
2287
|
+
const talk = this.conversationFor(entry);
|
|
2178
2288
|
return renderHome({
|
|
2179
2289
|
...(zen.task ? { task: zen.task, scene: (w: number, h: number, animated: boolean) => this.host.scene(w, h, animated) } : {}),
|
|
2180
|
-
chat:
|
|
2290
|
+
chat: talk.chat,
|
|
2291
|
+
...(talk.older ? { olderNote: OLDER_NOTE } : {}),
|
|
2181
2292
|
...(feed.reply ? { liveReply: feed.reply } : {}),
|
|
2182
2293
|
activity: feed.activity,
|
|
2183
2294
|
thoughts: feed.thoughts,
|
|
@@ -2185,6 +2296,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2185
2296
|
others,
|
|
2186
2297
|
pending: this.data.plans.filter((plan) => plan.status === "pending").length,
|
|
2187
2298
|
offsets: this.homeOffsets,
|
|
2299
|
+
anchors: this.marks,
|
|
2188
2300
|
...(this.mode === "browse" ? { focus: this.homeFocus } : {}),
|
|
2189
2301
|
panes: this.panes,
|
|
2190
2302
|
tick: this.tick,
|
|
@@ -2207,6 +2319,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2207
2319
|
*/
|
|
2208
2320
|
private otherSessionBody(entry: SessionEntry, width: number, height: number, theme: LobbyTheme, now: number): string[] {
|
|
2209
2321
|
const session = this.viewedSession();
|
|
2322
|
+
const talk = this.conversationFor(entry);
|
|
2210
2323
|
const task = entry.task;
|
|
2211
2324
|
const query = this.query();
|
|
2212
2325
|
const common = {
|
|
@@ -2214,6 +2327,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2214
2327
|
others: 0,
|
|
2215
2328
|
pending: 0,
|
|
2216
2329
|
offsets: this.homeOffsets,
|
|
2330
|
+
anchors: this.marks,
|
|
2217
2331
|
...(this.mode === "browse" ? { focus: this.homeFocus } : {}),
|
|
2218
2332
|
panes: this.panes,
|
|
2219
2333
|
tick: this.tick,
|
|
@@ -2232,7 +2346,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
2232
2346
|
const emptyNote = session.status === "starting" ? `starting ${session.name}…` : !session.alive ? `${session.name} has ended.` : `${session.name} has not said anything yet.`;
|
|
2233
2347
|
return renderHome({
|
|
2234
2348
|
...common,
|
|
2235
|
-
chat:
|
|
2349
|
+
chat: talk.chat,
|
|
2350
|
+
...(talk.older ? { olderNote: OLDER_NOTE } : {}),
|
|
2236
2351
|
...(session.feed.reply ? { liveReply: session.feed.reply } : {}),
|
|
2237
2352
|
activity: session.feed.activity,
|
|
2238
2353
|
thoughts: session.feed.thoughts,
|
|
@@ -2243,7 +2358,8 @@ export class LobbyView implements Component, Focusable {
|
|
|
2243
2358
|
const running = entry.where === "other terminal";
|
|
2244
2359
|
return renderHome({
|
|
2245
2360
|
...common,
|
|
2246
|
-
chat:
|
|
2361
|
+
chat: talk.chat,
|
|
2362
|
+
...(talk.older ? { olderNote: OLDER_NOTE } : {}),
|
|
2247
2363
|
activity: [],
|
|
2248
2364
|
thoughts: [],
|
|
2249
2365
|
busy: false,
|
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,
|