@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/pi/start-task.ts
CHANGED
|
@@ -32,9 +32,6 @@ function uniqueTaskId(root: string, configDir: string, request: string): string
|
|
|
32
32
|
return id;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/** In a kickoff after the oracle routed the request to the team (the lobby then shows the request once). */
|
|
36
|
-
export const ROUTED_LINE = "Routed: you sent this request to the team.";
|
|
37
|
-
|
|
38
35
|
/** The fast track's steps: straight to the agents the request needs, QA only for tests, then complete. */
|
|
39
36
|
function fastSteps(track: TaskTrack): string[] {
|
|
40
37
|
const building = track.roster.filter((member): member is Domain => member === "designer" || member === "backend");
|
|
@@ -74,14 +71,13 @@ function plannedSteps(task: Task): string[] {
|
|
|
74
71
|
].filter(Boolean);
|
|
75
72
|
}
|
|
76
73
|
|
|
77
|
-
export function kickoff(task: Task, options: { fastTrack?: boolean
|
|
74
|
+
export function kickoff(task: Task, options: { fastTrack?: boolean } = {}): string {
|
|
78
75
|
const track = task.track;
|
|
79
76
|
const head = [
|
|
80
77
|
`A bot-lobby task is active: ${task.id}`,
|
|
81
78
|
`Title: ${task.title}`,
|
|
82
79
|
`Request: ${taskRequest(task)}`,
|
|
83
80
|
`State: ${task.state}`,
|
|
84
|
-
...(options.routed ? [ROUTED_LINE] : []),
|
|
85
81
|
...(track ? [trackSummary(track)] : []),
|
|
86
82
|
...(task.git ? [gitLine(task.git)] : []),
|
|
87
83
|
"",
|
|
@@ -135,11 +131,9 @@ export interface StartOptions {
|
|
|
135
131
|
title?: string;
|
|
136
132
|
/** The user's `--fast` or `--full`: the path the task takes, whatever its request reads as. */
|
|
137
133
|
track?: TrackPath;
|
|
138
|
-
/** The classifier already read the request
|
|
134
|
+
/** The classifier already read the request: `triage` is its read, or absent when it was off. */
|
|
139
135
|
triaged?: boolean;
|
|
140
136
|
triage?: TaskTriage;
|
|
141
|
-
/** The oracle sent this request to the team after the classifier read it as a quick fix. */
|
|
142
|
-
routed?: boolean;
|
|
143
137
|
/** `--branch`, `--worktree` or `--no-branch`: whether this task gets a git branch or worktree of its own; `workflow.gitIsolation` when absent. */
|
|
144
138
|
isolation?: GitIsolation;
|
|
145
139
|
}
|
|
@@ -182,7 +176,7 @@ export async function startTask(pi: ExtensionAPI, ctx: ExtensionContext, configD
|
|
|
182
176
|
// The oracle takes the task on with a clean context: nothing said before the kickoff is sent to its model.
|
|
183
177
|
if (freshContextOn()) markContext(pi, { kind: "start", taskId: task.id, at: Date.now() });
|
|
184
178
|
// A kickoff while pi is still busy (another turn) queues behind it instead of throwing.
|
|
185
|
-
pi.sendUserMessage(kickoff(task, { fastTrack: config.workflow.fastTrack
|
|
179
|
+
pi.sendUserMessage(kickoff(task, { fastTrack: config.workflow.fastTrack }), ctx.isIdle() ? undefined : { deliverAs: "followUp" });
|
|
186
180
|
return task;
|
|
187
181
|
}
|
|
188
182
|
|
package/src/pi/switch.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* that is still under way (a resumed session picks its task back up).
|
|
6
6
|
* Ctrl+Shift+M and `/bot-lobby on|off` switch it; starting a task, opening the
|
|
7
7
|
* web lobby or answering a task's proposal turns it on. Off stops the web
|
|
8
|
-
* lobby too, and anything it runs (a
|
|
8
|
+
* lobby too, and anything it runs (a chat agent's reply, a review, a planning round)
|
|
9
9
|
* stops with it, so the user is asked first when something is running.
|
|
10
10
|
*/
|
|
11
11
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -53,11 +53,11 @@ export interface ScoutConfig extends HarnessChoice {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/** Subagent kinds with their own settings entry. */
|
|
56
|
-
export const SUBAGENT_KINDS = ["designer", "backend", "qa", "scout", "researcher", "
|
|
56
|
+
export const SUBAGENT_KINDS = ["designer", "backend", "qa", "scout", "researcher", "planner"] as const;
|
|
57
57
|
export type SubagentKind = (typeof SUBAGENT_KINDS)[number];
|
|
58
58
|
|
|
59
|
-
/** Lobby agents that run outside the workflow
|
|
60
|
-
export type LobbyAgentKind = "
|
|
59
|
+
/** Lobby agents that run outside the workflow and have their own settings entry: the task planner. */
|
|
60
|
+
export type LobbyAgentKind = "planner";
|
|
61
61
|
|
|
62
62
|
/** Settings kinds a workflow run (domain + role) can draw from. */
|
|
63
63
|
export type WorkflowProfileKind = Domain | "scout" | "researcher";
|
|
@@ -94,11 +94,9 @@ export interface WorkflowConfig {
|
|
|
94
94
|
fastTrack: boolean;
|
|
95
95
|
/** A delegation that names nothing concrete, or is long without done criteria, is sent back to the oracle once for a fuller brief (sending it again unchanged goes through). */
|
|
96
96
|
briefCheck: boolean;
|
|
97
|
-
/** A new request that one agent can do alone goes to the quick-fix agent once the oracle confirms; false makes every request a task. */
|
|
98
|
-
routeQuickFixes: boolean;
|
|
99
97
|
/** A new task gets a git branch or worktree of its own, named after it; `--branch` and `--worktree` decide for one task. */
|
|
100
98
|
gitIsolation: GitIsolation;
|
|
101
|
-
/** Scouts, workers,
|
|
99
|
+
/** Scouts, workers, lobby chat replies and the planner get the repository map: the layout and the files matching their step, with each file's purpose, names and unchanged scout notes. */
|
|
102
100
|
repoMap: boolean;
|
|
103
101
|
/** A scout asked the same thing again for the same task, on a tree that has not changed, reuses its findings instead of running. */
|
|
104
102
|
reuseScouts: boolean;
|
|
@@ -202,7 +200,7 @@ export interface LobbyConfig {
|
|
|
202
200
|
}
|
|
203
201
|
|
|
204
202
|
/** Decisions the classifier can make, each switched on or off on its own. */
|
|
205
|
-
export const CLASSIFIER_FEATURES = ["seats", "answers", "files", "triage", "effort", "review", "knowledge", "scripts", "qa"] as const;
|
|
203
|
+
export const CLASSIFIER_FEATURES = ["seats", "answers", "files", "triage", "effort", "review", "knowledge", "scripts", "qa", "lobby"] as const;
|
|
206
204
|
export type ClassifierFeature = (typeof CLASSIFIER_FEATURES)[number];
|
|
207
205
|
|
|
208
206
|
/** Hosts that serve Jev behind the same System One API; `auto` takes OpenCode's free Jev when pi holds an OpenCode key, else TypeSafe. */
|
|
@@ -229,10 +227,8 @@ export interface ClassifierThresholds {
|
|
|
229
227
|
simpleAt: number;
|
|
230
228
|
/** A step scored trivial at this confidence runs on the cheaper model. */
|
|
231
229
|
trivialAt: number;
|
|
232
|
-
/** A
|
|
233
|
-
|
|
234
|
-
/** A quick fix scored large at this confidence is held instead of started. */
|
|
235
|
-
quickFixLargeAt: number;
|
|
230
|
+
/** A lobby message goes to the agent the classifier picks for it at least this likely; below it, the Master answers. */
|
|
231
|
+
lobbyAgentAt: number;
|
|
236
232
|
}
|
|
237
233
|
|
|
238
234
|
/** The classifier: a fast model for obvious decisions, so large models spend fewer tokens on them. */
|
|
@@ -266,8 +262,6 @@ export interface BotLobbyConfig {
|
|
|
266
262
|
agents: Record<"designer" | "backend" | "qa", AgentModelConfig>;
|
|
267
263
|
scout: ScoutConfig;
|
|
268
264
|
researcher: AgentModelConfig;
|
|
269
|
-
/** Direct quick fixes from the lobby: no scouting, planning or review. */
|
|
270
|
-
quickFix: AgentModelConfig;
|
|
271
265
|
/** The task planner that grills the user until a plan is clear. */
|
|
272
266
|
planner: AgentModelConfig;
|
|
273
267
|
workflow: WorkflowConfig;
|
|
@@ -289,7 +283,6 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
|
|
|
289
283
|
},
|
|
290
284
|
scout: { model: INHERIT_MODEL, maxToolCalls: 30 },
|
|
291
285
|
researcher: { model: INHERIT_MODEL, thinking: "low", instructions: "" },
|
|
292
|
-
quickFix: { model: INHERIT_MODEL, thinking: "low", instructions: "" },
|
|
293
286
|
planner: { model: INHERIT_MODEL, thinking: "high", instructions: "" },
|
|
294
287
|
workflow: {
|
|
295
288
|
maxReviewIterations: 2,
|
|
@@ -304,7 +297,6 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
|
|
|
304
297
|
freshContext: true,
|
|
305
298
|
fastTrack: true,
|
|
306
299
|
briefCheck: true,
|
|
307
|
-
routeQuickFixes: true,
|
|
308
300
|
gitIsolation: "off",
|
|
309
301
|
repoMap: true,
|
|
310
302
|
reuseScouts: true,
|
|
@@ -338,7 +330,7 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
|
|
|
338
330
|
model: "",
|
|
339
331
|
baseUrl: "",
|
|
340
332
|
timeoutMs: 4000,
|
|
341
|
-
features: { seats: true, answers: true, files: true, triage: true, effort: true, review: true, knowledge: true, scripts: true, qa: true },
|
|
333
|
+
features: { seats: true, answers: true, files: true, triage: true, effort: true, review: true, knowledge: true, scripts: true, qa: true, lobby: true },
|
|
342
334
|
thresholds: {
|
|
343
335
|
seatAt: 0.35,
|
|
344
336
|
reseatReadyAt: 0.6,
|
|
@@ -349,8 +341,7 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
|
|
|
349
341
|
scriptRelevantAt: 0.5,
|
|
350
342
|
simpleAt: 0.7,
|
|
351
343
|
trivialAt: 0.8,
|
|
352
|
-
|
|
353
|
-
quickFixLargeAt: 0.8,
|
|
344
|
+
lobbyAgentAt: 0.5,
|
|
354
345
|
},
|
|
355
346
|
fileHints: { topK: 8, maxCandidates: 480, budgetMs: 1500 },
|
|
356
347
|
effort: { cheapModel: INHERIT_MODEL },
|
|
@@ -536,7 +527,6 @@ export function resolveConfig(partial: unknown): BotLobbyConfig {
|
|
|
536
527
|
workflow.freshContext = workflow.freshContext !== false;
|
|
537
528
|
workflow.fastTrack = workflow.fastTrack !== false;
|
|
538
529
|
workflow.briefCheck = workflow.briefCheck !== false;
|
|
539
|
-
workflow.routeQuickFixes = workflow.routeQuickFixes !== false;
|
|
540
530
|
workflow.repoMap = workflow.repoMap !== false;
|
|
541
531
|
workflow.reuseScouts = workflow.reuseScouts !== false;
|
|
542
532
|
workflow.parallelMode = isParallelMode(workflow.parallelMode) ? workflow.parallelMode : DEFAULT_CONFIG.workflow.parallelMode;
|
|
@@ -546,7 +536,8 @@ export function resolveConfig(partial: unknown): BotLobbyConfig {
|
|
|
546
536
|
workflow.expertModel = typeof workflow.expertModel === "string" ? workflow.expertModel.trim() : "";
|
|
547
537
|
workflow.gitIsolation = isGitIsolation(workflow.gitIsolation) ? workflow.gitIsolation : DEFAULT_CONFIG.workflow.gitIsolation;
|
|
548
538
|
// Agent time limits are gone: a config file that still names them is cleaned on its next save.
|
|
549
|
-
|
|
539
|
+
// So are quick fixes: the lobby chat does that work now.
|
|
540
|
+
for (const key of ["agentTimeoutMs", "wrapUpAt", "taskBudgetMinutes", "routeQuickFixes"]) delete (workflow as unknown as Record<string, unknown>)[key];
|
|
550
541
|
const knowledge = { ...DEFAULT_CONFIG.knowledge, ...(src.knowledge as Partial<KnowledgeConfig> | undefined) };
|
|
551
542
|
const srcAgents = (src.agents ?? {}) as Partial<BotLobbyConfig["agents"]>;
|
|
552
543
|
return {
|
|
@@ -558,7 +549,6 @@ export function resolveConfig(partial: unknown): BotLobbyConfig {
|
|
|
558
549
|
},
|
|
559
550
|
scout: normalizeScout(src.scout as Partial<ScoutConfig> | undefined),
|
|
560
551
|
researcher: normalizeAgent(DEFAULT_CONFIG.researcher, src.researcher as Partial<AgentModelConfig> | undefined),
|
|
561
|
-
quickFix: normalizeAgent(DEFAULT_CONFIG.quickFix, src.quickFix as Partial<AgentModelConfig> | undefined),
|
|
562
552
|
planner: normalizeAgent(DEFAULT_CONFIG.planner, src.planner as Partial<AgentModelConfig> | undefined, false),
|
|
563
553
|
workflow,
|
|
564
554
|
lint: normalizeLint(src.lint),
|
|
@@ -636,9 +626,9 @@ export function agentProfile(config: BotLobbyConfig, domain: Domain, role: Role)
|
|
|
636
626
|
return { kind, model: modelOf(entry.model), thinking: entry.thinking, instructions, ...grantFields(entry), ...compactField(entry), ...harnessField(config, entry) };
|
|
637
627
|
}
|
|
638
628
|
|
|
639
|
-
/** The settings entry of a lobby agent (
|
|
640
|
-
export function lobbyAgentConfig(config: BotLobbyConfig,
|
|
641
|
-
return
|
|
629
|
+
/** The settings entry of a lobby agent (the planner). */
|
|
630
|
+
export function lobbyAgentConfig(config: BotLobbyConfig, _kind: LobbyAgentKind): AgentModelConfig {
|
|
631
|
+
return config.planner;
|
|
642
632
|
}
|
|
643
633
|
|
|
644
634
|
/** Profile for a lobby agent run, from settings alone; `model` is undefined while unset. */
|
package/src/state/changes.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Who changed what: a project-wide ledger of the files each
|
|
3
|
-
* task worker edited, read from their own `edit`/`write` tool calls
|
|
4
|
-
* from their reports). The QA gate and the Master see every changed
|
|
5
|
-
* the working tree against it, so a
|
|
2
|
+
* Who changed what: a project-wide ledger of the files each lobby chat reply
|
|
3
|
+
* and each task worker edited, read from their own `edit`/`write` tool calls
|
|
4
|
+
* (never from their reports). The QA gate and the Master see every changed
|
|
5
|
+
* file of the working tree against it, so a change the user asked for in the
|
|
6
|
+
* lobby chat, a planned
|
|
6
7
|
* step, work that was there before the task and an edit no agent made each
|
|
7
8
|
* read as what they are, instead of all looking like unplanned changes.
|
|
8
9
|
* Append-only JSON lines beside the metrics; best-effort like them.
|
|
@@ -13,11 +14,12 @@ import type { Domain } from "../schemas/agent.ts";
|
|
|
13
14
|
import type { Task } from "../schemas/task.ts";
|
|
14
15
|
import { dataRoot } from "./project.ts";
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
/** `chat`: an agent answering the user in the lobby chat (older ledgers call it `quickfix`). */
|
|
18
|
+
export type ChangeSource = "chat" | "worker";
|
|
17
19
|
|
|
18
20
|
export interface ChangeRecord {
|
|
19
21
|
source: ChangeSource;
|
|
20
|
-
/** The
|
|
22
|
+
/** The chat reply (`CHAT-3`) or the worker run that made the edits. */
|
|
21
23
|
id: string;
|
|
22
24
|
/** The task a worker ran for. */
|
|
23
25
|
taskId?: string;
|
|
@@ -106,7 +108,7 @@ export function appendChange(root: string, configDir: string, record: ChangeReco
|
|
|
106
108
|
function isRecord(value: unknown): value is ChangeRecord {
|
|
107
109
|
if (!value || typeof value !== "object") return false;
|
|
108
110
|
const record = value as Partial<ChangeRecord>;
|
|
109
|
-
return (record.source === "
|
|
111
|
+
return (record.source === "chat" || record.source === "worker" || (record.source as unknown) === "quickfix") && typeof record.id === "string" && Array.isArray(record.files) && typeof record.finishedAt === "string";
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
/** Every record in the ledger finished at or after `since` (ISO), oldest first. */
|
|
@@ -123,7 +125,7 @@ export function readChanges(root: string, configDir: string, since?: string): Ch
|
|
|
123
125
|
if (!line.trim()) continue;
|
|
124
126
|
try {
|
|
125
127
|
const record: unknown = JSON.parse(line);
|
|
126
|
-
if (isRecord(record) && Date.parse(record.finishedAt) >= from) records.push(record);
|
|
128
|
+
if (isRecord(record) && Date.parse(record.finishedAt) >= from) records.push((record.source as unknown) === "quickfix" ? { ...record, source: "chat" } : record);
|
|
127
129
|
} catch {
|
|
128
130
|
// A torn line (a crash mid-append) is skipped.
|
|
129
131
|
}
|
|
@@ -131,7 +133,7 @@ export function readChanges(root: string, configDir: string, since?: string): Ch
|
|
|
131
133
|
return records;
|
|
132
134
|
}
|
|
133
135
|
|
|
134
|
-
export type Provenance = "planned" | "
|
|
136
|
+
export type Provenance = "planned" | "chat" | "other-task" | "pre-existing" | "unattributed";
|
|
135
137
|
|
|
136
138
|
export interface FileProvenance {
|
|
137
139
|
/** Repository-relative, as git reports it. */
|
|
@@ -140,8 +142,8 @@ export interface FileProvenance {
|
|
|
140
142
|
kinds: Provenance[];
|
|
141
143
|
/** One clause per source: `planned: DEV — Add the login endpoint`. */
|
|
142
144
|
notes: string[];
|
|
143
|
-
/** The
|
|
144
|
-
|
|
145
|
+
/** The lobby chat replies that edited it. */
|
|
146
|
+
chats: string[];
|
|
145
147
|
/** This task's domains whose workers edited it. */
|
|
146
148
|
domains?: Domain[];
|
|
147
149
|
}
|
|
@@ -161,7 +163,7 @@ function clock(iso: string): string {
|
|
|
161
163
|
/**
|
|
162
164
|
* Explain each changed file of the working tree (`changed`, relative to the
|
|
163
165
|
* repository at `top`) for `task`: edits by its own workers are planned; by a
|
|
164
|
-
*
|
|
166
|
+
* lobby chat reply, the user's direct request; by another task's workers, that
|
|
165
167
|
* task's; changed before its agents started, pre-existing; anything else, no
|
|
166
168
|
* agent recorded it. `records` should reach back to the task's start.
|
|
167
169
|
*/
|
|
@@ -181,16 +183,16 @@ export function explainChanges(task: Pick<Task, "id" | "baseline">, changed: rea
|
|
|
181
183
|
return changed.map((path) => {
|
|
182
184
|
const kinds: Provenance[] = [];
|
|
183
185
|
const notes: string[] = [];
|
|
184
|
-
const
|
|
186
|
+
const chats: string[] = [];
|
|
185
187
|
const domains: Domain[] = [];
|
|
186
188
|
const add = (kind: Provenance, note: string) => {
|
|
187
189
|
if (!kinds.includes(kind)) kinds.push(kind);
|
|
188
190
|
if (!notes.includes(note)) notes.push(note);
|
|
189
191
|
};
|
|
190
192
|
for (const record of byFile.get(path) ?? []) {
|
|
191
|
-
if (record.source === "
|
|
192
|
-
add("
|
|
193
|
-
if (!
|
|
193
|
+
if (record.source === "chat") {
|
|
194
|
+
add("chat", `lobby chat ${record.id}${clock(record.finishedAt)}, asked by the user: "${clip(record.what)}"`);
|
|
195
|
+
if (!chats.includes(record.id)) chats.push(record.id);
|
|
194
196
|
} else if (record.taskId === task.id) {
|
|
195
197
|
add("planned", `planned: ${DOMAIN_LABELS[record.domain ?? ""] ?? record.domain ?? "worker"} — ${clip(record.what)}`);
|
|
196
198
|
if (record.domain && !domains.includes(record.domain)) domains.push(record.domain);
|
|
@@ -199,7 +201,7 @@ export function explainChanges(task: Pick<Task, "id" | "baseline">, changed: rea
|
|
|
199
201
|
}
|
|
200
202
|
if (before.has(path)) add("pre-existing", "pre-existing: it already had uncommitted changes before this task's agents started");
|
|
201
203
|
if (kinds.length === 0) add("unattributed", "unattributed: no bot-lobby agent recorded editing it");
|
|
202
|
-
return { path, kinds, notes,
|
|
204
|
+
return { path, kinds, notes, chats, ...(domains.length ? { domains } : {}) };
|
|
203
205
|
});
|
|
204
206
|
}
|
|
205
207
|
|
|
@@ -208,7 +210,7 @@ export const MAX_PROVENANCE_LINES = 60;
|
|
|
208
210
|
|
|
209
211
|
/** The provenance as the reviewer reads it: one line per changed file, the unexplained ones first, the rest counted past `max`. */
|
|
210
212
|
export function provenanceLines(files: readonly FileProvenance[], max = MAX_PROVENANCE_LINES): string {
|
|
211
|
-
const order = (file: FileProvenance) => (file.kinds.includes("unattributed") ? 0 : file.kinds.includes("
|
|
213
|
+
const order = (file: FileProvenance) => (file.kinds.includes("unattributed") ? 0 : file.kinds.includes("chat") ? 1 : 2);
|
|
212
214
|
const sorted = [...files].sort((a, b) => order(a) - order(b));
|
|
213
215
|
const lines = sorted.slice(0, max).map((file) => `- ${file.path} — ${file.notes.join("; ")}`);
|
|
214
216
|
const rest = sorted.slice(max);
|
|
@@ -216,16 +218,16 @@ export function provenanceLines(files: readonly FileProvenance[], max = MAX_PROV
|
|
|
216
218
|
return lines.join("\n");
|
|
217
219
|
}
|
|
218
220
|
|
|
219
|
-
/** A one-line count for the Master: `4 planned · 1
|
|
221
|
+
/** A one-line count for the Master: `4 planned · 1 by lobby chat (CHAT-2) · 1 unattributed (src/x.ts)`. */
|
|
220
222
|
export function provenanceSummary(files: readonly FileProvenance[]): string {
|
|
221
223
|
const count = (kind: Provenance) => files.filter((file) => file.kinds.includes(kind));
|
|
222
224
|
const parts: string[] = [];
|
|
223
225
|
const planned = count("planned").length;
|
|
224
226
|
if (planned > 0) parts.push(`${planned} planned`);
|
|
225
|
-
const
|
|
226
|
-
if (
|
|
227
|
-
const ids = [...new Set(
|
|
228
|
-
parts.push(`${
|
|
227
|
+
const chat = count("chat");
|
|
228
|
+
if (chat.length > 0) {
|
|
229
|
+
const ids = [...new Set(chat.flatMap((file) => file.chats))];
|
|
230
|
+
parts.push(`${chat.length} by lobby chat (${ids.join(", ")})`);
|
|
229
231
|
}
|
|
230
232
|
const other = count("other-task").length;
|
|
231
233
|
if (other > 0) parts.push(`${other} by another task`);
|
package/src/state/metrics.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Model performance records: one line per finished run of any agent — the
|
|
3
|
-
* Master's own turns, scouts, workers, the QA gate, researchers,
|
|
3
|
+
* Master's own turns, scouts, workers, the QA gate, researchers, lobby chat agents,
|
|
4
4
|
* planner turns and planning panel seats — so the lobby can show how long each model takes at each
|
|
5
5
|
* thinking level, how often it succeeds and what it costs. Append-only JSON
|
|
6
6
|
* lines per project; reads keep the newest `MAX_READ` records.
|
|
@@ -11,7 +11,8 @@ import type { AgentRun } from "../schemas/findings.ts";
|
|
|
11
11
|
import type { RunLogEntry, Task } from "../schemas/task.ts";
|
|
12
12
|
import { dataRoot } from "./project.ts";
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
/** `chat`: an agent answering in the lobby chat; `quickfix` is kept so older logs still read. */
|
|
15
|
+
export const METRIC_KINDS = ["master", "scout", "worker", "reviewer", "researcher", "chat", "quickfix", "planner", "panel", "classifier"] as const;
|
|
15
16
|
export type MetricKind = (typeof METRIC_KINDS)[number];
|
|
16
17
|
|
|
17
18
|
export type MetricStatus = "success" | "failed" | "cancelled" | "timeout";
|
|
@@ -19,7 +20,7 @@ export type MetricStatus = "success" | "failed" | "cancelled" | "timeout";
|
|
|
19
20
|
export interface MetricRecord {
|
|
20
21
|
id: string;
|
|
21
22
|
kind: MetricKind;
|
|
22
|
-
/** Display name of the agent: MASTER, DEV, DESIGN, QA, RESEARCH,
|
|
23
|
+
/** Display name of the agent: MASTER, DEV, DESIGN, QA, RESEARCH, SCOUT, ORACLE (planning). */
|
|
23
24
|
agent: string;
|
|
24
25
|
model?: string;
|
|
25
26
|
thinking?: string;
|
|
@@ -37,7 +38,7 @@ export interface MetricRecord {
|
|
|
37
38
|
purpose?: string;
|
|
38
39
|
/** A run the classifier routed down: the configured profile it came from. */
|
|
39
40
|
routedFrom?: string;
|
|
40
|
-
/** Classifier calls: what the decision spared (seat runs skipped, questions answered
|
|
41
|
+
/** Classifier calls: what the decision spared (seat runs skipped, questions answered). */
|
|
41
42
|
saved?: number;
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -53,7 +54,6 @@ export interface ClassifierSummary {
|
|
|
53
54
|
byPurpose: Record<string, number>;
|
|
54
55
|
seatRunsSkipped: number;
|
|
55
56
|
questionsAnswered: number;
|
|
56
|
-
quickFixesHeld: number;
|
|
57
57
|
/** Agent runs the classifier routed down, and how many of them succeeded. */
|
|
58
58
|
routed: number;
|
|
59
59
|
routedOk: number;
|
|
@@ -72,14 +72,12 @@ export function summarizeClassifier(calls: readonly MetricRecord[], runs: readon
|
|
|
72
72
|
const byPurpose: Record<string, number> = {};
|
|
73
73
|
let seatRunsSkipped = 0;
|
|
74
74
|
let questionsAnswered = 0;
|
|
75
|
-
let quickFixesHeld = 0;
|
|
76
75
|
for (const record of calls) {
|
|
77
76
|
const purpose = record.purpose ?? "other";
|
|
78
77
|
byPurpose[purpose] = (byPurpose[purpose] ?? 0) + 1;
|
|
79
78
|
const saved = record.saved ?? 0;
|
|
80
79
|
if (purpose === "seats") seatRunsSkipped += saved;
|
|
81
80
|
else if (purpose === "answers") questionsAnswered += saved;
|
|
82
|
-
else if (purpose === "triage") quickFixesHeld += saved;
|
|
83
81
|
}
|
|
84
82
|
return {
|
|
85
83
|
calls: calls.length,
|
|
@@ -90,7 +88,6 @@ export function summarizeClassifier(calls: readonly MetricRecord[], runs: readon
|
|
|
90
88
|
byPurpose,
|
|
91
89
|
seatRunsSkipped,
|
|
92
90
|
questionsAnswered,
|
|
93
|
-
quickFixesHeld,
|
|
94
91
|
routed: routed.length,
|
|
95
92
|
routedOk: routed.filter((record) => record.status === "success").length,
|
|
96
93
|
};
|
package/src/state/recovery.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* background sessions it drives, and whether each was in the middle of a turn.
|
|
7
7
|
* A window that quits on purpose removes it; one that stops any other way
|
|
8
8
|
* leaves it behind, and the next window to start in the project takes it over
|
|
9
|
-
* (`claimStoppedWindows`). Each session's planning panel
|
|
10
|
-
*
|
|
9
|
+
* (`claimStoppedWindows`). Each session's planning panel is
|
|
10
|
+
* kept beside it, by session, so it carries on as well.
|
|
11
11
|
*/
|
|
12
12
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
@@ -39,7 +39,7 @@ export interface WindowRecord {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/** What each kept session state holds. */
|
|
42
|
-
export type SessionStateKind = "planner"
|
|
42
|
+
export type SessionStateKind = "planner";
|
|
43
43
|
|
|
44
44
|
/** This process's token: written into its record, so a later process with the same pid is not mistaken for it. */
|
|
45
45
|
const TOKEN = randomBytes(8).toString("hex");
|
package/src/webui/api/index.ts
CHANGED
|
@@ -14,7 +14,6 @@ import { statusGet } from "./status.ts";
|
|
|
14
14
|
import { lobbyAbort, lobbyHistory, lobbySend, lobbySnapshot } from "./lobby.ts";
|
|
15
15
|
import { promptsAnswer, promptsDismiss, promptsList } from "./prompts.ts";
|
|
16
16
|
import { plannerAnswer, plannerCommentLine, plannerGet, plannerNew, plannerRetry, plannerSend, plannerEditMessage, plannerToggleSeat, plannerSave, plannerPrevious, plannerPreviousArchive, plannerPreviousDelete, plannerPreviousGet, plannerPreviousOpen } from "./planner.ts";
|
|
17
|
-
import { quickfixCancel, quickfixList, quickfixMovedToTask, quickfixRunAnyway, quickfixSubmit } from "./quickfix.ts";
|
|
18
17
|
import { tasksArchive, tasksArchived, tasksAuto, tasksComment, tasksComments, tasksDelete, tasksEditComment, tasksGet, tasksList, tasksMessage, tasksRestore, tasksOpen, tasksResume, tasksDeliveryReview, tasksDeliveryDefer, tasksDeliver, tasksAccept, tasksSkipStep, tasksForceClose, tasksResolveApproval } from "./tasks.ts";
|
|
19
18
|
import { knowledgeAdd, knowledgeAsk, knowledgeComment, knowledgeEdit, knowledgeFiles, knowledgeOpen, knowledgeRemove, knowledgeReplaceFile, knowledgeUnnote } from "./knowledge.ts";
|
|
20
19
|
import { plansDiscard, plansGet, plansStart } from "./plans.ts";
|
|
@@ -26,6 +25,7 @@ import { terminalClose, terminalInput, terminalList, terminalOpen, terminalResiz
|
|
|
26
25
|
import { MAX_INPUT_CHARS } from "../terminal.ts";
|
|
27
26
|
import { themesChoose, themesGet, themesRemove, themesRename, themesSave } from "./themes.ts";
|
|
28
27
|
import { noticeText, pushNotice } from "../notices.ts";
|
|
28
|
+
import { codeChanges, codeRead, codeSave } from "../code.ts";
|
|
29
29
|
|
|
30
30
|
/** What a handler reads besides the request body. */
|
|
31
31
|
export interface ApiContext {
|
|
@@ -296,23 +296,6 @@ function buildRoutes(): Record<string, Route> {
|
|
|
296
296
|
routes["planner.previousArchive"] = { schema: Type.Object({ id: PlanId, archived: Type.Boolean() }, { additionalProperties: false }), run: (body, ctx) => plannerPreviousArchive(body as { id: string; archived: boolean }, ctx) };
|
|
297
297
|
routes["planner.previousDelete"] = { schema: Type.Object({ id: PlanId }, { additionalProperties: false }), run: (body, ctx) => plannerPreviousDelete(body as { id: string }, ctx) };
|
|
298
298
|
routes["planner.previousOpen"] = { schema: Type.Object({ id: PlanId }, { additionalProperties: false }), run: (body, ctx) => plannerPreviousOpen(body as { id: string }, ctx) };
|
|
299
|
-
routes["quickfix.list"] = { schema: Empty, run: (_body, ctx) => quickfixList(ctx) };
|
|
300
|
-
routes["quickfix.submit"] = {
|
|
301
|
-
schema: Type.Object({ text: NoticeText, attachments: Attachments }, { additionalProperties: false }),
|
|
302
|
-
run: (body, ctx) => quickfixSubmit(body as { text: string; attachments?: string[] }, ctx),
|
|
303
|
-
};
|
|
304
|
-
routes["quickfix.cancel"] = {
|
|
305
|
-
schema: Type.Object({ id: TaskId }, { additionalProperties: false }),
|
|
306
|
-
run: (body, ctx) => quickfixCancel(body as { id: string }, ctx),
|
|
307
|
-
};
|
|
308
|
-
routes["quickfix.runAnyway"] = {
|
|
309
|
-
schema: Type.Object({ id: TaskId }, { additionalProperties: false }),
|
|
310
|
-
run: (body, ctx) => quickfixRunAnyway(body as { id: string }, ctx),
|
|
311
|
-
};
|
|
312
|
-
routes["quickfix.movedToTask"] = {
|
|
313
|
-
schema: Type.Object({ id: TaskId }, { additionalProperties: false }),
|
|
314
|
-
run: (body, ctx) => quickfixMovedToTask(body as { id: string }, ctx),
|
|
315
|
-
};
|
|
316
299
|
routes["knowledge.files"] = { schema: Empty, run: (_body, ctx) => knowledgeFiles(ctx) };
|
|
317
300
|
routes["knowledge.ask"] = {
|
|
318
301
|
schema: Type.Object({ text: NoticeText, attachments: Attachments }, { additionalProperties: false }),
|
|
@@ -378,6 +361,17 @@ function buildRoutes(): Record<string, Route> {
|
|
|
378
361
|
schema: Type.Object({ text: Type.String({ maxLength: 20_000 }) }, { additionalProperties: false }),
|
|
379
362
|
run: (body, ctx) => issuesCreate(body as { text: string }, ctx),
|
|
380
363
|
};
|
|
364
|
+
const codeRoot = (ctx: ApiContext) => ctx.service.projectRoot?.() ?? process.cwd();
|
|
365
|
+
const CodePath = Type.String({ minLength: 1, maxLength: 4096 });
|
|
366
|
+
routes["code.changes"] = { schema: Empty, run: (_body, ctx) => codeChanges(codeRoot(ctx)) };
|
|
367
|
+
routes["code.read"] = {
|
|
368
|
+
schema: Type.Object({ path: CodePath }, { additionalProperties: false }),
|
|
369
|
+
run: (body, ctx) => codeRead(codeRoot(ctx), (body as { path: string }).path),
|
|
370
|
+
};
|
|
371
|
+
routes["code.save"] = {
|
|
372
|
+
schema: Type.Object({ path: CodePath, contents: Type.String({ maxLength: 900_000 }), mtimeMs: Type.Optional(Type.Number()), force: Type.Optional(Type.Boolean()) }, { additionalProperties: false }),
|
|
373
|
+
run: (body, ctx) => codeSave(codeRoot(ctx), body as { path: string; contents: string; mtimeMs?: number; force?: boolean }),
|
|
374
|
+
};
|
|
381
375
|
routes["settings.get"] = { schema: Empty, run: (_body, ctx) => settingsGet(ctx) };
|
|
382
376
|
routes["settings.set"] = {
|
|
383
377
|
// The patch's shape is the normaliser's job (`resolveConfig`); only a plain object is required here.
|
package/src/webui/api/lobby.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The Lobby tab over HTTP: one snapshot (task header, runs, newest chat,
|
|
3
3
|
* streaming reply, activity, thoughts, older-chat flag), older chat history,
|
|
4
|
-
* sending
|
|
5
|
-
* feed module, so the page sees
|
|
4
|
+
* sending to the chat (the oracle, an agent, /plan or /task), and stopping
|
|
5
|
+
* whoever is answering. Bounds come from the feed module, so the page sees
|
|
6
|
+
* exactly what the terminal keeps.
|
|
6
7
|
*/
|
|
7
8
|
import { MAX_ACTIVITY, MAX_CHAT, MAX_THOUGHTS } from "../../lobby/feed.ts";
|
|
8
9
|
import type { Task } from "../../schemas/task.ts";
|
|
@@ -52,7 +53,8 @@ export function lobbySnapshot(ctx: ApiContext): LobbySnapshot {
|
|
|
52
53
|
return {
|
|
53
54
|
...(taskOf(ctx) ? { task: taskOf(ctx)! } : {}),
|
|
54
55
|
runs: [...service.zen().runs],
|
|
55
|
-
chat: feed.chat.slice(-MAX_CHAT).map((entry) => ({ id: entry.id, at: entry.at, role: entry.role, text: entry.text })),
|
|
56
|
+
chat: feed.chat.slice(-MAX_CHAT).map((entry) => ({ id: entry.id, at: entry.at, role: entry.role, text: entry.text, ...(entry.agent ? { agent: entry.agent } : {}), ...(entry.to ? { to: entry.to } : {}) })),
|
|
57
|
+
...(service.answering?.() ? { answering: service.answering!()! } : {}),
|
|
56
58
|
...(reply ? { reply } : {}),
|
|
57
59
|
activity: feed.activity.slice(-MAX_ACTIVITY).map((entry) => ({
|
|
58
60
|
id: entry.id,
|
|
@@ -82,10 +84,11 @@ export function lobbyHistory(body: { before?: number }, ctx: ApiContext): { entr
|
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
/** Text for the oracle; a notice when it was not simply sent. */
|
|
85
|
-
export function lobbySend(body: { text: string; attachments?: string[] }, ctx: ApiContext): { notice?: string } {
|
|
87
|
+
export function lobbySend(body: { text: string; attachments?: string[] }, ctx: ApiContext): { notice?: string; open?: "plan" } {
|
|
86
88
|
if (!body.text.trim() && !body.attachments?.length) return { notice: "type something first" };
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
+
const sent = ctx.service.toOracle(withAttachments(body.text, body.attachments, ctx.service.zen().task?.id, ctx.service.projectRoot?.()));
|
|
90
|
+
if (!sent) return {};
|
|
91
|
+
return typeof sent === "string" ? { notice: sent } : sent;
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
/** Stop the oracle's turn. */
|