pi-cohort 2.0.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 (92) hide show
  1. package/CHANGELOG.md +1151 -0
  2. package/LICENSE +22 -0
  3. package/README.md +1220 -0
  4. package/agents/context-builder.md +45 -0
  5. package/agents/delegate.md +12 -0
  6. package/agents/oracle.md +73 -0
  7. package/agents/planner.md +55 -0
  8. package/agents/reviewer.md +91 -0
  9. package/agents/scout.md +50 -0
  10. package/agents/worker.md +67 -0
  11. package/package.json +87 -0
  12. package/prompts/gather-context-and-clarify.md +13 -0
  13. package/prompts/parallel-cleanup.md +59 -0
  14. package/prompts/parallel-context-build.md +55 -0
  15. package/prompts/parallel-handoff-plan.md +61 -0
  16. package/prompts/parallel-review.md +54 -0
  17. package/prompts/review-loop.md +41 -0
  18. package/skills/pi-cohort/SKILL.md +818 -0
  19. package/src/agents/agent-management.ts +685 -0
  20. package/src/agents/agent-scope.ts +6 -0
  21. package/src/agents/agent-selection.ts +23 -0
  22. package/src/agents/agent-serializer.ts +83 -0
  23. package/src/agents/agents.ts +1141 -0
  24. package/src/agents/chain-serializer.ts +251 -0
  25. package/src/agents/frontmatter.ts +29 -0
  26. package/src/agents/identity.ts +30 -0
  27. package/src/agents/skills.ts +632 -0
  28. package/src/extension/config.ts +16 -0
  29. package/src/extension/control-notices.ts +92 -0
  30. package/src/extension/doctor.ts +236 -0
  31. package/src/extension/fanout-child.ts +170 -0
  32. package/src/extension/grand-total.ts +109 -0
  33. package/src/extension/index.ts +630 -0
  34. package/src/extension/schemas.ts +306 -0
  35. package/src/intercom/intercom-bridge.ts +379 -0
  36. package/src/intercom/result-intercom.ts +377 -0
  37. package/src/runs/background/async-execution.ts +796 -0
  38. package/src/runs/background/async-job-tracker.ts +320 -0
  39. package/src/runs/background/async-resume.ts +345 -0
  40. package/src/runs/background/async-status.ts +335 -0
  41. package/src/runs/background/completion-dedupe.ts +63 -0
  42. package/src/runs/background/notify.ts +108 -0
  43. package/src/runs/background/parallel-groups.ts +45 -0
  44. package/src/runs/background/result-watcher.ts +307 -0
  45. package/src/runs/background/run-id-resolver.ts +83 -0
  46. package/src/runs/background/run-status.ts +272 -0
  47. package/src/runs/background/stale-run-reconciler.ts +336 -0
  48. package/src/runs/background/subagent-runner.ts +2326 -0
  49. package/src/runs/background/top-level-async.ts +13 -0
  50. package/src/runs/foreground/chain-clarify.ts +1333 -0
  51. package/src/runs/foreground/chain-execution.ts +1187 -0
  52. package/src/runs/foreground/execution.ts +1028 -0
  53. package/src/runs/foreground/subagent-executor.ts +2580 -0
  54. package/src/runs/shared/acceptance.ts +605 -0
  55. package/src/runs/shared/chain-outputs.ts +101 -0
  56. package/src/runs/shared/completion-guard.ts +143 -0
  57. package/src/runs/shared/dynamic-fanout.ts +293 -0
  58. package/src/runs/shared/long-running-guard.ts +175 -0
  59. package/src/runs/shared/model-fallback.ts +103 -0
  60. package/src/runs/shared/nested-events.ts +822 -0
  61. package/src/runs/shared/nested-path.ts +52 -0
  62. package/src/runs/shared/nested-render.ts +115 -0
  63. package/src/runs/shared/parallel-utils.ts +136 -0
  64. package/src/runs/shared/pi-args.ts +221 -0
  65. package/src/runs/shared/pi-spawn.ts +115 -0
  66. package/src/runs/shared/run-history.ts +60 -0
  67. package/src/runs/shared/single-output.ts +164 -0
  68. package/src/runs/shared/structured-output.ts +77 -0
  69. package/src/runs/shared/subagent-control.ts +287 -0
  70. package/src/runs/shared/subagent-prompt-runtime.ts +220 -0
  71. package/src/runs/shared/workflow-graph.ts +206 -0
  72. package/src/runs/shared/worktree.ts +577 -0
  73. package/src/shared/artifacts.ts +98 -0
  74. package/src/shared/atomic-json.ts +16 -0
  75. package/src/shared/file-coalescer.ts +40 -0
  76. package/src/shared/fork-context.ts +76 -0
  77. package/src/shared/formatters.ts +133 -0
  78. package/src/shared/jsonl-writer.ts +81 -0
  79. package/src/shared/model-info.ts +78 -0
  80. package/src/shared/post-exit-stdio-guard.ts +85 -0
  81. package/src/shared/session-identity.ts +10 -0
  82. package/src/shared/session-tokens.ts +46 -0
  83. package/src/shared/settings.ts +447 -0
  84. package/src/shared/status-format.ts +59 -0
  85. package/src/shared/types.ts +1072 -0
  86. package/src/shared/utils.ts +451 -0
  87. package/src/slash/prompt-template-bridge.ts +397 -0
  88. package/src/slash/slash-bridge.ts +174 -0
  89. package/src/slash/slash-commands.ts +567 -0
  90. package/src/slash/slash-live-state.ts +292 -0
  91. package/src/tui/render-helpers.ts +80 -0
  92. package/src/tui/render.ts +1476 -0
@@ -0,0 +1,52 @@
1
+ import * as path from "node:path";
2
+
3
+ const MAX_NESTED_ID_LENGTH = 128;
4
+ export const MAX_NESTED_PATH_ENTRIES = 4;
5
+
6
+ export type NestedPathEntry = { runId: string; stepIndex?: number; agent?: string };
7
+
8
+ export function isSafeNestedPathId(value: unknown): value is string {
9
+ return typeof value === "string"
10
+ && value.length > 0
11
+ && value.length <= MAX_NESTED_ID_LENGTH
12
+ && !path.isAbsolute(value)
13
+ && !value.includes("/")
14
+ && !value.includes("\\")
15
+ && !value.includes("..");
16
+ }
17
+
18
+ function finiteNumber(value: unknown): number | undefined {
19
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
20
+ }
21
+
22
+ function nonEmptyString(value: unknown, max: number): string | undefined {
23
+ return typeof value === "string" && value.length > 0 ? value.slice(0, max) : undefined;
24
+ }
25
+
26
+ export function sanitizeNestedPath(value: unknown): NestedPathEntry[] {
27
+ if (!Array.isArray(value)) return [];
28
+ return value.map((part) => {
29
+ if (!part || typeof part !== "object") return undefined;
30
+ const record = part as Record<string, unknown>;
31
+ if (!isSafeNestedPathId(record.runId)) return undefined;
32
+ return {
33
+ runId: record.runId,
34
+ ...(finiteNumber(record.stepIndex) !== undefined ? { stepIndex: finiteNumber(record.stepIndex) } : {}),
35
+ ...(nonEmptyString(record.agent, 128) ? { agent: nonEmptyString(record.agent, 128) } : {}),
36
+ };
37
+ }).filter((part): part is NestedPathEntry => Boolean(part)).slice(0, MAX_NESTED_PATH_ENTRIES);
38
+ }
39
+
40
+ export function parseNestedPathEnv(value: string | undefined): NestedPathEntry[] {
41
+ if (!value) return [];
42
+ try {
43
+ return sanitizeNestedPath(JSON.parse(value) as unknown);
44
+ } catch {
45
+ return [];
46
+ }
47
+ }
48
+
49
+ export function encodeNestedPathEnv(value: NestedPathEntry[]): string {
50
+ const sanitized = sanitizeNestedPath(value);
51
+ return sanitized.length ? JSON.stringify(sanitized) : "";
52
+ }
@@ -0,0 +1,115 @@
1
+ import { formatDuration, formatTokens, shortenPath } from "../../shared/formatters.ts";
2
+ import { formatActivityLabel } from "../../shared/status-format.ts";
3
+ import type { ActivityState, NestedRunSummary, NestedStepSummary } from "../../shared/types.ts";
4
+
5
+ export interface NestedRunCounts {
6
+ total: number;
7
+ running: number;
8
+ paused: number;
9
+ complete: number;
10
+ failed: number;
11
+ queued: number;
12
+ }
13
+
14
+ export function countNestedRuns(children: NestedRunSummary[] | undefined): NestedRunCounts {
15
+ const counts: NestedRunCounts = { total: 0, running: 0, paused: 0, complete: 0, failed: 0, queued: 0 };
16
+ for (const child of children ?? []) {
17
+ counts.total++;
18
+ counts[child.state]++;
19
+ const nested = countNestedRuns([...(child.children ?? []), ...(child.steps?.flatMap((step) => step.children ?? []) ?? [])]);
20
+ counts.total += nested.total;
21
+ counts.running += nested.running;
22
+ counts.paused += nested.paused;
23
+ counts.complete += nested.complete;
24
+ counts.failed += nested.failed;
25
+ counts.queued += nested.queued;
26
+ }
27
+ return counts;
28
+ }
29
+
30
+ export function formatNestedAggregate(children: NestedRunSummary[] | undefined): string | undefined {
31
+ const counts = countNestedRuns(children);
32
+ if (counts.total === 0) return undefined;
33
+ const parts = [
34
+ counts.running > 0 ? `${counts.running} running` : "",
35
+ counts.paused > 0 ? `${counts.paused} paused` : "",
36
+ counts.failed > 0 ? `${counts.failed} failed` : "",
37
+ counts.complete > 0 ? `${counts.complete} complete` : "",
38
+ counts.queued > 0 ? `${counts.queued} queued` : "",
39
+ ].filter(Boolean);
40
+ return `+${counts.total} nested run${counts.total === 1 ? "" : "s"}${parts.length ? ` (${parts.join(", ")})` : ""}`;
41
+ }
42
+
43
+ function nestedRunLabel(run: NestedRunSummary): string {
44
+ if (run.agent) return run.agent;
45
+ if (run.agents?.length) return run.agents.length === 1 ? run.agents[0]! : `${run.agents.slice(0, 2).join(", ")}${run.agents.length > 2 ? ` +${run.agents.length - 2}` : ""}`;
46
+ return run.id;
47
+ }
48
+
49
+ function formatNestedActivity(input: {
50
+ activityState?: ActivityState;
51
+ lastActivityAt?: number;
52
+ currentTool?: string;
53
+ currentToolStartedAt?: number;
54
+ currentPath?: string;
55
+ turnCount?: number;
56
+ toolCount?: number;
57
+ totalTokens?: NestedRunSummary["totalTokens"];
58
+ }): string | undefined {
59
+ const facts: string[] = [];
60
+ if (input.currentTool && input.currentToolStartedAt !== undefined) facts.push(`tool ${input.currentTool} ${formatDuration(Math.max(0, Date.now() - input.currentToolStartedAt))}`);
61
+ else if (input.currentTool) facts.push(`tool ${input.currentTool}`);
62
+ if (input.currentPath) facts.push(shortenPath(input.currentPath));
63
+ if (input.turnCount !== undefined) facts.push(`${input.turnCount} turns`);
64
+ if (input.toolCount !== undefined) facts.push(`${input.toolCount} tools`);
65
+ if (input.totalTokens) facts.push(`${formatTokens(input.totalTokens.total)} tok`);
66
+ const activity = formatActivityLabel(input.lastActivityAt, input.activityState as ActivityState | undefined);
67
+ return activity || facts.length ? [activity, ...facts].filter(Boolean).join(" | ") : undefined;
68
+ }
69
+
70
+ function formatNestedRunLines(children: NestedRunSummary[] | undefined, options: { indent: string; maxDepth: number; maxLines: number; commandHints?: boolean }): string[] {
71
+ const lines: string[] = [];
72
+ const append = (items: NestedRunSummary[] | undefined, depth: number, indent: string): void => {
73
+ if (!items?.length || lines.length >= options.maxLines) return;
74
+ if (depth > options.maxDepth) {
75
+ const aggregate = formatNestedAggregate(items);
76
+ if (aggregate && lines.length < options.maxLines) lines.push(`${indent}↳ ${aggregate}`);
77
+ return;
78
+ }
79
+ for (let index = 0; index < items.length; index++) {
80
+ const child = items[index]!;
81
+ if (lines.length >= options.maxLines) {
82
+ const aggregate = formatNestedAggregate(items.slice(index));
83
+ if (aggregate) lines[lines.length - 1] = `${indent}↳ ${aggregate}`;
84
+ return;
85
+ }
86
+ const activity = child.state === "running" ? formatNestedActivity(child) : undefined;
87
+ const error = child.error ? ` | error: ${child.error}` : "";
88
+ lines.push(`${indent}↳ ${nestedRunLabel(child)} [${child.id}] ${child.state}${activity ? ` | ${activity}` : ""}${error}`);
89
+ if (options.commandHints && lines.length < options.maxLines) lines.push(`${indent} Status: subagent({ action: "status", id: "${child.id}" })`);
90
+ if (depth === options.maxDepth) {
91
+ const aggregate = formatNestedAggregate([...(child.steps?.flatMap((step) => step.children ?? []) ?? []), ...(child.children ?? [])]);
92
+ if (aggregate && lines.length < options.maxLines) lines.push(`${indent} ↳ ${aggregate}`);
93
+ continue;
94
+ }
95
+ for (const [stepIndex, step] of (child.steps ?? []).entries()) {
96
+ if (lines.length >= options.maxLines) return;
97
+ const stepActivity = step.status === "running" ? formatNestedActivity(step) : undefined;
98
+ lines.push(`${indent} ${stepIndex + 1}. ${step.agent} ${step.status}${stepActivity ? ` | ${stepActivity}` : ""}${step.error ? ` | error: ${step.error}` : ""}`);
99
+ append(step.children, depth + 1, `${indent} `);
100
+ }
101
+ append(child.children, depth + 1, `${indent} `);
102
+ }
103
+ };
104
+ append(children, 0, options.indent);
105
+ return lines;
106
+ }
107
+
108
+ export function formatNestedRunStatusLines(children: NestedRunSummary[] | undefined, options: { indent?: string; maxDepth?: number; maxLines?: number; commandHints?: boolean } = {}): string[] {
109
+ return formatNestedRunLines(children, {
110
+ indent: options.indent ?? " ",
111
+ maxDepth: options.maxDepth ?? 2,
112
+ maxLines: options.maxLines ?? 40,
113
+ commandHints: options.commandHints ?? false,
114
+ });
115
+ }
@@ -0,0 +1,136 @@
1
+ export interface RunnerSubagentStep {
2
+ agent: string;
3
+ task: string;
4
+ phase?: string;
5
+ label?: string;
6
+ outputName?: string;
7
+ structured?: boolean;
8
+ cwd?: string;
9
+ model?: string;
10
+ thinking?: string;
11
+ modelCandidates?: string[];
12
+ tools?: string[];
13
+ extensions?: string[];
14
+ completionGuard?: boolean;
15
+ systemPrompt?: string | null;
16
+ systemPromptMode?: "append" | "replace";
17
+ inheritProjectContext: boolean;
18
+ inheritSkills: boolean;
19
+ skills?: string[];
20
+ outputPath?: string;
21
+ outputMode?: "inline" | "file-only";
22
+ sessionFile?: string;
23
+ maxSubagentDepth?: number;
24
+ structuredOutput?: {
25
+ schema: import("../../shared/types.ts").JsonSchemaObject;
26
+ schemaPath: string;
27
+ outputPath: string;
28
+ };
29
+ structuredOutputSchema?: import("../../shared/types.ts").JsonSchemaObject;
30
+ effectiveAcceptance?: import("../../shared/types.ts").ResolvedAcceptanceConfig;
31
+ }
32
+
33
+ export interface ParallelStepGroup {
34
+ parallel: RunnerSubagentStep[];
35
+ concurrency?: number;
36
+ failFast?: boolean;
37
+ worktree?: boolean;
38
+ }
39
+
40
+ export interface DynamicRunnerGroup {
41
+ expand: import("../../shared/settings.ts").DynamicExpandSpec;
42
+ parallel: RunnerSubagentStep;
43
+ collect: import("../../shared/settings.ts").DynamicCollectSpec;
44
+ concurrency?: number;
45
+ failFast?: boolean;
46
+ phase?: string;
47
+ label?: string;
48
+ effectiveAcceptance?: import("../../shared/types.ts").ResolvedAcceptanceConfig;
49
+ }
50
+
51
+ export type RunnerStep = RunnerSubagentStep | ParallelStepGroup | DynamicRunnerGroup;
52
+
53
+ export function isParallelGroup(step: RunnerStep): step is ParallelStepGroup {
54
+ return "parallel" in step && Array.isArray(step.parallel);
55
+ }
56
+
57
+ export function isDynamicRunnerGroup(step: RunnerStep): step is DynamicRunnerGroup {
58
+ return "expand" in step && "collect" in step && "parallel" in step && !Array.isArray((step as { parallel?: unknown }).parallel);
59
+ }
60
+
61
+ export function flattenSteps(steps: RunnerStep[]): RunnerSubagentStep[] {
62
+ const flat: RunnerSubagentStep[] = [];
63
+ for (const step of steps) {
64
+ if (isParallelGroup(step)) {
65
+ for (const task of step.parallel) flat.push(task);
66
+ } else if (isDynamicRunnerGroup(step)) {
67
+ continue;
68
+ } else {
69
+ flat.push(step);
70
+ }
71
+ }
72
+ return flat;
73
+ }
74
+
75
+ export async function mapConcurrent<T, R>(
76
+ items: T[],
77
+ limit: number,
78
+ fn: (item: T, i: number) => Promise<R>,
79
+ ): Promise<R[]> {
80
+ const safeLimit = Math.max(1, Math.floor(limit) || 1);
81
+ const results: R[] = new Array(items.length);
82
+ let next = 0;
83
+
84
+ async function worker(_workerIndex: number): Promise<void> {
85
+ while (next < items.length) {
86
+ const i = next++;
87
+ results[i] = await fn(items[i], i);
88
+ }
89
+ }
90
+
91
+ await Promise.all(
92
+ Array.from({ length: Math.min(safeLimit, items.length) }, (_, wi) => worker(wi)),
93
+ );
94
+ return results;
95
+ }
96
+
97
+ export interface ParallelTaskResult {
98
+ agent: string;
99
+ taskIndex?: number;
100
+ output: string;
101
+ exitCode: number | null;
102
+ error?: string;
103
+ model?: string;
104
+ attemptedModels?: string[];
105
+ outputTargetPath?: string;
106
+ outputTargetExists?: boolean;
107
+ }
108
+
109
+ export function aggregateParallelOutputs(
110
+ results: ParallelTaskResult[],
111
+ headerFormat: (index: number, agent: string) => string = (i, agent) =>
112
+ `=== Parallel Task ${i + 1} (${agent}) ===`,
113
+ ): string {
114
+ return results
115
+ .map((r, i) => {
116
+ const header = headerFormat(r.taskIndex ?? i, r.agent);
117
+ const hasOutput = Boolean(r.output?.trim());
118
+ const status =
119
+ r.exitCode === -1
120
+ ? "SKIPPED"
121
+ : r.exitCode !== 0 && r.exitCode !== null
122
+ ? `FAILED (exit code ${r.exitCode})${r.error ? `: ${r.error}` : ""}`
123
+ : r.error
124
+ ? `WARNING: ${r.error}`
125
+ : !hasOutput && r.outputTargetPath && r.outputTargetExists === false
126
+ ? `EMPTY OUTPUT (expected output file missing: ${r.outputTargetPath})`
127
+ : !hasOutput && !r.outputTargetPath
128
+ ? "EMPTY OUTPUT (no textual response returned)"
129
+ : "";
130
+ const body = status ? (hasOutput ? `${status}\n${r.output}` : status) : r.output;
131
+ return `${header}\n${body}`;
132
+ })
133
+ .join("\n\n");
134
+ }
135
+
136
+ export const MAX_PARALLEL_CONCURRENCY = 4;
@@ -0,0 +1,221 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { encodeNestedPathEnv, parseNestedPathEnv, type NestedPathEntry } from "./nested-path.ts";
6
+ import { STRUCTURED_OUTPUT_CAPTURE_ENV, STRUCTURED_OUTPUT_SCHEMA_ENV } from "./structured-output.ts";
7
+ import type { JsonSchemaObject } from "../../shared/types.ts";
8
+
9
+ const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
10
+ const TASK_ARG_LIMIT = 8000;
11
+ const PROMPT_RUNTIME_EXTENSION_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "subagent-prompt-runtime.ts");
12
+ const FANOUT_CHILD_EXTENSION_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "extension", "fanout-child.ts");
13
+ export const SUBAGENT_CHILD_ENV = "PI_SUBAGENT_CHILD";
14
+ export const SUBAGENT_ORCHESTRATOR_TARGET_ENV = "PI_SUBAGENT_ORCHESTRATOR_TARGET";
15
+ export const SUBAGENT_RUN_ID_ENV = "PI_SUBAGENT_RUN_ID";
16
+ export const SUBAGENT_CHILD_AGENT_ENV = "PI_SUBAGENT_CHILD_AGENT";
17
+ export const SUBAGENT_CHILD_INDEX_ENV = "PI_SUBAGENT_CHILD_INDEX";
18
+ export const SUBAGENT_FANOUT_CHILD_ENV = "PI_SUBAGENT_FANOUT_CHILD";
19
+ export const SUBAGENT_PARENT_EVENT_SINK_ENV = "PI_SUBAGENT_PARENT_EVENT_SINK";
20
+ export const SUBAGENT_PARENT_CONTROL_INBOX_ENV = "PI_SUBAGENT_PARENT_CONTROL_INBOX";
21
+ export const SUBAGENT_PARENT_ROOT_RUN_ID_ENV = "PI_SUBAGENT_PARENT_ROOT_RUN_ID";
22
+ export const SUBAGENT_PARENT_RUN_ID_ENV = "PI_SUBAGENT_PARENT_RUN_ID";
23
+ export const SUBAGENT_PARENT_CHILD_INDEX_ENV = "PI_SUBAGENT_PARENT_CHILD_INDEX";
24
+ export const SUBAGENT_PARENT_DEPTH_ENV = "PI_SUBAGENT_PARENT_DEPTH";
25
+ export const SUBAGENT_PARENT_PATH_ENV = "PI_SUBAGENT_PARENT_PATH";
26
+ export const SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV = "PI_SUBAGENT_PARENT_CAPABILITY_TOKEN";
27
+
28
+ interface BuildPiArgsInput {
29
+ baseArgs: string[];
30
+ task: string;
31
+ sessionEnabled: boolean;
32
+ sessionDir?: string;
33
+ sessionFile?: string;
34
+ model?: string;
35
+ thinking?: string;
36
+ systemPromptMode?: "append" | "replace";
37
+ inheritProjectContext: boolean;
38
+ inheritSkills: boolean;
39
+ tools?: string[];
40
+ extensions?: string[];
41
+ systemPrompt?: string | null;
42
+ cwd?: string;
43
+ promptFileStem?: string;
44
+ intercomSessionName?: string;
45
+ orchestratorIntercomTarget?: string;
46
+ runId?: string;
47
+ childAgentName?: string;
48
+ childIndex?: number;
49
+ parentEventSink?: string;
50
+ parentControlInbox?: string;
51
+ parentRootRunId?: string;
52
+ parentRunId?: string;
53
+ parentChildIndex?: number;
54
+ parentDepth?: number;
55
+ parentPath?: NestedPathEntry[];
56
+ parentCapabilityToken?: string;
57
+ structuredOutput?: {
58
+ schema: JsonSchemaObject;
59
+ schemaPath: string;
60
+ outputPath: string;
61
+ };
62
+ }
63
+
64
+ interface BuildPiArgsResult {
65
+ args: string[];
66
+ env: Record<string, string | undefined>;
67
+ tempDir?: string;
68
+ }
69
+
70
+ export function applyThinkingSuffix(model: string | undefined, thinking: string | undefined): string | undefined {
71
+ if (!model || !thinking || thinking === "off") return model;
72
+ const colonIdx = model.lastIndexOf(":");
73
+ if (colonIdx !== -1 && THINKING_LEVELS.includes(model.substring(colonIdx + 1))) return model;
74
+ return `${model}:${thinking}`;
75
+ }
76
+
77
+ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
78
+ const args = [...input.baseArgs];
79
+
80
+ if (input.sessionFile) {
81
+ fs.mkdirSync(path.dirname(input.sessionFile), { recursive: true });
82
+ args.push("--session", input.sessionFile);
83
+ } else {
84
+ if (!input.sessionEnabled) {
85
+ args.push("--no-session");
86
+ }
87
+ if (input.sessionDir) {
88
+ fs.mkdirSync(input.sessionDir, { recursive: true });
89
+ args.push("--session-dir", input.sessionDir);
90
+ }
91
+ }
92
+
93
+ const modelArg = applyThinkingSuffix(input.model, input.thinking);
94
+ if (modelArg) {
95
+ args.push("--model", modelArg);
96
+ }
97
+
98
+ const declaredBuiltinTools = input.tools?.filter((tool) => !(tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js"))) ?? [];
99
+ const fanoutAuthorized = declaredBuiltinTools.includes("subagent");
100
+ const toolExtensionPaths: string[] = [];
101
+ if (input.tools?.length) {
102
+ const builtinTools = [...declaredBuiltinTools];
103
+ for (const tool of input.tools) {
104
+ if (!declaredBuiltinTools.includes(tool) && (tool.includes("/") || tool.endsWith(".ts") || tool.endsWith(".js"))) {
105
+ toolExtensionPaths.push(tool);
106
+ }
107
+ }
108
+ if (builtinTools.length > 0) {
109
+ args.push("--tools", builtinTools.join(","));
110
+ }
111
+ }
112
+
113
+ const runtimeExtensions = fanoutAuthorized
114
+ ? [PROMPT_RUNTIME_EXTENSION_PATH, FANOUT_CHILD_EXTENSION_PATH]
115
+ : [PROMPT_RUNTIME_EXTENSION_PATH];
116
+ if (input.extensions !== undefined) {
117
+ args.push("--no-extensions");
118
+ for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths, ...input.extensions])]) {
119
+ args.push("--extension", extPath);
120
+ }
121
+ } else {
122
+ for (const extPath of [...new Set([...runtimeExtensions, ...toolExtensionPaths])]) {
123
+ args.push("--extension", extPath);
124
+ }
125
+ }
126
+
127
+ if (!input.inheritSkills) {
128
+ args.push("--no-skills");
129
+ }
130
+
131
+ let tempDir: string | undefined;
132
+ if (input.systemPrompt !== undefined && input.systemPrompt !== null) {
133
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
134
+ const stem = (input.promptFileStem ?? "prompt").replace(/[^\w.-]/g, "_");
135
+ const promptPath = path.join(tempDir, `${stem}.md`);
136
+ fs.writeFileSync(promptPath, input.systemPrompt, { mode: 0o600 });
137
+ args.push(input.systemPromptMode === "replace" ? "--system-prompt" : "--append-system-prompt", promptPath);
138
+ }
139
+
140
+ if (input.task.length > TASK_ARG_LIMIT) {
141
+ if (!tempDir) {
142
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-"));
143
+ }
144
+ const taskFilePath = path.join(tempDir, "task.md");
145
+ fs.writeFileSync(taskFilePath, `Task: ${input.task}`, { mode: 0o600 });
146
+ args.push(`@${taskFilePath}`);
147
+ } else {
148
+ args.push(`Task: ${input.task}`);
149
+ }
150
+
151
+ const env: Record<string, string | undefined> = {};
152
+ env[SUBAGENT_CHILD_ENV] = "1";
153
+ env[SUBAGENT_FANOUT_CHILD_ENV] = fanoutAuthorized ? "1" : "0";
154
+ const inheritedNestedRoute = Boolean(process.env[SUBAGENT_PARENT_EVENT_SINK_ENV] && process.env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] && process.env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV]);
155
+ const parentRunId = input.parentRunId ?? input.runId ?? (inheritedNestedRoute ? process.env[SUBAGENT_RUN_ID_ENV] : undefined) ?? process.env[SUBAGENT_PARENT_RUN_ID_ENV] ?? "";
156
+ const parentChildIndex = input.parentChildIndex !== undefined
157
+ ? String(input.parentChildIndex)
158
+ : input.childIndex !== undefined
159
+ ? String(input.childIndex)
160
+ : process.env[SUBAGENT_PARENT_CHILD_INDEX_ENV] ?? "";
161
+ const inheritedDepth = Number(process.env[SUBAGENT_PARENT_DEPTH_ENV]);
162
+ const parentDepth = input.parentDepth ?? (inheritedNestedRoute && Number.isFinite(inheritedDepth) ? inheritedDepth + 1 : 1);
163
+ const parentPath = input.parentPath ?? [
164
+ ...parseNestedPathEnv(process.env[SUBAGENT_PARENT_PATH_ENV]),
165
+ ...(parentRunId ? [{
166
+ runId: parentRunId,
167
+ ...(parentChildIndex && /^\d+$/.test(parentChildIndex) ? { stepIndex: Number(parentChildIndex) } : {}),
168
+ ...(input.childAgentName ? { agent: input.childAgentName } : {}),
169
+ }] : []),
170
+ ];
171
+ env[SUBAGENT_PARENT_EVENT_SINK_ENV] = fanoutAuthorized
172
+ ? input.parentEventSink ?? process.env[SUBAGENT_PARENT_EVENT_SINK_ENV] ?? ""
173
+ : "";
174
+ env[SUBAGENT_PARENT_CONTROL_INBOX_ENV] = fanoutAuthorized
175
+ ? input.parentControlInbox ?? process.env[SUBAGENT_PARENT_CONTROL_INBOX_ENV] ?? ""
176
+ : "";
177
+ env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] = fanoutAuthorized
178
+ ? input.parentRootRunId ?? process.env[SUBAGENT_PARENT_ROOT_RUN_ID_ENV] ?? input.runId ?? ""
179
+ : "";
180
+ env[SUBAGENT_PARENT_RUN_ID_ENV] = fanoutAuthorized ? parentRunId : "";
181
+ env[SUBAGENT_PARENT_CHILD_INDEX_ENV] = fanoutAuthorized ? parentChildIndex : "";
182
+ env[SUBAGENT_PARENT_DEPTH_ENV] = fanoutAuthorized ? String(parentDepth) : "";
183
+ env[SUBAGENT_PARENT_PATH_ENV] = fanoutAuthorized ? encodeNestedPathEnv(parentPath) : "";
184
+ env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV] = fanoutAuthorized
185
+ ? input.parentCapabilityToken ?? process.env[SUBAGENT_PARENT_CAPABILITY_TOKEN_ENV] ?? ""
186
+ : "";
187
+ env.PI_SUBAGENT_INHERIT_PROJECT_CONTEXT = input.inheritProjectContext ? "1" : "0";
188
+ env.PI_SUBAGENT_INHERIT_SKILLS = input.inheritSkills ? "1" : "0";
189
+ if (input.intercomSessionName) {
190
+ env.PI_SUBAGENT_INTERCOM_SESSION_NAME = input.intercomSessionName;
191
+ }
192
+ if (input.orchestratorIntercomTarget) {
193
+ env[SUBAGENT_ORCHESTRATOR_TARGET_ENV] = input.orchestratorIntercomTarget;
194
+ }
195
+ if (input.runId) {
196
+ env[SUBAGENT_RUN_ID_ENV] = input.runId;
197
+ }
198
+ if (input.childAgentName) {
199
+ env[SUBAGENT_CHILD_AGENT_ENV] = input.childAgentName;
200
+ }
201
+ if (input.childIndex !== undefined) {
202
+ env[SUBAGENT_CHILD_INDEX_ENV] = String(input.childIndex);
203
+ }
204
+ if (input.structuredOutput) {
205
+ env[STRUCTURED_OUTPUT_CAPTURE_ENV] = input.structuredOutput.outputPath;
206
+ env[STRUCTURED_OUTPUT_SCHEMA_ENV] = input.structuredOutput.schemaPath;
207
+ }
208
+
209
+ return { args, env, tempDir };
210
+ }
211
+
212
+ export const parseParentPathEnv = parseNestedPathEnv;
213
+
214
+ export function cleanupTempDir(tempDir: string | null | undefined): void {
215
+ if (!tempDir) return;
216
+ try {
217
+ fs.rmSync(tempDir, { recursive: true, force: true });
218
+ } catch {
219
+ // Temp cleanup is best effort.
220
+ }
221
+ }
@@ -0,0 +1,115 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ export const PI_CODING_AGENT_PACKAGE = "@earendil-works/pi-coding-agent";
6
+
7
+ export function findPiPackageRootFromEntry(entryPoint: string): string | undefined {
8
+ let dir = path.dirname(entryPoint);
9
+ while (dir !== path.dirname(dir)) {
10
+ const packageJsonPath = path.join(dir, "package.json");
11
+ if (fs.existsSync(packageJsonPath)) {
12
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as { name?: unknown };
13
+ if (pkg.name === PI_CODING_AGENT_PACKAGE) return dir;
14
+ }
15
+ dir = path.dirname(dir);
16
+ }
17
+ return undefined;
18
+ }
19
+
20
+ export function resolveInstalledPiPackageRoot(): string | undefined {
21
+ return findPiPackageRootFromEntry(fileURLToPath(import.meta.resolve(PI_CODING_AGENT_PACKAGE)));
22
+ }
23
+
24
+ export function resolvePiPackageRoot(): string | undefined {
25
+ try {
26
+ const entry = process.argv[1];
27
+ return entry ? findPiPackageRootFromEntry(fs.realpathSync(entry)) : undefined;
28
+ } catch {
29
+ // process.argv[1] probing is best-effort; callers can fall back to PATH/package resolution.
30
+ return undefined;
31
+ }
32
+ }
33
+
34
+ export interface PiSpawnDeps {
35
+ platform?: NodeJS.Platform;
36
+ execPath?: string;
37
+ argv1?: string;
38
+ existsSync?: (filePath: string) => boolean;
39
+ readFileSync?: (filePath: string, encoding: "utf-8") => string;
40
+ resolvePackageJson?: () => string;
41
+ resolvePackageEntry?: () => string;
42
+ piPackageRoot?: string;
43
+ }
44
+
45
+ interface PiSpawnCommand {
46
+ command: string;
47
+ args: string[];
48
+ }
49
+
50
+ function isRunnableNodeScript(filePath: string, existsSync: (filePath: string) => boolean): boolean {
51
+ if (!existsSync(filePath)) return false;
52
+ return /\.(?:mjs|cjs|js)$/i.test(filePath);
53
+ }
54
+
55
+ function normalizePath(filePath: string): string {
56
+ return path.isAbsolute(filePath) ? filePath : path.resolve(filePath);
57
+ }
58
+
59
+ export function resolveWindowsPiCliScript(deps: PiSpawnDeps = {}): string | undefined {
60
+ const existsSync = deps.existsSync ?? fs.existsSync;
61
+ const readFileSync = deps.readFileSync ?? ((filePath, encoding) => fs.readFileSync(filePath, encoding));
62
+ const argv1 = deps.argv1 ?? process.argv[1];
63
+
64
+ if (argv1) {
65
+ const argvPath = normalizePath(argv1);
66
+ if (isRunnableNodeScript(argvPath, existsSync)) {
67
+ return argvPath;
68
+ }
69
+ }
70
+
71
+ try {
72
+ const resolvePackageJson = deps.resolvePackageJson ?? (() => {
73
+ const root = deps.piPackageRoot ?? resolvePiPackageRoot();
74
+ if (root) return path.join(root, "package.json");
75
+ const packageRoot = deps.resolvePackageEntry
76
+ ? findPiPackageRootFromEntry(deps.resolvePackageEntry())
77
+ : resolveInstalledPiPackageRoot();
78
+ if (!packageRoot) throw new Error(`Could not resolve ${PI_CODING_AGENT_PACKAGE} package root`);
79
+ return path.join(packageRoot, "package.json");
80
+ });
81
+ const packageJsonPath = resolvePackageJson();
82
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")) as {
83
+ bin?: string | Record<string, string>;
84
+ };
85
+ const binField = packageJson.bin;
86
+ const binPath = typeof binField === "string"
87
+ ? binField
88
+ : binField?.pi ?? Object.values(binField ?? {})[0];
89
+ if (!binPath) return undefined;
90
+ const candidate = path.resolve(path.dirname(packageJsonPath), binPath);
91
+ if (isRunnableNodeScript(candidate, existsSync)) {
92
+ return candidate;
93
+ }
94
+ } catch {
95
+ // Windows CLI resolution is optional; falling back to `pi` lets PATH handle execution.
96
+ return undefined;
97
+ }
98
+
99
+ return undefined;
100
+ }
101
+
102
+ export function getPiSpawnCommand(args: string[], deps: PiSpawnDeps = {}): PiSpawnCommand {
103
+ const platform = deps.platform ?? process.platform;
104
+ if (platform === "win32") {
105
+ const piCliPath = resolveWindowsPiCliScript(deps);
106
+ if (piCliPath) {
107
+ return {
108
+ command: deps.execPath ?? process.execPath,
109
+ args: [piCliPath, ...args],
110
+ };
111
+ }
112
+ }
113
+
114
+ return { command: "pi", args };
115
+ }