@a-t-h-i/bot-lobby 0.6.25 → 0.6.26
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 +3 -1
- package/package.json +1 -5
- package/prompts/master.md +12 -7
- package/prompts/planner.md +5 -2
- package/src/classifier/hosts.ts +0 -11
- package/src/classifier/instance.ts +0 -5
- package/src/execution/agent-runner.ts +0 -18
- package/src/knowledge/compactor.ts +0 -70
- package/src/lobby/ask.ts +0 -15
- package/src/lobby/host.ts +2 -0
- package/src/lobby/issues.ts +0 -8
- package/src/lobby/pulls.ts +1 -44
- package/src/lobby/runtime.ts +1 -1
- package/src/lobby/service.ts +3 -1
- package/src/lobby/session-files.ts +1 -45
- package/src/lobby/split.ts +33 -1
- package/src/lobby/task-rows.ts +2 -0
- package/src/master/research.ts +0 -16
- package/src/pi/events.ts +9 -3
- package/src/pi/model-settings.ts +0 -1
- package/src/pi/plan-checklist.ts +98 -8
- package/src/pi/route.ts +0 -1
- package/src/pi/run-summary.ts +0 -57
- package/src/schemas/agent.ts +0 -4
- package/src/schemas/configuration.ts +0 -2
- package/src/state/budget.ts +49 -22
- package/src/state/metrics.ts +0 -1
- package/src/state/persistence.ts +0 -5
- package/src/state/work-time.ts +123 -0
- package/src/webui/api/index.ts +0 -5
- package/src/webui/api/lobby.ts +13 -6
- package/src/webui/api/plan-facts.ts +36 -6
- package/src/webui/api/tasks.ts +27 -6
- package/src/webui/dev/fake-service.ts +6 -0
- package/src/webui/dev/fixtures.ts +2 -0
- package/src/webui/protocol.ts +20 -3
- package/src/workflow/workflow.ts +35 -8
- package/webui/dist/assets/index-C1IwecNS.css +2 -0
- package/webui/dist/assets/{index-BRVbs5In.js → index-C4Ww36TW.js} +38 -38
- package/webui/dist/build.json +25 -37
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-BeRXVi_r.css +0 -2
package/README.md
CHANGED
|
@@ -20,7 +20,9 @@ The questionnaire and web tools are built in, so don't also install extensions w
|
|
|
20
20
|
1. In Pi, run `/bot-lobby add a login page`. Bot-lobby turns on and the lobby opens in your browser.
|
|
21
21
|
A new pi session starts with bot-lobby off, as plain pi: `Ctrl+Shift+M` or `/bot-lobby on|off` switches it.
|
|
22
22
|
2. Answer the oracle's questions and approve its plan, or agree one with the panel in **Plan** first.
|
|
23
|
-
|
|
23
|
+
A plan opens with a short list of steps; the detail of each follows for whoever wants it.
|
|
24
|
+
3. Follow the agents in **Lobby**: the step being worked on reads `active · 1m 12s`, and each task shows
|
|
25
|
+
how long its agents have worked on it (idle time left out). Models, effort and time limits are in **Settings**.
|
|
24
26
|
|
|
25
27
|
Small changes go to **Quick fix**. `--task` forces a full task, `--fast` / `--full` picks the workflow,
|
|
26
28
|
and `--branch` / `--worktree` isolates it in git. A finished isolated task pushes its branch and tries
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a-t-h-i/bot-lobby",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.26",
|
|
4
4
|
"description": "Structured multi-agent software engineering orchestrator for Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -58,7 +58,6 @@
|
|
|
58
58
|
"@earendil-works/pi-tui": "0.87.0",
|
|
59
59
|
"@fontsource-variable/inter": "5.3.0",
|
|
60
60
|
"@playwright/test": "1.62.0",
|
|
61
|
-
"@shadcn/react": "0.3.1",
|
|
62
61
|
"@tailwindcss/vite": "4.3.3",
|
|
63
62
|
"@types/node": "^22.10.0",
|
|
64
63
|
"@types/react": "19.3.0",
|
|
@@ -72,11 +71,8 @@
|
|
|
72
71
|
"react": "19.3.0",
|
|
73
72
|
"react-dom": "19.3.0",
|
|
74
73
|
"react-markdown": "10.1.0",
|
|
75
|
-
"recharts": "3.10.1",
|
|
76
74
|
"remark-gfm": "4.0.1",
|
|
77
|
-
"shiki": "4.5.0",
|
|
78
75
|
"socket.io": "4.8.4",
|
|
79
|
-
"sonner": "2.0.8",
|
|
80
76
|
"tailwind-merge": "3.7.0",
|
|
81
77
|
"tailwindcss": "4.3.3",
|
|
82
78
|
"tw-animate-css": "1.4.0",
|
package/prompts/master.md
CHANGED
|
@@ -169,10 +169,15 @@ integrations. One file has one owner: never split a file between domains, and
|
|
|
169
169
|
never give DESIGN only the styling of something another domain built —
|
|
170
170
|
whoever builds a visual thing owns how it looks.
|
|
171
171
|
|
|
172
|
-
Write the plan
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
172
|
+
Write the plan for the user to skim first and read in full only if they want
|
|
173
|
+
to. It opens with `## Steps`: a numbered list, one short plain line per step,
|
|
174
|
+
in the order they happen — what gets done, in a few words (`Add the work timer
|
|
175
|
+
to the task`), never how: no file paths, code, flags or jargon. That list is
|
|
176
|
+
the user's checklist, so keep it to the steps themselves. The detail follows
|
|
177
|
+
under `## Details`, one `### Step N: <the same words>` section per step, and
|
|
178
|
+
then the rest of the plan (contracts, risks, notes). Open each `implement` task
|
|
179
|
+
with its step number (`Step 3: ...`, or `Steps 3-4: ...` when one delegation
|
|
180
|
+
covers several) so the checklist tracks progress exactly.
|
|
176
181
|
|
|
177
182
|
## Briefing the agents
|
|
178
183
|
|
|
@@ -228,9 +233,9 @@ Rules for the brief:
|
|
|
228
233
|
a few actions. Split anything larger into consecutive steps in the same
|
|
229
234
|
`implement` call rather than leaving the agent to sequence it. Prefer more
|
|
230
235
|
explicit detail to fewer, larger chunks.
|
|
231
|
-
-
|
|
232
|
-
files, its actions and its done criteria, so the brief is the step
|
|
233
|
-
explicit, never a new decision.
|
|
236
|
+
- Each step's detail section is written to the same standard: it names the
|
|
237
|
+
step's files, its actions and its done criteria, so the brief is the step
|
|
238
|
+
made explicit, never a new decision.
|
|
234
239
|
- Scout and research instructions ask specific questions with the answer
|
|
235
240
|
format you want (paths, names, versions, yes/no plus evidence), and say what
|
|
236
241
|
you will do with the answer.
|
package/prompts/planner.md
CHANGED
|
@@ -106,6 +106,11 @@ or when you decided everything yourself.)
|
|
|
106
106
|
## Plan
|
|
107
107
|
The current draft, in Markdown:
|
|
108
108
|
|
|
109
|
+
### Steps
|
|
110
|
+
1. …
|
|
111
|
+
(first, so the user can skim the plan: one short plain line per step, in the
|
|
112
|
+
order they happen — what gets done in a few words, never how: no file paths,
|
|
113
|
+
code or jargon; the sections below carry the detail)
|
|
109
114
|
### Objective
|
|
110
115
|
### Scope and non-goals
|
|
111
116
|
### Acceptance criteria
|
|
@@ -115,6 +120,4 @@ The current draft, in Markdown:
|
|
|
115
120
|
(what the user decided for DEV, DESIGN, QA and RESEARCH, one bullet each)
|
|
116
121
|
### Assumptions
|
|
117
122
|
(what you decided without asking, each with its seat: "[QA] Test in evergreen browsers only")
|
|
118
|
-
### Steps
|
|
119
|
-
1. …
|
|
120
123
|
### Risks and open points
|
package/src/classifier/hosts.ts
CHANGED
|
@@ -40,11 +40,6 @@ export const JEV_HOST_TABLE: Readonly<Record<JevHostId, JevHost>> = Object.freez
|
|
|
40
40
|
/** The hosts `auto` tries, in order: OpenCode's free Jev first. */
|
|
41
41
|
export const AUTO_ORDER: readonly JevHostId[] = ["opencode", "typesafe"];
|
|
42
42
|
|
|
43
|
-
/** How a host name reads in settings. */
|
|
44
|
-
export function hostLabel(name: JevHostName): string {
|
|
45
|
-
return name === "auto" ? "Auto (OpenCode's free Jev, else TypeSafe)" : JEV_HOST_TABLE[name].label;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
43
|
/** Resolves a pi provider's API key (stored credential, then its environment variable). */
|
|
49
44
|
export type KeySource = (piProvider: string) => Promise<string | undefined>;
|
|
50
45
|
|
|
@@ -122,12 +117,6 @@ export function keyHint(config: Pick<ClassifierConfig, "provider">): string {
|
|
|
122
117
|
return how(JEV_HOST_TABLE[config.provider]);
|
|
123
118
|
}
|
|
124
119
|
|
|
125
|
-
/** `ts_ab…cd`: enough to recognise which key is loaded, never the key. */
|
|
126
|
-
export function maskKey(key: string): string {
|
|
127
|
-
if (key.length <= 8) return "*".repeat(key.length);
|
|
128
|
-
return `${key.slice(0, 4)}…${key.slice(-4)}`;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
120
|
/** One line on where a host's key comes from, for settings and `/bot-lobby config`. */
|
|
132
121
|
export function describeKey(host: JevHost, status: StatusSource | undefined, env: NodeJS.ProcessEnv = process.env): string {
|
|
133
122
|
const found = configuredProvider(host, status);
|
|
@@ -76,11 +76,6 @@ export function keyStatus(piProvider: string): KeyStatus | undefined {
|
|
|
76
76
|
return binding?.status(piProvider);
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/** Tests: use this classifier for the process (undefined restores the default). */
|
|
80
|
-
export function useClassifier(next: Classifier | undefined): void {
|
|
81
|
-
instance = next;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
79
|
/** Register the `typesafe` login entry and bind the classifier to each session's key store. */
|
|
85
80
|
export function registerClassifier(pi: ExtensionAPI, configDir: string): void {
|
|
86
81
|
registerJevProvider(pi);
|
|
@@ -380,21 +380,3 @@ export async function mapConcurrent<TIn, TOut>(items: TIn[], limit: number, fn:
|
|
|
380
380
|
export async function runParallel(requests: AgentRequest[], limit: number, run: ProcessRunner = spawnPiProcess): Promise<AgentRun[]> {
|
|
381
381
|
return mapWithConcurrencyLimit(requests, limit, (request) => runAgent(request, run));
|
|
382
382
|
}
|
|
383
|
-
|
|
384
|
-
function lastOutput(results: AgentRun[]): string {
|
|
385
|
-
return results.length > 0 ? results[results.length - 1]!.output : "";
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
/** Run agents in order, substituting `{previous}` with the prior output. */
|
|
389
|
-
export async function runSequential(requests: AgentRequest[], run: ProcessRunner = spawnPiProcess): Promise<AgentRun[]> {
|
|
390
|
-
const results: AgentRun[] = [];
|
|
391
|
-
for (const request of requests) {
|
|
392
|
-
const instruction = request.instruction.includes("{previous}")
|
|
393
|
-
? request.instruction.replaceAll("{previous}", lastOutput(results))
|
|
394
|
-
: request.instruction;
|
|
395
|
-
const result = await runAgent({ ...request, instruction }, run);
|
|
396
|
-
results.push(result);
|
|
397
|
-
if (result.status !== "success") break;
|
|
398
|
-
}
|
|
399
|
-
return results;
|
|
400
|
-
}
|
|
@@ -4,76 +4,6 @@ import type { KnowledgeAgent } from "./paths.ts";
|
|
|
4
4
|
import { AGENT_DIR_NAMES, KNOWLEDGE_FILES, knowledgeDir } from "./paths.ts";
|
|
5
5
|
import { readFirstExisting, readFileOr, writeFileEnsured } from "./store.ts";
|
|
6
6
|
|
|
7
|
-
export interface KnowledgeAnalysis {
|
|
8
|
-
/** Lines that say the same thing twice. */
|
|
9
|
-
duplicates: string[];
|
|
10
|
-
/** Lines with hedging or unresolved wording. */
|
|
11
|
-
ambiguity: string[];
|
|
12
|
-
/** Different statements sharing a subject: candidates for conflict review. */
|
|
13
|
-
conflictCandidates: { subject: string; statements: string[] }[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const AMBIGUITY = /\bmaybe|possibly|probably|tbd|todo|unclear|not sure|some kind|etc\.|approximately\b/i;
|
|
17
|
-
const HEDGE_STOPWORDS = new Set(["this", "that", "with", "from", "into", "when", "they", "have", "will", "should", "must"]);
|
|
18
|
-
|
|
19
|
-
function significantLines(content: string, minLength = 12): string[] {
|
|
20
|
-
return content
|
|
21
|
-
.split("\n")
|
|
22
|
-
.map((line) => line.replace(/^[-*#>\s]+/, "").trim())
|
|
23
|
-
.filter((line) => line.length >= minLength);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function normalize(line: string): string {
|
|
27
|
-
return line
|
|
28
|
-
.toLowerCase()
|
|
29
|
-
.replace(/[^\w\s]/g, "")
|
|
30
|
-
.replace(/\s+/g, " ")
|
|
31
|
-
.trim();
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function subjectKey(line: string): string {
|
|
35
|
-
return normalize(line)
|
|
36
|
-
.split(" ")
|
|
37
|
-
.filter((word) => word.length > 3 && !HEDGE_STOPWORDS.has(word))
|
|
38
|
-
.slice(0, 2)
|
|
39
|
-
.join(" ");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function findDuplicates(lines: string[]): string[] {
|
|
43
|
-
const seen = new Map<string, string>();
|
|
44
|
-
const duplicates: string[] = [];
|
|
45
|
-
for (const line of lines) {
|
|
46
|
-
const key = normalize(line);
|
|
47
|
-
if (seen.has(key)) duplicates.push(line);
|
|
48
|
-
else seen.set(key, line);
|
|
49
|
-
}
|
|
50
|
-
return duplicates;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function findConflicts(lines: string[]): { subject: string; statements: string[] }[] {
|
|
54
|
-
const groups = new Map<string, Set<string>>();
|
|
55
|
-
for (const line of lines) {
|
|
56
|
-
const key = subjectKey(line);
|
|
57
|
-
if (key.split(" ").length < 2) continue;
|
|
58
|
-
const statements = groups.get(key) ?? new Set<string>();
|
|
59
|
-
statements.add(line);
|
|
60
|
-
groups.set(key, statements);
|
|
61
|
-
}
|
|
62
|
-
return [...groups.entries()]
|
|
63
|
-
.filter(([, statements]) => statements.size > 1)
|
|
64
|
-
.map(([subject, statements]) => ({ subject, statements: [...statements] }));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** Mechanical candidates that help the Master decide what to compact (§24). */
|
|
68
|
-
export function analyzeKnowledge(content: string): KnowledgeAnalysis {
|
|
69
|
-
const lines = significantLines(content);
|
|
70
|
-
return {
|
|
71
|
-
duplicates: findDuplicates(lines),
|
|
72
|
-
ambiguity: lines.filter((line) => AMBIGUITY.test(line)),
|
|
73
|
-
conflictCandidates: findConflicts(lines),
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
7
|
export interface OversizedFile {
|
|
78
8
|
agent: KnowledgeAgent;
|
|
79
9
|
file: string;
|
package/src/lobby/ask.ts
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* The panel's questions become questionnaires of at most four, and the
|
|
7
7
|
* answers become the user's turn for the next round.
|
|
8
8
|
*/
|
|
9
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
9
|
import type { PanelQuestion, SettledQuestion } from "./planner.ts";
|
|
11
10
|
import { MAX_HEADER, MAX_LABEL, MAX_OPTIONS, MAX_QUESTIONS, MIN_OPTIONS, RESERVED, type AskAnswer, type AskOption, type AskQuestion, type AskResult, type Asker } from "../ask/types.ts";
|
|
12
11
|
|
|
@@ -122,17 +121,3 @@ export function answerMessage(questions: readonly PanelQuestion[], results: read
|
|
|
122
121
|
...(skipped.length > 0 ? ["", "Not answered this round:", ...skipped] : []),
|
|
123
122
|
].join("\n");
|
|
124
123
|
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Put every question to the user, one questionnaire after another. Stopping
|
|
128
|
-
* a questionnaire (esc) stops the rest; what was answered before it is kept.
|
|
129
|
-
*/
|
|
130
|
-
export async function askPanel(questions: readonly PanelQuestion[], ask: Asker, ctx: ExtensionContext): Promise<{ results: AskResult[]; stopped: boolean }> {
|
|
131
|
-
const results: AskResult[] = [];
|
|
132
|
-
for (const chunk of questionnaires(questions)) {
|
|
133
|
-
const result = await ask(chunk, ctx);
|
|
134
|
-
if (result.cancelled) return { results, stopped: true };
|
|
135
|
-
results.push(result);
|
|
136
|
-
}
|
|
137
|
-
return { results, stopped: false };
|
|
138
|
-
}
|
package/src/lobby/host.ts
CHANGED
|
@@ -95,6 +95,8 @@ export interface LobbyService {
|
|
|
95
95
|
feed: LobbyFeed;
|
|
96
96
|
masterBusy(): boolean;
|
|
97
97
|
tasks(): Task[];
|
|
98
|
+
/** How long a task's agents have worked on it, and the worker runs under way (`work.json`); undefined without one. */
|
|
99
|
+
work?(taskId: string): import("../state/work-time.ts").WorkProjection | undefined;
|
|
98
100
|
deliveryDeliver?(taskId: string, request: import("../delivery/operations.ts").DeliveryRequest): Promise<import("../delivery/types.ts").Delivery>;
|
|
99
101
|
deliveryReview?(taskId: string): Promise<import("../delivery/types.ts").Delivery>;
|
|
100
102
|
deliveryDefer?(taskId: string, reviewId: string): import("../delivery/types.ts").Delivery;
|
package/src/lobby/issues.ts
CHANGED
|
@@ -154,14 +154,6 @@ export async function createIssue(exec: Exec, cwd: string, title: string, body:
|
|
|
154
154
|
return { url, ...(Number.isFinite(number) ? { number } : {}) };
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
/** The issue as text for the planner: body plus comments. */
|
|
158
|
-
export function issueText(issue: IssueDetail): string {
|
|
159
|
-
const comments = issue.comments
|
|
160
|
-
.filter((comment) => comment.body.trim())
|
|
161
|
-
.map((comment) => `**${comment.author ?? "someone"}** commented:\n${comment.body.trim()}`);
|
|
162
|
-
return [issue.body.trim() || "(no description)", ...comments].join("\n\n");
|
|
163
|
-
}
|
|
164
|
-
|
|
165
157
|
/** Lobby state for the Issues tab: the list, the open issue, and what is loading. */
|
|
166
158
|
export class IssuesState {
|
|
167
159
|
issues: IssueSummary[] = [];
|
package/src/lobby/pulls.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Like issues, `gh` owns authentication and repository detection; every
|
|
5
5
|
* failure becomes one readable line.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { gh, type Exec } from "./issues.ts";
|
|
8
8
|
|
|
9
9
|
export type ChecksState = "passing" | "failing" | "pending";
|
|
10
10
|
|
|
@@ -193,49 +193,6 @@ export async function pullDiff(exec: Exec, cwd: string, number: number, limit =
|
|
|
193
193
|
return stdout.length > limit ? `${stdout.slice(0, limit)}\n[…the diff continues: ${stdout.length - limit} more characters]` : stdout;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
/** A pull request opened for a completed task's branch. */
|
|
197
|
-
export interface CreatedPull {
|
|
198
|
-
number: number;
|
|
199
|
-
url: string;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const PUSH_TIMEOUT_MS = 60_000;
|
|
203
|
-
|
|
204
|
-
async function run(exec: Exec, cwd: string, command: string, args: string[]): Promise<string> {
|
|
205
|
-
let result: Awaited<ReturnType<Exec>>;
|
|
206
|
-
try {
|
|
207
|
-
result = await exec(command, args, { cwd, timeout: PUSH_TIMEOUT_MS });
|
|
208
|
-
} catch (error) {
|
|
209
|
-
throw new Error(`${command} could not run — ${(error as Error).message}`);
|
|
210
|
-
}
|
|
211
|
-
if (result.code !== 0) throw new Error(result.stderr.trim() || `${command} exited with code ${result.code}`);
|
|
212
|
-
return result.stdout;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** `gh pr create` ends with the URL on its own last line; the number is its tail. */
|
|
216
|
-
function parseCreated(stdout: string): CreatedPull {
|
|
217
|
-
const url = stdout.trim().split("\n").map((line) => line.trim()).filter((line) => /^https?:\/\/\S+$/.test(line)).pop();
|
|
218
|
-
if (!url) throw new Error("gh pr create printed no pull request URL");
|
|
219
|
-
const match = /\/pull\/([1-9]\d*)$/.exec(url);
|
|
220
|
-
const number = Number(match?.[1]);
|
|
221
|
-
if (!Number.isSafeInteger(number) || number <= 0) throw new Error(`gh pr create printed an unreadable URL: ${url}`);
|
|
222
|
-
return { number, url };
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Push a finished task's branch and open its pull request, so the work lands
|
|
227
|
-
* in review without the oracle having to remember a second step.
|
|
228
|
-
*/
|
|
229
|
-
export async function createPullRequest(options: { exec?: Exec; cwd: string; branch: string; base: string; title: string; body: string }): Promise<CreatedPull> {
|
|
230
|
-
for (const ref of [options.branch, options.base]) {
|
|
231
|
-
if (!ref || ref.startsWith("-") || /[\s~^:?*\[\\]|\.\.|@\{|\/\//.test(ref) || /[/.]$/.test(ref)) throw new Error("invalid pull request branch");
|
|
232
|
-
}
|
|
233
|
-
const exec = options.exec ?? execCommand;
|
|
234
|
-
await run(exec, options.cwd, "git", ["push", "-u", "origin", options.branch]);
|
|
235
|
-
const stdout = await gh(exec, options.cwd, ["pr", "create", "--base", options.base, "--head", options.branch, "--title", options.title, "--body", options.body.slice(0, 60_000)]);
|
|
236
|
-
return parseCreated(stdout);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
196
|
/** Lobby state for the Git tab: the list, the open pull request, and what is loading. */
|
|
240
197
|
export class PullsState {
|
|
241
198
|
pulls: PullSummary[] = [];
|
package/src/lobby/runtime.ts
CHANGED
|
@@ -20,7 +20,7 @@ import type { Domain } from "../schemas/agent.ts";
|
|
|
20
20
|
import { chatFromEntries, lobbyFeed, narrateEvent, type AgentEventLike } from "./feed.ts";
|
|
21
21
|
import { classifier, effortFor, hintsFor } from "../classifier/instance.ts";
|
|
22
22
|
import { checkThinking } from "../pi/model-support.ts";
|
|
23
|
-
import {
|
|
23
|
+
import { QuickFixQueue } from "./quickfix.ts";
|
|
24
24
|
import type { Asker } from "../ask/types.ts";
|
|
25
25
|
import type { PlanningSession } from "./planner.ts";
|
|
26
26
|
import { execCommand, IssuesState } from "./issues.ts";
|
package/src/lobby/service.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { randomUUID } from "node:crypto";
|
|
|
10
10
|
import { realpathSync } from "node:fs";
|
|
11
11
|
import { resolve } from "node:path";
|
|
12
12
|
import { loadTask, ownedTask, peekTasks, saveTask } from "../state/persistence.ts";
|
|
13
|
+
import { taskWork } from "../state/work-time.ts";
|
|
13
14
|
import { refreshReview, deferReview } from "../delivery/review.ts";
|
|
14
15
|
import { deliver, type DeliveryRequest, type DeliveryStore } from "../delivery/operations.ts";
|
|
15
16
|
import type { LockIdentity } from "../delivery/lock.ts";
|
|
@@ -34,7 +35,7 @@ import { pendingRequest, startRequest } from "../pi/route.ts";
|
|
|
34
35
|
import { deliverComments, setAuto } from "../pi/owner.ts";
|
|
35
36
|
import { chatFromEntries, lobbyFeed, type ChatEntry } from "./feed.ts";
|
|
36
37
|
import { classifier, effortFor, hintsFor } from "../classifier/instance.ts";
|
|
37
|
-
import { launchPi, SessionRegistry, type BackgroundSession, type
|
|
38
|
+
import { launchPi, SessionRegistry, type BackgroundSession, type SessionLauncher } from "./sessions.ts";
|
|
38
39
|
import { SessionChats } from "./session-files.ts";
|
|
39
40
|
import { answerMessage, questionnaires, settledQuestions } from "./ask.ts";
|
|
40
41
|
import { askUser } from "../ask/web.ts";
|
|
@@ -572,6 +573,7 @@ export function createLobbyService(state: Runtime): LobbyService {
|
|
|
572
573
|
feed: lobbyFeed,
|
|
573
574
|
masterBusy: () => !state.ctx.isIdle(),
|
|
574
575
|
tasks: () => peekTasks(state.root, state.configDir),
|
|
576
|
+
work: (taskId) => taskWork(state.root, state.configDir, taskId),
|
|
575
577
|
deliveryDeliver: (taskId, request: DeliveryRequest) => deliver(request, { cwd: state.root, exec: execCommand }, deliveryStore(state, taskId)),
|
|
576
578
|
deliveryReview: async (taskId) => {
|
|
577
579
|
const task = deliveryTask(state, taskId);
|
|
@@ -7,51 +7,7 @@
|
|
|
7
7
|
* parent, and the text of user and assistant messages (never tool output).
|
|
8
8
|
*/
|
|
9
9
|
import { closeSync, openSync, readSync, statSync } from "node:fs";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
interface EntryLike {
|
|
13
|
-
type?: string;
|
|
14
|
-
id?: string;
|
|
15
|
-
parentId?: string | null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** The entries on the branch that ends at the newest entry, oldest first (a session file is a tree). */
|
|
19
|
-
export function currentBranch(entries: readonly unknown[]): unknown[] {
|
|
20
|
-
const byId = new Map<string, EntryLike>();
|
|
21
|
-
let leaf: EntryLike | undefined;
|
|
22
|
-
for (const entry of entries) {
|
|
23
|
-
const record = entry as EntryLike;
|
|
24
|
-
if (!record || typeof record.id !== "string") continue;
|
|
25
|
-
byId.set(record.id, record);
|
|
26
|
-
leaf = record;
|
|
27
|
-
}
|
|
28
|
-
const branch: unknown[] = [];
|
|
29
|
-
const seen = new Set<string>();
|
|
30
|
-
for (let entry = leaf; entry?.id && !seen.has(entry.id); entry = entry.parentId ? byId.get(entry.parentId) : undefined) {
|
|
31
|
-
seen.add(entry.id);
|
|
32
|
-
branch.push(entry);
|
|
33
|
-
}
|
|
34
|
-
return branch.reverse();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Parse a session file's JSON lines, skipping torn ones. */
|
|
38
|
-
export function parseEntries(text: string): unknown[] {
|
|
39
|
-
const entries: unknown[] = [];
|
|
40
|
-
for (const line of text.split("\n")) {
|
|
41
|
-
if (!line.trim()) continue;
|
|
42
|
-
try {
|
|
43
|
-
entries.push(JSON.parse(line));
|
|
44
|
-
} catch {
|
|
45
|
-
// A line being written right now; the next read has it.
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return entries;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** The conversation a session file holds, as the lobby's chat entries. */
|
|
52
|
-
export function chatFromFile(text: string, max = MAX_CHAT): ChatEntry[] {
|
|
53
|
-
return chatFromEntries(currentBranch(parseEntries(text)), max).map((line, index) => ({ id: index + 1, at: line.at ?? 0, role: line.role, text: line.text }));
|
|
54
|
-
}
|
|
10
|
+
import { chatText, CLEARED_NOTE, isClearedMark, MAX_CHAT, textOf, type ChatEntry } from "./feed.ts";
|
|
55
11
|
|
|
56
12
|
/** What a session log keeps of one entry: its parent, and its conversation lines if it is a message. */
|
|
57
13
|
interface LogNode {
|
package/src/lobby/split.ts
CHANGED
|
@@ -202,7 +202,39 @@ function planWithSteps(plan: string, covers: readonly number[], stepCount: numbe
|
|
|
202
202
|
const lines = plan.split("\n");
|
|
203
203
|
const kept = covers.map((step, index) => found.blocks[step - 1]!.replace(/^(\s*)\d+([.)])/, (_all, indent: string, mark: string) => `${indent}${index + 1}${mark}`));
|
|
204
204
|
const spacer = found.to > found.from && !(lines[found.to - 1] ?? "").trim() ? [""] : [];
|
|
205
|
-
return [...lines.slice(0, found.from), ...kept.flatMap((block) => block.split("\n")), ...spacer, ...lines.slice(found.to)].join("\n");
|
|
205
|
+
return [...lines.slice(0, found.from), ...kept.flatMap((block) => block.split("\n")), ...spacer, ...stepDetails(lines.slice(found.to), covers)].join("\n");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** `### Step 3: …` (or `**Step 3 — …**`): the heading of one step's detail. */
|
|
209
|
+
const DETAIL_HEADING = /^(\s*(?:#{1,6}\s+)?(?:\*\*)?\s*step\s+#?)(\d+)(?!\d)/i;
|
|
210
|
+
const ANY_HEADING = /^\s*(?:(#{1,6})\s+\S.*|\*\*[^*]+\*\*.*)$/;
|
|
211
|
+
|
|
212
|
+
/** A heading's depth: `#` is 1, a bold line deeper than any `#`. */
|
|
213
|
+
const depth = (line: string): number => ANY_HEADING.exec(line)?.[1]?.length ?? 7;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The lines after a plan's steps list with only the detail of the steps in `covers`, renumbered as
|
|
217
|
+
* the list was: a step's detail runs from its `Step N` heading to the next heading as deep or deeper.
|
|
218
|
+
*/
|
|
219
|
+
function stepDetails(lines: readonly string[], covers: readonly number[]): string[] {
|
|
220
|
+
const out: string[] = [];
|
|
221
|
+
let skipping: number | undefined;
|
|
222
|
+
for (const line of lines) {
|
|
223
|
+
const heading = ANY_HEADING.test(line) ? depth(line) : undefined;
|
|
224
|
+
if (skipping !== undefined && heading !== undefined && heading <= skipping) skipping = undefined;
|
|
225
|
+
const detail = heading !== undefined ? DETAIL_HEADING.exec(line) : null;
|
|
226
|
+
if (detail) {
|
|
227
|
+
const at = covers.indexOf(Number(detail[2]));
|
|
228
|
+
if (at < 0) {
|
|
229
|
+
skipping = heading;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
out.push(line.replace(DETAIL_HEADING, (_all, lead: string) => `${lead}${at + 1}`));
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (skipping === undefined) out.push(line);
|
|
236
|
+
}
|
|
237
|
+
return out;
|
|
206
238
|
}
|
|
207
239
|
|
|
208
240
|
/** One part's brief: where it sits, what it delivers, then the plan as written with only its own steps. */
|
package/src/lobby/task-rows.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface TaskRow {
|
|
|
22
22
|
check: CheckState;
|
|
23
23
|
/** Plan steps done, once the task has a plan. */
|
|
24
24
|
progress?: { done: number; total: number };
|
|
25
|
+
/** How long the task's agents have worked on it, idle time left out (ms, as of the reply). */
|
|
26
|
+
work?: { workedMs: number; running: boolean };
|
|
25
27
|
/** Who drives it, for tasks other sessions own. */
|
|
26
28
|
owner?: string;
|
|
27
29
|
/** How long ago it finished (finished tasks), was saved (plans) or was archived, as `3h`. */
|
package/src/master/research.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
1
|
import { join } from "node:path";
|
|
3
2
|
import type { Domain } from "../schemas/agent.ts";
|
|
4
3
|
import { profileFor, type BotLobbyConfig, type ProfileResolver } from "../schemas/configuration.ts";
|
|
@@ -39,21 +38,6 @@ export function saveResearchResult(taskDir: string, outcome: ResearchOutcome): v
|
|
|
39
38
|
writeFileEnsured(researchResultPath(taskDir, outcome.result.domain), JSON.stringify(outcome, null, 2));
|
|
40
39
|
}
|
|
41
40
|
|
|
42
|
-
/** Re-read persisted research reports for the given domains. */
|
|
43
|
-
export function loadResearchResults(taskDir: string, domains: Domain[]): ResearchOutcome[] {
|
|
44
|
-
const outcomes: ResearchOutcome[] = [];
|
|
45
|
-
for (const domain of domains) {
|
|
46
|
-
const path = researchResultPath(taskDir, domain);
|
|
47
|
-
if (!existsSync(path)) continue;
|
|
48
|
-
try {
|
|
49
|
-
outcomes.push(JSON.parse(readFileSync(path, "utf8")) as ResearchOutcome);
|
|
50
|
-
} catch {
|
|
51
|
-
// Corrupted research artifact: skip it rather than failing the workflow.
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return outcomes;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
41
|
function researchContext(request: ResearchRequest): AgentRequest["context"] {
|
|
58
42
|
return {
|
|
59
43
|
task: request.instruction,
|
package/src/pi/events.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { isAutoMode } from "../state/auto.ts";
|
|
|
17
17
|
import { triageContext } from "../classifier/triage.ts";
|
|
18
18
|
import { qaStillDue } from "../workflow/track.ts";
|
|
19
19
|
import { previousTaskNote } from "./fresh-context.ts";
|
|
20
|
-
import { budgetLine, budgetState, pauseClocks, readBudget, resumeClocks, startClock, stopClocks } from "../state/budget.ts";
|
|
20
|
+
import { budgetLine, budgetState, clockTask, pauseClocks, readBudget, resumeClocks, startClock, stopClocks, turnStarted } from "../state/budget.ts";
|
|
21
21
|
|
|
22
22
|
/** Tools that wait on the user: the task's clock waits with them. */
|
|
23
23
|
const ASKING_TOOLS: ReadonlySet<string> = new Set(["ask_user_question"]);
|
|
@@ -77,10 +77,11 @@ export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
|
|
80
|
-
// A task's time budget counts while the oracle works on it, not while it waits on the user.
|
|
80
|
+
// A task's work time (and its time budget) counts while the oracle works on it, not while it waits on the user.
|
|
81
81
|
const asking = new Set<string>();
|
|
82
82
|
pi.on("agent_start", (_event, ctx) => {
|
|
83
83
|
if (isSubagentProcess()) return;
|
|
84
|
+
turnStarted();
|
|
84
85
|
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
85
86
|
const task = activeTask(root, configDir, ctx.sessionManager.getSessionId());
|
|
86
87
|
if (task) startClock(root, configDir, task.id);
|
|
@@ -92,9 +93,14 @@ export function registerLifecycle(pi: ExtensionAPI, configDir: string): void {
|
|
|
92
93
|
pauseClocks();
|
|
93
94
|
}
|
|
94
95
|
});
|
|
95
|
-
pi.on("tool_execution_end", (event) => {
|
|
96
|
+
pi.on("tool_execution_end", (event, ctx) => {
|
|
96
97
|
if (isSubagentProcess()) return;
|
|
97
98
|
if (asking.delete(event.toolCallId)) resumeClocks();
|
|
99
|
+
// A task started, finished or taken over within the turn: the clock follows the task the session drives now.
|
|
100
|
+
if (event.toolName === "orchestrate") {
|
|
101
|
+
const root = detectProjectRoot(ctx.cwd, configDir);
|
|
102
|
+
clockTask(root, configDir, activeTask(root, configDir, ctx.sessionManager.getSessionId())?.id);
|
|
103
|
+
}
|
|
98
104
|
});
|
|
99
105
|
pi.on("agent_end", () => {
|
|
100
106
|
if (isSubagentProcess()) return;
|
package/src/pi/model-settings.ts
CHANGED
|
@@ -18,7 +18,6 @@ import { describeKey, jevEndpoint, type StatusSource } from "../classifier/hosts
|
|
|
18
18
|
|
|
19
19
|
/** One settings entry: the master or a subagent profile. */
|
|
20
20
|
export type SettingsKind = "master" | SubagentKind;
|
|
21
|
-
export const SETTINGS_KINDS: readonly SettingsKind[] = ["master", ...SUBAGENT_KINDS];
|
|
22
21
|
|
|
23
22
|
/** The editable fields of one entry; scouts have no thinking and no instructions of their own. */
|
|
24
23
|
export interface EntryView {
|