pi-subagents 0.56.0 → 0.57.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 (83) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/agents/claude-code-writer.md +15 -0
  3. package/agents/claude-code.md +15 -0
  4. package/agents/codex-exec-writer.md +15 -0
  5. package/agents/codex-exec.md +15 -0
  6. package/agents/cursor-agent-writer.md +14 -0
  7. package/agents/cursor-agent.md +14 -0
  8. package/docs/agents.md +111 -15
  9. package/docs/configuration.md +37 -0
  10. package/docs/extension-api.md +2 -2
  11. package/docs/observability.md +8 -7
  12. package/docs/tool-reference.md +11 -1
  13. package/docs/workflows.md +20 -0
  14. package/package.json +1 -1
  15. package/skills/pi-subagents/references/execution-controls.md +1 -1
  16. package/src/agents/agent-management.ts +13 -5
  17. package/src/agents/agent-refinements.ts +4 -4
  18. package/src/agents/agents.ts +219 -48
  19. package/src/agents/builtin-names.ts +6 -0
  20. package/src/agents/runtime-agent-registry.ts +8 -2
  21. package/src/api/preflight.ts +2 -2
  22. package/src/extension/config.ts +70 -0
  23. package/src/extension/doctor.ts +3 -3
  24. package/src/extension/index.ts +18 -4
  25. package/src/extension/public-execution.ts +29 -13
  26. package/src/extension/rpc.ts +20 -5
  27. package/src/extension/schemas.ts +4 -3
  28. package/src/extension/tool-description.ts +11 -9
  29. package/src/inspectors/herdr/actions.ts +2 -1
  30. package/src/inspectors/herdr/inspector-runner.ts +2 -10
  31. package/src/inspectors/herdr/session-roots-codec.ts +42 -0
  32. package/src/integrations/herdr-status.ts +46 -3
  33. package/src/runs/background/active-async-capacity.ts +77 -10
  34. package/src/runs/background/async-execution.ts +100 -9
  35. package/src/runs/background/async-job-tracker.ts +5 -0
  36. package/src/runs/background/async-retention.ts +20 -3
  37. package/src/runs/background/async-status.ts +5 -0
  38. package/src/runs/background/chain-root-attachment.ts +15 -1
  39. package/src/runs/background/fleet-view.ts +16 -10
  40. package/src/runs/background/inspect-rpc.ts +8 -8
  41. package/src/runs/background/result-files.ts +27 -14
  42. package/src/runs/background/result-watcher.ts +9 -5
  43. package/src/runs/background/run-status.ts +33 -6
  44. package/src/runs/background/scheduled-runs.ts +7 -1
  45. package/src/runs/background/subagent-runner.ts +177 -44
  46. package/src/runs/background/wait-completions.ts +4 -0
  47. package/src/runs/foreground/execution.ts +72 -10
  48. package/src/runs/foreground/foreground-control.ts +6 -0
  49. package/src/runs/foreground/foreground-history.ts +22 -1
  50. package/src/runs/foreground/subagent-executor.ts +270 -81
  51. package/src/runs/foreground/workflow-detach-reconcile.ts +16 -1
  52. package/src/runs/shared/child-protocol.ts +21 -7
  53. package/src/runs/shared/claude-code-adapter.ts +129 -0
  54. package/src/runs/shared/codex-exec-adapter.ts +129 -0
  55. package/src/runs/shared/cursor-agent-adapter.ts +114 -0
  56. package/src/runs/shared/external-cli-contract.ts +167 -0
  57. package/src/runs/shared/external-cli-preflight.ts +122 -0
  58. package/src/runs/shared/external-cli-runner.ts +348 -55
  59. package/src/runs/shared/model-exclusions.ts +57 -7
  60. package/src/runs/shared/model-fallback.ts +23 -1
  61. package/src/runs/shared/mutation-evidence.ts +7 -2
  62. package/src/runs/shared/nested-events.ts +3 -1
  63. package/src/runs/shared/nested-render.ts +2 -2
  64. package/src/runs/shared/process-signal.ts +13 -0
  65. package/src/runs/shared/run-history.ts +21 -1
  66. package/src/shared/fork-context.ts +21 -0
  67. package/src/shared/formatters.ts +7 -1
  68. package/src/shared/pruned-fork.ts +450 -0
  69. package/src/shared/session-file-trust.ts +19 -0
  70. package/src/shared/session-tokens.ts +14 -3
  71. package/src/shared/settings.ts +10 -2
  72. package/src/shared/shortcuts.ts +17 -0
  73. package/src/shared/types.ts +118 -9
  74. package/src/shared/workflow-child-permit.ts +116 -0
  75. package/src/slash/delegation-adapters.ts +0 -1
  76. package/src/slash/slash-commands.ts +8 -6
  77. package/src/tui/fleet-status.ts +27 -10
  78. package/src/tui/fleet-transcript.ts +11 -5
  79. package/src/tui/fleet.ts +28 -13
  80. package/src/tui/render.ts +47 -21
  81. package/src/workflows/scripted-workflow.ts +248 -27
  82. package/src/workflows/workflow-child-summary.ts +117 -0
  83. package/src/workflows/workflow-receipt.ts +119 -4
@@ -9,6 +9,7 @@ import * as os from "node:os";
9
9
  import * as path from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import type { AcceptanceInput, AcceptanceRole, AgentRunnerConfig, OutputMode, ToolBudgetConfig, TurnBudgetConfig } from "../shared/types.ts";
12
+ import { CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL, isCodeOwnedExternalCliAdapterId, parseExternalCliCapabilityNarrowing, validateCodeOwnedProfileRunner } from "../runs/shared/external-cli-contract.ts";
12
13
  import { getAgentDir, getProjectConfigDir } from "../shared/utils.ts";
13
14
  import { KNOWN_FIELDS } from "./agent-serializer.ts";
14
15
  import { parseChain, parseJsonChain } from "./chain-serializer.ts";
@@ -262,14 +263,72 @@ export function findBlockingAgentDiagnostic(name: string, agent: AgentConfig | r
262
263
  return !agents.length || (match && agentDefinitionPriority(match) > highestPriority) ? match : undefined;
263
264
  }
264
265
 
265
- interface AgentDiscoveryResult {
266
+ export type AgentDefinitionDirectoryState = "absent" | "empty" | "candidates" | "unreadable" | "not-directory";
267
+
268
+ /** A definition directory actually inspected during one agent-discovery operation. */
269
+ export interface AgentDefinitionDirectoryReport {
270
+ source: AgentSource;
271
+ path: string;
272
+ state: AgentDefinitionDirectoryState;
273
+ candidateCount?: number;
274
+ }
275
+
276
+ /**
277
+ * Filesystem provenance for a failed agent resolution. Context is created from
278
+ * the discovery result that supplied the effective agents; callers must not
279
+ * pair arbitrary agent arrays with these directory reports.
280
+ */
281
+ export interface UnknownAgentDiagnosticContext {
282
+ cwd: string;
283
+ scope: AgentScope;
284
+ directories: readonly AgentDefinitionDirectoryReport[];
285
+ agents: readonly AgentConfig[];
286
+ }
287
+
288
+ export interface AgentDiscoveryResult {
266
289
  agents: AgentConfig[];
267
290
  agentDiagnostics?: AgentDiscoveryDiagnostic[];
268
291
  projectAgentsDir: string | null;
292
+ cwd: string;
293
+ scope: AgentScope;
294
+ directories: readonly AgentDefinitionDirectoryReport[];
269
295
  modelScope?: ModelScopeConfig;
270
296
  maxThinking?: ThinkingLevel;
271
297
  }
272
298
 
299
+ /** Create formatter input from the exact discovery operation used for resolution. */
300
+ export function unknownAgentDiagnosticContext(discovered: Pick<AgentDiscoveryResult, "cwd" | "scope" | "directories" | "agents">): UnknownAgentDiagnosticContext {
301
+ return {
302
+ cwd: discovered.cwd,
303
+ scope: discovered.scope,
304
+ directories: discovered.directories,
305
+ agents: discovered.agents,
306
+ };
307
+ }
308
+
309
+ /** Render local discovery evidence without exposing filesystem error details. */
310
+ export function formatUnknownAgentError(name: string, context: UnknownAgentDiagnosticContext, prefix = "Unknown agent"): string {
311
+ const directories = context.directories.map((directory) => {
312
+ const state = directory.state === "candidates"
313
+ ? `${directory.candidateCount ?? 0} candidate${directory.candidateCount === 1 ? "" : "s"}`
314
+ : directory.state === "empty" ? "present but empty"
315
+ : directory.state === "not-directory" ? "not a directory"
316
+ : directory.state;
317
+ return `- ${directory.source}: ${directory.path} (${state})`;
318
+ });
319
+ const agents = [...context.agents]
320
+ .sort((left, right) => left.name.localeCompare(right.name) || left.source.localeCompare(right.source))
321
+ .map((agent) => `- ${agent.name} (${agent.source})`);
322
+ return [
323
+ `${prefix}: ${name}`,
324
+ `Effective cwd: ${path.resolve(context.cwd)}`,
325
+ "Consulted agent-definition directories:",
326
+ ...(directories.length ? directories : ["- (none)"]),
327
+ "Discovered agents:",
328
+ ...(agents.length ? agents : ["- (none)"]),
329
+ ].join("\n");
330
+ }
331
+
273
332
  function getUserChainDir(): string {
274
333
  return path.join(getAgentDir(), "chains");
275
334
  }
@@ -381,6 +440,10 @@ function resolveSettingsPackageRoot(source: string, baseDir: string): string | u
381
440
  if (normalized === "." || normalized === ".." || normalized.startsWith("./") || normalized.startsWith("../")) {
382
441
  return path.resolve(baseDir, normalized);
383
442
  }
443
+ if (/^https?:\/\//i.test(trimmed)) {
444
+ const parsed = parseGitPackagePath(`git:${trimmed}`);
445
+ return parsed ? path.join(baseDir, "git", parsed.host, parsed.repoPath) : undefined;
446
+ }
384
447
  return undefined;
385
448
  }
386
449
 
@@ -1598,9 +1661,22 @@ function shouldPruneDiscoveryDir(rootDir: string, dir: string, dirName: string):
1598
1661
  return path.resolve(dir) !== path.resolve(rootDir) && isDiscoveryNestedProjectRoot(dir);
1599
1662
  }
1600
1663
 
1601
- function listFilesRecursive(dir: string, predicate: (fileName: string) => boolean, rootDir = dir): string[] {
1664
+ function listFilesRecursive(
1665
+ dir: string,
1666
+ predicate: (fileName: string) => boolean,
1667
+ rootDir = dir,
1668
+ visitedDirectories = new Set<string>(),
1669
+ ): string[] {
1602
1670
  const files: string[] = [];
1603
1671
  if (!fs.existsSync(dir)) return files;
1672
+ let realDir: string;
1673
+ try {
1674
+ realDir = fs.realpathSync(dir);
1675
+ } catch {
1676
+ return files;
1677
+ }
1678
+ if (visitedDirectories.has(realDir)) return files;
1679
+ visitedDirectories.add(realDir);
1604
1680
 
1605
1681
  let entries: fs.Dirent[];
1606
1682
  try {
@@ -1611,9 +1687,17 @@ function listFilesRecursive(dir: string, predicate: (fileName: string) => boolea
1611
1687
 
1612
1688
  for (const entry of entries) {
1613
1689
  const filePath = path.join(dir, entry.name);
1614
- if (entry.isDirectory()) {
1690
+ let isDirectory = entry.isDirectory();
1691
+ if (entry.isSymbolicLink()) {
1692
+ try {
1693
+ isDirectory = fs.statSync(filePath).isDirectory();
1694
+ } catch {
1695
+ isDirectory = false;
1696
+ }
1697
+ }
1698
+ if (isDirectory) {
1615
1699
  if (!shouldPruneDiscoveryDir(rootDir, filePath, entry.name)) {
1616
- files.push(...listFilesRecursive(filePath, predicate, rootDir));
1700
+ files.push(...listFilesRecursive(filePath, predicate, rootDir, visitedDirectories));
1617
1701
  }
1618
1702
  continue;
1619
1703
  }
@@ -1624,6 +1708,81 @@ function listFilesRecursive(dir: string, predicate: (fileName: string) => boolea
1624
1708
  return files;
1625
1709
  }
1626
1710
 
1711
+ export interface AgentDefinitionInspection {
1712
+ files: string[];
1713
+ state: AgentDefinitionDirectoryState;
1714
+ }
1715
+
1716
+ /** Narrow filesystem seam so unavailable paths can be tested without permissions. */
1717
+ export interface AgentDefinitionInspectionFs {
1718
+ existsSync(filePath: string): boolean;
1719
+ realpathSync?(filePath: string): string;
1720
+ statSync(filePath: string): { isDirectory(): boolean };
1721
+ readdirSync(dir: string): fs.Dirent[];
1722
+ }
1723
+
1724
+ const DEFAULT_AGENT_DEFINITION_INSPECTION_FS: AgentDefinitionInspectionFs = {
1725
+ existsSync: fs.existsSync,
1726
+ realpathSync: fs.realpathSync,
1727
+ statSync: fs.statSync,
1728
+ readdirSync: (dir) => fs.readdirSync(dir, { withFileTypes: true }),
1729
+ };
1730
+
1731
+ /**
1732
+ * Inspect one agent-definition directory while retaining traversal failure
1733
+ * state. A nested unreadable directory makes the whole inspection unavailable,
1734
+ * preventing a partial traversal from being reported as empty or complete.
1735
+ */
1736
+ export function inspectAgentDefinitionDirectory(dir: string, operations: AgentDefinitionInspectionFs = DEFAULT_AGENT_DEFINITION_INSPECTION_FS): AgentDefinitionInspection {
1737
+ const root = path.resolve(dir);
1738
+ try {
1739
+ if (!operations.existsSync(root)) return { files: [], state: "absent" };
1740
+ if (!operations.statSync(root).isDirectory()) return { files: [], state: "not-directory" };
1741
+ } catch {
1742
+ return { files: [], state: "unreadable" };
1743
+ }
1744
+ const files: string[] = [];
1745
+ let unreadable = false;
1746
+ const visitedDirectories = new Set<string>();
1747
+ const visit = (current: string): void => {
1748
+ let realDir: string;
1749
+ try {
1750
+ realDir = operations.realpathSync?.(current) ?? path.resolve(current);
1751
+ } catch {
1752
+ unreadable = true;
1753
+ return;
1754
+ }
1755
+ if (visitedDirectories.has(realDir)) return;
1756
+ visitedDirectories.add(realDir);
1757
+
1758
+ let entries: fs.Dirent[];
1759
+ try {
1760
+ entries = operations.readdirSync(current).sort((a, b) => a.name.localeCompare(b.name));
1761
+ } catch {
1762
+ unreadable = true;
1763
+ return;
1764
+ }
1765
+ for (const entry of entries) {
1766
+ const filePath = path.join(current, entry.name);
1767
+ let isDirectory = entry.isDirectory();
1768
+ if (entry.isSymbolicLink()) {
1769
+ try {
1770
+ isDirectory = operations.statSync(filePath).isDirectory();
1771
+ } catch {
1772
+ isDirectory = false;
1773
+ }
1774
+ }
1775
+ if (isDirectory) {
1776
+ if (!shouldPruneDiscoveryDir(root, filePath, entry.name)) visit(filePath);
1777
+ continue;
1778
+ }
1779
+ if ((entry.isFile() || entry.isSymbolicLink()) && entry.name.endsWith(".md") && !entry.name.endsWith(".chain.md") && !isLegacyAgentSkillPath(root, filePath)) files.push(filePath);
1780
+ }
1781
+ };
1782
+ visit(root);
1783
+ return { files, state: unreadable ? "unreadable" : files.length ? "candidates" : "empty" };
1784
+ }
1785
+
1627
1786
  function isLegacyAgentSkillPath(rootDir: string, filePath: string): boolean {
1628
1787
  const relative = path.relative(rootDir, filePath);
1629
1788
  const parts = relative.split(path.sep).map((part) => part.toLowerCase());
@@ -1682,18 +1841,23 @@ function parseAgentRunnerFrontmatter(raw: string | undefined, agentName: string)
1682
1841
  if (runner.args !== undefined && (!Array.isArray(runner.args) || runner.args.some((arg) => typeof arg !== "string"))) {
1683
1842
  throw new Error(`Agent '${agentName}' external-cli runner args must be an array of strings.`);
1684
1843
  }
1844
+ if (runner.adapter !== undefined && !isCodeOwnedExternalCliAdapterId(runner.adapter)) throw new Error(`Agent '${agentName}' external-cli runner adapter must be ${CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL}.`);
1845
+ if (runner.adapter !== undefined && Array.isArray(runner.args) && runner.args.length > 0) throw new Error(`Agent '${agentName}' ${runner.adapter} adapter owns its argv; runner args are not supported.`);
1685
1846
  if (runner.promptDelivery !== undefined && runner.promptDelivery !== "stdin") {
1686
1847
  throw new Error(`Agent '${agentName}' external-cli runner promptDelivery must be 'stdin'.`);
1687
1848
  }
1688
- const supported = new Set(["type", "command", "args", "promptDelivery"]);
1849
+ const capabilities = parseExternalCliCapabilityNarrowing(runner.capabilities, `Agent '${agentName}' external-cli runner capabilities`);
1850
+ const supported = new Set(["type", "adapter", "command", "args", "promptDelivery", "capabilities"]);
1689
1851
  const unknown = Object.keys(runner).filter((key) => !supported.has(key));
1690
1852
  if (unknown.length > 0) throw new Error(`Agent '${agentName}' external-cli runner has unsupported fields: ${unknown.join(", ")}.`);
1691
1853
  const runnerArgs = Array.isArray(runner.args) ? runner.args.filter((arg): arg is string => typeof arg === "string") : undefined;
1692
1854
  return {
1693
1855
  type: "external-cli",
1856
+ ...(isCodeOwnedExternalCliAdapterId(runner.adapter) ? { adapter: runner.adapter } : {}),
1694
1857
  command: runner.command.trim(),
1695
1858
  ...(runnerArgs?.length ? { args: runnerArgs } : {}),
1696
1859
  ...(runner.promptDelivery ? { promptDelivery: "stdin" as const } : {}),
1860
+ ...(capabilities ? { capabilities } : {}),
1697
1861
  };
1698
1862
  }
1699
1863
 
@@ -1724,9 +1888,9 @@ interface AgentDefinitionFile {
1724
1888
  content: string;
1725
1889
  }
1726
1890
 
1727
- function readAgentDefinitionFiles(dir: string): AgentDefinitionFile[] {
1891
+ function readAgentDefinitionFiles(dir: string, inspection = inspectAgentDefinitionDirectory(dir)): AgentDefinitionFile[] {
1728
1892
  const files: AgentDefinitionFile[] = [];
1729
- for (const filePath of listFilesRecursive(dir, (fileName) => fileName.endsWith(".md") && !fileName.endsWith(".chain.md"))) {
1893
+ for (const filePath of inspection.files) {
1730
1894
  if (isLegacyAgentSkillPath(dir, filePath)) {
1731
1895
  continue;
1732
1896
  }
@@ -1783,6 +1947,8 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
1783
1947
  const mcpDirectTools = parsedTools.mcpDirectTools ?? [];
1784
1948
  const defaultReads = parseFrontmatterList(frontmatter.defaultReads);
1785
1949
  const aliases = normalizeAgentAliases(parseFrontmatterList(frontmatter.aliases ?? frontmatter.alias), runtimeName);
1950
+ const profileError = validateCodeOwnedProfileRunner({ name: runtimeName, localName, aliases, runner });
1951
+ if (profileError) throw new Error(profileError);
1786
1952
  const skillStr = frontmatter.skill || frontmatter.skills;
1787
1953
  const skills = parseFrontmatterList(skillStr);
1788
1954
  const skillPath = parseFrontmatterList(frontmatter.skillPath);
@@ -1944,8 +2110,17 @@ function loadAgentsFromDefinitionFiles(files: AgentDefinitionFile[], source: Age
1944
2110
  return { agents, diagnostics };
1945
2111
  }
1946
2112
 
1947
- function loadAgentsFromDir(dir: string, source: AgentSource, discoveryPriority?: number, packageSource?: Omit<PackageSubagentPath, "dir">): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
1948
- return loadAgentsFromDefinitionFiles(readAgentDefinitionFiles(dir), source, discoveryPriority, packageSource);
2113
+ function loadAgentsFromDir(dir: string, source: AgentSource, discoveryPriority?: number, packageSource?: Omit<PackageSubagentPath, "dir">, inspection = inspectAgentDefinitionDirectory(dir)): { agents: AgentConfig[]; diagnostics: AgentDiscoveryDiagnostic[] } {
2114
+ return loadAgentsFromDefinitionFiles(readAgentDefinitionFiles(dir, inspection), source, discoveryPriority, packageSource);
2115
+ }
2116
+
2117
+ function reportAgentDefinitionDirectory(source: AgentSource, dir: string, inspection: AgentDefinitionInspection): AgentDefinitionDirectoryReport {
2118
+ return {
2119
+ source,
2120
+ path: path.resolve(dir),
2121
+ state: inspection.state,
2122
+ ...(inspection.state === "candidates" ? { candidateCount: inspection.files.length } : {}),
2123
+ };
1949
2124
  }
1950
2125
 
1951
2126
  function loadChainsFromDir(dir: string, source: AgentSource): { chains: ChainConfig[]; diagnostics: ChainDiscoveryDiagnostic[] } {
@@ -1982,20 +2157,18 @@ function isDirectory(p: string): boolean {
1982
2157
  }
1983
2158
  }
1984
2159
 
1985
- function resolveNearestProjectAgentDirs(cwd: string): { readDirs: string[]; preferredDir: string | null } {
2160
+ function resolveNearestProjectAgentDirs(cwd: string): { readDirs: string[]; candidateDirs: string[]; preferredDir: string | null } {
1986
2161
  const projectRoot = findConfiguredProjectRoot(cwd);
1987
- if (!projectRoot) return { readDirs: [], preferredDir: null };
2162
+ if (!projectRoot) return { readDirs: [], candidateDirs: [], preferredDir: null };
1988
2163
 
1989
2164
  const legacyDir = path.join(projectRoot, ".agents");
1990
2165
  const preferredDir = path.join(getProjectConfigDir(projectRoot), "agents");
2166
+ const candidateDirs = [legacyDir, preferredDir];
1991
2167
  const readDirs: string[] = [];
1992
2168
  if (isDirectory(legacyDir)) readDirs.push(legacyDir);
1993
2169
  if (isDirectory(preferredDir)) readDirs.push(preferredDir);
1994
2170
 
1995
- return {
1996
- readDirs,
1997
- preferredDir,
1998
- };
2171
+ return { readDirs, candidateDirs, preferredDir };
1999
2172
  }
2000
2173
 
2001
2174
  function resolveNearestProjectChainDirs(cwd: string): { readDirs: string[]; preferredDir: string | null } {
@@ -2009,7 +2182,9 @@ function resolveNearestProjectChainDirs(cwd: string): { readDirs: string[]; pref
2009
2182
  };
2010
2183
  }
2011
2184
  const BUILTIN_AGENTS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "agents");
2012
- const BUILTIN_AGENT_DEFINITION_FILES = readAgentDefinitionFiles(BUILTIN_AGENTS_DIR);
2185
+ // Candidate files and inspection state must describe the same cached builtin scan.
2186
+ const BUILTIN_AGENT_DEFINITION_INSPECTION = inspectAgentDefinitionDirectory(BUILTIN_AGENTS_DIR);
2187
+ const BUILTIN_AGENT_DEFINITION_FILES = readAgentDefinitionFiles(BUILTIN_AGENTS_DIR, BUILTIN_AGENT_DEFINITION_INSPECTION);
2013
2188
 
2014
2189
  export const EXTRA_AGENT_DIRS_ENV = "PI_SUBAGENT_EXTRA_AGENT_DIRS";
2015
2190
 
@@ -2028,11 +2203,12 @@ function extraUserAgentDirs(): string[] {
2028
2203
  }
2029
2204
 
2030
2205
  export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
2206
+ const effectiveCwd = path.resolve(cwd);
2031
2207
  const userDirOld = path.join(getAgentDir(), "agents");
2032
2208
  const userDirNew = path.join(os.homedir(), ".agents");
2033
- const { readDirs: projectAgentDirs, preferredDir: projectAgentsDir } = resolveNearestProjectAgentDirs(cwd);
2209
+ const { readDirs: projectAgentDirs, candidateDirs: projectCandidateDirs, preferredDir: projectAgentsDir } = resolveNearestProjectAgentDirs(effectiveCwd);
2034
2210
  const userSettingsPath = getUserAgentSettingsPath();
2035
- const projectSettingsPath = getProjectAgentSettingsPath(cwd);
2211
+ const projectSettingsPath = getProjectAgentSettingsPath(effectiveCwd);
2036
2212
  const userSettings = scope === "project" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(userSettingsPath);
2037
2213
  const projectSettings = scope === "user" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(projectSettingsPath);
2038
2214
  const defaultProvider = resolveSubagentDefaultProvider(userSettings, projectSettings, projectSettingsPath);
@@ -2041,65 +2217,60 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
2041
2217
  const maxThinking = resolveSubagentMaxThinking(userSettings, projectSettings, projectSettingsPath);
2042
2218
  const defaultExtensions = resolveSubagentDefaultExtensions(userSettings, projectSettings, projectSettingsPath);
2043
2219
  const modelScope = projectSettings.modelScope ?? userSettings.modelScope;
2044
- const packageSubagentPaths = collectPackageSubagentPaths(cwd, {
2220
+ const packageSubagentPaths = collectPackageSubagentPaths(effectiveCwd, {
2045
2221
  includeUser: scope !== "project",
2046
2222
  includeProject: scope !== "user",
2047
2223
  });
2224
+ const directories: AgentDefinitionDirectoryReport[] = [reportAgentDefinitionDirectory("builtin", BUILTIN_AGENTS_DIR, BUILTIN_AGENT_DEFINITION_INSPECTION)];
2048
2225
 
2049
2226
  const builtinLoaded = loadAgentsFromDefinitionFiles(BUILTIN_AGENT_DEFINITION_FILES, "builtin");
2050
2227
  const builtinAgents = applyBuiltinOverrides(
2051
2228
  applySubagentDefaults(builtinLoaded.agents, defaultModel, defaultProvider, defaultThinking, defaultExtensions),
2052
- userSettings,
2053
- projectSettings,
2054
- userSettingsPath,
2055
- projectSettingsPath,
2229
+ userSettings, projectSettings, userSettingsPath, projectSettingsPath,
2056
2230
  );
2057
2231
 
2058
- const userLoaded = scope === "project" ? [] : [...extraUserAgentDirs(), userDirOld, userDirNew]
2059
- .map((dir, discoveryPriority) => loadAgentsFromDir(dir, "user", discoveryPriority));
2232
+ const userLoaded = scope === "project" ? [] : [...extraUserAgentDirs(), userDirOld, userDirNew].map((dir, discoveryPriority) => {
2233
+ const inspection = inspectAgentDefinitionDirectory(dir);
2234
+ directories.push(reportAgentDefinitionDirectory("user", dir, inspection));
2235
+ return loadAgentsFromDir(dir, "user", discoveryPriority, undefined, inspection);
2236
+ });
2060
2237
  const userAgents = applyCustomAgentOverrides(
2061
2238
  applySubagentDefaults(userLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultProvider, defaultThinking, defaultExtensions),
2062
- userSettings,
2063
- projectSettings,
2064
- userSettingsPath,
2065
- projectSettingsPath,
2239
+ userSettings, projectSettings, userSettingsPath, projectSettingsPath,
2066
2240
  );
2067
2241
 
2068
- const projectLoaded = scope === "user" ? [] : projectAgentDirs.map((dir) => loadAgentsFromDir(dir, "project", dir === projectAgentsDir ? 1 : 0));
2242
+ // Report both legacy and preferred project definition paths only after the
2243
+ // existing configured-root resolver found a root; never synthesize cwd paths.
2244
+ const projectInspections = scope === "user" ? new Map<string, AgentDefinitionInspection>() : new Map(projectCandidateDirs.map((dir) => [dir, inspectAgentDefinitionDirectory(dir)]));
2245
+ if (scope !== "user") for (const dir of projectCandidateDirs) directories.push(reportAgentDefinitionDirectory("project", dir, projectInspections.get(dir)!));
2246
+ const projectLoaded = scope === "user" ? [] : projectAgentDirs.map((dir) => loadAgentsFromDir(dir, "project", dir === projectAgentsDir ? 1 : 0, undefined, projectInspections.get(dir)!));
2069
2247
  const projectAgents = applyCustomAgentOverrides(
2070
2248
  applySubagentDefaults(projectLoaded.flatMap((loaded) => loaded.agents), defaultModel, defaultProvider, defaultThinking, defaultExtensions),
2071
- userSettings,
2072
- projectSettings,
2073
- userSettingsPath,
2074
- projectSettingsPath,
2249
+ userSettings, projectSettings, userSettingsPath, projectSettingsPath,
2075
2250
  );
2076
- const packageLoaded = packageSubagentPaths.agents.map((entry, index) => loadAgentsFromDir(entry.dir, "package", packageSubagentPaths.agents.length - index, entry));
2251
+
2252
+ const packageLoaded = packageSubagentPaths.agents.map((entry, index) => {
2253
+ const inspection = inspectAgentDefinitionDirectory(entry.dir);
2254
+ directories.push(reportAgentDefinitionDirectory("package", entry.dir, inspection));
2255
+ return loadAgentsFromDir(entry.dir, "package", packageSubagentPaths.agents.length - index, entry, inspection);
2256
+ });
2077
2257
  const packageMap = new Map<string, AgentConfig>();
2078
- for (const loaded of packageLoaded) {
2079
- for (const agent of loaded.agents) {
2080
- if (!packageMap.has(agent.name)) packageMap.set(agent.name, agent);
2081
- }
2082
- }
2258
+ for (const loaded of packageLoaded) for (const agent of loaded.agents) if (!packageMap.has(agent.name)) packageMap.set(agent.name, agent);
2083
2259
  const packageAgents = applyCustomAgentOverrides(
2084
2260
  applySubagentDefaults(Array.from(packageMap.values()), defaultModel, defaultProvider, defaultThinking, defaultExtensions),
2085
- userSettings,
2086
- projectSettings,
2087
- userSettingsPath,
2088
- projectSettingsPath,
2261
+ userSettings, projectSettings, userSettingsPath, projectSettingsPath,
2089
2262
  );
2090
2263
  const agents = applySubagentMaxThinking(
2091
- mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents, packageAgents)
2092
- .filter((agent) => agent.disabled !== true),
2264
+ mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents, packageAgents).filter((agent) => agent.disabled !== true),
2093
2265
  maxThinking,
2094
2266
  );
2095
-
2096
2267
  const agentDiagnostics = [
2097
2268
  ...builtinLoaded.diagnostics,
2098
2269
  ...userLoaded.flatMap((loaded) => loaded.diagnostics),
2099
2270
  ...projectLoaded.flatMap((loaded) => loaded.diagnostics),
2100
2271
  ...packageLoaded.flatMap((loaded) => loaded.diagnostics),
2101
2272
  ];
2102
- return { agents, agentDiagnostics, projectAgentsDir, ...(modelScope !== undefined ? { modelScope } : {}), ...(maxThinking !== undefined ? { maxThinking } : {}) };
2273
+ return { agents, agentDiagnostics, projectAgentsDir, cwd: effectiveCwd, scope, directories, ...(modelScope !== undefined ? { modelScope } : {}), ...(maxThinking !== undefined ? { maxThinking } : {}) };
2103
2274
  }
2104
2275
 
2105
2276
  export function discoverAgentsAll(cwd: string): {
@@ -1,5 +1,11 @@
1
1
  export const BUILTIN_AGENT_NAMES = [
2
2
  "advisor",
3
+ "claude-code",
4
+ "claude-code-writer",
5
+ "codex-exec",
6
+ "codex-exec-writer",
7
+ "cursor-agent",
8
+ "cursor-agent-writer",
3
9
  "delegate",
4
10
  "oracle",
5
11
  "researcher",
@@ -1,5 +1,6 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { AcceptanceInput, AcceptanceRole, AgentRunnerConfig, OutputMode, ToolBudgetConfig, TurnBudgetConfig } from "../shared/types.ts";
3
+ import { CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL, isCodeOwnedExternalCliAdapterId, parseExternalCliCapabilityNarrowing, validateCodeOwnedProfileRunner } from "../runs/shared/external-cli-contract.ts";
3
4
  import { validateAcceptanceInput } from "../runs/shared/acceptance.ts";
4
5
  import { validatePermissionRules, type PermissionRules } from "../runs/shared/permissions.ts";
5
6
  import { validateToolBudgetConfig } from "../runs/shared/tool-budget.ts";
@@ -169,12 +170,15 @@ function validateRunner(value: unknown): AgentRunnerConfig | undefined {
169
170
  if (runner.type !== "external-cli") throw new Error("Runtime agent definition runner.type must be 'pi', 'external-cli', or 'external-job'.");
170
171
  if (typeof runner.command !== "string" || !runner.command.trim()) throw new Error("Runtime agent definition external-cli runner requires a non-empty command string.");
171
172
  if (runner.args !== undefined && (!Array.isArray(runner.args) || runner.args.some((arg) => typeof arg !== "string"))) throw new Error("Runtime agent definition external-cli runner args must be an array of strings.");
173
+ if (runner.adapter !== undefined && !isCodeOwnedExternalCliAdapterId(runner.adapter)) throw new Error(`Runtime agent definition external-cli runner adapter must be ${CODE_OWNED_EXTERNAL_CLI_ADAPTER_LABEL}.`);
174
+ if (runner.adapter !== undefined && Array.isArray(runner.args) && runner.args.length > 0) throw new Error(`Runtime agent definition ${runner.adapter} adapter owns its argv; runner args are not supported.`);
172
175
  if (runner.promptDelivery !== undefined && runner.promptDelivery !== "stdin") throw new Error("Runtime agent definition external-cli runner promptDelivery must be 'stdin'.");
173
- const supported = new Set(["type", "command", "args", "promptDelivery"]);
176
+ const capabilities = parseExternalCliCapabilityNarrowing(runner.capabilities, "Runtime agent definition external-cli runner capabilities");
177
+ const supported = new Set(["type", "adapter", "command", "args", "promptDelivery", "capabilities"]);
174
178
  const unknown = Object.keys(runner).filter((key) => !supported.has(key));
175
179
  if (unknown.length > 0) throw new Error(`Runtime agent definition external-cli runner has unsupported fields: ${unknown.join(", ")}.`);
176
180
  const args = runner.args as string[] | undefined;
177
- return { type: "external-cli", command: runner.command.trim(), ...(args?.length ? { args } : {}), ...(runner.promptDelivery ? { promptDelivery: "stdin" as const } : {}) };
181
+ return { type: "external-cli", ...(isCodeOwnedExternalCliAdapterId(runner.adapter) ? { adapter: runner.adapter } : {}), command: runner.command.trim(), ...(args?.length ? { args } : {}), ...(runner.promptDelivery ? { promptDelivery: "stdin" as const } : {}), ...(capabilities ? { capabilities } : {}) };
178
182
  }
179
183
 
180
184
  function validateTurnBudget(value: unknown): TurnBudgetConfig | undefined {
@@ -364,6 +368,8 @@ export function registerRuntimeAgent(input: RegisterRuntimeAgentInput): RuntimeA
364
368
  const name = validateString(input.name, "Runtime agent name", MAX_AGENT_NAME_LENGTH);
365
369
  const definition = validateDefinition(input.definition);
366
370
  const agent = toAgentConfig(name, definition);
371
+ const profileError = validateCodeOwnedProfileRunner(agent);
372
+ if (profileError) throw new Error(profileError);
367
373
  assertNoBuiltinCollision(agent);
368
374
  const current = registry();
369
375
  const records = current.byPi.get(pi) ?? [];
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
- import { discoverAgents, discoverAgentsAll, findBlockingAgentDiagnostic, resolveAgentName, type AgentConfig, type AgentScope, type AgentSource } from "../agents/agents.ts";
4
+ import { discoverAgents, discoverAgentsAll, findBlockingAgentDiagnostic, formatUnknownAgentError, resolveAgentName, unknownAgentDiagnosticContext, type AgentConfig, type AgentScope, type AgentSource } from "../agents/agents.ts";
5
5
  import { resolveExecutionAgentScope } from "../agents/agent-scope.ts";
6
6
  import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../agents/skills.ts";
7
7
  import { buildAgentMemoryInjection } from "../agents/agent-memory.ts";
@@ -251,7 +251,7 @@ export async function resolveSubagentLaunchContract(input: SubagentLaunchContrac
251
251
  return { ok: false, code: "ambiguous_agent", message: resolvedAgent.error, diagnostics };
252
252
  }
253
253
  if (!resolvedAgent.agent) {
254
- return { ok: false, code: "missing_agent", message: `Unknown agent: ${input.agent}`, diagnostics };
254
+ return { ok: false, code: "missing_agent", message: formatUnknownAgentError(input.agent, unknownAgentDiagnosticContext(discovered)), diagnostics };
255
255
  }
256
256
  const agent = resolvedAgent.agent;
257
257
  let extensionBindings: ExtensionBindings | undefined;
@@ -6,7 +6,9 @@ import { FLEET_KEYBINDING_ACTIONS, type ArtifactDirPreference, type ExtensionCon
6
6
  import { validateMissionStoreConfig } from "../missions/store.ts";
7
7
  import { validateAuthorityPolicy } from "../policy/authority.ts";
8
8
  import { getAgentDir } from "../shared/utils.ts";
9
+ import { DEFAULT_MODEL_EXCLUSION_TTL_MS, MAX_MODEL_EXCLUSION_TTL_MS, setDefaultTTL } from "../runs/shared/model-exclusions.ts";
9
10
  import { validatePermissionConfig } from "../runs/shared/permissions.ts";
11
+ import { MAX_ABANDONED_SLOT_RELEASE_AFTER_MS, MIN_ABANDONED_SLOT_RELEASE_AFTER_MS } from "../runs/background/active-async-capacity.ts";
10
12
 
11
13
  const ARTIFACT_DIR_PREFERENCES = new Set<ArtifactDirPreference>(["project", "session", "temp"]);
12
14
  const FLEET_KEYBINDING_ACTION_SET = new Set<string>(FLEET_KEYBINDING_ACTIONS);
@@ -16,6 +18,23 @@ const BASE_KEY_IDS = new Set([
16
18
  ...Object.values(Key).flatMap((value) => typeof value === "string" ? [value.toLowerCase()] : []),
17
19
  ]);
18
20
 
21
+ class PrunedForkConfigError extends Error {}
22
+
23
+ function validateForkContextConfig(value: unknown): void {
24
+ if (value === undefined) return;
25
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.forkContext must be a JSON object");
26
+ const config = value as Record<string, unknown>;
27
+ if (config.mode !== undefined && config.mode !== "full" && config.mode !== "pruned") {
28
+ throw new Error('config.forkContext.mode must be "full" or "pruned"');
29
+ }
30
+ if (config.model !== undefined && (typeof config.model !== "string" || !config.model.trim())) {
31
+ throw new PrunedForkConfigError("config.forkContext.model must be a non-empty string");
32
+ }
33
+ if (config.mode === "pruned" && config.model === undefined) {
34
+ throw new PrunedForkConfigError('config.forkContext.model is required when config.forkContext.mode is "pruned"');
35
+ }
36
+ }
37
+
19
38
  function isValidKeyId(value: string): boolean {
20
39
  if (value !== value.trim()) return false;
21
40
  const parts = value.toLowerCase().split("+");
@@ -62,6 +81,31 @@ function validateArtifactConfig(value: unknown): void {
62
81
  }
63
82
  }
64
83
 
84
+ function validateCapacityConfig(value: unknown): void {
85
+ if (value === undefined) return;
86
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.capacity must be a JSON object");
87
+ const abandonedSlotReleaseAfterMs = (value as Record<string, unknown>).abandonedSlotReleaseAfterMs;
88
+ if (abandonedSlotReleaseAfterMs !== undefined
89
+ && abandonedSlotReleaseAfterMs !== false
90
+ && (typeof abandonedSlotReleaseAfterMs !== "number"
91
+ || !Number.isInteger(abandonedSlotReleaseAfterMs)
92
+ || abandonedSlotReleaseAfterMs < MIN_ABANDONED_SLOT_RELEASE_AFTER_MS
93
+ || abandonedSlotReleaseAfterMs > MAX_ABANDONED_SLOT_RELEASE_AFTER_MS)) {
94
+ throw new Error(`config.capacity.abandonedSlotReleaseAfterMs must be false or an integer from ${MIN_ABANDONED_SLOT_RELEASE_AFTER_MS} to ${MAX_ABANDONED_SLOT_RELEASE_AFTER_MS}`);
95
+ }
96
+ }
97
+
98
+ /** Validate the user-controlled TTL policy before it reaches the exclusion store. */
99
+ // TEST:test/unit/pi-coding-agent-dir.test.ts[loads and applies model exclusion TTL config]
100
+ function validateModelExclusionsConfig(value: unknown): void {
101
+ if (value === undefined) return;
102
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.modelExclusions must be a JSON object");
103
+ const defaultTtlMs = (value as Record<string, unknown>).defaultTtlMs;
104
+ if (defaultTtlMs !== undefined && (typeof defaultTtlMs !== "number" || !Number.isFinite(defaultTtlMs) || defaultTtlMs <= 0 || defaultTtlMs > MAX_MODEL_EXCLUSION_TTL_MS)) {
105
+ throw new Error(`config.modelExclusions.defaultTtlMs must be a finite positive number no greater than ${MAX_MODEL_EXCLUSION_TTL_MS}`);
106
+ }
107
+ }
108
+
65
109
  function validateOrcaProgressTabsConfig(value: unknown): void {
66
110
  if (value === undefined) return;
67
111
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("config.orcaProgressTabs must be a JSON object");
@@ -97,6 +141,7 @@ function validateConfig(config: Record<string, unknown>): void {
97
141
  if (config.defaultSubagentContext !== undefined && config.defaultSubagentContext !== "fresh" && config.defaultSubagentContext !== "fork") {
98
142
  throw new Error('config.defaultSubagentContext must be "fresh" or "fork"');
99
143
  }
144
+ validateForkContextConfig(config.forkContext);
100
145
  if (config.foregroundDetachShortcut !== undefined
101
146
  && (typeof config.foregroundDetachShortcut !== "string" || !isValidKeyId(config.foregroundDetachShortcut))) {
102
147
  throw new Error("config.foregroundDetachShortcut must be a valid keybinding string such as \"ctrl+b\"");
@@ -119,6 +164,8 @@ function validateConfig(config: Record<string, unknown>): void {
119
164
  validateScheduledRunsConfig(config.scheduledRuns);
120
165
  validateFleetKeybindingsConfig(config.fleetKeybindings);
121
166
  validateArtifactConfig(config.artifactConfig);
167
+ validateCapacityConfig(config.capacity);
168
+ validateModelExclusionsConfig(config.modelExclusions);
122
169
  validateMainWindowRendererConfig(config.mainWindowRenderer);
123
170
  validateOrcaProgressTabsConfig(config.orcaProgressTabs);
124
171
  }
@@ -150,6 +197,28 @@ export function updateConfig(updater: (config: ExtensionConfig) => ExtensionConf
150
197
  return next;
151
198
  }
152
199
 
200
+ /**
201
+ * Resolve the default TTL that the process-wide exclusion store should use.
202
+ *
203
+ * @param config Extension configuration after validation.
204
+ * @returns The configured TTL in milliseconds, or the built-in 24-hour default.
205
+ */
206
+ // TEST:test/unit/pi-coding-agent-dir.test.ts[loads and applies model exclusion TTL config]
207
+ export function resolveModelExclusionTTL(config: Pick<ExtensionConfig, "modelExclusions">): number {
208
+ return config.modelExclusions?.defaultTtlMs ?? DEFAULT_MODEL_EXCLUSION_TTL_MS;
209
+ }
210
+
211
+ /**
212
+ * Apply the configured exclusion policy to the process-wide model store.
213
+ *
214
+ * @param config Extension configuration after validation.
215
+ * @returns Nothing.
216
+ */
217
+ // TEST:test/unit/pi-coding-agent-dir.test.ts[loads and applies model exclusion TTL config]
218
+ export function applyModelExclusionsConfig(config: Pick<ExtensionConfig, "modelExclusions">): void {
219
+ setDefaultTTL(resolveModelExclusionTTL(config), { shortenExisting: config.modelExclusions?.defaultTtlMs !== undefined });
220
+ }
221
+
153
222
  export function resolveAsyncByDefault(config: Pick<ExtensionConfig, "asyncByDefault">): boolean {
154
223
  return config.asyncByDefault !== false;
155
224
  }
@@ -159,6 +228,7 @@ export function loadConfig(): ExtensionConfig {
159
228
  try {
160
229
  return readConfigForUpdate(configPath);
161
230
  } catch (error) {
231
+ if (error instanceof PrunedForkConfigError) throw error;
162
232
  console.error(`Failed to load subagent config from '${configPath}':`, error);
163
233
  }
164
234
  return {};
@@ -3,7 +3,7 @@ import * as path from "node:path";
3
3
  import { discoverAgentsAll, type AgentSource } from "../agents/agents.ts";
4
4
  import { isAsyncAvailable } from "../runs/background/async-execution.ts";
5
5
  import { formatSpawnBudgetSummary, getSpawnBudgetSnapshot } from "../runs/shared/spawn-budget.ts";
6
- import { getActiveAsyncCapacitySnapshot, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
6
+ import { getActiveAsyncCapacitySnapshot, resolveAbandonedSlotReleaseAfterMs, resolveMaxActiveAsyncRunsPerSession } from "../runs/background/active-async-capacity.ts";
7
7
  import { decodeRunFanoutBudgetDescriptor, formatRunFanoutBudget, getRunFanoutBudgetSnapshot, RUN_FANOUT_BUDGET_ENV } from "../runs/shared/run-fanout-budget.ts";
8
8
  import { diagnoseIntercomBridge, type IntercomBridgeDiagnostic } from "../intercom/intercom-bridge.ts";
9
9
  import { discoverAvailableSkills, type SkillSource } from "../agents/skills.ts";
@@ -197,13 +197,13 @@ function formatActiveAsyncCapacitySection(input: DoctorReportInput): string[] {
197
197
  const limit = resolveMaxActiveAsyncRunsPerSession(input.config.maxActiveAsyncRunsPerSession);
198
198
  const sessionId = input.currentSessionId ?? input.state.currentSessionId;
199
199
  const snapshot = sessionId
200
- ? getActiveAsyncCapacitySnapshot(sessionId, limit, { liveWorkflowRunIds: new Set(input.state.workflowControllers?.keys() ?? []) })
200
+ ? getActiveAsyncCapacitySnapshot(sessionId, limit, { liveWorkflowRunIds: new Set(input.state.workflowControllers?.keys() ?? []), abandonedSlotReleaseAfterMs: resolveAbandonedSlotReleaseAfterMs(input.config.capacity?.abandonedSlotReleaseAfterMs) })
201
201
  : { used: 0, limit: limit ?? 0 };
202
202
  input.state.activeAsyncCapacity = snapshot;
203
203
  return [
204
204
  `- usage: ${snapshot.used}/${snapshot.limit || "unlimited"} used`,
205
205
  "- scope: top-level async runs in the current parent session; foreground and nested workflow children are not charged again",
206
- "- release: terminal logical state plus verified process exit; missing or unknown cleanup proof retains capacity",
206
+ "- release: terminal state plus matching observed process-terminal proof, or abandoned-timeout for failed runs with a dead runner PID and stale activity when enabled; false keeps unknown-proof slots",
207
207
  ];
208
208
  }
209
209