@a-t-h-i/bot-lobby 0.6.4 → 0.6.5
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 +81 -9
- package/package.json +12 -3
- package/prompts/backend.md +3 -1
- package/prompts/designer.md +24 -1
- package/prompts/master.md +51 -18
- package/prompts/quickfix.md +16 -3
- package/src/agents/backend.ts +2 -2
- package/src/agents/designer.ts +2 -2
- package/src/classifier/triage.ts +24 -7
- package/src/index.ts +3 -0
- package/src/lobby/feed.ts +6 -1
- package/src/lobby/quickfix.ts +29 -3
- package/src/lobby/runtime.ts +27 -4
- package/src/lobby/tabs/quickfix.ts +1 -1
- package/src/lobby/tabs/tasks.ts +2 -1
- package/src/lobby/view.ts +9 -0
- package/src/master/decisions.ts +10 -2
- package/src/pi/commands.ts +22 -11
- package/src/pi/events.ts +2 -1
- package/src/pi/route.ts +180 -0
- package/src/pi/start-task.ts +51 -5
- package/src/pi/tools.ts +8 -3
- package/src/roles/worker.ts +10 -2
- package/src/schemas/configuration.ts +11 -0
- package/src/schemas/task.ts +31 -0
- package/src/workflow/track.ts +436 -0
- package/src/workflow/workflow.ts +148 -10
package/src/workflow/workflow.ts
CHANGED
|
@@ -59,6 +59,7 @@ import type { EffortRouter } from "../classifier/effort.ts";
|
|
|
59
59
|
import { answerClarify } from "../classifier/triage.ts";
|
|
60
60
|
import { appendMetrics, metricFromRun } from "../state/metrics.ts";
|
|
61
61
|
import { markCommentsAddressed, pendingComments, readPlanComments } from "../state/comments.ts";
|
|
62
|
+
import { fastNext, onFastTrack, parseRoster, qaRequired, qaStillDue, qaTookPart, rosterWords, trackSummary } from "./track.ts";
|
|
62
63
|
|
|
63
64
|
export const ORCHESTRATE_ACTIONS = [
|
|
64
65
|
"clarify",
|
|
@@ -76,6 +77,7 @@ export const ORCHESTRATE_ACTIONS = [
|
|
|
76
77
|
"resume",
|
|
77
78
|
"decide",
|
|
78
79
|
"budget",
|
|
80
|
+
"track",
|
|
79
81
|
"status",
|
|
80
82
|
"cancel",
|
|
81
83
|
] as const;
|
|
@@ -112,6 +114,10 @@ export interface OrchestrateParams {
|
|
|
112
114
|
note?: string;
|
|
113
115
|
reason?: string;
|
|
114
116
|
text?: string;
|
|
117
|
+
/** track: the path the task takes. */
|
|
118
|
+
track?: "fast" | "full";
|
|
119
|
+
/** track: who takes part (designer, backend, qa, researcher). */
|
|
120
|
+
roster?: string[];
|
|
115
121
|
}
|
|
116
122
|
|
|
117
123
|
export interface WorkflowDeps {
|
|
@@ -204,6 +210,7 @@ export function describeTask(task: Task): string {
|
|
|
204
210
|
`${task.id} — state: ${task.state}${task.paused ? " (paused)" : ""}`,
|
|
205
211
|
`Title: ${task.title}`,
|
|
206
212
|
`Request: ${truncate(taskRequest(task), 200)}`,
|
|
213
|
+
task.track ? trackSummary(task.track) : "",
|
|
207
214
|
task.domains.length > 0 ? `Domains: ${task.domains.join(", ")}` : "",
|
|
208
215
|
`Review iterations: ${Object.entries(task.reviewIterations).map(([d, n]) => `${d}=${n}`).join(", ")}`,
|
|
209
216
|
pendingApprovals(task).length > 0
|
|
@@ -512,10 +519,12 @@ function handlePlan(task: Task, params: OrchestrateParams, deps: WorkflowDeps):
|
|
|
512
519
|
requireState(task, ["planning", ...AMEND_PLAN_STATES]);
|
|
513
520
|
const plan = params.plan?.trim();
|
|
514
521
|
if (!plan) throw new Error("plan requires the plan text");
|
|
515
|
-
|
|
522
|
+
// The fast track keeps a short plan: the full plan's required areas are the full workflow's.
|
|
523
|
+
const missing = onFastTrack(task) ? [] : validatePlan(plan);
|
|
516
524
|
if (missing.length > 0) throw new Error(`plan is missing: ${missing.join(", ")}`);
|
|
517
525
|
const amending = AMEND_PLAN_STATES.includes(task.state);
|
|
518
526
|
task.plan = plan;
|
|
527
|
+
if (task.track?.autoPlan) delete task.track.autoPlan;
|
|
519
528
|
writeFileEnsured(join(taskDirFor(deps.root, deps.configDir, task.id), "plan.md"), plan);
|
|
520
529
|
const addressed = addressComments(task, deps);
|
|
521
530
|
if (amending) {
|
|
@@ -570,7 +579,9 @@ function recordAdvisoryPushbacks(task: Task, entries: Array<{ pushback?: Pushbac
|
|
|
570
579
|
}
|
|
571
580
|
}
|
|
572
581
|
|
|
573
|
-
|
|
582
|
+
const FULL_NEXT = "Next: inspect the diff, then run action=qa once this domain's work is complete.";
|
|
583
|
+
|
|
584
|
+
function workerReport(outcome: WorkerOutcome, approvals: Approval[], pushback?: Approval, next = FULL_NEXT): string {
|
|
574
585
|
const { result, run, issues } = outcome;
|
|
575
586
|
const objection = result.pushback;
|
|
576
587
|
return [
|
|
@@ -593,7 +604,7 @@ function workerReport(outcome: WorkerOutcome, approvals: Approval[], pushback?:
|
|
|
593
604
|
pushback && objection
|
|
594
605
|
? `Pushback recorded (${pushback.id}): ${truncate(objection.reason, 240)}. Resolve with action=resolve_approval before re-delegating ${result.domain}.`
|
|
595
606
|
: "",
|
|
596
|
-
|
|
607
|
+
next,
|
|
597
608
|
]
|
|
598
609
|
.filter((line) => line.length > 0)
|
|
599
610
|
.join("\n");
|
|
@@ -835,17 +846,44 @@ function absorbWorkerOutcome(task: Task, deps: WorkflowDeps, outcome: WorkerOutc
|
|
|
835
846
|
const pushback = recordPushback(task, outcome);
|
|
836
847
|
task.blockers = [...task.blockers.filter((blocker) => blocker.domain !== domain), ...outcome.result.blockers];
|
|
837
848
|
updateScratchpad(deps, task, outcome);
|
|
838
|
-
const
|
|
839
|
-
|
|
849
|
+
const grown = qaJoinsGrownFastTask(task, outcome);
|
|
850
|
+
const report = workerReport(outcome, approvals, pushback, onFastTrack(task) ? fastNext(task) : FULL_NEXT);
|
|
851
|
+
return [report, grown, timeReport(outcome)].filter(Boolean).join("\n");
|
|
840
852
|
}
|
|
841
853
|
|
|
854
|
+
/**
|
|
855
|
+
* A fast task whose worker asks for a new dependency or an architecture
|
|
856
|
+
* change is not the small change it read as: QA joins, so it is checked
|
|
857
|
+
* before it completes. Returns the line for the oracle, or "".
|
|
858
|
+
*/
|
|
859
|
+
function qaJoinsGrownFastTask(task: Task, outcome: WorkerOutcome): string {
|
|
860
|
+
const track = task.track;
|
|
861
|
+
if (!track || track.path !== "fast" || track.roster.includes("qa")) return "";
|
|
862
|
+
const asks = [...outcome.result.dependencyNeeds, ...outcome.result.architectureChanges];
|
|
863
|
+
if (asks.length === 0) return "";
|
|
864
|
+
track.roster = parseRoster([...track.roster, "qa"]);
|
|
865
|
+
track.reasons = [...track.reasons, `tests: ${AGENT_LABELS[outcome.result.domain]} asked for a dependency or an architecture change`];
|
|
866
|
+
recordDecision(task, `QA joined the fast track: ${AGENT_LABELS[outcome.result.domain]} asked for ${truncate(asks.join("; "), 200)}.`);
|
|
867
|
+
return "QA now takes part: a fast-track change that needs a new dependency or an architecture change is checked before it completes.";
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/** States a fast task starts its work from: before anything is planned, and never while the user decides on a proposal. */
|
|
871
|
+
const FAST_START_STATES: readonly TaskState[] = ["created", "clarifying", "scouting", "synthesizing"];
|
|
872
|
+
|
|
842
873
|
async function handleImplement(task: Task, params: OrchestrateParams, deps: WorkflowDeps): Promise<string> {
|
|
843
|
-
|
|
874
|
+
const starting = onFastTrack(task) && FAST_START_STATES.includes(task.state);
|
|
875
|
+
if (!starting && FAST_START_STATES.includes(task.state)) {
|
|
876
|
+
throw new Error(`action not allowed in state "${task.state}": on the full workflow the user approves a proposal first (a small, clear change can take the fast track: action=track track=fast)`);
|
|
877
|
+
}
|
|
878
|
+
if (!starting) requireState(task, ["planning", "implementing", "reviewing"]);
|
|
844
879
|
const assignments = parseAssignments(params);
|
|
845
880
|
for (const { domain } of assignments) assertNoPendingApprovals(task, domain);
|
|
846
881
|
for (const { domain } of assignments) if (!task.domains.includes(domain)) task.domains.push(domain);
|
|
847
882
|
// Under a time budget every step is given its share before any starts; a spent budget starts none.
|
|
848
883
|
const times = workerTimes(task, deps, assignments);
|
|
884
|
+
if (task.track && onFastTrack(task)) task.track.roster = parseRoster([...task.track.roster, ...assignments.map((entry) => entry.domain)]);
|
|
885
|
+
if (starting) startFast(task);
|
|
886
|
+
if (task.track?.autoPlan) addFastSteps(task, deps, assignments);
|
|
849
887
|
if (task.state !== "implementing") transition(task, "implementing");
|
|
850
888
|
await takeBaseline(task, deps);
|
|
851
889
|
let report: string;
|
|
@@ -863,6 +901,61 @@ async function handleImplement(task: Task, params: OrchestrateParams, deps: Work
|
|
|
863
901
|
return note ? `${report}\n\n${note}` : report;
|
|
864
902
|
}
|
|
865
903
|
|
|
904
|
+
/**
|
|
905
|
+
* The fast track's start: the request is small and clear, so it goes from
|
|
906
|
+
* shaping straight to planning without a proposal round (the engine walks the
|
|
907
|
+
* same states, so every transition stays legal), and the engine keeps a short
|
|
908
|
+
* plan whose steps are the delegations themselves.
|
|
909
|
+
*/
|
|
910
|
+
function startFast(task: Task): void {
|
|
911
|
+
const walk: Partial<Record<TaskState, TaskState[]>> = {
|
|
912
|
+
created: ["clarifying", "awaiting_approval", "planning"],
|
|
913
|
+
clarifying: ["awaiting_approval", "planning"],
|
|
914
|
+
scouting: ["synthesizing", "awaiting_approval", "planning"],
|
|
915
|
+
synthesizing: ["awaiting_approval", "planning"],
|
|
916
|
+
};
|
|
917
|
+
for (const state of walk[task.state] ?? []) transition(task, state);
|
|
918
|
+
const track = task.track!;
|
|
919
|
+
if (!task.plan) {
|
|
920
|
+
task.plan = [
|
|
921
|
+
"## Objective",
|
|
922
|
+
oneLine(taskRequest(task), 600),
|
|
923
|
+
"",
|
|
924
|
+
"## Track",
|
|
925
|
+
`Fast track (${track.size}): ${rosterWords(track.roster)}. No scouts, proposal round or plan review; ${qaRequired(task) ? "QA takes part before completion (its tests as the last step, or the QA gate)" : "no QA gate, as nothing here needs tests"}.`,
|
|
926
|
+
"",
|
|
927
|
+
"## Steps",
|
|
928
|
+
].join("\n");
|
|
929
|
+
track.autoPlan = true;
|
|
930
|
+
}
|
|
931
|
+
recordDecision(task, `Fast track: started without a proposal round (${track.size}; ${rosterWords(track.roster)}).`);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/** `Step 3: …`, `Steps 2-4 — …` at the start of a delegation. */
|
|
935
|
+
const STEP_PREFIX = /^\s*(?:\*\*)?steps?\s*#?\s*(\d+)(?:\s*(?:-|\u2013|\u2014|to|and|&)\s*#?\s*(\d+))?(?:\*\*)?\s*[:.)\u2013\u2014-]?\s*/i;
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* The fast track's plan grows with its delegations: a delegation that does
|
|
939
|
+
* not name a step already in the plan adds one, so the lobby's checklist
|
|
940
|
+
* follows the work without a plan document.
|
|
941
|
+
*/
|
|
942
|
+
function addFastSteps(task: Task, deps: WorkflowDeps, assignments: readonly Assignment[]): void {
|
|
943
|
+
const plan = task.plan ?? "";
|
|
944
|
+
const lines = plan.split("\n");
|
|
945
|
+
const heading = lines.findIndex((line) => /^##\s+steps\s*$/i.test(line));
|
|
946
|
+
let count = heading < 0 ? 0 : lines.slice(heading + 1).filter((line) => /^\d+\.\s/.test(line)).length;
|
|
947
|
+
const added: string[] = [];
|
|
948
|
+
for (const { domain, instruction } of assignments) {
|
|
949
|
+
const named = STEP_PREFIX.exec(instruction);
|
|
950
|
+
if (named && Number(named[2] ?? named[1]) <= count) continue;
|
|
951
|
+
count += 1;
|
|
952
|
+
added.push(`${count}. ${AGENT_LABELS[domain]}: ${oneLine(instruction.replace(STEP_PREFIX, ""), 160) || "its part of the request"}`);
|
|
953
|
+
}
|
|
954
|
+
if (added.length === 0) return;
|
|
955
|
+
task.plan = `${plan.trimEnd()}\n${added.join("\n")}`;
|
|
956
|
+
writeFileEnsured(join(taskDirFor(deps.root, deps.configDir, task.id), "plan.md"), task.plan);
|
|
957
|
+
}
|
|
958
|
+
|
|
866
959
|
/**
|
|
867
960
|
* Several domains at once. The workers share one file desk: each claims a file
|
|
868
961
|
* before editing it, queues for a busy one, and hands it over with a note; a
|
|
@@ -1159,9 +1252,10 @@ function handleCompact(task: Task, params: OrchestrateParams, deps: WorkflowDeps
|
|
|
1159
1252
|
|
|
1160
1253
|
/** §63: record history, drop scratchpads, then mark the task completed. */
|
|
1161
1254
|
async function handleComplete(task: Task, params: OrchestrateParams, deps: WorkflowDeps): Promise<string> {
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1255
|
+
// The fast track completes straight from its work; the full workflow from review.
|
|
1256
|
+
requireState(task, onFastTrack(task) ? ["implementing", "reviewing", "blocked"] : ["reviewing", "blocked"]);
|
|
1257
|
+
// Without QA's part (where the track asks for it) only the user can let the task finish: they are asked, never overruled.
|
|
1258
|
+
if (qaRequired(task) && !qaTookPart(task) && !task.qaWaiver) await offerAcceptance(task, deps);
|
|
1165
1259
|
if (task.state === "blocked") throw new Error("cannot complete: the task is blocked, and the user did not accept its work as it is");
|
|
1166
1260
|
const blockers = completionBlockers(task, pendingApprovals(task).length);
|
|
1167
1261
|
if (blockers.length > 0) throw new Error(`cannot complete: ${blockers.join("; ")}`);
|
|
@@ -1170,6 +1264,7 @@ async function handleComplete(task: Task, params: OrchestrateParams, deps: Workf
|
|
|
1170
1264
|
recordCompletion(deps, task, summary);
|
|
1171
1265
|
removeTaskScratchpads(deps.root, deps.configDir, task.id);
|
|
1172
1266
|
task.blockers = [];
|
|
1267
|
+
if (task.state === "implementing") transition(task, "reviewing");
|
|
1173
1268
|
transition(task, "completed");
|
|
1174
1269
|
const oversized = overThreshold(readDataRoots(deps.root, deps.configDir), deps.config.knowledge.compactionThreshold);
|
|
1175
1270
|
const advice =
|
|
@@ -1227,6 +1322,48 @@ function handleResume(task: Task, params: OrchestrateParams): string {
|
|
|
1227
1322
|
return "Task resumed. Continue with action=implement.";
|
|
1228
1323
|
}
|
|
1229
1324
|
|
|
1325
|
+
/**
|
|
1326
|
+
* `action=track`: with neither `track` nor `roster`, the task's track; with
|
|
1327
|
+
* them, the oracle corrects the engine's read of the request. The fast track
|
|
1328
|
+
* is taken only before the work is planned, never against the user's --full
|
|
1329
|
+
* or the settings; once work is under way a track only gets stricter (the
|
|
1330
|
+
* full workflow, more members, QA never dropped).
|
|
1331
|
+
*/
|
|
1332
|
+
function handleTrack(task: Task, params: OrchestrateParams, deps: WorkflowDeps): string {
|
|
1333
|
+
const current = task.track;
|
|
1334
|
+
if (!params.track && !params.roster) return current ? trackSummary(current) : "This task began before tracks: it takes the full workflow.";
|
|
1335
|
+
const reason = oneLine(params.reason?.trim() || params.text?.trim() || "", 240);
|
|
1336
|
+
if (!reason) throw new Error("track requires reason: why the task takes that path, or needs those members");
|
|
1337
|
+
const path = params.track ?? current?.path ?? "full";
|
|
1338
|
+
const shaping = FAST_START_STATES.includes(task.state);
|
|
1339
|
+
if (path === "fast" && current?.path !== "fast") {
|
|
1340
|
+
if (!deps.config.workflow.fastTrack) throw new Error("the fast track is off in settings (workflow.fastTrack): this task takes the full workflow");
|
|
1341
|
+
if (current?.userChoice === "full") throw new Error("the user asked for the full workflow on this task (--full)");
|
|
1342
|
+
if (!shaping) throw new Error(`a task takes the fast track before its work is planned; this one is ${task.state}`);
|
|
1343
|
+
}
|
|
1344
|
+
const asked = params.roster ? parseRoster(params.roster) : [...(current?.roster ?? [])];
|
|
1345
|
+
// The full workflow always ends with the QA gate.
|
|
1346
|
+
const roster = path === "full" ? parseRoster([...asked, "qa"]) : asked;
|
|
1347
|
+
if (!shaping && current?.roster.includes("qa") && !roster.includes("qa")) throw new Error("QA stays on the roster once the work is under way");
|
|
1348
|
+
const grew = current?.path === "fast" && path === "full";
|
|
1349
|
+
task.track = {
|
|
1350
|
+
path,
|
|
1351
|
+
size: current?.size ?? "small",
|
|
1352
|
+
roster,
|
|
1353
|
+
reasons: [`oracle: ${reason}`, ...(current?.reasons ?? [])].slice(0, 8),
|
|
1354
|
+
source: "oracle",
|
|
1355
|
+
...(current?.userChoice ? { userChoice: current.userChoice } : {}),
|
|
1356
|
+
...(current?.autoPlan ? { autoPlan: true } : {}),
|
|
1357
|
+
at: new Date().toISOString(),
|
|
1358
|
+
};
|
|
1359
|
+
recordDecision(task, `Track: ${path === "fast" ? "fast track" : "full workflow"}; ${rosterWords(roster)} — ${reason}`);
|
|
1360
|
+
if (path === "fast") {
|
|
1361
|
+
return `Fast track: ${rosterWords(roster)}. Delegate straight away with action=implement (open each task with "Step N:"); no scouts, proposal or plan. ${qaRequired(task) ? "QA takes part before completion: its tests as the last step, or action=qa." : "No QA gate: nothing here needs tests."} Then action=complete.`;
|
|
1362
|
+
}
|
|
1363
|
+
if (!shaping) return `Full workflow from here: ${rosterWords(roster)}. The QA gate runs before the task completes${grew ? "; tell the user in one line why the task grew" : ""}.`;
|
|
1364
|
+
return `Full workflow: ${rosterWords(roster)}. Scout what the request touches, then propose a short bullet list for approval.`;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1230
1367
|
function handleDecide(task: Task, params: OrchestrateParams): string {
|
|
1231
1368
|
const text = params.text?.trim();
|
|
1232
1369
|
if (!text) throw new Error("decide requires text");
|
|
@@ -1254,7 +1391,7 @@ type WorkerTime = AgentTime & { id: string };
|
|
|
1254
1391
|
/** The task's budget and where it stands, when it has one. */
|
|
1255
1392
|
function budgetFor(task: Task, deps: WorkflowDeps): { budget: TaskBudget; state: BudgetState } | undefined {
|
|
1256
1393
|
const budget = readBudget(deps.root, deps.configDir, task.id);
|
|
1257
|
-
return budget ? { budget, state: budgetState(task.id, budget, task
|
|
1394
|
+
return budget ? { budget, state: budgetState(task.id, budget, !qaStillDue(task)) } : undefined;
|
|
1258
1395
|
}
|
|
1259
1396
|
|
|
1260
1397
|
/** No new work starts once the budget is spent: the oracle asks the user for more, or wraps up. */
|
|
@@ -1501,6 +1638,7 @@ const HANDLERS: Record<OrchestrateAction, (task: Task, params: OrchestrateParams
|
|
|
1501
1638
|
resume: handleResume,
|
|
1502
1639
|
decide: handleDecide,
|
|
1503
1640
|
budget: handleBudget,
|
|
1641
|
+
track: handleTrack,
|
|
1504
1642
|
status: (task) => describeTask(task),
|
|
1505
1643
|
cancel: handleCancel,
|
|
1506
1644
|
};
|