pi-cohort 7.2.3 → 7.3.1

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.
@@ -18,7 +18,7 @@ interface WorktreeInfo {
18
18
  syntheticPaths: string[];
19
19
  }
20
20
 
21
- interface WorktreeDiff {
21
+ export interface WorktreeDiff {
22
22
  index: number;
23
23
  agent: string;
24
24
  branch: string;
@@ -448,11 +448,10 @@ function captureWorktreeDiff(
448
448
  removeSyntheticPathsBeforeDiff(worktree);
449
449
  runGitChecked(worktree.path, ["add", "-A"]);
450
450
  const diffStat = runGitChecked(worktree.path, ["diff", "--cached", "--stat", setup.baseCommit]).trim();
451
- const patch = runGitChecked(worktree.path, ["diff", "--cached", setup.baseCommit]);
451
+ runGitChecked(worktree.path, ["diff", "--cached", "--binary", `--output=${path.resolve(patchPath)}`, setup.baseCommit]);
452
452
  const numstat = runGitChecked(worktree.path, ["diff", "--cached", "--numstat", setup.baseCommit]);
453
- fs.writeFileSync(patchPath, patch, "utf-8");
454
453
 
455
- if (!patch.trim()) {
454
+ if (fs.statSync(patchPath).size === 0) {
456
455
  return emptyDiff(worktree.index, agent, worktree.branch, patchPath);
457
456
  }
458
457
 
@@ -490,6 +489,12 @@ function hasWorktreeChanges(diff: WorktreeDiff): boolean {
490
489
  return diff.filesChanged > 0 || diff.insertions > 0 || diff.deletions > 0 || diff.diffStat.trim().length > 0;
491
490
  }
492
491
 
492
+ /** The patch a later chain step may reference: only a clean capture with real changes qualifies. */
493
+ export function patchPathForDiff(diff: WorktreeDiff): string | undefined {
494
+ if (diff.captureError) return undefined;
495
+ return hasWorktreeChanges(diff) ? diff.patchPath : undefined;
496
+ }
497
+
493
498
  export function createWorktrees(cwd: string, runId: string, count: number, options?: CreateWorktreesOptions): WorktreeSetup {
494
499
  const repo = resolveRepoState(cwd);
495
500
  const setupHook = resolveWorktreeSetupHook(repo.toplevel, options?.setupHook);
@@ -4,7 +4,8 @@
4
4
 
5
5
  import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
- import type { Usage, SingleResult } from "./types.ts";
7
+ import type { Usage, SingleResult, ChainTaskOutputEntry, StopRecord } from "./types.ts";
8
+ import { formatStopCondition } from "../runs/shared/stop-conditions.ts";
8
9
  import type { ChainStep } from "./settings.ts";
9
10
  import { isDynamicParallelStep, isParallelStep } from "./settings.ts";
10
11
  import { splitKnownThinkingSuffix, THINKING_LEVELS } from "./model-info.ts";
@@ -52,15 +53,62 @@ export function formatDuration(ms: number): string {
52
53
  return `${Math.floor(ms / 60000)}m${Math.floor((ms % 60000) / 1000)}s`;
53
54
  }
54
55
 
56
+ export interface ChainSummaryOptions {
57
+ stop?: StopRecord;
58
+ outputs?: ChainTaskOutputEntry[];
59
+ }
60
+
61
+ function renderTaskHeading(entry: ChainTaskOutputEntry): string {
62
+ const asPart = entry.as ? `, as ${entry.as}` : "";
63
+ const base = `#### Task ${entry.taskIndex + 1} (${entry.agent}${asPart})`;
64
+ if (entry.exitCode === -1) return `${base} - skipped (fail-fast)`;
65
+ if (entry.exitCode !== 0) return `${base} - failed`;
66
+ return base;
67
+ }
68
+
69
+ /** `## Outputs`, grouped by step in execution order; skipped tasks get a heading only, failed tasks their error line. */
70
+ export function buildChainOutputsSection(entries: ChainTaskOutputEntry[]): string {
71
+ const lines: string[] = ["## Outputs"];
72
+ let currentStep = -1;
73
+ for (const entry of entries) {
74
+ if (entry.stepIndex !== currentStep) {
75
+ currentStep = entry.stepIndex;
76
+ lines.push(`### Step ${currentStep + 1}`);
77
+ }
78
+ lines.push(renderTaskHeading(entry));
79
+ if (entry.exitCode === -1) continue;
80
+ if (entry.exitCode !== 0) {
81
+ lines.push(entry.error || "failed");
82
+ continue;
83
+ }
84
+ if (entry.text) lines.push(entry.text);
85
+ if (entry.structured !== undefined) lines.push("```json", JSON.stringify(entry.structured), "```");
86
+ }
87
+ return lines.join("\n");
88
+ }
89
+
90
+ export function buildChainStopSection(stop: StopRecord): string {
91
+ return `## Stop\n\`\`\`json\n${JSON.stringify(stop, null, 2)}\n\`\`\``;
92
+ }
93
+
94
+ function appendChainSections(text: string, options: ChainSummaryOptions | undefined): string {
95
+ if (!options) return text;
96
+ const sections: string[] = [];
97
+ if (options.stop) sections.push(buildChainStopSection(options.stop));
98
+ if (options.outputs) sections.push(buildChainOutputsSection(options.outputs));
99
+ return sections.length ? `${text}\n\n${sections.join("\n\n")}\n` : text;
100
+ }
101
+
55
102
  /**
56
- * Build a summary string for a completed/failed chain
103
+ * Build a summary string for a completed/failed/stopped chain
57
104
  */
58
105
  export function buildChainSummary(
59
106
  steps: ChainStep[],
60
107
  results: SingleResult[],
61
108
  chainDir: string,
62
- status: "completed" | "failed",
109
+ status: "completed" | "failed" | "stopped",
63
110
  failedStep?: { index: number; error: string },
111
+ options?: ChainSummaryOptions,
64
112
  ): string {
65
113
  const stepNames = steps
66
114
  .map((step) => (isParallelStep(step) ? `parallel[${step.parallel.length}]` : isDynamicParallelStep(step) ? `expand:${step.parallel.agent}` : step.agent))
@@ -77,20 +125,21 @@ export function buildChainSummary(
77
125
  }
78
126
  const skillsLine = allSkills.size > 0 ? `šŸ”§ Skills: ${[...allSkills].join(", ")}` : "";
79
127
 
80
- if (status === "completed") {
81
- const stepWord = results.length === 1 ? "step" : "steps";
82
- return `āœ… Chain completed: ${stepNames} (${results.length} ${stepWord}, ${durationStr})${skillsLine ? `\n${skillsLine}` : ""}
83
-
84
- šŸ“‹ Progress: ${hasProgress ? progressPath : "(none)"}
85
- šŸ“ Artifacts: ${chainDir}`;
86
- } else {
87
- const stepInfo = failedStep ? ` at step ${failedStep.index + 1}` : "";
88
- const errorInfo = failedStep?.error ? `: ${failedStep.error}` : "";
89
- return `āŒ Chain failed${stepInfo}${errorInfo}${skillsLine ? `\n${skillsLine}` : ""}
128
+ const footer = `\n\nšŸ“‹ Progress: ${hasProgress ? progressPath : "(none)"}\nšŸ“ Artifacts: ${chainDir}`;
90
129
 
91
- šŸ“‹ Progress: ${hasProgress ? progressPath : "(none)"}
92
- šŸ“ Artifacts: ${chainDir}`;
130
+ if (status === "stopped" && options?.stop) {
131
+ const header = `ā¹ Chain stopped at step ${options.stop.stepIndex + 1}/${steps.length} by condition ${formatStopCondition(options.stop.condition)}`;
132
+ return appendChainSections(`${header}${skillsLine ? `\n${skillsLine}` : ""}${footer}`, options);
133
+ }
134
+ if (status === "completed" || status === "stopped") {
135
+ const stepWord = results.length === 1 ? "step" : "steps";
136
+ const header = `āœ… Chain completed: ${stepNames} (${results.length} ${stepWord}, ${durationStr})${skillsLine ? `\n${skillsLine}` : ""}`;
137
+ return appendChainSections(`${header}${footer}`, options);
93
138
  }
139
+ const stepInfo = failedStep ? ` at step ${failedStep.index + 1}` : "";
140
+ const errorInfo = failedStep?.error ? `: ${failedStep.error}` : "";
141
+ const header = `āŒ Chain failed${stepInfo}${errorInfo}${skillsLine ? `\n${skillsLine}` : ""}`;
142
+ return appendChainSections(`${header}${footer}`, options);
94
143
  }
95
144
 
96
145
  /**
@@ -6,7 +6,7 @@ import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
7
  import type { AgentConfig } from "../agents/agents.ts";
8
8
  import { normalizeSkillInput } from "../agents/skills.ts";
9
- import { CHAIN_RUNS_DIR, type AcceptanceInput, type JsonSchemaObject, type OutputMode } from "./types.ts";
9
+ import { CHAIN_RUNS_DIR, type AcceptanceInput, type JsonSchemaObject, type OutputMode, type StopCondition } from "./types.ts";
10
10
  const CHAIN_DIR_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
11
11
  const INITIAL_PROGRESS_CONTENT = "# Progress\n\n## Status\nIn Progress\n\n## Tasks\n\n## Files Changed\n\n## Notes\n";
12
12
 
@@ -56,6 +56,7 @@ export interface SequentialStep {
56
56
  skill?: string | string[] | false;
57
57
  model?: string;
58
58
  acceptance?: AcceptanceInput;
59
+ stopOn?: StopCondition;
59
60
  }
60
61
 
61
62
  /** Parallel task item within a parallel step */
@@ -104,6 +105,7 @@ export interface DynamicParallelStep {
104
105
  phase?: string;
105
106
  label?: string;
106
107
  acceptance?: AcceptanceInput;
108
+ stopOn?: StopCondition;
107
109
  }
108
110
 
109
111
  /** Parallel step: multiple agents running concurrently */
@@ -113,6 +115,7 @@ export interface ParallelStep {
113
115
  failFast?: boolean;
114
116
  worktree?: boolean;
115
117
  cwd?: string;
118
+ stopOn?: StopCondition;
116
119
  }
117
120
 
118
121
  /** Union type for chain steps */
@@ -23,11 +23,47 @@ export type OutputMode = "inline" | "file-only";
23
23
 
24
24
  export type JsonSchemaObject = Record<string, unknown>;
25
25
 
26
+ export interface StopCondition {
27
+ path: string;
28
+ in: string[];
29
+ }
30
+
31
+ export interface StopMatch {
32
+ stepIndex: number;
33
+ taskIndex?: number;
34
+ as?: string;
35
+ label?: string;
36
+ agent: string;
37
+ value: string;
38
+ }
39
+
40
+ export interface StopRecord {
41
+ reason: "condition";
42
+ stepIndex: number;
43
+ condition: StopCondition;
44
+ matches: StopMatch[];
45
+ }
46
+
47
+ /** One finished task's output as rendered in the chain result's `## Outputs` section. */
48
+ export interface ChainTaskOutputEntry {
49
+ stepIndex: number;
50
+ taskIndex: number;
51
+ agent: string;
52
+ as?: string;
53
+ label?: string;
54
+ text: string;
55
+ structured?: unknown;
56
+ exitCode: number | null | undefined;
57
+ error?: string;
58
+ }
59
+
26
60
  export interface ChainOutputMapEntry {
27
61
  text: string;
28
62
  structured?: unknown;
29
63
  agent: string;
30
64
  stepIndex: number;
65
+ patchPath?: string;
66
+ patchError?: string;
31
67
  }
32
68
 
33
69
  export type ChainOutputMap = Record<string, ChainOutputMapEntry>;
@@ -435,6 +471,7 @@ export interface Details {
435
471
  currentStepIndex?: number; // 0-indexed current step (for running chains)
436
472
  workflowGraph?: WorkflowGraphSnapshot;
437
473
  outputs?: ChainOutputMap;
474
+ stop?: StopRecord;
438
475
  }
439
476
 
440
477
  // ============================================================================
@@ -174,6 +174,7 @@ const mapSavedChainSteps = (chain: ChainConfig, worktree = false): ChainStep[] =
174
174
  progress: step.progress,
175
175
  skill: step.skill ?? step.skills,
176
176
  model: step.model,
177
+ ...(step.stopOn ? { stopOn: step.stopOn } : {}),
177
178
  };
178
179
  });
179
180
  };