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 CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [7.4.0] - 2026-10-19
4
+
5
+ ### Added
6
+ - Chains can use `{outputs.name.treePath}` in a later child's `cwd` to review a worktree task's base commit plus captured patch in a lazy detached checkout. Trees are shared per named output and cleaned up at chain end; only `treePath` output references are allowed in `cwd`, and not on `worktree: true` steps.
7
+
8
+ ### Fixed
9
+ - Children with both acceptance and `outputSchema` are instructed to write their acceptance report before calling `structured_output`, so both contracts can be satisfied.
10
+
3
11
  ## [7.3.1] - 2026-10-18
4
12
 
5
13
  ### Fixed
package/README.md CHANGED
@@ -126,8 +126,8 @@ subagent({ async: false, chain: [
126
126
  { agent: "implementer", as: "impl_2", task: "Implement task 2", outputSchema: statusSchema },
127
127
  ], worktree: true, stopOn: { path: "/status", in: ["NEEDS_CONTEXT", "BLOCKED"] } },
128
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 },
129
+ { agent: "spec-reviewer", as: "sr_1", cwd: "{outputs.impl_1.treePath}", acceptance: { level: "attested" }, task: "Review {outputs.impl_1.patchPath}", outputSchema: verdictSchema },
130
+ { agent: "spec-reviewer", as: "sr_2", cwd: "{outputs.impl_2.treePath}", acceptance: { level: "attested" }, task: "Review {outputs.impl_2.patchPath}", outputSchema: verdictSchema },
131
131
  ], stopOn: { path: "/verdict", in: ["FAIL"] } },
132
132
  ]})
133
133
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cohort",
3
- "version": "7.3.1",
3
+ "version": "7.4.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",
@@ -446,13 +446,16 @@ export function executeAsyncChain(
446
446
  ...(s.model ? { model: s.model } : {}),
447
447
  };
448
448
  };
449
- const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior, isolatedLeaf?: { runDir: string; index: number }) => {
449
+ const buildSeqStep = (s: SequentialStep, sessionFile?: string, behaviorCwd?: string, progressPrecreated = false, resolvedBehavior?: ResolvedStepBehavior, isolatedLeaf?: { runDir: string; index: number }, groupCwd?: string) => {
450
450
  const a = agents.find((x) => x.name === s.agent)!;
451
- const stepCwd = resolveChildCwd(runnerCwd, s.cwd);
452
- const instructionCwd = behaviorCwd ?? stepCwd;
451
+ const effectiveCwd = s.cwd ?? groupCwd;
452
+ const templatedCwd = effectiveCwd?.includes("{outputs.");
453
+ const stepCwd = templatedCwd ? effectiveCwd : resolveChildCwd(runnerCwd, effectiveCwd);
454
+ const preparationCwd = templatedCwd ? runnerCwd : stepCwd;
455
+ const instructionCwd = behaviorCwd ?? preparationCwd;
453
456
  const behavior = suppressProgressForReadOnlyTask(resolvedBehavior ?? resolveStepBehavior(a, buildStepOverrides(s), chainSkills), s.task, originalTask);
454
457
  const skillNames = behavior.skills === false ? [] : behavior.skills;
455
- const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(skillNames, stepCwd, ctx.cwd);
458
+ const { resolved: resolvedSkills, missing: missingSkills } = resolveSkillsWithFallback(skillNames, preparationCwd, ctx.cwd);
456
459
  if (missingSkills.includes("pi-cohort")) throw new UnavailableSubagentSkillError(UNAVAILABLE_SUBAGENT_SKILL_ERROR);
457
460
 
458
461
  let systemPrompt = a.systemPrompt?.trim() ?? "";
@@ -536,13 +539,14 @@ export function executeAsyncChain(
536
539
  try {
537
540
  steps = chain.map((s, stepIndex) => {
538
541
  if (isParallelStep(s)) {
542
+ const groupCwd = s.cwd?.includes("{outputs.") ? runnerCwd : resolveChildCwd(runnerCwd, s.cwd);
539
543
  const parallelBehaviors = s.parallel.map((task) => {
540
544
  const agent = agents.find((candidate) => candidate.name === task.agent)!;
541
545
  return suppressProgressForReadOnlyTask(resolveStepBehavior(agent, buildStepOverrides(task), chainSkills), task.task, originalTask);
542
546
  });
543
547
  const progressPrecreated = parallelBehaviors.some((behavior) => behavior.progress);
544
548
  if (progressPrecreated) {
545
- if (!s.worktree) writeInitialProgressFile(runnerCwd);
549
+ if (!s.worktree) writeInitialProgressFile(groupCwd);
546
550
  progressInstructionCreated = true;
547
551
  }
548
552
  return {
@@ -550,7 +554,7 @@ export function executeAsyncChain(
550
554
  let behaviorCwd: string | undefined;
551
555
  if (s.worktree) {
552
556
  try {
553
- behaviorCwd = resolveExpectedWorktreeAgentCwd(runnerCwd, `${id}-s${stepIndex}`, taskIndex);
557
+ behaviorCwd = resolveExpectedWorktreeAgentCwd(groupCwd, `${id}-s${stepIndex}`, taskIndex);
554
558
  } catch {
555
559
  behaviorCwd = undefined;
556
560
  }
@@ -562,8 +566,10 @@ export function executeAsyncChain(
562
566
  progressPrecreated,
563
567
  parallelBehaviors[taskIndex],
564
568
  s.worktree ? { runDir: path.join(asyncDir, `step-${stepIndex}`), index: taskIndex } : undefined,
569
+ s.cwd,
565
570
  );
566
571
  }),
572
+ cwd: s.cwd,
567
573
  concurrency: s.concurrency,
568
574
  failFast: s.failFast,
569
575
  worktree: s.worktree,
@@ -46,14 +46,14 @@ import {
46
46
  MAX_PARALLEL_CONCURRENCY,
47
47
  } from "../shared/parallel-utils.ts";
48
48
  import { buildPiArgs, cleanupTempDir, runDirEnv } from "../shared/pi-args.ts";
49
- import { ChainOutputValidationError, outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
49
+ import { ChainOutputValidationError, TreeRegistry, outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
50
50
  import { buildStopRecord, evaluateStopCondition } from "../shared/stop-conditions.ts";
51
51
  import { createStructuredOutputRuntime, readStructuredOutput } from "../shared/structured-output.ts";
52
52
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
53
53
  import { nestedSummaryFromAsyncStatus, writeNestedEvent } from "../shared/nested-events.ts";
54
54
  import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
55
55
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
56
- import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "../../shared/utils.ts";
56
+ import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput, resolveChildCwd } from "../../shared/utils.ts";
57
57
  import { blockedLine, evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
58
58
  import {
59
59
  createMutatingFailureState,
@@ -70,6 +70,7 @@ import { parseSessionTokens } from "../../shared/session-tokens.ts";
70
70
  import type { TokenUsage } from "../../shared/types.ts";
71
71
  import {
72
72
  cleanupWorktrees,
73
+ removeTrees,
73
74
  createWorktrees,
74
75
  diffWorktrees,
75
76
  findWorktreeTaskCwdConflict,
@@ -570,6 +571,7 @@ function writeRunLog(
570
571
  export interface SingleStepContext {
571
572
  previousOutput: string;
572
573
  outputs?: ChainOutputMap;
574
+ trees?: TreeRegistry;
573
575
  placeholder: string;
574
576
  cwd: string;
575
577
  sessionEnabled: boolean;
@@ -612,10 +614,20 @@ export async function runSingleStep(
612
614
  acceptance?: import("../../shared/types.ts").AcceptanceLedger;
613
615
  executionSurface?: ExecutionSurfaceResult;
614
616
  }> {
617
+ let childCwd: string;
618
+ const placeholderRegex = new RegExp(ctx.placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
619
+ let task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
620
+ try {
621
+ childCwd = resolveChildCwd(ctx.cwd, resolveOutputReferences(step.cwd ?? ctx.cwd, ctx.outputs ?? {}, ctx.trees));
622
+ task = resolveOutputReferences(task, ctx.outputs ?? {}, ctx.trees);
623
+ } catch (error) {
624
+ if (!(error instanceof ChainOutputValidationError)) throw error;
625
+ return { agent: step.agent, output: error.message, exitCode: 1, error: error.message, sessionFile: step.sessionFile };
626
+ }
615
627
  let backendSelection: import("../../execution-backend/selection.ts").ExecutionBackendSelectionResult;
616
628
  try {
617
629
  backendSelection = await selectConfiguredExecutionBackend({
618
- cwd: step.cwd ?? ctx.cwd,
630
+ cwd: childCwd,
619
631
  userConfig: { executionBackend: ctx.executionBackendConfig?.userPreference },
620
632
  preparation: ctx.executionBackendCoordinator,
621
633
  });
@@ -635,17 +647,9 @@ export async function runSingleStep(
635
647
  const effectiveStructuredOutput = step.structuredOutput ?? (step.structuredOutputSchema
636
648
  ? createStructuredOutputRuntime(step.structuredOutputSchema, path.join(path.dirname(ctx.outputFile), "structured-output"))
637
649
  : undefined);
638
- const placeholderRegex = new RegExp(ctx.placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
639
- let task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
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
- }
646
650
  const taskForCompletionGuard = task;
647
651
  if (step.effectiveAcceptance) {
648
- const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance);
652
+ const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance, { structuredOutput: Boolean(effectiveStructuredOutput) });
649
653
  if (acceptancePrompt) task = `${task}\n${acceptancePrompt}`;
650
654
  }
651
655
  const sessionEnabled = Boolean(step.sessionFile) || ctx.sessionEnabled;
@@ -689,7 +693,7 @@ export async function runSingleStep(
689
693
  runId: ctx.id,
690
694
  childId: `${step.agent}-${ctx.flatIndex}`,
691
695
  backend: selection.backend,
692
- cwd: step.cwd ?? ctx.cwd,
696
+ cwd: childCwd,
693
697
  title: step.agent,
694
698
  signal: ownerController.signal,
695
699
  });
@@ -735,7 +739,7 @@ export async function runSingleStep(
735
739
  extensions: step.extensions,
736
740
  systemPrompt: step.systemPrompt,
737
741
  systemPromptMode: step.systemPromptMode,
738
- cwd: step.cwd ?? ctx.cwd,
742
+ cwd: childCwd,
739
743
  promptFileStem: step.agent,
740
744
  runId: ctx.id,
741
745
  childAgentName: step.agent,
@@ -760,7 +764,7 @@ export async function runSingleStep(
760
764
  attemptId: `${ctx.id}-${ctx.flatIndex}-attempt-${index}`,
761
765
  args,
762
766
  environment: mergedEnv,
763
- cwd: step.cwd ?? ctx.cwd,
767
+ cwd: childCwd,
764
768
  sessionFile: attemptSessionFile,
765
769
  interruptSignal: interruptController.signal,
766
770
  onChildEvent: ctx.onChildEvent,
@@ -881,7 +885,7 @@ export async function runSingleStep(
881
885
  extensions: step.extensions,
882
886
  systemPrompt: step.systemPrompt,
883
887
  systemPromptMode: step.systemPromptMode,
884
- cwd: step.cwd ?? ctx.cwd,
888
+ cwd: childCwd,
885
889
  promptFileStem: step.agent,
886
890
  runId: ctx.id,
887
891
  childAgentName: step.agent,
@@ -897,7 +901,7 @@ export async function runSingleStep(
897
901
  try {
898
902
  run = await runPiStreaming(
899
903
  args,
900
- step.cwd ?? ctx.cwd,
904
+ childCwd,
901
905
  ctx.outputFile,
902
906
  env,
903
907
  ctx.runtime,
@@ -1014,7 +1018,7 @@ export async function runSingleStep(
1014
1018
  ? await evaluateAcceptance({
1015
1019
  acceptance: step.effectiveAcceptance,
1016
1020
  output: outputForAcceptance,
1017
- cwd: step.cwd ?? ctx.cwd,
1021
+ cwd: childCwd,
1018
1022
  })
1019
1023
  : undefined;
1020
1024
  const acceptanceFailure = acceptance ? acceptanceFailureMessage(acceptance) : undefined;
@@ -1204,8 +1208,9 @@ function prepareParallelTaskRun(
1204
1208
  cwd: string,
1205
1209
  worktreeSetup: WorktreeSetup | undefined,
1206
1210
  taskIndex: number,
1211
+ groupCwd?: string,
1207
1212
  ): { taskForRun: SubagentStep; taskCwd: string } {
1208
- if (!worktreeSetup) return { taskForRun: task, taskCwd: cwd };
1213
+ if (!worktreeSetup) return { taskForRun: { ...task, cwd: task.cwd ?? groupCwd }, taskCwd: cwd };
1209
1214
  return {
1210
1215
  taskForRun: { ...task, cwd: undefined },
1211
1216
  taskCwd: worktreeSetup.worktrees[taskIndex]!.agentCwd,
@@ -1251,6 +1256,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1251
1256
  const overallStartTime = Date.now();
1252
1257
  const shareEnabled = config.share === true;
1253
1258
  const asyncDir = config.asyncDir;
1259
+ const trees = new TreeRegistry(asyncDir);
1254
1260
  const executionBackendConfig = config.executionBackends ?? {
1255
1261
  protocolVersion: 1 as const,
1256
1262
  registrations: [],
@@ -1669,7 +1675,6 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1669
1675
  if (interrupted || statusPayload.state !== "running") return;
1670
1676
  interrupted = true;
1671
1677
  const now = Date.now();
1672
- statusPayload.state = "paused";
1673
1678
  currentActivityState = undefined;
1674
1679
  statusPayload.activityState = undefined;
1675
1680
  statusPayload.lastUpdate = now;
@@ -1683,11 +1688,6 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1683
1688
  }
1684
1689
  }
1685
1690
  writeStatusPayload();
1686
- appendJsonl(eventsPath, JSON.stringify({
1687
- type: "subagent.run.paused",
1688
- ts: now,
1689
- runId: id,
1690
- }));
1691
1691
  activeChildInterrupt?.();
1692
1692
  };
1693
1693
  process.on(ASYNC_INTERRUPT_SIGNAL, interruptRunner);
@@ -1705,6 +1705,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1705
1705
 
1706
1706
  let flatIndex = 0;
1707
1707
 
1708
+ try {
1708
1709
  for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
1709
1710
  if (interrupted) break;
1710
1711
  const step = steps[stepIndex];
@@ -1884,6 +1885,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1884
1885
  const singleResult = await runSingleStep(task, {
1885
1886
  previousOutput, placeholder, cwd, sessionEnabled,
1886
1887
  outputs,
1888
+ trees,
1887
1889
  sessionDir: config.sessionDir ? path.join(config.sessionDir, `dynamic-${stepIndex}-${taskIdx}`) : undefined,
1888
1890
  artifactsDir, artifactConfig, id,
1889
1891
  flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
@@ -2022,13 +2024,14 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2022
2024
 
2023
2025
  if (isParallelGroup(step)) {
2024
2026
  const group = step;
2027
+ const groupCwd = group.cwd?.includes("{outputs.") ? group.cwd : resolveChildCwd(cwd, group.cwd);
2025
2028
  const concurrency = group.concurrency ?? MAX_PARALLEL_CONCURRENCY;
2026
2029
  const failFast = group.failFast ?? false;
2027
2030
  const groupStartFlatIndex = flatIndex;
2028
2031
  let aborted = false;
2029
2032
  let worktreeSetup: WorktreeSetup | undefined;
2030
2033
  if (group.worktree) {
2031
- const worktreeTaskCwdConflict = findWorktreeTaskCwdConflict(group.parallel, cwd);
2034
+ const worktreeTaskCwdConflict = findWorktreeTaskCwdConflict(group.parallel, groupCwd);
2032
2035
  if (worktreeTaskCwdConflict) {
2033
2036
  const failedAt = Date.now();
2034
2037
  markParallelGroupSetupFailure({
@@ -2036,7 +2039,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2036
2039
  results,
2037
2040
  group,
2038
2041
  groupStartFlatIndex,
2039
- setupError: formatWorktreeTaskCwdConflict(worktreeTaskCwdConflict, cwd),
2042
+ setupError: formatWorktreeTaskCwdConflict(worktreeTaskCwdConflict, groupCwd),
2040
2043
  failedAt,
2041
2044
  statusPath,
2042
2045
  eventsPath,
@@ -2048,7 +2051,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2048
2051
  break;
2049
2052
  }
2050
2053
  try {
2051
- worktreeSetup = createWorktrees(cwd, `${id}-s${stepIndex}`, group.parallel.length, {
2054
+ worktreeSetup = createWorktrees(groupCwd, `${id}-s${stepIndex}`, group.parallel.length, {
2052
2055
  agents: group.parallel.map((task) => task.agent),
2053
2056
  setupHook: config.worktreeSetupHook
2054
2057
  ? { hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs }
@@ -2076,7 +2079,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2076
2079
  }
2077
2080
 
2078
2081
  try {
2079
- if (group.worktree) ensureParallelProgressFile(cwd, group);
2082
+ if (group.worktree) ensureParallelProgressFile(groupCwd, group);
2080
2083
  const groupStartTime = Date.now();
2081
2084
  markParallelGroupRunning({
2082
2085
  statusPayload,
@@ -2133,11 +2136,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2133
2136
  const taskSessionDir = config.sessionDir
2134
2137
  ? path.join(config.sessionDir, `parallel-${fi}`)
2135
2138
  : undefined;
2136
- const { taskForRun, taskCwd } = prepareParallelTaskRun(task, cwd, worktreeSetup, taskIdx);
2139
+ const { taskForRun, taskCwd } = prepareParallelTaskRun(task, cwd, worktreeSetup, taskIdx, group.cwd);
2137
2140
 
2138
2141
  const singleResult = await runSingleStep(taskForRun, {
2139
2142
  previousOutput, placeholder, cwd: taskCwd, sessionEnabled,
2140
2143
  outputs,
2144
+ trees,
2141
2145
  sessionDir: taskSessionDir,
2142
2146
  artifactsDir, artifactConfig, id,
2143
2147
  flatIndex: fi, flatStepCount: flatSteps.length,
@@ -2247,7 +2251,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2247
2251
  agent: parallelResults[t]!.agent,
2248
2252
  output: parallelResults[t]!.output,
2249
2253
  structuredOutput: parallelResults[t]!.structuredOutput,
2250
- }, stepIndex, patchInfoForDiff(diffs[t]));
2254
+ }, stepIndex, worktreeSetup ? {
2255
+ ...patchInfoForDiff(diffs[t]),
2256
+ taskIndex: t,
2257
+ baseCommit: worktreeSetup.baseCommit,
2258
+ repoCwd: worktreeSetup.cwd,
2259
+ } : undefined);
2251
2260
  }
2252
2261
  statusPayload.outputs = outputs;
2253
2262
  if (!interrupted && group.stopOn) {
@@ -2310,6 +2319,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2310
2319
  const singleResult = await runSingleStep(seqStep, {
2311
2320
  previousOutput, placeholder, cwd, sessionEnabled,
2312
2321
  outputs,
2322
+ trees,
2313
2323
  sessionDir: config.sessionDir ? path.join(config.sessionDir, `parallel-${flatIndex}`) : undefined,
2314
2324
  artifactsDir, artifactConfig, id,
2315
2325
  flatIndex, flatStepCount: flatSteps.length,
@@ -2423,6 +2433,18 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2423
2433
  }
2424
2434
  }
2425
2435
 
2436
+ } finally {
2437
+ removeTrees(trees.created);
2438
+ }
2439
+
2440
+ if (interrupted) {
2441
+ const pausedAt = Date.now();
2442
+ statusPayload.state = "paused";
2443
+ statusPayload.lastUpdate = pausedAt;
2444
+ writeStatusPayload();
2445
+ appendJsonl(eventsPath, JSON.stringify({ type: "subagent.run.paused", ts: pausedAt, runId: id }));
2446
+ }
2447
+
2426
2448
  let summary = results.map((r) => `${r.agent}:\n${r.output}`).join("\n\n");
2427
2449
  let truncated = false;
2428
2450