@a-t-h-i/bot-lobby 0.6.3 → 0.6.5
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 +180 -17
- package/package.json +12 -6
- package/prompts/backend.md +3 -1
- package/prompts/designer.md +24 -1
- package/prompts/master.md +98 -19
- package/prompts/quickfix.md +16 -3
- package/prompts/researcher.md +8 -2
- package/prompts/reviewer.md +42 -0
- package/prompts/worker.md +7 -0
- package/src/agents/backend.ts +2 -2
- package/src/agents/designer.ts +2 -2
- 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/classifier/triage.ts +24 -7
- 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 +6 -1
- package/src/lobby/layout.ts +27 -8
- package/src/lobby/markdown.ts +36 -6
- package/src/lobby/planner.ts +1 -1
- package/src/lobby/quickfix.ts +50 -3
- package/src/lobby/runtime.ts +41 -40
- package/src/lobby/tabs/home.ts +112 -48
- package/src/lobby/tabs/issues.ts +4 -3
- package/src/lobby/tabs/plan.ts +4 -4
- package/src/lobby/tabs/quickfix.ts +8 -2
- package/src/lobby/tabs/tasks.ts +16 -5
- package/src/lobby/theme.ts +30 -0
- package/src/lobby/view.ts +36 -5
- package/src/master/decisions.ts +10 -2
- package/src/master/master.ts +41 -4
- package/src/master/research.ts +5 -2
- package/src/pi/commands.ts +108 -13
- package/src/pi/events.ts +48 -10
- package/src/pi/quiet.ts +22 -4
- package/src/pi/route.ts +180 -0
- package/src/pi/start-task.ts +56 -4
- package/src/pi/tools.ts +32 -9
- package/src/pi/ui.ts +6 -1
- package/src/pi/zen-metrics.ts +13 -3
- package/src/pi/zen.ts +16 -9
- package/src/roles/reviewer.ts +23 -4
- package/src/roles/worker.ts +28 -2
- package/src/schemas/configuration.ts +15 -0
- package/src/schemas/findings.ts +14 -0
- package/src/schemas/task.ts +52 -0
- package/src/state/budget.ts +274 -0
- package/src/state/changes.ts +231 -0
- package/src/text.ts +28 -2
- package/src/web/extract.ts +332 -0
- package/src/web/fetch.ts +232 -0
- package/src/web/html.ts +183 -0
- package/src/web/read.ts +113 -0
- package/src/web/search.ts +202 -0
- package/src/web/tools.ts +279 -0
- package/src/workflow/track.ts +436 -0
- package/src/workflow/workflow.ts +734 -46
package/src/master/master.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { profileFor, type BotLobbyConfig, type ProfileResolver } from "../schema
|
|
|
4
4
|
import type { Domain } from "../schemas/agent.ts";
|
|
5
5
|
import type { AgentRun, ReviewResult, ScoutResult, WorkerResult } from "../schemas/findings.ts";
|
|
6
6
|
import { domainSpec } from "../agents/registry.ts";
|
|
7
|
-
import { runAgent, runParallel, watchdogOptions, type AgentRequest } from "../execution/agent-runner.ts";
|
|
8
|
-
import { spawnPiProcess, type ProcessRunner } from "../execution/pi-runner.ts";
|
|
7
|
+
import { runAgent, runParallel, watchdogOptions, type AgentRequest, type AgentTime } from "../execution/agent-runner.ts";
|
|
8
|
+
import { spawnPiProcess, type ProcessRunner, type RelayAsk } from "../execution/pi-runner.ts";
|
|
9
9
|
import { readAgentKnowledge, writeFileEnsured } from "../knowledge/store.ts";
|
|
10
10
|
import { selectKnowledge, type KnowledgeSelection } from "../knowledge/selector.ts";
|
|
11
11
|
import { summarizeOutcomes } from "./synthesis.ts";
|
|
@@ -24,6 +24,8 @@ export interface ScoutOutcome {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export interface ScoutRequest {
|
|
27
|
+
/** Under a task time budget: the batch's time (every scout runs within it). */
|
|
28
|
+
time?: AgentTime;
|
|
27
29
|
taskId: string;
|
|
28
30
|
/** Requirements/objective text used for context selection. */
|
|
29
31
|
taskText: string;
|
|
@@ -65,7 +67,7 @@ function scoutContext(request: ScoutRequest, domain: Domain, likely = ""): Agent
|
|
|
65
67
|
task: request.taskText,
|
|
66
68
|
...selectKnowledge(`${request.taskText} ${domainSpec(domain).scoutFocus}`, slices),
|
|
67
69
|
instructions: request.config.agents[domain].instructions,
|
|
68
|
-
workflowContext: [`Task state: scouting. Domain: ${domain}. Read-only reconnaissance; no implementation.`, likely].filter(Boolean).join("\n\n"),
|
|
70
|
+
workflowContext: [`Task state: scouting. Domain: ${domain}. Read-only reconnaissance; no implementation.`, likely, request.time?.note ?? ""].filter(Boolean).join("\n\n"),
|
|
69
71
|
};
|
|
70
72
|
}
|
|
71
73
|
|
|
@@ -120,6 +122,7 @@ export async function runScouts(request: ScoutRequest, run: ProcessRunner = spaw
|
|
|
120
122
|
context: scoutContext(request, domain, likely[index]),
|
|
121
123
|
...(extraTools.length > 0 ? { extraTools } : {}),
|
|
122
124
|
...profileFields(request.config, request.profile, domain, "scout"),
|
|
125
|
+
...(request.time ? { time: { ...request.time } } : {}),
|
|
123
126
|
cwd: request.cwd,
|
|
124
127
|
signal: request.signal,
|
|
125
128
|
onUpdate: request.onUpdate,
|
|
@@ -203,6 +206,21 @@ export interface WorkerRequest {
|
|
|
203
206
|
hints?: FileHinter;
|
|
204
207
|
/** Lowers thinking (or the model) for a step the classifier judges simple or trivial. */
|
|
205
208
|
effort?: EffortRouter;
|
|
209
|
+
/** Under a task time budget: the step's time, and who decides on more when it runs out. */
|
|
210
|
+
time?: AgentTime;
|
|
211
|
+
/** The worker may ask the user (the designer): its questions are relayed, and its images saved in `previews`. */
|
|
212
|
+
ask?: { onAsk: RelayAsk; previews: string };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** What a worker that may ask the user is told about it. */
|
|
216
|
+
function askGuidance(previews: string): string {
|
|
217
|
+
return [
|
|
218
|
+
"You can ask the user with ask_user_question: the oracle relays it, and your clock stops while they answer.",
|
|
219
|
+
"Ask when a design decision is genuinely theirs (a visual direction, a layout, a style), once, with all such questions together, before you build it; decide everything else yourself.",
|
|
220
|
+
"Show each option. Give it a `preview`: a Markdown wireframe (a fenced block drawn with box characters) or a short snippet of the component.",
|
|
221
|
+
`When you can render an option (a screenshot from the project's own tooling, a rendered mockup), save it as a PNG under ${previews} (never in the repository) and give its path as the option's \`image\`.`,
|
|
222
|
+
"Put the option you recommend first. The answers are recorded as the task's decisions: build what the user chose.",
|
|
223
|
+
].join(" ");
|
|
206
224
|
}
|
|
207
225
|
|
|
208
226
|
function workerWorkflowContext(request: WorkerRequest, likely = ""): string {
|
|
@@ -218,6 +236,8 @@ function workerWorkflowContext(request: WorkerRequest, likely = ""): string {
|
|
|
218
236
|
? `Scout findings for your domain:\n${summarizeOutcomes(own, 1500)}`
|
|
219
237
|
: "No scout findings were collected for your domain; verify the repository yourself.",
|
|
220
238
|
likely,
|
|
239
|
+
request.time?.note ?? "",
|
|
240
|
+
request.ask ? askGuidance(request.ask.previews) : "",
|
|
221
241
|
].filter(Boolean).join("\n\n");
|
|
222
242
|
}
|
|
223
243
|
|
|
@@ -253,11 +273,18 @@ export async function runWorker(
|
|
|
253
273
|
...watchdogOptions(request.config.workflow),
|
|
254
274
|
...request.agent,
|
|
255
275
|
...(extraTools.length > 0 ? { extraTools } : {}),
|
|
276
|
+
// One allotment for the step: a routed attempt that falls short re-runs on what is left of it.
|
|
277
|
+
...(request.time ? { time: request.time } : {}),
|
|
278
|
+
...(request.ask ? { onAsk: request.ask.onAsk } : {}),
|
|
256
279
|
};
|
|
257
280
|
let outcome = workerOutcome(request.domain, await runAgent(route ? { ...base, ...routedFields(route) } : base, run));
|
|
258
281
|
// A routed step that fell short runs again at the configured model and thinking.
|
|
259
282
|
if (route && fellShort(outcome.run, outcome.issues) && !request.signal?.aborted) {
|
|
283
|
+
const routed = outcome.run.edited ?? [];
|
|
260
284
|
outcome = workerOutcome(request.domain, await runAgent(base, run));
|
|
285
|
+
// The step owns what the routed attempt edited too.
|
|
286
|
+
const edited = [...new Set([...routed, ...(outcome.run.edited ?? [])])];
|
|
287
|
+
if (edited.length > 0) outcome = { ...outcome, run: { ...outcome.run, edited } };
|
|
261
288
|
}
|
|
262
289
|
return outcome;
|
|
263
290
|
}
|
|
@@ -284,6 +311,12 @@ export interface ReviewerRequest {
|
|
|
284
311
|
workerSummary: string;
|
|
285
312
|
scoutOutcomes: ScoutOutcome[];
|
|
286
313
|
diff: string;
|
|
314
|
+
/** Who changed each changed file (planned, quick fix, pre-existing, …), one line each. */
|
|
315
|
+
provenance?: string;
|
|
316
|
+
/** What the previous QA round asked for, when there was one: this round verifies it. */
|
|
317
|
+
previousRound?: string;
|
|
318
|
+
/** Under a task time budget: the gate's time. */
|
|
319
|
+
time?: AgentTime;
|
|
287
320
|
instruction?: string;
|
|
288
321
|
cwd: string;
|
|
289
322
|
dataRoots: readonly string[];
|
|
@@ -301,8 +334,11 @@ function reviewerContext(request: ReviewerRequest): string {
|
|
|
301
334
|
return [
|
|
302
335
|
`Task state: reviewing. Domain: ${request.domain}.`,
|
|
303
336
|
"You may not modify implementation. Report required changes instead.",
|
|
304
|
-
request.
|
|
337
|
+
request.previousRound ?? "",
|
|
338
|
+
request.workerSummary ? `Worker summary (each domain's newest entries):\n${truncate(request.workerSummary, 4800)}` : "No worker summary available.",
|
|
305
339
|
owns.length > 0 ? `Scout findings:\n${summarizeOutcomes(owns, 1200)}` : "",
|
|
340
|
+
request.provenance ? `Who changed each file (bot-lobby's record of every agent's edit and write calls; judge each as your role's Change provenance says):\n${request.provenance}` : "",
|
|
341
|
+
request.time?.note ?? "",
|
|
306
342
|
`Repository changes:\n${request.diff}`,
|
|
307
343
|
]
|
|
308
344
|
.filter((line) => line.length > 0)
|
|
@@ -334,6 +370,7 @@ function reviewerAgentRequest(request: ReviewerRequest, selected: KnowledgeSelec
|
|
|
334
370
|
workflowContext: reviewerContext(request),
|
|
335
371
|
},
|
|
336
372
|
...profileFields(request.config, request.profile, request.domain, "reviewer"),
|
|
373
|
+
...(request.time ? { time: request.time } : {}),
|
|
337
374
|
cwd: request.cwd,
|
|
338
375
|
signal: request.signal,
|
|
339
376
|
onUpdate: request.onUpdate,
|
package/src/master/research.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import type { Domain } from "../schemas/agent.ts";
|
|
4
4
|
import { profileFor, type BotLobbyConfig, type ProfileResolver } from "../schemas/configuration.ts";
|
|
5
5
|
import type { AgentRun, ResearchResult } from "../schemas/findings.ts";
|
|
6
|
-
import { runAgent, watchdogOptions, type AgentRequest } from "../execution/agent-runner.ts";
|
|
6
|
+
import { runAgent, watchdogOptions, type AgentRequest, type AgentTime } from "../execution/agent-runner.ts";
|
|
7
7
|
import { spawnPiProcess, type ProcessRunner } from "../execution/pi-runner.ts";
|
|
8
8
|
import { writeFileEnsured } from "../knowledge/store.ts";
|
|
9
9
|
import { isResearchResultUsable, parseResearchResult, validateResearchResult } from "../roles/researcher.ts";
|
|
@@ -27,6 +27,8 @@ export interface ResearchRequest {
|
|
|
27
27
|
profile?: ProfileResolver;
|
|
28
28
|
signal?: AbortSignal;
|
|
29
29
|
onUpdate?: (run: AgentRun) => void;
|
|
30
|
+
/** Under a task time budget: the researcher's time. */
|
|
31
|
+
time?: AgentTime;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export function researchResultPath(taskDir: string, domain: Domain): string {
|
|
@@ -56,7 +58,7 @@ function researchContext(request: ResearchRequest): AgentRequest["context"] {
|
|
|
56
58
|
return {
|
|
57
59
|
task: request.instruction,
|
|
58
60
|
instructions: request.config.agents[request.domain].instructions,
|
|
59
|
-
workflowContext: `Task state: research requested by the Master. Domain: ${request.domain}. Read-only; report cited findings only.`,
|
|
61
|
+
workflowContext: [`Task state: research requested by the Master. Domain: ${request.domain}. Read-only; report cited findings only.`, request.time?.note ?? ""].filter(Boolean).join("\n\n"),
|
|
60
62
|
};
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -86,6 +88,7 @@ export async function runResearch(request: ResearchRequest, run: ProcessRunner =
|
|
|
86
88
|
model: profile.model,
|
|
87
89
|
thinking: profile.thinking,
|
|
88
90
|
timeoutMs: profile.timeoutMs,
|
|
91
|
+
...(request.time ? { time: request.time } : {}),
|
|
89
92
|
cwd: request.cwd,
|
|
90
93
|
signal: request.signal,
|
|
91
94
|
onUpdate: request.onUpdate,
|
package/src/pi/commands.ts
CHANGED
|
@@ -17,26 +17,33 @@ import { transition } from "../state/task-state.ts";
|
|
|
17
17
|
import { AGENT_DIR_NAMES, KNOWLEDGE_FILES, knowledgeDir, type KnowledgeAgent } from "../knowledge/paths.ts";
|
|
18
18
|
import { readFirstExisting } from "../knowledge/store.ts";
|
|
19
19
|
import { overThreshold } from "../knowledge/compactor.ts";
|
|
20
|
-
import { applyApprovalChoice, describeTask, describeOversizedKnowledge, type ApprovalChoice } from "../workflow/workflow.ts";
|
|
20
|
+
import { applyApprovalChoice, describeTask, describeOversizedKnowledge, lastQaAsks, waiveQa, type ApprovalChoice } from "../workflow/workflow.ts";
|
|
21
21
|
import { applyStatus, registerRevealShortcut, setMinimized } from "./ui.ts";
|
|
22
22
|
import { classifierSummary, openSettings } from "./settings-ui.ts";
|
|
23
23
|
import { keyStatus } from "../classifier/instance.ts";
|
|
24
|
-
import { kickoff, startPlannedTask
|
|
24
|
+
import { kickoff, startPlannedTask } from "./start-task.ts";
|
|
25
|
+
import { startRequest } from "./route.ts";
|
|
25
26
|
import { setAuto, toggleOwnAuto } from "./owner.ts";
|
|
26
27
|
import { autoOpenLobby, showLobby } from "../lobby/runtime.ts";
|
|
27
28
|
import { modelRef, thinkingMismatches } from "./model-support.ts";
|
|
28
29
|
import { describeRun, runFromLog } from "./run-summary.ts";
|
|
29
30
|
import { modelLookup } from "./tools.ts";
|
|
31
|
+
import { budgetLine, budgetState, parseMinutes, readBudget, setBudget, startClock } from "../state/budget.ts";
|
|
32
|
+
import { qaStillDue } from "../workflow/track.ts";
|
|
30
33
|
|
|
31
34
|
const HELP = [
|
|
32
35
|
"/bot-lobby Open the lobby: tasks, plan, quick fix, metrics (alt+l)",
|
|
33
|
-
"/bot-lobby <request> Start a
|
|
34
|
-
"/bot-lobby --task [--auto] <request>
|
|
36
|
+
"/bot-lobby <request> Start a request: a quick fix when one agent can do it alone (the oracle confirms), else a task",
|
|
37
|
+
"/bot-lobby --task [--auto] <request> Always a task (also when the request begins with a subcommand word)",
|
|
38
|
+
"/bot-lobby --budget 90m <request> Start a task with a time budget the oracle divides between its agents",
|
|
39
|
+
"/bot-lobby --fast|--full <request> Start a task on the fast track (straight to the agents it needs) or the full workflow, whatever it reads as",
|
|
40
|
+
"/bot-lobby budget [90m|off] Show or set this session's task time budget",
|
|
35
41
|
"/bot-lobby status [taskId] Show the active task",
|
|
36
42
|
"/bot-lobby tasks List tasks",
|
|
37
43
|
"/bot-lobby pause|resume Pause or resume the active task",
|
|
38
44
|
"/bot-lobby cancel [taskId] Abandon a task",
|
|
39
45
|
"/bot-lobby approve|amend <text>|decline Answer the current proposal",
|
|
46
|
+
"/bot-lobby accept [taskId] Accept a task's work as it is, without a QA pass; the oracle then completes it",
|
|
40
47
|
"/bot-lobby knowledge Show persistent knowledge files",
|
|
41
48
|
"/bot-lobby runs [taskId] Recent subagent runs: time, turns, tokens, cost, model",
|
|
42
49
|
"/bot-lobby settings Edit per-agent model/thinking/instructions",
|
|
@@ -50,21 +57,54 @@ const HELP = [
|
|
|
50
57
|
].join("\n");
|
|
51
58
|
|
|
52
59
|
/** Subcommands only win when no free-form text follows (so tasks still start). */
|
|
53
|
-
const SUBCOMMANDS = new Set(["lobby", "help", "status", "runs", "tasks", "pause", "resume", "cancel", "approve", "amend", "decline", "knowledge", "config", "settings", "minimize", "restore", "claim", "auto", "start-plan", "switch"]);
|
|
60
|
+
const SUBCOMMANDS = new Set(["lobby", "help", "status", "runs", "tasks", "pause", "resume", "cancel", "approve", "amend", "decline", "accept", "budget", "knowledge", "config", "settings", "minimize", "restore", "claim", "auto", "start-plan", "switch"]);
|
|
54
61
|
|
|
55
62
|
function isTaskId(value: string | undefined): boolean {
|
|
56
63
|
return Boolean(value && /^TASK-/.test(value));
|
|
57
64
|
}
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
/** A task start's leading flags: `--task` (always a task), `--auto`, `--fast`, `--full`, `--budget <time>` (or `--budget=<time>`). */
|
|
67
|
+
const START_FLAG = /^--(task|auto|fast|full)(?=\s|$)\s*|^--budget(?:=|\s+)(\S+)\s*/;
|
|
68
|
+
|
|
69
|
+
export interface ParsedCommand {
|
|
70
|
+
sub: string | undefined;
|
|
71
|
+
rest: string[];
|
|
72
|
+
restText: string;
|
|
73
|
+
auto?: boolean;
|
|
74
|
+
/** Minutes from `--budget`. */
|
|
75
|
+
budget?: number;
|
|
76
|
+
/** `--budget` with something that is not a time. */
|
|
77
|
+
budgetError?: string;
|
|
78
|
+
/** `--fast` or `--full`: the task's path, whatever its request reads as. */
|
|
79
|
+
track?: "fast" | "full";
|
|
80
|
+
/** `--task`: a task, never routed to the quick-fix agent. */
|
|
81
|
+
task?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function parseCommand(args: string): ParsedCommand {
|
|
85
|
+
let trimmed = args.trim();
|
|
86
|
+
// Leading flags start a task: what follows is its request, even when it begins with a subcommand word
|
|
87
|
+
// (a background session started from the lobby sends `--task [--auto] <request>`).
|
|
88
|
+
let flagged = false;
|
|
89
|
+
const extras: Pick<ParsedCommand, "auto" | "budget" | "budgetError" | "track" | "task"> = {};
|
|
90
|
+
for (let match = START_FLAG.exec(trimmed); match; match = START_FLAG.exec(trimmed)) {
|
|
91
|
+
flagged = true;
|
|
92
|
+
if (match[1] === "auto") extras.auto = true;
|
|
93
|
+
else if (match[1] === "task") extras.task = true;
|
|
94
|
+
else if (match[1] === "fast" || match[1] === "full") extras.track = match[1];
|
|
95
|
+
else if (match[2] !== undefined) {
|
|
96
|
+
const minutes = parseMinutes(match[2]);
|
|
97
|
+
if (minutes) extras.budget = minutes;
|
|
98
|
+
else extras.budgetError = `"${match[2]}" is not a time budget (try 90m, 1h or 1h30m)`;
|
|
99
|
+
}
|
|
100
|
+
trimmed = trimmed.slice(match[0].length);
|
|
101
|
+
}
|
|
102
|
+
if (flagged) return { sub: undefined, rest: [], restText: trimmed.trim(), ...extras };
|
|
64
103
|
const [sub, ...rest] = trimmed.split(/\s+/).filter(Boolean);
|
|
65
104
|
if (!sub || !SUBCOMMANDS.has(sub)) return { sub: undefined, rest: [], restText: trimmed };
|
|
66
105
|
const takesArgs = sub === "amend" || sub === "claim"
|
|
67
106
|
|| (sub === "auto" && rest.length === 1 && /^(on|off)$/i.test(rest[0]!))
|
|
107
|
+
|| (sub === "budget" && rest.length >= 1 && rest.length <= 2 && (/^off$/i.test(rest[0]!) || parseMinutes(rest.join("")) !== undefined))
|
|
68
108
|
|| (sub === "start-plan" && rest.length >= 1 && rest.length <= 2 && /^PLAN-/.test(rest[0]!) && (rest.length === 1 || rest[1] === "auto"))
|
|
69
109
|
|| (sub === "switch" && rest.length >= 1 && /\.jsonl$/.test(rest.join(" ")));
|
|
70
110
|
if (!takesArgs && rest.length > 0 && !(rest.length === 1 && isTaskId(rest[0]))) {
|
|
@@ -87,7 +127,9 @@ function showStatus(ctx: ExtensionCommandContext, configDir: string, taskId?: st
|
|
|
87
127
|
broken.length > 0 ? `Unreadable task state: ${broken.join(", ")}` : "",
|
|
88
128
|
].filter(Boolean).join("\n");
|
|
89
129
|
const footer = knowledge ? `\n${knowledge}` : "";
|
|
90
|
-
|
|
130
|
+
const budget = task ? readBudget(root, configDir, task.id) : undefined;
|
|
131
|
+
const time = task && budget ? `\n${budgetLine(budget, budgetState(task.id, budget, !qaStillDue(task)))}` : "";
|
|
132
|
+
ctx.ui.notify(task ? `${describeTask(task)}${time}${footer}` : `No bot-lobby task found in ${root}.${footer}`, task ? "info" : "warning");
|
|
91
133
|
}
|
|
92
134
|
|
|
93
135
|
/** Recent runs of the active (or named) task, newest last, so slow models are easy to spot. */
|
|
@@ -175,6 +217,52 @@ function answerProposal(
|
|
|
175
217
|
}
|
|
176
218
|
}
|
|
177
219
|
|
|
220
|
+
/**
|
|
221
|
+
* `/bot-lobby accept [taskId]`: the user accepts a task's work as it stands,
|
|
222
|
+
* without a QA pass (QA keeps asking for more, and the user has seen enough).
|
|
223
|
+
* The gate is waived and blockers cleared; the oracle is asked to complete it.
|
|
224
|
+
*/
|
|
225
|
+
function acceptWork(pi: ExtensionAPI, ctx: ExtensionCommandContext, configDir: string, taskId?: string): void {
|
|
226
|
+
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
227
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
228
|
+
const task = selectTask(root, configDir, taskId, sessionId);
|
|
229
|
+
if (!task || TERMINAL_STATES.includes(task.state)) return ctx.ui.notify("bot-lobby: no active task to accept.", "warning");
|
|
230
|
+
if (task.ownerSessionId && task.ownerSessionId !== sessionId) {
|
|
231
|
+
return ctx.ui.notify(`${task.id} is owned by another pi session; accept it there, or /bot-lobby claim ${task.id} first.`, "warning");
|
|
232
|
+
}
|
|
233
|
+
if (!["implementing", "reviewing", "blocked"].includes(task.state)) {
|
|
234
|
+
return ctx.ui.notify(`bot-lobby: ${task.id} is still ${task.state}; there is no work to accept yet.`, "warning");
|
|
235
|
+
}
|
|
236
|
+
if (task.qaVerdict !== "pass" && !task.qaWaiver) waiveQa(task, lastQaAsks(task), "with /bot-lobby accept");
|
|
237
|
+
saveTask(root, configDir, task);
|
|
238
|
+
applyStatus(ctx, root, configDir);
|
|
239
|
+
ctx.ui.notify(`bot-lobby: ${task.id} accepted as it is; the oracle will complete it.`, "info");
|
|
240
|
+
const message = `The user accepted ${task.id}'s work as it is (the QA gate is waived). Call orchestrate action=complete now with a short summary that names anything QA still asked for.`;
|
|
241
|
+
pi.sendUserMessage(message, ctx.isIdle() ? undefined : { deliverAs: "followUp" });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* `/bot-lobby budget [time|off]`: show, set or remove the time budget of this
|
|
246
|
+
* session's task. Time already spent is kept; the oracle sees the change on
|
|
247
|
+
* its next step.
|
|
248
|
+
*/
|
|
249
|
+
function budgetCommand(ctx: ExtensionCommandContext, configDir: string, value: string): void {
|
|
250
|
+
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
251
|
+
const task = activeTask(root, configDir, ctx.sessionManager.getSessionId());
|
|
252
|
+
if (!task) return ctx.ui.notify("bot-lobby: no active task in this session — a time budget applies to a task (start one with /bot-lobby --budget 90m <request>)", "warning");
|
|
253
|
+
if (!value) {
|
|
254
|
+
const budget = readBudget(root, configDir, task.id);
|
|
255
|
+
return ctx.ui.notify(budget ? `${task.id} — ${budgetLine(budget, budgetState(task.id, budget, !qaStillDue(task)))}` : `${task.id} has no time budget. Set one with /bot-lobby budget 90m.`, "info");
|
|
256
|
+
}
|
|
257
|
+
const minutes = /^off$/i.test(value) ? 0 : parseMinutes(value);
|
|
258
|
+
if (minutes === undefined) return ctx.ui.notify(`bot-lobby: "${value}" is not a time budget (try 90m, 1h or 1h30m)`, "warning");
|
|
259
|
+
const budget = setBudget(root, configDir, task.id, minutes);
|
|
260
|
+
// Set while the oracle works: its clock runs from now, not from its next turn.
|
|
261
|
+
if (budget && !ctx.isIdle()) startClock(root, configDir, task.id);
|
|
262
|
+
applyStatus(ctx, root, configDir);
|
|
263
|
+
ctx.ui.notify(budget ? `bot-lobby: ${task.id} — ${budgetLine(budget, budgetState(task.id, budget, !qaStillDue(task)))}` : `bot-lobby: ${task.id} has no time budget now.`, "info");
|
|
264
|
+
}
|
|
265
|
+
|
|
178
266
|
function showKnowledge(ctx: ExtensionCommandContext, configDir: string): void {
|
|
179
267
|
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
180
268
|
const cfg = loadConfig();
|
|
@@ -236,13 +324,16 @@ export function registerCommands(pi: ExtensionAPI, configDir: string): void {
|
|
|
236
324
|
return filtered.length > 0 ? filtered : null;
|
|
237
325
|
},
|
|
238
326
|
handler: async (args, ctx) => {
|
|
239
|
-
const { sub, rest, restText, auto } = parseCommand(args ?? "");
|
|
327
|
+
const { sub, rest, restText, auto, budget, budgetError, track, task } = parseCommand(args ?? "");
|
|
328
|
+
if (budgetError) return ctx.ui.notify(`bot-lobby: ${budgetError}`, "warning");
|
|
240
329
|
if (!sub) {
|
|
241
330
|
if (!restText) {
|
|
242
331
|
if (!showLobby()) ctx.ui.notify(HELP, "info");
|
|
243
332
|
return;
|
|
244
333
|
}
|
|
245
|
-
|
|
334
|
+
// A request one agent can do alone may go to the quick-fix agent once the oracle confirms; the rest start as tasks.
|
|
335
|
+
const started = await startRequest(pi, ctx, configDir, restText, { ...(auto ? { auto } : {}), ...(budget ? { budget } : {}), ...(track ? { track } : {}), ...(task ? { task } : {}) });
|
|
336
|
+
if (started === "task") autoOpenLobby();
|
|
246
337
|
return;
|
|
247
338
|
}
|
|
248
339
|
switch (sub) {
|
|
@@ -269,6 +360,10 @@ export function registerCommands(pi: ExtensionAPI, configDir: string): void {
|
|
|
269
360
|
return answerProposal(ctx, configDir, "amend", restText);
|
|
270
361
|
case "decline":
|
|
271
362
|
return answerProposal(ctx, configDir, "decline");
|
|
363
|
+
case "accept":
|
|
364
|
+
return acceptWork(pi, ctx, configDir, rest[0]);
|
|
365
|
+
case "budget":
|
|
366
|
+
return budgetCommand(ctx, configDir, rest.join(""));
|
|
272
367
|
case "knowledge":
|
|
273
368
|
return showKnowledge(ctx, configDir);
|
|
274
369
|
case "settings":
|
package/src/pi/events.ts
CHANGED
|
@@ -9,20 +9,38 @@ import { describeTask } from "../workflow/workflow.ts";
|
|
|
9
9
|
import { truncate } from "../text.ts";
|
|
10
10
|
import { applyStatus, clearStatus, isMinimized, setMinimized, setOracleActivity } from "./ui.ts";
|
|
11
11
|
import { ORACLE_THINKING, oracleActivityWord } from "./activity.ts";
|
|
12
|
-
import { isSubagentProcess,
|
|
12
|
+
import { isSubagentProcess, webToolsFor } from "./quiet.ts";
|
|
13
13
|
import { registerQuietTools } from "./tool-renderers.ts";
|
|
14
14
|
import { taskRequest, type Task, type TaskState } from "../schemas/task.ts";
|
|
15
15
|
import { pendingComments, readPlanComments, type PlanComment } from "../state/comments.ts";
|
|
16
16
|
import { isAutoMode } from "../state/auto.ts";
|
|
17
17
|
import { triageContext } from "../classifier/triage.ts";
|
|
18
|
+
import { qaStillDue } from "../workflow/track.ts";
|
|
18
19
|
import { previousTaskNote } from "./fresh-context.ts";
|
|
20
|
+
import { budgetLine, budgetState, pauseClocks, readBudget, resumeClocks, startClock, stopClocks } from "../state/budget.ts";
|
|
21
|
+
|
|
22
|
+
/** Tools that wait on the user: the task's clock waits with them. */
|
|
23
|
+
const ASKING_TOOLS: ReadonlySet<string> = new Set(["ask_user_question"]);
|
|
19
24
|
|
|
20
25
|
/** States in which the triage still helps the Master shape the task; once it is planned, the hints are noise. */
|
|
21
26
|
const SHAPING_STATES: ReadonlySet<TaskState> = new Set(["created", "clarifying", "scouting", "synthesizing", "awaiting_approval"]);
|
|
22
27
|
|
|
23
|
-
/** The Master's workflow context: the task's state,
|
|
24
|
-
export function masterWorkflowContext(task: Task): string {
|
|
25
|
-
return [describeTask(task), SHAPING_STATES.has(task.state) ? triageContext(task.triage) : ""].filter(Boolean).join("\n\n");
|
|
28
|
+
/** The Master's workflow context: the task's state, where its time budget stands, and the classifier's triage while the task is being shaped. */
|
|
29
|
+
export function masterWorkflowContext(task: Task, time = ""): string {
|
|
30
|
+
return [describeTask(task), time, SHAPING_STATES.has(task.state) ? triageContext(task.triage) : ""].filter(Boolean).join("\n\n");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The oracle's view of the task's time budget: where it stands and how to spend it; "" without one. */
|
|
34
|
+
export function budgetContext(root: string, configDir: string, task: Task): string {
|
|
35
|
+
const budget = readBudget(root, configDir, task.id);
|
|
36
|
+
if (!budget) return "";
|
|
37
|
+
const state = budgetState(task.id, budget, !qaStillDue(task));
|
|
38
|
+
const running = budget.allotments.filter((entry) => !entry.endedAt).map((entry) => `${entry.who} (${entry.minutes}m)`);
|
|
39
|
+
return [
|
|
40
|
+
budgetLine(budget, state),
|
|
41
|
+
running.length > 0 ? `Running now: ${running.join(", ")}.` : "",
|
|
42
|
+
"Divide what is left by scope: pass `minutes` on each implement (per assignment in a parallel batch), keeping the QA gate's reserve. Every agent is told its minutes; one that runs out reports where it left off and the user decides on more.",
|
|
43
|
+
].filter(Boolean).join(" ");
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
export function masterTaskContext(task: Task, comments: readonly PlanComment[] = [], auto = false): string {
|
|
@@ -47,12 +65,11 @@ export function masterTaskContext(task: Task, comments: readonly PlanComment[] =
|
|
|
47
65
|
* section rather than replacing the whole prompt.
|
|
48
66
|
*/
|
|
49
67
|
export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
68
|
+
// Web tools the oracle put away for the task in hand, given back when it ends.
|
|
69
|
+
let hiddenWeb: string[] = [];
|
|
50
70
|
pi.on("session_start", (_event, ctx) => {
|
|
51
71
|
// Master-only: subagents keep their own --tools allowlist (see quiet.ts).
|
|
52
|
-
if (!isSubagentProcess())
|
|
53
|
-
registerQuietTools(pi);
|
|
54
|
-
pi.setActiveTools(visibleTools(pi.getActiveTools()));
|
|
55
|
-
}
|
|
72
|
+
if (!isSubagentProcess()) registerQuietTools(pi);
|
|
56
73
|
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
57
74
|
setMinimized(false);
|
|
58
75
|
applyStatus(ctx, root, configDir);
|
|
@@ -65,25 +82,38 @@ export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
|
65
82
|
// Parallel tool calls: the newest still-running call keeps the word.
|
|
66
83
|
const inFlight = new Map<string, string>();
|
|
67
84
|
const showOracle = () => setOracleActivity([...inFlight.values()].at(-1) ?? ORACLE_THINKING);
|
|
68
|
-
|
|
85
|
+
// A task's time budget counts while the oracle works on it, not while it waits on the user.
|
|
86
|
+
const asking = new Set<string>();
|
|
87
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
69
88
|
if (isSubagentProcess()) return;
|
|
70
89
|
inFlight.clear();
|
|
71
90
|
showOracle();
|
|
91
|
+
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
92
|
+
const task = activeTask(root, configDir, ctx.sessionManager.getSessionId());
|
|
93
|
+
if (task) startClock(root, configDir, task.id);
|
|
72
94
|
});
|
|
73
95
|
pi.on("tool_execution_start", (event) => {
|
|
74
96
|
if (isSubagentProcess()) return;
|
|
75
97
|
inFlight.set(event.toolCallId, oracleActivityWord(event.toolName, event.args));
|
|
76
98
|
showOracle();
|
|
99
|
+
if (ASKING_TOOLS.has(event.toolName) && !asking.has(event.toolCallId)) {
|
|
100
|
+
asking.add(event.toolCallId);
|
|
101
|
+
pauseClocks();
|
|
102
|
+
}
|
|
77
103
|
});
|
|
78
104
|
pi.on("tool_execution_end", (event) => {
|
|
79
105
|
if (isSubagentProcess()) return;
|
|
80
106
|
inFlight.delete(event.toolCallId);
|
|
81
107
|
showOracle();
|
|
108
|
+
if (asking.delete(event.toolCallId)) resumeClocks();
|
|
82
109
|
});
|
|
83
110
|
pi.on("agent_end", () => {
|
|
84
111
|
if (isSubagentProcess()) return;
|
|
85
112
|
inFlight.clear();
|
|
86
113
|
setOracleActivity(undefined);
|
|
114
|
+
for (const _call of asking) resumeClocks();
|
|
115
|
+
asking.clear();
|
|
116
|
+
stopClocks();
|
|
87
117
|
});
|
|
88
118
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
89
119
|
cancelAllRuns();
|
|
@@ -95,6 +125,14 @@ export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
|
95
125
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
96
126
|
const hidden = isSubagentProcess() || isMinimized();
|
|
97
127
|
const task = hidden ? undefined : activeTask(root, configDir, sessionId);
|
|
128
|
+
if (!isSubagentProcess()) {
|
|
129
|
+
// The researcher is the task's only web path; without a task pi keeps them.
|
|
130
|
+
const tools = webToolsFor(pi.getActiveTools(), Boolean(task), hiddenWeb);
|
|
131
|
+
if (tools) {
|
|
132
|
+
hiddenWeb = tools.hidden;
|
|
133
|
+
pi.setActiveTools(tools.active);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
98
136
|
if (!task) {
|
|
99
137
|
// After a task ended and cleared the context, a line on where its record is, in case the user refers back to it.
|
|
100
138
|
const previous = hidden ? undefined : previousTaskNote(ctx, configDir);
|
|
@@ -110,7 +148,7 @@ export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
|
110
148
|
standards: selected.standards,
|
|
111
149
|
knowledge: selected.knowledge,
|
|
112
150
|
decisions: selected.decisions,
|
|
113
|
-
workflowContext: masterWorkflowContext(task),
|
|
151
|
+
workflowContext: masterWorkflowContext(task, budgetContext(root, configDir, task)),
|
|
114
152
|
instructions: loadConfig().master.instructions,
|
|
115
153
|
});
|
|
116
154
|
});
|
package/src/pi/quiet.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Quiet mode for the master transcript: built-in tool rows render zero lines
|
|
3
|
-
* by default and `alt+t` reveals a compact one-line summary per call.
|
|
4
|
-
* tools are removed from the master's active set so the
|
|
5
|
-
* running in its own process, stays the only web
|
|
3
|
+
* by default and `alt+t` reveals a compact one-line summary per call. While a
|
|
4
|
+
* task runs, the web tools are removed from the master's active set so the
|
|
5
|
+
* researcher subagent, running in its own process, stays the task's only web
|
|
6
|
+
* path; without a task, pi has them like any other tool.
|
|
6
7
|
*
|
|
7
8
|
* bot-lobby's extension also loads inside the subagent processes it spawns
|
|
8
9
|
* (see `spawnPiProcess`). Those children receive their tools from an explicit
|
|
@@ -10,7 +11,7 @@
|
|
|
10
11
|
* never run there or research silently degrades to repository-only.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
|
-
/**
|
|
14
|
+
/** bot-lobby's web tools (src/web/tools.ts): the researcher's, and pi's outside a task, never the oracle's. */
|
|
14
15
|
export const WEB_TOOL_NAMES: readonly string[] = [
|
|
15
16
|
"web_search",
|
|
16
17
|
"fetch_content",
|
|
@@ -40,6 +41,23 @@ export function visibleTools(active: string[]): string[] {
|
|
|
40
41
|
return active.filter((name) => !WEB_TOOL_NAMES.includes(name));
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The master's active tools for a turn: without the web tools while a task
|
|
46
|
+
* is active, and with those it took away given back once none is. Only tools
|
|
47
|
+
* it hid itself come back, so a tool the user turned off stays off. Returns
|
|
48
|
+
* undefined when nothing changes.
|
|
49
|
+
*/
|
|
50
|
+
export function webToolsFor(active: string[], taskActive: boolean, hidden: readonly string[]): { active: string[]; hidden: string[] } | undefined {
|
|
51
|
+
if (taskActive) {
|
|
52
|
+
const taken = active.filter((name) => WEB_TOOL_NAMES.includes(name));
|
|
53
|
+
if (taken.length === 0) return undefined;
|
|
54
|
+
return { active: visibleTools(active), hidden: [...new Set([...hidden, ...taken])] };
|
|
55
|
+
}
|
|
56
|
+
const back = hidden.filter((name) => !active.includes(name));
|
|
57
|
+
if (hidden.length === 0) return undefined;
|
|
58
|
+
return { active: [...active, ...back], hidden: [] };
|
|
59
|
+
}
|
|
60
|
+
|
|
43
61
|
/** True inside a bot-lobby subagent process (marked in `spawnPiProcess`). */
|
|
44
62
|
export function isSubagentProcess(): boolean {
|
|
45
63
|
return process.env.BOT_LOBBY_SUBAGENT === "1";
|