@henryqw/pi-subagent 4.1.1 → 5.0.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.
@@ -0,0 +1,9 @@
1
+ import type { EphemeralSubagentExecutor, EphemeralSubagentResult, EphemeralSubagentRunInput } from "@henryqw/pi-subagent";
2
+
3
+ /** Runs one caller-prepared child launch without making lifecycle decisions. */
4
+ export function runDelegation(
5
+ executor: EphemeralSubagentExecutor,
6
+ input: EphemeralSubagentRunInput,
7
+ ): Promise<EphemeralSubagentResult> {
8
+ return executor.run(input);
9
+ }
@@ -35,6 +35,7 @@ export type WorkflowTransportEntryDetails = {
35
35
  index: number;
36
36
  role: string;
37
37
  status: WorkflowTransportStatus;
38
+ summary?: string;
38
39
  model?: string;
39
40
  thinkingLevel?: string;
40
41
  worktree?: WorktreePayload;
@@ -71,6 +72,11 @@ function label(kind: TransportKind, failed: boolean): string {
71
72
  return `Workflow ${failed ? "failed" : "succeeded"}.`;
72
73
  }
73
74
 
75
+ export function displaySummary(text: string): string {
76
+ const line = text.split(/\r?\n/).find((candidate) => candidate.trim()) ?? "";
77
+ return Array.from(line.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").trim().split(/\s+/).join(" ")).slice(0, 160).join("");
78
+ }
79
+
74
80
  function evidenceFor(entry: WorkflowTransportEntry): Evidence | undefined {
75
81
  if (entry.status === "pending" || entry.status === "skipped") return;
76
82
  const source = entry.status === "failed" || entry.status === "rejected" ? entry.failure : entry.assistantOutput;
@@ -123,15 +129,20 @@ function formatWorkflowTransport(
123
129
  text: capEphemeralSubagentOutput(lines.join("\n")),
124
130
  details: {
125
131
  mode,
126
- entries: ordered.map((entry) => ({
127
- id: entry.id,
128
- index: entry.index,
129
- role: entry.role,
130
- status: entry.status,
131
- ...(entry.model === undefined ? {} : { model: entry.model }),
132
- ...(entry.thinkingLevel === undefined ? {} : { thinkingLevel: entry.thinkingLevel }),
133
- ...(entry.worktreePayload === undefined ? {} : { worktree: { ...entry.worktreePayload } }),
134
- })),
132
+ entries: ordered.map((entry) => {
133
+ const source = entry.status === "failed" || entry.status === "rejected" ? entry.failure
134
+ : entry.status === "running" || entry.status === "succeeded" ? entry.assistantOutput : undefined;
135
+ return {
136
+ id: entry.id,
137
+ index: entry.index,
138
+ role: entry.role,
139
+ status: entry.status,
140
+ ...(source === undefined ? {} : { summary: displaySummary(source) }),
141
+ ...(entry.model === undefined ? {} : { model: entry.model }),
142
+ ...(entry.thinkingLevel === undefined ? {} : { thinkingLevel: entry.thinkingLevel }),
143
+ ...(entry.worktreePayload === undefined ? {} : { worktree: { ...entry.worktreePayload } }),
144
+ };
145
+ }),
135
146
  },
136
147
  ...(usage === undefined ? {} : { usage }),
137
148
  failed,
@@ -1,10 +1,9 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { CHILD_EXCLUDED_TOOLS, ROLE_TOOL_POLICY_FLAG } from "@henryqw/pi-subagent";
2
+ import { CHILD_EXCLUDED_TOOL_NAMES, ROLE_TOOL_POLICY_FLAG } from "@henryqw/pi-subagent";
3
3
 
4
- const childExcludedTools = new Set(CHILD_EXCLUDED_TOOLS.split(","));
4
+ const childExcludedTools: ReadonlySet<string> = new Set(CHILD_EXCLUDED_TOOL_NAMES);
5
5
 
6
- function configuredTools(value: unknown): string[] | undefined {
7
- if (value === undefined) return;
6
+ function configuredTools(value: unknown): string[] {
8
7
  if (typeof value !== "string") throw new Error(`${ROLE_TOOL_POLICY_FLAG} must be JSON tool names.`);
9
8
  let parsed: unknown;
10
9
  try {
@@ -25,10 +24,16 @@ export default function roleTools(pi: ExtensionAPI): void {
25
24
  });
26
25
  pi.on("session_start", () => {
27
26
  const selected = configuredTools(pi.getFlag(ROLE_TOOL_POLICY_FLAG));
28
- if (!selected) return;
29
- const extensionTools = pi.getAllTools()
27
+ const allTools = pi.getAllTools();
28
+ const registeredTools = new Set(allTools.map((tool) => tool.name));
29
+ const extensionTools = allTools
30
30
  .filter((tool) => !["builtin", "sdk", "inline"].includes(tool.sourceInfo.source))
31
31
  .map((tool) => tool.name);
32
32
  pi.setActiveTools([...new Set([...selected, ...extensionTools])].filter((name) => !childExcludedTools.has(name)));
33
+ const activeTools = new Set(pi.getActiveTools().filter((name) => registeredTools.has(name) && !childExcludedTools.has(name)));
34
+ const unavailable = selected.filter((name) => !activeTools.has(name));
35
+ if (unavailable.length) {
36
+ throw new Error(`Subagent requested unavailable tools: ${unavailable.join(", ")}. Check spelling and load the provider extension that registers them.`);
37
+ }
33
38
  });
34
39
  }
@@ -1,6 +1,6 @@
1
1
  import type { Usage } from "@earendil-works/pi-ai";
2
2
  import { type ExtensionAPI, type ExtensionContext, type Theme } from "@earendil-works/pi-coding-agent";
3
- import { type Component, truncateToWidth, type TUI, visibleWidth } from "@earendil-works/pi-tui";
3
+ import { type Component, type TUI, wrapTextWithAnsi } from "@earendil-works/pi-tui";
4
4
  import {
5
5
  availableTaskModels,
6
6
  type ThinkingLevel,
@@ -28,16 +28,21 @@ import {
28
28
  type WorktreePayload,
29
29
  } from "@henryqw/pi-subagent";
30
30
  import { DEFAULT_TIMEOUT_CONFIG, readSubagentConfig, type SubagentTimeoutConfig } from "./config.ts";
31
+ import { registerDelegateFlow } from "./delegate-flow.ts";
32
+ import { runDelegation } from "./delegation.ts";
31
33
  import {
32
34
  formatBackgroundWorkflowResult,
33
35
  formatWorkflowResult,
34
36
  formatWorkflowUpdate,
35
37
  WorkflowAbortedError,
36
38
  WorkflowFailureError,
39
+ displaySummary,
37
40
  type WorkflowTransportEntry,
41
+ type WorkflowTransportDetails,
38
42
  } from "./result-transport.ts";
39
43
  import {
40
44
  identifyWorkflowEntries,
45
+ MAX_WORKFLOW_ENTRIES,
41
46
  parseWorkflow,
42
47
  runForegroundWorkflow,
43
48
  WorkflowSchema,
@@ -49,7 +54,6 @@ import {
49
54
  const SUBAGENT_TASK = "pi-subagent/delegateTask";
50
55
  const WIDGET_KEY = "subagent-status";
51
56
  const WIDGET_INTERVAL_MS = 80;
52
- const TERMINAL_DISPLAY_MS = 1_000;
53
57
  const MAX_WIDGET_ROWS = 8;
54
58
  const DEFAULT_TIMEOUT_POLICY = {
55
59
  idleMs: DEFAULT_TIMEOUT_CONFIG.idleMinutes * 60_000,
@@ -68,17 +72,18 @@ export function resolveTimeoutPolicy(partial: SubagentTimeoutConfig | undefined)
68
72
  }
69
73
  type WidgetStatus = "working" | "success" | "failure" | "aborted";
70
74
  type WidgetItem = {
71
- roleRoute: string;
75
+ role: string;
76
+ model: string;
77
+ thinkingLevel: string;
72
78
  task: string;
73
79
  tokens: number;
74
80
  startedAt: number;
75
81
  status: WidgetStatus;
76
82
  finishedAt?: number;
77
- removeAt?: number;
78
83
  };
79
84
 
80
85
  function taskSummary(task: string): string {
81
- return task.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").trim().split(/\s+/).slice(0, 4).join(" ");
86
+ return Array.from(task.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ").trim().split(/\s+/).slice(0, 4).join(" ")).slice(0, 160).join("");
82
87
  }
83
88
 
84
89
  function formatTokens(tokens: number): string {
@@ -97,12 +102,51 @@ function statusGlyph(status: WidgetStatus, spinnerIndex: number, theme: Theme):
97
102
  }
98
103
  }
99
104
 
100
- function leftColumn(value: string, width: number): string {
101
- return truncateToWidth(value, width, "…", true);
105
+ function statusLabel(status: WidgetStatus): string {
106
+ switch (status) {
107
+ case "working": return "working";
108
+ case "success": return "complete";
109
+ case "failure": return "failed";
110
+ case "aborted": return "stopped";
111
+ }
112
+ }
113
+
114
+ function isWorkflowTransportDetails(value: unknown): value is WorkflowTransportDetails {
115
+ const isRecord = (candidate: unknown): candidate is Record<string, unknown> => typeof candidate === "object" && candidate !== null && !Array.isArray(candidate);
116
+ const isOptionalString = (candidate: unknown) => candidate === undefined || typeof candidate === "string";
117
+ if (!isRecord(value) || !(value.mode === "single" || value.mode === "parallel" || value.mode === "chain") || !Array.isArray(value.entries)) return false;
118
+ const entries = value.entries;
119
+ return entries.length >= 1 && entries.length <= MAX_WORKFLOW_ENTRIES
120
+ && (value.mode !== "single" || entries.length === 1)
121
+ && entries.every((entry) => isRecord(entry)
122
+ && typeof entry.id === "string" && typeof entry.index === "number" && Number.isFinite(entry.index) && Number.isInteger(entry.index) && entry.index >= 0
123
+ && typeof entry.role === "string" && ["pending", "running", "succeeded", "failed", "rejected", "skipped"].includes(entry.status as string)
124
+ && (["running", "succeeded", "failed", "rejected"].includes(entry.status as string)
125
+ ? typeof entry.summary === "string" && entry.summary === displaySummary(entry.summary)
126
+ : entry.summary === undefined)
127
+ && isOptionalString(entry.model) && isOptionalString(entry.thinkingLevel)
128
+ && (entry.worktree === undefined || isRecord(entry.worktree)
129
+ && typeof entry.worktree.path === "string" && typeof entry.worktree.branch === "string"
130
+ && typeof entry.worktree.commits === "number" && Number.isFinite(entry.worktree.commits) && Number.isInteger(entry.worktree.commits) && entry.worktree.commits >= 0
131
+ && typeof entry.worktree.dirty === "boolean" && typeof entry.worktree.pruned === "boolean"
132
+ && (entry.worktree.inspection_failed === undefined || typeof entry.worktree.inspection_failed === "boolean")
133
+ && (entry.worktree.note === undefined || typeof entry.worktree.note === "string")))
134
+ && entries.every((entry, index) => index === 0 || (entry as { index: number }).index > (entries[index - 1] as { index: number }).index);
102
135
  }
103
136
 
104
- function rightColumn(value: string, width: number): string {
105
- return " ".repeat(Math.max(0, width - visibleWidth(value))) + value;
137
+ function renderWorkflowResult(details: WorkflowTransportDetails, width: number, theme: Theme): string[] {
138
+ if (details.entries.some(({ status }) => status === "pending" || status === "running")) return [];
139
+ return details.entries.flatMap((entry) => {
140
+ if (entry.status === "skipped") return [];
141
+ const summary = entry.summary || "(no output)";
142
+ const text = details.mode === "single" ? summary : `${entry.role}: ${summary}`;
143
+ const style = entry.status === "failed" || entry.status === "rejected" ? "error" : "text";
144
+ const recovery = entry.worktree && !entry.worktree.pruned ? `Recovery: ${entry.worktree.path}` : undefined;
145
+ return [
146
+ ...wrapTextWithAnsi(theme.fg(style, text), Math.max(1, width)),
147
+ ...(recovery ? wrapTextWithAnsi(theme.fg("warning", recovery), Math.max(1, width)) : []),
148
+ ];
149
+ });
106
150
  }
107
151
 
108
152
  function renderWidgetRows(
@@ -114,30 +158,14 @@ function renderWidgetRows(
114
158
  ): string[] {
115
159
  const visible = items.slice(0, MAX_WIDGET_ROWS);
116
160
  if (!visible.length) return [];
117
- const tokens = visible.map((item) => formatTokens(item.tokens));
118
- const elapsed = visible.map((item) => formatDuration((item.finishedAt ?? now) - item.startedAt));
119
- const tokenWidth = Math.max(...tokens.map(visibleWidth));
120
- const elapsedWidth = Math.max(...elapsed.map(visibleWidth));
121
- const fixedWidth = 1 + 8 + tokenWidth + elapsedWidth;
122
- if (width < fixedWidth) {
123
- return visible.map((item, index) => truncateToWidth(
124
- `${statusGlyph(item.status, spinnerIndex, theme)} ${tokens[index]} ${elapsed[index]}`,
125
- width,
126
- "",
127
- ));
128
- }
129
- const contentWidth = width - fixedWidth;
130
- const naturalRoleWidth = Math.min(32, Math.max(...visible.map((item) => visibleWidth(item.roleRoute))));
131
- const roleWidth = Math.min(naturalRoleWidth, contentWidth);
132
- const taskWidth = contentWidth - roleWidth;
133
- const lines = visible.map((item, index) => [
134
- statusGlyph(item.status, spinnerIndex, theme),
135
- theme.fg("accent", leftColumn(item.roleRoute, roleWidth)),
136
- theme.fg("text", leftColumn(item.task, taskWidth)),
137
- theme.fg("muted", rightColumn(tokens[index]!, tokenWidth)),
138
- theme.fg("dim", rightColumn(elapsed[index]!, elapsedWidth)),
139
- ].join(" "));
140
- if (items.length > visible.length) lines.push(theme.fg("muted", `… ${items.length - visible.length} more`));
161
+ const indent = " ".repeat(Math.min(2, Math.max(0, width - 1)));
162
+ const contentWidth = Math.max(1, width - indent.length);
163
+ const lines = visible.flatMap((item) => [
164
+ ...wrapTextWithAnsi(`${statusGlyph(item.status, spinnerIndex, theme)} ${theme.fg("accent", item.role)} · ${statusLabel(item.status)}`, Math.max(1, width)),
165
+ ...wrapTextWithAnsi(theme.fg("text", item.task), contentWidth).map((line) => `${indent}${line}`),
166
+ ...wrapTextWithAnsi(theme.fg("muted", `${item.model} · ${item.thinkingLevel} · ${formatTokens(item.tokens)} tok · ${formatDuration((item.finishedAt ?? now) - item.startedAt)}`), contentWidth).map((line) => `${indent}${line}`),
167
+ ]);
168
+ if (items.length > visible.length) lines.push(...wrapTextWithAnsi(theme.fg("muted", `… ${items.length - visible.length} more`), Math.max(1, width)));
141
169
  return lines;
142
170
  }
143
171
 
@@ -224,6 +252,7 @@ export default function subagentExtension(
224
252
  // Bumped by session_start and session_shutdown; background tasks may only
225
253
  // deliver into the exact session that launched them.
226
254
  let sessionEpoch = 0;
255
+ let invalidateDelegateFlow = () => {};
227
256
  let widgetInstalled = false;
228
257
  let widgetTimer: ReturnType<typeof setInterval> | undefined;
229
258
  let spinnerIndex = 0;
@@ -240,12 +269,7 @@ export default function subagentExtension(
240
269
  if (widgetTimer) return;
241
270
  widgetTimer = setInterval(() => {
242
271
  spinnerIndex = (spinnerIndex + 1) % SPINNER_FRAMES.length;
243
- const now = Date.now();
244
- for (const [id, item] of widgetItems) {
245
- if (item.removeAt !== undefined && item.removeAt <= now) widgetItems.delete(id);
246
- }
247
272
  requestWidgetRender();
248
- if (!widgetItems.size) stopWidgetTimer();
249
273
  }, WIDGET_INTERVAL_MS);
250
274
  widgetTimer.unref();
251
275
  };
@@ -272,8 +296,17 @@ export default function subagentExtension(
272
296
  ) => {
273
297
  if (!ctx.hasUI) return;
274
298
  ensureWidget(ctx);
299
+ if (!widgetItems.has(id) && widgetItems.size >= MAX_WIDGET_ROWS) {
300
+ for (const [oldestId, item] of widgetItems) {
301
+ if (item.status === "working") continue;
302
+ widgetItems.delete(oldestId);
303
+ if (widgetItems.size < MAX_WIDGET_ROWS) break;
304
+ }
305
+ }
275
306
  widgetItems.set(id, {
276
- roleRoute: `${role}[${model}:${thinkingLevel ?? "default"}]`,
307
+ role,
308
+ model,
309
+ thinkingLevel: thinkingLevel ?? "default",
277
310
  task: taskSummary(task),
278
311
  tokens: 0,
279
312
  startedAt: Date.now(),
@@ -295,13 +328,13 @@ export default function subagentExtension(
295
328
  if (!item) return;
296
329
  item.status = status;
297
330
  item.finishedAt = Date.now();
298
- item.removeAt = item.finishedAt + TERMINAL_DISPLAY_MS;
299
- startWidgetTimer();
331
+ if (![...widgetItems.values()].some(({ status }) => status === "working")) stopWidgetTimer();
300
332
  requestWidgetRender();
301
333
  };
302
334
 
303
335
  pi.on("session_start", (_event, ctx) => {
304
336
  sessionEpoch += 1;
337
+ invalidateDelegateFlow();
305
338
  latestCtx = ctx;
306
339
  ensureWidget(ctx);
307
340
  for (const warning of startupWarnings.splice(0)) ctx.ui.notify(warning, "warning");
@@ -316,6 +349,7 @@ export default function subagentExtension(
316
349
  // Invalidate ordinary outcomes, abort children, then let preserved isolated
317
350
  // work report into the outgoing session before Pi tears it down.
318
351
  sessionEpoch += 1;
352
+ invalidateDelegateFlow();
319
353
  const tasks = [...backgroundTasks.values()];
320
354
  for (const { controller } of tasks) controller.abort();
321
355
  await Promise.allSettled(tasks.map(({ settled }) => settled));
@@ -323,6 +357,13 @@ export default function subagentExtension(
323
357
  });
324
358
  // btw-style context refresh: model_select carries the new model on the event,
325
359
  // agent_settled delivers the freshest full context after each turn.
360
+ pi.on("input", (event) => {
361
+ if (event.source === "extension") return;
362
+ for (const [id, item] of widgetItems) {
363
+ if (item.status !== "working") widgetItems.delete(id);
364
+ }
365
+ requestWidgetRender();
366
+ });
326
367
  pi.on("model_select", (event, ctx) => {
327
368
  latestCtx = { ...ctx, model: event.model } as ExtensionContext;
328
369
  });
@@ -397,6 +438,23 @@ export default function subagentExtension(
397
438
  }
398
439
  };
399
440
 
441
+ invalidateDelegateFlow = registerDelegateFlow(pi, {
442
+ executor,
443
+ maxRuntimeMs: timeoutPolicy.maxMs,
444
+ getSessionGeneration: () => sessionEpoch,
445
+ loadRoles,
446
+ resolveLaunch: (role, ctx) => {
447
+ const launchCtx = latestCtx ?? ctx;
448
+ return createRoleLaunch(pi, launchCtx, {
449
+ role,
450
+ route: resolveConfiguredTaskRoute(launchCtx, SUBAGENT_TASK),
451
+ });
452
+ },
453
+ startWidget: startWidgetItem,
454
+ updateWidgetTokens,
455
+ finishWidget: finishWidgetItem,
456
+ });
457
+
400
458
  pi.registerTool({
401
459
  name: "delegate_task",
402
460
  label: "Subagent",
@@ -410,6 +468,20 @@ export default function subagentExtension(
410
468
  "delegate_task background applies to the whole selected workflow and returns before results exist; use it only when the user explicitly asks for non-blocking work.",
411
469
  ],
412
470
  parameters: WorkflowSchema,
471
+ renderResult(result, _options, theme, _context) {
472
+ const details = result.details;
473
+ if (isWorkflowTransportDetails(details)) {
474
+ return {
475
+ invalidate() {},
476
+ render: (width) => renderWorkflowResult(details, width, theme),
477
+ };
478
+ }
479
+ if (typeof details === "object" && details !== null && (details as { background?: unknown }).background === true) {
480
+ return { invalidate() {}, render: (width) => wrapTextWithAnsi(theme.fg("muted", "Background workflow accepted."), Math.max(1, width)) };
481
+ }
482
+ const text = result.content.find((part) => part.type === "text")?.text ?? "(no output)";
483
+ return { invalidate() {}, render: (width) => wrapTextWithAnsi(theme.fg("muted", text), Math.max(1, width)) };
484
+ },
413
485
  prepareArguments(args) {
414
486
  try {
415
487
  const workflow = parseWorkflow(args);
@@ -512,7 +584,7 @@ export default function subagentExtension(
512
584
  : { ...base, status: nextStatus, assistantOutput: nextText });
513
585
  };
514
586
  try {
515
- child = await executor.run({
587
+ child = await runDelegation(executor, {
516
588
  signal: workflowSignal,
517
589
  onUpdate: (output) => {
518
590
  setState("running", output);
@@ -586,7 +658,6 @@ export default function subagentExtension(
586
658
  setState(status, text);
587
659
  try {
588
660
  finishWidgetItem(entry.id, aborted ? "aborted" : status === "succeeded" ? "success" : "failure");
589
- emitUpdate(emitToolUpdates);
590
661
  } catch (error) {
591
662
  rejected = error;
592
663
  status = "rejected";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@henryqw/pi-subagent",
3
- "version": "4.1.1",
3
+ "version": "5.0.0",
4
4
  "description": "Delegate bounded single, parallel, or chained tasks to isolated Pi roles.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -1,54 +1,32 @@
1
1
  ---
2
2
  name: pi-subagent-delegated-development
3
- description: Orchestrate delegated development with pi-subagent delegate_task. Use when splitting implementation work into bounded units for isolated implementer children reviewed by a read-only reviewer before merging.
3
+ description: Run bounded independent implementation through the runtime-managed Delegate Flow.
4
4
  ---
5
5
 
6
6
  # Delegated Development
7
7
 
8
- You are Main. Decompose work into bounded units and coordinate; never edit files yourself.
8
+ You are Main: slice work and call `delegate_flow`. Do not implement child work yourself or use external model tools, push, publish, or release.
9
9
 
10
- All model and agent work in this workflow goes through Pi's `delegate_task` and Pi-managed children. Do not invoke external LLM APIs, SDKs, agent harnesses, or model CLIs; ordinary deterministic developer tools such as `git`, npm, test runners, and compilers remain allowed.
10
+ ## Slice
11
11
 
12
- ## Required preflight
12
+ Use the fewest cohesive units. `delegate_flow` is for independent units expected to commute; combine or sequence work that overlaps files, APIs, schemas, generated output, package metadata, lockfiles, or invariants. Dependent work remains outside Flow, in one task or ordinary caller-controlled sequencing.
13
13
 
14
- Before any delegation, require that Main's cwd is a Git working tree with a committed `HEAD`:
14
+ Give every unit a bounded objective, owned scope and exclusions, acceptance criteria, and its direct validation command/argument array. Do not pass the parent request unchanged. Call `delegate_flow` with 1–8 units; the runtime supplies the effective Implementer and Reviewer Roles.
15
15
 
16
- ```bash
17
- test "$(git rev-parse --is-inside-work-tree)" = true &&
18
- git rev-parse --verify -q HEAD^{commit} >/dev/null
19
- ```
20
-
21
- If this fails, stop before delegation and report that this Skill requires a Git repository with a committed `HEAD`; do not rely on the generic worktree fallback to Main's cwd.
22
-
23
- Before recording each unit's `base=$(git rev-parse HEAD)`, and again immediately before integration and validation, require `git status --porcelain=v1 --untracked-files=all` to be empty. If Git cannot inspect status or any tracked or untracked change is present, stop and ask the user to preserve the changes; never stash, discard, hide, or work around them.
24
-
25
- ## Roles
26
-
27
- The package-shipped built-in Roles `implementer` (`isolation: worktree`) and `reviewer` (read-only patch-and-file review) are always available; no installation step is required. A same-name user override replaces the built-in entirely. Before using this workflow, Main must verify that each override preserves implementer worktree isolation and reviewer read-only patch-and-file constraints; fail clearly if it does not. Do not invent substitute Roles.
16
+ ## Runtime Flow
28
17
 
29
- ## Per-unit loop
18
+ The runtime owns the unit worktrees and all Git identity, rebasing, declared validation, exact read-only review, fast-forward integration, and cleanup. It records the clean committed Main state, runs independent Implementers in isolated unit worktrees, processes units in declared order, and integrates only the exact reviewed tip OID.
30
19
 
31
- For each bounded unit:
20
+ Trust the structured Flow outcome. Never edit a child worktree, manage its branches, prepare review evidence, reimplement Flow, or manually integrate its changes. Do not repeat Flow validation after it has completed or integrated a unit.
32
21
 
33
- 1. **Implement** one `delegate_task` single call to `implementer`. The packet states the objective, touched scope, required validation, and recorded base commit. Require its output to identify the retained worktree path, branch, base commit, tip commit, changed files from the base-to-tip committed diff, and clean `git status --porcelain=v1 --untracked-files=all` result.
34
- 2. **Verify and review** — refuse review or merge unless Main independently verifies that the worktree was retained and is clean (including untracked files), the reported base/branch/tip identities are complete and match Git, the tip is descended from the recorded base, and the reported changed files equal `git diff --name-only "$base" "$tip"`. All intended changes must be in that committed base-to-tip diff. If any evidence is missing or any check fails, send the work to a fresh implementer repair; never review dirty or uncommitted work.
22
+ A successful Flow owns integration and cleanup. A blocked outcome is repairable: provide one explicit continuation and no more:
35
23
 
36
- Create a private temporary exact-patch artifact outside the repository from `git diff --no-textconv --no-ext-diff --ignore-submodules=none --binary "$base" "$tip"`. Independently regenerate that same diff and byte-compare it with the artifact, then record its path, byte count, and SHA-256. If creation, regeneration, comparison, byte count, or checksum fails, stop and report the artifact failure clearly; do not review or merge. Do not put any complete patch content in `delegate_task` text or argv.
37
-
38
- Make one `delegate_task` single call to `reviewer` with only a bounded metadata packet: base, tip, review context `{type:'child_branch', branch}`, the verified complete patch file reference (`path`, `bytes`, `sha256`), and the verified changed paths. If that complete metadata cannot fit the task transport bound, stop and report it rather than truncating or inlining patch content. Chain entries do not share files: `{previous}` passes text only.
39
- 3. **Merge** — only after an approving review, re-check Main's clean status, verify the branch tip still equals the reviewed tip commit, then merge that exact commit (not the branch name) into Main's current worktree and run focused validation there. Never merge on unresolved findings.
40
- 4. **Clean up after success** — only after the exact reviewed tip is integrated and focused validation passes, remove each reported retained worktree, then safely delete its task branch. Include a superseded repair-round worktree only when its exact tip is an ancestor of integrated `HEAD`. For each candidate, verify ancestry first, use non-forced worktree removal followed by `git branch -d`, and stop/report cleanup failure without deleting later evidence. On any integration or validation failure, preserve every temporary patch artifact, retained worktree, and task branch for recovery. After integration and validation succeed, remove the temporary reviewer patch artifacts.
41
-
42
- ## Findings
43
-
44
- Any reviewer finding goes back as a **fresh** `implementer` delegation containing the findings plus the reviewed base/branch/tip identities and complete exact patch file reference, followed by a fresh review of the new state. A fresh repair implementer starts in a new worktree from Main HEAD and does not contain the prior unit commit: the repair packet must first bring the whole reviewed `$base..$tip` range into its fresh worktree by merging the exact `$tip`, or cherry-picking every range commit in order. Cherry-pick `$tip` alone only after `git rev-list --count "$base..$tip"` verifies the range is exactly one commit; then address the findings. Dirty or uncommitted work also goes only to this fresh repair path. Bound the loop (e.g. three rounds); past the bound, stop and report to the user instead of merging.
45
-
46
- ## Parallelism
24
+ ```ts
25
+ delegate_flow_continue({ guidance: "Address the reported block and complete the bounded unit." })
26
+ ```
47
27
 
48
- Independent units may run concurrently via one `delegate_task` parallel call (max 8 entries) or concurrent single calls, each still following its own implement review cycle. Never parallelize a unit's review ahead of its implementation. Each child gets its own deterministic worktree; they never share files implicitly.
28
+ Make the guidance specific to the reported implementation, validation, or review failure. Do not call continuation unless Flow reports a repairable block. If continuation or Flow returns a terminal failure, inspect every retained path reported by the runtime, then reslice or manually recover from Main; do not retry the Flow or guess a rebase resolution. A cleanup warning does not undo successful integration.
49
29
 
50
- ## Boundaries
30
+ ## Ordinary delegation
51
31
 
52
- - Never bypass review, edit inside a child's worktree, or re-implement a child's work yourself.
53
- - On child failure, recover from the reported preserved-worktree evidence; retry at most once per unit before escalating to the user.
54
- - Do not push, publish, release, or open PRs without explicit user authorization.
32
+ Use `delegate_task` for a single bounded task, independent parallel tasks, or dependent chain work that is not a Flow. Give each entry its objective, exact scope and exclusions, relevant context and constraints, expected deliverable, and focused validation. Preserve ordinary Role selection and isolation behavior. Keep integration and cross-cutting decisions in Main, and use the minimum number of Subagents needed.