@pi-unipi/subagents 2.6.1 → 2.9.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 (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 +22 -20
  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 +271 -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,249 @@
1
+ /**
2
+ * @pi-unipi/subagents — Native supervisor channel
3
+ *
4
+ * Ported from pi-subagents src/intercom/native-supervisor-channel.ts (core).
5
+ * File-based channel: children write request JSON into a per-run channel dir;
6
+ * the parent polls, surfaces decisions via followUp messages, and writes
7
+ * reply JSON back. Children discover the channel through env vars set at
8
+ * launch (UNIPI_SUBAGENT_SUPERVISOR_CHANNEL_DIR etc.) — NO external
9
+ * pi-intercom dependency.
10
+ */
11
+
12
+ import * as fs from "node:fs";
13
+ import * as path from "node:path";
14
+ import { randomUUID } from "node:crypto";
15
+
16
+ export const SUPERVISOR_TOOL_NAME = "contact_supervisor";
17
+ export const PARENT_SUPERVISOR_TOOL_NAME = "subagent_supervisor";
18
+
19
+ const MAX_MESSAGE_BYTES = 64 * 1024;
20
+ const DEFAULT_ASK_TIMEOUT_MS = 10 * 60 * 1000;
21
+ const CHANNEL_POLL_MS = 500;
22
+
23
+ export type SupervisorReason = "need_decision" | "interview_request" | "progress_update";
24
+
25
+ export interface SupervisorRequest {
26
+ type: "subagent.supervisor.request";
27
+ id: string;
28
+ createdAt: number;
29
+ expiresAt?: number;
30
+ reason: SupervisorReason;
31
+ message: string;
32
+ expectsReply: boolean;
33
+ runId: string;
34
+ agent: string;
35
+ }
36
+
37
+ export interface SupervisorReply {
38
+ type: "subagent.supervisor.reply";
39
+ requestId: string;
40
+ createdAt: number;
41
+ message: string;
42
+ }
43
+
44
+ // ============================================================================
45
+ // Env contract (child side)
46
+ // ============================================================================
47
+
48
+ export const SUPERVISOR_CHANNEL_DIR_ENV = "UNIPI_SUBAGENT_SUPERVISOR_CHANNEL_DIR";
49
+ export const SUPERVISOR_RUN_ID_ENV = "UNIPI_SUBAGENT_RUN_ID";
50
+ export const SUPERVISOR_AGENT_ENV = "UNIPI_SUBAGENT_CHILD_AGENT";
51
+ export const SUPERVISOR_PARENT_SESSION_ENV = "UNIPI_SUBAGENT_PARENT_SESSION";
52
+
53
+ /** Child-side metadata read from env. All fields required to activate. */
54
+ export function readChildSupervisorMetadata(env: NodeJS.ProcessEnv = process.env): {
55
+ channelDir: string;
56
+ runId: string;
57
+ agent: string;
58
+ parentSessionId: string;
59
+ } | undefined {
60
+ const channelDir = env[SUPERVISOR_CHANNEL_DIR_ENV]?.trim();
61
+ const runId = env[SUPERVISOR_RUN_ID_ENV]?.trim();
62
+ const agent = env[SUPERVISOR_AGENT_ENV]?.trim();
63
+ const parentSessionId = env[SUPERVISOR_PARENT_SESSION_ENV]?.trim();
64
+ if (!channelDir || !runId || !agent || !parentSessionId) return undefined;
65
+ return { channelDir, runId, agent, parentSessionId };
66
+ }
67
+
68
+ function safeSegment(value: string): string {
69
+ return value.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
70
+ }
71
+
72
+ /** Channel dir under our temp root: supervisor-channels/<runId>-<agent>/. */
73
+ export function resolveSupervisorChannelDir(root: string, runId: string, agent: string): string {
74
+ return path.join(root, `${safeSegment(runId)}-${safeSegment(agent)}`);
75
+ }
76
+
77
+ export function ensureSupervisorChannelDir(channelDir: string): void {
78
+ fs.mkdirSync(path.join(channelDir, "requests"), { recursive: true, mode: 0o700 });
79
+ fs.mkdirSync(path.join(channelDir, "replies"), { recursive: true, mode: 0o700 });
80
+ }
81
+
82
+ function requestsDir(channelDir: string): string {
83
+ return path.join(channelDir, "requests");
84
+ }
85
+ function repliesDir(channelDir: string): string {
86
+ return path.join(channelDir, "replies");
87
+ }
88
+
89
+ function validateMessage(message: string | undefined): string {
90
+ if (!message || !message.trim()) throw new Error("supervisor message must be a non-empty string.");
91
+ if (Buffer.byteLength(message, "utf8") > MAX_MESSAGE_BYTES) {
92
+ throw new Error(`supervisor message exceeds ${MAX_MESSAGE_BYTES} bytes.`);
93
+ }
94
+ return message;
95
+ }
96
+
97
+ // ============================================================================
98
+ // Child side: contact_supervisor
99
+ // ============================================================================
100
+
101
+ /**
102
+ * Write a supervisor request and (optionally) block until a reply lands.
103
+ * Returns the reply text for asks; undefined for progress updates.
104
+ */
105
+ export function childContactSupervisor(
106
+ params: { reason: SupervisorReason; message?: string; timeoutMs?: number },
107
+ metadata = readChildSupervisorMetadata(),
108
+ now = Date.now(),
109
+ ): { replied: boolean; reply?: string } {
110
+ if (!metadata) {
111
+ throw new Error("contact_supervisor is unavailable: no supervisor channel in this environment.");
112
+ }
113
+ const message = validateMessage(params.message);
114
+ ensureSupervisorChannelDir(metadata.channelDir);
115
+
116
+ const request: SupervisorRequest = {
117
+ type: "subagent.supervisor.request",
118
+ id: randomUUID(),
119
+ createdAt: now,
120
+ reason: params.reason,
121
+ message,
122
+ expectsReply: params.reason !== "progress_update",
123
+ runId: metadata.runId,
124
+ agent: metadata.agent,
125
+ ...(params.reason !== "progress_update"
126
+ ? { expiresAt: now + (params.timeoutMs ?? DEFAULT_ASK_TIMEOUT_MS) }
127
+ : {}),
128
+ };
129
+ fs.writeFileSync(
130
+ path.join(requestsDir(metadata.channelDir), `${request.id}.json`),
131
+ JSON.stringify(request),
132
+ { mode: 0o600 },
133
+ );
134
+
135
+ if (!request.expectsReply) return { replied: false };
136
+
137
+ // Block on the reply file. In production children run as separate processes
138
+ // (Atomics.wait parks without burning CPU); when Atomics is unavailable
139
+ // (e.g. same-process tests), fall back to a busy poll.
140
+ const replyFile = path.join(repliesDir(metadata.channelDir), `${request.id}.json`);
141
+ const deadline = request.expiresAt ?? now + DEFAULT_ASK_TIMEOUT_MS;
142
+ const waitBuffer = typeof SharedArrayBuffer !== "undefined" ? new Int32Array(new SharedArrayBuffer(4)) : undefined;
143
+ for (;;) {
144
+ try {
145
+ const reply = JSON.parse(fs.readFileSync(replyFile, "utf8")) as SupervisorReply;
146
+ if (reply.requestId === request.id) return { replied: true, reply: reply.message };
147
+ } catch {
148
+ // Not there yet.
149
+ }
150
+ if (Date.now() > deadline) {
151
+ return { replied: false, reply: undefined };
152
+ }
153
+ if (waitBuffer) {
154
+ try {
155
+ Atomics.wait(waitBuffer, 0, 0, CHANNEL_POLL_MS);
156
+ } catch {
157
+ // Busy fallback below.
158
+ }
159
+ }
160
+ }
161
+ }
162
+
163
+ // ============================================================================
164
+ // Parent side: poll pending requests, deliver, reply
165
+ // ============================================================================
166
+
167
+ export interface PendingSupervisorRequest extends SupervisorRequest {
168
+ channelDir: string;
169
+ }
170
+
171
+ /** List open requests across all channel dirs under the root. */
172
+ export function listPendingSupervisorRequests(root: string): PendingSupervisorRequest[] {
173
+ let entries: fs.Dirent[];
174
+ try {
175
+ entries = fs.readdirSync(root, { withFileTypes: true });
176
+ } catch {
177
+ return [];
178
+ }
179
+ const pending: PendingSupervisorRequest[] = [];
180
+ const now = Date.now();
181
+ for (const entry of entries) {
182
+ if (!entry.isDirectory()) continue;
183
+ const channelDir = path.join(root, entry.name);
184
+ let files: string[];
185
+ try {
186
+ files = fs.readdirSync(requestsDir(channelDir));
187
+ } catch {
188
+ continue;
189
+ }
190
+ for (const file of files) {
191
+ if (!file.endsWith(".json")) continue;
192
+ try {
193
+ const request = JSON.parse(fs.readFileSync(path.join(requestsDir(channelDir), file), "utf8")) as SupervisorRequest;
194
+ if (request.type !== "subagent.supervisor.request") continue;
195
+ if (request.expiresAt !== undefined && request.expiresAt < now) {
196
+ // Expired: clean up so it never re-surfaces.
197
+ fs.rmSync(path.join(requestsDir(channelDir), file), { force: true });
198
+ continue;
199
+ }
200
+ pending.push({ ...request, channelDir });
201
+ } catch {
202
+ // corrupt → skip
203
+ }
204
+ }
205
+ }
206
+ return pending.sort((left, right) => left.createdAt - right.createdAt);
207
+ }
208
+
209
+ /** Reply to a request (removes it from pending). */
210
+ export function replyToSupervisorRequest(root: string, requestId: string, message: string): boolean {
211
+ const validMessage = validateMessage(message);
212
+ for (const pending of listPendingSupervisorRequests(root)) {
213
+ if (pending.id !== requestId) continue;
214
+ const reply: SupervisorReply = {
215
+ type: "subagent.supervisor.reply",
216
+ requestId,
217
+ createdAt: Date.now(),
218
+ message: validMessage,
219
+ };
220
+ fs.writeFileSync(path.join(repliesDir(pending.channelDir), `${requestId}.json`), JSON.stringify(reply), { mode: 0o600 });
221
+ try {
222
+ fs.rmSync(path.join(requestsDir(pending.channelDir), `${requestId}.json`), { force: true });
223
+ } catch { /* best effort */ }
224
+ return true;
225
+ }
226
+ return false;
227
+ }
228
+
229
+ /** Create the parent-side poller that delivers requests as notifications. */
230
+ export function createSupervisorPoller(
231
+ root: string,
232
+ opts: {
233
+ pollMs?: number;
234
+ notify: (request: PendingSupervisorRequest) => void;
235
+ },
236
+ ): { stop(): void } {
237
+ let timer: ReturnType<typeof setInterval> | undefined;
238
+ timer = setInterval(() => {
239
+ for (const request of listPendingSupervisorRequests(root)) {
240
+ opts.notify(request);
241
+ }
242
+ }, opts.pollMs ?? CHANNEL_POLL_MS);
243
+ timer.unref?.();
244
+ return {
245
+ stop(): void {
246
+ if (timer) clearInterval(timer);
247
+ },
248
+ };
249
+ }