@a-t-h-i/bot-lobby 0.6.4 → 0.6.6
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 +150 -12
- package/package.json +12 -3
- package/prompts/backend.md +3 -1
- package/prompts/designer.md +24 -1
- package/prompts/global.md +18 -0
- package/prompts/master.md +122 -20
- package/prompts/quickfix.md +16 -3
- package/prompts/worker.md +7 -2
- package/src/agents/backend.ts +2 -2
- package/src/agents/designer.ts +2 -2
- package/src/ask/dialog.ts +11 -1
- package/src/classifier/triage.ts +24 -7
- package/src/index.ts +4 -1
- package/src/lobby/feed.ts +7 -2
- package/src/lobby/keys.ts +0 -1
- package/src/lobby/quickfix.ts +29 -3
- package/src/lobby/runtime.ts +29 -26
- package/src/lobby/tabs/home.ts +6 -42
- package/src/lobby/tabs/quickfix.ts +1 -1
- package/src/lobby/tabs/tasks.ts +3 -2
- package/src/lobby/view.ts +18 -23
- package/src/master/decisions.ts +10 -2
- package/src/pi/activity.ts +1 -33
- package/src/pi/commands.ts +22 -11
- package/src/pi/events.ts +3 -17
- package/src/pi/plan-checklist.ts +305 -0
- package/src/pi/route.ts +180 -0
- package/src/pi/run-summary.ts +12 -3
- package/src/pi/settings-ui.ts +2 -2
- package/src/pi/start-task.ts +51 -5
- package/src/pi/tools.ts +13 -7
- package/src/pi/ui.ts +18 -284
- package/src/roles/worker.ts +11 -3
- package/src/schemas/configuration.ts +19 -6
- package/src/schemas/task.ts +31 -0
- package/src/workflow/brief.ts +59 -0
- package/src/workflow/track.ts +436 -0
- package/src/workflow/workflow.ts +151 -10
- package/src/pi/expressions.ts +0 -169
- package/src/pi/kaomoji.ts +0 -227
- package/src/pi/mascot-art.ts +0 -359
- package/src/pi/zen-large.ts +0 -699
- package/src/pi/zen-metrics.ts +0 -130
- package/src/pi/zen.ts +0 -659
package/src/classifier/triage.ts
CHANGED
|
@@ -48,6 +48,15 @@ function sizeQuestion(subject: string) {
|
|
|
48
48
|
return score(`How big is the change ${subject} asks for, judged by what it would take to build and verify in this repository?`, SIZE_LEVELS);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** Whether one agent can just do it: the quick fix or the team. */
|
|
52
|
+
function soloQuestion(subject: string) {
|
|
53
|
+
return noul(
|
|
54
|
+
`Could one engineer do ${subject} alone, right away — in one file or one area of the code — with no contract to agree between frontend and backend, no unfamiliar codebase to survey first, no risky change (security, data, payments, migrations) and no decision the user must make first?`,
|
|
55
|
+
"Yes: one person can just do it now, even if it is a rich piece of work in one place.",
|
|
56
|
+
"No: it needs a team — several areas, a survey of the codebase, a plan to agree, or review before it lands.",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
51
60
|
export function triageRequest(input: TriageInput): SystemOneRequest {
|
|
52
61
|
const questions: SystemOneRequest["questions"] = {
|
|
53
62
|
size: sizeQuestion("`request`"),
|
|
@@ -62,6 +71,7 @@ export function triageRequest(input: TriageInput): SystemOneRequest {
|
|
|
62
71
|
"No: a competent engineer could build it as written, deciding details from the codebase.",
|
|
63
72
|
),
|
|
64
73
|
kind: choice("What kind of work does `request` ask for?", TRIAGE_KINDS),
|
|
74
|
+
solo: soloQuestion("`request`"),
|
|
65
75
|
};
|
|
66
76
|
for (const { domain, owns } of input.domains) {
|
|
67
77
|
questions[`domain_${domain}`] = noul(`Does building \`request\` require changes in this area: ${owns}?`);
|
|
@@ -85,12 +95,14 @@ export async function triageTask(classifier: Classifier, input: TriageInput, sig
|
|
|
85
95
|
if (probability !== undefined) domains[domain] = probability;
|
|
86
96
|
}
|
|
87
97
|
const kind = choiceOf(result.answers, "kind");
|
|
98
|
+
const solo = yesOf(result.answers, "solo");
|
|
88
99
|
return {
|
|
89
100
|
size: TRIAGE_SIZES[Math.max(0, Math.min(TRIAGE_SIZES.length - 1, size.level))]!,
|
|
90
101
|
sizeConfidence: size.confidence,
|
|
91
102
|
domains,
|
|
92
103
|
research: yesOf(result.answers, "needs_research") ?? 0,
|
|
93
104
|
ambiguous: yesOf(result.answers, "ambiguous") ?? 0,
|
|
105
|
+
...(solo !== undefined ? { solo } : {}),
|
|
94
106
|
...(kind ? { kind: kind.choice, kindProbability: kind.probability } : {}),
|
|
95
107
|
at: new Date().toISOString(),
|
|
96
108
|
};
|
|
@@ -131,20 +143,25 @@ export async function triageWithContext(classifier: Classifier, scope: FileScope
|
|
|
131
143
|
return files.length > 0 ? { ...triage, likelyFiles: files } : triage;
|
|
132
144
|
}
|
|
133
145
|
|
|
134
|
-
/**
|
|
135
|
-
|
|
146
|
+
/**
|
|
147
|
+
* The size of a quick fix prompt, and how likely one engineer can do it alone,
|
|
148
|
+
* or undefined when the classifier is off or fails. A large prompt is held as
|
|
149
|
+
* a task unless it is still one engineer's work (a rich page in one file).
|
|
150
|
+
*/
|
|
151
|
+
export async function quickFixSize(classifier: Classifier, prompt: string, signal?: AbortSignal): Promise<{ size: TriageSize; confidence: number; solo?: number } | undefined> {
|
|
136
152
|
if (!classifier.enabled("triage")) return undefined;
|
|
137
|
-
const largeAt = classifier.config.thresholds
|
|
138
|
-
const result = await classifier.ask("triage", { state: { prompt: clip(prompt, 6000) }, questions: { size: sizeQuestion("`prompt`") } }, {
|
|
153
|
+
const { quickFixLargeAt: largeAt, quickFixAt } = classifier.config.thresholds;
|
|
154
|
+
const result = await classifier.ask("triage", { state: { prompt: clip(prompt, 6000) }, questions: { size: sizeQuestion("`prompt`"), solo: soloQuestion("`prompt`") } }, {
|
|
139
155
|
...(signal ? { signal } : {}),
|
|
140
156
|
saved: (answers) => {
|
|
141
157
|
const sized = scoreOf(answers, "size");
|
|
142
|
-
return sized && sized.level >= TRIAGE_SIZES.length - 1 && sized.confidence >= largeAt ? 1 : 0;
|
|
158
|
+
return sized && sized.level >= TRIAGE_SIZES.length - 1 && sized.confidence >= largeAt && (yesOf(answers, "solo") ?? 0) < quickFixAt ? 1 : 0;
|
|
143
159
|
},
|
|
144
160
|
});
|
|
145
161
|
const size = scoreOf(result?.answers, "size");
|
|
146
162
|
if (!size) return undefined;
|
|
147
|
-
|
|
163
|
+
const solo = yesOf(result?.answers, "solo");
|
|
164
|
+
return { size: TRIAGE_SIZES[Math.max(0, Math.min(TRIAGE_SIZES.length - 1, size.level))]!, confidence: size.confidence, ...(solo !== undefined ? { solo } : {}) };
|
|
148
165
|
}
|
|
149
166
|
|
|
150
167
|
const HINT = 0.5;
|
|
@@ -155,7 +172,7 @@ export function suggestedPath(triage: TaskTriage): string {
|
|
|
155
172
|
if (triage.ambiguous >= HINT) return "clarify first: the request as written probably misses a decision.";
|
|
156
173
|
const smallish = (triage.size === "trivial" || triage.size === "small") && triage.sizeConfidence >= 0.6;
|
|
157
174
|
const steps: string[] = [];
|
|
158
|
-
if (smallish && touched.length === 1) steps.push(`
|
|
175
|
+
if (smallish && touched.length === 1) steps.push(`fast track (${touched[0]}): no scouts, proposal or plan; delegate straight away`);
|
|
159
176
|
else if (touched.length > 0) steps.push(`scout only ${touched.join(", ")}`);
|
|
160
177
|
if (triage.research >= HINT) steps.push("summon the researcher for the outside facts");
|
|
161
178
|
return steps.length > 0 ? `${steps.join("; ")}.` : "no strong signal; decide from the request.";
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { CONFIG_DIR_NAME, type ExtensionAPI } from "@earendil-works/pi-coding-ag
|
|
|
2
2
|
import { registerCommands } from "./pi/commands.ts";
|
|
3
3
|
import { registerLifecycle } from "./pi/events.ts";
|
|
4
4
|
import { registerOrchestrateTool } from "./pi/tools.ts";
|
|
5
|
+
import { registerRouteTool } from "./pi/route.ts";
|
|
5
6
|
import { onTransition } from "./state/task-state.ts";
|
|
6
7
|
import { pingTransition } from "./pi/notify.ts";
|
|
7
8
|
import { isSubagentProcess } from "./pi/quiet.ts";
|
|
@@ -20,12 +21,14 @@ export default function (pi: ExtensionAPI): void {
|
|
|
20
21
|
// Before the lifecycle, so a task that ended while the oracle was idle is closed before it builds the next turn's prompt.
|
|
21
22
|
registerFreshContext(pi, CONFIG_DIR_NAME);
|
|
22
23
|
registerLifecycle(pi, CONFIG_DIR_NAME);
|
|
23
|
-
// After the lifecycle, so the lobby opens over a task the
|
|
24
|
+
// After the lifecycle, so the lobby opens over a task the status already knows.
|
|
24
25
|
registerLobbyEvents(pi, CONFIG_DIR_NAME);
|
|
25
26
|
registerOwner(pi, CONFIG_DIR_NAME);
|
|
26
27
|
onTransition((task) => pingTransition(task));
|
|
27
28
|
registerCommands(pi, CONFIG_DIR_NAME);
|
|
28
29
|
registerOrchestrateTool(pi, CONFIG_DIR_NAME);
|
|
30
|
+
// A new request one agent can do alone: the oracle confirms, and the lobby hands it to the quick-fix agent.
|
|
31
|
+
registerRouteTool(pi, CONFIG_DIR_NAME);
|
|
29
32
|
// The questionnaire the oracle (and pi without a task) asks the user with; no other extension is needed for it.
|
|
30
33
|
registerAskTool(pi);
|
|
31
34
|
// The web tools, for the researcher and for pi without a task (the oracle leaves them to the researcher).
|
package/src/lobby/feed.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* What the lobby narrates: an activity log of plain-words tool steps from the
|
|
3
3
|
* Master and every subagent, a single place for thoughts, and the conversation
|
|
4
4
|
* with the Master stripped of tool rows and thinking. Bounded ring buffers in
|
|
5
|
-
* one module-level store (like the
|
|
5
|
+
* one module-level store (like the task state in ui.ts), so every event source can
|
|
6
6
|
* write without a reference to the lobby, and the lobby repaints on `version`.
|
|
7
7
|
*/
|
|
8
8
|
import type { AgentRun } from "../schemas/findings.ts";
|
|
@@ -284,8 +284,13 @@ export function chatText(role: "user" | "assistant", text: string): Array<{ role
|
|
|
284
284
|
const kickoff = /^A bot-lobby task is active: (\S+)\nTitle: (.*)/.exec(body);
|
|
285
285
|
if (kickoff) {
|
|
286
286
|
const request = /\nRequest: ([\s\S]*?)(?:\nState: |$)/.exec(body)?.[1]?.trim();
|
|
287
|
-
|
|
287
|
+
// A request the oracle routed to the team is already in the conversation, under the routing note.
|
|
288
|
+
const shown = /\nRouted: /.test(body);
|
|
289
|
+
return [{ role: "note", text: `task started · ${kickoff[2]}` }, ...(request && !shown ? [{ role: "you" as const, text: request }] : [])];
|
|
288
290
|
}
|
|
291
|
+
// A new request the classifier read as a quick fix, before the oracle confirms where it goes.
|
|
292
|
+
const routing = /^bot-lobby: a new request, not a task yet\.\nRequest: ([\s\S]*?)\nRead: /.exec(body);
|
|
293
|
+
if (routing) return [{ role: "you", text: routing[1]!.trim() }, { role: "note", text: "reads as a quick fix · the oracle confirms where it goes" }];
|
|
289
294
|
const comment = /^The user left (?:a comment|\d+ comments) on (the approved plan|the proposal) of (\S+) from the lobby:/.exec(body);
|
|
290
295
|
if (comment) return [{ role: "note", text: `your comment on ${comment[1]} went to the oracle` }];
|
|
291
296
|
return [{ role: "you", text: body }];
|
package/src/lobby/keys.ts
CHANGED
|
@@ -17,7 +17,6 @@ export const LOBBY_ACTIONS = {
|
|
|
17
17
|
toggleAuto: { key: "alt+g", help: "auto mode: the oracle drives the task to completion without asking" },
|
|
18
18
|
nextTab: { key: "tab", help: "next tab" },
|
|
19
19
|
prevTab: { key: "shift+tab", help: "previous tab" },
|
|
20
|
-
toggleScene: { key: "alt+z", help: "show or hide the oracle and agent animations" },
|
|
21
20
|
toggleConversation: { key: "alt+c", help: "show or hide the conversation" },
|
|
22
21
|
toggleActivity: { key: "alt+a", help: "show or hide the activity log" },
|
|
23
22
|
toggleThinking: { key: "alt+k", help: "show or hide thinking" },
|
package/src/lobby/quickfix.ts
CHANGED
|
@@ -54,6 +54,19 @@ export interface QuickFixJob {
|
|
|
54
54
|
routedFrom?: string;
|
|
55
55
|
/** Files it changed with `edit`/`write`, as shown (relative to the project); the change ledger lets a running task's QA gate tell them from its own. */
|
|
56
56
|
files?: string[];
|
|
57
|
+
/** Runs on this profile instead of the quick-fix settings: a quick feature the oracle routed here gets its builder's model, thinking and time. */
|
|
58
|
+
profile?: QuickFixProfile;
|
|
59
|
+
/** The oracle sent it here from a request typed to the lobby. */
|
|
60
|
+
routed?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** How a job enters the queue besides its prompt. */
|
|
64
|
+
export interface SubmitOptions {
|
|
65
|
+
/** Skip the classifier's "looks like a task" hold (the oracle already routed it here). */
|
|
66
|
+
force?: boolean;
|
|
67
|
+
profile?: QuickFixProfile;
|
|
68
|
+
/** Shown with the job: where it came from. */
|
|
69
|
+
note?: string;
|
|
57
70
|
}
|
|
58
71
|
|
|
59
72
|
export interface QuickFixProfile {
|
|
@@ -118,10 +131,21 @@ export class QuickFixQueue {
|
|
|
118
131
|
}
|
|
119
132
|
|
|
120
133
|
/** Queue a quick fix; it starts at once when nothing else is running. */
|
|
121
|
-
submit(prompt: string, now = Date.now()): QuickFixJob {
|
|
134
|
+
submit(prompt: string, now = Date.now(), options: SubmitOptions = {}): QuickFixJob {
|
|
122
135
|
const text = prompt.trim();
|
|
123
136
|
if (!text) throw new Error("a quick fix needs a prompt");
|
|
124
|
-
const job: QuickFixJob = {
|
|
137
|
+
const job: QuickFixJob = {
|
|
138
|
+
id: `QF-${++this.counter}`,
|
|
139
|
+
prompt: text,
|
|
140
|
+
status: "queued",
|
|
141
|
+
createdAt: now,
|
|
142
|
+
steps: [],
|
|
143
|
+
tools: 0,
|
|
144
|
+
turns: 0,
|
|
145
|
+
...(options.force ? { force: true, routed: true } : {}),
|
|
146
|
+
...(options.profile ? { profile: options.profile } : {}),
|
|
147
|
+
...(options.note ? { note: options.note } : {}),
|
|
148
|
+
};
|
|
125
149
|
this.jobs = [...this.jobs, job].filter((entry, index, all) => isActive(entry) || index >= all.length - MAX_JOBS);
|
|
126
150
|
this.changed();
|
|
127
151
|
this.pump();
|
|
@@ -203,7 +227,7 @@ export class QuickFixQueue {
|
|
|
203
227
|
}
|
|
204
228
|
|
|
205
229
|
private async run(job: QuickFixJob): Promise<void> {
|
|
206
|
-
const profile = this.deps.profile();
|
|
230
|
+
const profile = job.profile ?? this.deps.profile();
|
|
207
231
|
const controller = new AbortController();
|
|
208
232
|
this.controllers.set(job.id, controller);
|
|
209
233
|
job.status = "running";
|
|
@@ -287,6 +311,8 @@ export class QuickFixQueue {
|
|
|
287
311
|
try {
|
|
288
312
|
const sized = await quickFixSize(jev, job.prompt, signal);
|
|
289
313
|
if (!sized || sized.size !== "large" || sized.confidence < jev.config.thresholds.quickFixLargeAt) return undefined;
|
|
314
|
+
// Large but still one engineer's work (a rich page in one file): it runs.
|
|
315
|
+
if ((sized.solo ?? 0) >= jev.config.thresholds.quickFixAt) return undefined;
|
|
290
316
|
return `looks like a task (large, ${sized.confidence.toFixed(2)})`;
|
|
291
317
|
} catch {
|
|
292
318
|
return undefined;
|
package/src/lobby/runtime.ts
CHANGED
|
@@ -20,13 +20,14 @@ import { archiveTask as archiveTaskOnDisk, deleteTask as deleteTaskOnDisk, listA
|
|
|
20
20
|
import { discardPlannedTask, listPlannedTasks, type PlannedTask } from "../state/backlog.ts";
|
|
21
21
|
import { appendMetrics, readClassifierMetrics, readMetrics, type MetricStatus } from "../state/metrics.ts";
|
|
22
22
|
import { describeToolCall } from "../pi/activity.ts";
|
|
23
|
-
import { applyStatus, currentZenTask, isMinimized, onMinimizeChange, onRunUpdates,
|
|
24
|
-
import { panelLines } from "../pi/zen.ts";
|
|
23
|
+
import { applyStatus, currentZenTask, isMinimized, onMinimizeChange, onRunUpdates, setMinimized, taskSnapshot } from "../pi/ui.ts";
|
|
25
24
|
import { shortTitle } from "../text.ts";
|
|
26
25
|
import { isSubagentProcess } from "../pi/quiet.ts";
|
|
27
26
|
import { modelRef, resolveLobbyProfile, resolvePanelProfile } from "../pi/model-support.ts";
|
|
28
27
|
import { modelLookup } from "../pi/tools.ts";
|
|
29
|
-
import { startPlannedTask
|
|
28
|
+
import { startPlannedTask } from "../pi/start-task.ts";
|
|
29
|
+
import { pendingRequest, setQuickFixHandoff, startRequest } from "../pi/route.ts";
|
|
30
|
+
import type { Domain } from "../schemas/agent.ts";
|
|
30
31
|
import type { LobbyAgentKind, LobbyPanel, PanelMember } from "../schemas/configuration.ts";
|
|
31
32
|
import { chatFromEntries, lobbyFeed, narrateEvent, type AgentEventLike, type ChatEntry } from "./feed.ts";
|
|
32
33
|
import { classifier, effortFor, hintsFor } from "../classifier/instance.ts";
|
|
@@ -41,7 +42,6 @@ import { LobbyView, type LiveSession, type LobbyHost, type SwitchTarget, type Ta
|
|
|
41
42
|
import { lobbyTheme } from "./theme.ts";
|
|
42
43
|
import { deliverComments, onOwnerEvent, setAuto } from "../pi/owner.ts";
|
|
43
44
|
import { openEntrySettings, openSettings } from "../pi/settings-ui.ts";
|
|
44
|
-
import { budgetClock } from "../state/budget.ts";
|
|
45
45
|
|
|
46
46
|
export const ANCHOR_KEY = "bot-lobby-anchor";
|
|
47
47
|
export { deliverComments };
|
|
@@ -57,7 +57,6 @@ interface Runtime {
|
|
|
57
57
|
visible: boolean;
|
|
58
58
|
/** Stepped aside while pi shows a dialog. */
|
|
59
59
|
asideForPrompt: boolean;
|
|
60
|
-
scene: ZenScene;
|
|
61
60
|
quickfix: QuickFixQueue;
|
|
62
61
|
planner?: PlanningSession;
|
|
63
62
|
issues: IssuesState;
|
|
@@ -149,9 +148,10 @@ function toOracle(state: Runtime, text: string): string | undefined {
|
|
|
149
148
|
state.pi.sendUserMessage(text, { expandPromptTemplates: true, ...(busy ? { deliverAs: "followUp" as const } : {}) });
|
|
150
149
|
return `sent ${text.split(/\s+/)[0]} to pi (built-in commands need the lobby hidden: alt+l)`;
|
|
151
150
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
151
|
+
// While the oracle decides where a request goes, what the user types joins that conversation.
|
|
152
|
+
if (!currentZenTask() && !pendingRequest()) {
|
|
153
|
+
startRequest(state.pi, state.ctx, state.configDir, text).catch((error: Error) => failed(state, "could not start the task", error));
|
|
154
|
+
return "reading your request — a quick fix or a task, the oracle takes it from here";
|
|
155
155
|
}
|
|
156
156
|
state.pi.sendUserMessage(text, busy ? { deliverAs: "steer" } : undefined);
|
|
157
157
|
return undefined;
|
|
@@ -423,13 +423,6 @@ function deleteTask(state: Runtime, taskId: string, where: "list" | "archive"):
|
|
|
423
423
|
return `deleted ${taskId} for good`;
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
-
/** A task's status box without animations, for a session other than this window's. */
|
|
427
|
-
function taskScene(state: Runtime, task: Task, width: number, height: number): string[] {
|
|
428
|
-
const now = Date.now();
|
|
429
|
-
const time = budgetClock(state.root, state.configDir, task.id, now);
|
|
430
|
-
return panelLines(task, persistedRuns(task), now, false, { width, rows: Math.floor(height / 0.75), still: true, theme: state.ctx.ui.theme, ...(time ? { time } : {}) });
|
|
431
|
-
}
|
|
432
|
-
|
|
433
426
|
function seatProfile(state: Runtime, member: PanelMember) {
|
|
434
427
|
return resolvePanelProfile(loadConfig(), member, {
|
|
435
428
|
lookup: modelLookup(state.ctx),
|
|
@@ -515,15 +508,9 @@ function host(state: Runtime, tui: TUI): LobbyHost {
|
|
|
515
508
|
theme: () => lobbyTheme(state.ctx.ui.theme),
|
|
516
509
|
sessionId: () => state.ctx.sessionManager.getSessionId(),
|
|
517
510
|
zen: () => {
|
|
518
|
-
const snapshot =
|
|
511
|
+
const snapshot = taskSnapshot();
|
|
519
512
|
return { ...(snapshot.task ? { task: snapshot.task } : {}), runs: snapshot.runs };
|
|
520
513
|
},
|
|
521
|
-
// The scene's height budget is 3/4 of the rows it is given.
|
|
522
|
-
scene: (width, height, animated) => state.scene.lines(width, Math.floor(height / 0.75), state.ctx.ui.theme, Date.now(), !animated),
|
|
523
|
-
advanceScene: (now) => {
|
|
524
|
-
state.scene.advance(now);
|
|
525
|
-
return state.scene.delay(now);
|
|
526
|
-
},
|
|
527
514
|
feed: lobbyFeed,
|
|
528
515
|
masterBusy: () => !state.ctx.isIdle(),
|
|
529
516
|
tasks: () => peekTasks(state.root, state.configDir),
|
|
@@ -574,7 +561,6 @@ function host(state: Runtime, tui: TUI): LobbyHost {
|
|
|
574
561
|
sessionChat: (sessionId) => chats.chat(sessionId),
|
|
575
562
|
hasOlderChat: (sessionId) => chats.hasOlder(sessionId),
|
|
576
563
|
chatHistory: (sessionId) => chatHistory(state, sessionId),
|
|
577
|
-
taskScene: (task, width, height) => taskScene(state, task, width, height),
|
|
578
564
|
requestRender: () => tui.requestRender(),
|
|
579
565
|
};
|
|
580
566
|
}
|
|
@@ -706,17 +692,35 @@ function shutdown(): void {
|
|
|
706
692
|
const state = runtime;
|
|
707
693
|
if (!state) return;
|
|
708
694
|
runtime = undefined;
|
|
695
|
+
setQuickFixHandoff(undefined);
|
|
709
696
|
setMouse(state, false);
|
|
710
697
|
state.unsubscribeFeed?.();
|
|
711
698
|
state.quickfix.cancelAll();
|
|
712
699
|
state.planner?.cancel();
|
|
713
700
|
state.view?.dispose();
|
|
714
701
|
state.handle?.hide();
|
|
715
|
-
setWidgetSuppressor(() => false);
|
|
716
702
|
onRunUpdates(undefined);
|
|
717
703
|
onMinimizeChange(undefined);
|
|
718
704
|
}
|
|
719
705
|
|
|
706
|
+
/**
|
|
707
|
+
* The oracle routed a request to the quick-fix agent: queue it (no "looks like
|
|
708
|
+
* a task" hold, the oracle already decided), on its builder's settings when it
|
|
709
|
+
* is a quick feature, and show it on the Quick fix tab.
|
|
710
|
+
*/
|
|
711
|
+
function handToQuickFix(state: Runtime, request: string, builder: Domain | undefined, reason: string): string {
|
|
712
|
+
const quick = lobbyProfile(state, "quickfix");
|
|
713
|
+
const profile = builder ? { ...seatProfile(state, builder), instructions: quick.instructions } : undefined;
|
|
714
|
+
const job = state.quickfix.submit(request, Date.now(), {
|
|
715
|
+
force: true,
|
|
716
|
+
...(profile ? { profile } : {}),
|
|
717
|
+
note: `The oracle sent your request here: ${reason.replace(/[.\s]+$/, "")}.${builder ? ` A quick feature: it runs on ${builder === "designer" ? "DESIGN" : "DEV"}'s model, thinking and time limit.` : ""}`,
|
|
718
|
+
});
|
|
719
|
+
showLobby("quickfix");
|
|
720
|
+
state.view?.showQuickFix(job.id);
|
|
721
|
+
return job.id;
|
|
722
|
+
}
|
|
723
|
+
|
|
720
724
|
/** Create the session's lobby runtime (interactive master sessions only). */
|
|
721
725
|
export function initLobby(pi: ExtensionAPI, ctx: ExtensionContext, configDir: string): void {
|
|
722
726
|
shutdown();
|
|
@@ -733,7 +737,6 @@ export function initLobby(pi: ExtensionAPI, ctx: ExtensionContext, configDir: st
|
|
|
733
737
|
asking: false,
|
|
734
738
|
mouse: false,
|
|
735
739
|
inSettings: false,
|
|
736
|
-
scene: new ZenScene(),
|
|
737
740
|
quickfix: undefined as unknown as QuickFixQueue,
|
|
738
741
|
issues: new IssuesState(execCommand, ctx.cwd, rerender),
|
|
739
742
|
};
|
|
@@ -754,11 +757,11 @@ export function initLobby(pi: ExtensionAPI, ctx: ExtensionContext, configDir: st
|
|
|
754
757
|
effort: effortFor((model, thinking) => checkThinking(modelLookup(ctx)(model), thinking).level),
|
|
755
758
|
});
|
|
756
759
|
runtime = state;
|
|
760
|
+
setQuickFixHandoff((request, builder, reason) => (runtime === state ? handToQuickFix(state, request, builder, reason) : undefined));
|
|
757
761
|
lobbyFeed.clear();
|
|
758
762
|
// Only the newest messages are kept; the feed learns whether earlier ones exist, and loads them when scrolled to.
|
|
759
763
|
lobbyFeed.seedChat(chatFromEntries(ctx.sessionManager.getBranch(), Number.POSITIVE_INFINITY));
|
|
760
764
|
state.unsubscribeFeed = lobbyFeed.onChange(rerender);
|
|
761
|
-
setWidgetSuppressor(() => runtime?.visible === true);
|
|
762
765
|
onRunUpdates((runs) => lobbyFeed.runs(runs));
|
|
763
766
|
onMinimizeChange((value) => {
|
|
764
767
|
if (value) hideLobby();
|
package/src/lobby/tabs/home.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Lobby tab: the
|
|
3
|
-
* and agent animations, or just their status when animations are off), then the
|
|
2
|
+
* The Lobby tab: the status of this session's task on top, then the
|
|
4
3
|
* conversation with the oracle (text only, no tool rows, no thinking), the
|
|
5
4
|
* activity log of plain-words steps from every agent, and the one place where
|
|
6
|
-
* thoughts show up. Pure: the
|
|
5
|
+
* thoughts show up. Pure: the clock arrives as `now`.
|
|
7
6
|
*/
|
|
8
7
|
import { textWidth } from "../../width.ts";
|
|
9
8
|
import type { Task } from "../../schemas/task.ts";
|
|
@@ -18,8 +17,6 @@ export type HomePane = (typeof HOME_PANES)[number];
|
|
|
18
17
|
|
|
19
18
|
export interface HomeInput {
|
|
20
19
|
task?: Task;
|
|
21
|
-
/** Draws the zen scene into at most `height` lines, animated or still; absent when no task is active. */
|
|
22
|
-
scene?: (width: number, height: number, animated: boolean) => string[];
|
|
23
20
|
chat: readonly ChatEntry[];
|
|
24
21
|
/** The oracle's reply while it streams. */
|
|
25
22
|
liveReply?: string;
|
|
@@ -48,8 +45,6 @@ export interface HomeInput {
|
|
|
48
45
|
keys?: Record<LobbyPanel, string>;
|
|
49
46
|
/** Another session is shown: its name titles the conversation. */
|
|
50
47
|
title?: string;
|
|
51
|
-
/** The scene is another session's still status (no animations to toggle). */
|
|
52
|
-
stillScene?: boolean;
|
|
53
48
|
/** What the empty conversation says instead of the default (a session starting, say). */
|
|
54
49
|
emptyNote?: string;
|
|
55
50
|
/** What the empty activity log says instead of "No activity yet.". */
|
|
@@ -60,11 +55,6 @@ export interface HomeInput {
|
|
|
60
55
|
|
|
61
56
|
/** Wide terminals put the conversation and the activity log side by side. */
|
|
62
57
|
export const HOME_COLUMNS_MIN = 100;
|
|
63
|
-
/** Most lines the scene may take, and its share of the body; the still status needs far fewer. */
|
|
64
|
-
const SCENE_SHARE = 0.45;
|
|
65
|
-
const MAX_SCENE = 36;
|
|
66
|
-
const MAX_STILL = 10;
|
|
67
|
-
|
|
68
58
|
const SOURCE_COLORS: Record<string, LobbyColor> = {
|
|
69
59
|
MASTER: "accent",
|
|
70
60
|
DEV: "success",
|
|
@@ -490,31 +480,6 @@ function thinkingContent(input: HomeInput, thoughts: readonly ThoughtEntry[], wi
|
|
|
490
480
|
return whole([paint(theme, "dim", input.query ? `No thought matches "${input.query}".` : "Thoughts from the oracle and every agent appear here, and only here.")]);
|
|
491
481
|
}
|
|
492
482
|
|
|
493
|
-
/**
|
|
494
|
-
* The scene: with animations off (the `animations` panel, off by default)
|
|
495
|
-
* only the task's status box, what the agents are doing and the checklist;
|
|
496
|
-
* with them on, the animated oracle and agents too. Its first line names the
|
|
497
|
-
* key that toggles the animations.
|
|
498
|
-
*/
|
|
499
|
-
function sceneLines(input: HomeInput, width: number, height: number, theme?: LobbyTheme): string[] {
|
|
500
|
-
if (!input.task || !input.scene) return [];
|
|
501
|
-
const animated = input.panels.animations && !input.stillScene;
|
|
502
|
-
const budget = Math.min(animated ? MAX_SCENE : MAX_STILL, Math.floor(height * SCENE_SHARE));
|
|
503
|
-
if (budget < (animated ? 6 : 4)) return [];
|
|
504
|
-
const lines = input.scene(width, budget, animated).slice(0, budget);
|
|
505
|
-
const note = input.keys && !input.stillScene ? `${input.keys.animations} ${animated ? "hides" : "shows"} animations` : undefined;
|
|
506
|
-
return keyNote(lines, width, note, theme);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/** `note` at the right end of the first line, dimmed, when it fits beside what is there. */
|
|
510
|
-
function keyNote(lines: readonly string[], width: number, note: string | undefined, theme?: LobbyTheme): string[] {
|
|
511
|
-
const first = lines[0];
|
|
512
|
-
if (!note || first === undefined) return [...lines];
|
|
513
|
-
const gap = width - textWidth(first) - textWidth(note) - 1;
|
|
514
|
-
if (gap < 2) return [...lines];
|
|
515
|
-
return [`${first}${" ".repeat(gap)}${paint(theme, "dim", note)}`, ...lines.slice(1)];
|
|
516
|
-
}
|
|
517
|
-
|
|
518
483
|
function hiddenHint(input: HomeInput, width: number, height: number, theme?: LobbyTheme): string[] {
|
|
519
484
|
const keys = input.keys;
|
|
520
485
|
const text = keys
|
|
@@ -536,12 +501,11 @@ export function renderHome(input: HomeInput, width: number, height: number, them
|
|
|
536
501
|
if (height <= 0) return [];
|
|
537
502
|
const { panels } = input;
|
|
538
503
|
const feed = filterFeed(input);
|
|
539
|
-
const
|
|
540
|
-
const rest = height - scene.length;
|
|
504
|
+
const rest = height;
|
|
541
505
|
const showMain = panels.conversation || panels.activity;
|
|
542
506
|
const thinkHeight = !panels.thinking ? 0 : !showMain ? rest : rest >= 18 ? Math.max(5, Math.floor(rest * 0.25)) : rest >= 10 ? 4 : 0;
|
|
543
507
|
const main = rest - thinkHeight;
|
|
544
|
-
if (!showMain && !panels.thinking) return fill(
|
|
508
|
+
if (!showMain && !panels.thinking) return fill(hiddenHint(input, width, rest, theme), height, width);
|
|
545
509
|
const matches = (count: number) => `${count} match${count === 1 ? "" : "es"}`;
|
|
546
510
|
/**
|
|
547
511
|
* One scrollable pane: only the lines it shows are drawn — its newest, or
|
|
@@ -563,7 +527,7 @@ export function renderHome(input: HomeInput, width: number, height: number, them
|
|
|
563
527
|
const thought = currentThought(feed.thoughts);
|
|
564
528
|
const thinkingNote = thought ? `${thought.source} · ${thought.live ? "thinking" : since(input.now - thought.at)}` : input.keys?.thinking;
|
|
565
529
|
const thinkingBox = pane("thinking", "Thinking", thinkingNote, (inner, need, anchor) => thinkingContent(input, feed.thoughts, inner, need, anchor, theme));
|
|
566
|
-
const top =
|
|
530
|
+
const top = 0;
|
|
567
531
|
let body: string[] = [];
|
|
568
532
|
if (main > 0 && panels.conversation && panels.activity) {
|
|
569
533
|
if (width >= HOME_COLUMNS_MIN) {
|
|
@@ -576,5 +540,5 @@ export function renderHome(input: HomeInput, width: number, height: number, them
|
|
|
576
540
|
} else if (main > 0 && panels.conversation) body = chatBox(top, 0, width, main);
|
|
577
541
|
else if (main > 0 && panels.activity) body = activityBox(top, 0, width, main);
|
|
578
542
|
const think = thinkHeight >= 3 ? thinkingBox(top + main, 0, width, thinkHeight) : [];
|
|
579
|
-
return fill([...
|
|
543
|
+
return fill([...fill(body, main), ...think], height, width);
|
|
580
544
|
}
|
|
@@ -65,7 +65,7 @@ export function jobDetailLines(job: QuickFixJob, width: number, tick: number, no
|
|
|
65
65
|
const asked = theme?.markdown ? markdownLines(job.prompt, width, theme, true, "you") : wrap(bold(theme, job.prompt), width);
|
|
66
66
|
const lines = [paint(theme, "dim", job.id), facts.join(paint(theme, "dim", " · ")), "", ...asked];
|
|
67
67
|
if (job.route) lines.push("", ...wrap(paint(theme, "dim", `↓ routed ${job.route}`), width));
|
|
68
|
-
if (job.note) lines.push("", ...wrap(paint(theme, "warning", job.status === "held" ? `‖ Held: ${job.note}. r runs it anyway · t starts it as a task in a new session.` : job.note), width));
|
|
68
|
+
if (job.note) lines.push("", ...wrap(paint(theme, job.status === "held" ? "warning" : job.routed ? "muted" : "warning", job.status === "held" ? `‖ Held: ${job.note}. r runs it anyway · t starts it as a task in a new session.` : job.note), width));
|
|
69
69
|
lines.push("", rule(width, "Steps", theme));
|
|
70
70
|
if (job.steps.length === 0) lines.push(paint(theme, "dim", job.status === "queued" ? "Waiting for the quick fix ahead of it." : job.status === "running" ? `${spinner(tick)} starting…` : job.status === "held" ? "Not started." : "No tool calls."));
|
|
71
71
|
for (const step of job.steps) {
|
package/src/lobby/tabs/tasks.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { TERMINAL_STATES, taskRequest, type Task } from "../../schemas/task.ts";
|
|
8
8
|
import type { PlannedTask } from "../../state/backlog.ts";
|
|
9
9
|
import type { PlanComment } from "../../state/comments.ts";
|
|
10
|
-
import { planChecklist } from "../../pi/
|
|
10
|
+
import { planChecklist } from "../../pi/plan-checklist.ts";
|
|
11
11
|
import { persistedRuns } from "../../pi/ui.ts";
|
|
12
12
|
import { describeRun, runFromLog } from "../../pi/run-summary.ts";
|
|
13
13
|
import { pendingApprovals } from "../../workflow/approvals.ts";
|
|
@@ -268,7 +268,8 @@ export function taskDetailLines(task: Task, comments: readonly PlanComment[], se
|
|
|
268
268
|
finished ? paint(theme, "muted", `${task.state === "abandoned" ? "dropped" : "done"} ${since(now - Date.parse(task.updatedAt))}`) : "",
|
|
269
269
|
].filter(Boolean);
|
|
270
270
|
lines.push(...wrapHanging(" ", facts.join(dot), width));
|
|
271
|
-
|
|
271
|
+
const track = task.track ? (task.track.path === "fast" ? `fast track (${task.track.size})` : `full workflow (${task.track.size})`) : "";
|
|
272
|
+
lines.push(...wrapHanging(" ", paint(theme, "dim", [task.id, track, task.domains.length > 0 ? task.domains.join(", ") : ""].filter(Boolean).join(" · ")), width));
|
|
272
273
|
const steps = task.plan ? planChecklist(task.plan, persistedRuns(task)) : [];
|
|
273
274
|
const done = steps.filter((step) => step.status === "done").length;
|
|
274
275
|
if (steps.length > 0) {
|
package/src/lobby/view.ts
CHANGED
|
@@ -54,8 +54,8 @@ const PROMPT_FIRST: ReadonlySet<TabId> = new Set(["lobby", "plan", "quickfix"]);
|
|
|
54
54
|
export type LobbyMode = "type" | "browse";
|
|
55
55
|
|
|
56
56
|
/** What each pane toggle is called in notices and help. */
|
|
57
|
-
const PANEL_NAMES: Record<LobbyPanel, string> = {
|
|
58
|
-
const PANEL_ACTIONS: Record<LobbyPanel, LobbyAction> = {
|
|
57
|
+
const PANEL_NAMES: Record<LobbyPanel, string> = { conversation: "conversation", activity: "activity log", thinking: "thinking" };
|
|
58
|
+
const PANEL_ACTIONS: Record<LobbyPanel, LobbyAction> = { conversation: "toggleConversation", activity: "toggleActivity", thinking: "toggleThinking" };
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* Which session the Lobby tab shows and talks to: this window, one it started
|
|
@@ -125,12 +125,8 @@ export interface LobbyHost {
|
|
|
125
125
|
rows(): number;
|
|
126
126
|
theme(): LobbyTheme;
|
|
127
127
|
sessionId(): string | undefined;
|
|
128
|
-
/** This session's active task and its runs, as the
|
|
128
|
+
/** This session's active task and its runs, as the lobby reads them. */
|
|
129
129
|
zen(): { task?: Task; runs: readonly AgentRun[] };
|
|
130
|
-
/** Draw the zen scene into at most `height` lines: animated, or only the task's status when `animated` is false. */
|
|
131
|
-
scene(width: number, height: number, animated: boolean): string[];
|
|
132
|
-
/** Advance the scene's clock; returns the delay it wants until the next step. */
|
|
133
|
-
advanceScene(now: number): number;
|
|
134
130
|
feed: LobbyFeed;
|
|
135
131
|
masterBusy(): boolean;
|
|
136
132
|
tasks(): Task[];
|
|
@@ -199,14 +195,12 @@ export interface LobbyHost {
|
|
|
199
195
|
hasOlderChat(sessionId: string): boolean;
|
|
200
196
|
/** A session's whole conversation, oldest first: this window's (no id) or another's; loaded only while scrolled back to it. */
|
|
201
197
|
chatHistory(sessionId?: string): readonly ChatEntry[];
|
|
202
|
-
/** A task's status without animations, for a session other than this window's. */
|
|
203
|
-
taskScene(task: Task, width: number, height: number): string[];
|
|
204
198
|
profileLabel(kind: LobbyAgentKind): string;
|
|
205
199
|
requestRender(): void;
|
|
206
200
|
now?(): number;
|
|
207
201
|
}
|
|
208
202
|
|
|
209
|
-
/** Live work speeds the clock up so spinners
|
|
203
|
+
/** Live work speeds the clock up so spinners move. */
|
|
210
204
|
export const LIVE_MS = 250;
|
|
211
205
|
export const IDLE_MS = 1000;
|
|
212
206
|
/** How often task and metrics data is reread from disk while the lobby is open. */
|
|
@@ -424,16 +418,13 @@ export class LobbyView implements Component, Focusable {
|
|
|
424
418
|
|| Boolean(zen.task && !zen.task.paused && !TERMINAL_STATES.includes(zen.task.state));
|
|
425
419
|
}
|
|
426
420
|
|
|
427
|
-
/** One clock step:
|
|
421
|
+
/** One clock step: reread data when due, repaint. */
|
|
428
422
|
private step(): void {
|
|
429
423
|
if (!this.running) return;
|
|
430
|
-
const now = this.now();
|
|
431
424
|
this.tick += 1;
|
|
432
|
-
let delay = this.isLive() ? LIVE_MS : IDLE_MS;
|
|
433
|
-
if (this.tab === "lobby" && this.panels.animations && this.host.zen().task) delay = Math.min(delay, this.host.advanceScene(now));
|
|
434
425
|
this.refreshData(false);
|
|
435
426
|
this.host.requestRender();
|
|
436
|
-
this.schedule(
|
|
427
|
+
this.schedule(this.isLive() ? LIVE_MS : IDLE_MS);
|
|
437
428
|
}
|
|
438
429
|
|
|
439
430
|
/** Reread tasks, plans, comments and metrics from disk (throttled unless forced). */
|
|
@@ -471,6 +462,15 @@ export class LobbyView implements Component, Focusable {
|
|
|
471
462
|
return visibleTabs(this.issuesOn);
|
|
472
463
|
}
|
|
473
464
|
|
|
465
|
+
/** Open the Quick fix tab on a job the oracle routed there, with a line saying so. */
|
|
466
|
+
showQuickFix(id: string): void {
|
|
467
|
+
this.setTab("quickfix");
|
|
468
|
+
delete this.queries.quickfix;
|
|
469
|
+
this.fixSelected = Math.max(0, this.fixJobs().findIndex((job) => job.id === id));
|
|
470
|
+
this.fixDetailOffset = 0;
|
|
471
|
+
this.say(`${id} — the oracle sent your request to the quick-fix agent`);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
474
|
setTab(tab: TabId): void {
|
|
475
475
|
if (tab === this.tab) return;
|
|
476
476
|
if (!this.tabs().includes(tab)) return this.say(`the ${TAB_LABELS[tab]} tab is off — set lobby.${tab} to true in the config to bring it back`, "warning");
|
|
@@ -666,8 +666,6 @@ export class LobbyView implements Component, Focusable {
|
|
|
666
666
|
return this.cycleTab(1);
|
|
667
667
|
case "prevTab":
|
|
668
668
|
return this.cycleTab(-1);
|
|
669
|
-
case "toggleScene":
|
|
670
|
-
return this.togglePanel("animations");
|
|
671
669
|
case "toggleConversation":
|
|
672
670
|
return this.togglePanel("conversation");
|
|
673
671
|
case "toggleActivity":
|
|
@@ -2286,7 +2284,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2286
2284
|
if (entry.view.kind !== "here") return this.otherSessionBody(entry, width, height, theme, now);
|
|
2287
2285
|
const talk = this.conversationFor(entry);
|
|
2288
2286
|
return renderHome({
|
|
2289
|
-
...(zen.task ? { task: zen.task
|
|
2287
|
+
...(zen.task ? { task: zen.task } : {}),
|
|
2290
2288
|
chat: talk.chat,
|
|
2291
2289
|
...(talk.older ? { olderNote: OLDER_NOTE } : {}),
|
|
2292
2290
|
...(feed.reply ? { liveReply: feed.reply } : {}),
|
|
@@ -2304,7 +2302,6 @@ export class LobbyView implements Component, Focusable {
|
|
|
2304
2302
|
panels: this.panels,
|
|
2305
2303
|
...(query ? { query } : {}),
|
|
2306
2304
|
keys: {
|
|
2307
|
-
animations: keyLabel(this.keys.toggleScene),
|
|
2308
2305
|
conversation: keyLabel(this.keys.toggleConversation),
|
|
2309
2306
|
activity: keyLabel(this.keys.toggleActivity),
|
|
2310
2307
|
thinking: keyLabel(this.keys.toggleThinking),
|
|
@@ -2314,8 +2311,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2314
2311
|
|
|
2315
2312
|
/**
|
|
2316
2313
|
* Another session on the Lobby tab: a background session's live feed, or an
|
|
2317
|
-
* other terminal's conversation read from its session file
|
|
2318
|
-
* status sits on top without animations.
|
|
2314
|
+
* other terminal's conversation read from its session file.
|
|
2319
2315
|
*/
|
|
2320
2316
|
private otherSessionBody(entry: SessionEntry, width: number, height: number, theme: LobbyTheme, now: number): string[] {
|
|
2321
2317
|
const session = this.viewedSession();
|
|
@@ -2323,7 +2319,7 @@ export class LobbyView implements Component, Focusable {
|
|
|
2323
2319
|
const task = entry.task;
|
|
2324
2320
|
const query = this.query();
|
|
2325
2321
|
const common = {
|
|
2326
|
-
...(task ? { task
|
|
2322
|
+
...(task ? { task } : {}),
|
|
2327
2323
|
others: 0,
|
|
2328
2324
|
pending: 0,
|
|
2329
2325
|
offsets: this.homeOffsets,
|
|
@@ -2336,7 +2332,6 @@ export class LobbyView implements Component, Focusable {
|
|
|
2336
2332
|
...(query ? { query } : {}),
|
|
2337
2333
|
title: entry.name,
|
|
2338
2334
|
keys: {
|
|
2339
|
-
animations: keyLabel(this.keys.toggleScene),
|
|
2340
2335
|
conversation: keyLabel(this.keys.toggleConversation),
|
|
2341
2336
|
activity: keyLabel(this.keys.toggleActivity),
|
|
2342
2337
|
thinking: keyLabel(this.keys.toggleThinking),
|
package/src/master/decisions.ts
CHANGED
|
@@ -2,12 +2,20 @@ import type { Decision, Task, Verdict } from "../schemas/task.ts";
|
|
|
2
2
|
import type { Domain } from "../schemas/agent.ts";
|
|
3
3
|
import type { ScoutOutcome } from "./master.ts";
|
|
4
4
|
import { detectGaps, domainsInvolved } from "./synthesis.ts";
|
|
5
|
+
import { builtSomething, onFastTrack, qaRequired, qaTookPart } from "../workflow/track.ts";
|
|
5
6
|
|
|
6
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* §19: everything the engine requires before completion may be declared. The
|
|
9
|
+
* fast track asks for a finished worker step instead of a QA pass, and for
|
|
10
|
+
* QA's part only when the change needs tests.
|
|
11
|
+
*/
|
|
7
12
|
export function completionBlockers(task: Task, pendingCount: number): string[] {
|
|
8
13
|
const blockers: string[] = [];
|
|
9
14
|
if (!task.plan) blockers.push("no approved plan is recorded");
|
|
10
|
-
if (task
|
|
15
|
+
if (onFastTrack(task)) {
|
|
16
|
+
if (!builtSomething(task)) blockers.push("no worker step has finished yet");
|
|
17
|
+
if (qaRequired(task) && !qaTookPart(task) && !task.qaWaiver) blockers.push("QA has not taken part (give qa the tests as the last step, or run the QA gate)");
|
|
18
|
+
} else if (task.qaVerdict !== "pass" && !task.qaWaiver) blockers.push(`QA gate is ${task.qaVerdict ?? "not run"}`);
|
|
11
19
|
if (pendingCount > 0) blockers.push(`${pendingCount} unresolved approval request(s)`);
|
|
12
20
|
if (task.blockers.length > 0) blockers.push(`${task.blockers.length} unresolved blocker(s)`);
|
|
13
21
|
return blockers;
|