@pi-unipi/subagents 2.6.1 → 2.6.2

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 (107) hide show
  1. package/README.md +89 -78
  2. package/agents/delegate.md +14 -0
  3. package/agents/oracle.md +78 -0
  4. package/agents/researcher.md +52 -0
  5. package/agents/reviewer.md +79 -0
  6. package/agents/scout.md +50 -0
  7. package/agents/worker.md +59 -0
  8. package/package.json +19 -17
  9. package/prompts/council.md +48 -0
  10. package/prompts/gather-context-and-clarify.md +13 -0
  11. package/prompts/parallel-cleanup.md +59 -0
  12. package/prompts/parallel-research.md +50 -0
  13. package/prompts/parallel-review.md +54 -0
  14. package/prompts/review-loop.md +43 -0
  15. package/skills/council-mode/SKILL.md +230 -0
  16. package/skills/subagents/SKILL.md +49 -0
  17. package/skills/subagents/references/constraints-and-recipes.md +259 -0
  18. package/skills/subagents/references/execution-controls.md +454 -0
  19. package/skills/subagents/references/management-authoring-rpc.md +161 -0
  20. package/skills/subagents/references/multi-lane-orchestration.md +39 -0
  21. package/skills/subagents/references/prompting-and-roles.md +267 -0
  22. package/src/acceptance.ts +331 -0
  23. package/src/agent-manager.ts +405 -0
  24. package/src/agent-memory.ts +300 -0
  25. package/src/agent-overrides.ts +183 -0
  26. package/src/agent-runner.ts +265 -0
  27. package/src/async-runner.ts +417 -0
  28. package/src/authority-policy.ts +47 -0
  29. package/src/budgets.ts +285 -0
  30. package/src/child-safety.ts +140 -0
  31. package/src/config.ts +226 -0
  32. package/src/conversation-viewer.ts +281 -0
  33. package/src/core-compat.ts +94 -0
  34. package/src/custom-agents.ts +323 -0
  35. package/src/file-system-retry.ts +50 -0
  36. package/src/fleet-data.ts +61 -0
  37. package/src/fleet-view.ts +316 -0
  38. package/src/foreground-detach.ts +59 -0
  39. package/src/fork-context.ts +285 -0
  40. package/src/global.d.ts +7 -0
  41. package/src/guide.ts +129 -0
  42. package/src/index.ts +1149 -0
  43. package/src/mission-state.ts +133 -0
  44. package/src/mission-store.ts +434 -0
  45. package/src/model-resolver.ts +79 -0
  46. package/src/output-limits.ts +142 -0
  47. package/src/parity-types.ts +315 -0
  48. package/src/pi-args.ts +161 -0
  49. package/src/pi-spawn.ts +147 -0
  50. package/src/result-files.ts +232 -0
  51. package/src/result-watcher.ts +221 -0
  52. package/src/retained-children.ts +176 -0
  53. package/src/run-fanout-budget.ts +356 -0
  54. package/src/scheduled-runs.ts +351 -0
  55. package/src/schemas.ts +263 -0
  56. package/src/slash-commands.ts +77 -0
  57. package/src/supervisor-channel.ts +249 -0
  58. package/src/tool-handler.ts +1144 -0
  59. package/src/types.ts +238 -0
  60. package/src/widget.ts +447 -0
  61. package/src/workflow-script.ts +737 -0
  62. package/src/workflow-worker.ts +384 -0
  63. package/src/worktree.ts +614 -0
  64. package/dist/agent-manager.d.ts +0 -81
  65. package/dist/agent-manager.d.ts.map +0 -1
  66. package/dist/agent-manager.js +0 -295
  67. package/dist/agent-manager.js.map +0 -1
  68. package/dist/agent-runner.d.ts +0 -51
  69. package/dist/agent-runner.d.ts.map +0 -1
  70. package/dist/agent-runner.js +0 -262
  71. package/dist/agent-runner.js.map +0 -1
  72. package/dist/config.d.ts +0 -24
  73. package/dist/config.d.ts.map +0 -1
  74. package/dist/config.js +0 -132
  75. package/dist/config.js.map +0 -1
  76. package/dist/conversation-viewer.d.ts +0 -40
  77. package/dist/conversation-viewer.d.ts.map +0 -1
  78. package/dist/conversation-viewer.js +0 -276
  79. package/dist/conversation-viewer.js.map +0 -1
  80. package/dist/core-compat.d.ts +0 -21
  81. package/dist/core-compat.d.ts.map +0 -1
  82. package/dist/core-compat.js +0 -86
  83. package/dist/core-compat.js.map +0 -1
  84. package/dist/custom-agents.d.ts +0 -14
  85. package/dist/custom-agents.d.ts.map +0 -1
  86. package/dist/custom-agents.js +0 -113
  87. package/dist/custom-agents.js.map +0 -1
  88. package/dist/file-lock.d.ts +0 -42
  89. package/dist/file-lock.d.ts.map +0 -1
  90. package/dist/file-lock.js +0 -91
  91. package/dist/file-lock.js.map +0 -1
  92. package/dist/index.d.ts +0 -10
  93. package/dist/index.d.ts.map +0 -1
  94. package/dist/index.js +0 -758
  95. package/dist/index.js.map +0 -1
  96. package/dist/model-resolver.d.ts +0 -19
  97. package/dist/model-resolver.d.ts.map +0 -1
  98. package/dist/model-resolver.js +0 -61
  99. package/dist/model-resolver.js.map +0 -1
  100. package/dist/types.d.ts +0 -98
  101. package/dist/types.d.ts.map +0 -1
  102. package/dist/types.js +0 -47
  103. package/dist/types.js.map +0 -1
  104. package/dist/widget.d.ts +0 -56
  105. package/dist/widget.d.ts.map +0 -1
  106. package/dist/widget.js +0 -396
  107. package/dist/widget.js.map +0 -1
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @pi-unipi/subagents — Output truncation + run/tool timeout defaults
3
+ *
4
+ * truncateOutput ported verbatim from pi-subagents src/shared/types.ts.
5
+ * Timeout resolution follows their precedence: call param > agent frontmatter >
6
+ * config > default (30min foreground; 5min for known-fast tools when
7
+ * toolTimeoutMs is unset). Exemptions use OUR tool names.
8
+ */
9
+
10
+ import { DEFAULT_MAX_OUTPUT, type MaxOutputConfig } from "./parity-types.js";
11
+ import { KNOWN_FAST_TOOLS, FAST_TOOL_TIMEOUT_MS, DEFAULT_RUN_TIMEOUT_MS } from "./parity-types.js";
12
+ import type { AgentConfig } from "./types.js";
13
+ import type { SubagentsConfig } from "./types.js";
14
+
15
+ // ============================================================================
16
+ // Output truncation
17
+ // ============================================================================
18
+
19
+ export interface TruncationResult {
20
+ text: string;
21
+ truncated: boolean;
22
+ originalBytes?: number;
23
+ originalLines?: number;
24
+ artifactPath?: string;
25
+ }
26
+
27
+ function formatBytes(bytes: number): string {
28
+ if (bytes < 1024) return `${bytes}B`;
29
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
30
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
31
+ }
32
+
33
+ export function truncateOutput(
34
+ output: string,
35
+ config: Required<MaxOutputConfig>,
36
+ artifactPath?: string,
37
+ ): TruncationResult {
38
+ const lines = output.split("\n");
39
+ const bytes = Buffer.byteLength(output, "utf-8");
40
+
41
+ if (bytes <= config.bytes && lines.length <= config.lines) {
42
+ return { text: output, truncated: false };
43
+ }
44
+
45
+ let truncatedLines = lines;
46
+ if (lines.length > config.lines) {
47
+ truncatedLines = lines.slice(0, config.lines);
48
+ }
49
+
50
+ let result = truncatedLines.join("\n");
51
+ if (Buffer.byteLength(result, "utf-8") > config.bytes) {
52
+ let low = 0;
53
+ let high = result.length;
54
+ while (low < high) {
55
+ const mid = Math.floor((low + high + 1) / 2);
56
+ if (Buffer.byteLength(result.slice(0, mid), "utf-8") <= config.bytes) {
57
+ low = mid;
58
+ } else {
59
+ high = mid - 1;
60
+ }
61
+ }
62
+ result = result.slice(0, low);
63
+ }
64
+
65
+ const keptLines = result.split("\n").length;
66
+ const marker = `[TRUNCATED: showing first ${keptLines} of ${lines.length} lines, ${formatBytes(Buffer.byteLength(result))} of ${formatBytes(bytes)}${artifactPath ? ` - full output at ${artifactPath}` : ""}]\n`;
67
+
68
+ return {
69
+ text: marker + result,
70
+ truncated: true,
71
+ originalBytes: bytes,
72
+ originalLines: lines.length,
73
+ artifactPath,
74
+ };
75
+ }
76
+
77
+ export function resolveMaxOutput(
78
+ callMaxOutput: MaxOutputConfig | undefined,
79
+ config: SubagentsConfig | undefined,
80
+ ): Required<MaxOutputConfig> {
81
+ const fromConfig = config?.maxOutput;
82
+ return {
83
+ bytes: callMaxOutput?.bytes ?? fromConfig?.bytes ?? DEFAULT_MAX_OUTPUT.bytes,
84
+ lines: callMaxOutput?.lines ?? fromConfig?.lines ?? DEFAULT_MAX_OUTPUT.lines,
85
+ };
86
+ }
87
+
88
+ // ============================================================================
89
+ // Timeout resolution (reference precedence)
90
+ // ============================================================================
91
+
92
+ /**
93
+ * Run-level deadline: call param > agent frontmatter > config > 30-minute
94
+ * foreground backstop.
95
+ */
96
+ export function resolveRunTimeoutMs(
97
+ callTimeoutMs: number | undefined,
98
+ agent: AgentConfig | undefined,
99
+ config: SubagentsConfig | undefined,
100
+ ): number {
101
+ return (
102
+ callTimeoutMs ??
103
+ agent?.timeoutMs ??
104
+ config?.timeoutMs ??
105
+ DEFAULT_RUN_TIMEOUT_MS
106
+ );
107
+ }
108
+
109
+ /**
110
+ * Per-tool-call hard deadline: call param > agent frontmatter > config >
111
+ * env (UNIPI_SUBAGENT_TOOL_TIMEOUT_MS) > 5-minute default for known-fast
112
+ * tools only (long-running tools get no hard default).
113
+ */
114
+ export function resolveToolTimeoutMs(
115
+ callToolTimeoutMs: number | undefined,
116
+ agent: AgentConfig | undefined,
117
+ config: SubagentsConfig | undefined,
118
+ env: NodeJS.ProcessEnv = process.env,
119
+ ): number | undefined {
120
+ const envRaw = env.UNIPI_SUBAGENT_TOOL_TIMEOUT_MS;
121
+ const envParsed =
122
+ envRaw !== undefined && envRaw.trim() !== "" ? Number(envRaw) : undefined;
123
+ if (envParsed !== undefined && (!Number.isInteger(envParsed) || envParsed <= 0)) {
124
+ throw new Error("UNIPI_SUBAGENT_TOOL_TIMEOUT_MS must be a positive integer number of milliseconds.");
125
+ }
126
+ const configured = callToolTimeoutMs ?? agent?.toolTimeoutMs ?? config?.toolTimeoutMs ?? envParsed;
127
+ if (configured !== undefined) return configured;
128
+ // No configured value: hard timeout only for known-fast built-ins.
129
+ return undefined;
130
+ }
131
+
132
+ /** Whether a tool gets the 5-minute known-fast default when nothing is configured. */
133
+ export function isKnownFastTool(toolName: string): boolean {
134
+ return (KNOWN_FAST_TOOLS as readonly string[]).includes(toolName);
135
+ }
136
+
137
+ export function effectiveToolDeadlineMs(toolName: string, configured: number | undefined): number | undefined {
138
+ return configured ?? (isKnownFastTool(toolName) ? FAST_TOOL_TIMEOUT_MS : undefined);
139
+ }
140
+
141
+ /** Tools exempt from hard tool timeouts (their legitimate purpose is waiting). */
142
+ export const TOOL_TIMEOUT_EXEMPT = new Set(["contact_supervisor", "intercom", "get_helper_result", "ask_user"]);
@@ -0,0 +1,315 @@
1
+ /**
2
+ * @pi-unipi/subagents — Parity types (pi-subagents v0.53.0 feature surface)
3
+ *
4
+ * Ported from nicobailon/pi-subagents src/shared/types.ts. Semantics follow
5
+ * the reference; naming and on-disk layout follow unipi conventions:
6
+ * - runtime artifacts under os.tmpdir()/unipi-subagents-<scope>
7
+ * - env overrides use UNIPI_SUBAGENT_* (not PI_SUBAGENT_*)
8
+ * - tool surface stays spawn_helper / get_helper_result
9
+ */
10
+
11
+ import { existsSync, mkdirSync } from "node:fs";
12
+ import { join } from "node:path";
13
+ import { tmpdir, userInfo } from "node:os";
14
+
15
+ // ============================================================================
16
+ // Output / truncation
17
+ // ============================================================================
18
+
19
+ export interface MaxOutputConfig {
20
+ bytes?: number;
21
+ lines?: number;
22
+ }
23
+
24
+ export type OutputMode = "inline" | "file-only";
25
+
26
+ export const DEFAULT_MAX_OUTPUT: Required<MaxOutputConfig> = {
27
+ bytes: 200 * 1024,
28
+ lines: 5000,
29
+ };
30
+
31
+ // ============================================================================
32
+ // Budgets
33
+ // ============================================================================
34
+
35
+ export interface TurnBudgetConfig {
36
+ maxTurns?: number;
37
+ graceTurns?: number;
38
+ }
39
+
40
+ export interface ResolvedTurnBudget {
41
+ maxTurns: number;
42
+ graceTurns: number;
43
+ }
44
+
45
+ export type TurnBudgetOutcome =
46
+ | "within-budget"
47
+ | "wrap-up-requested"
48
+ | "termination-deferred"
49
+ | "exceeded";
50
+
51
+ export interface TurnBudgetState extends ResolvedTurnBudget {
52
+ turns: number;
53
+ outcome: TurnBudgetOutcome;
54
+ }
55
+
56
+ export interface ToolBudgetConfig {
57
+ soft?: number;
58
+ hard: number;
59
+ block?: string[] | "*";
60
+ }
61
+
62
+ export interface ResolvedToolBudget {
63
+ soft?: number;
64
+ hard: number;
65
+ block: "*" | string[];
66
+ }
67
+
68
+ export type ToolBudgetOutcome = "within-budget" | "soft-reached" | "hard-blocked";
69
+
70
+ export interface ToolBudgetState extends ResolvedToolBudget {
71
+ toolCalls: number;
72
+ outcome: ToolBudgetOutcome;
73
+ }
74
+
75
+ export interface UsageBudgetLimit {
76
+ soft?: number;
77
+ hard: number;
78
+ }
79
+
80
+ export interface UsageBudgetConfig {
81
+ tokens?: UsageBudgetLimit;
82
+ costUsd?: UsageBudgetLimit;
83
+ }
84
+
85
+ // ============================================================================
86
+ // Run / result states
87
+ // ============================================================================
88
+
89
+ export type SubagentRunMode = "single" | "parallel" | "chain" | "workflow";
90
+
91
+ export type SubagentResultStatus =
92
+ | "completed"
93
+ | "failed"
94
+ | "paused"
95
+ | "stopped"
96
+ | "detached";
97
+
98
+ export type SubagentOutputState = "present" | "absent" | "unknown";
99
+
100
+ export type WorkflowNodeStatus =
101
+ | "pending"
102
+ | "running"
103
+ | "completed"
104
+ | "failed"
105
+ | "paused"
106
+ | "stopped"
107
+ | "detached"
108
+ | "rejected";
109
+
110
+ export type ProcessTerminalState = "pending" | "observed" | "unknown" | "not-started";
111
+
112
+ export interface Usage {
113
+ input: number;
114
+ output: number;
115
+ cacheRead: number;
116
+ cacheWrite: number;
117
+ cost: number;
118
+ turns: number;
119
+ }
120
+
121
+ export type ContextMode = "fresh" | "fork";
122
+
123
+ export type IsolationMode = "none" | "worktree";
124
+
125
+ // ============================================================================
126
+ // Config sub-objects (ExtensionConfig pieces we port into our subagents.json)
127
+ // ============================================================================
128
+
129
+ export type FleetViewPlacement = "belowEditor" | "aboveEditor";
130
+
131
+ export type ToolDescriptionMode = "default" | "full" | "compact" | "custom";
132
+
133
+ export type InlineToolDisplay = "rich" | "summary";
134
+
135
+ export interface MainWindowRendererConfig {
136
+ horizontalSpacing?: number;
137
+ compactResultMaxLines?: number;
138
+ }
139
+
140
+ export interface WaitToolConfigObject {
141
+ enabled?: boolean;
142
+ }
143
+
144
+ export type WaitToolConfig = boolean | WaitToolConfigObject;
145
+
146
+ export interface TopLevelParallelConfig {
147
+ maxTasks?: number;
148
+ concurrency?: number;
149
+ }
150
+
151
+ export interface ResultScanLoggingConfig {
152
+ mode: "all" | "activity" | "off";
153
+ }
154
+
155
+ // ============================================================================
156
+ // Constants — artifact layout (temp-scoped like the reference, unipi-named)
157
+ // ============================================================================
158
+
159
+ function sanitizeScopeSegment(value: string): string {
160
+ const sanitized = value
161
+ .trim()
162
+ .replace(/[^A-Za-z0-9._-]+/g, "-")
163
+ .replace(/^-+|-+$/g, "");
164
+ return sanitized || "unknown";
165
+ }
166
+
167
+ /** Stable per-user temp scope so concurrent users never share artifacts. */
168
+ export function resolveTempScopeId(options?: {
169
+ env?: NodeJS.ProcessEnv;
170
+ getuid?: (() => number) | undefined;
171
+ }): string {
172
+ const env = options?.env ?? process.env;
173
+ const getuid =
174
+ options && Object.hasOwn(options, "getuid") ? options.getuid : process.getuid?.bind(process);
175
+ if (typeof getuid === "function") return `uid-${getuid()}`;
176
+ for (const key of ["USERNAME", "USER", "LOGNAME"] as const) {
177
+ const value = env[key];
178
+ if (value) return `user-${sanitizeScopeSegment(value)}`;
179
+ }
180
+ try {
181
+ const username = userInfo().username;
182
+ if (username) return `user-${sanitizeScopeSegment(username)}`;
183
+ } catch {
184
+ // fall through
185
+ }
186
+ const home = env.USERPROFILE ?? env.HOME;
187
+ if (home) return `home-${sanitizeScopeSegment(home)}`;
188
+ return "shared";
189
+ }
190
+
191
+ const configuredTempRoot = process.env.UNIPI_SUBAGENTS_TEMP_ROOT?.trim();
192
+ export const TEMP_ROOT_DIR = configuredTempRoot
193
+ ? join(configuredTempRoot)
194
+ : join(tmpdir(), `unipi-subagents-${resolveTempScopeId()}`);
195
+
196
+ export const RESULTS_DIR = join(TEMP_ROOT_DIR, "async-subagent-results");
197
+ export const ASYNC_DIR = join(TEMP_ROOT_DIR, "async-subagent-runs");
198
+ export const CHAIN_RUNS_DIR = join(TEMP_ROOT_DIR, "chain-runs");
199
+ export const TEMP_ARTIFACTS_DIR = join(TEMP_ROOT_DIR, "artifacts");
200
+
201
+ export const DIRS = {
202
+ results: RESULTS_DIR,
203
+ async: ASYNC_DIR,
204
+ chain: CHAIN_RUNS_DIR,
205
+ artifacts: TEMP_ARTIFACTS_DIR,
206
+ } as const;
207
+
208
+ export function ensureDirs(): void {
209
+ for (const dir of Object.values(DIRS)) {
210
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
211
+ }
212
+ }
213
+
214
+ // ============================================================================
215
+ // Widget / render constants
216
+ // ============================================================================
217
+
218
+ export const WIDGET_KEY_ASYNC = "unipi-agents-async";
219
+ export const MAX_WIDGET_JOBS = 4;
220
+ export const POLL_INTERVAL_MS = 250;
221
+ export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
222
+ export const DEFAULT_MAX_SUBAGENTS_PER_RUN = 64;
223
+ export const MAX_PARALLEL_TASKS = 8;
224
+ export const DEFAULT_CONCURRENCY = 4;
225
+ export const DEFAULT_GLOBAL_CONCURRENCY_LIMIT = 20;
226
+
227
+ /** Foreground backstop when nothing else configures a deadline. */
228
+ export const DEFAULT_RUN_TIMEOUT_MS = 30 * 60 * 1000;
229
+ /** Hard per-tool deadline for known-fast built-in tools when toolTimeoutMs unset. */
230
+ export const FAST_TOOL_TIMEOUT_MS = 5 * 60 * 1000;
231
+ export const KNOWN_FAST_TOOLS = ["read", "grep", "find", "ls", "edit", "write"] as const;
232
+
233
+ // ============================================================================
234
+ // Events (unipi event bus payloads for async lifecycle)
235
+ // ============================================================================
236
+
237
+ export const SUBAGENT_ASYNC_STARTED_EVENT = "subagent:async-started";
238
+ export const SUBAGENT_ASYNC_COMPLETE_EVENT = "subagent:async-complete";
239
+ export const SUBAGENT_PROCESS_TERMINAL_EVENT = "subagent:process-terminal";
240
+ export const SUBAGENT_CONTROL_EVENT = "subagent:control";
241
+
242
+ /**
243
+ * Actions accepted by spawn_helper. Ported from SUBAGENT_ACTIONS; names kept
244
+ * identical (they are parameter values, not tool names — no convention clash).
245
+ * Actions land phase by phase; unimplemented ones return a clear error.
246
+ */
247
+ export const SUBAGENT_ACTIONS = [
248
+ "list",
249
+ "get",
250
+ "children.list",
251
+ "guide",
252
+ "create",
253
+ "update",
254
+ "delete",
255
+ "eject",
256
+ "disable",
257
+ "enable",
258
+ "reset",
259
+ "mission.create",
260
+ "mission.list",
261
+ "mission.show",
262
+ "mission.update",
263
+ "mission.resolve-decision",
264
+ "mission.attach-run",
265
+ "mission.close",
266
+ "worktree.discard",
267
+ "refine",
268
+ "refine.show",
269
+ "refine.rollback",
270
+ "inspector.open",
271
+ "inspector.status",
272
+ "inspector.close",
273
+ "project.open",
274
+ "project.status",
275
+ "project.close",
276
+ "status",
277
+ "debug.run",
278
+ "grant-spawn-budget",
279
+ "interrupt",
280
+ "resume",
281
+ "steer",
282
+ "stop",
283
+ "dismiss",
284
+ "doctor",
285
+ "watchdog.status",
286
+ "watchdog.check",
287
+ "watchdog.configure",
288
+ "watchdog.recommend-model",
289
+ "schedule.create",
290
+ "schedule.list",
291
+ "schedule.show",
292
+ "schedule.history",
293
+ "schedule.pause",
294
+ "schedule.resume",
295
+ "schedule.run",
296
+ "schedule.run-due",
297
+ "schedule.delete",
298
+ ] as const;
299
+
300
+ export type SubagentAction = (typeof SUBAGENT_ACTIONS)[number];
301
+
302
+ export const GUIDE_TOPICS = [
303
+ "overview",
304
+ "workflows",
305
+ "agents",
306
+ "missions",
307
+ "observability",
308
+ "tool-reference",
309
+ "configuration",
310
+ "models",
311
+ "watchdog",
312
+ "extension-api",
313
+ ] as const;
314
+
315
+ export type GuideTopic = (typeof GUIDE_TOPICS)[number];
package/src/pi-args.ts ADDED
@@ -0,0 +1,161 @@
1
+ /**
2
+ * @pi-unipi/subagents — Child pi CLI argument builder
3
+ *
4
+ * Ported from pi-subagents src/runs/shared/pi-args.ts (buildPiArgs core).
5
+ * Env overrides use OUR prefix: UNIPI_SUBAGENT_TASK_DELIVERY,
6
+ * UNIPI_SUBAGENT_CHILD, UNIPI_SUBAGENT_PARENT_SESSION. Task delivery follows
7
+ * the reference: 'auto' passes short tasks inline and writes tasks over
8
+ * 8000 chars to a temp task.md referenced as @<path> (EDR workaround); 'file'
9
+ * always uses the file. Zero-activity SIGKILL escalation to file delivery
10
+ * lives in the runner (Phase 3 next slice).
11
+ */
12
+
13
+ import * as fs from "node:fs";
14
+ import * as os from "node:os";
15
+ import * as path from "node:path";
16
+
17
+ export const SUBAGENT_TASK_DELIVERY_ENV = "UNIPI_SUBAGENT_TASK_DELIVERY";
18
+ export const SUBAGENT_CHILD_ENV = "UNIPI_SUBAGENT_CHILD";
19
+ export const SUBAGENT_PARENT_SESSION_ENV = "UNIPI_SUBAGENT_PARENT_SESSION";
20
+ export const SUBAGENT_CHILD_AGENT_ENV = "UNIPI_SUBAGENT_CHILD_AGENT";
21
+ export const SUBAGENT_RUN_ID_ENV = "UNIPI_SUBAGENT_RUN_ID";
22
+
23
+ const TASK_ARG_LIMIT = 8000;
24
+
25
+ export type SubagentTaskDelivery = "auto" | "file";
26
+
27
+ export function resolveSubagentTaskDelivery(
28
+ env: NodeJS.ProcessEnv = process.env,
29
+ ): SubagentTaskDelivery {
30
+ return env[SUBAGENT_TASK_DELIVERY_ENV]?.trim().toLowerCase() === "file" ? "file" : "auto";
31
+ }
32
+
33
+ function shouldDeliverTaskViaFile(task: string, delivery: SubagentTaskDelivery): boolean {
34
+ return delivery === "file" || task.length > TASK_ARG_LIMIT;
35
+ }
36
+
37
+ function escapeXmlAttr(value: string): string {
38
+ return value
39
+ .replace(/&/g, "&amp;")
40
+ .replace(/</g, "&lt;")
41
+ .replace(/>/g, "&gt;")
42
+ .replace(/"/g, "&quot;");
43
+ }
44
+
45
+ export interface BuildPiArgsInput {
46
+ /** Extra CLI args before the task (e.g. ['--print', mode flags]). */
47
+ baseArgs: string[];
48
+ task: string;
49
+ /** pi flags */
50
+ sessionFile?: string;
51
+ sessionDir?: string;
52
+ noSession?: boolean;
53
+ model?: string;
54
+ thinking?: string | false;
55
+ tools?: string[];
56
+ extensions?: string[];
57
+ systemPrompt?: string;
58
+ systemPromptMode?: "replace" | "append";
59
+ promptFileStem?: string;
60
+ inheritProjectContext?: boolean;
61
+ inheritSkills?: boolean;
62
+ cwd?: string;
63
+ taskDelivery?: SubagentTaskDelivery;
64
+ /** Child identification env (parent session targeting for Phase 6 intercom). */
65
+ parentSessionId?: string;
66
+ childAgentName?: string;
67
+ runId?: string;
68
+ }
69
+
70
+ export interface BuildPiArgsResult {
71
+ args: string[];
72
+ env: Record<string, string | undefined>;
73
+ /** Temp dir holding prompt/task files; caller cleans up after exit. */
74
+ tempDir: string;
75
+ }
76
+
77
+ export function buildPiArgs(input: BuildPiArgsInput): BuildPiArgsResult {
78
+ const args = [...input.baseArgs];
79
+
80
+ // ---- Session ----
81
+ if (input.sessionFile) {
82
+ fs.mkdirSync(path.dirname(input.sessionFile), { recursive: true });
83
+ args.push("--session", input.sessionFile);
84
+ } else {
85
+ if (input.noSession) args.push("--no-session");
86
+ if (input.sessionDir) {
87
+ fs.mkdirSync(input.sessionDir, { recursive: true });
88
+ args.push("--session-dir", input.sessionDir);
89
+ }
90
+ }
91
+
92
+ // ---- Model (+ thinking suffix) ----
93
+ if (input.model) {
94
+ const thinking = input.thinking;
95
+ const modelArg =
96
+ thinking && thinking !== "false" && thinking !== "off"
97
+ ? `${input.model}--thinking=${thinking}`
98
+ : input.model;
99
+ args.push("--model", modelArg);
100
+ }
101
+
102
+ // ---- Tools ----
103
+ if (input.tools !== undefined) {
104
+ if (input.tools.length === 0) args.push("--no-tools");
105
+ else args.push("--tools", input.tools.join(","));
106
+ }
107
+
108
+ // ---- Extensions ----
109
+ if (input.extensions !== undefined) {
110
+ args.push("--no-extensions");
111
+ for (const extPath of input.extensions) args.push("--extension", extPath);
112
+ }
113
+
114
+ // ---- Project context / skills ----
115
+ if (!input.inheritProjectContext) args.push("--no-context-files");
116
+ if (!input.inheritSkills) args.push("--no-skills");
117
+
118
+ // ---- System prompt via temp file ----
119
+ let tempDir: string | undefined;
120
+ if (input.systemPrompt !== undefined && input.systemPrompt !== null) {
121
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "unipi-subagent-"));
122
+ const stem = (input.promptFileStem ?? "prompt").replace(/[^\w.-]/g, "_");
123
+ const promptPath = path.join(tempDir, `${stem}.md`);
124
+ const taggedPrompt = input.childAgentName
125
+ ? `<active_agent name="${escapeXmlAttr(input.childAgentName)}"/>\n\n${input.systemPrompt}`
126
+ : input.systemPrompt;
127
+ fs.writeFileSync(promptPath, taggedPrompt, { mode: 0o600 });
128
+ args.push(
129
+ input.systemPromptMode === "replace" ? "--system-prompt" : "--append-system-prompt",
130
+ promptPath,
131
+ );
132
+ }
133
+
134
+ // ---- Task delivery (EDR-safe) ----
135
+ if (shouldDeliverTaskViaFile(input.task, input.taskDelivery ?? resolveSubagentTaskDelivery())) {
136
+ if (!tempDir) tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "unipi-subagent-"));
137
+ const taskFilePath = path.join(tempDir, "task.md");
138
+ fs.writeFileSync(taskFilePath, `Task: ${input.task}`, { mode: 0o600 });
139
+ args.push(`@${taskFilePath}`);
140
+ } else {
141
+ args.push(`Task: ${input.task}`);
142
+ }
143
+
144
+ // ---- Child env ----
145
+ const env: Record<string, string | undefined> = {};
146
+ env[SUBAGENT_CHILD_ENV] = "1";
147
+ if (input.parentSessionId) env[SUBAGENT_PARENT_SESSION_ENV] = input.parentSessionId;
148
+ if (input.childAgentName) env[SUBAGENT_CHILD_AGENT_ENV] = input.childAgentName;
149
+ if (input.runId) env[SUBAGENT_RUN_ID_ENV] = input.runId;
150
+
151
+ return { args, env, tempDir: tempDir ?? "" };
152
+ }
153
+
154
+ export function cleanupTempDir(tempDir: string | null | undefined): void {
155
+ if (!tempDir) return;
156
+ try {
157
+ fs.rmSync(tempDir, { recursive: true, force: true });
158
+ } catch {
159
+ // Best-effort cleanup.
160
+ }
161
+ }