pi-cohort 7.2.3 → 7.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [7.3.0] - 2026-10-18
4
+
5
+ ### Added
6
+
7
+ - Chain steps accept an optional `stopOn: { path, in }` condition over schema-validated structured output. A match ends the chain after that step as a controlled stop (`isError: false`) with a fixed `## Stop` JSON record naming every matching task and a `## Outputs` section carrying every finished task's text and structured output; a sibling failure in the same step keeps the failed status but still attaches the record and the finished outputs. Accepted on call-time `chain:` items, `.chain.json`, management `config.steps[]`, and `.chain.md` (`stopOn: {"path":"/verdict","in":["FAIL"]}`); rejected before any child starts when the step has no `outputSchema`. Background runs persist `stop` in the result file and the completion notice renders it. Worktree parallel tasks expose their patch to later steps as `{outputs.name.patchPath}`. Chains without `stopOn` are unchanged. (#23)
8
+
3
9
  ## [7.2.3] - 2026-10-18
4
10
 
5
11
  ### Fixed
package/README.md CHANGED
@@ -117,6 +117,35 @@ Ask oracle for a second opinion on my current plan.
117
117
  Run parallel reviewers: one for correctness, one for tests, and one for unnecessary complexity.
118
118
  ```
119
119
 
120
+ Run implementers and spec reviewers in the first call, stopping the chain on `FAIL`:
121
+
122
+ ```ts
123
+ subagent({ async: false, chain: [
124
+ { parallel: [
125
+ { agent: "implementer", as: "impl_1", task: "Implement task 1", outputSchema: statusSchema },
126
+ { agent: "implementer", as: "impl_2", task: "Implement task 2", outputSchema: statusSchema },
127
+ ], worktree: true, stopOn: { path: "/status", in: ["NEEDS_CONTEXT", "BLOCKED"] } },
128
+ { parallel: [
129
+ { agent: "spec-reviewer", as: "sr_1", task: "Review {outputs.impl_1.patchPath}", outputSchema: verdictSchema },
130
+ { agent: "spec-reviewer", as: "sr_2", task: "Review {outputs.impl_2.patchPath}", outputSchema: verdictSchema },
131
+ ], stopOn: { path: "/verdict", in: ["FAIL"] } },
132
+ ]})
133
+ ```
134
+
135
+ The parent applies the accepted patches, runs its tests, and issues a second call for code review over the integrated diff:
136
+
137
+ ```ts
138
+ subagent({ async: false, agent: "code-reviewer", task: "Review the integrated diff" })
139
+ ```
140
+
141
+ A match ends the chain after that step as a controlled stop (not an error): the result carries a `## Stop` JSON record naming every matching task and a `## Outputs` section with every finished task's output. A chain without `stopOn` behaves exactly as before.
142
+
143
+ In a saved `.chain.md` the condition is one line per step, the object as JSON:
144
+
145
+ ```md
146
+ stopOn: {"path":"/verdict","in":["FAIL"]}
147
+ ```
148
+
120
149
  That's the whole surface for day-to-day use. More phrasing patterns: [doc/commands.md](doc/commands.md#prompt-cookbook-appendix).
121
150
 
122
151
  Review and delivery workflows live in pi-gauntlet; pi-cohort ships delegation primitives plus `/investigate` and `/skill:handoff [--out <path> | --key <stem>]` (the brief lands in `<tmpdir>/pi-handoff/<primary>--<leaf>.md` by default; contract in [doc/handoff-template.md](doc/handoff-template.md)).
@@ -142,7 +171,7 @@ Full reference: agent/chain authoring in [doc/agents-and-chains.md](doc/agents-a
142
171
  |---|---|
143
172
  | Subagent | A focused child Pi session with one job and (by default) a fresh context. |
144
173
  | Agent (persona) | A markdown file with frontmatter defining a specialist: `scout`, `planner`, `worker`, `reviewer`, `context-builder`, `oracle`, `delegate`, `monitor`. Full table: [doc/agents-and-chains.md](doc/agents-and-chains.md#builtin-agents-in-plain-english). |
145
- | Chain | A saved or inline sequence of agent steps, with fan-out/fan-in support. |
174
+ | Chain | A saved or inline sequence of agent steps, with fan-out/fan-in support and an optional per-step `stopOn` condition that ends the chain when a child's structured output matches (for example a reviewer's `FAIL` verdict). Field reference: [doc/agents-and-chains.md](doc/agents-and-chains.md#stop-conditions-stopon). |
146
175
  | Fresh vs. forked context | Fresh = clean slate; forked = a real branch of the parent's session history. |
147
176
  | Recursion guard | Depth cap on nested delegation so a child can only fan out if explicitly allowed. |
148
177
  | `Σ$` / `cost:external` | The session-wide cost rollup, and the protocol other extensions use to report into it. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cohort",
3
- "version": "7.2.3",
3
+ "version": "7.3.0",
4
4
  "description": "Delegate Pi work to focused child agents: code review, scouting, implementation, parallel audits, saved chains, and background jobs.",
5
5
  "author": "Jacek Juraszek",
6
6
  "license": "MIT",
@@ -65,6 +65,7 @@ A child saying "done" is evidence, not verification. `reviewed` means a reviewer
65
65
  | `cwd` | child working directory; pass the worktree path or the child inherits pi's launch dir |
66
66
  | `acceptance` | per-task override, same shape as top-level |
67
67
  | `as`, `outputSchema` | the child must call `structured_output` with schema-valid JSON or the step fails; later steps read `{outputs.<as>}` |
68
+ | `stopOn` | Use step-level `{ path, in }` to end the chain after this step when any task's structured output at the JSON Pointer `path` equals a member of `in`; supply `outputSchema` on every task of the step |
68
69
 
69
70
  ## Control actions
70
71
 
@@ -98,6 +99,15 @@ subagent({ async: false, chain: [
98
99
  { agent: "worker", task: "Apply accepted fixes from {previous}" }
99
100
  ]})
100
101
 
102
+ // Run implementers and spec reviewers in the first call (stop on FAIL).
103
+ subagent({ async: false, chain: [
104
+ { parallel: [ { agent: "implementer", as: "impl_1", task: "...", outputSchema: statusSchema } ], worktree: true, stopOn: { path: "/status", in: ["NEEDS_CONTEXT", "BLOCKED"] } },
105
+ { parallel: [ { agent: "spec-reviewer", as: "sr_1", task: "Review {outputs.impl_1.patchPath}", outputSchema: verdictSchema } ], stopOn: { path: "/verdict", in: ["FAIL"] } },
106
+ ]})
107
+ // Read a controlled match (isError false) in the `## Stop` JSON record and every finished output under `## Outputs`.
108
+ // Apply accepted patches in the parent, run tests, then issue a second call for code review over the integrated diff.
109
+ subagent({ async: false, agent: "code-reviewer", task: "Review the integrated diff" })
110
+
101
111
  // background job, then control (pair long jobs with a monitor - see Control actions)
102
112
  subagent({ agent: "worker", async: true, task: "<long job - emit progress lines as you work>" })
103
113
  subagent({ action: "status" })
@@ -53,6 +53,7 @@ Per step:
53
53
  | `model` | string | Model override. |
54
54
  | `skills` | string[] \| false | Skills to inject into this step's agent (management config, plural); the execution-time `chain[].skill` param is singular. |
55
55
  | `progress` | boolean | progress.md tracking. |
56
+ | `stopOn` | `{ path: string, in: string[] }` | End the chain after this step when the structured output at JSON Pointer `path` equals a member of `in`; supply `outputSchema`. Write to `.chain.md` as `stopOn: {"path":"/verdict","in":["FAIL"]}`. |
56
57
 
57
58
  ### Call-time `control` overrides
58
59
 
@@ -20,7 +20,8 @@ import {
20
20
  import { serializeAgent } from "./agent-serializer.ts";
21
21
  import { serializeChain, serializeJsonChain } from "./chain-serializer.ts";
22
22
  import { discoverAvailableSkills } from "./skills.ts";
23
- import type { Details } from "../shared/types.ts";
23
+ import type { Details, StopCondition } from "../shared/types.ts";
24
+ import { stopConditionShapeError, validateStopConditions } from "../runs/shared/stop-conditions.ts";
24
25
 
25
26
  type ManagementAction = "list" | "get" | "create" | "update" | "delete";
26
27
  type ManagementScope = "user" | "project";
@@ -213,8 +214,15 @@ function parseStepList(raw: unknown): { steps?: ChainStepConfig[]; error?: strin
213
214
  if (typeof s.progress === "boolean") step.progress = s.progress;
214
215
  else return { error: `config.steps[${i}].progress must be a boolean.` };
215
216
  }
217
+ if (hasKey(s, "stopOn")) {
218
+ const shapeError = stopConditionShapeError(s.stopOn, `config.steps[${i}].stopOn`);
219
+ if (shapeError) return { error: shapeError };
220
+ step.stopOn = s.stopOn as StopCondition;
221
+ }
216
222
  steps.push(step);
217
223
  }
224
+ const stopError = validateStopConditions(steps);
225
+ if (stopError) return { error: stopError };
218
226
  return { steps };
219
227
  }
220
228
 
@@ -6,7 +6,7 @@ import * as fs from "node:fs";
6
6
  import * as os from "node:os";
7
7
  import * as path from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
- import type { AcceptanceInput, OutputMode } from "../shared/types.ts";
9
+ import type { AcceptanceInput, OutputMode, StopCondition } from "../shared/types.ts";
10
10
  import { getAgentDir } from "../shared/utils.ts";
11
11
  import { KNOWN_FIELDS } from "./agent-serializer.ts";
12
12
  import { parseChain, parseJsonChain } from "./chain-serializer.ts";
@@ -142,6 +142,7 @@ export interface ChainStepConfig {
142
142
  failFast?: boolean;
143
143
  worktree?: boolean;
144
144
  acceptance?: AcceptanceInput;
145
+ stopOn?: StopCondition;
145
146
  }
146
147
 
147
148
  export interface ChainConfig {
@@ -4,6 +4,8 @@ import { parseFrontmatter } from "./frontmatter.ts";
4
4
  import { ChainOutputValidationError, validateChainOutputBindings } from "../runs/shared/chain-outputs.ts";
5
5
  import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
6
6
  import type { ChainStep } from "../shared/settings.ts";
7
+ import { stopConditionShapeError } from "../runs/shared/stop-conditions.ts";
8
+ import type { StopCondition } from "../shared/types.ts";
7
9
 
8
10
  function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
9
11
  const lines = sectionBody.split("\n");
@@ -78,6 +80,17 @@ function parseStepBody(agent: string, sectionBody: string): ChainStepConfig {
78
80
  if (rawValue === "true") step.progress = true;
79
81
  else if (rawValue === "false") step.progress = false;
80
82
  }
83
+ if (key === "stopon") {
84
+ const invalid = `stopOn line '${line.trim()}' must be a JSON object {"path": "<json-pointer>", "in": ["<value>", ...]}.`;
85
+ let parsed: unknown;
86
+ try {
87
+ parsed = JSON.parse(rawValue);
88
+ } catch {
89
+ throw new Error(invalid);
90
+ }
91
+ if (stopConditionShapeError(parsed, "stopOn")) throw new Error(invalid);
92
+ step.stopOn = parsed as StopCondition;
93
+ }
81
94
  }
82
95
 
83
96
  return step;
@@ -242,6 +255,7 @@ export function serializeChain(config: ChainConfig): string {
242
255
  if (step.skills === false) lines.push("skills: false");
243
256
  else if (Array.isArray(step.skills) && step.skills.length > 0) lines.push(`skills: ${step.skills.join(", ")}`);
244
257
  if (step.progress !== undefined) lines.push(`progress: ${step.progress ? "true" : "false"}`);
258
+ if (step.stopOn) lines.push(`stopOn: ${JSON.stringify(step.stopOn)}`);
245
259
  lines.push("");
246
260
  lines.push(step.task ?? "");
247
261
  if (i < config.steps.length - 1) lines.push("");
@@ -233,6 +233,10 @@ const ChainItem = Type.Object({
233
233
  worktree: Type.Optional(Type.Boolean({
234
234
  description: "Isolated worktree per task."
235
235
  })),
236
+ stopOn: Type.Optional(Type.Object({
237
+ path: Type.String({ description: "JSON Pointer into the step's structured output." }),
238
+ in: Type.Array(Type.String(), { minItems: 1, description: "Stop the chain when the value equals any member." }),
239
+ }, { additionalProperties: false, description: "Stop after this step when any task's structured output matches; requires outputSchema." })),
236
240
  }, {
237
241
  description: "{agent,task?} seq; {parallel:[..]} concurrent; {expand,parallel,collect} fanout.",
238
242
  additionalProperties: false,
@@ -519,6 +519,7 @@ export function executeAsyncChain(
519
519
  async: true,
520
520
  dynamic: false,
521
521
  }),
522
+ ...(s.stopOn ? { stopOn: s.stopOn } : {}),
522
523
  ...(s.outputSchema ? { structuredOutputSchema: s.outputSchema } : {}),
523
524
  ...(s.outputSchema ? { structuredOutput: createStructuredOutputRuntime(s.outputSchema, path.join(asyncDir, "structured-output")) } : {}),
524
525
  };
@@ -566,6 +567,7 @@ export function executeAsyncChain(
566
567
  concurrency: s.concurrency,
567
568
  failFast: s.failFast,
568
569
  worktree: s.worktree,
570
+ stopOn: s.stopOn,
569
571
  };
570
572
  }
571
573
  if (isDynamicParallelStep(s)) {
@@ -584,6 +586,7 @@ export function executeAsyncChain(
584
586
  failFast: s.failFast,
585
587
  phase: s.phase,
586
588
  label: s.label,
589
+ stopOn: s.stopOn,
587
590
  effectiveAcceptance: resolveEffectiveAcceptance({
588
591
  explicit: s.acceptance,
589
592
  agentName: s.parallel.agent,
@@ -5,6 +5,8 @@
5
5
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
6
  import { buildCompletionKey, getGlobalSeenMap, markSeenWithTtl } from "./completion-dedupe.ts";
7
7
  import { SUBAGENT_ASYNC_COMPLETE_EVENT } from "../../shared/types.ts";
8
+ import { buildChainStopSection } from "../../shared/formatters.ts";
9
+ import type { StopRecord } from "../../shared/types.ts";
8
10
 
9
11
  interface ChainStepResult {
10
12
  agent: string;
@@ -14,7 +16,7 @@ interface ChainStepResult {
14
16
 
15
17
  export interface SubagentNotifyDetails {
16
18
  agent: string;
17
- status: "completed" | "failed" | "paused";
19
+ status: "completed" | "failed" | "paused" | "stopped";
18
20
  taskInfo?: string;
19
21
  resultPreview: string;
20
22
  durationMs?: number;
@@ -36,6 +38,7 @@ interface SubagentResult {
36
38
  gistUrl?: string;
37
39
  shareError?: string;
38
40
  results?: ChainStepResult[];
41
+ stop?: StopRecord;
39
42
  taskIndex?: number;
40
43
  totalTasks?: number;
41
44
  }
@@ -57,7 +60,7 @@ export default function registerSubagentNotify(pi: ExtensionAPI): () => void {
57
60
  || result.state === "paused"
58
61
  || summary.startsWith("Paused after interrupt.")
59
62
  );
60
- const status = paused ? "paused" : result.success ? "completed" : "failed";
63
+ const status = paused ? "paused" : result.success ? (result.stop ? "stopped" : "completed") : "failed";
61
64
 
62
65
  const taskInfo =
63
66
  result.taskIndex !== undefined && result.totalTasks !== undefined
@@ -73,10 +76,13 @@ export default function registerSubagentNotify(pi: ExtensionAPI): () => void {
73
76
  : undefined;
74
77
 
75
78
  const displaySummary = summary.trim() ? summary : "(no output)";
79
+ const stopBlock = result.stop ? buildChainStopSection(result.stop) : undefined;
76
80
  const content = [
77
81
  `Background task ${status}: **${agent}**${taskInfo}`,
78
82
  "",
79
83
  displaySummary,
84
+ stopBlock ? "" : undefined,
85
+ stopBlock,
80
86
  sessionLine ? "" : undefined,
81
87
  sessionLine,
82
88
  ]
@@ -15,6 +15,7 @@ import {
15
15
  type AsyncParallelGroupStatus,
16
16
  type AsyncStatus,
17
17
  type ChainOutputMap,
18
+ type StopRecord,
18
19
  type ModelAttempt,
19
20
  type NestedRouteInfo,
20
21
  type ResolvedControlConfig,
@@ -45,7 +46,8 @@ import {
45
46
  MAX_PARALLEL_CONCURRENCY,
46
47
  } from "../shared/parallel-utils.ts";
47
48
  import { buildPiArgs, cleanupTempDir, runDirEnv } from "../shared/pi-args.ts";
48
- import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
49
+ import { ChainOutputValidationError, outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
50
+ import { buildStopRecord, evaluateStopCondition } from "../shared/stop-conditions.ts";
49
51
  import { createStructuredOutputRuntime, readStructuredOutput } from "../shared/structured-output.ts";
50
52
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
51
53
  import { nestedSummaryFromAsyncStatus, writeNestedEvent } from "../shared/nested-events.ts";
@@ -74,6 +76,8 @@ import {
74
76
  formatWorktreeDiffSummary,
75
77
  formatWorktreeTaskCwdConflict,
76
78
  type WorktreeSetup,
79
+ type WorktreeDiff,
80
+ patchPathForDiff,
77
81
  } from "../shared/worktree.ts";
78
82
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
79
83
  import { writeInitialProgressFile } from "../../shared/settings.ts";
@@ -633,7 +637,12 @@ export async function runSingleStep(
633
637
  : undefined);
634
638
  const placeholderRegex = new RegExp(ctx.placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
635
639
  let task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
636
- task = resolveOutputReferences(task, ctx.outputs ?? {});
640
+ try {
641
+ task = resolveOutputReferences(task, ctx.outputs ?? {});
642
+ } catch (error) {
643
+ if (!(error instanceof ChainOutputValidationError)) throw error;
644
+ return { agent: step.agent, output: error.message, exitCode: 1, error: error.message, sessionFile: step.sessionFile };
645
+ }
637
646
  const taskForCompletionGuard = task;
638
647
  if (step.effectiveAcceptance) {
639
648
  const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance);
@@ -1203,16 +1212,23 @@ function prepareParallelTaskRun(
1203
1212
  };
1204
1213
  }
1205
1214
 
1206
- function appendParallelWorktreeSummary(
1207
- previousOutput: string,
1215
+ function captureParallelWorktreeDiffs(
1208
1216
  worktreeSetup: WorktreeSetup | undefined,
1209
1217
  asyncDir: string,
1210
1218
  stepIndex: number,
1211
1219
  group: Extract<RunnerStep, { parallel: SubagentStep[] }>,
1212
- ): string {
1213
- if (!worktreeSetup) return previousOutput;
1220
+ ): WorktreeDiff[] {
1221
+ if (!worktreeSetup) return [];
1214
1222
  const diffsDir = path.join(asyncDir, "worktree-diffs", `step-${stepIndex}`);
1215
- const diffs = diffWorktrees(worktreeSetup, group.parallel.map((task) => task.agent), diffsDir);
1223
+ return diffWorktrees(worktreeSetup, group.parallel.map((task) => task.agent), diffsDir);
1224
+ }
1225
+
1226
+ function patchInfoForDiff(diff: WorktreeDiff | undefined) {
1227
+ if (!diff) return undefined;
1228
+ return { patchPath: patchPathForDiff(diff), patchError: diff.captureError };
1229
+ }
1230
+
1231
+ function appendParallelWorktreeSummary(previousOutput: string, diffs: WorktreeDiff[]): string {
1216
1232
  const diffSummary = formatWorktreeDiffSummary(diffs);
1217
1233
  if (!diffSummary) return previousOutput;
1218
1234
  return `${previousOutput}\n\n${diffSummary}`;
@@ -1231,6 +1247,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1231
1247
  let previousOutput = "";
1232
1248
  const outputs: ChainOutputMap = {};
1233
1249
  const results: StepResult[] = [];
1250
+ let stopRecord: StopRecord | undefined;
1234
1251
  const overallStartTime = Date.now();
1235
1252
  const shareEnabled = config.share === true;
1236
1253
  const asyncDir = config.asyncDir;
@@ -1931,16 +1948,22 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1931
1948
  }
1932
1949
  const collection = collectDynamicResults(step as Parameters<typeof collectDynamicResults>[0], materialized.items, parallelResults);
1933
1950
  const failures = parallelResults.filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
1951
+ if (!interrupted && step.stopOn) {
1952
+ const matches = evaluateStopCondition(step.stopOn, stepIndex, parallelResults.map((r, i) => ({
1953
+ agent: r.agent, label: materialized.parallel[i]?.label, exitCode: r.exitCode, structuredOutput: r.structuredOutput,
1954
+ })));
1955
+ if (matches.length > 0) stopRecord = buildStopRecord(step.stopOn, stepIndex, matches);
1956
+ }
1957
+ outputs[step.collect.as] = {
1958
+ text: JSON.stringify(collection),
1959
+ structured: collection,
1960
+ agent: step.parallel.agent,
1961
+ stepIndex,
1962
+ };
1963
+ statusPayload.outputs = outputs;
1934
1964
  if (failures.length === 0) {
1935
1965
  try {
1936
1966
  validateDynamicCollection(step.collect.outputSchema, collection);
1937
- outputs[step.collect.as] = {
1938
- text: JSON.stringify(collection),
1939
- structured: collection,
1940
- agent: step.parallel.agent,
1941
- stepIndex,
1942
- };
1943
- statusPayload.outputs = outputs;
1944
1967
  const groupAcceptance = step.effectiveAcceptance
1945
1968
  ? await evaluateAcceptance({
1946
1969
  acceptance: step.effectiveAcceptance,
@@ -1993,7 +2016,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1993
2016
  if (failures.length > 0) markDynamicGraphGroup(stepIndex, "failed", failures[0]?.error ?? "Dynamic fanout child failed.");
1994
2017
  statusPayload.lastUpdate = Date.now();
1995
2018
  writeStatusPayload();
1996
- if (failures.length > 0 || statusPayload.error) break;
2019
+ if (stopRecord || failures.length > 0 || statusPayload.error) break;
1997
2020
  continue;
1998
2021
  }
1999
2022
 
@@ -2217,15 +2240,22 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2217
2240
  executionSurface: pr.executionSurface,
2218
2241
  });
2219
2242
  }
2243
+ const diffs = captureParallelWorktreeDiffs(worktreeSetup, asyncDir, stepIndex, group);
2220
2244
  for (let t = 0; t < group.parallel.length; t++) {
2221
2245
  const outputName = group.parallel[t]?.outputName;
2222
- if (outputName) outputs[outputName] = outputEntryFromAsyncResult({
2246
+ if (outputName && parallelResults[t]!.exitCode === 0) outputs[outputName] = outputEntryFromAsyncResult({
2223
2247
  agent: parallelResults[t]!.agent,
2224
2248
  output: parallelResults[t]!.output,
2225
2249
  structuredOutput: parallelResults[t]!.structuredOutput,
2226
- }, stepIndex);
2250
+ }, stepIndex, patchInfoForDiff(diffs[t]));
2227
2251
  }
2228
2252
  statusPayload.outputs = outputs;
2253
+ if (!interrupted && group.stopOn) {
2254
+ const matches = evaluateStopCondition(group.stopOn, stepIndex, parallelResults.map((r, i) => ({
2255
+ agent: r.agent, as: group.parallel[i]?.outputName, label: group.parallel[i]?.label, exitCode: r.exitCode, structuredOutput: r.structuredOutput,
2256
+ })));
2257
+ if (matches.length > 0) stopRecord = buildStopRecord(group.stopOn, stepIndex, matches);
2258
+ }
2229
2259
 
2230
2260
  previousOutput = aggregateParallelOutputs(
2231
2261
  parallelResults.map((r) => ({
@@ -2237,7 +2267,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2237
2267
  attemptedModels: r.attemptedModels,
2238
2268
  })),
2239
2269
  );
2240
- previousOutput = appendParallelWorktreeSummary(previousOutput, worktreeSetup, asyncDir, stepIndex, group);
2270
+ previousOutput = appendParallelWorktreeSummary(previousOutput, diffs);
2241
2271
 
2242
2272
  appendJsonl(eventsPath, JSON.stringify({
2243
2273
  type: "subagent.parallel.completed",
@@ -2247,7 +2277,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2247
2277
  success: parallelResults.every((r) => r.exitCode === 0 || r.exitCode === -1),
2248
2278
  }));
2249
2279
 
2250
- if (parallelResults.some((r) => r.exitCode !== 0 && r.exitCode !== -1)) {
2280
+ if (stopRecord || parallelResults.some((r) => r.exitCode !== 0 && r.exitCode !== -1)) {
2251
2281
  break;
2252
2282
  }
2253
2283
  } finally {
@@ -2382,8 +2412,13 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2382
2412
  }
2383
2413
 
2384
2414
  flatIndex++;
2385
- if (singleResult.exitCode !== 0) {
2386
- break;
2415
+ if (singleResult.exitCode !== 0) break;
2416
+ if (!interrupted && seqStep.stopOn) {
2417
+ const matches = evaluateStopCondition(seqStep.stopOn, stepIndex, [{ agent: singleResult.agent, as: seqStep.outputName, label: seqStep.label, exitCode: singleResult.exitCode, structuredOutput: singleResult.structuredOutput }], { sequential: true });
2418
+ if (matches.length > 0) {
2419
+ stopRecord = buildStopRecord(seqStep.stopOn, stepIndex, matches);
2420
+ break;
2421
+ }
2387
2422
  }
2388
2423
  }
2389
2424
  }
@@ -2509,6 +2544,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2509
2544
  executionSurface: r.executionSurface,
2510
2545
  })),
2511
2546
  outputs,
2547
+ ...(stopRecord ? { stop: stopRecord } : {}),
2512
2548
  workflowGraph: statusPayload.workflowGraph,
2513
2549
  exitCode: interrupted || results.every((r) => r.success) ? 0 : 1,
2514
2550
  timestamp: runEndedAt,