pi-subagents 0.40.0 → 0.41.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.
Files changed (119) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +246 -525
  3. package/agents/oracle.md +1 -0
  4. package/package.json +12 -4
  5. package/prompts/parallel-context-build.md +1 -1
  6. package/prompts/parallel-handoff-plan.md +1 -1
  7. package/prompts/review-loop.md +1 -1
  8. package/skills/pi-subagents/SKILL.md +6 -6
  9. package/skills/pi-subagents/references/constraints-and-recipes.md +19 -26
  10. package/skills/pi-subagents/references/execution-controls.md +98 -97
  11. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  12. package/skills/pi-subagents/references/prompting-and-roles.md +18 -27
  13. package/src/agents/agent-management.ts +155 -65
  14. package/src/agents/agent-serializer.ts +19 -0
  15. package/src/agents/agents.ts +154 -71
  16. package/src/agents/chain-serializer.ts +10 -7
  17. package/src/agents/frontmatter.ts +5 -3
  18. package/src/agents/identity.ts +1 -1
  19. package/src/agents/proactive-skills.ts +13 -10
  20. package/src/agents/skills.ts +23 -6
  21. package/src/api/control-channel.ts +4 -0
  22. package/src/api/delegation.ts +26 -194
  23. package/src/api/external-runs.ts +129 -0
  24. package/src/api/intercom-bridge.ts +3 -0
  25. package/src/api/pi-args.ts +5 -0
  26. package/src/api/preflight.ts +3 -3
  27. package/src/api/shared-types.ts +19 -0
  28. package/src/extension/config.ts +10 -0
  29. package/src/extension/control-notices.ts +5 -39
  30. package/src/extension/doctor.ts +10 -9
  31. package/src/extension/fanout-child.ts +7 -4
  32. package/src/extension/index.ts +232 -68
  33. package/src/extension/rpc.ts +18 -12
  34. package/src/extension/schemas.ts +48 -37
  35. package/src/extension/tool-description.ts +36 -86
  36. package/src/inspectors/herdr/actions.ts +229 -0
  37. package/src/inspectors/herdr/client.ts +130 -0
  38. package/src/inspectors/herdr/inspector-runner.ts +141 -0
  39. package/src/inspectors/herdr/project-panes.ts +154 -0
  40. package/src/integrations/herdr-status.ts +330 -0
  41. package/src/intercom/intercom-bridge.ts +3 -2
  42. package/src/intercom/result-intercom.ts +5 -1
  43. package/src/missions/actions.ts +372 -0
  44. package/src/missions/lifecycle.ts +314 -0
  45. package/src/missions/store.ts +442 -0
  46. package/src/missions/types.ts +135 -0
  47. package/src/policy/authority.ts +46 -0
  48. package/src/profiles/profiles.ts +29 -3
  49. package/src/runs/background/async-execution.ts +98 -49
  50. package/src/runs/background/async-job-tracker.ts +10 -2
  51. package/src/runs/background/async-resume.ts +6 -6
  52. package/src/runs/background/async-status.ts +29 -1
  53. package/src/runs/background/auto-drain.ts +3 -3
  54. package/src/runs/background/chain-append.ts +3 -2
  55. package/src/runs/background/control-channel.ts +9 -7
  56. package/src/runs/background/fleet-view.ts +3 -4
  57. package/src/runs/background/notify.ts +2 -1
  58. package/src/runs/background/process-terminal.ts +5 -5
  59. package/src/runs/background/result-watcher.ts +13 -5
  60. package/src/runs/background/run-id-resolver.ts +3 -3
  61. package/src/runs/background/run-status.ts +35 -8
  62. package/src/runs/background/scheduled-runs.ts +602 -375
  63. package/src/runs/background/stale-run-reconciler.ts +3 -3
  64. package/src/runs/background/subagent-runner.ts +608 -445
  65. package/src/runs/background/subagent-wait.ts +50 -9
  66. package/src/runs/background/wait-subscriptions.ts +253 -0
  67. package/src/runs/background/wait-tool.ts +12 -4
  68. package/src/runs/foreground/async-steering-action.ts +3 -3
  69. package/src/runs/foreground/chain-clarify.ts +8 -4
  70. package/src/runs/foreground/chain-execution.ts +56 -30
  71. package/src/runs/foreground/execution.ts +15 -2
  72. package/src/runs/foreground/subagent-executor.ts +1023 -273
  73. package/src/runs/shared/acceptance.ts +28 -6
  74. package/src/runs/shared/child-protocol.ts +302 -22
  75. package/src/runs/shared/dynamic-fanout.ts +1 -1
  76. package/src/runs/shared/external-cli-runner.ts +130 -0
  77. package/src/runs/shared/long-running-guard.ts +42 -1
  78. package/src/runs/shared/nested-events.ts +59 -5
  79. package/src/runs/shared/nested-render.ts +9 -4
  80. package/src/runs/shared/parallel-handoff.ts +86 -2
  81. package/src/runs/shared/parallel-utils.ts +11 -2
  82. package/src/runs/shared/permissions.ts +95 -0
  83. package/src/runs/shared/pi-args.ts +11 -1
  84. package/src/runs/shared/pi-spawn.ts +11 -1
  85. package/src/runs/shared/run-history.ts +1 -1
  86. package/src/runs/shared/subagent-prompt-runtime.ts +36 -5
  87. package/src/runs/shared/subagent-startup-retry.ts +5 -2
  88. package/src/runs/shared/turn-budget.ts +6 -6
  89. package/src/runs/shared/worktree.ts +122 -12
  90. package/src/shared/accessible-dir.ts +29 -7
  91. package/src/shared/artifacts.ts +18 -1
  92. package/src/shared/fork-context.ts +3 -2
  93. package/src/shared/launch-contract.ts +1 -0
  94. package/src/shared/settings.ts +10 -0
  95. package/src/shared/types.ts +156 -14
  96. package/src/shared/utils.ts +8 -6
  97. package/src/slash/delegation-adapters.ts +32 -194
  98. package/src/slash/delegation-request.ts +43 -126
  99. package/src/slash/prompt-template-bridge.ts +158 -205
  100. package/src/slash/prompt-workflows.ts +21 -57
  101. package/src/slash/slash-bridge.ts +14 -0
  102. package/src/slash/slash-commands.ts +31 -632
  103. package/src/slash/subagents-admin.ts +18 -14
  104. package/src/tui/fleet-status.ts +156 -21
  105. package/src/tui/fleet-transcript.ts +110 -5
  106. package/src/tui/fleet.ts +56 -24
  107. package/src/tui/render.ts +291 -109
  108. package/src/types/pi-runtime-compat.d.ts +14 -0
  109. package/src/watchdog/lsp-diagnostics.ts +12 -7
  110. package/src/watchdog/model-selection.ts +2 -2
  111. package/src/watchdog/permission-arbiter.ts +145 -0
  112. package/src/watchdog/register-child.ts +1 -1
  113. package/src/watchdog/register-main.ts +1 -1
  114. package/src/watchdog/review.ts +4 -1
  115. package/src/watchdog/runtime.ts +3 -2
  116. package/src/workflows/chat-progress.ts +140 -0
  117. package/src/workflows/scripted-workflow.ts +415 -0
  118. package/agents/advisor.md +0 -73
  119. package/src/extension/chain-validation.ts +0 -181
@@ -1,4 +1,5 @@
1
1
  import type { Usage } from "../../shared/types.ts";
2
+ import { formatProcessSignalError } from "./process-signal.ts";
2
3
 
3
4
  /**
4
5
  * Retry delays for child processes that exit before any model or tool activity.
@@ -10,6 +11,8 @@ export const SUBAGENT_STARTUP_RETRY_DELAYS_MS = [250, 750, 1500] as const;
10
11
  /** A genuine startup race should fail well before a model request can complete. */
11
12
  export const MAX_SUBAGENT_STARTUP_FAILURE_DURATION_MS = 2000;
12
13
 
14
+ const SIGKILL_PROCESS_SIGNAL_ERROR = formatProcessSignalError("SIGKILL");
15
+
13
16
  export interface SubagentStartupFailureEvidence {
14
17
  exitCode: number | null | undefined;
15
18
  error?: string;
@@ -46,7 +49,7 @@ export function isRetryableSubagentStartupFailure(evidence: SubagentStartupFailu
46
49
  return evidence.exitCode !== undefined
47
50
  && evidence.exitCode !== null
48
51
  && evidence.exitCode !== 0
49
- && !evidence.error?.trim()
52
+ && (!evidence.error?.trim() || evidence.error === SIGKILL_PROCESS_SIGNAL_ERROR)
50
53
  && !evidence.finalOutput?.trim()
51
54
  && evidence.messageCount === 0
52
55
  && evidence.toolCount === 0
@@ -54,7 +57,7 @@ export function isRetryableSubagentStartupFailure(evidence: SubagentStartupFailu
54
57
  && evidence.durationMs >= 0
55
58
  && evidence.durationMs <= MAX_SUBAGENT_STARTUP_FAILURE_DURATION_MS
56
59
  && evidence.protocolError === undefined
57
- && (evidence.processSignal === undefined || evidence.processSignal === null)
60
+ && (evidence.processSignal === undefined || evidence.processSignal === null || evidence.processSignal === "SIGKILL")
58
61
  && evidence.observedMutationAttempt !== true
59
62
  && evidence.detached !== true
60
63
  && evidence.interrupted !== true
@@ -23,9 +23,10 @@ export function resolveTurnBudgetConfig(
23
23
  return { turnBudget: { maxTurns: budget.maxTurns, graceTurns } };
24
24
  }
25
25
 
26
- export function appendTurnBudgetSystemPrompt(systemPrompt: string, budget: ResolvedTurnBudget | undefined): string {
26
+ export function appendTurnBudgetSystemPrompt(systemPrompt: string, budget: { maxTurns: number; graceTurns?: number } | undefined): string {
27
27
  if (!budget) return systemPrompt;
28
- const grace = budget.graceTurns === 1 ? "1 additional assistant turn" : `${budget.graceTurns} additional assistant turns`;
28
+ const graceTurns = budget.graceTurns ?? DEFAULT_TURN_BUDGET_GRACE_TURNS;
29
+ const grace = graceTurns === 1 ? "1 additional assistant turn" : `${graceTurns} additional assistant turns`;
29
30
  const block = [
30
31
  "## Turn budget",
31
32
  `This child run has a soft budget of ${budget.maxTurns} assistant turn${budget.maxTurns === 1 ? "" : "s"}.`,
@@ -55,8 +56,8 @@ export function formatTurnBudgetOutput(message: string, output: string): string
55
56
  : message;
56
57
  }
57
58
 
58
- export function initialTurnBudgetState(budget: ResolvedTurnBudget): TurnBudgetState {
59
- return { ...budget, outcome: "within-budget", turnCount: 0 };
59
+ export function initialTurnBudgetState(budget: { maxTurns: number; graceTurns?: number }): TurnBudgetState {
60
+ return { maxTurns: budget.maxTurns, graceTurns: budget.graceTurns ?? DEFAULT_TURN_BUDGET_GRACE_TURNS, outcome: "within-budget", turnCount: 0 };
60
61
  }
61
62
 
62
63
  export function turnBudgetState(budget: ResolvedTurnBudget, turnCount: number, exceeded: boolean): TurnBudgetState {
@@ -91,8 +92,7 @@ export function turnBudgetDecision(
91
92
  enforceHardLimit = false,
92
93
  ): "continue" | "defer" | "abort" {
93
94
  const hardLimit = budget.maxTurns + budget.graceTurns;
94
- if (turnCount < hardLimit) return "continue";
95
+ if (terminalAssistantStop || turnCount < hardLimit) return "continue";
95
96
  if (toolWorkActiveOrStarting && !enforceHardLimit) return "defer";
96
- if (turnCount === hardLimit && terminalAssistantStop) return "continue";
97
97
  return "abort";
98
98
  }
@@ -2,14 +2,16 @@ import { spawnSync } from "node:child_process";
2
2
  import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
+ import { resolveAuthorityDecision, type AuthorityPolicyConfig } from "../../policy/authority.ts";
5
6
 
6
7
  export interface WorktreeSetup {
7
8
  cwd: string;
8
9
  worktrees: WorktreeInfo[];
9
10
  baseCommit: string;
11
+ capturedDiffs?: WorktreeDiff[];
10
12
  }
11
13
 
12
- interface WorktreeInfo {
14
+ export interface WorktreeInfo {
13
15
  path: string;
14
16
  agentCwd: string;
15
17
  branch: string;
@@ -36,9 +38,21 @@ export interface WorktreeCleanupTask {
36
38
  branch: string;
37
39
  worktreeRemoved: boolean;
38
40
  branchRemoved: boolean;
41
+ preserved?: boolean;
42
+ reason?: string;
39
43
  errors?: string[];
40
44
  }
41
45
 
46
+ export type WorktreeCleanupIntent =
47
+ | { kind: "preserve"; capturedDiffs?: WorktreeDiff[]; handoffManifestPath?: string }
48
+ | {
49
+ kind: "discard";
50
+ authorization:
51
+ | { kind: "policy"; policy?: AuthorityPolicyConfig }
52
+ | { kind: "confirmed"; policy?: AuthorityPolicyConfig };
53
+ }
54
+ | { kind: "setup-rollback" };
55
+
42
56
  export interface WorktreeCleanupReport {
43
57
  state: "complete" | "partial";
44
58
  tasks: WorktreeCleanupTask[];
@@ -121,7 +135,9 @@ function resolveRepoState(cwd: string): RepoState {
121
135
  const cwdRelative = resolveRepoCwdRelative(cwd);
122
136
  const toplevel = runGitChecked(cwd, ["rev-parse", "--show-toplevel"]).trim();
123
137
 
124
- const status = runGitChecked(toplevel, ["status", "--porcelain"]);
138
+ // pi-subagents writes durable runtime state under .pi-subagents/ by default;
139
+ // that state must not make managed isolation unusable for later runs.
140
+ const status = runGitChecked(toplevel, ["status", "--porcelain", "--", ":!.pi-subagents"]);
125
141
  if (status.trim().length > 0) {
126
142
  throw new Error("worktree isolation requires a clean git working tree. Commit or stash changes first.");
127
143
  }
@@ -515,21 +531,111 @@ function writeEmptyPatch(patchPath: string): void {
515
531
  }
516
532
  }
517
533
 
518
- function cleanupSingleWorktree(repoCwd: string, worktree: WorktreeInfo): WorktreeCleanupTask {
534
+ function handoffRecordsPatch(manifestPath: string | undefined, patchPath: string): boolean {
535
+ if (!manifestPath || !fs.existsSync(manifestPath)) return false;
536
+ try {
537
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as {
538
+ version?: unknown;
539
+ groups?: Array<{ children?: Array<{ patch?: { path?: unknown; error?: unknown } }> }>;
540
+ };
541
+ if (manifest.version !== 1 || !Array.isArray(manifest.groups)) return false;
542
+ const resolvedPatchPath = path.resolve(patchPath);
543
+ return manifest.groups.some((group) => Array.isArray(group.children) && group.children.some((child) =>
544
+ child.patch?.error === undefined
545
+ && typeof child.patch?.path === "string"
546
+ && path.resolve(child.patch.path) === resolvedPatchPath,
547
+ ));
548
+ } catch {
549
+ return false;
550
+ }
551
+ }
552
+
553
+ function cleanupSingleWorktree(
554
+ setup: WorktreeSetup,
555
+ worktree: WorktreeInfo,
556
+ intent: WorktreeCleanupIntent,
557
+ ): WorktreeCleanupTask {
519
558
  const errors: string[] = [];
520
559
  let worktreeRemoved = false;
521
560
  let branchRemoved = false;
561
+ if (intent.kind !== "setup-rollback") {
562
+ try {
563
+ removeSyntheticPathsBeforeDiff(worktree);
564
+ } catch (error) {
565
+ errors.push(`synthetic path cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
566
+ }
567
+ const status = runGit(worktree.path, ["status", "--porcelain"]);
568
+ const baseDiff = runGit(worktree.path, ["diff", "--quiet", setup.baseCommit, "--"]);
569
+ if (status.status !== 0 || (baseDiff.status !== 0 && baseDiff.status !== 1)) {
570
+ const reason = status.status !== 0
571
+ ? status.stderr.trim() || status.stdout.trim() || "git status failed"
572
+ : baseDiff.stderr.trim() || baseDiff.stdout.trim() || "git diff check failed";
573
+ return {
574
+ index: worktree.index,
575
+ path: worktree.path,
576
+ branch: worktree.branch,
577
+ worktreeRemoved: false,
578
+ branchRemoved: false,
579
+ preserved: true,
580
+ reason: "cleanup safety check failed",
581
+ errors: [...errors, `cleanup refused: ${reason}`],
582
+ };
583
+ }
584
+ const hasWork = status.stdout.trim().length > 0 || baseDiff.status === 1;
585
+ if (hasWork && intent.kind === "preserve") {
586
+ const captured = (intent.capturedDiffs ?? setup.capturedDiffs)?.find((diff) => diff.index === worktree.index);
587
+ const patchCaptured = captured !== undefined
588
+ && captured.error === undefined
589
+ && fs.existsSync(captured.patchPath)
590
+ && fs.statSync(captured.patchPath).size > 0
591
+ && handoffRecordsPatch(intent.handoffManifestPath, captured.patchPath);
592
+ if (!patchCaptured) {
593
+ const reason = "worktree contains changes that are not represented by a captured handoff patch";
594
+ return {
595
+ index: worktree.index,
596
+ path: worktree.path,
597
+ branch: worktree.branch,
598
+ worktreeRemoved: false,
599
+ branchRemoved: false,
600
+ preserved: true,
601
+ reason,
602
+ errors: [...errors, `cleanup refused: ${reason}; preserved ${worktree.path}`],
603
+ };
604
+ }
605
+ }
606
+ if (hasWork && intent.kind === "discard") {
607
+ const decision = resolveAuthorityDecision({ action: "discardWorktree", policy: intent.authorization.policy });
608
+ const authorized = decision === "auto" || (decision === "confirm" && intent.authorization.kind === "confirmed");
609
+ if (!authorized) {
610
+ const reason = decision === "forbid"
611
+ ? "authority policy forbids worktree discard"
612
+ : "worktree discard requires explicit user confirmation";
613
+ return {
614
+ index: worktree.index,
615
+ path: worktree.path,
616
+ branch: worktree.branch,
617
+ worktreeRemoved: false,
618
+ branchRemoved: false,
619
+ preserved: true,
620
+ reason,
621
+ errors: [...errors, `cleanup refused: ${reason}; preserved ${worktree.path}`],
622
+ };
623
+ }
624
+ }
625
+ }
522
626
  try {
523
- runGitChecked(repoCwd, ["worktree", "remove", "--force", worktree.path]);
627
+ runGitChecked(setup.cwd, ["worktree", "remove", "--force", worktree.path]);
524
628
  worktreeRemoved = true;
525
629
  } catch (error) {
526
630
  errors.push(`worktree removal failed: ${error instanceof Error ? error.message : String(error)}`);
527
631
  }
528
- try {
529
- runGitChecked(repoCwd, ["branch", "-D", worktree.branch]);
530
- branchRemoved = true;
531
- } catch (error) {
532
- errors.push(`branch removal failed: ${error instanceof Error ? error.message : String(error)}`);
632
+ if (worktreeRemoved) {
633
+ try {
634
+ runGitChecked(setup.cwd, ["branch", "-D", worktree.branch]);
635
+ branchRemoved = true;
636
+ } catch (error) {
637
+ errors.push(`branch removal failed: ${error instanceof Error ? error.message : String(error)}`);
638
+ }
533
639
  }
534
640
  return {
535
641
  index: worktree.index,
@@ -569,7 +675,7 @@ export function createWorktrees(cwd: string, runId: string, count: number, optio
569
675
  cwd: repo.toplevel,
570
676
  worktrees,
571
677
  baseCommit: repo.baseCommit,
572
- });
678
+ }, { kind: "setup-rollback" });
573
679
  throw error;
574
680
  }
575
681
 
@@ -602,13 +708,17 @@ export function diffWorktrees(setup: WorktreeSetup, agents: string[], diffsDir:
602
708
  }
603
709
  }
604
710
 
711
+ setup.capturedDiffs = diffs;
605
712
  return diffs;
606
713
  }
607
714
 
608
- export function cleanupWorktrees(setup: WorktreeSetup): WorktreeCleanupReport {
715
+ export function cleanupWorktrees(
716
+ setup: WorktreeSetup,
717
+ intent: WorktreeCleanupIntent = { kind: "preserve", ...(setup.capturedDiffs ? { capturedDiffs: setup.capturedDiffs } : {}) },
718
+ ): WorktreeCleanupReport {
609
719
  const tasks: WorktreeCleanupTask[] = [];
610
720
  for (let index = setup.worktrees.length - 1; index >= 0; index--) {
611
- tasks.push(cleanupSingleWorktree(setup.cwd, setup.worktrees[index]!));
721
+ tasks.push(cleanupSingleWorktree(setup, setup.worktrees[index]!, intent));
612
722
  }
613
723
  tasks.sort((left, right) => left.index - right.index);
614
724
  const errors: string[] = [];
@@ -8,18 +8,40 @@ type AccessibleDirOptions = {
8
8
  retryDirectoryErrors?: boolean;
9
9
  retryDelaysMs?: readonly number[];
10
10
  wait?: (delayMs: number) => void;
11
+ pid?: number;
11
12
  };
12
13
 
13
- export function ensureAccessibleDir(dirPath: string, options: AccessibleDirOptions = {}): void {
14
+ export function ensureAccessibleDir(dirPath: string, options: AccessibleDirOptions = {}): string {
14
15
  const fsImpl = options.fs ?? fs;
16
+ const pid = options.pid ?? process.pid;
15
17
  const retryDirectoryErrors = options.retryDirectoryErrors ?? process.platform === "win32";
16
18
  const retryDelaysMs = retryDirectoryErrors ? options.retryDelaysMs ?? DEFAULT_FILE_SYSTEM_RETRY_DELAYS_MS : [];
17
19
  const wait = options.wait ?? waitForFileSystemRetry;
18
20
 
19
- runFileSystemOperationWithRetry(() => {
20
- fsImpl.mkdirSync(dirPath, { recursive: true });
21
- }, { retryDelaysMs, wait });
22
- runFileSystemOperationWithRetry(() => {
23
- fsImpl.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);
24
- }, { retryDelaysMs, wait });
21
+ const mkdirWithRetry = (target: string): void => {
22
+ runFileSystemOperationWithRetry(() => {
23
+ fsImpl.mkdirSync(target, { recursive: true });
24
+ }, { retryDelaysMs, wait });
25
+ };
26
+ const accessWithRetry = (target: string): void => {
27
+ runFileSystemOperationWithRetry(() => {
28
+ fsImpl.accessSync(target, fs.constants.R_OK | fs.constants.W_OK);
29
+ }, { retryDelaysMs, wait });
30
+ };
31
+ const fallbackPath = (): string => {
32
+ const fallback = `${dirPath}-${pid}`;
33
+ mkdirWithRetry(fallback);
34
+ accessWithRetry(fallback);
35
+ return fallback;
36
+ };
37
+
38
+ try {
39
+ mkdirWithRetry(dirPath);
40
+ accessWithRetry(dirPath);
41
+ return dirPath;
42
+ } catch (error) {
43
+ const code = (error as NodeJS.ErrnoException | undefined)?.code;
44
+ if (code !== "EPERM" && code !== "EACCES") throw error;
45
+ return fallbackPath();
46
+ }
25
47
  }
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { TEMP_ARTIFACTS_DIR, type ArtifactPaths, type ArtifactDirPreference } from "./types.ts";
3
+ import { CHAIN_RUNS_DIR, TEMP_ARTIFACTS_DIR, type ArtifactPaths, type ArtifactDirPreference } from "./types.ts";
4
4
  import { getAgentDir } from "./utils.ts";
5
5
  const CLEANUP_MARKER_FILE = ".last-cleanup";
6
6
  const PROJECT_ARTIFACT_ROOT = ".pi-subagents";
@@ -17,6 +17,21 @@ export function getProjectChainRunsDir(cwd: string): string {
17
17
  return path.join(getProjectSubagentsDir(cwd), "chain-runs");
18
18
  }
19
19
 
20
+ export function getChainRunsDir(
21
+ projectCwd: string,
22
+ dirPreference: ArtifactDirPreference = "project",
23
+ ): string {
24
+ switch (dirPreference) {
25
+ case "project":
26
+ return getProjectChainRunsDir(projectCwd);
27
+ case "session":
28
+ case "temp":
29
+ return CHAIN_RUNS_DIR;
30
+ default:
31
+ throw new Error(`Unsupported artifactDir ${JSON.stringify(dirPreference)}; expected "project", "session", or "temp".`);
32
+ }
33
+ }
34
+
20
35
  export function getArtifactsDir(
21
36
  sessionFile: string | null,
22
37
  projectCwd?: string,
@@ -61,6 +76,7 @@ export function ensureArtifactsDir(dir: string): void {
61
76
  }
62
77
 
63
78
  export function writeArtifact(filePath: string, content: string): void {
79
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
64
80
  fs.writeFileSync(filePath, content, "utf-8");
65
81
  }
66
82
 
@@ -78,6 +94,7 @@ export function formatOutputArtifactContent(input: {
78
94
  }
79
95
 
80
96
  export function writeMetadata(filePath: string, metadata: object): void {
97
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
81
98
  fs.writeFileSync(filePath, JSON.stringify(metadata, null, 2), "utf-8");
82
99
  }
83
100
 
@@ -77,8 +77,9 @@ function isUnsafeAnthropicThinkingBlock(message: BranchSessionEntry["message"],
77
77
  const isAnthropic = provider === "anthropic" || api === "anthropic-messages" || model.startsWith("anthropic/");
78
78
  if (block.type === "redacted_thinking") return true;
79
79
  if (block.type !== "thinking" || !isAnthropic) return false;
80
- const signature = "thinkingSignature" in block ? block.thinkingSignature : "signature" in block ? block.signature : undefined;
81
- return block.redacted === true || (typeof signature === "string" && signature.length > 0);
80
+ const record = block as Record<string, unknown>;
81
+ const signature = "thinkingSignature" in record ? record.thinkingSignature : "signature" in record ? record.signature : undefined;
82
+ return record.redacted === true || (typeof signature === "string" && signature.length > 0);
82
83
  }
83
84
 
84
85
  function createEntryId(entries: BranchSessionEntry[]): string {
@@ -38,6 +38,7 @@ export function projectAgentDefinition(agent: AgentConfig): Record<string, unkno
38
38
  packageName: agent.packageName,
39
39
  filePath: agent.filePath,
40
40
  fileContentDigest: fileDigest(agent.filePath),
41
+ runner: agent.runner,
41
42
  systemPrompt: agent.systemPrompt,
42
43
  systemPromptMode: agent.systemPromptMode,
43
44
  inheritProjectContext: agent.inheritProjectContext,
@@ -120,6 +120,16 @@ export interface CheckpointStep {
120
120
  message?: string;
121
121
  phase?: string;
122
122
  label?: string;
123
+ agent?: string;
124
+ task?: string;
125
+ as?: string;
126
+ output?: string | false;
127
+ outputMode?: OutputMode;
128
+ reads?: string[] | false;
129
+ progress?: boolean;
130
+ skill?: string | string[] | false;
131
+ skills?: string[] | false;
132
+ model?: string;
123
133
  }
124
134
 
125
135
  /** Parallel step: multiple agents running concurrently */