@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.
Files changed (118) hide show
  1. package/README.md +17 -11
  2. package/THIRD_PARTY_NOTICES.md +17 -3
  3. package/package.json +6 -1
  4. package/prompts/lobby-chat.md +39 -0
  5. package/prompts/master.md +7 -7
  6. package/prompts/reviewer.md +6 -6
  7. package/src/classifier/instance.ts +1 -1
  8. package/src/classifier/lobby.ts +37 -0
  9. package/src/classifier/triage.ts +2 -24
  10. package/src/execution/delegation.ts +2 -2
  11. package/src/index.ts +0 -3
  12. package/src/lobby/chat-agents.ts +293 -0
  13. package/src/lobby/feed.ts +73 -17
  14. package/src/lobby/host.ts +8 -4
  15. package/src/lobby/keys.ts +1 -0
  16. package/src/lobby/planner.ts +5 -5
  17. package/src/lobby/pr-review.ts +2 -2
  18. package/src/lobby/profile.ts +16 -0
  19. package/src/lobby/prompts.ts +56 -4
  20. package/src/lobby/recovery.ts +10 -42
  21. package/src/lobby/runtime.ts +27 -45
  22. package/src/lobby/service.ts +142 -13
  23. package/src/lobby/session-files.ts +4 -1
  24. package/src/lobby/topics.ts +0 -1
  25. package/src/master/master.ts +1 -1
  26. package/src/pi/commands.ts +6 -8
  27. package/src/pi/events.ts +11 -3
  28. package/src/pi/fresh-context.ts +1 -1
  29. package/src/pi/model-settings.ts +0 -2
  30. package/src/pi/model-support.ts +11 -2
  31. package/src/pi/owner.ts +35 -4
  32. package/src/pi/start-task.ts +3 -9
  33. package/src/pi/switch.ts +1 -1
  34. package/src/schemas/configuration.ts +14 -24
  35. package/src/state/changes.ts +25 -23
  36. package/src/state/metrics.ts +5 -8
  37. package/src/state/recovery.ts +3 -3
  38. package/src/webui/api/index.ts +12 -18
  39. package/src/webui/api/lobby.ts +9 -6
  40. package/src/webui/code.ts +202 -0
  41. package/src/webui/dev/fake-service.ts +28 -62
  42. package/src/webui/dev/fixtures.ts +1 -4
  43. package/src/webui/protocol.ts +15 -9
  44. package/src/webui/static.ts +5 -2
  45. package/src/workflow/track.ts +1 -52
  46. package/src/workflow/workflow.ts +2 -2
  47. package/webui/dist/assets/CodeSurface-BtzJOVyH.js +448 -0
  48. package/webui/dist/assets/{TerminalView-C8VC7KXy.js → TerminalView-Cz-KNWsq.js} +2 -2
  49. package/webui/dist/assets/astro-p42oEArs.js +1 -0
  50. package/webui/dist/assets/c-DaO8xguW.js +1 -0
  51. package/webui/dist/assets/ccount-DQxSH8e6.js +1 -0
  52. package/webui/dist/assets/csharp-dmMxLVLW.js +1 -0
  53. package/webui/dist/assets/css-C8fQr_0Q.js +1 -0
  54. package/webui/dist/assets/diff-woXpYk--.js +1 -0
  55. package/webui/dist/assets/dockerfile-IyjqRm3v.js +1 -0
  56. package/webui/dist/assets/dotenv-CuH5my32.js +1 -0
  57. package/webui/dist/assets/fish-BJitypiv.js +1 -0
  58. package/webui/dist/assets/go-rLFTqkRN.js +1 -0
  59. package/webui/dist/assets/graphql-AVu4aKQX.js +1 -0
  60. package/webui/dist/assets/html-wSbaAAK3.js +1 -0
  61. package/webui/dist/assets/index-D5fvY8f8.js +112 -0
  62. package/webui/dist/assets/index-DFvk8ADr.css +2 -0
  63. package/webui/dist/assets/ini-B5eOa1yu.js +1 -0
  64. package/webui/dist/assets/java-DdHNQ9hk.js +1 -0
  65. package/webui/dist/assets/javascript-BgS3c2Ky.js +1 -0
  66. package/webui/dist/assets/json-qhed-kSA.js +1 -0
  67. package/webui/dist/assets/json5-BR5RXkoi.js +1 -0
  68. package/webui/dist/assets/jsonc-CYpm1nAK.js +1 -0
  69. package/webui/dist/assets/jsonl-CmCQp5Yx.js +1 -0
  70. package/webui/dist/assets/jsx-gTgmArGx.js +1 -0
  71. package/webui/dist/assets/kotlin-DhhofPvG.js +1 -0
  72. package/webui/dist/assets/kts-DFAlwc6n.js +1 -0
  73. package/webui/dist/assets/less-DVTAwKKz.js +1 -0
  74. package/webui/dist/assets/lua-xZcbXUE9.js +1 -0
  75. package/webui/dist/assets/makefile-Dixweg8N.js +1 -0
  76. package/webui/dist/assets/markdown-BYOwaDjH.js +1 -0
  77. package/webui/dist/assets/php-C6PDnbTe.js +1 -0
  78. package/webui/dist/assets/pierre-dark-CpLgRqie.js +1 -0
  79. package/webui/dist/assets/pierre-dark-protanopia-deuteranopia-B35FxJx-.js +1 -0
  80. package/webui/dist/assets/pierre-dark-soft-kZQmAZld.js +1 -0
  81. package/webui/dist/assets/pierre-dark-tritanopia-CpjhbsIL.js +1 -0
  82. package/webui/dist/assets/pierre-dark-vibrant-CpQYzh95.js +1 -0
  83. package/webui/dist/assets/pierre-light-CoaEpmwp.js +1 -0
  84. package/webui/dist/assets/pierre-light-protanopia-deuteranopia-0fSaH845.js +1 -0
  85. package/webui/dist/assets/pierre-light-soft-lWLdNTOI.js +1 -0
  86. package/webui/dist/assets/pierre-light-tritanopia-CEbqgOJL.js +1 -0
  87. package/webui/dist/assets/pierre-light-vibrant-D80Fkn33.js +1 -0
  88. package/webui/dist/assets/postcss-D_Wz94rJ.js +1 -0
  89. package/webui/dist/assets/powershell-QtSnmXcu.js +1 -0
  90. package/webui/dist/assets/prisma-DMgNhnYS.js +1 -0
  91. package/webui/dist/assets/properties-CxAuzHS9.js +1 -0
  92. package/webui/dist/assets/python-gzcpVVnB.js +1 -0
  93. package/webui/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
  94. package/webui/dist/assets/ruby-CPL7DL0w.js +1 -0
  95. package/webui/dist/assets/rust-DpiIf5JO.js +1 -0
  96. package/webui/dist/assets/sass-DXrisJhu.js +1 -0
  97. package/webui/dist/assets/scss-CHVQqdPx.js +1 -0
  98. package/webui/dist/assets/shellscript-CLZ0U2zV.js +1 -0
  99. package/webui/dist/assets/sql-CEbI0hMM.js +1 -0
  100. package/webui/dist/assets/svelte-DKhxH1PN.js +1 -0
  101. package/webui/dist/assets/swift-CyEgAFGc.js +1 -0
  102. package/webui/dist/assets/toml-CcmNWLt0.js +1 -0
  103. package/webui/dist/assets/tsx-CUOciP0b.js +1 -0
  104. package/webui/dist/assets/typescript-BydbNFcO.js +1 -0
  105. package/webui/dist/assets/vue-kBwcnTZb.js +1 -0
  106. package/webui/dist/assets/wasm-BnjxR4X6.js +1 -0
  107. package/webui/dist/assets/xml-CeXJrFlG.js +1 -0
  108. package/webui/dist/assets/yaml-rwi0_p6S.js +1 -0
  109. package/webui/dist/assets/yml-s9iVJXCw.js +1 -0
  110. package/webui/dist/assets/zsh-B7CDPTg5.js +1 -0
  111. package/webui/dist/build.json +61 -45
  112. package/webui/dist/index.html +4 -2
  113. package/prompts/quickfix.md +0 -67
  114. package/src/lobby/quickfix.ts +0 -546
  115. package/src/pi/route.ts +0 -180
  116. package/src/webui/api/quickfix.ts +0 -49
  117. package/webui/dist/assets/index-Bs1_77_1.js +0 -114
  118. package/webui/dist/assets/index-DOLQ4FDd.css +0 -2
@@ -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; routed?: boolean } = {}): string {
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 (routing did): `triage` is its read, or absent when it was off. */
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, ...(options.routed ? { routed: true } : {}) }), ctx.isIdle() ? undefined : { deliverAs: "followUp" });
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 quick fix, a review, a planning round)
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", "quickfix", "planner"] as const;
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: direct quick fixes and the task planner. */
60
- export type LobbyAgentKind = "quickfix" | "planner";
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, quick fixes 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. */
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 new request goes to the quick-fix agent when one engineer can do it alone at least this likely (the oracle confirms). */
233
- quickFixAt: number;
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
- quickFixAt: 0.7,
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
- for (const key of ["agentTimeoutMs", "wrapUpAt", "taskBudgetMinutes"]) delete (workflow as unknown as Record<string, unknown>)[key];
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 (quick fix or planner). */
640
- export function lobbyAgentConfig(config: BotLobbyConfig, kind: LobbyAgentKind): AgentModelConfig {
641
- return kind === "quickfix" ? config.quickFix : config.planner;
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. */
@@ -1,8 +1,9 @@
1
1
  /**
2
- * Who changed what: a project-wide ledger of the files each quick fix and each
3
- * task worker edited, read from their own `edit`/`write` tool calls (never
4
- * from their reports). The QA gate and the Master see every changed file of
5
- * the working tree against it, so a quick fix the user asked for, a planned
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
- export type ChangeSource = "quickfix" | "worker";
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 quick fix (`QF-3`) or the worker run that made the edits. */
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 === "quickfix" || record.source === "worker") && typeof record.id === "string" && Array.isArray(record.files) && typeof record.finishedAt === "string";
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" | "quickfix" | "other-task" | "pre-existing" | "unattributed";
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 quick fixes that edited it. */
144
- quickFixes: string[];
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
- * quick fix, the user's direct request; by another task's workers, that
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 quickFixes: string[] = [];
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 === "quickfix") {
192
- add("quickfix", `quick fix ${record.id}${clock(record.finishedAt)}, asked by the user: "${clip(record.what)}"`);
193
- if (!quickFixes.includes(record.id)) quickFixes.push(record.id);
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, quickFixes, ...(domains.length ? { domains } : {}) };
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("quickfix") ? 1 : 2);
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 quick fix (QF-2) · 1 unattributed (src/x.ts)`. */
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 quick = count("quickfix");
226
- if (quick.length > 0) {
227
- const ids = [...new Set(quick.flatMap((file) => file.quickFixes))];
228
- parts.push(`${quick.length} by quick fix (${ids.join(", ")})`);
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`);
@@ -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, quick fixes,
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
- export const METRIC_KINDS = ["master", "scout", "worker", "reviewer", "researcher", "quickfix", "planner", "panel", "classifier"] as const;
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, QUICK FIX, ORACLE (planning). */
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, quick fixes held). */
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
  };
@@ -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 and quick-fix queue
10
- * are kept beside it, by session, so they carry on as well.
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" | "quickfix";
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");
@@ -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.
@@ -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 text to the oracle, and stopping its turn. Bounds come from the
5
- * feed module, so the page sees exactly what the terminal keeps.
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 notice = ctx.service.toOracle(withAttachments(body.text, body.attachments, ctx.service.zen().task?.id, ctx.service.projectRoot?.()));
88
- return notice ? { notice } : {};
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. */