pi-cohort 7.2.2 ā 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 +12 -0
- package/README.md +31 -2
- package/package.json +1 -1
- package/skills/pi-cohort/SKILL.md +10 -0
- package/skills/pi-cohort/reference/config-fields.md +1 -0
- package/src/agents/agent-management.ts +9 -1
- package/src/agents/agents.ts +2 -1
- package/src/agents/chain-serializer.ts +14 -0
- package/src/extension/schemas.ts +4 -0
- package/src/runs/background/async-execution.ts +3 -0
- package/src/runs/background/notify.ts +8 -2
- package/src/runs/background/subagent-runner.ts +57 -21
- package/src/runs/foreground/chain-execution.ts +148 -54
- package/src/runs/foreground/subagent-executor.ts +3 -2
- package/src/runs/shared/chain-outputs.ts +49 -9
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/parallel-utils.ts +3 -0
- package/src/runs/shared/pi-spawn.ts +38 -33
- package/src/runs/shared/stop-conditions.ts +100 -0
- package/src/runs/shared/worktree.ts +7 -1
- package/src/shared/formatters.ts +64 -15
- package/src/shared/settings.ts +4 -1
- package/src/shared/types.ts +37 -0
- package/src/slash/slash-commands.ts +1 -0
|
@@ -1,36 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
3
|
|
|
5
4
|
export const PI_CODING_AGENT_PACKAGE = "@earendil-works/pi-coding-agent";
|
|
6
5
|
|
|
7
|
-
export function findPiPackageRootFromEntry(entryPoint: string): string | undefined {
|
|
8
|
-
let dir = path.dirname(entryPoint);
|
|
9
|
-
while (true) {
|
|
10
|
-
const packageJsonPath = path.join(dir, "package.json");
|
|
11
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
12
|
-
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as { name?: unknown };
|
|
13
|
-
if (pkg.name === PI_CODING_AGENT_PACKAGE) return dir;
|
|
14
|
-
}
|
|
15
|
-
if (dir === path.dirname(dir)) return undefined;
|
|
16
|
-
dir = path.dirname(dir);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function resolveInstalledPiPackageRoot(): string | undefined {
|
|
21
|
-
return findPiPackageRootFromEntry(fileURLToPath(import.meta.resolve(PI_CODING_AGENT_PACKAGE)));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function resolvePiPackageRoot(): string | undefined {
|
|
25
|
-
try {
|
|
26
|
-
const entry = process.argv[1];
|
|
27
|
-
return entry ? findPiPackageRootFromEntry(fs.realpathSync(entry)) : undefined;
|
|
28
|
-
} catch {
|
|
29
|
-
// Non-launch package inspection is best-effort.
|
|
30
|
-
return undefined;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
6
|
export interface PiRuntimeIdentity {
|
|
35
7
|
readonly nodeExecPath: string;
|
|
36
8
|
readonly piEntryPath: string;
|
|
@@ -90,13 +62,46 @@ function checkAvailability(file: string, executable: boolean): void {
|
|
|
90
62
|
}
|
|
91
63
|
}
|
|
92
64
|
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
const piEntryPath = canonicalPath(deps.argv1 ?? process.argv[1], "Pi entry");
|
|
96
|
-
checkAvailability(nodeExecPath, true);
|
|
65
|
+
function identifyFromEntry(entry: string | undefined): { piEntryPath: string; piPackageRoot: string } {
|
|
66
|
+
const piEntryPath = canonicalPath(entry, "Pi entry");
|
|
97
67
|
checkAvailability(piEntryPath, false);
|
|
98
68
|
const piPackageRoot = canonicalPath(owningPackageRoot(piEntryPath), "Pi package root");
|
|
99
|
-
return {
|
|
69
|
+
return { piEntryPath, piPackageRoot };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function identifyFromInstallRoot(installRoot: string): { piEntryPath: string; piPackageRoot: string } {
|
|
73
|
+
const piPackageRoot = canonicalPath(installRoot, "Pi installation root");
|
|
74
|
+
const manifest = path.join(piPackageRoot, "package.json");
|
|
75
|
+
let bin: unknown;
|
|
76
|
+
try {
|
|
77
|
+
const pkg = JSON.parse(fs.readFileSync(manifest, "utf-8"));
|
|
78
|
+
if (pkg?.name !== PI_CODING_AGENT_PACKAGE) {
|
|
79
|
+
throw new PiRuntimeLaunchError(`${manifest}: installation is not ${PI_CODING_AGENT_PACKAGE}`);
|
|
80
|
+
}
|
|
81
|
+
bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.pi;
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error instanceof PiRuntimeLaunchError) throw error;
|
|
84
|
+
throw new PiRuntimeLaunchError(`${manifest}: ${String(error)}`);
|
|
85
|
+
}
|
|
86
|
+
if (typeof bin !== "string" || !bin) throw new PiRuntimeLaunchError(`${manifest}: missing declared Pi bin`);
|
|
87
|
+
const piEntryPath = canonicalPath(path.resolve(piPackageRoot, bin), "Declared Pi bin");
|
|
88
|
+
checkAvailability(piEntryPath, false);
|
|
89
|
+
const owner = canonicalPath(owningPackageRoot(piEntryPath), "Pi package root");
|
|
90
|
+
if (owner !== piPackageRoot) {
|
|
91
|
+
throw new PiRuntimeLaunchError(`${piEntryPath}: owning package ${owner} does not match installation ${piPackageRoot}`);
|
|
92
|
+
}
|
|
93
|
+
return { piEntryPath, piPackageRoot };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function capturePiRuntime(deps: { execPath?: string; argv1?: string; installRoot?: string } = {}): PiRuntimeIdentity {
|
|
97
|
+
const nodeExecPath = canonicalPath(deps.execPath ?? process.execPath, "Node executable");
|
|
98
|
+
checkAvailability(nodeExecPath, true);
|
|
99
|
+
try {
|
|
100
|
+
return { nodeExecPath, ...identifyFromEntry(deps.argv1 ?? process.argv[1]) };
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (!(error instanceof PiRuntimeLaunchError) || deps.installRoot === undefined) throw error;
|
|
103
|
+
return { nodeExecPath, ...identifyFromInstallRoot(deps.installRoot) };
|
|
104
|
+
}
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
export function parsePiRuntime(value: unknown, source: string): PiRuntimeIdentity {
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { isDynamicParallelStep, isParallelStep, type ChainStep, type SequentialStep } from "../../shared/settings.ts";
|
|
2
|
+
import type { StopCondition, StopMatch, StopRecord } from "../../shared/types.ts";
|
|
3
|
+
import { assertJsonPointer, DynamicFanoutError, resolveJsonPointer } from "./dynamic-fanout.ts";
|
|
4
|
+
|
|
5
|
+
export interface StopEvaluationTask {
|
|
6
|
+
agent: string;
|
|
7
|
+
as?: string;
|
|
8
|
+
label?: string;
|
|
9
|
+
exitCode: number | null | undefined;
|
|
10
|
+
structuredOutput?: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Shape check for one condition value; `label` prefixes the message (`Chain step 1 stopOn`, `config.steps[0].stopOn`). */
|
|
14
|
+
export function stopConditionShapeError(value: unknown, label: string): string | undefined {
|
|
15
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return `${label} must be an object with path and in.`;
|
|
16
|
+
const record = value as Record<string, unknown>;
|
|
17
|
+
try {
|
|
18
|
+
if (typeof record.path !== "string") throw new DynamicFanoutError("Invalid JSON Pointer type.");
|
|
19
|
+
assertJsonPointer(record.path, `${label}.path`);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (!(error instanceof DynamicFanoutError)) throw error;
|
|
22
|
+
return `${label}.path '${String(record.path)}' is not a JSON Pointer (must be "" or start with "/").`;
|
|
23
|
+
}
|
|
24
|
+
const list = record.in;
|
|
25
|
+
if (!Array.isArray(list) || list.length === 0 || list.some((member) => typeof member !== "string")) {
|
|
26
|
+
return `${label}.in must be a non-empty array of strings.`;
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Pre-run check across a chain; returns the first error message or undefined. */
|
|
32
|
+
export function validateStopConditions(steps: ChainStep[]): string | undefined {
|
|
33
|
+
for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
|
|
34
|
+
const step = steps[stepIndex]!;
|
|
35
|
+
const stopOn = (step as { stopOn?: unknown }).stopOn;
|
|
36
|
+
if (stopOn === undefined) continue;
|
|
37
|
+
const stepNumber = stepIndex + 1;
|
|
38
|
+
const shapeError = stopConditionShapeError(stopOn, `Chain step ${stepNumber} stopOn`);
|
|
39
|
+
if (shapeError) return shapeError;
|
|
40
|
+
if (isParallelStep(step)) {
|
|
41
|
+
for (let taskIndex = 0; taskIndex < step.parallel.length; taskIndex++) {
|
|
42
|
+
if (!step.parallel[taskIndex]!.outputSchema) {
|
|
43
|
+
return `Chain step ${stepNumber} task ${taskIndex + 1} has no outputSchema; stopOn requires one on every task.`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (isDynamicParallelStep(step)) {
|
|
49
|
+
if (!step.parallel.outputSchema) return `Chain step ${stepNumber} task 1 has no outputSchema; stopOn requires one on every task.`;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!(step as SequentialStep).outputSchema) return `Chain step ${stepNumber} has stopOn but no outputSchema.`;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function resolveStringAt(value: unknown, pointer: string): string | undefined {
|
|
58
|
+
let resolved: unknown;
|
|
59
|
+
try {
|
|
60
|
+
resolved = resolveJsonPointer(value, pointer, "stopOn.path");
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error instanceof DynamicFanoutError) return undefined;
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
return typeof resolved === "string" ? resolved : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Matches in task order; only exit-0 tasks with structured output are considered. Never throws for data. */
|
|
69
|
+
export function evaluateStopCondition(
|
|
70
|
+
condition: StopCondition,
|
|
71
|
+
stepIndex: number,
|
|
72
|
+
tasks: StopEvaluationTask[],
|
|
73
|
+
options: { sequential?: boolean } = {},
|
|
74
|
+
): StopMatch[] {
|
|
75
|
+
const matches: StopMatch[] = [];
|
|
76
|
+
for (let taskIndex = 0; taskIndex < tasks.length; taskIndex++) {
|
|
77
|
+
const task = tasks[taskIndex]!;
|
|
78
|
+
if (task.exitCode !== 0 || task.structuredOutput === undefined) continue;
|
|
79
|
+
const value = resolveStringAt(task.structuredOutput, condition.path);
|
|
80
|
+
if (value === undefined || !condition.in.includes(value)) continue;
|
|
81
|
+
matches.push({
|
|
82
|
+
stepIndex,
|
|
83
|
+
...(options.sequential ? {} : { taskIndex }),
|
|
84
|
+
...(task.as ? { as: task.as } : {}),
|
|
85
|
+
...(task.label ? { label: task.label } : {}),
|
|
86
|
+
agent: task.agent,
|
|
87
|
+
value,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return matches;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function buildStopRecord(condition: StopCondition, stepIndex: number, matches: StopMatch[]): StopRecord {
|
|
94
|
+
return { reason: "condition", stepIndex, condition, matches };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function formatStopCondition(condition: StopCondition): string {
|
|
98
|
+
const pointer = condition.path === "" ? '""' : condition.path;
|
|
99
|
+
return `${pointer} in [${condition.in.join(", ")}]`;
|
|
100
|
+
}
|
|
@@ -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;
|
|
@@ -490,6 +490,12 @@ function hasWorktreeChanges(diff: WorktreeDiff): boolean {
|
|
|
490
490
|
return diff.filesChanged > 0 || diff.insertions > 0 || diff.deletions > 0 || diff.diffStat.trim().length > 0;
|
|
491
491
|
}
|
|
492
492
|
|
|
493
|
+
/** The patch a later chain step may reference: only a clean capture with real changes qualifies. */
|
|
494
|
+
export function patchPathForDiff(diff: WorktreeDiff): string | undefined {
|
|
495
|
+
if (diff.captureError) return undefined;
|
|
496
|
+
return hasWorktreeChanges(diff) ? diff.patchPath : undefined;
|
|
497
|
+
}
|
|
498
|
+
|
|
493
499
|
export function createWorktrees(cwd: string, runId: string, count: number, options?: CreateWorktreesOptions): WorktreeSetup {
|
|
494
500
|
const repo = resolveRepoState(cwd);
|
|
495
501
|
const setupHook = resolveWorktreeSetupHook(repo.toplevel, options?.setupHook);
|
package/src/shared/formatters.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
/**
|
package/src/shared/settings.ts
CHANGED
|
@@ -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 */
|
package/src/shared/types.ts
CHANGED
|
@@ -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
|
// ============================================================================
|