@a-t-h-i/bot-lobby 0.6.4 → 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 +81 -9
- package/package.json +12 -3
- package/prompts/backend.md +3 -1
- package/prompts/designer.md +24 -1
- package/prompts/master.md +51 -18
- package/prompts/quickfix.md +16 -3
- package/src/agents/backend.ts +2 -2
- package/src/agents/designer.ts +2 -2
- package/src/classifier/triage.ts +24 -7
- package/src/index.ts +3 -0
- package/src/lobby/feed.ts +6 -1
- package/src/lobby/quickfix.ts +29 -3
- package/src/lobby/runtime.ts +27 -4
- package/src/lobby/tabs/quickfix.ts +1 -1
- package/src/lobby/tabs/tasks.ts +2 -1
- package/src/lobby/view.ts +9 -0
- package/src/master/decisions.ts +10 -2
- package/src/pi/commands.ts +22 -11
- package/src/pi/events.ts +2 -1
- package/src/pi/route.ts +180 -0
- package/src/pi/start-task.ts +51 -5
- package/src/pi/tools.ts +8 -3
- package/src/roles/worker.ts +10 -2
- package/src/schemas/configuration.ts +11 -0
- package/src/schemas/task.ts +31 -0
- package/src/workflow/track.ts +436 -0
- package/src/workflow/workflow.ts +148 -10
|
@@ -65,7 +65,7 @@ export function jobDetailLines(job: QuickFixJob, width: number, tick: number, no
|
|
|
65
65
|
const asked = theme?.markdown ? markdownLines(job.prompt, width, theme, true, "you") : wrap(bold(theme, job.prompt), width);
|
|
66
66
|
const lines = [paint(theme, "dim", job.id), facts.join(paint(theme, "dim", " · ")), "", ...asked];
|
|
67
67
|
if (job.route) lines.push("", ...wrap(paint(theme, "dim", `↓ routed ${job.route}`), width));
|
|
68
|
-
if (job.note) lines.push("", ...wrap(paint(theme, "warning", job.status === "held" ? `‖ Held: ${job.note}. r runs it anyway · t starts it as a task in a new session.` : job.note), width));
|
|
68
|
+
if (job.note) lines.push("", ...wrap(paint(theme, job.status === "held" ? "warning" : job.routed ? "muted" : "warning", job.status === "held" ? `‖ Held: ${job.note}. r runs it anyway · t starts it as a task in a new session.` : job.note), width));
|
|
69
69
|
lines.push("", rule(width, "Steps", theme));
|
|
70
70
|
if (job.steps.length === 0) lines.push(paint(theme, "dim", job.status === "queued" ? "Waiting for the quick fix ahead of it." : job.status === "running" ? `${spinner(tick)} starting…` : job.status === "held" ? "Not started." : "No tool calls."));
|
|
71
71
|
for (const step of job.steps) {
|
package/src/lobby/tabs/tasks.ts
CHANGED
|
@@ -268,7 +268,8 @@ export function taskDetailLines(task: Task, comments: readonly PlanComment[], se
|
|
|
268
268
|
finished ? paint(theme, "muted", `${task.state === "abandoned" ? "dropped" : "done"} ${since(now - Date.parse(task.updatedAt))}`) : "",
|
|
269
269
|
].filter(Boolean);
|
|
270
270
|
lines.push(...wrapHanging(" ", facts.join(dot), width));
|
|
271
|
-
|
|
271
|
+
const track = task.track ? (task.track.path === "fast" ? `fast track (${task.track.size})` : `full workflow (${task.track.size})`) : "";
|
|
272
|
+
lines.push(...wrapHanging(" ", paint(theme, "dim", [task.id, track, task.domains.length > 0 ? task.domains.join(", ") : ""].filter(Boolean).join(" · ")), width));
|
|
272
273
|
const steps = task.plan ? planChecklist(task.plan, persistedRuns(task)) : [];
|
|
273
274
|
const done = steps.filter((step) => step.status === "done").length;
|
|
274
275
|
if (steps.length > 0) {
|
package/src/lobby/view.ts
CHANGED
|
@@ -471,6 +471,15 @@ export class LobbyView implements Component, Focusable {
|
|
|
471
471
|
return visibleTabs(this.issuesOn);
|
|
472
472
|
}
|
|
473
473
|
|
|
474
|
+
/** Open the Quick fix tab on a job the oracle routed there, with a line saying so. */
|
|
475
|
+
showQuickFix(id: string): void {
|
|
476
|
+
this.setTab("quickfix");
|
|
477
|
+
delete this.queries.quickfix;
|
|
478
|
+
this.fixSelected = Math.max(0, this.fixJobs().findIndex((job) => job.id === id));
|
|
479
|
+
this.fixDetailOffset = 0;
|
|
480
|
+
this.say(`${id} — the oracle sent your request to the quick-fix agent`);
|
|
481
|
+
}
|
|
482
|
+
|
|
474
483
|
setTab(tab: TabId): void {
|
|
475
484
|
if (tab === this.tab) return;
|
|
476
485
|
if (!this.tabs().includes(tab)) return this.say(`the ${TAB_LABELS[tab]} tab is off — set lobby.${tab} to true in the config to bring it back`, "warning");
|
package/src/master/decisions.ts
CHANGED
|
@@ -2,12 +2,20 @@ import type { Decision, Task, Verdict } from "../schemas/task.ts";
|
|
|
2
2
|
import type { Domain } from "../schemas/agent.ts";
|
|
3
3
|
import type { ScoutOutcome } from "./master.ts";
|
|
4
4
|
import { detectGaps, domainsInvolved } from "./synthesis.ts";
|
|
5
|
+
import { builtSomething, onFastTrack, qaRequired, qaTookPart } from "../workflow/track.ts";
|
|
5
6
|
|
|
6
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* §19: everything the engine requires before completion may be declared. The
|
|
9
|
+
* fast track asks for a finished worker step instead of a QA pass, and for
|
|
10
|
+
* QA's part only when the change needs tests.
|
|
11
|
+
*/
|
|
7
12
|
export function completionBlockers(task: Task, pendingCount: number): string[] {
|
|
8
13
|
const blockers: string[] = [];
|
|
9
14
|
if (!task.plan) blockers.push("no approved plan is recorded");
|
|
10
|
-
if (task
|
|
15
|
+
if (onFastTrack(task)) {
|
|
16
|
+
if (!builtSomething(task)) blockers.push("no worker step has finished yet");
|
|
17
|
+
if (qaRequired(task) && !qaTookPart(task) && !task.qaWaiver) blockers.push("QA has not taken part (give qa the tests as the last step, or run the QA gate)");
|
|
18
|
+
} else if (task.qaVerdict !== "pass" && !task.qaWaiver) blockers.push(`QA gate is ${task.qaVerdict ?? "not run"}`);
|
|
11
19
|
if (pendingCount > 0) blockers.push(`${pendingCount} unresolved approval request(s)`);
|
|
12
20
|
if (task.blockers.length > 0) blockers.push(`${task.blockers.length} unresolved blocker(s)`);
|
|
13
21
|
return blockers;
|
package/src/pi/commands.ts
CHANGED
|
@@ -21,19 +21,22 @@ import { applyApprovalChoice, describeTask, describeOversizedKnowledge, lastQaAs
|
|
|
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";
|
|
30
31
|
import { budgetLine, budgetState, parseMinutes, readBudget, setBudget, startClock } from "../state/budget.ts";
|
|
32
|
+
import { qaStillDue } from "../workflow/track.ts";
|
|
31
33
|
|
|
32
34
|
const HELP = [
|
|
33
35
|
"/bot-lobby Open the lobby: tasks, plan, quick fix, metrics (alt+l)",
|
|
34
|
-
"/bot-lobby <request> Start a
|
|
35
|
-
"/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)",
|
|
36
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",
|
|
37
40
|
"/bot-lobby budget [90m|off] Show or set this session's task time budget",
|
|
38
41
|
"/bot-lobby status [taskId] Show the active task",
|
|
39
42
|
"/bot-lobby tasks List tasks",
|
|
@@ -60,8 +63,8 @@ function isTaskId(value: string | undefined): boolean {
|
|
|
60
63
|
return Boolean(value && /^TASK-/.test(value));
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
/** A task start's leading flags: `--task` (always a task), `--auto`, `--budget <time>` (or `--budget=<time>`). */
|
|
64
|
-
const START_FLAG = /^--(task|auto)(?=\s|$)\s*|^--budget(?:=|\s+)(\S+)\s*/;
|
|
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*/;
|
|
65
68
|
|
|
66
69
|
export interface ParsedCommand {
|
|
67
70
|
sub: string | undefined;
|
|
@@ -72,6 +75,10 @@ export interface ParsedCommand {
|
|
|
72
75
|
budget?: number;
|
|
73
76
|
/** `--budget` with something that is not a time. */
|
|
74
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;
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
export function parseCommand(args: string): ParsedCommand {
|
|
@@ -79,10 +86,12 @@ export function parseCommand(args: string): ParsedCommand {
|
|
|
79
86
|
// Leading flags start a task: what follows is its request, even when it begins with a subcommand word
|
|
80
87
|
// (a background session started from the lobby sends `--task [--auto] <request>`).
|
|
81
88
|
let flagged = false;
|
|
82
|
-
const extras: Pick<ParsedCommand, "auto" | "budget" | "budgetError"> = {};
|
|
89
|
+
const extras: Pick<ParsedCommand, "auto" | "budget" | "budgetError" | "track" | "task"> = {};
|
|
83
90
|
for (let match = START_FLAG.exec(trimmed); match; match = START_FLAG.exec(trimmed)) {
|
|
84
91
|
flagged = true;
|
|
85
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];
|
|
86
95
|
else if (match[2] !== undefined) {
|
|
87
96
|
const minutes = parseMinutes(match[2]);
|
|
88
97
|
if (minutes) extras.budget = minutes;
|
|
@@ -119,7 +128,7 @@ function showStatus(ctx: ExtensionCommandContext, configDir: string, taskId?: st
|
|
|
119
128
|
].filter(Boolean).join("\n");
|
|
120
129
|
const footer = knowledge ? `\n${knowledge}` : "";
|
|
121
130
|
const budget = task ? readBudget(root, configDir, task.id) : undefined;
|
|
122
|
-
const time = task && budget ? `\n${budgetLine(budget, budgetState(task.id, budget, task
|
|
131
|
+
const time = task && budget ? `\n${budgetLine(budget, budgetState(task.id, budget, !qaStillDue(task)))}` : "";
|
|
123
132
|
ctx.ui.notify(task ? `${describeTask(task)}${time}${footer}` : `No bot-lobby task found in ${root}.${footer}`, task ? "info" : "warning");
|
|
124
133
|
}
|
|
125
134
|
|
|
@@ -243,7 +252,7 @@ function budgetCommand(ctx: ExtensionCommandContext, configDir: string, value: s
|
|
|
243
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");
|
|
244
253
|
if (!value) {
|
|
245
254
|
const budget = readBudget(root, configDir, task.id);
|
|
246
|
-
return ctx.ui.notify(budget ? `${task.id} — ${budgetLine(budget, budgetState(task.id, budget, task
|
|
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");
|
|
247
256
|
}
|
|
248
257
|
const minutes = /^off$/i.test(value) ? 0 : parseMinutes(value);
|
|
249
258
|
if (minutes === undefined) return ctx.ui.notify(`bot-lobby: "${value}" is not a time budget (try 90m, 1h or 1h30m)`, "warning");
|
|
@@ -251,7 +260,7 @@ function budgetCommand(ctx: ExtensionCommandContext, configDir: string, value: s
|
|
|
251
260
|
// Set while the oracle works: its clock runs from now, not from its next turn.
|
|
252
261
|
if (budget && !ctx.isIdle()) startClock(root, configDir, task.id);
|
|
253
262
|
applyStatus(ctx, root, configDir);
|
|
254
|
-
ctx.ui.notify(budget ? `bot-lobby: ${task.id} — ${budgetLine(budget, budgetState(task.id, budget, task
|
|
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");
|
|
255
264
|
}
|
|
256
265
|
|
|
257
266
|
function showKnowledge(ctx: ExtensionCommandContext, configDir: string): void {
|
|
@@ -315,14 +324,16 @@ export function registerCommands(pi: ExtensionAPI, configDir: string): void {
|
|
|
315
324
|
return filtered.length > 0 ? filtered : null;
|
|
316
325
|
},
|
|
317
326
|
handler: async (args, ctx) => {
|
|
318
|
-
const { sub, rest, restText, auto, budget, budgetError } = parseCommand(args ?? "");
|
|
327
|
+
const { sub, rest, restText, auto, budget, budgetError, track, task } = parseCommand(args ?? "");
|
|
319
328
|
if (budgetError) return ctx.ui.notify(`bot-lobby: ${budgetError}`, "warning");
|
|
320
329
|
if (!sub) {
|
|
321
330
|
if (!restText) {
|
|
322
331
|
if (!showLobby()) ctx.ui.notify(HELP, "info");
|
|
323
332
|
return;
|
|
324
333
|
}
|
|
325
|
-
|
|
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();
|
|
326
337
|
return;
|
|
327
338
|
}
|
|
328
339
|
switch (sub) {
|
package/src/pi/events.ts
CHANGED
|
@@ -15,6 +15,7 @@ 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";
|
|
19
20
|
import { budgetLine, budgetState, pauseClocks, readBudget, resumeClocks, startClock, stopClocks } from "../state/budget.ts";
|
|
20
21
|
|
|
@@ -33,7 +34,7 @@ export function masterWorkflowContext(task: Task, time = ""): string {
|
|
|
33
34
|
export function budgetContext(root: string, configDir: string, task: Task): string {
|
|
34
35
|
const budget = readBudget(root, configDir, task.id);
|
|
35
36
|
if (!budget) return "";
|
|
36
|
-
const state = budgetState(task.id, budget, task
|
|
37
|
+
const state = budgetState(task.id, budget, !qaStillDue(task));
|
|
37
38
|
const running = budget.allotments.filter((entry) => !entry.endedAt).map((entry) => `${entry.who} (${entry.minutes}m)`);
|
|
38
39
|
return [
|
|
39
40
|
budgetLine(budget, state),
|
package/src/pi/route.ts
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a new request goes before any task exists. The classifier (Jev, or
|
|
3
|
+
* the rules while it is off) reads whether one agent can do it alone, right
|
|
4
|
+
* away. When it can, the oracle confirms with `route_request`: the lobby hands
|
|
5
|
+
* the request to the quick-fix agent and shows the Quick fix tab. Otherwise,
|
|
6
|
+
* or when the oracle says it needs the team, the task starts as before.
|
|
7
|
+
*
|
|
8
|
+
* Nothing is routed without the lobby (it runs quick fixes), when routing is
|
|
9
|
+
* off (`workflow.routeQuickFixes`), or for a request the user already placed
|
|
10
|
+
* (`--task`, `--fast`, `--full`) or a planned task.
|
|
11
|
+
*/
|
|
12
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
13
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { Container, Text } from "@earendil-works/pi-tui";
|
|
15
|
+
import { Type } from "typebox";
|
|
16
|
+
import type { Domain } from "../schemas/agent.ts";
|
|
17
|
+
import type { TaskTriage } from "../schemas/task.ts";
|
|
18
|
+
import { chooseRoute, type RequestRoute } from "../workflow/track.ts";
|
|
19
|
+
import { triageFor } from "../classifier/instance.ts";
|
|
20
|
+
import { ownedTask } from "../state/persistence.ts";
|
|
21
|
+
import { detectProjectRoot, loadConfig } from "../state/project.ts";
|
|
22
|
+
import { lobbyFeed } from "../lobby/feed.ts";
|
|
23
|
+
import { startTask, type StartOptions } from "./start-task.ts";
|
|
24
|
+
import { isQuiet, isSubagentProcess } from "./quiet.ts";
|
|
25
|
+
|
|
26
|
+
export const ROUTE_TOOL = "route_request";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Hands a request to the lobby's quick-fix agent and shows the Quick fix tab;
|
|
30
|
+
* the job's id, or undefined without a lobby. `builder` is set for a quick
|
|
31
|
+
* feature: it runs on that domain's model, thinking and time limit.
|
|
32
|
+
*/
|
|
33
|
+
export type QuickFixHandoff = (request: string, builder: Domain | undefined, reason: string) => string | undefined;
|
|
34
|
+
|
|
35
|
+
let handoff: QuickFixHandoff | undefined;
|
|
36
|
+
|
|
37
|
+
/** The lobby registers its quick-fix hand-off while it runs. */
|
|
38
|
+
export function setQuickFixHandoff(next: QuickFixHandoff | undefined): void {
|
|
39
|
+
handoff = next;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface Pending {
|
|
43
|
+
request: string;
|
|
44
|
+
options: StartOptions;
|
|
45
|
+
route: RequestRoute;
|
|
46
|
+
at: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let pending: Pending | undefined;
|
|
50
|
+
|
|
51
|
+
/** A request the oracle has not routed in this long is forgotten: the next one starts afresh. */
|
|
52
|
+
const STALE_MS = 30 * 60_000;
|
|
53
|
+
|
|
54
|
+
/** The request waiting for the oracle to route it, if any. */
|
|
55
|
+
export function pendingRequest(now = Date.now()): { request: string; route: RequestRoute } | undefined {
|
|
56
|
+
if (pending && now - pending.at >= STALE_MS) pending = undefined;
|
|
57
|
+
return pending ? { request: pending.request, route: pending.route } : undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A quick feature (bigger than a small change, but self-contained) runs with its builder's settings; a quick fix with the quick-fix agent's. */
|
|
61
|
+
function builderFor(route: RequestRoute): Domain | undefined {
|
|
62
|
+
return route.size === "medium" || route.size === "large" ? route.builder : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** What the oracle is asked: one step, from the request alone. */
|
|
66
|
+
export function routeMessage(request: string, route: RequestRoute): string {
|
|
67
|
+
return [
|
|
68
|
+
"bot-lobby: a new request, not a task yet.",
|
|
69
|
+
`Request: ${request}`,
|
|
70
|
+
`Read: a ${builderFor(route) ? "quick feature" : "quick fix"} — ${route.reasons.join("; ")} (${route.source}).`,
|
|
71
|
+
"",
|
|
72
|
+
"Confirm where it goes with route_request, in one step and from the request alone: do not read files, plan or ask.",
|
|
73
|
+
"- to=quickfix: one agent does it now and the lobby shows it on the Quick fix tab. Right for anything one person can just do, even a rich piece of work in one file.",
|
|
74
|
+
"- to=task: the team (bot-lobby's workflow), when it needs several areas agreeing on a contract, a survey of an unfamiliar codebase, a risky change, or decisions the user must make first.",
|
|
75
|
+
"Then tell the user in one line where it went.",
|
|
76
|
+
].join("\n");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface RequestOptions extends StartOptions {
|
|
80
|
+
/** `--task`: always a task. */
|
|
81
|
+
task?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A new request from the lobby or `/bot-lobby <request>`: a task at once, or
|
|
86
|
+
* (when it reads as one agent's work) a question to the oracle first.
|
|
87
|
+
* Resolves with what happened.
|
|
88
|
+
*/
|
|
89
|
+
export async function startRequest(pi: ExtensionAPI, ctx: ExtensionContext, configDir: string, request: string, options: RequestOptions = {}): Promise<"task" | "routing" | undefined> {
|
|
90
|
+
const { task: asTask, ...start } = options;
|
|
91
|
+
const config = loadConfig();
|
|
92
|
+
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
93
|
+
const placed = asTask || start.track || start.approvedPlan || !config.workflow.routeQuickFixes || !handoff;
|
|
94
|
+
// A session that already drives a task gets startTask's own warning.
|
|
95
|
+
if (placed || ownedTask(root, configDir, ctx.sessionManager.getSessionId())) {
|
|
96
|
+
return (await startTask(pi, ctx, configDir, request, start)) ? "task" : undefined;
|
|
97
|
+
}
|
|
98
|
+
const triage = await triageFor({ cwd: ctx.cwd, root, configDir }, request);
|
|
99
|
+
const route = chooseRoute(request, triage, config.classifier.thresholds.quickFixAt);
|
|
100
|
+
const read: StartOptions = { ...start, triaged: true, ...(triage ? { triage } : {}) };
|
|
101
|
+
if (route.to === "task") return (await startTask(pi, ctx, configDir, request, read)) ? "task" : undefined;
|
|
102
|
+
pending = { request, options: read, route, at: Date.now() };
|
|
103
|
+
lobbyFeed.log("LOBBY", `reads as a ${builderFor(route) ? "quick feature" : "quick fix"} (${route.source}): ${route.reasons[0]} — the oracle confirms`, "info");
|
|
104
|
+
pi.sendUserMessage(routeMessage(request, route), ctx.isIdle() ? undefined : { deliverAs: "followUp" });
|
|
105
|
+
return "routing";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const RouteSchema = Type.Object({
|
|
109
|
+
to: StringEnum(["quickfix", "task"] as const, { description: "quickfix: one agent does it now, on the lobby's Quick fix tab; task: the team (bot-lobby's workflow)" }),
|
|
110
|
+
reason: Type.Optional(Type.String({ description: "one line: why it goes there" })),
|
|
111
|
+
request: Type.Optional(Type.String({ description: "the request as it should be done, when the user refined it since; their words otherwise" })),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
/** The oracle's answer to a routing question: the quick-fix agent, or a task. */
|
|
115
|
+
export async function routeRequest(pi: ExtensionAPI, ctx: ExtensionContext, configDir: string, params: { to: "quickfix" | "task"; reason?: string; request?: string }): Promise<{ ok: boolean; text: string }> {
|
|
116
|
+
const waiting = pendingRequest();
|
|
117
|
+
if (!waiting || !pending) return { ok: false, text: "No request is waiting to be routed. Start a task with /bot-lobby <request>." };
|
|
118
|
+
const { options, route } = pending;
|
|
119
|
+
const request = params.request?.trim() || pending.request;
|
|
120
|
+
pending = undefined;
|
|
121
|
+
const why = params.reason?.trim();
|
|
122
|
+
if (params.to === "quickfix") {
|
|
123
|
+
const id = handoff?.(request, builderFor(route), why || route.reasons[0] || "one agent can do it alone");
|
|
124
|
+
if (id) {
|
|
125
|
+
lobbyFeed.log("LOBBY", `the oracle sent it to the quick-fix agent: ${id}${why ? ` — ${why}` : ""}`, "success");
|
|
126
|
+
return { ok: true, text: `${id} is running on the lobby's Quick fix tab, which is now open. Tell the user in one line that this looks like a ${builderFor(route) ? "quick feature" : "quick fix"} and the quick-fix agent is on it; nothing else to do.` };
|
|
127
|
+
}
|
|
128
|
+
// No lobby to run it any more: it becomes a task.
|
|
129
|
+
}
|
|
130
|
+
lobbyFeed.log("LOBBY", `the oracle sent it to the team${why ? ` — ${why}` : ""}`, "info");
|
|
131
|
+
const task = await startTask(pi, ctx, configDir, request, { ...options, routed: true });
|
|
132
|
+
return task
|
|
133
|
+
? { ok: true, text: `Started ${task.id} as a task; its kickoff follows as the next message. Say nothing more now.` }
|
|
134
|
+
: { ok: false, text: "Could not start a task: this session already drives one (finish or cancel it first)." };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* `route_request`, active only while a request waits for the oracle, so it
|
|
139
|
+
* never sits in the tool list of plain pi or of a running task.
|
|
140
|
+
*/
|
|
141
|
+
export function registerRouteTool(pi: ExtensionAPI, configDir: string): void {
|
|
142
|
+
if (isSubagentProcess()) return;
|
|
143
|
+
pi.on("session_start", () => {
|
|
144
|
+
pending = undefined;
|
|
145
|
+
});
|
|
146
|
+
pi.on("before_agent_start", () => {
|
|
147
|
+
const active = pi.getActiveTools();
|
|
148
|
+
const want = Boolean(pendingRequest());
|
|
149
|
+
if (want === active.includes(ROUTE_TOOL)) return;
|
|
150
|
+
pi.setActiveTools(want ? [...active, ROUTE_TOOL] : active.filter((name) => name !== ROUTE_TOOL));
|
|
151
|
+
});
|
|
152
|
+
pi.registerTool({
|
|
153
|
+
name: ROUTE_TOOL,
|
|
154
|
+
label: "Route request",
|
|
155
|
+
description: "Send a new bot-lobby request to the quick-fix agent (to=quickfix: one agent does it now, shown on the lobby's Quick fix tab) or to the team (to=task: bot-lobby's workflow). Only while a request waits to be routed.",
|
|
156
|
+
promptSnippet: "Route a new bot-lobby request: quickfix (one agent now) or task (the team)",
|
|
157
|
+
parameters: RouteSchema,
|
|
158
|
+
renderShell: "self",
|
|
159
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
160
|
+
const result = await routeRequest(pi, ctx, configDir, params as { to: "quickfix" | "task"; reason?: string; request?: string });
|
|
161
|
+
return { content: [{ type: "text", text: result.text }], details: { ok: result.ok, to: (params as { to: string }).to } };
|
|
162
|
+
},
|
|
163
|
+
renderCall(args, theme) {
|
|
164
|
+
if (isQuiet()) return new Container();
|
|
165
|
+
const call = args as { to?: string };
|
|
166
|
+
return new Text(`${theme.fg("toolTitle", theme.bold("route_request"))} ${theme.fg("accent", call.to ?? "")}`, 0, 0);
|
|
167
|
+
},
|
|
168
|
+
renderResult(result, _options, theme) {
|
|
169
|
+
if (isQuiet()) return new Container();
|
|
170
|
+
const details = result.details as { ok?: boolean; to?: string } | undefined;
|
|
171
|
+
const icon = details?.ok ? theme.fg("success", "✓") : theme.fg("warning", "!");
|
|
172
|
+
return new Text(`${icon} ${theme.fg("toolTitle", theme.bold("route_request"))} ${theme.fg("muted", `→ ${details?.to === "quickfix" ? "Quick fix tab" : "a task"}`)}`, 0, 0);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Tests: forget any request waiting for the oracle. */
|
|
178
|
+
export function clearPendingRequest(): void {
|
|
179
|
+
pending = undefined;
|
|
180
|
+
}
|
package/src/pi/start-task.ts
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import { createTask, taskRequest, type Task } from "../schemas/task.ts";
|
|
8
|
+
import { createTask, taskRequest, type Task, type TaskTrack, type TaskTriage, type TrackPath } from "../schemas/task.ts";
|
|
9
|
+
import type { Domain } from "../schemas/agent.ts";
|
|
10
|
+
import { chooseTrack, trackLine, trackSummary } from "../workflow/track.ts";
|
|
11
|
+
import { lobbyFeed } from "../lobby/feed.ts";
|
|
9
12
|
import { createTaskDir, ensureProjectStructure, nextTaskId, ownedTask, saveTask, taskDirFor } from "../state/persistence.ts";
|
|
10
13
|
import { detectProjectRoot, loadConfig } from "../state/project.ts";
|
|
11
14
|
import { transition } from "../state/task-state.ts";
|
|
@@ -26,19 +29,51 @@ function uniqueTaskId(root: string, configDir: string, request: string): string
|
|
|
26
29
|
return id;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
/** In a kickoff after the oracle routed the request to the team (the lobby then shows the request once). */
|
|
33
|
+
export const ROUTED_LINE = "Routed: you sent this request to the team.";
|
|
34
|
+
|
|
35
|
+
/** The fast track's steps: straight to the agents the request needs, QA only for tests, then complete. */
|
|
36
|
+
function fastSteps(track: TaskTrack): string[] {
|
|
37
|
+
const building = track.roster.filter((member): member is Domain => member === "designer" || member === "backend");
|
|
38
|
+
const builders = building.length > 1
|
|
39
|
+
? `one call with assignments for ${building.join(" and ")}, each task stating the contract between them`
|
|
40
|
+
: building.length === 1 ? `domain ${building[0]}` : track.roster.includes("qa") ? "domain qa (the change is its tests)" : "the one domain that owns the files";
|
|
41
|
+
const steps = [
|
|
42
|
+
...(track.roster.includes("researcher") ? ["First summon the researcher for the outside facts it needs: orchestrate action=research with a domain and the question."] : []),
|
|
43
|
+
`Delegate now with orchestrate action=implement: ${builders}; open each task with "Step 1:" (the engine keeps the plan).`,
|
|
44
|
+
...(track.roster.includes("qa") && building.length > 0 ? ["QA takes part (tests): once the change is in, give qa the tests as the last step (action=implement domain=qa), or run action=qa."] : []),
|
|
45
|
+
"Check `git diff --stat` and the report, then orchestrate action=complete with a one-line summary.",
|
|
46
|
+
];
|
|
30
47
|
return [
|
|
48
|
+
"Fast track: the request reads small and clear, so skip the ceremony: no scouts, no proposal, no plan document.",
|
|
49
|
+
...steps.map((step, index) => `${index + 1}. ${step}`),
|
|
50
|
+
track.roster.includes("qa") ? "" : "No QA gate on this track: nothing here needs tests.",
|
|
51
|
+
"If the request is bigger, riskier or less clear than it reads, switch before delegating: orchestrate action=track track=full with a reason. Otherwise do not deliberate over the track.",
|
|
52
|
+
].filter(Boolean);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function kickoff(task: Task, budgetMinutes = 0, options: { fastTrack?: boolean; routed?: boolean } = {}): string {
|
|
56
|
+
const track = task.track;
|
|
57
|
+
const head = [
|
|
31
58
|
`A bot-lobby task is active: ${task.id}`,
|
|
32
59
|
`Title: ${task.title}`,
|
|
33
60
|
`Request: ${taskRequest(task)}`,
|
|
34
61
|
`State: ${task.state}`,
|
|
62
|
+
...(options.routed ? [ROUTED_LINE] : []),
|
|
63
|
+
...(track ? [trackSummary(track)] : []),
|
|
35
64
|
...(budgetMinutes > 0 ? [`Time budget: ${budgetMinutes} minutes of work, for you and every agent. Size the plan to fit it and divide it by scope (see Time budget in your prompt).`] : []),
|
|
36
65
|
"",
|
|
66
|
+
];
|
|
67
|
+
if (track?.path === "fast") return [...head, ...fastSteps(track)].join("\n");
|
|
68
|
+
const fastAllowed = Boolean(track) && options.fastTrack !== false && track?.userChoice !== "full" && track?.source !== "plan";
|
|
69
|
+
return [
|
|
70
|
+
...head,
|
|
37
71
|
"Drive it with the orchestrate tool:",
|
|
38
72
|
"1. clarify if the request is genuinely ambiguous,",
|
|
39
73
|
"2. scout the domains the request touches,",
|
|
40
74
|
"3. synthesize the findings and propose a short `- ` bullet list for approval.",
|
|
41
75
|
"Do not implement anything before the user approves the proposal.",
|
|
76
|
+
...(fastAllowed ? ["If it is in fact a small, clear, low-risk change, take the fast track instead: orchestrate action=track track=fast with a reason."] : []),
|
|
42
77
|
].join("\n");
|
|
43
78
|
}
|
|
44
79
|
|
|
@@ -56,6 +91,13 @@ export interface StartOptions {
|
|
|
56
91
|
title?: string;
|
|
57
92
|
/** Minutes of work time the task gets; `workflow.taskBudgetMinutes` when absent. */
|
|
58
93
|
budget?: number;
|
|
94
|
+
/** The user's `--fast` or `--full`: the path the task takes, whatever its request reads as. */
|
|
95
|
+
track?: TrackPath;
|
|
96
|
+
/** The classifier already read the request (routing did): `triage` is its read, or absent when it was off. */
|
|
97
|
+
triaged?: boolean;
|
|
98
|
+
triage?: TaskTriage;
|
|
99
|
+
/** The oracle sent this request to the team after the classifier read it as a quick fix. */
|
|
100
|
+
routed?: boolean;
|
|
59
101
|
}
|
|
60
102
|
|
|
61
103
|
export async function startTask(pi: ExtensionAPI, ctx: ExtensionContext, configDir: string, request: string, options: StartOptions = {}): Promise<Task | undefined> {
|
|
@@ -72,10 +114,14 @@ export async function startTask(pi: ExtensionAPI, ctx: ExtensionContext, configD
|
|
|
72
114
|
createTaskDir(root, configDir, task);
|
|
73
115
|
transition(task, "clarifying");
|
|
74
116
|
// The classifier's read of the request (when it is on) reaches the Master's very first turn.
|
|
75
|
-
const triage = await triageFor({ cwd: ctx.cwd, root, configDir }, request);
|
|
117
|
+
const triage = options.triaged ? options.triage : await triageFor({ cwd: ctx.cwd, root, configDir }, request);
|
|
76
118
|
if (triage) task.triage = triage;
|
|
119
|
+
// How serious the request reads: who takes part, and whether it takes the fast track or the full workflow.
|
|
120
|
+
const config = loadConfig();
|
|
121
|
+
task.track = chooseTrack(request, triage, { fastTrack: config.workflow.fastTrack, ...(options.track ? { forced: options.track } : {}), ...(options.approvedPlan ? { approvedPlan: true } : {}) });
|
|
122
|
+
lobbyFeed.log("LOBBY", trackLine(task.track), "info");
|
|
77
123
|
saveTask(root, configDir, task);
|
|
78
|
-
const budgetMinutes = options.budget ??
|
|
124
|
+
const budgetMinutes = options.budget ?? config.workflow.taskBudgetMinutes;
|
|
79
125
|
if (budgetMinutes > 0) setBudget(root, configDir, task.id, budgetMinutes);
|
|
80
126
|
if (options.auto) setAutoMode(root, configDir, task.id, true, sessionId);
|
|
81
127
|
// A session that starts a task is named after it, so /resume and the lobby list it by name.
|
|
@@ -86,7 +132,7 @@ export async function startTask(pi: ExtensionAPI, ctx: ExtensionContext, configD
|
|
|
86
132
|
// The oracle takes the task on with a clean context: nothing said before the kickoff is sent to its model.
|
|
87
133
|
if (freshContextOn()) markContext(pi, { kind: "start", taskId: task.id, at: Date.now() });
|
|
88
134
|
// A kickoff while pi is still busy (another turn) queues behind it instead of throwing.
|
|
89
|
-
pi.sendUserMessage(kickoff(task, budgetMinutes), ctx.isIdle() ? undefined : { deliverAs: "followUp" });
|
|
135
|
+
pi.sendUserMessage(kickoff(task, budgetMinutes, { fastTrack: config.workflow.fastTrack, ...(options.routed ? { routed: true } : {}) }), ctx.isIdle() ? undefined : { deliverAs: "followUp" });
|
|
90
136
|
return task;
|
|
91
137
|
}
|
|
92
138
|
|
package/src/pi/tools.ts
CHANGED
|
@@ -51,7 +51,11 @@ const OrchestrateSchema = Type.Object({
|
|
|
51
51
|
StringEnum(["approved", "rejected"] as const, { description: "resolve_approval: approve or reject the request" }),
|
|
52
52
|
),
|
|
53
53
|
note: Type.Optional(Type.String({ description: "resolve_approval: rationale, or what to do instead when rejected" })),
|
|
54
|
-
reason: Type.Optional(Type.String({ description: "block: why the task cannot continue; budget: why the task needs more time" })),
|
|
54
|
+
reason: Type.Optional(Type.String({ description: "block: why the task cannot continue; budget: why the task needs more time; track: why the task takes that path or needs those members" })),
|
|
55
|
+
track: Type.Optional(
|
|
56
|
+
StringEnum(["fast", "full"] as const, { description: "track: fast (straight to the roster's agents, no scouts, proposal or plan; QA only for tests) or full (the whole workflow)" }),
|
|
57
|
+
),
|
|
58
|
+
roster: Type.Optional(Type.Array(Type.String(), { description: "track: who takes part — any of designer, backend, qa, researcher" })),
|
|
55
59
|
text: Type.Optional(Type.String({ description: "decide/complete/knowledge: the decision, completion summary, or knowledge text" })),
|
|
56
60
|
file: Type.Optional(Type.String({ description: "compact: the knowledge file to rewrite, e.g. knowledge.md" })),
|
|
57
61
|
kind: Type.Optional(
|
|
@@ -71,7 +75,8 @@ const DESCRIPTION = [
|
|
|
71
75
|
"compact (replace a knowledge file with a rewritten version, archiving the old one),",
|
|
72
76
|
"resolve_approval (approve or reject a request), complete (declare the task done after the gates",
|
|
73
77
|
"pass), block/resume (escalate or continue), budget (under a time budget: where it stands, or ask the",
|
|
74
|
-
"user for more minutes with a reason),
|
|
78
|
+
"user for more minutes with a reason), track (the task's path and who takes part: show it, or correct it with",
|
|
79
|
+
"track=fast|full, roster and a reason), status, cancel.",
|
|
75
80
|
"The engine validates every step against the task state machine, so a rejected action means the workflow is not at that step yet.",
|
|
76
81
|
].join(" ");
|
|
77
82
|
|
|
@@ -184,7 +189,7 @@ export function registerOrchestrateTool(pi: ExtensionAPI, configDir: string, run
|
|
|
184
189
|
name: "orchestrate",
|
|
185
190
|
label: "Orchestrate",
|
|
186
191
|
description: DESCRIPTION,
|
|
187
|
-
promptSnippet: "Run a bot-lobby workflow step (clarify, scout, propose, plan, decide, status, cancel)",
|
|
192
|
+
promptSnippet: "Run a bot-lobby workflow step (clarify, scout, propose, plan, implement, track, decide, status, cancel)",
|
|
188
193
|
promptGuidelines: [
|
|
189
194
|
"Use orchestrate for every bot-lobby workflow step; it enforces the task state machine and records results.",
|
|
190
195
|
],
|
package/src/roles/worker.ts
CHANGED
|
@@ -76,6 +76,14 @@ export function parseMoreTime(text: string): { minutes?: number; reason: string
|
|
|
76
76
|
return { ...(minutes && minutes > 0 ? { minutes } : {}), reason: reason || flat };
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/** A bullet that says there is nothing: `None.`, `N/A`, `No new dependencies (three.js from a CDN)`. */
|
|
80
|
+
const NOTHING = /^(?:\*\*|_)?(?:none|n\/?a|nil|nothing|not applicable|no(?:ne)?\s+(?:new\s+|additional\s+|extra\s+)?(?:dependenc|packages?|librar|architecture|architectural|changes?\b))/i;
|
|
81
|
+
|
|
82
|
+
/** Asks that need the Master's approval: a report that lists "None." asks for nothing. */
|
|
83
|
+
export function realAsks(items: readonly string[]): string[] {
|
|
84
|
+
return items.filter((item) => !NOTHING.test(item.trim()));
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
/** Parse a worker's markdown into a structured result (never throws). */
|
|
80
88
|
export function parseWorkerResult(domain: Domain, raw: string, now = new Date().toISOString()): WorkerResult {
|
|
81
89
|
const sections = parseSections(raw);
|
|
@@ -93,8 +101,8 @@ export function parseWorkerResult(domain: Domain, raw: string, now = new Date().
|
|
|
93
101
|
blockers: parseBlockers(sections, domain, now),
|
|
94
102
|
knowledgeProposals: parseKnowledgeProposals(domain, findSection(sections, "knowledge proposals")),
|
|
95
103
|
pushback: parsePushback(sections),
|
|
96
|
-
dependencyNeeds: bullets(findSection(sections, "dependencies needed")),
|
|
97
|
-
architectureChanges: bullets(findSection(sections, "architecture changes")),
|
|
104
|
+
dependencyNeeds: realAsks(bullets(findSection(sections, "dependencies needed"))),
|
|
105
|
+
architectureChanges: realAsks(bullets(findSection(sections, "architecture changes"))),
|
|
98
106
|
raw,
|
|
99
107
|
};
|
|
100
108
|
}
|
|
@@ -70,6 +70,10 @@ export interface WorkflowConfig {
|
|
|
70
70
|
freshContext: boolean;
|
|
71
71
|
/** Minutes of work time a new task gets unless it is started with its own (`--budget`); 0 = no budget. */
|
|
72
72
|
taskBudgetMinutes: number;
|
|
73
|
+
/** Small, clear, low-risk requests take the fast track (no scouts, proposal or plan; QA only when tests are needed); false puts every task on the full workflow. */
|
|
74
|
+
fastTrack: boolean;
|
|
75
|
+
/** 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. */
|
|
76
|
+
routeQuickFixes: boolean;
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
export interface KnowledgeConfig {
|
|
@@ -143,6 +147,8 @@ export interface ClassifierThresholds {
|
|
|
143
147
|
simpleAt: number;
|
|
144
148
|
/** A step scored trivial at this confidence runs on the cheaper model. */
|
|
145
149
|
trivialAt: number;
|
|
150
|
+
/** A new request goes to the quick-fix agent when one engineer can do it alone at least this likely (the oracle confirms). */
|
|
151
|
+
quickFixAt: number;
|
|
146
152
|
/** A quick fix scored large at this confidence is held instead of started. */
|
|
147
153
|
quickFixLargeAt: number;
|
|
148
154
|
}
|
|
@@ -207,6 +213,8 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
|
|
|
207
213
|
maxParallelWorkers: 3,
|
|
208
214
|
freshContext: true,
|
|
209
215
|
taskBudgetMinutes: 0,
|
|
216
|
+
fastTrack: true,
|
|
217
|
+
routeQuickFixes: true,
|
|
210
218
|
},
|
|
211
219
|
knowledge: {
|
|
212
220
|
compactionThreshold: 20000,
|
|
@@ -239,6 +247,7 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
|
|
|
239
247
|
fileRelevantAt: 0.5,
|
|
240
248
|
simpleAt: 0.7,
|
|
241
249
|
trivialAt: 0.8,
|
|
250
|
+
quickFixAt: 0.7,
|
|
242
251
|
quickFixLargeAt: 0.8,
|
|
243
252
|
},
|
|
244
253
|
fileHints: { topK: 8, maxCandidates: 480, budgetMs: 1500 },
|
|
@@ -343,6 +352,8 @@ export function resolveConfig(partial: unknown): BotLobbyConfig {
|
|
|
343
352
|
const src = (partial ?? {}) as Record<string, unknown>;
|
|
344
353
|
const workflow = { ...DEFAULT_CONFIG.workflow, ...(src.workflow as Partial<WorkflowConfig> | undefined) };
|
|
345
354
|
workflow.freshContext = workflow.freshContext !== false;
|
|
355
|
+
workflow.fastTrack = workflow.fastTrack !== false;
|
|
356
|
+
workflow.routeQuickFixes = workflow.routeQuickFixes !== false;
|
|
346
357
|
workflow.taskBudgetMinutes = typeof workflow.taskBudgetMinutes === "number" && workflow.taskBudgetMinutes > 0 ? Math.min(24 * 60, Math.round(workflow.taskBudgetMinutes)) : 0;
|
|
347
358
|
const knowledge = { ...DEFAULT_CONFIG.knowledge, ...(src.knowledge as Partial<KnowledgeConfig> | undefined) };
|
|
348
359
|
const srcAgents = (src.agents ?? {}) as Partial<BotLobbyConfig["agents"]>;
|
package/src/schemas/task.ts
CHANGED
|
@@ -108,12 +108,41 @@ export interface TaskTriage {
|
|
|
108
108
|
research: number;
|
|
109
109
|
/** How likely it is ambiguous as written. */
|
|
110
110
|
ambiguous: number;
|
|
111
|
+
/** How likely one engineer can do it alone, right away (a quick fix, not a task for the team). */
|
|
112
|
+
solo?: number;
|
|
111
113
|
kind?: string;
|
|
112
114
|
kindProbability?: number;
|
|
113
115
|
likelyFiles?: string[];
|
|
114
116
|
at: string;
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
/** How much process a task gets: the fast track for a small, clear, low-risk change, the full workflow for the rest. */
|
|
120
|
+
export type TrackPath = "fast" | "full";
|
|
121
|
+
|
|
122
|
+
/** Who can take part in a task: the three domains and the researcher. */
|
|
123
|
+
export type TrackMember = Domain | "researcher";
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* How serious the request reads, and so who takes part and how much process
|
|
127
|
+
* it gets. The engine reads it as the task starts; the oracle may correct it
|
|
128
|
+
* (`action=track`) and the user may force the path (`--fast`, `--full`).
|
|
129
|
+
*/
|
|
130
|
+
export interface TaskTrack {
|
|
131
|
+
path: TrackPath;
|
|
132
|
+
size: TriageSize;
|
|
133
|
+
/** Who takes part: the domains that build, QA when the change needs tests, the researcher when it needs outside facts. */
|
|
134
|
+
roster: TrackMember[];
|
|
135
|
+
/** Why, a few words each. */
|
|
136
|
+
reasons: string[];
|
|
137
|
+
/** Who set it: the engine's rules, the classifier's triage, the user, the oracle, or a plan agreed in the planning panel. */
|
|
138
|
+
source: "rules" | "classifier" | "user" | "oracle" | "plan";
|
|
139
|
+
/** The path the user asked for with --fast or --full; the oracle never moves a task off it toward less process. */
|
|
140
|
+
userChoice?: TrackPath;
|
|
141
|
+
/** The engine keeps the plan (fast track): each delegation adds its step. */
|
|
142
|
+
autoPlan?: boolean;
|
|
143
|
+
at: string;
|
|
144
|
+
}
|
|
145
|
+
|
|
117
146
|
/** What the working tree held when a task's agents started editing: its changed files, repository-relative, and the commit it stood on. */
|
|
118
147
|
export interface ChangeBaseline {
|
|
119
148
|
at: string;
|
|
@@ -160,6 +189,8 @@ export interface Task {
|
|
|
160
189
|
archivedAt?: string;
|
|
161
190
|
/** The classifier's read of the request, when it was on as the task started. */
|
|
162
191
|
triage?: TaskTriage;
|
|
192
|
+
/** The task's track; absent on tasks created before tracks, which take the full workflow. */
|
|
193
|
+
track?: TaskTrack;
|
|
163
194
|
/** Files already changed when the first worker started: the QA gate reads them as pre-existing, not as this task's work. */
|
|
164
195
|
baseline?: ChangeBaseline;
|
|
165
196
|
/** Review rounds the user granted past `workflow.maxReviewIterations`. */
|