@a-t-h-i/bot-lobby 0.6.37 → 0.6.39
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 +24 -5
- package/package.json +1 -1
- package/prompts/master.md +38 -2
- package/prompts/quickfix.md +5 -0
- package/prompts/worker.md +12 -0
- package/src/classifier/instance.ts +10 -2
- package/src/classifier/knowledge.ts +5 -2
- package/src/classifier/scripts.ts +76 -0
- package/src/execution/agent-runner.ts +2 -0
- package/src/execution/git.ts +63 -0
- package/src/knowledge/selector.ts +2 -0
- package/src/lobby/quickfix.ts +65 -4
- package/src/lobby/runtime.ts +2 -1
- package/src/master/master.ts +9 -1
- package/src/pi/activity.ts +2 -0
- package/src/pi/events.ts +4 -1
- package/src/pi/plan-checklist.ts +2 -1
- package/src/pi/tools.ts +7 -4
- package/src/prompts/compiler.ts +4 -1
- package/src/roles/worker.ts +15 -1
- package/src/schemas/configuration.ts +5 -2
- package/src/schemas/findings.ts +12 -0
- package/src/scripts/library.ts +168 -0
- package/src/scripts/pick.ts +79 -0
- package/src/state/work-time.ts +4 -2
- package/src/webui/api/plan-facts.ts +17 -2
- package/src/webui/api/tasks.ts +1 -1
- package/src/webui/protocol.ts +10 -1
- package/src/workflow/workflow.ts +90 -8
- package/webui/dist/assets/index-D8TuTAMK.js +109 -0
- package/webui/dist/assets/index-uTndyUfm.css +2 -0
- package/webui/dist/build.json +15 -19
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-BTk14vBb.css +0 -2
- package/webui/dist/assets/index-BwyOrEcV.js +0 -110
package/src/workflow/workflow.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, realpathSync } from "node:fs";
|
|
2
|
-
import { isAbsolute, join, relative } from "node:path";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { profileFor, type BotLobbyConfig, type ProfileResolver } from "../schemas/configuration.ts";
|
|
4
4
|
import type { AgentRun, Pushback, ResearchResult, ReviewResult, WorkerResult } from "../schemas/findings.ts";
|
|
5
5
|
import {
|
|
@@ -24,6 +24,7 @@ import { ExcalidrawBook } from "../excalidraw/sessions.ts";
|
|
|
24
24
|
import { knowledgeDir, type KnowledgeAgent } from "../knowledge/paths.ts";
|
|
25
25
|
import { lobbyTopics } from "../lobby/topics.ts";
|
|
26
26
|
import { writeScratchpad } from "../state/persistence.ts";
|
|
27
|
+
import { saveScript } from "../scripts/library.ts";
|
|
27
28
|
import { spawnPiProcess, type ProcessRunner, type RelayAsk } from "../execution/pi-runner.ts";
|
|
28
29
|
import { previewDir } from "../ask/relay.ts";
|
|
29
30
|
import type { AskQuestion, AskResult } from "../ask/types.ts";
|
|
@@ -31,7 +32,7 @@ import { mapConcurrent } from "../execution/agent-runner.ts";
|
|
|
31
32
|
import { parseWorkerResult } from "../roles/worker.ts";
|
|
32
33
|
import { autoNote, DESK_TOOLS, DeskSession } from "../desk/session.ts";
|
|
33
34
|
import type { Handover } from "../desk/desk.ts";
|
|
34
|
-
import { changedFiles, commitBefore, headCommit, readRepositoryDiff } from "../execution/git.ts";
|
|
35
|
+
import { changedFiles, commitBefore, commitFiles, commitMessage, headCommit, readRepositoryDiff, uncommitted } from "../execution/git.ts";
|
|
35
36
|
import { appendChange, explainChanges, provenanceLines, provenanceSummary, readChanges, type FileProvenance } from "../state/changes.ts";
|
|
36
37
|
import { budgetLine, budgetState, formatMinutes, MIN_READ_MS, parseMinutes, qaAllotment, readBudget, readerAllotment, updateBudget, workerAllotment, type Allotment, type BudgetState, type TaskBudget } from "../state/budget.ts";
|
|
37
38
|
import type { AgentTime } from "../execution/agent-runner.ts";
|
|
@@ -90,6 +91,7 @@ export const ORCHESTRATE_ACTIONS = [
|
|
|
90
91
|
"qa",
|
|
91
92
|
"knowledge",
|
|
92
93
|
"compact",
|
|
94
|
+
"script",
|
|
93
95
|
"whiteboard",
|
|
94
96
|
"complete",
|
|
95
97
|
"block",
|
|
@@ -126,15 +128,21 @@ export interface OrchestrateParams {
|
|
|
126
128
|
minutes?: number;
|
|
127
129
|
/** knowledge: which persistent file the text belongs to. */
|
|
128
130
|
kind?: KnowledgeKind;
|
|
129
|
-
/** compact: the knowledge file being rewritten
|
|
131
|
+
/** compact: the knowledge file being rewritten; script: a drafted script to take instead of `text`. */
|
|
130
132
|
file?: string;
|
|
131
|
-
/** whiteboard: name for the new Excalidraw session. */
|
|
133
|
+
/** whiteboard: name for the new Excalidraw session; script: the script's file name (`seed_public_holidays.py`). */
|
|
132
134
|
name?: string;
|
|
135
|
+
/** script: one line on what the script does. */
|
|
136
|
+
purpose?: string;
|
|
137
|
+
/** script: how to run it, with its arguments. */
|
|
138
|
+
usage?: string;
|
|
133
139
|
approvalId?: string;
|
|
134
140
|
decision?: "approved" | "rejected";
|
|
135
141
|
note?: string;
|
|
136
142
|
reason?: string;
|
|
137
143
|
text?: string;
|
|
144
|
+
/** complete: the commit message for the agents' changes. */
|
|
145
|
+
message?: string;
|
|
138
146
|
/** track: the path the task takes. */
|
|
139
147
|
track?: "fast" | "full";
|
|
140
148
|
/** track: who takes part (designer, backend, qa, researcher). */
|
|
@@ -635,6 +643,9 @@ function workerReport(outcome: WorkerOutcome, approvals: Approval[], pushback?:
|
|
|
635
643
|
result.knowledgeProposals.length > 0
|
|
636
644
|
? `Knowledge proposals (accept with action=knowledge, or ignore): ${result.knowledgeProposals.map((proposal) => `${proposal.kind}: ${truncate(proposal.content, 160)}`).join("; ")}`
|
|
637
645
|
: "",
|
|
646
|
+
result.scriptProposals?.length
|
|
647
|
+
? `Script proposals (save with action=script name purpose usage file=<draft>, when the job will come back; or ignore): ${result.scriptProposals.map((proposal) => `${proposal.name}${proposal.purpose ? ` — ${truncate(proposal.purpose, 120)}` : ""}${proposal.draft ? ` (draft: ${proposal.draft})` : ""}`).join("; ")}`
|
|
648
|
+
: "",
|
|
638
649
|
issues.length > 0 ? `Issues: ${issues.join("; ")}` : "",
|
|
639
650
|
pushback && objection
|
|
640
651
|
? `Pushback recorded (${pushback.id}): ${truncate(objection.reason, 240)}. Resolve with action=resolve_approval before re-delegating ${result.domain}.`
|
|
@@ -1481,6 +1492,34 @@ function handleKnowledge(task: Task | undefined, params: OrchestrateParams, deps
|
|
|
1481
1492
|
return `Recorded ${kind} for ${agent} in ${outcome.path}.`;
|
|
1482
1493
|
}
|
|
1483
1494
|
|
|
1495
|
+
/**
|
|
1496
|
+
* The oracle adds a script to the shared library, or replaces one, so every
|
|
1497
|
+
* agent can run it the next time the job comes up. Its body is `text`, or a
|
|
1498
|
+
* file a worker drafted (`file`, inside the project or the task's directory).
|
|
1499
|
+
*/
|
|
1500
|
+
function handleScript(task: Task | undefined, params: OrchestrateParams, deps: WorkflowDeps): string {
|
|
1501
|
+
const name = params.name?.trim();
|
|
1502
|
+
if (!name) throw new Error("script requires name: what it does in snake_case, with its extension (seed_public_holidays.py)");
|
|
1503
|
+
const purpose = params.purpose?.trim();
|
|
1504
|
+
if (!purpose) throw new Error("script requires purpose: one line on what it does");
|
|
1505
|
+
const body = params.file?.trim() ? readDraft(params.file.trim(), task, deps) : params.text;
|
|
1506
|
+
if (!body?.trim()) throw new Error("script requires text (the script) or file (a drafted script)");
|
|
1507
|
+
const saved = saveScript({ dataRoot: dataRoot(deps.root, deps.configDir), name, purpose, body, ...(params.usage?.trim() ? { usage: params.usage.trim() } : {}) });
|
|
1508
|
+
const { entry } = saved;
|
|
1509
|
+
if (task) recordDecision(task, `${saved.archived ? "Replaced" : "Saved"} the shared script ${entry.name}: ${truncate(entry.purpose, 160)}`);
|
|
1510
|
+
lobbyTopics.bump("knowledge");
|
|
1511
|
+
return [`${saved.archived ? "Replaced" : "Saved"} ${entry.name} in the shared scripts library (${entry.path}); run: ${entry.usage}.`, saved.archived ? `The previous version is kept at ${saved.archived}.` : "", "Agents whose steps it fits are told to run it."].filter(Boolean).join(" ");
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
/** A drafted script, read only from inside the project, its worktree or the task's directory. */
|
|
1515
|
+
function readDraft(file: string, task: Task | undefined, deps: WorkflowDeps): string {
|
|
1516
|
+
const roots = [deps.cwd, deps.root, ...(task ? [taskDirFor(deps.root, deps.configDir, task.id)] : [])].map((dir) => resolve(dir));
|
|
1517
|
+
const path = isAbsolute(file) ? resolve(file) : resolve(deps.cwd, file);
|
|
1518
|
+
if (!roots.some((root) => path === root || path.startsWith(`${root}${sep}`))) throw new Error(`the drafted script must be inside the project: ${file}`);
|
|
1519
|
+
if (!existsSync(path)) throw new Error(`no drafted script at ${file}`);
|
|
1520
|
+
return readFileSync(path, "utf8");
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1484
1523
|
/**
|
|
1485
1524
|
* §24: the Master rewrites the file (asking the user about ambiguity), the
|
|
1486
1525
|
* engine archives the previous version and writes the compacted text.
|
|
@@ -1533,6 +1572,7 @@ async function handleComplete(task: Task, params: OrchestrateParams, deps: Workf
|
|
|
1533
1572
|
// The gate refuses before any side effect, so a refusal leaves the task untouched.
|
|
1534
1573
|
const oversized = overThreshold(readDataRoots(deps.root, deps.configDir), deps.config.knowledge.compactionThreshold);
|
|
1535
1574
|
if (oversized.length > 0) throw new Error(`cannot complete: knowledge files over the compaction threshold: ${oversized.map((entry) => `${entry.agent}/${entry.file} (${entry.chars})`).join(", ")}. First record domain-relevant facts where they belong with action=knowledge (domain=designer|backend|qa), then rewrite each oversized file with action=compact, then call complete again.`);
|
|
1575
|
+
const committed = await commitTaskWork(task, params, deps);
|
|
1536
1576
|
const summary = params.text?.trim() || task.proposal || task.title;
|
|
1537
1577
|
recordCompletion(deps, task, summary);
|
|
1538
1578
|
removeTaskScratchpads(deps.root, deps.configDir, task.id);
|
|
@@ -1544,7 +1584,43 @@ async function handleComplete(task: Task, params: OrchestrateParams, deps: Workf
|
|
|
1544
1584
|
flushDecisions(deps, task);
|
|
1545
1585
|
const delivery = await offerDelivery(task, deps);
|
|
1546
1586
|
const left = lint?.state === "failing" ? ` ${task.lintWaiver ? "Accepted by the user with lint errors" : "Lint (advisory)"}: ${lintSummary(lint)} Tell the user.` : "";
|
|
1547
|
-
return `Task ${task.id} completed
|
|
1587
|
+
return `Task ${task.id} completed.${committed} History recorded and temporary scratchpads removed. ${delivery}${left}`;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
/** The files a task's own workers changed: not one that held changes before they started, nor another task's. */
|
|
1591
|
+
function agentsWork(files: readonly FileProvenance[]): string[] {
|
|
1592
|
+
return files.filter((file) => file.kinds.includes("planned") && !file.kinds.includes("pre-existing") && !file.kinds.includes("other-task")).map((file) => file.path);
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
/**
|
|
1596
|
+
* The oracle commits what its agents changed before the task is called done,
|
|
1597
|
+
* with its own short message (`message`); nothing else in the tree is
|
|
1598
|
+
* staged or committed. "" when git is not in use or nothing is left to commit.
|
|
1599
|
+
*/
|
|
1600
|
+
async function commitTaskWork(task: Task, params: OrchestrateParams, deps: WorkflowDeps): Promise<string> {
|
|
1601
|
+
const state = await changeState(deps, task, await reviewBase(task, deps));
|
|
1602
|
+
if (!state) return "";
|
|
1603
|
+
const ours = agentsWork(state.files);
|
|
1604
|
+
const dirty = await uncommitted(state.top, ours).catch(() => []);
|
|
1605
|
+
if (dirty.length === 0) return "";
|
|
1606
|
+
const listed = dirty.slice(0, 20).join(", ") + (dirty.length > 20 ? ` and ${dirty.length - 20} more` : "");
|
|
1607
|
+
if (!params.message?.trim()) throw new Error(`cannot complete: commit the agents' changes first. Call complete again with message: a concise commit message for them (a first line of 72 characters or fewer saying what changed, at most three short lines below it). The engine commits only these files: ${listed}`);
|
|
1608
|
+
let message: string;
|
|
1609
|
+
try {
|
|
1610
|
+
message = commitMessage(params.message);
|
|
1611
|
+
} catch (error) {
|
|
1612
|
+
throw new Error(`cannot complete: ${(error as Error).message}`);
|
|
1613
|
+
}
|
|
1614
|
+
let result: Awaited<ReturnType<typeof commitFiles>>;
|
|
1615
|
+
try {
|
|
1616
|
+
result = await commitFiles(state.top, dirty, message);
|
|
1617
|
+
} catch (error) {
|
|
1618
|
+
throw new Error(`cannot complete: git refused the commit: ${(error as Error).message}`);
|
|
1619
|
+
}
|
|
1620
|
+
if (!result) return "";
|
|
1621
|
+
const others = await uncommitted(state.top, state.files.map((file) => file.path).filter((path) => !ours.includes(path))).catch(() => []);
|
|
1622
|
+
const left = others.length > 0 ? ` Left uncommitted, not this task's agents' work: ${others.slice(0, 10).join(", ")}${others.length > 10 ? ` and ${others.length - 10} more` : ""}.` : "";
|
|
1623
|
+
return ` Committed ${result.files.length} file${result.files.length === 1 ? "" : "s"} as ${result.commit.slice(0, 7)}: "${message.split("\n")[0]}".${left}`;
|
|
1548
1624
|
}
|
|
1549
1625
|
|
|
1550
1626
|
/** Completion always offers delivery to the user, including in auto mode. */
|
|
@@ -1990,6 +2066,7 @@ const HANDLERS: Record<OrchestrateAction, (task: Task, params: OrchestrateParams
|
|
|
1990
2066
|
resolve_approval: handleResolveApproval,
|
|
1991
2067
|
qa: handleQa,
|
|
1992
2068
|
knowledge: handleKnowledge,
|
|
2069
|
+
script: handleScript,
|
|
1993
2070
|
compact: handleCompact,
|
|
1994
2071
|
whiteboard: handleWhiteboard,
|
|
1995
2072
|
complete: handleComplete,
|
|
@@ -2008,9 +2085,9 @@ const HANDLERS: Record<OrchestrateAction, (task: Task, params: OrchestrateParams
|
|
|
2008
2085
|
*/
|
|
2009
2086
|
export async function runWorkflowAction(params: OrchestrateParams, deps: WorkflowDeps): Promise<WorkflowResult> {
|
|
2010
2087
|
// Project knowledge can be learned without starting or changing a task.
|
|
2011
|
-
if ((params.action === "knowledge" || params.action === "compact") && !params.taskId) {
|
|
2088
|
+
if ((params.action === "knowledge" || params.action === "compact" || params.action === "script") && !params.taskId) {
|
|
2012
2089
|
try {
|
|
2013
|
-
const message = params.action === "knowledge" ? handleKnowledge(undefined, params, deps) : handleCompact(undefined, params, deps);
|
|
2090
|
+
const message = params.action === "knowledge" ? handleKnowledge(undefined, params, deps) : params.action === "script" ? handleScript(undefined, params, deps) : handleCompact(undefined, params, deps);
|
|
2014
2091
|
return { ok: true, taskId: "", state: "created", message };
|
|
2015
2092
|
} catch (error) {
|
|
2016
2093
|
return { ok: false, taskId: "", state: "created", message: `Rejected: ${(error as Error).message}` };
|
|
@@ -2050,6 +2127,11 @@ export async function runWorkflowAction(params: OrchestrateParams, deps: Workflo
|
|
|
2050
2127
|
...(deps.askQuestions ? { askQuestions: (...args: Parameters<NonNullable<WorkflowDeps["askQuestions"]>>) => wait(() => deps.askQuestions!(...args)) } : {}),
|
|
2051
2128
|
// Every agent, diff and desk of a task with its own worktree works there.
|
|
2052
2129
|
cwd: taskCwd(task, deps.cwd),
|
|
2130
|
+
// An agent can work for an hour: what the action has done so far (implementing, reviewing) is saved before it starts, so every window reads it.
|
|
2131
|
+
runProcess: (args, options) => {
|
|
2132
|
+
saveTask(deps.root, deps.configDir, task);
|
|
2133
|
+
return (deps.runProcess ?? spawnPiProcess)(args, options);
|
|
2134
|
+
},
|
|
2053
2135
|
onUpdate: (run) => {
|
|
2054
2136
|
if (run.status !== "running") {
|
|
2055
2137
|
finished.set(run.runId, run);
|