@a-t-h-i/bot-lobby 0.6.7 → 0.6.8
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 +132 -15
- package/package.json +1 -1
- package/prompts/master.md +15 -0
- package/prompts/panel.md +4 -0
- package/prompts/planner.md +5 -0
- package/prompts/pr-review.md +62 -0
- package/prompts/splitter.md +58 -0
- package/src/ask/dialog.ts +19 -5
- package/src/ask/state.ts +8 -2
- package/src/ask/tool.ts +3 -1
- package/src/ask/view.ts +6 -3
- package/src/classifier/instance.ts +6 -0
- package/src/classifier/knowledge.ts +158 -0
- package/src/classifier/review.ts +146 -0
- package/src/execution/workspace.ts +185 -0
- package/src/knowledge/edit.ts +109 -0
- package/src/knowledge/notes.ts +143 -0
- package/src/knowledge/selector.ts +1 -1
- package/src/knowledge/store.ts +16 -2
- package/src/lobby/ask.ts +42 -21
- package/src/lobby/issues.ts +1 -1
- package/src/lobby/knowledge.ts +179 -0
- package/src/lobby/layout.ts +95 -18
- package/src/lobby/planner.ts +199 -7
- package/src/lobby/pr-review.ts +360 -0
- package/src/lobby/pulls.ts +250 -0
- package/src/lobby/runtime.ts +115 -13
- package/src/lobby/split.ts +230 -0
- package/src/lobby/tabs/git.ts +162 -0
- package/src/lobby/tabs/knowledge.ts +135 -0
- package/src/lobby/tabs/plan.ts +3 -1
- package/src/lobby/tabs/tasks.ts +11 -2
- package/src/lobby/view.ts +432 -32
- package/src/master/master.ts +39 -13
- package/src/pi/commands.ts +12 -7
- package/src/pi/model-support.ts +9 -0
- package/src/pi/plan-checklist.ts +42 -0
- package/src/pi/route.ts +2 -1
- package/src/pi/settings-ui.ts +41 -1
- package/src/pi/start-flags.ts +14 -0
- package/src/pi/start-task.ts +56 -5
- package/src/pi/tools.ts +4 -2
- package/src/schemas/configuration.ts +32 -3
- package/src/schemas/task.ts +15 -0
- package/src/state/backlog.ts +27 -4
- package/src/state/metrics.ts +2 -2
- package/src/state/persistence.ts +5 -5
- package/src/text.ts +38 -0
- package/src/workflow/workflow.ts +11 -0
|
@@ -55,6 +55,18 @@ export type LobbyAgentKind = "quickfix" | "planner";
|
|
|
55
55
|
/** Settings kinds a workflow run (domain + role) can draw from. */
|
|
56
56
|
export type WorkflowProfileKind = Domain | "scout" | "researcher";
|
|
57
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Whether a new task gets a git branch of its own (`branch`: created and
|
|
60
|
+
* checked out in the working folder) or a worktree of its own (`worktree`: a
|
|
61
|
+
* second checkout every agent of the task runs in), named after the task.
|
|
62
|
+
*/
|
|
63
|
+
export const GIT_ISOLATIONS = ["off", "branch", "worktree"] as const;
|
|
64
|
+
export type GitIsolation = (typeof GIT_ISOLATIONS)[number];
|
|
65
|
+
|
|
66
|
+
export function isGitIsolation(value: unknown): value is GitIsolation {
|
|
67
|
+
return typeof value === "string" && (GIT_ISOLATIONS as readonly string[]).includes(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
export interface WorkflowConfig {
|
|
59
71
|
maxReviewIterations: number;
|
|
60
72
|
maxParallelScouts: number;
|
|
@@ -82,6 +94,8 @@ export interface WorkflowConfig {
|
|
|
82
94
|
briefCheck: boolean;
|
|
83
95
|
/** A new request that one agent can do alone goes to the quick-fix agent once the oracle confirms; false makes every request a task. */
|
|
84
96
|
routeQuickFixes: boolean;
|
|
97
|
+
/** A new task gets a git branch or worktree of its own, named after it; `--branch` and `--worktree` decide for one task. */
|
|
98
|
+
gitIsolation: GitIsolation;
|
|
85
99
|
}
|
|
86
100
|
|
|
87
101
|
export interface KnowledgeConfig {
|
|
@@ -129,10 +143,15 @@ export interface LobbyConfig {
|
|
|
129
143
|
* round skips the seats and asks nothing; later replies only revise. 0 = unlimited.
|
|
130
144
|
*/
|
|
131
145
|
maxPlanningRounds: number;
|
|
146
|
+
/**
|
|
147
|
+
* A plan with more steps than this is offered to be split into up to five
|
|
148
|
+
* tasks when it is saved (the oracle proposes, the user decides); 0 = never.
|
|
149
|
+
*/
|
|
150
|
+
splitPlanAbove: number;
|
|
132
151
|
}
|
|
133
152
|
|
|
134
153
|
/** Decisions the classifier can make, each switched on or off on its own. */
|
|
135
|
-
export const CLASSIFIER_FEATURES = ["seats", "answers", "files", "triage", "effort"] as const;
|
|
154
|
+
export const CLASSIFIER_FEATURES = ["seats", "answers", "files", "triage", "effort", "review", "knowledge"] as const;
|
|
136
155
|
export type ClassifierFeature = (typeof CLASSIFIER_FEATURES)[number];
|
|
137
156
|
|
|
138
157
|
/** Hosts that serve Jev behind the same System One API; `auto` takes OpenCode's free Jev when pi holds an OpenCode key, else TypeSafe. */
|
|
@@ -151,6 +170,8 @@ export interface ClassifierThresholds {
|
|
|
151
170
|
autoAnswerMargin: number;
|
|
152
171
|
/** A file is a likely file at this relevance. */
|
|
153
172
|
fileRelevantAt: number;
|
|
173
|
+
/** A section of a knowledge file that is over the prompt budget stays in at this relevance. */
|
|
174
|
+
knowledgeRelevantAt: number;
|
|
154
175
|
/** A step scored simple at this confidence runs one thinking level lower. */
|
|
155
176
|
simpleAt: number;
|
|
156
177
|
/** A step scored trivial at this confidence runs on the cheaper model. */
|
|
@@ -224,6 +245,7 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
|
|
|
224
245
|
fastTrack: true,
|
|
225
246
|
briefCheck: true,
|
|
226
247
|
routeQuickFixes: true,
|
|
248
|
+
gitIsolation: "off",
|
|
227
249
|
},
|
|
228
250
|
knowledge: {
|
|
229
251
|
compactionThreshold: 20000,
|
|
@@ -241,6 +263,7 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
|
|
|
241
263
|
mouse: true,
|
|
242
264
|
miniLine: true,
|
|
243
265
|
maxPlanningRounds: 5,
|
|
266
|
+
splitPlanAbove: 8,
|
|
244
267
|
},
|
|
245
268
|
classifier: {
|
|
246
269
|
enabled: false,
|
|
@@ -248,13 +271,14 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
|
|
|
248
271
|
model: "",
|
|
249
272
|
baseUrl: "",
|
|
250
273
|
timeoutMs: 4000,
|
|
251
|
-
features: { seats: true, answers: true, files: true, triage: true, effort: true },
|
|
274
|
+
features: { seats: true, answers: true, files: true, triage: true, effort: true, review: true, knowledge: true },
|
|
252
275
|
thresholds: {
|
|
253
276
|
seatAt: 0.35,
|
|
254
277
|
reseatReadyAt: 0.6,
|
|
255
278
|
autoAnswerAt: 0.9,
|
|
256
279
|
autoAnswerMargin: 0.5,
|
|
257
280
|
fileRelevantAt: 0.5,
|
|
281
|
+
knowledgeRelevantAt: 0.4,
|
|
258
282
|
simpleAt: 0.7,
|
|
259
283
|
trivialAt: 0.8,
|
|
260
284
|
quickFixAt: 0.7,
|
|
@@ -269,6 +293,9 @@ export const DEFAULT_CONFIG: BotLobbyConfig = {
|
|
|
269
293
|
/** Choices the settings menu cycles through for the planning round limit; 0 = unlimited. */
|
|
270
294
|
export const PLANNING_ROUND_CHOICES = [2, 3, 5, 8, 0] as const;
|
|
271
295
|
|
|
296
|
+
/** Choices the settings menu cycles through for the step count past which a saved plan is offered a split; 0 = never. */
|
|
297
|
+
export const SPLIT_PLAN_CHOICES = [6, 8, 10, 12, 0] as const;
|
|
298
|
+
|
|
272
299
|
function positive(value: unknown): number | undefined {
|
|
273
300
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
274
301
|
}
|
|
@@ -299,7 +326,7 @@ function flag(value: unknown, fallback: boolean): boolean {
|
|
|
299
326
|
}
|
|
300
327
|
|
|
301
328
|
function normalizeLobby(value: unknown): LobbyConfig {
|
|
302
|
-
const source = value as { autoOpen?: unknown; planningPanel?: unknown; autoAsk?: unknown; issues?: unknown; panels?: unknown; keys?: unknown; mouse?: unknown; miniLine?: unknown; maxPlanningRounds?: unknown } | undefined;
|
|
329
|
+
const source = value as { autoOpen?: unknown; planningPanel?: unknown; autoAsk?: unknown; issues?: unknown; panels?: unknown; keys?: unknown; mouse?: unknown; miniLine?: unknown; maxPlanningRounds?: unknown; splitPlanAbove?: unknown } | undefined;
|
|
303
330
|
const defaults = DEFAULT_CONFIG.lobby;
|
|
304
331
|
const panel = Array.isArray(source?.planningPanel)
|
|
305
332
|
? [...new Set(source.planningPanel.filter((entry): entry is PanelMember => typeof entry === "string" && isPanelMember(entry)))]
|
|
@@ -316,6 +343,7 @@ function normalizeLobby(value: unknown): LobbyConfig {
|
|
|
316
343
|
mouse: flag(source?.mouse, defaults.mouse),
|
|
317
344
|
miniLine: flag(source?.miniLine, defaults.miniLine),
|
|
318
345
|
maxPlanningRounds: roundLimit(source?.maxPlanningRounds, defaults.maxPlanningRounds),
|
|
346
|
+
splitPlanAbove: roundLimit(source?.splitPlanAbove, defaults.splitPlanAbove),
|
|
319
347
|
};
|
|
320
348
|
}
|
|
321
349
|
|
|
@@ -370,6 +398,7 @@ export function resolveConfig(partial: unknown): BotLobbyConfig {
|
|
|
370
398
|
workflow.fastTrack = workflow.fastTrack !== false;
|
|
371
399
|
workflow.briefCheck = workflow.briefCheck !== false;
|
|
372
400
|
workflow.routeQuickFixes = workflow.routeQuickFixes !== false;
|
|
401
|
+
workflow.gitIsolation = isGitIsolation(workflow.gitIsolation) ? workflow.gitIsolation : DEFAULT_CONFIG.workflow.gitIsolation;
|
|
373
402
|
workflow.taskBudgetMinutes = typeof workflow.taskBudgetMinutes === "number" && workflow.taskBudgetMinutes > 0 ? Math.min(24 * 60, Math.round(workflow.taskBudgetMinutes)) : 0;
|
|
374
403
|
const knowledge = { ...DEFAULT_CONFIG.knowledge, ...(src.knowledge as Partial<KnowledgeConfig> | undefined) };
|
|
375
404
|
const srcAgents = (src.agents ?? {}) as Partial<BotLobbyConfig["agents"]>;
|
package/src/schemas/task.ts
CHANGED
|
@@ -158,6 +158,19 @@ export interface QaWaiver {
|
|
|
158
158
|
open: string[];
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
/** The branch (or worktree and branch) a task works on, when git isolation was on as it started. */
|
|
162
|
+
export interface TaskGit {
|
|
163
|
+
mode: "branch" | "worktree";
|
|
164
|
+
/** Named after the task. */
|
|
165
|
+
branch: string;
|
|
166
|
+
/** What the working folder stood on when the task started: a branch name, or `detached at 1a2b3c4`. */
|
|
167
|
+
from?: string;
|
|
168
|
+
/** The commit it branched from. */
|
|
169
|
+
base?: string;
|
|
170
|
+
/** worktree: the folder every agent of the task runs in. */
|
|
171
|
+
path?: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
161
174
|
export interface Task {
|
|
162
175
|
id: string;
|
|
163
176
|
title: string;
|
|
@@ -197,6 +210,8 @@ export interface Task {
|
|
|
197
210
|
extraReviewRounds?: number;
|
|
198
211
|
/** Set only by the user: the work is accepted without a QA pass, so the task may complete. */
|
|
199
212
|
qaWaiver?: QaWaiver;
|
|
213
|
+
/** The git branch (or worktree) made for this task, when isolation was on as it started. */
|
|
214
|
+
git?: TaskGit;
|
|
200
215
|
}
|
|
201
216
|
|
|
202
217
|
export function createTask(
|
package/src/state/backlog.ts
CHANGED
|
@@ -18,6 +18,19 @@ export interface IssueRef {
|
|
|
18
18
|
url?: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/** What a task saved from a split plan knows of the others: it is one part of a plan that became several tasks. */
|
|
22
|
+
export interface SplitInfo {
|
|
23
|
+
/** Shared by every part of one plan. */
|
|
24
|
+
group: string;
|
|
25
|
+
/** This part's number (from 1) and how many parts there are. */
|
|
26
|
+
part: number;
|
|
27
|
+
of: number;
|
|
28
|
+
/** The title of every part, in order. */
|
|
29
|
+
titles: string[];
|
|
30
|
+
/** Parts (by number) that come before this one and should be finished first. */
|
|
31
|
+
after: number[];
|
|
32
|
+
}
|
|
33
|
+
|
|
21
34
|
export interface PlannedTask {
|
|
22
35
|
id: string;
|
|
23
36
|
title: string;
|
|
@@ -29,6 +42,8 @@ export interface PlannedTask {
|
|
|
29
42
|
issue?: IssueRef;
|
|
30
43
|
/** The bot-lobby task started from this entry. */
|
|
31
44
|
startedTaskId?: string;
|
|
45
|
+
/** Set when the plan was split into several tasks and this is one of them. */
|
|
46
|
+
split?: SplitInfo;
|
|
32
47
|
}
|
|
33
48
|
|
|
34
49
|
export function backlogDir(root: string, configDir: string): string {
|
|
@@ -48,7 +63,7 @@ function isPlannedTask(value: unknown): value is PlannedTask {
|
|
|
48
63
|
export function savePlannedTask(
|
|
49
64
|
root: string,
|
|
50
65
|
configDir: string,
|
|
51
|
-
input: { title: string; brief: string; issue?: IssueRef },
|
|
66
|
+
input: { title: string; brief: string; issue?: IssueRef; split?: SplitInfo },
|
|
52
67
|
now = new Date(),
|
|
53
68
|
): PlannedTask {
|
|
54
69
|
const title = input.title.trim() || "planned task";
|
|
@@ -58,7 +73,7 @@ export function savePlannedTask(
|
|
|
58
73
|
let id = base;
|
|
59
74
|
for (let n = 2; existsSync(entryPath(root, configDir, id)); n++) id = `${base}-${n}`;
|
|
60
75
|
const at = now.toISOString();
|
|
61
|
-
const entry: PlannedTask = { id, title, brief, createdAt: at, updatedAt: at, status: "pending", ...(input.issue ? { issue: input.issue } : {}) };
|
|
76
|
+
const entry: PlannedTask = { id, title, brief, createdAt: at, updatedAt: at, status: "pending", ...(input.issue ? { issue: input.issue } : {}), ...(input.split ? { split: input.split } : {}) };
|
|
62
77
|
writeFileEnsured(entryPath(root, configDir, id), JSON.stringify(entry, null, 2));
|
|
63
78
|
return entry;
|
|
64
79
|
}
|
|
@@ -109,8 +124,16 @@ export function discardPlannedTask(root: string, configDir: string, id: string):
|
|
|
109
124
|
rmSync(entryPath(root, configDir, id), { force: true });
|
|
110
125
|
}
|
|
111
126
|
|
|
112
|
-
/**
|
|
127
|
+
/** `Part 2 of 3 of one plan…` and the other parts' titles, for a task that is one part of a split plan. */
|
|
128
|
+
export function splitLine(split: SplitInfo): string {
|
|
129
|
+
const others = split.titles.map((title, index) => `${index + 1}. ${title}${index + 1 === split.part ? " (this task)" : ""}`).join("; ");
|
|
130
|
+
const before = split.after.length > 0 ? ` It builds on ${split.after.map((part) => `part ${part}`).join(" and ")}, which should be done first.` : "";
|
|
131
|
+
return `This is part ${split.part} of ${split.of} of one plan that was split into separate tasks: ${others}.${before} Do only this part; the others are their own tasks.`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The request a started task carries: the agreed plan, plus the issue it came from, plus where it sits in a split plan. */
|
|
113
135
|
export function plannedTaskRequest(entry: PlannedTask): string {
|
|
114
136
|
const source = entry.issue ? `\n\nFrom GitHub issue #${entry.issue.number}: ${entry.issue.title}${entry.issue.url ? ` (${entry.issue.url})` : ""}` : "";
|
|
115
|
-
|
|
137
|
+
const part = entry.split ? `\n\n${splitLine(entry.split)}` : "";
|
|
138
|
+
return `${entry.title}${part}\n\nAgreed plan (from the planning session):\n${entry.brief}${source}`;
|
|
116
139
|
}
|
package/src/state/metrics.ts
CHANGED
|
@@ -33,7 +33,7 @@ export interface MetricRecord {
|
|
|
33
33
|
cost?: number;
|
|
34
34
|
taskId?: string;
|
|
35
35
|
stalled?: boolean;
|
|
36
|
-
/** Classifier calls: what the call decided (seats, answers, files, triage, effort, test). */
|
|
36
|
+
/** Classifier calls: what the call decided (seats, answers, files, triage, effort, review, knowledge, test). */
|
|
37
37
|
purpose?: string;
|
|
38
38
|
/** A run the classifier routed down: the configured profile it came from. */
|
|
39
39
|
routedFrom?: string;
|
|
@@ -49,7 +49,7 @@ export interface ClassifierSummary {
|
|
|
49
49
|
p90Ms: number;
|
|
50
50
|
/** Tokens Jev read across every call. */
|
|
51
51
|
input: number;
|
|
52
|
-
/** Calls per decision (seats, answers, files, triage, effort, test). */
|
|
52
|
+
/** Calls per decision (seats, answers, files, triage, effort, review, knowledge, test). */
|
|
53
53
|
byPurpose: Record<string, number>;
|
|
54
54
|
seatRunsSkipped: number;
|
|
55
55
|
questionsAnswered: number;
|
package/src/state/persistence.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "../knowledge/paths.ts";
|
|
16
16
|
import { DEFAULT_KNOWLEDGE_CONTENT, ensureFile, readFileOr, writeFileEnsured } from "../knowledge/store.ts";
|
|
17
17
|
import { forgetCached, forgetCachedUnder, readJsonCached } from "./file-cache.ts";
|
|
18
|
+
import { taskName } from "../text.ts";
|
|
18
19
|
|
|
19
20
|
/** Idempotently create the full knowledge + tasks layout with seed files. */
|
|
20
21
|
export function ensureProjectStructure(root: string, configDir: string): void {
|
|
@@ -235,13 +236,12 @@ export function taskSlug(request: string, max = 40): string {
|
|
|
235
236
|
}
|
|
236
237
|
|
|
237
238
|
/**
|
|
238
|
-
* Task id from the request
|
|
239
|
-
*
|
|
239
|
+
* Task id from the request: its friendly name, `Task-<Words>-<DD-MM-YYYY>`
|
|
240
|
+
* (`Task-Change-Table-Font-27-09-2026`); a request with no words is
|
|
241
|
+
* `Task-Untitled-<date>`. Callers add a `-2`, `-3`… suffix when it is taken.
|
|
240
242
|
*/
|
|
241
243
|
export function nextTaskId(request: string, now = new Date()): string {
|
|
242
|
-
|
|
243
|
-
if (slug.length > 0) return `TASK-${slug}`;
|
|
244
|
-
return `TASK-task-${now.toISOString().replace(/[-:T]/g, "").slice(0, 14)}`;
|
|
244
|
+
return taskName(request, now);
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
/** Files that make up a task's temporary working state (§20). */
|
package/src/text.ts
CHANGED
|
@@ -57,6 +57,44 @@ export function shortTitle(request: string, maxWords = 3): string {
|
|
|
57
57
|
return (content.length > 0 ? content : words).slice(0, maxWords).join(" ").replace(/[:;,]+$/, "");
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/** Words a task's name leaves out: they join the words that matter without being any of them. */
|
|
61
|
+
const NAME_CONNECTORS = new Set(["and", "or", "but", "with", "without", "on", "in", "at", "of", "by", "from", "into", "so", "as", "is", "are", "be", "it", "its", "that", "this", "then", "also", "just", "some"]);
|
|
62
|
+
|
|
63
|
+
/** `27-09-2026`: the local date as day-month-year, as task names carry it. */
|
|
64
|
+
export function dateStamp(now: Date): string {
|
|
65
|
+
return `${String(now.getDate()).padStart(2, "0")}-${String(now.getMonth() + 1).padStart(2, "0")}-${now.getFullYear()}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The first content words of a request as one name part, each capitalised and
|
|
70
|
+
* joined by dashes (`Change-Table-Font`): at most `maxWords` words and
|
|
71
|
+
* `maxChars` characters, cut between words. Empty when the request has no
|
|
72
|
+
* letters or digits.
|
|
73
|
+
*/
|
|
74
|
+
export function nameWords(request: string, maxWords = 5, maxChars = 36): string {
|
|
75
|
+
// Connectors (`and`, `on`, `with`…) say little in a name and must not fill its few slots or end it.
|
|
76
|
+
const words = shortTitle(request, maxWords * 3)
|
|
77
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
78
|
+
.filter((word) => word.length > 0 && !NAME_CONNECTORS.has(word.toLowerCase()))
|
|
79
|
+
.map((word) => word.charAt(0).toLocaleUpperCase() + word.slice(1))
|
|
80
|
+
.slice(0, maxWords);
|
|
81
|
+
const kept: string[] = [];
|
|
82
|
+
for (const word of words) {
|
|
83
|
+
if ([...kept, word].join("-").length > maxChars && kept.length > 0) break;
|
|
84
|
+
kept.push(word);
|
|
85
|
+
}
|
|
86
|
+
return kept.join("-").slice(0, maxChars).replace(/-+$/, "");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A task's friendly name: `Task-Change-Table-Font-27-09-2026`, from its
|
|
91
|
+
* request (or title) and the day it started. It is the task's id, the name of
|
|
92
|
+
* the git branch it works on and of the session that drives it.
|
|
93
|
+
*/
|
|
94
|
+
export function taskName(request: string, now = new Date()): string {
|
|
95
|
+
return `Task-${nameWords(request) || "Untitled"}-${dateStamp(now)}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
60
98
|
/** Headings that only label a section (`### Objective`), not name the work. */
|
|
61
99
|
const LABEL_HEADING = /^#{1,6}\s*(objective|goal|goals|summary|overview|task|request|context|background|description|plan)\s*:?\s*$/i;
|
|
62
100
|
|
package/src/workflow/workflow.ts
CHANGED
|
@@ -55,12 +55,14 @@ import { pingApproval } from "../pi/notify.ts";
|
|
|
55
55
|
import { describeRun, runLogEntry } from "../pi/run-summary.ts";
|
|
56
56
|
import { nextStates } from "./transitions.ts";
|
|
57
57
|
import type { FileHinter } from "../classifier/files.ts";
|
|
58
|
+
import type { KnowledgePicker } from "../classifier/knowledge.ts";
|
|
58
59
|
import type { Classifier } from "../classifier/classifier.ts";
|
|
59
60
|
import type { EffortRouter } from "../classifier/effort.ts";
|
|
60
61
|
import { answerClarify } from "../classifier/triage.ts";
|
|
61
62
|
import { appendMetrics, metricFromRun } from "../state/metrics.ts";
|
|
62
63
|
import { markCommentsAddressed, pendingComments, readPlanComments } from "../state/comments.ts";
|
|
63
64
|
import { fastNext, onFastTrack, parseRoster, qaRequired, qaStillDue, qaTookPart, rosterWords, trackSummary } from "./track.ts";
|
|
65
|
+
import { missingWorktree, taskCwd } from "../execution/workspace.ts";
|
|
64
66
|
|
|
65
67
|
export const ORCHESTRATE_ACTIONS = [
|
|
66
68
|
"clarify",
|
|
@@ -140,6 +142,8 @@ export interface WorkflowDeps {
|
|
|
140
142
|
runProcess?: ProcessRunner;
|
|
141
143
|
/** Likely files for scouts and workers, while the classifier's file hints are on. */
|
|
142
144
|
hints?: FileHinter;
|
|
145
|
+
/** Picks the relevant sections of long knowledge files for scouts, workers and the QA gate, while the classifier's knowledge picks are on. */
|
|
146
|
+
knowledge?: KnowledgePicker;
|
|
143
147
|
/** Answers a clarify question whose recommended option is clearly right, when it is on. */
|
|
144
148
|
classifier?: Classifier;
|
|
145
149
|
/** Re-reads a request after an amendment (the task's triage), when the classifier is on. */
|
|
@@ -338,6 +342,7 @@ async function handleScout(task: Task, params: OrchestrateParams, deps: Workflow
|
|
|
338
342
|
signal: deps.signal,
|
|
339
343
|
onUpdate: deps.onUpdate,
|
|
340
344
|
...(deps.hints ? { hints: deps.hints } : {}),
|
|
345
|
+
...(deps.knowledge ? { knowledge: deps.knowledge } : {}),
|
|
341
346
|
...(deps.effort ? { effort: deps.effort } : {}),
|
|
342
347
|
},
|
|
343
348
|
deps.runProcess ?? spawnPiProcess,
|
|
@@ -677,6 +682,7 @@ function workerRequest(deps: WorkflowDeps, task: Task, domain: Domain, instructi
|
|
|
677
682
|
signal: deps.signal,
|
|
678
683
|
onUpdate: deps.onUpdate,
|
|
679
684
|
...(deps.hints ? { hints: deps.hints } : {}),
|
|
685
|
+
...(deps.knowledge ? { knowledge: deps.knowledge } : {}),
|
|
680
686
|
...(deps.effort ? { effort: deps.effort } : {}),
|
|
681
687
|
};
|
|
682
688
|
}
|
|
@@ -1105,6 +1111,7 @@ function qaRequest(deps: WorkflowDeps, task: Task, diff: string, instruction?: s
|
|
|
1105
1111
|
profile: deps.profile,
|
|
1106
1112
|
signal: deps.signal,
|
|
1107
1113
|
onUpdate: deps.onUpdate,
|
|
1114
|
+
...(deps.knowledge ? { knowledge: deps.knowledge } : {}),
|
|
1108
1115
|
};
|
|
1109
1116
|
}
|
|
1110
1117
|
|
|
@@ -1680,9 +1687,13 @@ export async function runWorkflowAction(params: OrchestrateParams, deps: Workflo
|
|
|
1680
1687
|
if (!handler) {
|
|
1681
1688
|
return { ok: false, taskId: task.id, state: task.state, message: `Unknown action "${params.action}".` };
|
|
1682
1689
|
}
|
|
1690
|
+
const gone = missingWorktree(task);
|
|
1691
|
+
if (gone && params.action !== "status") return { ok: false, taskId: task.id, state: task.state, message: `Rejected: ${gone}` };
|
|
1683
1692
|
const finished = new Map<string, AgentRun>();
|
|
1684
1693
|
const tracked: WorkflowDeps = {
|
|
1685
1694
|
...deps,
|
|
1695
|
+
// Every agent, diff and desk of a task with its own worktree works there.
|
|
1696
|
+
cwd: taskCwd(task, deps.cwd),
|
|
1686
1697
|
onUpdate: (run) => {
|
|
1687
1698
|
if (run.status !== "running") finished.set(run.runId, run);
|
|
1688
1699
|
deps.onUpdate?.(run);
|