pi-cohort 7.3.1 → 7.4.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 +8 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/runs/background/async-execution.ts +12 -6
- package/src/runs/background/subagent-runner.ts +53 -31
- package/src/runs/foreground/chain-execution.ts +515 -498
- package/src/runs/foreground/execution.ts +1 -1
- package/src/runs/shared/acceptance.ts +4 -2
- package/src/runs/shared/chain-outputs.ts +73 -12
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/worktree.ts +32 -0
- package/src/shared/types.ts +4 -0
|
@@ -669,7 +669,7 @@ export async function runSync(
|
|
|
669
669
|
dynamic: options.acceptanceContext?.dynamic,
|
|
670
670
|
dynamicGroup: options.acceptanceContext?.dynamicGroup,
|
|
671
671
|
});
|
|
672
|
-
const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance);
|
|
672
|
+
const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance, { structuredOutput: Boolean(options.structuredOutput) });
|
|
673
673
|
const taskWithAcceptance = acceptancePrompt ? `${task}\n${acceptancePrompt}` : task;
|
|
674
674
|
const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
|
|
675
675
|
const skillNames = options.skills ?? agent.skills ?? [];
|
|
@@ -238,7 +238,7 @@ export function resolveEffectiveAcceptance(input: {
|
|
|
238
238
|
};
|
|
239
239
|
}
|
|
240
240
|
|
|
241
|
-
export function formatAcceptancePrompt(acceptance: ResolvedAcceptanceConfig): string {
|
|
241
|
+
export function formatAcceptancePrompt(acceptance: ResolvedAcceptanceConfig, options: { structuredOutput: boolean } = { structuredOutput: false }): string {
|
|
242
242
|
if (acceptance.level === "none") return "";
|
|
243
243
|
const lines = [
|
|
244
244
|
"",
|
|
@@ -264,7 +264,9 @@ export function formatAcceptancePrompt(acceptance: ResolvedAcceptanceConfig): st
|
|
|
264
264
|
}
|
|
265
265
|
lines.push(
|
|
266
266
|
"",
|
|
267
|
-
|
|
267
|
+
options.structuredOutput
|
|
268
|
+
? "Write a fenced JSON block tagged `acceptance-report` in your final text message, then call `structured_output` as your final action. Shape:"
|
|
269
|
+
: "Finish with a fenced JSON block tagged `acceptance-report` in this shape:",
|
|
268
270
|
"```acceptance-report",
|
|
269
271
|
JSON.stringify({
|
|
270
272
|
criteriaSatisfied: [{ id: "criterion-1", status: "satisfied", evidence: "specific proof" }],
|
|
@@ -1,20 +1,23 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
1
2
|
import { isDynamicParallelStep, isParallelStep, type ChainStep, type SequentialStep } from "../../shared/settings.ts";
|
|
2
3
|
import type { ChainOutputMap, ChainOutputMapEntry, SingleResult } from "../../shared/types.ts";
|
|
3
4
|
import { getSingleResultOutput } from "../../shared/utils.ts";
|
|
4
5
|
import { DynamicFanoutError, hasDynamicFanoutFields, type DynamicFanoutConfig, validateDynamicStepShape } from "./dynamic-fanout.ts";
|
|
6
|
+
import { materializeTree } from "./worktree.ts";
|
|
5
7
|
import { validateStopConditions } from "./stop-conditions.ts";
|
|
6
8
|
|
|
7
9
|
const OUTPUT_REF_PATTERN = /\{outputs\.([^}]*)\}/g;
|
|
8
10
|
const SAFE_OUTPUT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
9
11
|
const PATCH_PATH_FIELD = "patchPath";
|
|
10
12
|
|
|
11
|
-
/**
|
|
12
|
-
function parseOutputReference(raw: string): { name: string; field?:
|
|
13
|
+
/** Only text and filesystem fields are valid output references. */
|
|
14
|
+
function parseOutputReference(raw: string): { name: string; field?: "patchPath" | "treePath" } | undefined {
|
|
13
15
|
const dot = raw.indexOf(".");
|
|
14
16
|
const name = dot === -1 ? raw : raw.slice(0, dot);
|
|
15
17
|
if (!SAFE_OUTPUT_NAME_PATTERN.test(name)) return undefined;
|
|
16
18
|
if (dot === -1) return { name };
|
|
17
|
-
|
|
19
|
+
const field = raw.slice(dot + 1);
|
|
20
|
+
return field === PATCH_PATH_FIELD || field === "treePath" ? { name, field } : undefined;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
export class ChainOutputValidationError extends Error {}
|
|
@@ -26,10 +29,22 @@ function outputNamesForStep(step: ChainStep): string[] {
|
|
|
26
29
|
return name ? [name] : [];
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
function taskTemplatesForStep(step: ChainStep): string
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
function taskTemplatesForStep(step: ChainStep): Array<{ template: string; cwd: boolean }> {
|
|
33
|
+
const templates: Array<{ template: string; cwd: boolean }> = [];
|
|
34
|
+
const add = (value: { task?: string; label?: string; cwd?: string }) => {
|
|
35
|
+
for (const template of [value.task, value.label]) {
|
|
36
|
+
if (template) templates.push({ template, cwd: false });
|
|
37
|
+
}
|
|
38
|
+
if (value.cwd) templates.push({ template: value.cwd, cwd: true });
|
|
39
|
+
};
|
|
40
|
+
if (isParallelStep(step)) {
|
|
41
|
+
if (step.cwd) templates.push({ template: step.cwd, cwd: true });
|
|
42
|
+
for (const task of step.parallel) add(task);
|
|
43
|
+
} else if (isDynamicParallelStep(step)) {
|
|
44
|
+
add(step);
|
|
45
|
+
add(step.parallel);
|
|
46
|
+
} else add(step);
|
|
47
|
+
return templates;
|
|
33
48
|
}
|
|
34
49
|
|
|
35
50
|
export function validateChainOutputBindings(steps: ChainStep[], dynamicFanoutConfig: DynamicFanoutConfig = {}): void {
|
|
@@ -64,17 +79,23 @@ export function validateChainOutputBindings(steps: ChainStep[], dynamicFanoutCon
|
|
|
64
79
|
}
|
|
65
80
|
seen.add(name);
|
|
66
81
|
}
|
|
67
|
-
for (const template of taskTemplatesForStep(step)) {
|
|
82
|
+
for (const { template, cwd } of taskTemplatesForStep(step)) {
|
|
68
83
|
for (const match of template.matchAll(OUTPUT_REF_PATTERN)) {
|
|
84
|
+
if (cwd && isParallelStep(step) && step.worktree) {
|
|
85
|
+
throw new ChainOutputValidationError(`Chain step ${stepIndex + 1}: cwd output references cannot be combined with worktree: true.`);
|
|
86
|
+
}
|
|
69
87
|
const rawReference = match[0];
|
|
70
88
|
const parsed = parseOutputReference(match[1]!);
|
|
71
89
|
if (!parsed) {
|
|
72
90
|
throw new ChainOutputValidationError(`Invalid chain output reference '${rawReference}' at step ${stepIndex + 1}. Use {outputs.name} with /^[A-Za-z_][A-Za-z0-9_]*$/ names.`);
|
|
73
91
|
}
|
|
92
|
+
if (cwd && parsed.field !== "treePath") {
|
|
93
|
+
throw new ChainOutputValidationError(`Chain step ${stepIndex + 1}: only {outputs.<name>.treePath} may appear in cwd.`);
|
|
94
|
+
}
|
|
74
95
|
if (!available.has(parsed.name)) {
|
|
75
96
|
throw new ChainOutputValidationError(`Unknown chain output reference '${rawReference}' at step ${stepIndex + 1}. Named outputs are only available after producing step/group completes.`);
|
|
76
97
|
}
|
|
77
|
-
if (parsed.field
|
|
98
|
+
if (parsed.field && !worktreeProducers.has(parsed.name)) {
|
|
78
99
|
const producerStep = producerSteps.get(parsed.name)!;
|
|
79
100
|
throw new ChainOutputValidationError(`Chain step ${stepIndex + 1} references ${rawReference} but step ${producerStep + 1} is not a worktree step.`);
|
|
80
101
|
}
|
|
@@ -88,7 +109,7 @@ export function validateChainOutputBindings(steps: ChainStep[], dynamicFanoutCon
|
|
|
88
109
|
}
|
|
89
110
|
}
|
|
90
111
|
|
|
91
|
-
export function resolveOutputReferences(template: string, outputs: ChainOutputMap): string {
|
|
112
|
+
export function resolveOutputReferences(template: string, outputs: ChainOutputMap, trees?: TreeRegistry): string {
|
|
92
113
|
return template.replace(OUTPUT_REF_PATTERN, (rawReference, rawName: string) => {
|
|
93
114
|
const parsed = parseOutputReference(rawName);
|
|
94
115
|
if (!parsed) {
|
|
@@ -96,25 +117,65 @@ export function resolveOutputReferences(template: string, outputs: ChainOutputMa
|
|
|
96
117
|
}
|
|
97
118
|
const entry = outputs[parsed.name];
|
|
98
119
|
if (!entry) throw new ChainOutputValidationError(`Unknown chain output reference '${rawReference}'.`);
|
|
99
|
-
if (parsed.field
|
|
120
|
+
if (parsed.field === "treePath") {
|
|
121
|
+
if (!trees) throw new ChainOutputValidationError(`Output '${parsed.name}' tree registry is unavailable.`);
|
|
122
|
+
return trees.resolve(parsed.name, entry);
|
|
123
|
+
}
|
|
124
|
+
if (!parsed.field) return entry.text;
|
|
100
125
|
if (entry.patchPath) return entry.patchPath;
|
|
101
126
|
const reason = entry.patchError ? `capture failed: ${entry.patchError}` : "no changes";
|
|
102
127
|
throw new ChainOutputValidationError(`Output '${parsed.name}' has no patch (${reason}).`);
|
|
103
128
|
});
|
|
104
129
|
}
|
|
105
130
|
|
|
131
|
+
export class TreeRegistry {
|
|
132
|
+
readonly built = new Map<string, string>();
|
|
133
|
+
readonly created: Array<{ repoCwd: string; dir: string }> = [];
|
|
134
|
+
|
|
135
|
+
private readonly chainDir: string;
|
|
136
|
+
|
|
137
|
+
constructor(chainDir: string) {
|
|
138
|
+
this.chainDir = chainDir;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
resolve(name: string, entry: ChainOutputMapEntry): string {
|
|
142
|
+
if (this.built.has(name)) return this.built.get(name)!;
|
|
143
|
+
if (!entry.patchPath) {
|
|
144
|
+
const reason = entry.patchError ? `capture failed: ${entry.patchError}` : "no changes";
|
|
145
|
+
throw new ChainOutputValidationError(`Output '${name}' has no patch (${reason}).`);
|
|
146
|
+
}
|
|
147
|
+
const dir = path.join(this.chainDir, "worktree-trees", `step-${entry.stepIndex}`, `task-${entry.taskIndex}-${entry.agent}`);
|
|
148
|
+
this.created.push({ repoCwd: entry.repoCwd!, dir });
|
|
149
|
+
try {
|
|
150
|
+
materializeTree({ repoCwd: entry.repoCwd!, baseCommit: entry.baseCommit!, patchPath: entry.patchPath, dir });
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
153
|
+
throw new ChainOutputValidationError(`Output '${name}' tree build failed: ${message.split("\n")[0]}`);
|
|
154
|
+
}
|
|
155
|
+
this.built.set(name, dir);
|
|
156
|
+
entry.treePath = dir;
|
|
157
|
+
return dir;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
106
161
|
function compactStructuredText(value: unknown): string {
|
|
107
162
|
return JSON.stringify(value);
|
|
108
163
|
}
|
|
109
164
|
|
|
110
165
|
export interface OutputEntryPatchInfo {
|
|
166
|
+
taskIndex?: number;
|
|
167
|
+
baseCommit?: string;
|
|
168
|
+
repoCwd?: string;
|
|
111
169
|
patchPath?: string;
|
|
112
170
|
patchError?: string;
|
|
113
171
|
}
|
|
114
172
|
|
|
115
|
-
function patchFields(patch: OutputEntryPatchInfo | undefined):
|
|
173
|
+
function patchFields(patch: OutputEntryPatchInfo | undefined): OutputEntryPatchInfo {
|
|
116
174
|
if (!patch) return {};
|
|
117
175
|
return {
|
|
176
|
+
...(patch.taskIndex !== undefined ? { taskIndex: patch.taskIndex } : {}),
|
|
177
|
+
...(patch.baseCommit !== undefined ? { baseCommit: patch.baseCommit } : {}),
|
|
178
|
+
...(patch.repoCwd !== undefined ? { repoCwd: patch.repoCwd } : {}),
|
|
118
179
|
...(patch.patchPath ? { patchPath: patch.patchPath } : {}),
|
|
119
180
|
...(patch.patchError ? { patchError: patch.patchError } : {}),
|
|
120
181
|
};
|
|
@@ -557,6 +557,38 @@ export function diffWorktrees(setup: WorktreeSetup, agents: string[], diffsDir:
|
|
|
557
557
|
return diffs;
|
|
558
558
|
}
|
|
559
559
|
|
|
560
|
+
export function materializeTree({ repoCwd, baseCommit, patchPath, dir }: {
|
|
561
|
+
repoCwd: string;
|
|
562
|
+
baseCommit: string;
|
|
563
|
+
patchPath: string;
|
|
564
|
+
dir: string;
|
|
565
|
+
}): string {
|
|
566
|
+
fs.mkdirSync(path.dirname(dir), { recursive: true });
|
|
567
|
+
const add = runGit(repoCwd, ["worktree", "add", "--detach", dir, baseCommit]);
|
|
568
|
+
if (add.status !== 0) {
|
|
569
|
+
throw new Error(add.stderr.split("\n").map((line) => line.trim()).find(Boolean) || "git worktree add failed");
|
|
570
|
+
}
|
|
571
|
+
const apply = runGit(dir, ["apply", "--index", patchPath]);
|
|
572
|
+
if (apply.status !== 0) {
|
|
573
|
+
throw new Error(apply.stderr.split("\n").map((line) => line.trim()).find(Boolean) || "git apply failed");
|
|
574
|
+
}
|
|
575
|
+
return dir;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export function removeTrees(entries: Array<{ repoCwd: string; dir: string }>): void {
|
|
579
|
+
for (const { repoCwd, dir } of entries) {
|
|
580
|
+
if (!fs.existsSync(dir)) continue;
|
|
581
|
+
try { runGitChecked(repoCwd, ["worktree", "remove", "--force", dir]); } catch (error) {
|
|
582
|
+
console.warn(`Failed to remove tree '${dir}': ${error instanceof Error ? error.message : String(error)}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
for (const repoCwd of new Set(entries.map((entry) => entry.repoCwd))) {
|
|
586
|
+
try { runGitChecked(repoCwd, ["worktree", "prune"]); } catch (error) {
|
|
587
|
+
console.warn(`Failed to prune worktrees in '${repoCwd}': ${error instanceof Error ? error.message : String(error)}`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
560
592
|
export function cleanupWorktrees(setup: WorktreeSetup): void {
|
|
561
593
|
for (let index = setup.worktrees.length - 1; index >= 0; index--) {
|
|
562
594
|
cleanupSingleWorktree(setup.cwd, setup.worktrees[index]!);
|
package/src/shared/types.ts
CHANGED