@nredd/pi-subagents 0.22.0-nredd.8 → 0.22.0-nredd.9
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 +136 -55
- package/package.json +14 -11
- package/src/abortable.ts +1 -1
- package/src/agent-file-toggle.ts +0 -1
- package/src/agent-manager.ts +360 -103
- package/src/agent-runner.ts +121 -63
- package/src/child-context.ts +19 -1
- package/src/completion-outbox.ts +22 -3
- package/src/cross-extension-rpc.ts +10 -6
- package/src/custom-agents.ts +2 -1
- package/src/default-agents.ts +19 -17
- package/src/detached-results.ts +18 -2
- package/src/enterprise-local.ts +2 -1
- package/src/error-message.ts +11 -0
- package/src/git-path.ts +66 -0
- package/src/group-join.ts +4 -10
- package/src/index.ts +731 -231
- package/src/invocation-config.ts +1 -5
- package/src/mention-clone.ts +4 -3
- package/src/model-resolver.ts +51 -14
- package/src/model-selection.ts +17 -6
- package/src/model-tiers.ts +5 -0
- package/src/nested-tools.ts +80 -21
- package/src/notification-result.ts +118 -0
- package/src/plan-mode-children.ts +176 -0
- package/src/prompts.ts +7 -2
- package/src/quota-admission.ts +16 -6
- package/src/resource-loader.ts +20 -6
- package/src/schedule.ts +65 -19
- package/src/settings.ts +21 -1
- package/src/skill-loader.ts +244 -94
- package/src/status-note.ts +28 -5
- package/src/types.ts +52 -2
- package/src/ui/agent-mention.ts +5 -2
- package/src/ui/agent-widget.ts +41 -22
- package/src/ui/conversation-viewer.ts +4 -2
- package/src/ui/fallback-flag.ts +28 -4
- package/src/ui/fleet-list.ts +89 -54
- package/src/ui/notification-view.ts +4 -1
- package/src/ui/workflow-card.ts +1 -8
- package/src/ui/workflow-dialog.ts +31 -18
- package/src/workflow/host.ts +4 -5
- package/src/workflow/meta.ts +7 -4
- package/src/workflow/progress.ts +4 -3
- package/src/workflow/runtime.ts +23 -7
- package/src/workflow/saved.ts +2 -4
- package/src/workflow/task.ts +12 -7
- package/src/workflow/tool-description.ts +2 -2
- package/src/workflow/worker-source.ts +6 -2
- package/src/worktree.ts +309 -19
|
@@ -61,12 +61,11 @@ import {
|
|
|
61
61
|
type WorkflowDisplayState,
|
|
62
62
|
type WorkflowEntry,
|
|
63
63
|
} from "../workflow/progress.js";
|
|
64
|
-
import { SPINNER, type Theme } from "./agent-widget.js";
|
|
65
|
-
import { fallbackFlagText, hasFallback } from "./fallback-flag.js";
|
|
64
|
+
import { formatCompactTokens, SPINNER, type Theme } from "./agent-widget.js";
|
|
65
|
+
import { boundedWrappedRows, fallbackFlagText, hasFallback, withLeadingFlag } from "./fallback-flag.js";
|
|
66
66
|
import {
|
|
67
67
|
ASCII_GLYPHS,
|
|
68
68
|
clampLine,
|
|
69
|
-
formatCompactTokens,
|
|
70
69
|
formatModel,
|
|
71
70
|
formatThinking,
|
|
72
71
|
REPLAYED_ANNOTATION,
|
|
@@ -105,6 +104,8 @@ export const MIN_PANE_BODY_ROWS = 6;
|
|
|
105
104
|
export const PROMPT_COLLAPSED_LINES = 4;
|
|
106
105
|
/** Spinner cadence. Unlike the card's 1s header tick, this row really animates. */
|
|
107
106
|
export const WORKFLOW_DIALOG_SPINNER_MS = 80;
|
|
107
|
+
/** Maximum physical detail rows a fallback note may occupy. */
|
|
108
|
+
const MODEL_NOTE_ROWS = 4;
|
|
108
109
|
|
|
109
110
|
/* ------------------------------------------------------------------------- *
|
|
110
111
|
* Glyphs
|
|
@@ -459,12 +460,6 @@ export function leftPaneWidth(width: number): number {
|
|
|
459
460
|
return Math.max(1, Math.min(LEFT_PANE_WIDTH, Math.floor(available / 3), available - 1));
|
|
460
461
|
}
|
|
461
462
|
|
|
462
|
-
/** Fill a title out to the cell width with rule, so the corners stay put. */
|
|
463
|
-
function padTitle(line: WorkflowCardLine, width: number, horizontal: string): WorkflowCardLine {
|
|
464
|
-
const gap = Math.max(0, width - lineWidth(line));
|
|
465
|
-
return gap > 0 ? [...line, { text: horizontal.repeat(gap), color: "dim" }] : line;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
463
|
/** One pane row, padded to exactly `width` so the frame stays vertical. */
|
|
469
464
|
function padCell(line: WorkflowCardLine, width: number): WorkflowCardLine {
|
|
470
465
|
const clamped = clampLine(line, width);
|
|
@@ -519,9 +514,9 @@ function paneFrame(options: {
|
|
|
519
514
|
const lines: WorkflowCardLine[] = [];
|
|
520
515
|
lines.push([
|
|
521
516
|
{ text: box.topLeft, color: "dim" },
|
|
522
|
-
...
|
|
517
|
+
...frameTitle(options.leftTitle, left, glyphs),
|
|
523
518
|
{ text: box.topTee, color: "dim" },
|
|
524
|
-
...
|
|
519
|
+
...frameTitle(options.rightTitle, right, glyphs),
|
|
525
520
|
{ text: box.topRight, color: "dim" },
|
|
526
521
|
]);
|
|
527
522
|
|
|
@@ -638,18 +633,33 @@ function agentRow(options: {
|
|
|
638
633
|
}): WorkflowCardLine {
|
|
639
634
|
const { entry, selected, glyphs, width } = options;
|
|
640
635
|
const display = displayState(entry, options.workflowActive);
|
|
641
|
-
const
|
|
636
|
+
const prefix: WorkflowCardLine = [
|
|
642
637
|
{ text: " " },
|
|
643
638
|
{ text: selected ? glyphs.pointer : " ", color: "accent" },
|
|
644
639
|
{ text: " " },
|
|
645
640
|
dialogRowGlyph(display, glyphs, options.spinnerFrame),
|
|
646
641
|
{ text: " " },
|
|
647
|
-
|
|
648
|
-
|
|
642
|
+
];
|
|
643
|
+
const flag = entry.modelTier || hasFallback(entry) ? fallbackFlagText(1, entry) : "";
|
|
644
|
+
const flagColor = hasFallback(entry) ? "warning" as const : "dim" as const;
|
|
645
|
+
// The narrow pane holds the label and nothing else; reserve its status prefix,
|
|
646
|
+
// then let the shared flag degrade rather than clipping it to `[fal…`.
|
|
647
|
+
if (options.compact && flag) {
|
|
648
|
+
const flagWidth = Math.max(0, width - lineWidth(prefix));
|
|
649
|
+
const compactFlag = withLeadingFlag("", flag, "", flagWidth);
|
|
650
|
+
const labelWidth = Math.max(0, flagWidth - visibleWidth(compactFlag) - 1);
|
|
651
|
+
const label = truncateToWidth(entry.label, labelWidth, "");
|
|
652
|
+
return [
|
|
653
|
+
...prefix,
|
|
654
|
+
{ text: compactFlag, color: flagColor },
|
|
655
|
+
...(label ? [{ text: ` ${label}`, color: selected ? "accent" as const : undefined }] : []),
|
|
656
|
+
];
|
|
657
|
+
}
|
|
658
|
+
const head: WorkflowCardLine = [
|
|
659
|
+
...prefix,
|
|
660
|
+
...(flag ? [{ text: `${flag} `, color: flagColor }] : []),
|
|
649
661
|
{ text: entry.label, color: selected ? "accent" : undefined },
|
|
650
662
|
];
|
|
651
|
-
// The narrow pane holds the label and nothing else; there is no room for a
|
|
652
|
-
// stat tail, and clamping one would just spend columns on a truncated word.
|
|
653
663
|
if (options.compact) return clampLine(head, width);
|
|
654
664
|
|
|
655
665
|
const model = formatModel(entry);
|
|
@@ -800,9 +810,12 @@ export function layoutWorkflowDialog(input: WorkflowDialogInput): WorkflowCardLi
|
|
|
800
810
|
detailRows.push(clampLine([{ text: ` ${stats.join(" · ")}`, color: "dim" }], rightWidth));
|
|
801
811
|
}
|
|
802
812
|
// A substituted model is never cosmetic: name the failed input and what ran.
|
|
803
|
-
//
|
|
813
|
+
// Bound physical rows, not text width: a configured value can contain
|
|
814
|
+
// newlines, each of which otherwise displaces the final diagnosis below the
|
|
815
|
+
// pane's fixed row budget.
|
|
804
816
|
if (entry.modelNote) {
|
|
805
|
-
|
|
817
|
+
const noteWidth = Math.max(1, rightWidth - 1);
|
|
818
|
+
for (const line of boundedWrappedRows(entry.modelNote, noteWidth, MODEL_NOTE_ROWS, glyphs.ellipsis)) {
|
|
806
819
|
detailRows.push(clampLine([{ text: ` ${line}`, color: "warning" }], rightWidth));
|
|
807
820
|
}
|
|
808
821
|
}
|
package/src/workflow/host.ts
CHANGED
|
@@ -167,10 +167,6 @@ function toSpawnResult(record: AgentRecord, opts: { resumed?: boolean } = {}): W
|
|
|
167
167
|
return { ...common, ...modelNote, ok: false, error: `${lead}${record.error ?? `Agent ${record.status}.`}` };
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
/** Shell used to run a `gate` command, mirroring how a user would type it. */
|
|
171
|
-
const GATE_SHELL: readonly [string, string] =
|
|
172
|
-
process.platform === "win32" ? ["cmd", "/c"] : ["sh", "-c"];
|
|
173
|
-
|
|
174
170
|
export function createWorkflowHost(deps: WorkflowHostOptions): WorkflowHost {
|
|
175
171
|
const { pi, ctx, manager } = deps;
|
|
176
172
|
/** Runtime agent id → the manager record it spawned. Never pruned mid-run. */
|
|
@@ -189,7 +185,10 @@ export function createWorkflowHost(deps: WorkflowHostOptions): WorkflowHost {
|
|
|
189
185
|
* decided from one behaviour, whichever route the command took.
|
|
190
186
|
*/
|
|
191
187
|
async function executeGate(command: string, cwd: string): Promise<WorkflowGateResult> {
|
|
192
|
-
|
|
188
|
+
// The shell a user would type the command into. Read per call rather than
|
|
189
|
+
// at import, so the choice follows the platform the gate actually runs on.
|
|
190
|
+
const [shell, flag] = process.platform === "win32" ? ["cmd", "/c"] : ["sh", "-c"];
|
|
191
|
+
const result = await pi.exec(shell, [flag, command], {
|
|
193
192
|
cwd,
|
|
194
193
|
timeout: deps.gateTimeoutMs ?? DEFAULT_GATE_TIMEOUT_MS,
|
|
195
194
|
...(deps.signal !== undefined ? { signal: deps.signal } : {}),
|
package/src/workflow/meta.ts
CHANGED
|
@@ -38,6 +38,8 @@ export interface WorkflowMeta {
|
|
|
38
38
|
|
|
39
39
|
export interface MetaExtraction {
|
|
40
40
|
meta: WorkflowMeta;
|
|
41
|
+
/** Byte offset immediately after the validated meta object literal. */
|
|
42
|
+
metaEnd: number;
|
|
41
43
|
/**
|
|
42
44
|
* The script with the leading `export ` stripped, so `const meta = {...}`
|
|
43
45
|
* compiles inside the vm. Byte offsets after the keyword are untouched, which
|
|
@@ -175,8 +177,9 @@ function scanObjectLiteral(source: string, open: number): ScanResult {
|
|
|
175
177
|
*/
|
|
176
178
|
function isRegexPosition(source: string, i: number): boolean {
|
|
177
179
|
let j = i - 1;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
+
// Never runs off the front: the scan starts at the literal's `{`, so the
|
|
181
|
+
// walk back always stops on it or something after it.
|
|
182
|
+
while (/\s/.test(source[j])) j--;
|
|
180
183
|
const prev = source[j];
|
|
181
184
|
// Identifier/number/closer before `/` means division.
|
|
182
185
|
return !/[\w$)\]]/.test(prev);
|
|
@@ -289,7 +292,7 @@ export function extractMeta(source: string): MetaExtraction {
|
|
|
289
292
|
const exportAt = source.indexOf("export", declaration.index);
|
|
290
293
|
const body = `${source.slice(0, exportAt)}${" ".repeat(6)}${source.slice(exportAt + 6)}`;
|
|
291
294
|
|
|
292
|
-
return { meta, body };
|
|
295
|
+
return { meta, metaEnd: close, body };
|
|
293
296
|
}
|
|
294
297
|
|
|
295
298
|
/**
|
|
@@ -309,7 +312,7 @@ export function workflowCallName(args: { script?: string; scriptPath?: string; n
|
|
|
309
312
|
if (source === undefined || source === "") {
|
|
310
313
|
// A path-only call would need a synchronous file read per rebuild to do
|
|
311
314
|
// better than this, and the file name is what the author will recognize.
|
|
312
|
-
if (args.scriptPath !== undefined) return args.scriptPath.
|
|
315
|
+
if (args.scriptPath !== undefined) return args.scriptPath.replace(/^[\s\S]*[/\\]/, "");
|
|
313
316
|
// A saved workflow is already named by the caller; no read needed at all.
|
|
314
317
|
return args.name !== undefined && args.name !== "" ? args.name : "workflow";
|
|
315
318
|
}
|
package/src/workflow/progress.ts
CHANGED
|
@@ -514,11 +514,12 @@ export function gerund(word: string): string {
|
|
|
514
514
|
|
|
515
515
|
// Short consonant-vowel-consonant words double the final consonant: run →
|
|
516
516
|
// running. `w`, `x` and `y` never double.
|
|
517
|
-
|
|
517
|
+
// GERUND_CANDIDATE guarantees at least three letters, so every index exists.
|
|
518
|
+
const last = lower[lower.length - 1];
|
|
518
519
|
if (
|
|
519
520
|
lower.length <= 4 &&
|
|
520
|
-
!VOWELS.includes(lower.
|
|
521
|
-
VOWELS.includes(lower.
|
|
521
|
+
!VOWELS.includes(lower[lower.length - 3]) &&
|
|
522
|
+
VOWELS.includes(lower[lower.length - 2]) &&
|
|
522
523
|
!VOWELS.includes(last) &&
|
|
523
524
|
!"wxy".includes(last)
|
|
524
525
|
) {
|
package/src/workflow/runtime.ts
CHANGED
|
@@ -153,8 +153,8 @@ export interface WorkflowSpawnResult {
|
|
|
153
153
|
* verifies the wrong working copy. Left unset, a gate runs wherever the host
|
|
154
154
|
* runs commands by default.
|
|
155
155
|
*
|
|
156
|
-
* Usually unset for a worktree child even so: the copy is removed during
|
|
157
|
-
* child's own settle, so it no longer exists by the time this is read. That
|
|
156
|
+
* Usually unset for a worktree child even so: the copy is normally removed during
|
|
157
|
+
* the child's own settle, so it no longer exists by the time this is read. That
|
|
158
158
|
* is what {@link gate} is for.
|
|
159
159
|
*/
|
|
160
160
|
cwd?: string;
|
|
@@ -282,6 +282,19 @@ export interface RunWorkflowOptions {
|
|
|
282
282
|
signal?: AbortSignal;
|
|
283
283
|
/** Fired per batch, not per entry — see the worker's progress batching. */
|
|
284
284
|
onProgress?(entries: readonly WorkflowEntry[]): void;
|
|
285
|
+
/**
|
|
286
|
+
* Receives optional Istanbul counters authored inside a workflow VM.
|
|
287
|
+
*
|
|
288
|
+
* The runtime itself never instruments scripts. Test producers may pass
|
|
289
|
+
* instrumented source and merge these counters into their own collector,
|
|
290
|
+
* retaining worker/VM execution rather than measuring a substitute runner.
|
|
291
|
+
*/
|
|
292
|
+
onScriptCoverage?(coverage: unknown): void;
|
|
293
|
+
/**
|
|
294
|
+
* Trusted caller-side transform applied after the workflow's metadata has
|
|
295
|
+
* passed validation and before its body enters the worker VM.
|
|
296
|
+
*/
|
|
297
|
+
transformScriptBody?(input: { script: string; body: string }): string;
|
|
285
298
|
concurrency?: number;
|
|
286
299
|
agentCap?: number;
|
|
287
300
|
itemCap?: number;
|
|
@@ -357,7 +370,8 @@ function walk(value: unknown, path: string, seen: Set<object>): void {
|
|
|
357
370
|
if (kind === "bigint") throw boundaryError("a BigInt", path);
|
|
358
371
|
if (kind === "symbol") throw boundaryError("a symbol", path);
|
|
359
372
|
if (kind === "function") throw boundaryError("a function", path);
|
|
360
|
-
|
|
373
|
+
// Every other `typeof` result is handled above; a new one fails `tsc` here.
|
|
374
|
+
kind satisfies "object";
|
|
361
375
|
|
|
362
376
|
const object = value as object;
|
|
363
377
|
if (seen.has(object)) throw boundaryError("a circular structure", path);
|
|
@@ -457,7 +471,7 @@ interface AgentCallPayload {
|
|
|
457
471
|
type WorkerMessage =
|
|
458
472
|
| { type: "call"; callId: number; method: string; payload: AgentCallPayload }
|
|
459
473
|
| { type: "progress"; entries: WorkflowEntry[] }
|
|
460
|
-
| { type: "complete"; resultJson?: string }
|
|
474
|
+
| { type: "complete"; resultJson?: string; coverage?: unknown }
|
|
461
475
|
| { type: "error"; message: string; stack?: string };
|
|
462
476
|
|
|
463
477
|
/** Everything below 0x20 except tab, newline and carriage return, plus DEL. */
|
|
@@ -595,7 +609,8 @@ export async function runWorkflow(options: RunWorkflowOptions): Promise<Workflow
|
|
|
595
609
|
|
|
596
610
|
assertBoundarySafe(options.args, "args");
|
|
597
611
|
|
|
598
|
-
const { meta, body } = validateScript(script);
|
|
612
|
+
const { meta, body: validatedBody } = validateScript(script);
|
|
613
|
+
const body = options.transformScriptBody?.({ script, body: validatedBody }) ?? validatedBody;
|
|
599
614
|
const agentCap = options.agentCap ?? WORKFLOW_AGENT_CAP;
|
|
600
615
|
const itemCap = options.itemCap ?? WORKFLOW_ITEM_CAP;
|
|
601
616
|
const semaphore = new Semaphore(options.concurrency ?? workflowConcurrency());
|
|
@@ -732,12 +747,12 @@ export async function runWorkflow(options: RunWorkflowOptions): Promise<Workflow
|
|
|
732
747
|
argsJson: options.args === undefined ? undefined : JSON.stringify(options.args),
|
|
733
748
|
itemCap,
|
|
734
749
|
nestedCap: options.nestedCap ?? WORKFLOW_NESTED_CAP,
|
|
750
|
+
collectCoverage: options.onScriptCoverage !== undefined,
|
|
735
751
|
},
|
|
736
752
|
});
|
|
737
753
|
|
|
738
754
|
return await new Promise<WorkflowRunResult>(resolve => {
|
|
739
755
|
const emit = (entries: WorkflowEntry[]) => {
|
|
740
|
-
if (entries.length === 0) return;
|
|
741
756
|
progress.push(...entries);
|
|
742
757
|
options.onProgress?.(entries);
|
|
743
758
|
};
|
|
@@ -1174,7 +1189,7 @@ export async function runWorkflow(options: RunWorkflowOptions): Promise<Workflow
|
|
|
1174
1189
|
respond(callId, true, {
|
|
1175
1190
|
name: child.meta.name,
|
|
1176
1191
|
metaJson: JSON.stringify(child.meta),
|
|
1177
|
-
body: child.body,
|
|
1192
|
+
body: options.transformScriptBody?.({ script: source.script, body: child.body }) ?? child.body,
|
|
1178
1193
|
});
|
|
1179
1194
|
} catch (error) {
|
|
1180
1195
|
respond(callId, false, undefined, error instanceof Error ? error.message : String(error));
|
|
@@ -1199,6 +1214,7 @@ export async function runWorkflow(options: RunWorkflowOptions): Promise<Workflow
|
|
|
1199
1214
|
void handleAgent(message.callId, message.payload as AgentCallPayload);
|
|
1200
1215
|
break;
|
|
1201
1216
|
case "complete": {
|
|
1217
|
+
if (message.coverage !== undefined) options.onScriptCoverage?.(message.coverage);
|
|
1202
1218
|
// The script is done, so every launch it made should have been
|
|
1203
1219
|
// answered by now — a response is sent before the worker can post
|
|
1204
1220
|
// this, so anything still open was never awaited. finish() aborts
|
package/src/workflow/saved.ts
CHANGED
|
@@ -162,10 +162,8 @@ export function resolveWorkflowSource(
|
|
|
162
162
|
try {
|
|
163
163
|
return { ok: true, script: readFileSync(resolved, "utf-8"), path: resolved };
|
|
164
164
|
} catch (err) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
message: `Could not read workflow script "${resolved}": ${err instanceof Error ? err.message : String(err)}`,
|
|
168
|
-
};
|
|
165
|
+
// `readFileSync` only ever throws Node's own `Error`s.
|
|
166
|
+
return { ok: false, message: `Could not read workflow script "${resolved}": ${(err as Error).message}` };
|
|
169
167
|
}
|
|
170
168
|
}
|
|
171
169
|
const name = ref.name?.trim();
|
package/src/workflow/task.ts
CHANGED
|
@@ -78,6 +78,8 @@ export interface WorkflowTask {
|
|
|
78
78
|
logs: string[];
|
|
79
79
|
|
|
80
80
|
abortController: AbortController;
|
|
81
|
+
/** Set when the parent stopped the run with `stop_subagent`; unset for a human stop. */
|
|
82
|
+
stoppedBy?: "parent";
|
|
81
83
|
startTime: number;
|
|
82
84
|
endTime?: number;
|
|
83
85
|
/** Excluded from the elapsed clock the header shows. */
|
|
@@ -184,19 +186,22 @@ export function resumeWorkflowTask(task: WorkflowTask, now = Date.now()): boolea
|
|
|
184
186
|
if (task.status !== "paused" || task.control === undefined) return false;
|
|
185
187
|
task.control.resume();
|
|
186
188
|
task.status = "running";
|
|
187
|
-
|
|
188
|
-
task.pausedAt = undefined;
|
|
189
|
+
bankPause(task, now);
|
|
189
190
|
return true;
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
/** Close out an open pause into `totalPausedMs`; a no-op when the run is not held. */
|
|
194
|
+
function bankPause(task: WorkflowTask, now: number): void {
|
|
195
|
+
if (task.pausedAt === undefined) return;
|
|
196
|
+
task.totalPausedMs += Math.max(0, now - task.pausedAt);
|
|
197
|
+
task.pausedAt = undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
192
200
|
/** Settle a task from the run's own result. */
|
|
193
201
|
export function completeWorkflowTask(task: WorkflowTask, result: WorkflowRunResult): void {
|
|
194
202
|
// Banked before the status moves off "paused": a run that finished while held
|
|
195
203
|
// still spent that time held, and the elapsed figure has to say so.
|
|
196
|
-
|
|
197
|
-
task.totalPausedMs = (task.totalPausedMs ?? 0) + Math.max(0, Date.now() - task.pausedAt);
|
|
198
|
-
task.pausedAt = undefined;
|
|
199
|
-
}
|
|
204
|
+
bankPause(task, Date.now());
|
|
200
205
|
// Nothing left to control, and holding the handle would let the dialog offer
|
|
201
206
|
// pause on a run that has already stopped.
|
|
202
207
|
task.control = undefined;
|
|
@@ -293,7 +298,7 @@ export function formatWorkflowNotification(task: WorkflowTask, now = Date.now())
|
|
|
293
298
|
const totals = stats(task.workflowProgress, task.agentCount);
|
|
294
299
|
const status =
|
|
295
300
|
task.status === "completed" ? "Done"
|
|
296
|
-
: task.status === "killed" ? "Stopped"
|
|
301
|
+
: task.status === "killed" ? (task.stoppedBy === "parent" ? "Stopped by the parent with stop_subagent" : "Stopped")
|
|
297
302
|
: `Error: ${task.error ?? "unknown"}`;
|
|
298
303
|
const result = workflowResultText(task);
|
|
299
304
|
return [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* - `budget.total` is always null; pi has no token-target directive.
|
|
20
20
|
* - `parallel` propagates a fatal run error instead of folding it to null.
|
|
21
21
|
* - `effort` inherits the agent definition's level, then the parent's.
|
|
22
|
-
* - `isolation` removes the worktree on settle, changes kept on a branch.
|
|
22
|
+
* - `isolation` removes the worktree on settle (kept if cleanup fails), changes kept on a branch.
|
|
23
23
|
* Additions with no upstream counterpart: `gate`, `resume`, `effort: "minimal"`,
|
|
24
24
|
* the saved-workflow directories, and the reject-unknown-options guarantee.
|
|
25
25
|
*
|
|
@@ -77,7 +77,7 @@ Every script must begin with \`export const meta = {...}\`:
|
|
|
77
77
|
The \`meta\` object must be a PURE LITERAL — no variables, function calls, spreads, or template interpolation. Required fields: \`name\`, \`description\`. Optional: \`whenToUse\` (shown in the workflow list), \`phases\`. Use the SAME phase titles in meta.phases as in phase() calls — titles are matched exactly; a phase() call with no matching meta entry just gets its own progress group. Add \`model\` to a phase entry when that phase uses a specific model override.
|
|
78
78
|
|
|
79
79
|
Script body hooks:
|
|
80
|
-
- agent(prompt: string, opts?: {label?: string, phase?: string, schema?: object, model?: string, effort?: string, isolation?: 'worktree', agentType?: string, gate?: string, resume?: string}): Promise<any> — spawn a subagent. Without schema, returns its final text as a string. With schema (a JSON Schema), the subagent is given a StructuredOutput tool built from it and agent() returns the validated object — no parsing needed. A payload that does not match is rejected back to the child, which corrects it; a child that never answers through the tool gets one more prompt and then fails, so the call returns null — filter after every schema stage. Returns null if the user skips the agent mid-run or the subagent dies on a terminal API error after retries (filter with .filter(Boolean)). opts.label overrides the display label. opts.phase explicitly assigns this agent to a progress group (use this inside pipeline()/parallel() stages to avoid races on the global phase() state — same phase string → same group box). opts.model overrides the model for this agent call. Default to omitting it
|
|
80
|
+
- agent(prompt: string, opts?: {label?: string, phase?: string, schema?: object, model?: string, effort?: string, isolation?: 'worktree', agentType?: string, gate?: string, resume?: string}): Promise<any> — spawn a subagent. Without schema, returns its final text as a string. With schema (a JSON Schema), the subagent is given a StructuredOutput tool built from it and agent() returns the validated object — no parsing needed. A payload that does not match is rejected back to the child, which corrects it; a child that never answers through the tool gets one more prompt and then fails, so the call returns null — filter after every schema stage. Returns null if the user skips the agent mid-run or the subagent dies on a terminal API error after retries (filter with .filter(Boolean)). opts.label overrides the display label. opts.phase explicitly assigns this agent to a progress group (use this inside pipeline()/parallel() stages to avoid races on the global phase() state — same phase string → same group box). opts.model overrides the model for this agent call. Default to omitting it so the configured selection policy applies: agentModels for the type, then agentModels["*"], then the agent definition, then the parent model. Only set it when you're highly confident a different tier fits the task; when unsure, omit. opts.effort overrides the reasoning effort for this agent call ('minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max') — omit to inherit the agent definition's own level, then the parent's; use 'low' for cheap mechanical stages and higher tiers only for the hardest verify/judge stages. opts.isolation: 'worktree' runs the agent in a fresh git worktree — EXPENSIVE (setup time + disk per agent), use ONLY when agents mutate files in parallel and would otherwise conflict; the worktree is removed when the agent settles, its changes preserved on a branch; if cleanup fails the worktree is kept and the agent errors. opts.gate: '<command>' runs a shell command after the agent finishes and requires it to pass — a non-zero exit marks the agent failed, returns null to the script, and records the command output on the workflow agent error; prefer gate: 'npm test' over asking another agent whether the code looks right. opts.resume: '<label>' continues the child that ran under that label instead of starting fresh, so an iterative loop keeps its context — it cannot be combined with agentType, model, effort, isolation, gate or schema. opts.agentType uses a custom subagent type instead of the default workflow subagent — resolved from the same registry as the Agent tool; composes with schema. Available types:
|
|
81
81
|
{{typeList}}
|
|
82
82
|
- pipeline(items, stage1, stage2, ...): Promise<any[]> — run each item through all stages independently, NO barrier between stages. Item A can be in stage 3 while item B is still in stage 1. This is the DEFAULT for multi-stage work. Wall-clock = slowest single-item chain, not sum-of-slowest-per-stage. Every stage callback receives (prevResult, originalItem, index) — use originalItem/index in later stages to label work without threading context through stage 1's return value. A stage that throws drops that item to \`null\` and skips its remaining stages.
|
|
83
83
|
- parallel(thunks: Array<() => Promise<any>>): Promise<any[]> — run tasks concurrently. This is a BARRIER: awaits all thunks before returning. A thunk that throws (or whose agent errors) resolves to \`null\` in the result array, so \`.filter(Boolean)\` before using the results; only a fatal run error — a cap breach, or a nested workflow that could not load — propagates instead of being folded into a null. Use ONLY when you genuinely need all results together.
|
|
@@ -863,10 +863,14 @@ async function main() {
|
|
|
863
863
|
const value = await run(rootScope.agent, rootScope.phase, rootScope.log, rootScope.workflow, rootScope.console, sandbox.args);
|
|
864
864
|
checkBoundary(value, "the workflow result");
|
|
865
865
|
flushProgress();
|
|
866
|
-
|
|
866
|
+
const result = {
|
|
867
867
|
type: "complete",
|
|
868
868
|
resultJson: value === undefined ? undefined : JSON.stringify(value),
|
|
869
|
-
}
|
|
869
|
+
};
|
|
870
|
+
if (workerData.collectCoverage) {
|
|
871
|
+
result.coverage = vm.runInContext("globalThis.__coverage__", context);
|
|
872
|
+
}
|
|
873
|
+
port.postMessage(result);
|
|
870
874
|
}
|
|
871
875
|
|
|
872
876
|
/** Serialize diagnostics only: keep realm errors untouched and bound cause traversal/output. */
|