@a-t-h-i/bot-lobby 0.6.53 → 0.6.55
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 +1 -1
- package/package.json +1 -1
- package/prompts/expert.md +17 -0
- package/prompts/goal-check.md +44 -0
- package/prompts/master.md +70 -32
- package/prompts/worker.md +25 -11
- package/src/desk/board-client.ts +65 -0
- package/src/desk/board.ts +200 -0
- package/src/desk/client-extension.ts +31 -1
- package/src/desk/ipc.ts +16 -4
- package/src/desk/session.ts +129 -1
- package/src/execution/agent-runner.ts +27 -132
- package/src/execution/delegation.ts +112 -38
- package/src/execution/live-run.ts +3 -14
- package/src/execution/pi-runner.ts +30 -163
- package/src/execution/worker-trees.ts +396 -0
- package/src/index.ts +6 -0
- package/src/lobby/feed.ts +1 -1
- package/src/lobby/host.ts +2 -0
- package/src/lobby/planner.ts +2 -2
- package/src/lobby/pr-review.ts +3 -3
- package/src/lobby/quickfix.ts +4 -3
- package/src/lobby/recovery.ts +2 -10
- package/src/lobby/runtime.ts +1 -1
- package/src/lobby/service.ts +3 -1
- package/src/master/goal-check.ts +114 -0
- package/src/master/master.ts +60 -24
- package/src/master/research.ts +3 -6
- package/src/pi/commands.ts +5 -45
- package/src/pi/delegation.ts +23 -4
- package/src/pi/events.ts +6 -19
- package/src/pi/grants.ts +89 -0
- package/src/pi/model-settings.ts +2 -4
- package/src/pi/model-support.ts +12 -5
- package/src/pi/route.ts +1 -1
- package/src/pi/run-summary.ts +3 -7
- package/src/pi/start-flags.ts +3 -3
- package/src/pi/start-task.ts +2 -8
- package/src/pi/tools.ts +10 -12
- package/src/roles/worker.ts +0 -18
- package/src/schemas/configuration.ts +81 -39
- package/src/schemas/findings.ts +0 -12
- package/src/schemas/task.ts +12 -1
- package/src/state/metrics.ts +1 -1
- package/src/state/work-clock.ts +140 -0
- package/src/state/work-time.ts +4 -7
- package/src/webui/api/index.ts +2 -1
- package/src/webui/api/settings.ts +6 -18
- package/src/webui/dev/fake-service.ts +14 -0
- package/src/webui/protocol.ts +23 -0
- package/src/workflow/workflow.ts +204 -344
- package/webui/dist/assets/{TerminalView-CbRc77C8.js → TerminalView-CIfjkb5H.js} +1 -1
- package/webui/dist/assets/index-BkXhtB6A.css +2 -0
- package/webui/dist/assets/index-DIM2-FUQ.js +114 -0
- package/webui/dist/build.json +10 -6
- package/webui/dist/index.html +2 -2
- package/src/state/budget.ts +0 -301
- package/src/state/step-limits.ts +0 -41
- package/webui/dist/assets/index-Cca5uEVs.js +0 -115
- package/webui/dist/assets/index-DsVdXmh5.css +0 -2
package/src/pi/grants.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What Settings can grant an agent: the MCP tools pi has connected and the
|
|
3
|
+
* skills pi finds. The main session's pi is the source (its MCP servers and
|
|
4
|
+
* skill settings are the user's); a skill grant is kept as names and turned
|
|
5
|
+
* into files only when an agent starts, so a moved skill still loads.
|
|
6
|
+
*/
|
|
7
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { getAgentDir, loadSkills } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
|
|
10
|
+
/** An MCP tool an agent can be granted. */
|
|
11
|
+
export interface GrantableTool {
|
|
12
|
+
name: string;
|
|
13
|
+
/** The server it comes from (`github` for `mcp__github__get_issue`). */
|
|
14
|
+
server: string;
|
|
15
|
+
description: string;
|
|
16
|
+
/** The server marks it read-only: scouts, QA and the researcher may call it too. */
|
|
17
|
+
readOnly: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A skill an agent can be granted. */
|
|
21
|
+
export interface GrantableSkill {
|
|
22
|
+
name: string;
|
|
23
|
+
description: string;
|
|
24
|
+
/** Its SKILL.md (or skill file). */
|
|
25
|
+
path: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let source: ExtensionAPI | undefined;
|
|
29
|
+
|
|
30
|
+
/** Read grants from this session's pi from now on. */
|
|
31
|
+
export function bindGrants(pi: ExtensionAPI): void {
|
|
32
|
+
source = pi;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Forget the session (tests). */
|
|
36
|
+
export function unbindGrants(): void {
|
|
37
|
+
source = undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function serverOf(name: string): string {
|
|
41
|
+
return /^mcp__([A-Za-z0-9_]+?)__/.exec(name)?.[1] ?? "";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The MCP tools pi has connected, by server then name. */
|
|
45
|
+
export function grantableTools(pi: ExtensionAPI | undefined = source): GrantableTool[] {
|
|
46
|
+
let tools: ReturnType<ExtensionAPI["getAllTools"]> = [];
|
|
47
|
+
try {
|
|
48
|
+
tools = pi?.getAllTools() ?? [];
|
|
49
|
+
} catch {
|
|
50
|
+
// Not bound to a running session yet.
|
|
51
|
+
}
|
|
52
|
+
return tools
|
|
53
|
+
.filter((tool) => tool.name.startsWith("mcp__") && tool.exposure !== "hidden")
|
|
54
|
+
.map((tool) => ({ name: tool.name, server: serverOf(tool.name), description: (tool.description ?? "").split("\n")[0]!.trim(), readOnly: tool.annotations?.readOnlyHint === true }))
|
|
55
|
+
.sort((a, b) => a.server.localeCompare(b.server) || a.name.localeCompare(b.name));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The skills an agent working in `cwd` would find: the session's own (its
|
|
60
|
+
* skill commands, which include packages and settings paths) and pi's default
|
|
61
|
+
* folders for `cwd`, first one per name.
|
|
62
|
+
*/
|
|
63
|
+
export function grantableSkills(cwd: string, pi: ExtensionAPI | undefined = source): GrantableSkill[] {
|
|
64
|
+
const found = new Map<string, GrantableSkill>();
|
|
65
|
+
try {
|
|
66
|
+
for (const command of pi?.getCommands() ?? []) {
|
|
67
|
+
if (command.source !== "skill") continue;
|
|
68
|
+
const name = command.name.replace(/^skill:/, "");
|
|
69
|
+
if (!found.has(name)) found.set(name, { name, description: command.description ?? "", path: command.sourceInfo.path });
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// Not bound to a running session yet.
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
for (const skill of loadSkills({ cwd, agentDir: getAgentDir(), skillPaths: [], includeDefaults: true }).skills) {
|
|
76
|
+
if (!found.has(skill.name)) found.set(skill.name, { name: skill.name, description: skill.description, path: skill.filePath });
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// An unreadable skill folder grants nothing.
|
|
80
|
+
}
|
|
81
|
+
return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The files of the granted skills that still exist; a name pi no longer finds is left out. */
|
|
85
|
+
export function skillFiles(names: readonly string[], cwd: string): string[] {
|
|
86
|
+
if (names.length === 0) return [];
|
|
87
|
+
const byName = new Map(grantableSkills(cwd).map((skill) => [skill.name, skill.path]));
|
|
88
|
+
return names.flatMap((name) => byName.get(name) ?? []);
|
|
89
|
+
}
|
package/src/pi/model-settings.ts
CHANGED
|
@@ -24,12 +24,11 @@ export interface EntryView {
|
|
|
24
24
|
model: string;
|
|
25
25
|
thinking?: string;
|
|
26
26
|
instructions?: string;
|
|
27
|
-
timeoutMs?: number;
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
export function entryView(config: BotLobbyConfig, kind: SettingsKind): EntryView {
|
|
31
30
|
if (kind === "master") return config.master;
|
|
32
|
-
if (kind === "scout") return { model: config.scout.model
|
|
31
|
+
if (kind === "scout") return { model: config.scout.model };
|
|
33
32
|
if (kind === "researcher") return config.researcher;
|
|
34
33
|
if (kind === "quickfix") return config.quickFix;
|
|
35
34
|
if (kind === "planner") return config.planner;
|
|
@@ -40,14 +39,13 @@ interface EntryPatch {
|
|
|
40
39
|
model?: string;
|
|
41
40
|
thinking?: string;
|
|
42
41
|
instructions?: string;
|
|
43
|
-
timeoutMs?: number;
|
|
44
42
|
}
|
|
45
43
|
|
|
46
44
|
/** Apply a patch to one entry in a config copy; scouts ignore thinking and instructions. */
|
|
47
45
|
export function patchEntry(config: BotLobbyConfig, kind: SettingsKind, patch: EntryPatch): BotLobbyConfig {
|
|
48
46
|
const next: BotLobbyConfig = { ...config, agents: { ...config.agents } };
|
|
49
47
|
if (kind === "master") next.master = { ...config.master, ...patch } as AgentModelConfig;
|
|
50
|
-
else if (kind === "scout") next.scout = { ...config.scout, model: patch.model ?? config.scout.model
|
|
48
|
+
else if (kind === "scout") next.scout = { ...config.scout, model: patch.model ?? config.scout.model };
|
|
51
49
|
else if (kind === "researcher") next.researcher = { ...config.researcher, ...patch } as AgentModelConfig;
|
|
52
50
|
else if (kind === "quickfix") next.quickFix = { ...config.quickFix, ...patch } as AgentModelConfig;
|
|
53
51
|
else if (kind === "planner") next.planner = { ...config.planner, ...patch } as AgentModelConfig;
|
package/src/pi/model-support.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { clampThinkingLevel, getSupportedThinkingLevels, type Api, type Model, type ModelThinkingLevel } from "@earendil-works/pi-ai";
|
|
2
2
|
import {
|
|
3
3
|
agentProfile,
|
|
4
|
+
expertProfile,
|
|
4
5
|
isThinkingLevel,
|
|
5
6
|
lobbyAgentProfile,
|
|
6
7
|
panelMemberProfile,
|
|
@@ -90,7 +91,7 @@ export function createProfileResolver(config: BotLobbyConfig, options: ResolverO
|
|
|
90
91
|
};
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
type RunProfile = { model?: string; thinking: string;
|
|
94
|
+
type RunProfile = { model?: string; thinking: string; instructions?: string; mcpTools?: string[]; skills?: string[] };
|
|
94
95
|
|
|
95
96
|
/** Unset models run on the session's; thinking is clamped to the model, with a one-time warning. */
|
|
96
97
|
function resolveRunProfile(profile: RunProfile, label: string, options: ResolverOptions): RunProfile {
|
|
@@ -106,23 +107,29 @@ function resolveRunProfile(profile: RunProfile, label: string, options: Resolver
|
|
|
106
107
|
return { ...profile, model, thinking: check.level };
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
/**
|
|
110
|
+
/** The model ask_expert consults on: the expert setting's, else the oracle's, else the session's; thinking clamped to it. */
|
|
111
|
+
export function resolveExpertProfile(config: BotLobbyConfig, options: ResolverOptions): { model?: string; thinking: string } {
|
|
112
|
+
const { model, thinking } = resolveRunProfile(expertProfile(config), "Expert consult", options);
|
|
113
|
+
return { ...(model ? { model } : {}), thinking };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A lobby agent's model, thinking and grants. */
|
|
110
117
|
export function resolveLobbyProfile(config: BotLobbyConfig, kind: LobbyAgentKind, options: ResolverOptions): RunProfile {
|
|
111
118
|
return resolveRunProfile(lobbyAgentProfile(config, kind), kindLabel(kind), options);
|
|
112
119
|
}
|
|
113
120
|
|
|
114
|
-
/** A planning panel seat's model, thinking and
|
|
121
|
+
/** A planning panel seat's model, thinking and grants, from its domain's (or the researcher's) settings. */
|
|
115
122
|
export function resolvePanelProfile(config: BotLobbyConfig, member: PanelMember, options: ResolverOptions): RunProfile {
|
|
116
123
|
return resolveRunProfile(panelMemberProfile(config, member), kindLabel(member), options);
|
|
117
124
|
}
|
|
118
125
|
|
|
119
126
|
/**
|
|
120
|
-
* The model, thinking and
|
|
127
|
+
* The model, thinking and grants a pull request review runs on: QA's, the
|
|
121
128
|
* agent whose job is judging work, with its custom instructions.
|
|
122
129
|
*/
|
|
123
130
|
export function resolveReviewProfile(config: BotLobbyConfig, options: ResolverOptions): RunProfile {
|
|
124
131
|
const profile = agentProfile(config, "qa", "reviewer");
|
|
125
|
-
return resolveRunProfile({ model: profile.model, thinking: profile.thinking,
|
|
132
|
+
return resolveRunProfile({ model: profile.model, thinking: profile.thinking, ...(profile.instructions ? { instructions: profile.instructions } : {}), ...(profile.mcpTools ? { mcpTools: profile.mcpTools } : {}), ...(profile.skills ? { skills: profile.skills } : {}) }, kindLabel("qa"), options);
|
|
126
133
|
}
|
|
127
134
|
|
|
128
135
|
/** Every configured subagent whose thinking level its model does not support, for `/bot-lobby config`. */
|
package/src/pi/route.ts
CHANGED
|
@@ -27,7 +27,7 @@ export const ROUTE_TOOL = "route_request";
|
|
|
27
27
|
/**
|
|
28
28
|
* Hands a request to the lobby's quick-fix agent and shows the Quick fix tab;
|
|
29
29
|
* the job's id, or undefined without a lobby. `builder` is set for a quick
|
|
30
|
-
* feature: it runs on that domain's model, thinking and
|
|
30
|
+
* feature: it runs on that domain's model, thinking and grants.
|
|
31
31
|
*/
|
|
32
32
|
export type QuickFixHandoff = (request: string, builder: Domain | undefined, reason: string) => string | undefined;
|
|
33
33
|
|
package/src/pi/run-summary.ts
CHANGED
|
@@ -51,21 +51,19 @@ const STATUS_ICONS: Record<AgentRun["status"], string> = {
|
|
|
51
51
|
};
|
|
52
52
|
|
|
53
53
|
/** Why a run ended the way it did, in words, when that is not plain success. */
|
|
54
|
-
export function runFlags(run: Pick<AgentRun, "status" | "stalled" | "
|
|
54
|
+
export function runFlags(run: Pick<AgentRun, "status" | "stalled" | "attempts" | "route">): string[] {
|
|
55
55
|
const flags: string[] = [];
|
|
56
56
|
if (run.route) flags.push(`routed ${run.route}`);
|
|
57
|
-
if (run.stalled) flags.push("stalled");
|
|
58
|
-
else if (run.status === "timeout") flags.push("hit its time limit");
|
|
57
|
+
if (run.stalled || run.status === "timeout") flags.push("stalled");
|
|
59
58
|
else if (run.status === "failed") flags.push("failed");
|
|
60
59
|
else if (run.status === "cancelled") flags.push("cancelled");
|
|
61
|
-
if (run.wrappedUp) flags.push(run.status === "success" ? "wrapped up early — report may be partial" : "asked to wrap up");
|
|
62
60
|
if (run.attempts > 1) flags.push(`${run.attempts} attempts`);
|
|
63
61
|
return flags;
|
|
64
62
|
}
|
|
65
63
|
|
|
66
64
|
/**
|
|
67
65
|
* `✓ DEV worker · 3m 12s · 9 turns · 23 tools · 41k↑ 6k↓ · $0.12 · provider/model`,
|
|
68
|
-
* followed by any flags (stalled,
|
|
66
|
+
* followed by any flags (stalled, failed, cancelled, attempts).
|
|
69
67
|
*/
|
|
70
68
|
export function describeRun(run: AgentRun, now = Date.now()): string {
|
|
71
69
|
const parts = [`${STATUS_ICONS[run.status]} ${agentName(run)} ${run.role}`, shortDuration(durationOf(run, now))];
|
|
@@ -101,7 +99,6 @@ export function runLogEntry(run: AgentRun): RunLogEntry {
|
|
|
101
99
|
...(run.usage ? { input: run.usage.input, output: run.usage.output, cost: run.usage.cost } : {}),
|
|
102
100
|
attempts: run.attempts,
|
|
103
101
|
...(run.stalled ? { stalled: true } : {}),
|
|
104
|
-
...(run.wrappedUp ? { wrappedUp: true } : {}),
|
|
105
102
|
...(run.error ? { error: run.error.split("\n")[0]!.slice(0, 200) } : {}),
|
|
106
103
|
...(run.routedFrom ? { routedFrom: run.routedFrom } : {}),
|
|
107
104
|
};
|
|
@@ -129,7 +126,6 @@ export function runFromLog(entry: RunLogEntry, taskId: string): AgentRun {
|
|
|
129
126
|
...(entry.tools ? { tools: entry.tools } : {}),
|
|
130
127
|
...(entry.input !== undefined ? { usage: { input: entry.input, output: entry.output ?? 0, cost: entry.cost ?? 0, turns: entry.turns ?? 0 } } : {}),
|
|
131
128
|
...(entry.stalled ? { stalled: true } : {}),
|
|
132
|
-
...(entry.wrappedUp ? { wrappedUp: true } : {}),
|
|
133
129
|
...(entry.error ? { error: entry.error } : {}),
|
|
134
130
|
};
|
|
135
131
|
}
|
package/src/pi/start-flags.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The flags that lead a task's request: `--task` (always a task), `--auto`,
|
|
3
|
-
* `--fast`, `--full`, `--branch`, `--worktree
|
|
4
|
-
*
|
|
3
|
+
* `--fast`, `--full`, `--branch`, `--worktree` and `--no-branch`. The
|
|
4
|
+
* command parses them; a session
|
|
5
5
|
* started from the lobby is named from what follows them.
|
|
6
6
|
*/
|
|
7
|
-
export const START_FLAG = /^--(task|auto|fast|full|branch|worktree|no-branch)(?=\s|$)\s
|
|
7
|
+
export const START_FLAG = /^--(task|auto|fast|full|branch|worktree|no-branch)(?=\s|$)\s*/;
|
|
8
8
|
|
|
9
9
|
/** The request without its leading flags. */
|
|
10
10
|
export function stripStartFlags(text: string): string {
|
package/src/pi/start-task.ts
CHANGED
|
@@ -21,7 +21,6 @@ import { setAutoMode } from "../state/auto.ts";
|
|
|
21
21
|
import { listPlannedTasks, loadPlannedTask, markPlannedTaskStarted, plannedTaskRequest, type PlannedTask } from "../state/backlog.ts";
|
|
22
22
|
import { triageFor } from "../classifier/instance.ts";
|
|
23
23
|
import { freshContextOn, markContext } from "./fresh-context.ts";
|
|
24
|
-
import { setBudget } from "../state/budget.ts";
|
|
25
24
|
import { createWorkspace, gitLine } from "../execution/workspace.ts";
|
|
26
25
|
import type { GitIsolation } from "../schemas/configuration.ts";
|
|
27
26
|
|
|
@@ -75,7 +74,7 @@ function plannedSteps(task: Task): string[] {
|
|
|
75
74
|
].filter(Boolean);
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
export function kickoff(task: Task,
|
|
77
|
+
export function kickoff(task: Task, options: { fastTrack?: boolean; routed?: boolean } = {}): string {
|
|
79
78
|
const track = task.track;
|
|
80
79
|
const head = [
|
|
81
80
|
`A bot-lobby task is active: ${task.id}`,
|
|
@@ -84,7 +83,6 @@ export function kickoff(task: Task, budgetMinutes = 0, options: { fastTrack?: bo
|
|
|
84
83
|
`State: ${task.state}`,
|
|
85
84
|
...(options.routed ? [ROUTED_LINE] : []),
|
|
86
85
|
...(track ? [trackSummary(track)] : []),
|
|
87
|
-
...(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).`] : []),
|
|
88
86
|
...(task.git ? [gitLine(task.git)] : []),
|
|
89
87
|
"",
|
|
90
88
|
];
|
|
@@ -135,8 +133,6 @@ export interface StartOptions {
|
|
|
135
133
|
auto?: boolean;
|
|
136
134
|
/** The task's short title (a planned task's own title); derived from the request otherwise. */
|
|
137
135
|
title?: string;
|
|
138
|
-
/** Minutes of work time the task gets; `workflow.taskBudgetMinutes` when absent. */
|
|
139
|
-
budget?: number;
|
|
140
136
|
/** The user's `--fast` or `--full`: the path the task takes, whatever its request reads as. */
|
|
141
137
|
track?: TrackPath;
|
|
142
138
|
/** The classifier already read the request (routing did): `triage` is its read, or absent when it was off. */
|
|
@@ -177,8 +173,6 @@ export async function startTask(pi: ExtensionAPI, ctx: ExtensionContext, configD
|
|
|
177
173
|
if (options.approvedPlan && options.plan) startFromAgreedPlan(task, options.plan, taskDirFor(root, configDir, task.id));
|
|
178
174
|
await giveWorkspace(ctx, root, configDir, task, options.isolation ?? config.workflow.gitIsolation);
|
|
179
175
|
saveTask(root, configDir, task);
|
|
180
|
-
const budgetMinutes = options.budget ?? config.workflow.taskBudgetMinutes;
|
|
181
|
-
if (budgetMinutes > 0) setBudget(root, configDir, task.id, budgetMinutes);
|
|
182
176
|
if (options.auto) setAutoMode(root, configDir, task.id, true, sessionId);
|
|
183
177
|
// A session that starts a task is named after it (its friendly name), so /resume and the lobby list it by name.
|
|
184
178
|
if (!pi.getSessionName() || pi.getSessionName() === taskName(request)) pi.setSessionName(task.id);
|
|
@@ -188,7 +182,7 @@ export async function startTask(pi: ExtensionAPI, ctx: ExtensionContext, configD
|
|
|
188
182
|
// The oracle takes the task on with a clean context: nothing said before the kickoff is sent to its model.
|
|
189
183
|
if (freshContextOn()) markContext(pi, { kind: "start", taskId: task.id, at: Date.now() });
|
|
190
184
|
// A kickoff while pi is still busy (another turn) queues behind it instead of throwing.
|
|
191
|
-
pi.sendUserMessage(kickoff(task,
|
|
185
|
+
pi.sendUserMessage(kickoff(task, { fastTrack: config.workflow.fastTrack, ...(options.routed ? { routed: true } : {}) }), ctx.isIdle() ? undefined : { deliverAs: "followUp" });
|
|
192
186
|
return task;
|
|
193
187
|
}
|
|
194
188
|
|
package/src/pi/tools.ts
CHANGED
|
@@ -8,12 +8,12 @@ import { detectProjectRoot, loadConfig } from "../state/project.ts";
|
|
|
8
8
|
import { classifier, effortFor, hintsFor, knowledgeFor, triageFor } from "../classifier/instance.ts";
|
|
9
9
|
import { truncate } from "../text.ts";
|
|
10
10
|
import { applyStatus, reportRuns, summarizeRun } from "./ui.ts";
|
|
11
|
-
import { whileAsking } from "../state/
|
|
11
|
+
import { whileAsking } from "../state/work-clock.ts";
|
|
12
12
|
import { askChoice, askText, askUser, canAsk } from "../ask/web.ts";
|
|
13
13
|
import type { AskQuestion } from "../ask/types.ts";
|
|
14
14
|
import { unescapeBreaks } from "../lobby/blocks.ts";
|
|
15
15
|
import { isQuiet } from "./quiet.ts";
|
|
16
|
-
import { checkThinking, createProfileResolver, modelRef, type ModelLookup } from "./model-support.ts";
|
|
16
|
+
import { checkThinking, createProfileResolver, modelRef, resolveExpertProfile, type ModelLookup } from "./model-support.ts";
|
|
17
17
|
import { agentName, describeRun } from "./run-summary.ts";
|
|
18
18
|
import { lintFeed } from "../workflow/lint.ts";
|
|
19
19
|
import { qaRiskLine } from "../classifier/qa-risk.ts";
|
|
@@ -43,18 +43,16 @@ const OrchestrateSchema = Type.Object({
|
|
|
43
43
|
Type.Object({
|
|
44
44
|
domain: Type.String({ description: "designer, backend, or qa" }),
|
|
45
45
|
task: Type.String({ description: "a self-contained brief for that domain's worker: Goal, exact Files, numbered What to do, Contracts (in full), Constraints, Done when, If stuck. No assumptions" }),
|
|
46
|
-
minutes: Type.Optional(Type.Number({ description: "under a time budget: minutes for this worker, by its scope" })),
|
|
47
46
|
}),
|
|
48
47
|
{ description: "implement: run several domains in parallel (distinct domains); workers share files through the file desk" },
|
|
49
48
|
),
|
|
50
49
|
),
|
|
51
|
-
minutes: Type.Optional(Type.Number({ description: "implement (under a time budget): minutes for this step, by its scope; budget: more minutes to ask the user for" })),
|
|
52
50
|
approvalId: Type.Optional(Type.String({ description: "resolve_approval: the approval id from a worker result" })),
|
|
53
51
|
decision: Type.Optional(
|
|
54
52
|
StringEnum(["approved", "rejected"] as const, { description: "resolve_approval: approve or reject the request" }),
|
|
55
53
|
),
|
|
56
54
|
note: Type.Optional(Type.String({ description: "resolve_approval: rationale, or what to do instead when rejected" })),
|
|
57
|
-
reason: Type.Optional(Type.String({ description: "block: why the task cannot continue;
|
|
55
|
+
reason: Type.Optional(Type.String({ description: "block: why the task cannot continue; track: why the task takes that path or needs those members; complete: why gaps the goal check named are deliberate (the user agreed)" })),
|
|
58
56
|
track: Type.Optional(
|
|
59
57
|
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)" }),
|
|
60
58
|
),
|
|
@@ -81,8 +79,8 @@ const DESCRIPTION = [
|
|
|
81
79
|
"review), knowledge (record approved project knowledge or a decision; works without a task),",
|
|
82
80
|
"compact (replace a knowledge file with a rewritten version, archiving the old one; works without a task; disperse domain-relevant facts to action=knowledge (domain=designer|backend|qa) first), script (save a script to the shared scripts library, or replace one, for a job that comes back; works without a task),",
|
|
83
81
|
"resolve_approval (approve or reject a request), complete (declare the task done after the gates",
|
|
84
|
-
"pass, then ask the user to confirm merging and pushing main and removing its worktree), block/resume (escalate or continue),
|
|
85
|
-
"
|
|
82
|
+
"pass, then ask the user to confirm merging and pushing main and removing its worktree), block/resume (escalate or continue),",
|
|
83
|
+
"track (the task's path and who takes part: show it, or correct it with",
|
|
86
84
|
"track=fast|full, roster and a reason), status, cancel.",
|
|
87
85
|
"Every instruction you give an agent is read by a possibly smaller, cheaper model that cannot infer intent: write each one as a complete, explicit brief with the goal, files, actions, contracts, constraints and done criteria, then check the report against it.",
|
|
88
86
|
"The engine validates every step against the task state machine, so a rejected action means the workflow is not at that step yet.",
|
|
@@ -121,6 +119,7 @@ export function workflowDeps(
|
|
|
121
119
|
sessionModel: ctx.model ? modelRef(ctx.model) : undefined,
|
|
122
120
|
warn,
|
|
123
121
|
}),
|
|
122
|
+
expert: resolveExpertProfile(config, { lookup: modelLookup(ctx), sessionModel: ctx.model ? modelRef(ctx.model) : undefined, warn }),
|
|
124
123
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
125
124
|
signal,
|
|
126
125
|
onUpdate,
|
|
@@ -130,7 +129,7 @@ export function workflowDeps(
|
|
|
130
129
|
classifier: classifier(),
|
|
131
130
|
triage: (request, triageSignal) => triageFor({ cwd: ctx.cwd, root, configDir }, request, triageSignal),
|
|
132
131
|
effort: effortFor((model, thinking) => checkThinking(modelLookup(ctx)(model), thinking).level),
|
|
133
|
-
// Time spent waiting on the user is not the task's: its
|
|
132
|
+
// Time spent waiting on the user is not the task's: its work clock waits too.
|
|
134
133
|
// The questions go to the web page, where a free-text answer may run to several lines.
|
|
135
134
|
ask: async (question) => (canAsk() ? whileAsking(() => askText(question, undefined, ctx)) : undefined),
|
|
136
135
|
choose: async (title, options) => (canAsk() ? whileAsking(() => askChoice(title, options, undefined, ctx)) : undefined),
|
|
@@ -189,14 +188,13 @@ function registerBotLobbyEntries(pi: ExtensionAPI): void {
|
|
|
189
188
|
});
|
|
190
189
|
}
|
|
191
190
|
|
|
192
|
-
/** A transcript line per finished run, plus a warning for anything that stalled
|
|
191
|
+
/** A transcript line per finished run, plus a warning for anything that stalled. */
|
|
193
192
|
function reportFinishedRuns(pi: ExtensionAPI, ctx: ExtensionContext, result: WorkflowResult): void {
|
|
194
193
|
for (const run of result.runs ?? []) {
|
|
195
|
-
const ok = run.status === "success"
|
|
194
|
+
const ok = run.status === "success";
|
|
196
195
|
pi.appendEntry("bot-lobby", { kind: "run", taskId: result.taskId, text: describeRun(run), ok });
|
|
197
196
|
if (!ctx.hasUI) continue;
|
|
198
|
-
if (run.stalled) ctx.ui.notify(`bot-lobby: ${agentName(run)} ${run.role} stalled — ${run.error ?? "no output"}`, "warning");
|
|
199
|
-
else if (run.status === "timeout") ctx.ui.notify(`bot-lobby: ${agentName(run)} ${run.role} ${run.error ?? "hit its time limit"}`, "warning");
|
|
197
|
+
if (run.stalled || run.status === "timeout") ctx.ui.notify(`bot-lobby: ${agentName(run)} ${run.role} stalled — ${run.error ?? "no output"}`, "warning");
|
|
200
198
|
}
|
|
201
199
|
}
|
|
202
200
|
|
package/src/roles/worker.ts
CHANGED
|
@@ -26,8 +26,6 @@ export const workerSpec: RoleSpec = {
|
|
|
26
26
|
"the matching section instead. Do not narrate. Report only what you actually changed and verified.",
|
|
27
27
|
"An optional `## Pushback` (`**Request:**`, `**Reason:**`, optional `**Alternative:**`) states a change you",
|
|
28
28
|
"believe is wrong, with your reason, instead of doing it; still complete everything else you safely can.",
|
|
29
|
-
"Stopped because your time is up, also add `## Left Off` (what you were doing, what is still to do) and",
|
|
30
|
-
"`## More Time` (`N minutes — why`).",
|
|
31
29
|
].join(" "),
|
|
32
30
|
};
|
|
33
31
|
|
|
@@ -80,15 +78,6 @@ export function parseBlockers(
|
|
|
80
78
|
];
|
|
81
79
|
}
|
|
82
80
|
|
|
83
|
-
/** `## More Time` as the minutes asked for (`15 minutes`, `1h`, `20m`) and the reason given. */
|
|
84
|
-
export function parseMoreTime(text: string): { minutes?: number; reason: string } {
|
|
85
|
-
const flat = text.replace(/^[-*]\s*/gm, "").replace(/\s+/g, " ").trim();
|
|
86
|
-
const match = /(\d+(?:\.\d+)?)\s*(h|hrs?|hours?|m|mins?|minutes?)\b/i.exec(flat);
|
|
87
|
-
const minutes = match ? Math.round(Number(match[1]) * (/^h/i.test(match[2]!) ? 60 : 1)) : undefined;
|
|
88
|
-
const reason = match ? flat.slice(match.index + match[0].length).replace(/^\s*(?:—|–|-|:|,|because|to)?\s*/i, "").trim() : flat;
|
|
89
|
-
return { ...(minutes && minutes > 0 ? { minutes } : {}), reason: reason || flat };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
81
|
/** A bullet that says there is nothing: `None.`, `N/A`, `No new dependencies (three.js from a CDN)`. */
|
|
93
82
|
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;
|
|
94
83
|
|
|
@@ -104,7 +93,6 @@ export function parseWorkerResult(domain: Domain, raw: string, now = new Date().
|
|
|
104
93
|
domain,
|
|
105
94
|
role: "worker",
|
|
106
95
|
completed: findSection(sections, "completed") ?? "",
|
|
107
|
-
...leftOffFields(sections),
|
|
108
96
|
filesChanged: bullets(findSection(sections, "files changed")).map((entry): FileChange => {
|
|
109
97
|
const file = parseFileBullet(entry);
|
|
110
98
|
return { path: file.path, change: file.reason };
|
|
@@ -121,12 +109,6 @@ export function parseWorkerResult(domain: Domain, raw: string, now = new Date().
|
|
|
121
109
|
};
|
|
122
110
|
}
|
|
123
111
|
|
|
124
|
-
function leftOffFields(sections: Map<string, string>): Pick<WorkerResult, "leftOff" | "moreTime"> {
|
|
125
|
-
const leftOff = findSection(sections, "left off")?.trim();
|
|
126
|
-
const more = findSection(sections, "more time")?.trim();
|
|
127
|
-
return { ...(leftOff ? { leftOff } : {}), ...(more ? { moreTime: parseMoreTime(more) } : {}) };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
112
|
/** Report contract deviations so the Master does not accept unverified work. */
|
|
131
113
|
export function validateWorkerResult(result: WorkerResult): string[] {
|
|
132
114
|
const issues: string[] = [];
|