pi-cohort 5.2.0 → 5.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.
- package/CHANGELOG.md +39 -0
- package/package.json +5 -9
- package/src/runs/background/async-execution.ts +20 -4
- package/src/runs/foreground/subagent-executor.ts +70 -15
- package/src/runs/shared/acceptance.ts +4 -4
- package/src/runs/shared/single-output.ts +31 -0
- package/src/runs/shared/worktree.ts +19 -10
- package/src/shared/artifacts.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [5.3.1] - 2026-09-01
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Async (detached) subagent runs no longer crash at boot with `Cannot find module 'typebox/compile'` on consumer installs: `typebox` is now a real runtime dependency (`^1.3.11`). This deliberately re-reverses 2.0.0's move of typebox to peerDependencies - that decision's "match pi's bundled packages" rationale only holds for code running under pi's jiti aliases, and the detached runner runs outside them (same reasoning as `jiti` itself being a real dependency). ([#9](https://github.com/jjuraszek/pi-cohort/issues/9))
|
|
8
|
+
- Acceptance evidence checks now treat present-but-empty `changedFiles`, `testsAddedOrUpdated`, `commandsRun`, and `validationOutput` arrays as reported evidence ("ran and touched nothing") instead of rejecting the run with `evidence missing`; an absent field still fails structurally. No-op async worker runs with all criteria passing now reach `checked` status. ([#9](https://github.com/jjuraszek/pi-cohort/issues/9))
|
|
9
|
+
|
|
10
|
+
## [5.3.0] - 2026-09-01
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Foreground parallel `worktree: true` patches now write to a run-scoped
|
|
15
|
+
directory (`<session artifacts>/<runId>/worktree-diffs`) instead of a
|
|
16
|
+
session-wide `worktree-diffs` directory shared by every dispatch, so
|
|
17
|
+
concurrent or sequential worktree runs no longer overwrite each other's
|
|
18
|
+
patches. Background (`<asyncDir>/worktree-diffs/step-<i>`) and chain
|
|
19
|
+
(`<chainDir>/worktree-diffs/step-<i>`) paths are unchanged. A relative
|
|
20
|
+
`output:` on a `worktree: true` task now resolves to the run's per-task
|
|
21
|
+
directory (`<session artifacts>/<runId>/task-<i>/` foreground,
|
|
22
|
+
`<asyncDir>/step-<i>/task-<j>/` async) instead of inside the throwaway
|
|
23
|
+
checkout, so the report survives worktree teardown and no longer pollutes
|
|
24
|
+
the captured patch; an absolute `output:` is unaffected, and `reads:` plus
|
|
25
|
+
the task's working directory still resolve inside the checkout. This is a
|
|
26
|
+
behavior change for anyone relying on the old patch/output paths, not a
|
|
27
|
+
breaking API change - no parameter, schema, or return shape changed.
|
|
28
|
+
- Stale artifact directories are now pruned recursively by the artifact
|
|
29
|
+
cleanup sweep, alongside files (previously only files were pruned, since
|
|
30
|
+
`unlinkSync` throws on a directory).
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- A relative `output:` that escapes its per-task directory (`../report.md`)
|
|
35
|
+
or normalizes to the directory itself (`.`) is now rejected at dispatch
|
|
36
|
+
with a tool error instead of writing outside the intended location.
|
|
37
|
+
- Worktree diffs are now captured before every post-execution return
|
|
38
|
+
(normal, interrupted, detached, intercom-receipt), so the diff summary is
|
|
39
|
+
attached even when a run ends early; a patch-capture failure is now
|
|
40
|
+
surfaced in the summary instead of silently reporting an empty patch.
|
|
41
|
+
|
|
3
42
|
## [5.2.0] - 2026-09-01
|
|
4
43
|
|
|
5
44
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cohort",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.1",
|
|
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",
|
|
@@ -60,8 +60,7 @@
|
|
|
60
60
|
"@earendil-works/pi-agent-core": "*",
|
|
61
61
|
"@earendil-works/pi-ai": "*",
|
|
62
62
|
"@earendil-works/pi-coding-agent": "*",
|
|
63
|
-
"@earendil-works/pi-tui": "*"
|
|
64
|
-
"typebox": "*"
|
|
63
|
+
"@earendil-works/pi-tui": "*"
|
|
65
64
|
},
|
|
66
65
|
"peerDependenciesMeta": {
|
|
67
66
|
"@earendil-works/pi-agent-core": {
|
|
@@ -75,19 +74,16 @@
|
|
|
75
74
|
},
|
|
76
75
|
"@earendil-works/pi-tui": {
|
|
77
76
|
"optional": true
|
|
78
|
-
},
|
|
79
|
-
"typebox": {
|
|
80
|
-
"optional": true
|
|
81
77
|
}
|
|
82
78
|
},
|
|
83
79
|
"dependencies": {
|
|
84
|
-
"jiti": "^2.7.0"
|
|
80
|
+
"jiti": "^2.7.0",
|
|
81
|
+
"typebox": "^1.3.11"
|
|
85
82
|
},
|
|
86
83
|
"devDependencies": {
|
|
87
84
|
"@earendil-works/pi-agent-core": "^0.74.0",
|
|
88
85
|
"@earendil-works/pi-ai": "^0.74.0",
|
|
89
86
|
"@earendil-works/pi-coding-agent": "^0.74.0",
|
|
90
|
-
"@earendil-works/pi-tui": "^0.74.0"
|
|
91
|
-
"typebox": "^1.1.24"
|
|
87
|
+
"@earendil-works/pi-tui": "^0.74.0"
|
|
92
88
|
}
|
|
93
89
|
}
|
|
@@ -11,7 +11,7 @@ import { createRequire } from "node:module";
|
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import type { AgentConfig } from "../../agents/agents.ts";
|
|
13
13
|
import { applyThinkingSuffix } from "../shared/pi-args.ts";
|
|
14
|
-
import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
14
|
+
import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveParallelTaskOutputPath, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
15
15
|
import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
|
|
16
16
|
import type { RunnerStep } from "../shared/parallel-utils.ts";
|
|
17
17
|
import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
|
|
@@ -373,7 +373,7 @@ export function executeAsyncChain(
|
|
|
373
373
|
...(s.model ? { model: s.model } : {}),
|
|
374
374
|
};
|
|
375
375
|
};
|
|
376
|
-
const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior) => {
|
|
376
|
+
const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior, isolatedLeaf?: { runDir: string; index: number }) => {
|
|
377
377
|
const a = agents.find((x) => x.name === s.agent)!;
|
|
378
378
|
const stepCwd = resolveChildCwd(runnerCwd, s.cwd);
|
|
379
379
|
const instructionCwd = behaviorCwd ?? stepCwd;
|
|
@@ -392,7 +392,16 @@ export function executeAsyncChain(
|
|
|
392
392
|
const isFirstProgressAgent = behavior.progress && !progressPrecreated && !progressInstructionCreated;
|
|
393
393
|
if (behavior.progress) progressInstructionCreated = true;
|
|
394
394
|
const progressInstructions = buildChainInstructions({ ...behavior, output: false, reads: false }, runnerCwd, isFirstProgressAgent);
|
|
395
|
-
const
|
|
395
|
+
const resolvedOutput = resolveParallelTaskOutputPath({
|
|
396
|
+
output: behavior.output,
|
|
397
|
+
ctxCwd: ctx.cwd,
|
|
398
|
+
taskCwd: instructionCwd,
|
|
399
|
+
isolated: Boolean(isolatedLeaf),
|
|
400
|
+
runDir: isolatedLeaf?.runDir,
|
|
401
|
+
index: isolatedLeaf?.index ?? 0,
|
|
402
|
+
});
|
|
403
|
+
if ("error" in resolvedOutput) throw new AsyncStartValidationError(resolvedOutput.error);
|
|
404
|
+
const outputPath = resolvedOutput.path;
|
|
396
405
|
const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Async step (${s.agent})`);
|
|
397
406
|
if (validationError) throw new AsyncStartValidationError(validationError);
|
|
398
407
|
let taskTemplate = s.task ?? "{previous}";
|
|
@@ -470,7 +479,14 @@ export function executeAsyncChain(
|
|
|
470
479
|
behaviorCwd = undefined;
|
|
471
480
|
}
|
|
472
481
|
}
|
|
473
|
-
return buildSeqStep(
|
|
482
|
+
return buildSeqStep(
|
|
483
|
+
t,
|
|
484
|
+
nextSessionFile(),
|
|
485
|
+
behaviorCwd,
|
|
486
|
+
progressPrecreated,
|
|
487
|
+
parallelBehaviors[taskIndex],
|
|
488
|
+
s.worktree ? { runDir: path.join(asyncDir, `step-${stepIndex}`), index: taskIndex } : undefined,
|
|
489
|
+
);
|
|
474
490
|
}),
|
|
475
491
|
concurrency: s.concurrency,
|
|
476
492
|
failFast: s.failFast,
|
|
@@ -38,7 +38,7 @@ import { createForkContextResolver } from "../../shared/fork-context.ts";
|
|
|
38
38
|
import { resolveCurrentSessionId } from "../../shared/session-identity.ts";
|
|
39
39
|
import { applyIntercomBridgeToAgent, INTERCOM_BRIDGE_MARKER, resolveIntercomBridge, resolveIntercomSessionTarget, resolveSubagentIntercomTarget, type IntercomBridgeState } from "../../intercom/intercom-bridge.ts";
|
|
40
40
|
import { formatControlIntercomMessage, formatControlNoticeMessage, resolveControlConfig, shouldNotifyControlEvent } from "../shared/subagent-control.ts";
|
|
41
|
-
import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeTopLevelOutput, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
41
|
+
import { finalizeSingleOutput, injectSingleOutputInstruction, normalizeTopLevelOutput, resolveParallelTaskOutputPath, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
|
|
42
42
|
import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, readStatus, resolveChildCwd } from "../../shared/utils.ts";
|
|
43
43
|
import {
|
|
44
44
|
attachNestedChildrenToResultChildren,
|
|
@@ -1468,28 +1468,62 @@ function resolveParallelTaskCwd(
|
|
|
1468
1468
|
function buildParallelWorktreeSuffix(
|
|
1469
1469
|
worktreeSetup: WorktreeSetup | undefined,
|
|
1470
1470
|
artifactsDir: string,
|
|
1471
|
+
runId: string,
|
|
1471
1472
|
tasks: TaskParam[],
|
|
1472
1473
|
): string {
|
|
1473
1474
|
if (!worktreeSetup) return "";
|
|
1474
|
-
const diffsDir = path.join(artifactsDir, "worktree-diffs");
|
|
1475
|
+
const diffsDir = path.join(artifactsDir, runId, "worktree-diffs");
|
|
1475
1476
|
const diffs = diffWorktrees(worktreeSetup, tasks.map((task) => task.agent), diffsDir);
|
|
1476
1477
|
return formatWorktreeDiffSummary(diffs);
|
|
1477
1478
|
}
|
|
1478
1479
|
|
|
1480
|
+
function resolveParallelTaskOutput(input: {
|
|
1481
|
+
output: string | boolean | undefined;
|
|
1482
|
+
task: TaskParam;
|
|
1483
|
+
paramsCwd: string;
|
|
1484
|
+
ctxCwd: string;
|
|
1485
|
+
worktreeSetup: WorktreeSetup | undefined;
|
|
1486
|
+
artifactsDir: string;
|
|
1487
|
+
runId: string;
|
|
1488
|
+
index: number;
|
|
1489
|
+
}): { path: string | undefined } | { error: string } {
|
|
1490
|
+
const taskCwd = resolveParallelTaskCwd(input.task, input.paramsCwd, input.worktreeSetup, input.index);
|
|
1491
|
+
return resolveParallelTaskOutputPath({
|
|
1492
|
+
output: input.output,
|
|
1493
|
+
ctxCwd: input.ctxCwd,
|
|
1494
|
+
taskCwd,
|
|
1495
|
+
isolated: Boolean(input.worktreeSetup),
|
|
1496
|
+
runDir: path.join(input.artifactsDir, input.runId),
|
|
1497
|
+
index: input.index,
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1479
1501
|
function findDuplicateParallelOutputPath(input: {
|
|
1480
1502
|
tasks: TaskParam[];
|
|
1481
1503
|
behaviors: ResolvedStepBehavior[];
|
|
1482
1504
|
paramsCwd: string;
|
|
1483
1505
|
ctxCwd: string;
|
|
1484
1506
|
worktreeSetup?: WorktreeSetup;
|
|
1507
|
+
artifactsDir: string;
|
|
1508
|
+
runId: string;
|
|
1485
1509
|
}): string | undefined {
|
|
1486
1510
|
const seen = new Map<string, { index: number; agent: string }>();
|
|
1487
1511
|
for (let index = 0; index < input.tasks.length; index++) {
|
|
1488
1512
|
const behavior = input.behaviors[index];
|
|
1489
1513
|
if (!behavior?.output) continue;
|
|
1490
1514
|
const task = input.tasks[index]!;
|
|
1491
|
-
const
|
|
1492
|
-
|
|
1515
|
+
const resolved = resolveParallelTaskOutput({
|
|
1516
|
+
output: behavior.output,
|
|
1517
|
+
task,
|
|
1518
|
+
paramsCwd: input.paramsCwd,
|
|
1519
|
+
ctxCwd: input.ctxCwd,
|
|
1520
|
+
worktreeSetup: input.worktreeSetup,
|
|
1521
|
+
artifactsDir: input.artifactsDir,
|
|
1522
|
+
runId: input.runId,
|
|
1523
|
+
index,
|
|
1524
|
+
});
|
|
1525
|
+
if ("error" in resolved) return resolved.error;
|
|
1526
|
+
const outputPath = resolved.path;
|
|
1493
1527
|
if (!outputPath) continue;
|
|
1494
1528
|
const previous = seen.get(outputPath);
|
|
1495
1529
|
if (previous) {
|
|
@@ -1511,7 +1545,17 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
|
|
|
1511
1545
|
const progressInstructions = behavior
|
|
1512
1546
|
? buildChainInstructions({ ...behavior, output: false, reads: false }, input.paramsCwd, index === input.firstProgressIndex)
|
|
1513
1547
|
: { prefix: "", suffix: "" };
|
|
1514
|
-
const
|
|
1548
|
+
const resolvedOutput = resolveParallelTaskOutput({
|
|
1549
|
+
output: behavior?.output,
|
|
1550
|
+
task,
|
|
1551
|
+
paramsCwd: input.paramsCwd,
|
|
1552
|
+
ctxCwd: input.ctx.cwd,
|
|
1553
|
+
worktreeSetup: input.worktreeSetup,
|
|
1554
|
+
artifactsDir: input.artifactsDir,
|
|
1555
|
+
runId: input.runId,
|
|
1556
|
+
index,
|
|
1557
|
+
});
|
|
1558
|
+
const outputPath = "error" in resolvedOutput ? undefined : resolvedOutput.path;
|
|
1515
1559
|
const taskText = injectSingleOutputInstruction(
|
|
1516
1560
|
`${readInstructions.prefix}${input.taskTexts[index]!}${progressInstructions.suffix}`,
|
|
1517
1561
|
outputPath,
|
|
@@ -1799,12 +1843,23 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1799
1843
|
paramsCwd: effectiveCwd,
|
|
1800
1844
|
ctxCwd: ctx.cwd,
|
|
1801
1845
|
worktreeSetup,
|
|
1846
|
+
artifactsDir,
|
|
1847
|
+
runId,
|
|
1802
1848
|
});
|
|
1803
1849
|
if (duplicateOutputError) return buildParallelModeError(duplicateOutputError);
|
|
1804
1850
|
for (let index = 0; index < tasks.length; index++) {
|
|
1805
|
-
const
|
|
1806
|
-
|
|
1807
|
-
|
|
1851
|
+
const resolved = resolveParallelTaskOutput({
|
|
1852
|
+
output: behaviors[index]?.output,
|
|
1853
|
+
task: tasks[index]!,
|
|
1854
|
+
paramsCwd: effectiveCwd,
|
|
1855
|
+
ctxCwd: ctx.cwd,
|
|
1856
|
+
worktreeSetup,
|
|
1857
|
+
artifactsDir,
|
|
1858
|
+
runId,
|
|
1859
|
+
index,
|
|
1860
|
+
});
|
|
1861
|
+
if ("error" in resolved) return buildParallelModeError(resolved.error);
|
|
1862
|
+
const validationError = validateFileOnlyOutputMode(behaviors[index]?.outputMode, resolved.path, `Parallel task ${index + 1} (${tasks[index]!.agent})`);
|
|
1808
1863
|
if (validationError) return buildParallelModeError(validationError);
|
|
1809
1864
|
}
|
|
1810
1865
|
|
|
@@ -1871,6 +1926,9 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1871
1926
|
if (result.artifactPaths) allArtifactPaths.push(result.artifactPaths);
|
|
1872
1927
|
}
|
|
1873
1928
|
|
|
1929
|
+
const worktreeSuffix = buildParallelWorktreeSuffix(worktreeSetup, artifactsDir, runId, tasks);
|
|
1930
|
+
const withWorktreeSuffix = (text: string): string => (worktreeSuffix ? `${text}\n\n${worktreeSuffix}` : text);
|
|
1931
|
+
|
|
1874
1932
|
const interrupted = results.find((result) => result.interrupted);
|
|
1875
1933
|
const details = compactForegroundDetails({
|
|
1876
1934
|
mode: "parallel",
|
|
@@ -1882,7 +1940,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1882
1940
|
rememberForegroundRun(deps.state, { runId, mode: "parallel", cwd: effectiveCwd, results: details.results });
|
|
1883
1941
|
if (interrupted) {
|
|
1884
1942
|
return {
|
|
1885
|
-
content: [{ type: "text", text: `Parallel run paused after interrupt (${interrupted.agent}). Waiting for explicit next action.` }],
|
|
1943
|
+
content: [{ type: "text", text: withWorktreeSuffix(`Parallel run paused after interrupt (${interrupted.agent}). Waiting for explicit next action.`) }],
|
|
1886
1944
|
details,
|
|
1887
1945
|
};
|
|
1888
1946
|
}
|
|
@@ -1890,7 +1948,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1890
1948
|
const detached = detachedIndex >= 0 ? results[detachedIndex] : undefined;
|
|
1891
1949
|
if (detached) {
|
|
1892
1950
|
return {
|
|
1893
|
-
content: [{ type: "text", text: `Parallel run detached for intercom coordination (${detached.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
|
|
1951
|
+
content: [{ type: "text", text: withWorktreeSuffix(`Parallel run detached for intercom coordination (${detached.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.`) }],
|
|
1894
1952
|
details,
|
|
1895
1953
|
};
|
|
1896
1954
|
}
|
|
@@ -1906,12 +1964,11 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1906
1964
|
});
|
|
1907
1965
|
if (intercomReceipt) {
|
|
1908
1966
|
return {
|
|
1909
|
-
content: [{ type: "text", text: intercomReceipt.text }],
|
|
1967
|
+
content: [{ type: "text", text: withWorktreeSuffix(intercomReceipt.text) }],
|
|
1910
1968
|
details: intercomReceipt.details,
|
|
1911
1969
|
};
|
|
1912
1970
|
}
|
|
1913
1971
|
|
|
1914
|
-
const worktreeSuffix = buildParallelWorktreeSuffix(worktreeSetup, artifactsDir, tasks);
|
|
1915
1972
|
const ok = results.filter((result) => result.exitCode === 0).length;
|
|
1916
1973
|
const downgradeNote = backgroundRequestedWhileClarifying ? " (background requested, but clarify kept this run foreground)" : "";
|
|
1917
1974
|
const aggregatedOutput = aggregateParallelOutputs(
|
|
@@ -1925,9 +1982,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
|
|
|
1925
1982
|
);
|
|
1926
1983
|
|
|
1927
1984
|
const summary = `${ok}/${results.length} succeeded${downgradeNote}`;
|
|
1928
|
-
const fullContent =
|
|
1929
|
-
? `${summary}\n\n${aggregatedOutput}\n\n${worktreeSuffix}`
|
|
1930
|
-
: `${summary}\n\n${aggregatedOutput}`;
|
|
1985
|
+
const fullContent = withWorktreeSuffix(`${summary}\n\n${aggregatedOutput}`);
|
|
1931
1986
|
|
|
1932
1987
|
return {
|
|
1933
1988
|
content: [{ type: "text", text: fullContent }],
|
|
@@ -387,10 +387,10 @@ function checkCriteriaSatisfied(criteria: ResolvedAcceptanceGate[], report: Acce
|
|
|
387
387
|
|
|
388
388
|
function reportEvidencePresent(report: AcceptanceReport, kind: AcceptanceEvidenceKind): boolean {
|
|
389
389
|
switch (kind) {
|
|
390
|
-
case "changed-files": return isStringArray(report.changedFiles)
|
|
391
|
-
case "tests-added": return isStringArray(report.testsAddedOrUpdated)
|
|
392
|
-
case "commands-run": return Array.isArray(report.commandsRun)
|
|
393
|
-
case "validation-output": return isStringArray(report.validationOutput)
|
|
390
|
+
case "changed-files": return isStringArray(report.changedFiles);
|
|
391
|
+
case "tests-added": return isStringArray(report.testsAddedOrUpdated);
|
|
392
|
+
case "commands-run": return Array.isArray(report.commandsRun);
|
|
393
|
+
case "validation-output": return isStringArray(report.validationOutput);
|
|
394
394
|
case "residual-risks": return isStringArray(report.residualRisks);
|
|
395
395
|
case "no-staged-files": return report.noStagedFiles === true;
|
|
396
396
|
case "diff-summary": return typeof report.diffSummary === "string" && report.diffSummary.trim().length > 0;
|
|
@@ -40,6 +40,37 @@ export function resolveSingleOutputPath(
|
|
|
40
40
|
return path.resolve(baseCwd, output);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
export interface ParallelTaskOutputInput {
|
|
44
|
+
output: string | boolean | undefined;
|
|
45
|
+
ctxCwd: string;
|
|
46
|
+
taskCwd?: string;
|
|
47
|
+
isolated: boolean;
|
|
48
|
+
runDir?: string;
|
|
49
|
+
index: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type ParallelTaskOutputResult = { path: string | undefined } | { error: string };
|
|
53
|
+
|
|
54
|
+
export function resolveParallelTaskOutputPath(input: ParallelTaskOutputInput): ParallelTaskOutputResult {
|
|
55
|
+
const { output, ctxCwd, taskCwd, isolated, runDir, index } = input;
|
|
56
|
+
const redirectable = isolated
|
|
57
|
+
&& Boolean(runDir)
|
|
58
|
+
&& typeof output === "string"
|
|
59
|
+
&& output.length > 0
|
|
60
|
+
&& output !== "false"
|
|
61
|
+
&& output !== "true"
|
|
62
|
+
&& !path.isAbsolute(output);
|
|
63
|
+
if (!redirectable) return { path: resolveSingleOutputPath(output, ctxCwd, taskCwd) };
|
|
64
|
+
|
|
65
|
+
const leaf = path.join(runDir!, `task-${index}`);
|
|
66
|
+
const resolved = path.resolve(leaf, output as string);
|
|
67
|
+
const relative = path.relative(leaf, resolved);
|
|
68
|
+
if (relative === "" || relative.split(path.sep)[0] === ".." || path.isAbsolute(relative)) {
|
|
69
|
+
return { error: `Parallel task ${index + 1} output '${output}' resolves outside its per-task directory (${resolved}). Use a path inside ${leaf}, or an absolute path.` };
|
|
70
|
+
}
|
|
71
|
+
return { path: resolved };
|
|
72
|
+
}
|
|
73
|
+
|
|
43
74
|
export function injectSingleOutputInstruction(task: string, outputPath: string | undefined): string {
|
|
44
75
|
if (!outputPath) return task;
|
|
45
76
|
return `${task}\n\n---\n**Output:** Write your findings to: ${outputPath}`;
|
|
@@ -27,6 +27,7 @@ interface WorktreeDiff {
|
|
|
27
27
|
insertions: number;
|
|
28
28
|
deletions: number;
|
|
29
29
|
patchPath: string;
|
|
30
|
+
captureError?: string;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
interface WorktreeTaskCwdConflict {
|
|
@@ -523,24 +524,28 @@ export function createWorktrees(cwd: string, runId: string, count: number, optio
|
|
|
523
524
|
}
|
|
524
525
|
|
|
525
526
|
export function diffWorktrees(setup: WorktreeSetup, agents: string[], diffsDir: string): WorktreeDiff[] {
|
|
527
|
+
const agentFor = (index: number): string => agents[index] ?? `task-${index + 1}`;
|
|
526
528
|
try {
|
|
527
529
|
fs.mkdirSync(diffsDir, { recursive: true });
|
|
528
|
-
} catch {
|
|
529
|
-
|
|
530
|
-
return
|
|
530
|
+
} catch (error) {
|
|
531
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
532
|
+
return setup.worktrees.map((worktree, index) => ({
|
|
533
|
+
...emptyDiff(index, agentFor(index), worktree.branch, ""),
|
|
534
|
+
captureError: `could not create patch directory '${diffsDir}': ${message}`,
|
|
535
|
+
}));
|
|
531
536
|
}
|
|
532
537
|
|
|
533
538
|
const diffs: WorktreeDiff[] = [];
|
|
534
539
|
for (let index = 0; index < setup.worktrees.length; index++) {
|
|
535
540
|
const worktree = setup.worktrees[index]!;
|
|
536
|
-
const agent =
|
|
541
|
+
const agent = agentFor(index);
|
|
537
542
|
const patchPath = path.join(diffsDir, `task-${index}-${safePatchAgentName(agent)}.patch`);
|
|
538
543
|
try {
|
|
539
544
|
diffs.push(captureWorktreeDiff(setup, worktree, agent, patchPath));
|
|
540
|
-
} catch {
|
|
541
|
-
|
|
545
|
+
} catch (error) {
|
|
546
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
542
547
|
writeEmptyPatch(patchPath);
|
|
543
|
-
diffs.push(emptyDiff(index, agent, worktree.branch, patchPath));
|
|
548
|
+
diffs.push({ ...emptyDiff(index, agent, worktree.branch, patchPath), captureError: message });
|
|
544
549
|
}
|
|
545
550
|
}
|
|
546
551
|
|
|
@@ -558,7 +563,8 @@ export function cleanupWorktrees(setup: WorktreeSetup): void {
|
|
|
558
563
|
|
|
559
564
|
export function formatWorktreeDiffSummary(diffs: WorktreeDiff[]): string {
|
|
560
565
|
const changed = diffs.filter(hasWorktreeChanges);
|
|
561
|
-
|
|
566
|
+
const failed = diffs.filter((diff) => diff.captureError);
|
|
567
|
+
if (changed.length === 0 && failed.length === 0) return "";
|
|
562
568
|
|
|
563
569
|
const lines: string[] = ["=== Worktree Changes ===", ""];
|
|
564
570
|
for (const diff of changed) {
|
|
@@ -570,8 +576,11 @@ export function formatWorktreeDiffSummary(diffs: WorktreeDiff[]): string {
|
|
|
570
576
|
}
|
|
571
577
|
lines.push("");
|
|
572
578
|
}
|
|
579
|
+
for (const diff of failed) {
|
|
580
|
+
lines.push(`--- Task ${diff.index + 1} (${diff.agent}): patch capture FAILED: ${diff.captureError} ---`, "");
|
|
581
|
+
}
|
|
573
582
|
|
|
574
|
-
const
|
|
575
|
-
lines.push(`Full patches: ${
|
|
583
|
+
const withPatch = changed.find((diff) => diff.patchPath.length > 0);
|
|
584
|
+
if (withPatch) lines.push(`Full patches: ${path.dirname(withPatch.patchPath)}`);
|
|
576
585
|
return lines.join("\n").trimEnd();
|
|
577
586
|
}
|
package/src/shared/artifacts.ts
CHANGED
|
@@ -60,7 +60,8 @@ export function cleanupOldArtifacts(dir: string, maxAgeDays: number): void {
|
|
|
60
60
|
try {
|
|
61
61
|
const stat = fs.statSync(filePath);
|
|
62
62
|
if (stat.mtimeMs < cutoff) {
|
|
63
|
-
fs.
|
|
63
|
+
if (stat.isDirectory()) fs.rmSync(filePath, { recursive: true, force: true });
|
|
64
|
+
else fs.unlinkSync(filePath);
|
|
64
65
|
}
|
|
65
66
|
} catch {
|
|
66
67
|
// Artifact cleanup is best-effort housekeeping. Skip files that disappear
|