@pify/swarm 0.9.2 → 0.11.0
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 +23 -4
- package/extensions/swarm.ts +342 -19
- package/package.json +1 -1
- package/skills/swarm/SKILL.md +21 -7
- package/src/builtin.ts +12 -2
- package/src/frontmatter.ts +16 -5
- package/src/gate.ts +351 -0
- package/src/isolate.ts +67 -7
- package/src/outcome.ts +94 -0
- package/src/pending.ts +34 -10
- package/src/repair-policy.ts +28 -0
- package/src/repair.ts +104 -0
- package/src/report.ts +83 -10
- package/src/types.ts +34 -1
- package/src/wait.ts +48 -0
- package/src/widget.ts +25 -13
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who gets sent back to fix a failed gate.
|
|
3
|
+
*
|
|
4
|
+
* repair.ts bounds *how many* repair passes a failing gate may spend; this
|
|
5
|
+
* decides whether one is worth spending at all. Two facts rule it out. An
|
|
6
|
+
* agent with no writing tool can only re-read the failure and re-report it,
|
|
7
|
+
* so the pass buys nothing. And a child that ended its report with
|
|
8
|
+
* `OUTCOME: blocked` has said the wall is outside its reach — a decision,
|
|
9
|
+
* access or information it does not have — and a failing gate does not move
|
|
10
|
+
* that wall; a repair pass would just re-discover it at the cost of a full
|
|
11
|
+
* child run.
|
|
12
|
+
*
|
|
13
|
+
* The declaration has to be read *before* the item settles: settleItem
|
|
14
|
+
* strips it from the result so the report does not repeat it.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { parseDeclaredOutcome } from "./outcome.ts";
|
|
18
|
+
import type { AgentDef } from "./types.ts";
|
|
19
|
+
|
|
20
|
+
/** An agent that can write is one that can fix what a gate complained about. */
|
|
21
|
+
export function canWrite(def: AgentDef): boolean {
|
|
22
|
+
return def.tools.some((t) => t === "edit" || t === "write" || t === "bash" || t === "powershell");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** May this item be sent on a repair pass, given the agent and what it said? */
|
|
26
|
+
export function repairAllowed(def: AgentDef, result: string | null | undefined): boolean {
|
|
27
|
+
return canWrite(def) && parseDeclaredOutcome(result) !== "blocked";
|
|
28
|
+
}
|
package/src/repair.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verification a model cannot talk its way past.
|
|
3
|
+
*
|
|
4
|
+
* `verify` asks a reviewer agent whether the work is good; that is a second
|
|
5
|
+
* opinion, and it defaults to PASS when the reply is unclear because a mangled
|
|
6
|
+
* review must not block a good result. A gate is the other kind of check: a
|
|
7
|
+
* command runs in the tree the child actually worked in, and its exit status
|
|
8
|
+
* and output are facts. The suite already had this — in @pify/workflow, where
|
|
9
|
+
* only a workflow script could reach it — while agent_run, the tool where
|
|
10
|
+
* children actually edit code, had nothing but the reviewer.
|
|
11
|
+
*
|
|
12
|
+
* A failed gate then gets one thing a workflow step does not: the child is
|
|
13
|
+
* still there, so it can be sent back with the failure and the gate re-run.
|
|
14
|
+
* Bounded, because an agent that cannot fix a build in two tries will not fix
|
|
15
|
+
* it in ten, and each attempt costs a full child run.
|
|
16
|
+
*
|
|
17
|
+
* Pure orchestration with injected seams, like verify.ts: the gate runner and
|
|
18
|
+
* the repair spawn are both parameters, so the whole cycle is testable without
|
|
19
|
+
* a shell or a live session.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { GateContract, GateOutcome, GateVerdict } from "./gate.ts";
|
|
23
|
+
import { gateVerification, type Verification } from "./outcome.ts";
|
|
24
|
+
import type { GateRecord } from "./types.ts";
|
|
25
|
+
|
|
26
|
+
/** The brief for a repair pass: the check, what it said, nothing else. */
|
|
27
|
+
export function repairPrompt(
|
|
28
|
+
task: string,
|
|
29
|
+
contract: GateContract,
|
|
30
|
+
verdict: { reason: string; output: string },
|
|
31
|
+
): string {
|
|
32
|
+
const output = verdict.output.trim();
|
|
33
|
+
return [
|
|
34
|
+
"Your work did not pass its verification check. Fix the cause and stop — do not change anything the",
|
|
35
|
+
"check did not complain about, and do not modify the check itself to make it pass.",
|
|
36
|
+
"",
|
|
37
|
+
"== Original task ==",
|
|
38
|
+
task.trim(),
|
|
39
|
+
"",
|
|
40
|
+
`== Check ==\n${contract.command}`,
|
|
41
|
+
"",
|
|
42
|
+
`== Verdict ==\n${verdict.reason}`,
|
|
43
|
+
...(output ? ["", `== Output ==\n${output.length > 4000 ? `…\n${output.slice(-4000)}` : output}`] : []),
|
|
44
|
+
"",
|
|
45
|
+
"When you are done, report exactly what you changed and why it fixes the check.",
|
|
46
|
+
].join("\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface GateCycleDeps {
|
|
50
|
+
/** Run the gate in `cwd` and judge it (src/gate.ts runGate); a stub may answer synchronously. */
|
|
51
|
+
runGate(contract: GateContract, cwd: string): (GateVerdict & { output: string }) | Promise<GateVerdict & { output: string }>;
|
|
52
|
+
/** Send the child back with a repair brief; resolves when that pass settles. */
|
|
53
|
+
repair(prompt: string): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Can this agent change anything? A read-only agent handed a failing gate can
|
|
56
|
+
* only re-report it, so asking it to repair burns a child run to no purpose.
|
|
57
|
+
*/
|
|
58
|
+
canRepair: boolean;
|
|
59
|
+
/** Repair passes allowed before the failure stands (0 disables). */
|
|
60
|
+
maxAttempts: number;
|
|
61
|
+
/** Other runs live in the same directory while the gate ran. */
|
|
62
|
+
sharedWith?: string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A repair is only worth spawning when the gate actually judged the work.
|
|
67
|
+
* `no_attestation` means the gate never produced a verdict — a missing runner,
|
|
68
|
+
* a typo, an unparseable pattern — and sending a child to fix a defect that was
|
|
69
|
+
* never demonstrated is how an agent ends up "fixing" working code.
|
|
70
|
+
*/
|
|
71
|
+
export function repairable(outcome: GateOutcome): boolean {
|
|
72
|
+
return outcome === "failure" || outcome === "result_missing" || outcome === "timeout";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Run the gate, repair once (or `maxAttempts` times) if it failed, re-run. */
|
|
76
|
+
export async function runGateCycle(
|
|
77
|
+
task: string,
|
|
78
|
+
contract: GateContract,
|
|
79
|
+
cwd: string,
|
|
80
|
+
deps: GateCycleDeps,
|
|
81
|
+
): Promise<{ record: GateRecord; verification: Verification }> {
|
|
82
|
+
let verdict = await deps.runGate(contract, cwd);
|
|
83
|
+
let repairs = 0;
|
|
84
|
+
const limit = Math.max(0, Math.min(5, deps.maxAttempts));
|
|
85
|
+
|
|
86
|
+
while (!verdict.ok && deps.canRepair && repairs < limit && repairable(verdict.outcome)) {
|
|
87
|
+
await deps.repair(repairPrompt(task, contract, verdict));
|
|
88
|
+
repairs++;
|
|
89
|
+
verdict = await deps.runGate(contract, cwd);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const record: GateRecord = {
|
|
93
|
+
command: contract.command,
|
|
94
|
+
outcome: verdict.outcome,
|
|
95
|
+
ok: verdict.ok,
|
|
96
|
+
reason: verdict.reason,
|
|
97
|
+
// A passing gate's output is noise in the caller's context; a failing one's
|
|
98
|
+
// is the whole point.
|
|
99
|
+
...(verdict.ok ? {} : { output: verdict.output }),
|
|
100
|
+
...(deps.sharedWith?.length ? { sharedWith: [...deps.sharedWith] } : {}),
|
|
101
|
+
...(repairs > 0 ? { repairs } : {}),
|
|
102
|
+
};
|
|
103
|
+
return { record, verification: gateVerification(verdict.outcome) };
|
|
104
|
+
}
|
package/src/report.ts
CHANGED
|
@@ -1,22 +1,77 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { outcomeLine } from "./outcome.ts";
|
|
2
|
+
import type { ItemState, SwarmRun } from "./types.ts";
|
|
2
3
|
|
|
3
|
-
/**
|
|
4
|
+
/** Longest gate output kept per item; a failing suite prints books. */
|
|
5
|
+
const GATE_TAIL = 1200;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* What the gate proved about one item. Shown whenever a gate ran — a pass is as
|
|
9
|
+
* much a fact as a failure, and silence would make "verified" and "never
|
|
10
|
+
* checked" look identical.
|
|
11
|
+
*/
|
|
12
|
+
function gateLines(item: ItemState): string[] {
|
|
13
|
+
const gate = item.gate;
|
|
14
|
+
if (!gate) return [];
|
|
15
|
+
const lines = [`[gate] ${gate.outcome} — ${gate.reason} (\`${gate.command}\`)`];
|
|
16
|
+
if (gate.repairs) {
|
|
17
|
+
lines.push(` repaired ${gate.repairs} time${gate.repairs === 1 ? "" : "s"} and re-run.`);
|
|
18
|
+
}
|
|
19
|
+
if (gate.sharedWith?.length) {
|
|
20
|
+
lines.push(
|
|
21
|
+
` ${gate.sharedWith.join(", ")} ${gate.sharedWith.length === 1 ? "was" : "were"} also changing this directory — the verdict is true of the tree, not of this item's work alone.`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
if (!gate.ok && gate.output) {
|
|
25
|
+
const tail = gate.output.length > GATE_TAIL ? `…\n${gate.output.slice(-GATE_TAIL)}` : gate.output;
|
|
26
|
+
lines.push(tail.replace(/^/gm, " "));
|
|
27
|
+
}
|
|
28
|
+
return lines;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function verdict(item: ItemState): string {
|
|
32
|
+
const parts = gateLines(item);
|
|
33
|
+
if (item.outcome) parts.push(outcomeLine(item.outcome, item.verification ?? "not-requested"));
|
|
34
|
+
return parts.length > 0 ? `\n\n${parts.join("\n")}` : "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Aggregated report returned to the parent model when a run finishes.
|
|
39
|
+
*
|
|
40
|
+
* The header counts *outcomes*, not statuses. An item whose child ran to the
|
|
41
|
+
* end and then failed its gate is not something the caller wants filed under
|
|
42
|
+
* "done" — that is the whole reason the two are recorded separately.
|
|
43
|
+
*/
|
|
4
44
|
export function buildReport(run: SwarmRun): string {
|
|
5
|
-
const counts = {
|
|
45
|
+
const counts = { succeeded: 0, blocked: 0, failed: 0, skipped: 0 };
|
|
6
46
|
for (const item of run.items) {
|
|
7
|
-
if (item.status === "
|
|
8
|
-
else if (item.
|
|
9
|
-
else if (item.status === "
|
|
47
|
+
if (item.status === "skipped") counts.skipped++;
|
|
48
|
+
else if (item.outcome) counts[item.outcome]++;
|
|
49
|
+
else if (item.status === "done") counts.succeeded++;
|
|
50
|
+
else counts.failed++;
|
|
10
51
|
}
|
|
52
|
+
const tally = [
|
|
53
|
+
`${counts.succeeded} succeeded`,
|
|
54
|
+
...(counts.blocked ? [`${counts.blocked} blocked`] : []),
|
|
55
|
+
`${counts.failed} failed`,
|
|
56
|
+
...(counts.skipped ? [`${counts.skipped} skipped`] : []),
|
|
57
|
+
].join(", ");
|
|
11
58
|
|
|
12
|
-
const header = `[swarm ${run.runId}] ${run.items.length} items — ${
|
|
59
|
+
const header = `[swarm ${run.runId}] ${run.items.length} items — ${tally}`;
|
|
13
60
|
|
|
14
61
|
const sections = run.items.map((item) => {
|
|
15
62
|
const label = `### ${item.index + 1}. [${item.agent}] ${item.item}`;
|
|
16
|
-
if (item.status === "done") return `${label}\n${item.result ?? "(empty report)"}`;
|
|
17
|
-
if (item.status === "error") return `${label}\nError: ${item.error ?? "unknown"}`;
|
|
63
|
+
if (item.status === "done") return `${label}\n${item.result ?? "(empty report)"}${verdict(item)}`;
|
|
64
|
+
if (item.status === "error") return `${label}\nError: ${item.error ?? "unknown"}${verdict(item)}`;
|
|
65
|
+
if (item.status === "skipped") {
|
|
66
|
+
return `${label}\nSkipped — ${item.error ?? "something it needed did not succeed"}. Nothing ran, so nothing was spent on it.`;
|
|
67
|
+
}
|
|
18
68
|
if (item.status === "aborted") {
|
|
19
|
-
|
|
69
|
+
// cancelRun writes who stopped the run and what that cost into `error`;
|
|
70
|
+
// a turn-cap stop leaves it empty. Either way the reader should not have
|
|
71
|
+
// to guess which one it was.
|
|
72
|
+
// cancelNote ends its sentence itself; do not add a second period.
|
|
73
|
+
const why = (item.error ?? "turn cap or stop").replace(/\.$/, "");
|
|
74
|
+
return `${label}\nAborted — ${why}. Partial:\n${item.result ?? "(none)"}`;
|
|
20
75
|
}
|
|
21
76
|
return `${label}\n(${item.status})`;
|
|
22
77
|
});
|
|
@@ -31,3 +86,21 @@ export function buildStatusLine(run: SwarmRun): string {
|
|
|
31
86
|
);
|
|
32
87
|
return `[swarm ${run.runId}] ${run.status} — ${parts.join(" · ")}`;
|
|
33
88
|
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The interrupt for an item that failed while the rest of the run is still
|
|
92
|
+
* going. @pify/subagent already treats a failed background child as a steer
|
|
93
|
+
* rather than a polite follow-up — a broken intermediate the caller is likely
|
|
94
|
+
* building on should arrive now, not after the remaining items finish. This is
|
|
95
|
+
* the same rule for a swarm: one wake, for the first hard failure only.
|
|
96
|
+
*/
|
|
97
|
+
export function earlyFailureNotice(run: SwarmRun, item: ItemState): string {
|
|
98
|
+
const reason = item.gate && !item.gate.ok ? item.gate.reason : (item.error ?? "unknown failure");
|
|
99
|
+
const pending = run.items.filter((i) => i.status === "queued" || i.status === "running").length;
|
|
100
|
+
return [
|
|
101
|
+
`[swarm ${run.runId}] item ${item.index + 1} (${item.id}, ${item.agent}) failed: ${reason}`,
|
|
102
|
+
pending > 0
|
|
103
|
+
? `${pending} item${pending === 1 ? " is" : "s are"} still running and the full report follows when they settle — this is a warning, not the result. Do not build on this item's output, and do not poll swarm_status.`
|
|
104
|
+
: "The full report follows.",
|
|
105
|
+
].join("\n");
|
|
106
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* No imports from pi packages: src/ typechecks and runs standalone.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import type { GateOutcome } from "./gate.ts";
|
|
7
|
+
import type { TaskOutcome, Verification } from "./outcome.ts";
|
|
8
|
+
|
|
6
9
|
export const VALID_TOOLS = [
|
|
7
10
|
"read",
|
|
8
11
|
"bash",
|
|
@@ -48,7 +51,27 @@ export const DEFAULT_CONCURRENCY = 4;
|
|
|
48
51
|
/** Safe default when no routing rule matches: read-only exploration. */
|
|
49
52
|
export const FALLBACK_AGENT = "scout";
|
|
50
53
|
|
|
51
|
-
|
|
54
|
+
/**
|
|
55
|
+
* "skipped" is a settled state, not a failure: the item never ran because
|
|
56
|
+
* something it needed did not produce usable input, and the caller asked for
|
|
57
|
+
* that to stop the branch rather than feed it a failure notice.
|
|
58
|
+
*/
|
|
59
|
+
export type ItemStatus = "queued" | "running" | "done" | "error" | "aborted" | "skipped";
|
|
60
|
+
|
|
61
|
+
/** What a gate proved about one item, kept alongside the item it judged. */
|
|
62
|
+
export interface GateRecord {
|
|
63
|
+
command: string;
|
|
64
|
+
outcome: GateOutcome;
|
|
65
|
+
ok: boolean;
|
|
66
|
+
/** One line in this package's words. */
|
|
67
|
+
reason: string;
|
|
68
|
+
/** Trimmed output, kept only when the gate did not pass. */
|
|
69
|
+
output?: string;
|
|
70
|
+
/** Other items that were live in the same directory while it ran. */
|
|
71
|
+
sharedWith?: string[];
|
|
72
|
+
/** Repair passes spent trying to make it pass. */
|
|
73
|
+
repairs?: number;
|
|
74
|
+
}
|
|
52
75
|
|
|
53
76
|
export interface ItemState {
|
|
54
77
|
index: number;
|
|
@@ -63,8 +86,18 @@ export interface ItemState {
|
|
|
63
86
|
tokens: number;
|
|
64
87
|
result: string | null;
|
|
65
88
|
error: string | null;
|
|
89
|
+
/** The directory the child worked in — its worktree when isolated. */
|
|
90
|
+
workDir?: string;
|
|
91
|
+
/** Set once the item settles: what the task came to, apart from whether the child finished. */
|
|
92
|
+
outcome?: TaskOutcome;
|
|
93
|
+
/** How well that outcome is known. "not-requested" when no gate ran. */
|
|
94
|
+
verification?: Verification;
|
|
95
|
+
gate?: GateRecord;
|
|
66
96
|
}
|
|
67
97
|
|
|
98
|
+
/** How a dependent behaves when something it needs did not succeed. */
|
|
99
|
+
export type UpstreamFailurePolicy = "continue" | "skip";
|
|
100
|
+
|
|
68
101
|
/**
|
|
69
102
|
* "cancelled" is its own outcome, not a completion: someone stopped the run,
|
|
70
103
|
* and calling it done would report results nobody produced.
|
package/src/wait.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waiting on a run without teaching the model to poll.
|
|
3
|
+
*
|
|
4
|
+
* pending.ts says "do not poll" and means it — in an interactive session the
|
|
5
|
+
* report arrives on its own. A headless `pi -p` run has no such delivery, so
|
|
6
|
+
* there the model's only move was to call swarm_status again, and again, each
|
|
7
|
+
* call a full turn. `wait` lets one call sit on the run for a bounded time
|
|
8
|
+
* instead: the answer is the same either way, it just arrives in one turn.
|
|
9
|
+
*
|
|
10
|
+
* Pure: the condition, the budget and the tool's own AbortSignal. The
|
|
11
|
+
* extension owns what is being waited for.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Resolve true as soon as `check()` holds, false when `ms` runs out or the
|
|
16
|
+
* signal fires first. Polls rather than subscribes: the run has no event to
|
|
17
|
+
* listen to, and a check every `intervalMs` costs nothing measurable.
|
|
18
|
+
*/
|
|
19
|
+
export function waitUntil(
|
|
20
|
+
check: () => boolean,
|
|
21
|
+
ms: number,
|
|
22
|
+
intervalMs = 250,
|
|
23
|
+
signal?: AbortSignal,
|
|
24
|
+
): Promise<boolean> {
|
|
25
|
+
if (check()) return Promise.resolve(true);
|
|
26
|
+
if (!(ms > 0) || signal?.aborted) return Promise.resolve(false);
|
|
27
|
+
const interval = Math.max(1, intervalMs);
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
const deadline = Date.now() + ms;
|
|
30
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
31
|
+
const finish = (value: boolean): void => {
|
|
32
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
33
|
+
signal?.removeEventListener("abort", onAbort);
|
|
34
|
+
resolve(value);
|
|
35
|
+
};
|
|
36
|
+
// An abort is the caller leaving, not a verdict; if the condition happens
|
|
37
|
+
// to hold by then, say so rather than reporting a wait that never happened.
|
|
38
|
+
const onAbort = (): void => finish(check());
|
|
39
|
+
const tick = (): void => {
|
|
40
|
+
if (check()) return finish(true);
|
|
41
|
+
const left = deadline - Date.now();
|
|
42
|
+
if (left <= 0) return finish(false);
|
|
43
|
+
timer = setTimeout(tick, Math.min(interval, left));
|
|
44
|
+
};
|
|
45
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
46
|
+
timer = setTimeout(tick, Math.min(interval, ms));
|
|
47
|
+
});
|
|
48
|
+
}
|
package/src/widget.ts
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import { clampRows, clampWidth, MAX_WIDGET_ROWS } from "./widget-clamp.ts";
|
|
2
|
-
import type { SwarmRun, ThemeLike } from "./types.ts";
|
|
2
|
+
import type { ItemState, SwarmRun, ThemeLike } from "./types.ts";
|
|
3
3
|
|
|
4
4
|
const WIDTH = 54;
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* The row reports the *task*, not the child session. An item whose agent ran to
|
|
8
|
+
* the end and then failed its gate used to sit here as a green ✓, which is
|
|
9
|
+
* precisely the confusion the outcome field exists to remove.
|
|
10
|
+
*/
|
|
11
|
+
function icon(item: ItemState): string {
|
|
12
|
+
switch (item.status) {
|
|
8
13
|
case "queued":
|
|
9
14
|
return "·";
|
|
10
15
|
case "running":
|
|
11
16
|
return "⟳";
|
|
17
|
+
case "skipped":
|
|
18
|
+
return "⊘";
|
|
12
19
|
case "done":
|
|
13
|
-
return "✓";
|
|
20
|
+
return item.outcome === "failed" ? "✗" : item.outcome === "blocked" ? "⚠" : "✓";
|
|
14
21
|
case "error":
|
|
15
22
|
return "✗";
|
|
16
23
|
default:
|
|
@@ -18,6 +25,17 @@ function icon(status: string): string {
|
|
|
18
25
|
}
|
|
19
26
|
}
|
|
20
27
|
|
|
28
|
+
type Tone = "dim" | "warning" | "success" | "error";
|
|
29
|
+
|
|
30
|
+
function tone(item: ItemState): Tone {
|
|
31
|
+
if (item.status === "queued" || item.status === "skipped") return "dim";
|
|
32
|
+
if (item.status === "running") return "warning";
|
|
33
|
+
if (item.status !== "done") return "error";
|
|
34
|
+
if (item.outcome === "failed") return "error";
|
|
35
|
+
if (item.outcome === "blocked") return "warning";
|
|
36
|
+
return "success";
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
/** Widget above the editor for the active (or just-finished) run. */
|
|
22
40
|
export function buildWidgetLines(run: SwarmRun | null, theme: ThemeLike, now: number): string[] {
|
|
23
41
|
if (!run) return [];
|
|
@@ -33,15 +51,9 @@ export function buildWidgetLines(run: SwarmRun | null, theme: ThemeLike, now: nu
|
|
|
33
51
|
// Cap the rows: a large swarm would otherwise push the editor off screen,
|
|
34
52
|
// since a Text-factory widget bypasses pi's ten-line guard.
|
|
35
53
|
const rows = run.items.map((item) => {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
: item.status === "done"
|
|
40
|
-
? (s: string) => theme.fg("success", s)
|
|
41
|
-
: item.status === "queued"
|
|
42
|
-
? dim
|
|
43
|
-
: (s: string) => theme.fg("error", s);
|
|
44
|
-
return `${dim("│ ")}${paint(`${icon(item.status)} ${clampWidth(item.agent, 24)}`)}${dim(` ${clampWidth(item.item, 32)}`)}`;
|
|
54
|
+
const color = tone(item);
|
|
55
|
+
const paint = (s: string) => theme.fg(color, s);
|
|
56
|
+
return `${dim("│ ")}${paint(`${icon(item)} ${clampWidth(item.agent, 24)}`)}${dim(` ${clampWidth(item.item, 32)}`)}`;
|
|
45
57
|
});
|
|
46
58
|
for (const row of clampRows(rows, MAX_WIDGET_ROWS, (hidden) => dim(`│ … +${hidden} more`))) {
|
|
47
59
|
lines.push(row);
|