@a-t-h-i/bot-lobby 0.4.0 → 0.5.0
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 +140 -7
- package/package.json +1 -1
- package/prompts/master.md +12 -0
- package/prompts/panel.md +39 -0
- package/prompts/planner.md +64 -0
- package/prompts/quickfix.md +41 -0
- package/src/execution/agent-runner.ts +7 -2
- package/src/execution/pi-runner.ts +25 -0
- package/src/index.ts +3 -0
- package/src/lobby/feed.ts +253 -0
- package/src/lobby/issues.ts +227 -0
- package/src/lobby/layout.ts +174 -0
- package/src/lobby/planner.ts +474 -0
- package/src/lobby/quickfix.ts +227 -0
- package/src/lobby/runtime.ts +440 -0
- package/src/lobby/tabs/home.ts +164 -0
- package/src/lobby/tabs/issues.ts +72 -0
- package/src/lobby/tabs/metrics.ts +162 -0
- package/src/lobby/tabs/plan.ts +160 -0
- package/src/lobby/tabs/quickfix.ts +101 -0
- package/src/lobby/tabs/tasks.ts +209 -0
- package/src/lobby/view.ts +855 -0
- package/src/pi/activity.ts +120 -0
- package/src/pi/commands.ts +19 -57
- package/src/pi/events.ts +5 -2
- package/src/pi/model-support.ts +34 -0
- package/src/pi/run-summary.ts +2 -0
- package/src/pi/settings-ui.ts +4 -0
- package/src/pi/start-task.ts +63 -0
- package/src/pi/tools.ts +2 -2
- package/src/pi/ui.ts +116 -55
- package/src/schemas/configuration.ts +68 -3
- package/src/schemas/findings.ts +6 -0
- package/src/schemas/task.ts +1 -0
- package/src/state/backlog.ts +106 -0
- package/src/state/comments.ts +136 -0
- package/src/state/metrics.ts +305 -0
- package/src/workflow/workflow.ts +39 -7
package/src/pi/activity.ts
CHANGED
|
@@ -106,3 +106,123 @@ export function activityDetail(toolName: string, args?: unknown): string | undef
|
|
|
106
106
|
const query = field(args, "query", "url", "path");
|
|
107
107
|
return query ? clip(query) : undefined;
|
|
108
108
|
}
|
|
109
|
+
|
|
110
|
+
const STEP_CHARS = 60;
|
|
111
|
+
|
|
112
|
+
function clipTo(text: string, max: number): string {
|
|
113
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
114
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** `https://example.com/docs/page?q=1` reads `example.com/docs/page`. */
|
|
118
|
+
function shortUrl(url: string): string {
|
|
119
|
+
return url.replace(/^[a-z]+:\/\//i, "").replace(/[?#].*$/, "").replace(/\/$/, "");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function list(value: unknown): string | undefined {
|
|
123
|
+
if (!Array.isArray(value)) return undefined;
|
|
124
|
+
const items = value.filter((item): item is string => typeof item === "string" && item.trim().length > 0);
|
|
125
|
+
return items.length > 0 ? items.join(", ") : undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** What the Master is doing with one `orchestrate` call, in words. */
|
|
129
|
+
function orchestrateStep(args: unknown): string {
|
|
130
|
+
const action = orchestrateAction(args);
|
|
131
|
+
const text = (...names: string[]) => field(args, ...names);
|
|
132
|
+
switch (action) {
|
|
133
|
+
case "clarify":
|
|
134
|
+
return `asking you: ${clipTo(text("question") ?? "a question", STEP_CHARS)}`;
|
|
135
|
+
case "scout":
|
|
136
|
+
return `scouting ${list((args as { domains?: unknown }).domains) ?? "the codebase"}`;
|
|
137
|
+
case "research":
|
|
138
|
+
return `summoning research${text("instruction") ? ` on ${clipTo(text("instruction")!, STEP_CHARS - 20)}` : ""}`;
|
|
139
|
+
case "propose":
|
|
140
|
+
return "proposing the change for your approval";
|
|
141
|
+
case "plan":
|
|
142
|
+
return "writing the plan";
|
|
143
|
+
case "implement": {
|
|
144
|
+
const assignments = (args as { assignments?: Array<{ domain?: unknown }> }).assignments;
|
|
145
|
+
const domains = Array.isArray(assignments) ? list(assignments.map((entry) => entry?.domain)) : text("domain");
|
|
146
|
+
const task = text("task");
|
|
147
|
+
return `delegating to ${domains ?? "a worker"}${task ? `: ${clipTo(task, STEP_CHARS - 20)}` : ""}`;
|
|
148
|
+
}
|
|
149
|
+
case "qa":
|
|
150
|
+
return "running the QA gate";
|
|
151
|
+
case "knowledge":
|
|
152
|
+
return "recording knowledge";
|
|
153
|
+
case "compact":
|
|
154
|
+
return `compacting ${text("file") ?? "knowledge"}`;
|
|
155
|
+
case "resolve_approval":
|
|
156
|
+
return `${text("decision") === "rejected" ? "rejecting" : "approving"} ${text("approvalId") ?? "a request"}`;
|
|
157
|
+
case "complete":
|
|
158
|
+
return "wrapping up the task";
|
|
159
|
+
case "block":
|
|
160
|
+
return `blocking the task${text("reason") ? `: ${clipTo(text("reason")!, STEP_CHARS - 20)}` : ""}`;
|
|
161
|
+
case "resume":
|
|
162
|
+
return "resuming the task";
|
|
163
|
+
case "decide":
|
|
164
|
+
return "recording a decision";
|
|
165
|
+
case "status":
|
|
166
|
+
return "checking the task status";
|
|
167
|
+
case "cancel":
|
|
168
|
+
return "cancelling the task";
|
|
169
|
+
default:
|
|
170
|
+
return "orchestrating";
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* One plain-words line for a tool call, the way a person would narrate it:
|
|
176
|
+
* `reading index.html`, `searching for "router" in src`, `running npm test`,
|
|
177
|
+
* `delegating to backend: Step 2 …`. Pure; unknown tools read `using <name>`.
|
|
178
|
+
*/
|
|
179
|
+
export function describeToolCall(toolName: string, args?: unknown): string {
|
|
180
|
+
const tool = toolName.trim().toLowerCase();
|
|
181
|
+
const path = field(args, "path", "file_path", "file");
|
|
182
|
+
switch (tool) {
|
|
183
|
+
case "read":
|
|
184
|
+
return `reading ${path ? baseName(path) : "a file"}`;
|
|
185
|
+
case "edit":
|
|
186
|
+
return `editing ${path ? baseName(path) : "a file"}`;
|
|
187
|
+
case "write":
|
|
188
|
+
return `writing ${path ? baseName(path) : "a file"}`;
|
|
189
|
+
case "ls":
|
|
190
|
+
return `listing ${path ? clipTo(path, STEP_CHARS) : "the folder"}`;
|
|
191
|
+
case "grep": {
|
|
192
|
+
const pattern = field(args, "pattern", "query");
|
|
193
|
+
return `searching for ${pattern ? `"${clipTo(pattern, 40)}"` : "a pattern"}${path ? ` in ${baseName(path)}` : ""}`;
|
|
194
|
+
}
|
|
195
|
+
case "find": {
|
|
196
|
+
const pattern = field(args, "pattern", "glob", "query");
|
|
197
|
+
return `finding ${pattern ? clipTo(pattern, 40) : "files"}${path ? ` in ${baseName(path)}` : ""}`;
|
|
198
|
+
}
|
|
199
|
+
case "bash": {
|
|
200
|
+
const command = field(args, "command", "cmd");
|
|
201
|
+
return command ? `running ${clipTo(command, STEP_CHARS)}` : "running a command";
|
|
202
|
+
}
|
|
203
|
+
case "web_search":
|
|
204
|
+
return `searching the web for "${clipTo(field(args, "query") ?? "…", 48)}"`;
|
|
205
|
+
case "fetch_content":
|
|
206
|
+
case "web_fetch":
|
|
207
|
+
case "fetch": {
|
|
208
|
+
const url = field(args, "url");
|
|
209
|
+
return `fetching ${url ? clipTo(shortUrl(url), STEP_CHARS) : "a page"}`;
|
|
210
|
+
}
|
|
211
|
+
case "get_search_content":
|
|
212
|
+
return "reading search results";
|
|
213
|
+
case "source_check":
|
|
214
|
+
return "checking sources";
|
|
215
|
+
case "claim_file":
|
|
216
|
+
return `claiming ${path ? baseName(path) : "a file"}`;
|
|
217
|
+
case "handover_file":
|
|
218
|
+
return `handing over ${path ? baseName(path) : "a file"}`;
|
|
219
|
+
case "wait_for_files":
|
|
220
|
+
return "waiting for files";
|
|
221
|
+
case "my_files":
|
|
222
|
+
return "checking its files";
|
|
223
|
+
case "orchestrate":
|
|
224
|
+
return orchestrateStep(args);
|
|
225
|
+
default:
|
|
226
|
+
return `using ${toolName.trim() || "a tool"}`;
|
|
227
|
+
}
|
|
228
|
+
}
|
package/src/pi/commands.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
1
|
import { join } from "node:path";
|
|
3
2
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { TERMINAL_STATES,
|
|
3
|
+
import { TERMINAL_STATES, type Task } from "../schemas/task.ts";
|
|
5
4
|
import { detectProjectRoot, globalConfigPath, loadConfig, readDataRoots, readRawConfig } from "../state/project.ts";
|
|
6
5
|
import { hasScoutThinking, SCOUT_THINKING } from "../schemas/configuration.ts";
|
|
7
6
|
import {
|
|
@@ -9,12 +8,8 @@ import {
|
|
|
9
8
|
claimTask,
|
|
10
9
|
ownedTask,
|
|
11
10
|
ownerlessTask,
|
|
12
|
-
createTaskDir,
|
|
13
|
-
ensureProjectStructure,
|
|
14
|
-
nextTaskId,
|
|
15
11
|
saveTask,
|
|
16
12
|
selectTask,
|
|
17
|
-
taskDirFor,
|
|
18
13
|
taskHealth,
|
|
19
14
|
} from "../state/persistence.ts";
|
|
20
15
|
import { transition } from "../state/task-state.ts";
|
|
@@ -22,14 +17,16 @@ import { AGENT_DIR_NAMES, KNOWLEDGE_FILES, knowledgeDir, type KnowledgeAgent } f
|
|
|
22
17
|
import { readFirstExisting } from "../knowledge/store.ts";
|
|
23
18
|
import { overThreshold } from "../knowledge/compactor.ts";
|
|
24
19
|
import { applyApprovalChoice, describeTask, describeOversizedKnowledge, type ApprovalChoice } from "../workflow/workflow.ts";
|
|
25
|
-
import { shortTitle } from "../text.ts";
|
|
26
20
|
import { applyStatus, registerRevealShortcut, setMinimized } from "./ui.ts";
|
|
27
|
-
import {
|
|
21
|
+
import { openSettings } from "./settings-ui.ts";
|
|
22
|
+
import { kickoff, startTask } from "./start-task.ts";
|
|
23
|
+
import { autoOpenLobby, showLobby } from "../lobby/runtime.ts";
|
|
28
24
|
import { modelRef, thinkingMismatches } from "./model-support.ts";
|
|
29
25
|
import { describeRun, runFromLog } from "./run-summary.ts";
|
|
30
26
|
import { modelLookup } from "./tools.ts";
|
|
31
27
|
|
|
32
28
|
const HELP = [
|
|
29
|
+
"/bot-lobby Open the lobby: tasks, plan, quick fix, issues, metrics (alt+l)",
|
|
33
30
|
"/bot-lobby <request> Start a task through the workflow",
|
|
34
31
|
"/bot-lobby status [taskId] Show the active task",
|
|
35
32
|
"/bot-lobby tasks List tasks",
|
|
@@ -42,10 +39,11 @@ const HELP = [
|
|
|
42
39
|
"/bot-lobby config Show effective configuration",
|
|
43
40
|
"/bot-lobby minimize|restore Hide or restore bot-lobby for this session (ctrl+shift+m)",
|
|
44
41
|
"/bot-lobby claim <taskId> Take ownership of an orphaned task",
|
|
42
|
+
"/bot-lobby help This help",
|
|
45
43
|
].join("\n");
|
|
46
44
|
|
|
47
45
|
/** Subcommands only win when no free-form text follows (so tasks still start). */
|
|
48
|
-
const SUBCOMMANDS = new Set(["status", "runs", "tasks", "pause", "resume", "cancel", "approve", "amend", "decline", "knowledge", "config", "settings", "minimize", "restore", "claim"]);
|
|
46
|
+
const SUBCOMMANDS = new Set(["lobby", "help", "status", "runs", "tasks", "pause", "resume", "cancel", "approve", "amend", "decline", "knowledge", "config", "settings", "minimize", "restore", "claim"]);
|
|
49
47
|
|
|
50
48
|
function isTaskId(value: string | undefined): boolean {
|
|
51
49
|
return Boolean(value && /^TASK-/.test(value));
|
|
@@ -61,52 +59,7 @@ function parseCommand(args: string): { sub: string | undefined; rest: string[];
|
|
|
61
59
|
return { sub, rest, restText: trimmed.slice(sub.length).trim() };
|
|
62
60
|
}
|
|
63
61
|
|
|
64
|
-
|
|
65
|
-
const base = nextTaskId(request);
|
|
66
|
-
let id = base;
|
|
67
|
-
let suffix = 2;
|
|
68
|
-
while (existsSync(taskDirFor(root, configDir, id))) id = `${base}-${suffix++}`;
|
|
69
|
-
return id;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function kickoff(task: Task): string {
|
|
73
|
-
return [
|
|
74
|
-
`A bot-lobby task is active: ${task.id}`,
|
|
75
|
-
`Title: ${task.title}`,
|
|
76
|
-
`Request: ${taskRequest(task)}`,
|
|
77
|
-
`State: ${task.state}`,
|
|
78
|
-
"",
|
|
79
|
-
"Drive it with the orchestrate tool:",
|
|
80
|
-
"1. clarify if the request is genuinely ambiguous,",
|
|
81
|
-
"2. scout the domains the request touches,",
|
|
82
|
-
"3. synthesize the findings and propose a short `- ` bullet list for approval.",
|
|
83
|
-
"Do not implement anything before the user approves the proposal.",
|
|
84
|
-
].join("\n");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function startTask(
|
|
88
|
-
pi: ExtensionAPI,
|
|
89
|
-
ctx: ExtensionCommandContext,
|
|
90
|
-
configDir: string,
|
|
91
|
-
request: string,
|
|
92
|
-
): Promise<void> {
|
|
93
|
-
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
94
|
-
ensureProjectStructure(root, configDir);
|
|
95
|
-
const sessionId = ctx.sessionManager.getSessionId();
|
|
96
|
-
const existing = ownedTask(root, configDir, sessionId);
|
|
97
|
-
if (existing) {
|
|
98
|
-
ctx.ui.notify(`bot-lobby ${existing.id} is already active in this session. Finish it or run /bot-lobby cancel ${existing.id} first.`, "warning");
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
const task = createTask(uniqueTaskId(root, configDir, request), shortTitle(request), new Date().toISOString(), request, sessionId);
|
|
102
|
-
createTaskDir(root, configDir, task);
|
|
103
|
-
transition(task, "clarifying");
|
|
104
|
-
saveTask(root, configDir, task);
|
|
105
|
-
applyStatus(ctx, root, configDir);
|
|
106
|
-
await applyMasterModel(pi, ctx, loadConfig());
|
|
107
|
-
ctx.ui.notify(`bot-lobby ${task.id} started`, "info");
|
|
108
|
-
pi.sendUserMessage(kickoff(task));
|
|
109
|
-
}
|
|
62
|
+
export { kickoff };
|
|
110
63
|
|
|
111
64
|
function showStatus(ctx: ExtensionCommandContext, configDir: string, taskId?: string): void {
|
|
112
65
|
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
@@ -246,10 +199,19 @@ export function registerCommands(pi: ExtensionAPI, configDir: string): void {
|
|
|
246
199
|
handler: async (args, ctx) => {
|
|
247
200
|
const { sub, rest, restText } = parseCommand(args ?? "");
|
|
248
201
|
if (!sub) {
|
|
249
|
-
if (!restText)
|
|
250
|
-
|
|
202
|
+
if (!restText) {
|
|
203
|
+
if (!showLobby()) ctx.ui.notify(HELP, "info");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (await startTask(pi, ctx, configDir, restText)) autoOpenLobby();
|
|
207
|
+
return;
|
|
251
208
|
}
|
|
252
209
|
switch (sub) {
|
|
210
|
+
case "lobby":
|
|
211
|
+
if (!showLobby()) ctx.ui.notify("The lobby needs pi's interactive terminal UI.", "warning");
|
|
212
|
+
return;
|
|
213
|
+
case "help":
|
|
214
|
+
return ctx.ui.notify(HELP, "info");
|
|
253
215
|
case "status":
|
|
254
216
|
return showStatus(ctx, configDir, rest[0]);
|
|
255
217
|
case "tasks":
|
package/src/pi/events.ts
CHANGED
|
@@ -12,14 +12,17 @@ import { ORACLE_THINKING, oracleActivityWord } from "./activity.ts";
|
|
|
12
12
|
import { isSubagentProcess, visibleTools } from "./quiet.ts";
|
|
13
13
|
import { registerQuietTools } from "./tool-renderers.ts";
|
|
14
14
|
import { taskRequest, type Task } from "../schemas/task.ts";
|
|
15
|
+
import { pendingComments, readPlanComments, type PlanComment } from "../state/comments.ts";
|
|
15
16
|
|
|
16
|
-
function masterTaskContext(task: Task): string {
|
|
17
|
+
export function masterTaskContext(task: Task, comments: readonly PlanComment[] = []): string {
|
|
18
|
+
const open = pendingComments(comments);
|
|
17
19
|
return [
|
|
18
20
|
`Task ${task.id}: ${task.title}`,
|
|
19
21
|
`Request: ${truncate(taskRequest(task), 2000)}`,
|
|
20
22
|
task.proposal ? `Current proposal:\n${truncate(task.proposal, 2000)}` : "",
|
|
21
23
|
task.plan ? `Approved plan:\n${truncate(task.plan, 3000)}` : "",
|
|
22
24
|
task.amendments.length > 0 ? `User amendments:\n${task.amendments.map((entry) => `- ${entry}`).join("\n")}` : "",
|
|
25
|
+
open.length > 0 ? `Open plan comments (from the lobby):\n${open.map((comment) => `- ${truncate(comment.text, 600)}`).join("\n")}` : "",
|
|
23
26
|
]
|
|
24
27
|
.filter((line) => line.length > 0)
|
|
25
28
|
.join("\n\n");
|
|
@@ -86,7 +89,7 @@ export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
|
86
89
|
const selected = selectKnowledge(`${taskRequest(task)} ${task.proposal ?? ""}`, slices);
|
|
87
90
|
event.systemPromptOptions.sections["bot-lobby"] = compilePrompt({
|
|
88
91
|
domain: "master",
|
|
89
|
-
task: masterTaskContext(task),
|
|
92
|
+
task: masterTaskContext(task, readPlanComments(root, configDir, task.id)),
|
|
90
93
|
standards: selected.standards,
|
|
91
94
|
knowledge: selected.knowledge,
|
|
92
95
|
decisions: selected.decisions,
|
package/src/pi/model-support.ts
CHANGED
|
@@ -2,6 +2,10 @@ import { clampThinkingLevel, getSupportedThinkingLevels, type Api, type Model, t
|
|
|
2
2
|
import {
|
|
3
3
|
agentProfile,
|
|
4
4
|
isThinkingLevel,
|
|
5
|
+
lobbyAgentProfile,
|
|
6
|
+
panelMemberProfile,
|
|
7
|
+
type LobbyAgentKind,
|
|
8
|
+
type PanelMember,
|
|
5
9
|
THINKING_LEVELS,
|
|
6
10
|
type BotLobbyConfig,
|
|
7
11
|
type ProfileResolver,
|
|
@@ -47,6 +51,8 @@ const LABELS: Record<SubagentKind | "master", string> = {
|
|
|
47
51
|
qa: "QA",
|
|
48
52
|
scout: "Scout",
|
|
49
53
|
researcher: "Researcher",
|
|
54
|
+
quickfix: "Quick fix",
|
|
55
|
+
planner: "Planner",
|
|
50
56
|
};
|
|
51
57
|
|
|
52
58
|
export function kindLabel(kind: SubagentKind | "master"): string {
|
|
@@ -84,6 +90,32 @@ export function createProfileResolver(config: BotLobbyConfig, options: ResolverO
|
|
|
84
90
|
};
|
|
85
91
|
}
|
|
86
92
|
|
|
93
|
+
type RunProfile = { model?: string; thinking: string; timeoutMs: number; instructions?: string };
|
|
94
|
+
|
|
95
|
+
/** Unset models run on the session's; thinking is clamped to the model, with a one-time warning. */
|
|
96
|
+
function resolveRunProfile(profile: RunProfile, label: string, options: ResolverOptions): RunProfile {
|
|
97
|
+
const model = profile.model ?? options.sessionModel;
|
|
98
|
+
const check = checkThinking(model ? options.lookup(model) : undefined, profile.thinking);
|
|
99
|
+
if (check.warning) {
|
|
100
|
+
const message = `bot-lobby: ${label} — ${check.warning}. Change it in /bot-lobby settings.`;
|
|
101
|
+
if (!warned.has(message)) {
|
|
102
|
+
warned.add(message);
|
|
103
|
+
options.warn?.(message);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { ...profile, model, thinking: check.level };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** A lobby agent's model, thinking and time limit. */
|
|
110
|
+
export function resolveLobbyProfile(config: BotLobbyConfig, kind: LobbyAgentKind, options: ResolverOptions): RunProfile {
|
|
111
|
+
return resolveRunProfile(lobbyAgentProfile(config, kind), kindLabel(kind), options);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** A planning panel seat's model, thinking and time limit, from its domain's (or the researcher's) settings. */
|
|
115
|
+
export function resolvePanelProfile(config: BotLobbyConfig, member: PanelMember, options: ResolverOptions): RunProfile {
|
|
116
|
+
return resolveRunProfile(panelMemberProfile(config, member), kindLabel(member), options);
|
|
117
|
+
}
|
|
118
|
+
|
|
87
119
|
/** Every configured subagent whose thinking level its model does not support, for `/bot-lobby config`. */
|
|
88
120
|
export function thinkingMismatches(config: BotLobbyConfig, lookup: ModelLookup, sessionModel?: string): string[] {
|
|
89
121
|
const entries: Array<[SubagentKind | "master", string, string]> = [
|
|
@@ -92,6 +124,8 @@ export function thinkingMismatches(config: BotLobbyConfig, lookup: ModelLookup,
|
|
|
92
124
|
["backend", config.agents.backend.model, config.agents.backend.thinking],
|
|
93
125
|
["qa", config.agents.qa.model, config.agents.qa.thinking],
|
|
94
126
|
["researcher", config.researcher.model, config.researcher.thinking],
|
|
127
|
+
["quickfix", config.quickFix.model, config.quickFix.thinking],
|
|
128
|
+
["planner", config.planner.model, config.planner.thinking],
|
|
95
129
|
];
|
|
96
130
|
return entries.flatMap(([kind, ref, level]) => {
|
|
97
131
|
const model = ref === "inherit" ? sessionModel : ref;
|
package/src/pi/run-summary.ts
CHANGED
|
@@ -80,6 +80,7 @@ export function runLogEntry(run: AgentRun): RunLogEntry {
|
|
|
80
80
|
startedAt: run.startedAt,
|
|
81
81
|
...(run.finishedAt ? { finishedAt: run.finishedAt } : {}),
|
|
82
82
|
...(run.model ? { model: run.model } : {}),
|
|
83
|
+
...(run.thinking ? { thinking: run.thinking } : {}),
|
|
83
84
|
...(run.turns ?? run.usage?.turns ? { turns: run.turns ?? run.usage?.turns } : {}),
|
|
84
85
|
...(run.tools ? { tools: run.tools } : {}),
|
|
85
86
|
...(run.usage ? { input: run.usage.input, output: run.usage.output, cost: run.usage.cost } : {}),
|
|
@@ -103,6 +104,7 @@ export function runFromLog(entry: RunLogEntry, taskId: string): AgentRun {
|
|
|
103
104
|
startedAt: entry.startedAt,
|
|
104
105
|
...(entry.finishedAt ? { finishedAt: entry.finishedAt } : {}),
|
|
105
106
|
...(entry.model ? { model: entry.model } : {}),
|
|
107
|
+
...(entry.thinking ? { thinking: entry.thinking } : {}),
|
|
106
108
|
...(entry.turns ? { turns: entry.turns } : {}),
|
|
107
109
|
...(entry.tools ? { tools: entry.tools } : {}),
|
|
108
110
|
...(entry.input !== undefined ? { usage: { input: entry.input, output: entry.output ?? 0, cost: entry.cost ?? 0, turns: entry.turns ?? 0 } } : {}),
|
package/src/pi/settings-ui.ts
CHANGED
|
@@ -38,6 +38,8 @@ function entryView(config: BotLobbyConfig, kind: SettingsKind): EntryView {
|
|
|
38
38
|
if (kind === "master") return config.master;
|
|
39
39
|
if (kind === "scout") return { model: config.scout.model, timeoutMs: config.scout.timeoutMs };
|
|
40
40
|
if (kind === "researcher") return config.researcher;
|
|
41
|
+
if (kind === "quickfix") return config.quickFix;
|
|
42
|
+
if (kind === "planner") return config.planner;
|
|
41
43
|
return config.agents[kind];
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -58,6 +60,8 @@ export function patchEntry(config: BotLobbyConfig, kind: SettingsKind, patch: En
|
|
|
58
60
|
timeoutMs: patch.timeoutMs ?? config.scout.timeoutMs,
|
|
59
61
|
};
|
|
60
62
|
} else if (kind === "researcher") next.researcher = { ...config.researcher, ...patch } as AgentModelConfig;
|
|
63
|
+
else if (kind === "quickfix") next.quickFix = { ...config.quickFix, ...patch } as AgentModelConfig;
|
|
64
|
+
else if (kind === "planner") next.planner = { ...config.planner, ...patch } as AgentModelConfig;
|
|
61
65
|
else next.agents[kind] = { ...config.agents[kind], ...patch } as AgentModelConfig;
|
|
62
66
|
return next;
|
|
63
67
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Starting a bot-lobby task in the current session, shared by
|
|
3
|
+
* `/bot-lobby <request>`, the lobby's prompt and pending tasks saved from the
|
|
4
|
+
* planner.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { createTask, taskRequest, type Task } from "../schemas/task.ts";
|
|
9
|
+
import { createTaskDir, ensureProjectStructure, nextTaskId, ownedTask, saveTask, taskDirFor } from "../state/persistence.ts";
|
|
10
|
+
import { detectProjectRoot, loadConfig } from "../state/project.ts";
|
|
11
|
+
import { transition } from "../state/task-state.ts";
|
|
12
|
+
import { shortTitle } from "../text.ts";
|
|
13
|
+
import { applyStatus } from "./ui.ts";
|
|
14
|
+
import { applyMasterModel } from "./settings-ui.ts";
|
|
15
|
+
|
|
16
|
+
function uniqueTaskId(root: string, configDir: string, request: string): string {
|
|
17
|
+
const base = nextTaskId(request);
|
|
18
|
+
let id = base;
|
|
19
|
+
let suffix = 2;
|
|
20
|
+
while (existsSync(taskDirFor(root, configDir, id))) id = `${base}-${suffix++}`;
|
|
21
|
+
return id;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function kickoff(task: Task): string {
|
|
25
|
+
return [
|
|
26
|
+
`A bot-lobby task is active: ${task.id}`,
|
|
27
|
+
`Title: ${task.title}`,
|
|
28
|
+
`Request: ${taskRequest(task)}`,
|
|
29
|
+
`State: ${task.state}`,
|
|
30
|
+
"",
|
|
31
|
+
"Drive it with the orchestrate tool:",
|
|
32
|
+
"1. clarify if the request is genuinely ambiguous,",
|
|
33
|
+
"2. scout the domains the request touches,",
|
|
34
|
+
"3. synthesize the findings and propose a short `- ` bullet list for approval.",
|
|
35
|
+
"Do not implement anything before the user approves the proposal.",
|
|
36
|
+
].join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create the task, hand it to this session's Master and send the kickoff.
|
|
41
|
+
* Returns the task, or undefined (with a warning) when this session already
|
|
42
|
+
* owns an active one.
|
|
43
|
+
*/
|
|
44
|
+
export async function startTask(pi: ExtensionAPI, ctx: ExtensionContext, configDir: string, request: string): Promise<Task | undefined> {
|
|
45
|
+
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
46
|
+
ensureProjectStructure(root, configDir);
|
|
47
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
48
|
+
const existing = ownedTask(root, configDir, sessionId);
|
|
49
|
+
if (existing) {
|
|
50
|
+
ctx.ui.notify(`bot-lobby ${existing.id} is already active in this session. Finish it or run /bot-lobby cancel ${existing.id} first.`, "warning");
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const task = createTask(uniqueTaskId(root, configDir, request), shortTitle(request), new Date().toISOString(), request, sessionId);
|
|
54
|
+
createTaskDir(root, configDir, task);
|
|
55
|
+
transition(task, "clarifying");
|
|
56
|
+
saveTask(root, configDir, task);
|
|
57
|
+
applyStatus(ctx, root, configDir);
|
|
58
|
+
await applyMasterModel(pi, ctx, loadConfig());
|
|
59
|
+
ctx.ui.notify(`bot-lobby ${task.id} started`, "info");
|
|
60
|
+
// A kickoff while pi is still busy (another turn) queues behind it instead of throwing.
|
|
61
|
+
pi.sendUserMessage(kickoff(task), ctx.isIdle() ? undefined : { deliverAs: "followUp" });
|
|
62
|
+
return task;
|
|
63
|
+
}
|
package/src/pi/tools.ts
CHANGED
|
@@ -27,7 +27,7 @@ const OrchestrateSchema = Type.Object({
|
|
|
27
27
|
instruction: Type.Optional(Type.String({ description: "scout/research: what to investigate (for scout, also used to target-verify a claim)" })),
|
|
28
28
|
proposal: Type.Optional(Type.String({ description: "propose: the user-facing proposal as a short `- ` bullet list, one line per change" })),
|
|
29
29
|
concerns: Type.Optional(Type.Array(Type.String(), { description: "propose: concerns raised while challenging the request" })),
|
|
30
|
-
plan: Type.Optional(Type.String({ description: "plan: the detailed internal plan" })),
|
|
30
|
+
plan: Type.Optional(Type.String({ description: "plan: the detailed internal plan (while implementing or reviewing, the full revised plan that replaces it)" })),
|
|
31
31
|
domain: Type.Optional(Type.String({ description: "implement/research: designer, backend, or qa" })),
|
|
32
32
|
task: Type.Optional(Type.String({ description: "implement: the concrete step for that domain's worker" })),
|
|
33
33
|
assignments: Type.Optional(
|
|
@@ -59,7 +59,7 @@ const DESCRIPTION = [
|
|
|
59
59
|
"Actions: clarify (ask the user), scout (domain reconnaissance in parallel), research (summon the",
|
|
60
60
|
"read-only researcher for cited internet evidence on a complex change, tool, plugin, doc set or",
|
|
61
61
|
"dependency), propose (record the proposal and request approval), plan (record the internal",
|
|
62
|
-
"plan), implement (delegate a step to a domain worker, or several domains in parallel with assignments), qa (final quality gate and the only",
|
|
62
|
+
"plan, or amend it later with the full revised plan), implement (delegate a step to a domain worker, or several domains in parallel with assignments), qa (final quality gate and the only",
|
|
63
63
|
"review), knowledge (record approved knowledge or a decision),",
|
|
64
64
|
"compact (replace a knowledge file with a rewritten version, archiving the old one),",
|
|
65
65
|
"resolve_approval (approve or reject a request), complete (declare the task done after the gates",
|