@a-t-h-i/bot-lobby 0.3.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 +266 -35
- package/package.json +1 -1
- package/prompts/backend.md +46 -1
- package/prompts/designer.md +94 -15
- package/prompts/master.md +70 -1
- package/prompts/panel.md +39 -0
- package/prompts/planner.md +64 -0
- package/prompts/qa.md +35 -2
- package/prompts/quickfix.md +41 -0
- package/prompts/researcher.md +6 -0
- package/prompts/reviewer.md +16 -0
- package/prompts/scout.md +11 -2
- package/prompts/worker.md +35 -2
- package/src/desk/client-extension.ts +101 -0
- package/src/desk/desk.ts +249 -0
- package/src/desk/ipc.ts +178 -0
- package/src/desk/session.ts +214 -0
- package/src/execution/agent-runner.ts +165 -14
- package/src/execution/pi-runner.ts +376 -65
- package/src/index.ts +7 -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/master/master.ts +21 -17
- package/src/master/research.ts +8 -7
- package/src/pi/activity.ts +165 -0
- package/src/pi/commands.ts +46 -59
- package/src/pi/events.ts +5 -2
- package/src/pi/expressions.ts +43 -12
- package/src/pi/kaomoji.ts +227 -0
- package/src/pi/mascot-art.ts +5 -15
- package/src/pi/model-support.ts +135 -0
- package/src/pi/run-summary.ts +172 -0
- package/src/pi/settings-ui.ts +162 -60
- package/src/pi/start-task.ts +63 -0
- package/src/pi/tools.ts +51 -8
- package/src/pi/ui.ts +151 -49
- package/src/pi/zen-large.ts +41 -4
- package/src/pi/zen-metrics.ts +47 -7
- package/src/pi/zen.ts +29 -8
- package/src/roles/reviewer.ts +24 -4
- package/src/roles/worker.ts +7 -1
- package/src/schemas/configuration.ts +177 -18
- package/src/schemas/findings.ts +26 -0
- package/src/schemas/task.ts +27 -1
- package/src/state/backlog.ts +106 -0
- package/src/state/comments.ts +136 -0
- package/src/state/metrics.ts +305 -0
- package/src/state/project.ts +9 -0
- package/src/text.ts +9 -0
- package/src/workflow/workflow.ts +161 -12
package/src/master/master.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import type
|
|
3
|
+
import { profileFor, type BotLobbyConfig, type ProfileResolver } from "../schemas/configuration.ts";
|
|
4
4
|
import type { Domain } from "../schemas/agent.ts";
|
|
5
5
|
import type { AgentRun, ReviewResult, ScoutResult, WorkerResult } from "../schemas/findings.ts";
|
|
6
6
|
import { domainSpec } from "../agents/registry.ts";
|
|
7
|
-
import { runAgent, runParallel, type AgentRequest } from "../execution/agent-runner.ts";
|
|
7
|
+
import { runAgent, runParallel, watchdogOptions, type AgentRequest } from "../execution/agent-runner.ts";
|
|
8
8
|
import { spawnPiProcess, type ProcessRunner } from "../execution/pi-runner.ts";
|
|
9
9
|
import { readAgentKnowledge, writeFileEnsured } from "../knowledge/store.ts";
|
|
10
10
|
import { selectKnowledge, type KnowledgeSelection } from "../knowledge/selector.ts";
|
|
@@ -32,14 +32,16 @@ export interface ScoutRequest {
|
|
|
32
32
|
dataRoots: readonly string[];
|
|
33
33
|
taskDir: string;
|
|
34
34
|
config: BotLobbyConfig;
|
|
35
|
+
/** Settings-derived model/thinking/time limit, clamped to the model; plain settings when absent. */
|
|
36
|
+
profile?: ProfileResolver;
|
|
35
37
|
signal?: AbortSignal;
|
|
36
38
|
onUpdate?: (run: AgentRun) => void;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
return model
|
|
41
|
+
/** Model, thinking and time limit for one run, from settings. */
|
|
42
|
+
function profileFields(config: BotLobbyConfig, resolver: ProfileResolver | undefined, domain: Domain, role: AgentRequest["role"]) {
|
|
43
|
+
const profile = profileFor(config, resolver, domain, role);
|
|
44
|
+
return { model: profile.model, thinking: profile.thinking, timeoutMs: profile.timeoutMs };
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
function scoutInstruction(request: ScoutRequest, domain: Domain): string {
|
|
@@ -82,13 +84,11 @@ export async function runScouts(request: ScoutRequest, run: ProcessRunner = spaw
|
|
|
82
84
|
role: "scout",
|
|
83
85
|
instruction: scoutInstruction(request, domain),
|
|
84
86
|
context: scoutContext(request, domain),
|
|
85
|
-
|
|
86
|
-
thinking: request.config.agents[domain].thinking,
|
|
87
|
-
timeoutMs: request.config.workflow.agentTimeoutMs,
|
|
87
|
+
...profileFields(request.config, request.profile, domain, "scout"),
|
|
88
88
|
cwd: request.cwd,
|
|
89
89
|
signal: request.signal,
|
|
90
90
|
onUpdate: request.onUpdate,
|
|
91
|
-
|
|
91
|
+
...watchdogOptions(request.config.workflow),
|
|
92
92
|
}));
|
|
93
93
|
const runs = await runParallel(requests, request.config.workflow.maxParallelScouts, run);
|
|
94
94
|
const outcomes = runs.map((agentRun) => toOutcome(agentRun, agentRun.domain));
|
|
@@ -136,6 +136,8 @@ export interface WorkerOutcome {
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
export interface WorkerRequest {
|
|
139
|
+
/** Extra run wiring for parallel batches: the file desk's env, tools and hooks. */
|
|
140
|
+
agent?: Pick<AgentRequest, "env" | "extraTools" | "onStart" | "onAttemptEnd">;
|
|
139
141
|
taskId: string;
|
|
140
142
|
domain: Domain;
|
|
141
143
|
/** What the Master wants implemented. */
|
|
@@ -146,6 +148,8 @@ export interface WorkerRequest {
|
|
|
146
148
|
cwd: string;
|
|
147
149
|
dataRoots: readonly string[];
|
|
148
150
|
config: BotLobbyConfig;
|
|
151
|
+
/** Settings-derived model/thinking/time limit, clamped to the model; plain settings when absent. */
|
|
152
|
+
profile?: ProfileResolver;
|
|
149
153
|
signal?: AbortSignal;
|
|
150
154
|
onUpdate?: (run: AgentRun) => void;
|
|
151
155
|
}
|
|
@@ -184,13 +188,12 @@ export async function runWorker(
|
|
|
184
188
|
instructions: request.config.agents[request.domain].instructions,
|
|
185
189
|
workflowContext: workerWorkflowContext(request),
|
|
186
190
|
},
|
|
187
|
-
|
|
188
|
-
thinking: request.config.agents[request.domain].thinking,
|
|
189
|
-
timeoutMs: request.config.workflow.agentTimeoutMs,
|
|
191
|
+
...profileFields(request.config, request.profile, request.domain, "worker"),
|
|
190
192
|
cwd: request.cwd,
|
|
191
193
|
signal: request.signal,
|
|
192
194
|
onUpdate: request.onUpdate,
|
|
193
|
-
|
|
195
|
+
...watchdogOptions(request.config.workflow),
|
|
196
|
+
...request.agent,
|
|
194
197
|
},
|
|
195
198
|
run,
|
|
196
199
|
);
|
|
@@ -219,6 +222,8 @@ export interface ReviewerRequest {
|
|
|
219
222
|
cwd: string;
|
|
220
223
|
dataRoots: readonly string[];
|
|
221
224
|
config: BotLobbyConfig;
|
|
225
|
+
/** Settings-derived model/thinking/time limit, clamped to the model; plain settings when absent. */
|
|
226
|
+
profile?: ProfileResolver;
|
|
222
227
|
signal?: AbortSignal;
|
|
223
228
|
onUpdate?: (run: AgentRun) => void;
|
|
224
229
|
}
|
|
@@ -262,12 +267,11 @@ function reviewerAgentRequest(request: ReviewerRequest, selected: KnowledgeSelec
|
|
|
262
267
|
instructions: request.config.agents[request.domain].instructions,
|
|
263
268
|
workflowContext: reviewerContext(request),
|
|
264
269
|
},
|
|
265
|
-
|
|
266
|
-
thinking: request.config.agents[request.domain].thinking,
|
|
267
|
-
timeoutMs: request.config.workflow.agentTimeoutMs,
|
|
270
|
+
...profileFields(request.config, request.profile, request.domain, "reviewer"),
|
|
268
271
|
cwd: request.cwd,
|
|
269
272
|
signal: request.signal,
|
|
270
273
|
onUpdate: request.onUpdate,
|
|
274
|
+
...watchdogOptions(request.config.workflow),
|
|
271
275
|
retries: 0,
|
|
272
276
|
};
|
|
273
277
|
}
|
package/src/master/research.ts
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { Domain } from "../schemas/agent.ts";
|
|
4
|
-
import type
|
|
4
|
+
import { profileFor, type BotLobbyConfig, type ProfileResolver } from "../schemas/configuration.ts";
|
|
5
5
|
import type { AgentRun, ResearchResult } from "../schemas/findings.ts";
|
|
6
|
-
import { runAgent, type AgentRequest } from "../execution/agent-runner.ts";
|
|
6
|
+
import { runAgent, watchdogOptions, type AgentRequest } from "../execution/agent-runner.ts";
|
|
7
7
|
import { spawnPiProcess, type ProcessRunner } from "../execution/pi-runner.ts";
|
|
8
8
|
import { writeFileEnsured } from "../knowledge/store.ts";
|
|
9
9
|
import { isResearchResultUsable, parseResearchResult, validateResearchResult } from "../roles/researcher.ts";
|
|
10
|
-
import { resolveModel } from "./master.ts";
|
|
11
10
|
|
|
12
11
|
export interface ResearchOutcome {
|
|
13
12
|
result: ResearchResult;
|
|
@@ -25,6 +24,7 @@ export interface ResearchRequest {
|
|
|
25
24
|
cwd: string;
|
|
26
25
|
taskDir: string;
|
|
27
26
|
config: BotLobbyConfig;
|
|
27
|
+
profile?: ProfileResolver;
|
|
28
28
|
signal?: AbortSignal;
|
|
29
29
|
onUpdate?: (run: AgentRun) => void;
|
|
30
30
|
}
|
|
@@ -75,6 +75,7 @@ function toOutcome(run: AgentRun, domain: Domain): ResearchOutcome {
|
|
|
75
75
|
|
|
76
76
|
/** Run the researcher for one domain and persist its report for audit. */
|
|
77
77
|
export async function runResearch(request: ResearchRequest, run: ProcessRunner = spawnPiProcess): Promise<ResearchOutcome> {
|
|
78
|
+
const profile = profileFor(request.config, request.profile, request.domain, "researcher");
|
|
78
79
|
const agentRun = await runAgent(
|
|
79
80
|
{
|
|
80
81
|
taskId: request.taskId,
|
|
@@ -82,13 +83,13 @@ export async function runResearch(request: ResearchRequest, run: ProcessRunner =
|
|
|
82
83
|
role: "researcher",
|
|
83
84
|
instruction: request.instruction,
|
|
84
85
|
context: researchContext(request),
|
|
85
|
-
model:
|
|
86
|
-
thinking:
|
|
87
|
-
timeoutMs:
|
|
86
|
+
model: profile.model,
|
|
87
|
+
thinking: profile.thinking,
|
|
88
|
+
timeoutMs: profile.timeoutMs,
|
|
88
89
|
cwd: request.cwd,
|
|
89
90
|
signal: request.signal,
|
|
90
91
|
onUpdate: request.onUpdate,
|
|
91
|
-
|
|
92
|
+
...watchdogOptions(request.config.workflow),
|
|
92
93
|
},
|
|
93
94
|
run,
|
|
94
95
|
);
|
package/src/pi/activity.ts
CHANGED
|
@@ -61,3 +61,168 @@ export function oracleActivityWord(toolName: string, args?: unknown): string {
|
|
|
61
61
|
const action = orchestrateAction(args);
|
|
62
62
|
return (action && ORACLE_ACTION_WORDS[action]) || FALLBACK_WORD;
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
const DETAIL_CHARS = 28;
|
|
66
|
+
|
|
67
|
+
function clip(text: string): string {
|
|
68
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
69
|
+
return flat.length > DETAIL_CHARS ? `${flat.slice(0, DETAIL_CHARS - 1)}…` : flat;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function field(args: unknown, ...names: string[]): string | undefined {
|
|
73
|
+
if (!args || typeof args !== "object") return undefined;
|
|
74
|
+
for (const name of names) {
|
|
75
|
+
const value = (args as Record<string, unknown>)[name];
|
|
76
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Last path segment, so `src/pi/zen.ts` reads `zen.ts`. */
|
|
82
|
+
function baseName(path: string): string {
|
|
83
|
+
const parts = path.replace(/[\\/]+$/, "").split(/[\\/]/);
|
|
84
|
+
return parts.at(-1) || path;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A short target for what a tool call touches: the file for read/edit/write,
|
|
89
|
+
* the command head for bash, the pattern for grep, the query for web search.
|
|
90
|
+
* Pure; undefined when the arguments name nothing useful.
|
|
91
|
+
*/
|
|
92
|
+
export function activityDetail(toolName: string, args?: unknown): string | undefined {
|
|
93
|
+
const tool = toolName.trim().toLowerCase();
|
|
94
|
+
if (tool === "read" || tool === "edit" || tool === "write" || tool === "ls") {
|
|
95
|
+
const path = field(args, "path", "file_path", "file");
|
|
96
|
+
return path ? clip(baseName(path)) : undefined;
|
|
97
|
+
}
|
|
98
|
+
if (tool === "bash") {
|
|
99
|
+
const command = field(args, "command", "cmd");
|
|
100
|
+
return command ? clip(command) : undefined;
|
|
101
|
+
}
|
|
102
|
+
if (tool === "grep" || tool === "find") {
|
|
103
|
+
const pattern = field(args, "pattern", "query", "glob");
|
|
104
|
+
return pattern ? clip(`"${pattern}"`) : undefined;
|
|
105
|
+
}
|
|
106
|
+
const query = field(args, "query", "url", "path");
|
|
107
|
+
return query ? clip(query) : undefined;
|
|
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,19 +1,15 @@
|
|
|
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,
|
|
5
|
-
import { detectProjectRoot, globalConfigPath, loadConfig, readDataRoots } from "../state/project.ts";
|
|
3
|
+
import { TERMINAL_STATES, type Task } from "../schemas/task.ts";
|
|
4
|
+
import { detectProjectRoot, globalConfigPath, loadConfig, readDataRoots, readRawConfig } from "../state/project.ts";
|
|
5
|
+
import { hasScoutThinking, SCOUT_THINKING } from "../schemas/configuration.ts";
|
|
6
6
|
import {
|
|
7
7
|
activeTask,
|
|
8
8
|
claimTask,
|
|
9
9
|
ownedTask,
|
|
10
10
|
ownerlessTask,
|
|
11
|
-
createTaskDir,
|
|
12
|
-
ensureProjectStructure,
|
|
13
|
-
nextTaskId,
|
|
14
11
|
saveTask,
|
|
15
12
|
selectTask,
|
|
16
|
-
taskDirFor,
|
|
17
13
|
taskHealth,
|
|
18
14
|
} from "../state/persistence.ts";
|
|
19
15
|
import { transition } from "../state/task-state.ts";
|
|
@@ -21,11 +17,16 @@ import { AGENT_DIR_NAMES, KNOWLEDGE_FILES, knowledgeDir, type KnowledgeAgent } f
|
|
|
21
17
|
import { readFirstExisting } from "../knowledge/store.ts";
|
|
22
18
|
import { overThreshold } from "../knowledge/compactor.ts";
|
|
23
19
|
import { applyApprovalChoice, describeTask, describeOversizedKnowledge, type ApprovalChoice } from "../workflow/workflow.ts";
|
|
24
|
-
import { shortTitle } from "../text.ts";
|
|
25
20
|
import { applyStatus, registerRevealShortcut, setMinimized } from "./ui.ts";
|
|
26
|
-
import {
|
|
21
|
+
import { openSettings } from "./settings-ui.ts";
|
|
22
|
+
import { kickoff, startTask } from "./start-task.ts";
|
|
23
|
+
import { autoOpenLobby, showLobby } from "../lobby/runtime.ts";
|
|
24
|
+
import { modelRef, thinkingMismatches } from "./model-support.ts";
|
|
25
|
+
import { describeRun, runFromLog } from "./run-summary.ts";
|
|
26
|
+
import { modelLookup } from "./tools.ts";
|
|
27
27
|
|
|
28
28
|
const HELP = [
|
|
29
|
+
"/bot-lobby Open the lobby: tasks, plan, quick fix, issues, metrics (alt+l)",
|
|
29
30
|
"/bot-lobby <request> Start a task through the workflow",
|
|
30
31
|
"/bot-lobby status [taskId] Show the active task",
|
|
31
32
|
"/bot-lobby tasks List tasks",
|
|
@@ -33,14 +34,16 @@ const HELP = [
|
|
|
33
34
|
"/bot-lobby cancel [taskId] Abandon a task",
|
|
34
35
|
"/bot-lobby approve|amend <text>|decline Answer the current proposal",
|
|
35
36
|
"/bot-lobby knowledge Show persistent knowledge files",
|
|
37
|
+
"/bot-lobby runs [taskId] Recent subagent runs: time, turns, tokens, cost, model",
|
|
36
38
|
"/bot-lobby settings Edit per-agent model/thinking/instructions",
|
|
37
39
|
"/bot-lobby config Show effective configuration",
|
|
38
40
|
"/bot-lobby minimize|restore Hide or restore bot-lobby for this session (ctrl+shift+m)",
|
|
39
41
|
"/bot-lobby claim <taskId> Take ownership of an orphaned task",
|
|
42
|
+
"/bot-lobby help This help",
|
|
40
43
|
].join("\n");
|
|
41
44
|
|
|
42
45
|
/** Subcommands only win when no free-form text follows (so tasks still start). */
|
|
43
|
-
const SUBCOMMANDS = new Set(["status", "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"]);
|
|
44
47
|
|
|
45
48
|
function isTaskId(value: string | undefined): boolean {
|
|
46
49
|
return Boolean(value && /^TASK-/.test(value));
|
|
@@ -56,52 +59,7 @@ function parseCommand(args: string): { sub: string | undefined; rest: string[];
|
|
|
56
59
|
return { sub, rest, restText: trimmed.slice(sub.length).trim() };
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
|
|
60
|
-
const base = nextTaskId(request);
|
|
61
|
-
let id = base;
|
|
62
|
-
let suffix = 2;
|
|
63
|
-
while (existsSync(taskDirFor(root, configDir, id))) id = `${base}-${suffix++}`;
|
|
64
|
-
return id;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function kickoff(task: Task): string {
|
|
68
|
-
return [
|
|
69
|
-
`A bot-lobby task is active: ${task.id}`,
|
|
70
|
-
`Title: ${task.title}`,
|
|
71
|
-
`Request: ${taskRequest(task)}`,
|
|
72
|
-
`State: ${task.state}`,
|
|
73
|
-
"",
|
|
74
|
-
"Drive it with the orchestrate tool:",
|
|
75
|
-
"1. clarify if the request is genuinely ambiguous,",
|
|
76
|
-
"2. scout the domains the request touches,",
|
|
77
|
-
"3. synthesize the findings and propose a short `- ` bullet list for approval.",
|
|
78
|
-
"Do not implement anything before the user approves the proposal.",
|
|
79
|
-
].join("\n");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function startTask(
|
|
83
|
-
pi: ExtensionAPI,
|
|
84
|
-
ctx: ExtensionCommandContext,
|
|
85
|
-
configDir: string,
|
|
86
|
-
request: string,
|
|
87
|
-
): Promise<void> {
|
|
88
|
-
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
89
|
-
ensureProjectStructure(root, configDir);
|
|
90
|
-
const sessionId = ctx.sessionManager.getSessionId();
|
|
91
|
-
const existing = ownedTask(root, configDir, sessionId);
|
|
92
|
-
if (existing) {
|
|
93
|
-
ctx.ui.notify(`bot-lobby ${existing.id} is already active in this session. Finish it or run /bot-lobby cancel ${existing.id} first.`, "warning");
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
const task = createTask(uniqueTaskId(root, configDir, request), shortTitle(request), new Date().toISOString(), request, sessionId);
|
|
97
|
-
createTaskDir(root, configDir, task);
|
|
98
|
-
transition(task, "clarifying");
|
|
99
|
-
saveTask(root, configDir, task);
|
|
100
|
-
applyStatus(ctx, root, configDir);
|
|
101
|
-
await applyMasterModel(pi, ctx, loadConfig());
|
|
102
|
-
ctx.ui.notify(`bot-lobby ${task.id} started`, "info");
|
|
103
|
-
pi.sendUserMessage(kickoff(task));
|
|
104
|
-
}
|
|
62
|
+
export { kickoff };
|
|
105
63
|
|
|
106
64
|
function showStatus(ctx: ExtensionCommandContext, configDir: string, taskId?: string): void {
|
|
107
65
|
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
@@ -118,6 +76,20 @@ function showStatus(ctx: ExtensionCommandContext, configDir: string, taskId?: st
|
|
|
118
76
|
ctx.ui.notify(task ? `${describeTask(task)}${footer}` : `No bot-lobby task found in ${root}.${footer}`, task ? "info" : "warning");
|
|
119
77
|
}
|
|
120
78
|
|
|
79
|
+
/** Recent runs of the active (or named) task, newest last, so slow models are easy to spot. */
|
|
80
|
+
export function runsReport(task: Task | undefined, limit = 20): string {
|
|
81
|
+
if (!task) return "No bot-lobby task found.";
|
|
82
|
+
const entries = (task.runLog ?? []).slice(-limit);
|
|
83
|
+
if (entries.length === 0) return `${task.id}: no finished subagent runs yet.`;
|
|
84
|
+
return [`${task.id} — last ${entries.length} run${entries.length === 1 ? "" : "s"}:`, ...entries.map((entry) => describeRun(runFromLog(entry, task.id)))].join("\n");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function showRuns(ctx: ExtensionCommandContext, configDir: string, taskId?: string): void {
|
|
88
|
+
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
89
|
+
const task = selectTask(root, configDir, taskId, ctx.sessionManager.getSessionId()) ?? (taskId ? undefined : ownerlessTask(root, configDir));
|
|
90
|
+
ctx.ui.notify(runsReport(task), task ? "info" : "warning");
|
|
91
|
+
}
|
|
92
|
+
|
|
121
93
|
function showTasks(ctx: ExtensionCommandContext, configDir: string): void {
|
|
122
94
|
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
123
95
|
const { tasks, corrupted } = taskHealth(root, configDir);
|
|
@@ -208,7 +180,11 @@ function showKnowledge(ctx: ExtensionCommandContext, configDir: string): void {
|
|
|
208
180
|
}
|
|
209
181
|
|
|
210
182
|
function showConfig(ctx: ExtensionCommandContext): void {
|
|
211
|
-
|
|
183
|
+
const config = loadConfig();
|
|
184
|
+
const warnings = thinkingMismatches(config, modelLookup(ctx), ctx.model ? modelRef(ctx.model) : undefined);
|
|
185
|
+
if (hasScoutThinking(readRawConfig())) warnings.push(`Scout: thinking is fixed at "${SCOUT_THINKING}"; the scout.thinking value in the file is ignored.`);
|
|
186
|
+
const notes = warnings.length > 0 ? `\n\nWarnings:\n${warnings.map((line) => `- ${line}`).join("\n")}` : "";
|
|
187
|
+
ctx.ui.notify(`${globalConfigPath()}\n${JSON.stringify(config, null, 2)}${notes}`, warnings.length > 0 ? "warning" : "info");
|
|
212
188
|
}
|
|
213
189
|
|
|
214
190
|
export function registerCommands(pi: ExtensionAPI, configDir: string): void {
|
|
@@ -223,14 +199,25 @@ export function registerCommands(pi: ExtensionAPI, configDir: string): void {
|
|
|
223
199
|
handler: async (args, ctx) => {
|
|
224
200
|
const { sub, rest, restText } = parseCommand(args ?? "");
|
|
225
201
|
if (!sub) {
|
|
226
|
-
if (!restText)
|
|
227
|
-
|
|
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;
|
|
228
208
|
}
|
|
229
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");
|
|
230
215
|
case "status":
|
|
231
216
|
return showStatus(ctx, configDir, rest[0]);
|
|
232
217
|
case "tasks":
|
|
233
218
|
return showTasks(ctx, configDir);
|
|
219
|
+
case "runs":
|
|
220
|
+
return showRuns(ctx, configDir, rest[0]);
|
|
234
221
|
case "pause":
|
|
235
222
|
return setPaused(ctx, configDir, true, rest[0]);
|
|
236
223
|
case "resume":
|
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/expressions.ts
CHANGED
|
@@ -20,12 +20,19 @@ export const FAST_TICK_MS = 120;
|
|
|
20
20
|
export const REST_FRAME = 0;
|
|
21
21
|
export const BLINK_FRAME = 1;
|
|
22
22
|
export const EMOTE_FRAME = 2;
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Emote frame range: `EMOTE_FRAME .. EMOTE_FRAME + EMOTE_FRAMES - 1`; an emote
|
|
25
|
+
* steps through them: the open face, a blink, then its action (see kaomoji.ts).
|
|
26
|
+
*/
|
|
27
|
+
export const EMOTE_FRAMES = 4;
|
|
25
28
|
/** Each emote frame is held this long, so an emote steps through its frames. */
|
|
26
29
|
export const EMOTE_STEP_MS = Math.floor(EMOTE_MS / EMOTE_FRAMES);
|
|
27
30
|
|
|
28
31
|
const BLINK_CHANCE = 0.65;
|
|
32
|
+
/** Working agents emote as often as they blink. */
|
|
33
|
+
export const WORKING_BLINK_CHANCE = 0.5;
|
|
34
|
+
/** Upper bound (exclusive) of an expression's variant. */
|
|
35
|
+
const VARIANT_SPAN = 2 ** 30;
|
|
29
36
|
|
|
30
37
|
export interface ExpressionState {
|
|
31
38
|
/** Earliest `now` the next expression may start. */
|
|
@@ -36,6 +43,8 @@ export interface ExpressionState {
|
|
|
36
43
|
startedAt: number;
|
|
37
44
|
/** Frame index for the art: 0 rest, 1 blink, 2+ emote. */
|
|
38
45
|
frame: number;
|
|
46
|
+
/** Drawn once per expression; picks which face an emote shows (see kaomoji.ts). */
|
|
47
|
+
variant: number;
|
|
39
48
|
}
|
|
40
49
|
|
|
41
50
|
/** A random draw clamped to `[0, 1]`, so a non-finite source value can never leak. */
|
|
@@ -50,8 +59,10 @@ export interface ExpressionGap {
|
|
|
50
59
|
max: number;
|
|
51
60
|
}
|
|
52
61
|
|
|
53
|
-
/**
|
|
62
|
+
/** Idle agent slots rest 20-30 s between expressions. */
|
|
54
63
|
export const SLOT_GAP: ExpressionGap = { min: BLINK_MIN_MS, max: BLINK_MAX_MS };
|
|
64
|
+
/** Working agents are livelier: 8-15 s between expressions. */
|
|
65
|
+
export const WORKING_GAP: ExpressionGap = { min: 8_000, max: 15_000 };
|
|
55
66
|
/** The oracle is the scene's centrepiece: it blinks and emotes every 6-12 s. */
|
|
56
67
|
export const ORACLE_GAP: ExpressionGap = { min: 6_000, max: 12_000 };
|
|
57
68
|
|
|
@@ -60,15 +71,19 @@ export function nextGap(rng: () => number, gap: ExpressionGap = SLOT_GAP): numbe
|
|
|
60
71
|
return gap.min + Math.round(unit(rng) * (gap.max - gap.min));
|
|
61
72
|
}
|
|
62
73
|
|
|
63
|
-
/** Whether the next expression blinks (~65%) or emotes
|
|
64
|
-
export function pickEvent(rng: () => number): "blink" | "emote" {
|
|
65
|
-
return unit(rng) <
|
|
74
|
+
/** Whether the next expression blinks (~65% by default) or emotes. */
|
|
75
|
+
export function pickEvent(rng: () => number, blinkChance = BLINK_CHANCE): "blink" | "emote" {
|
|
76
|
+
return unit(rng) < blinkChance ? "blink" : "emote";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function drawVariant(rng: () => number): number {
|
|
80
|
+
return Math.floor(unit(rng) * (VARIANT_SPAN - 1));
|
|
66
81
|
}
|
|
67
82
|
|
|
68
83
|
/** A freshly rested expression that fires for the first time after one random gap. */
|
|
69
84
|
export function createExpression(now: number, rng: () => number, gap: ExpressionGap = SLOT_GAP): ExpressionState {
|
|
70
85
|
const start = Number.isFinite(now) ? now : 0;
|
|
71
|
-
return { nextAt: start + nextGap(rng, gap), until: start, startedAt: start, frame: REST_FRAME };
|
|
86
|
+
return { nextAt: start + nextGap(rng, gap), until: start, startedAt: start, frame: REST_FRAME, variant: 0 };
|
|
72
87
|
}
|
|
73
88
|
|
|
74
89
|
/** True while `now` is inside a playing blink or emote. */
|
|
@@ -90,17 +105,33 @@ export function advanceExpression(
|
|
|
90
105
|
now: number,
|
|
91
106
|
rng: () => number,
|
|
92
107
|
gap: ExpressionGap = SLOT_GAP,
|
|
108
|
+
blinkChance = BLINK_CHANCE,
|
|
93
109
|
): ExpressionState {
|
|
94
110
|
if (!Number.isFinite(now)) return state;
|
|
95
111
|
if (now < state.until) return state.frame >= EMOTE_FRAME ? steppedEmote(state, now) : state;
|
|
96
112
|
if (now < state.nextAt) return state.frame === REST_FRAME ? state : { ...state, frame: REST_FRAME };
|
|
97
|
-
return play(now, rng, gap);
|
|
113
|
+
return play(now, rng, gap, blinkChance);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function play(now: number, rng: () => number, gap: ExpressionGap, blinkChance: number): ExpressionState {
|
|
117
|
+
const blink = pickEvent(rng, blinkChance) === "blink";
|
|
118
|
+
return blink ? startBlink(now, rng, gap) : triggerEmote(now, rng, gap);
|
|
98
119
|
}
|
|
99
120
|
|
|
100
|
-
function
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
121
|
+
function startBlink(now: number, rng: () => number, gap: ExpressionGap): ExpressionState {
|
|
122
|
+
const until = now + BLINK_MS;
|
|
123
|
+
return { nextAt: until + nextGap(rng, gap), until, startedAt: now, frame: BLINK_FRAME, variant: drawVariant(rng) };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Start an emote right now, whatever the sprite was doing: the reaction when a
|
|
128
|
+
* slot's situation changes (it starts, finishes, fails, stalls, waits or
|
|
129
|
+
* receives a file). The next scheduled expression follows one gap later.
|
|
130
|
+
*/
|
|
131
|
+
export function triggerEmote(now: number, rng: () => number, gap: ExpressionGap = SLOT_GAP): ExpressionState {
|
|
132
|
+
const start = Number.isFinite(now) ? now : 0;
|
|
133
|
+
const until = start + EMOTE_MS;
|
|
134
|
+
return { nextAt: until + nextGap(rng, gap), until, startedAt: start, frame: EMOTE_FRAME, variant: drawVariant(rng) };
|
|
104
135
|
}
|
|
105
136
|
|
|
106
137
|
/** One sub-step of a playing expression; the oracle's blink and glance step through these. */
|