@evandu/super-pi 0.1.57 → 0.1.59

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.
@@ -43,6 +43,11 @@ export const TASK_ROSTER_ENV = "SUPER_PI_TASK_ROSTER";
43
43
  export const TASK_CONVERSATION_ENV = "SUPER_PI_TASK_CONVERSATION";
44
44
  export const TASK_DEPTH_ENV = "SUPER_PI_TASK_DEPTH";
45
45
 
46
+ /** The independent background-command tool. NOT a sub-agent dispatch: a shell command run asynchronously. */
47
+ export const RUN_ASYNC_TOOL_NAME = "run_async";
48
+ /** Capability env name reused for `run_async` - same per-turn credential, same wire, same server check. */
49
+ export const RUN_ASYNC_CAPABILITY_ENV = "SUPER_PI_TASK_CAPABILITY";
50
+
46
51
  /**
47
52
  * The `todowrite` extension's half of the same environment contract, declared here for the reason the
48
53
  * four above are. `src/server/task-tool.ts` is the other end; `scripts/todo-tool.test.mjs` asserts they
@@ -72,6 +77,43 @@ export type TodoEntry = { content: string; status: "pending" | "in_progress" | "
72
77
  /** The three statuses Super Pi renders, as the model writes them. */
73
78
  const TODO_STATUSES = new Set(["pending", "in_progress", "completed"]);
74
79
 
80
+ /** Parameters for the `run_async` tool. Plain shell command, no subagent plumbing. */
81
+ export type RunAsyncParams = { command: string; description?: string };
82
+
83
+ /**
84
+ * The model-facing description for the `run_async` tool.
85
+ *
86
+ * THIS IS A SHELL COMMAND, NOT A SUB-AGENT. The tool runs the command via the platform's shell
87
+ * (bash on Unix, PowerShell on Windows) in the conversation's workspace, returns immediately with
88
+ * state="running", and delivers stdout, stderr and exit code as a follow-up turn when the command
89
+ * settles. Use it whenever the user says "in the background", "async", "without blocking me",
90
+ * "run this and tell me later", or "kick off a build" - any work measured in seconds to minutes
91
+ * that they would otherwise be waiting on.
92
+ *
93
+ * Do NOT use `task` with `background: true` for this: `task` dispatches a sub-agent, and a
94
+ * sub-agent for a one-shot shell command is minutes of process startup to do what `run_async`
95
+ * does in tens of milliseconds.
96
+ */
97
+ export const RUN_ASYNC_DESCRIPTION = `Run a shell command asynchronously in the current workspace and deliver its output as a follow-up message when it finishes.
98
+
99
+ When to use this:
100
+ - The user says "in the background", "async", "kick off", "start a build and tell me when it's done", or anything else that means "do not block me on this".
101
+ - A command is expected to take seconds to minutes (a long build, a test suite, a download, a file watcher).
102
+ - You want to start several independent commands and let them all run in parallel.
103
+
104
+ How it works:
105
+ 1. The command runs in the conversation's workspace under the platform's shell (bash on Linux/macOS, PowerShell on Windows).
106
+ 2. The call returns immediately. You will see <task id state="running"> in the result.
107
+ 3. When the command finishes, its stdout, stderr and exit code arrive in a new turn wrapped in a <task> envelope.
108
+
109
+ Usage notes:
110
+ 1. The "command" field is the shell command verbatim - do not wrap it in JSON.
111
+ 2. "description" is optional and only labels the row in the right-rail "background tasks" panel.
112
+ 3. Do NOT poll, sleep, or call this tool repeatedly to check on the same command. The result arrives on its own.
113
+ 4. For commands that should run in the FOREGROUND and whose output you need before the next step, use the bash tool instead.
114
+ 5. For delegating a multi-step task to a specialist sub-agent, use the task tool. This tool is for plain shell commands.
115
+ 6. The result envelope looks like <task id="..." state="completed|error"><task_result>stdout</task_result></task> or with <task_error> for failures. Read it the same way you would read a sub-agent's result.`;
116
+
75
117
  /** The parameters, exactly OpenCode's (`tool/task.ts:43-52`) minus the ones this step does not do. */
76
118
  export type TaskParams = {
77
119
  description: string;
@@ -143,8 +185,9 @@ Usage notes:
143
185
  6. If a subagent's description says it should be used proactively, use it without being asked.
144
186
  7. BY DEFAULT A DISPATCH BLOCKS: the call does not return until that subagent has finished, and its answer comes back to you in this same turn. So you CAN dispatch several in one turn and use each answer before writing the next prompt - which is the normal way to chain specialists.
145
187
  8. A LATER DISPATCH MUST CARRY THE EARLIER ONE'S ANSWER IN ITS PROMPT, because the next subagent starts with a fresh context and cannot see what the first one found. The tool does not do this for you. Write the findings you need into the next prompt yourself.
146
- 9. Pass background: true only for work that does not depend on anything and that you will not need before this turn ends. The result arrives later as a follow-up message; you are not told anything at call time. Do NOT sleep, poll, or ask for status.
147
- 10. For three or more steps where each depends on the previous one, prefer the workflow tool: it runs the chain server-side and hands each step the previous step's output without you copying it through your context.`;
188
+ 9. Pass background: true to run this sub-agent ASYNCHRONOUSLY. The dispatch returns immediately with state="running" and the answer arrives as a follow-up turn when the sub-agent finishes. Do NOT use this for a plain shell command - use the run_async tool instead, which is much faster for a one-shot command and does not spin up an entire sub-agent.
189
+ 10. To run a SHELL COMMAND in the background (a long build, a test suite, a watcher, anything measured in seconds to minutes), use the run_async tool. Do NOT pass background: true to the task tool for this.
190
+ 11. For three or more steps where each depends on the previous one, prefer the workflow tool: it runs the chain server-side and hands each step the previous step's output without you copying it through your context.`;
148
191
 
149
192
  /**
150
193
  * The roster block, OpenCode's `describeTask()` reduced to its two jobs: the ids that are legal, and
@@ -259,10 +302,9 @@ export function refuseTask(config: TaskToolConfig, params: TaskParams): string |
259
302
  if (!config.endpoint) return `The task tool is not available in this session: ${TASK_ENDPOINT_ENV} is not set, so there is no Super Pi server to dispatch to. Do the work yourself with your other tools.`;
260
303
  if (!config.conversationId) return `The task tool is not available in this session: ${TASK_CONVERSATION_ENV} is not set, so this run cannot be attributed to a conversation. Do the work yourself with your other tools.`;
261
304
  if (!config.roster.length) return "This install has no dispatchable sub-agents (no agents/*.md with `mode: subagent`), so there is nobody to delegate to. Do the work yourself with your other tools.";
262
- if (!params.subagent_type.trim()) return `subagent_type is required. Available: ${config.roster.map((entry) => entry.id).join(", ")}.`;
305
+ if (!params.subagent_type.trim()) return `subagent_type is required. Available: ${config.roster.map((entry) => entry.id).join(", ")}. If you wanted to run a SHELL COMMAND in the background instead of dispatching a sub-agent, use the run_async tool.`;
263
306
  if (!config.roster.some((entry) => entry.id === params.subagent_type)) return `${JSON.stringify(params.subagent_type)} is not a dispatchable subagent_type. Available: ${config.roster.map((entry) => entry.id).join(", ")}. Pick one of those, or do the work yourself.`;
264
307
  if (!params.prompt.trim()) return "prompt is required and must not be empty: the sub-agent starts with a fresh context and your prompt is everything it will know.";
265
- if (params.background === true && !config.execEndpoint) return `Background tasks are not available: ${TASK_EXEC_ENDPOINT_ENV} is not set. Do the work yourself with your other tools.`;
266
308
  return undefined;
267
309
  }
268
310
 
@@ -289,36 +331,6 @@ export async function executeTask(
289
331
  const refusal = refuseTask(config, params);
290
332
  if (refusal) return text(renderTaskResult({ id: "refused", state: "error", text: refusal }), true);
291
333
 
292
- // Background: true uses the lightweight exec endpoint instead of the subagent dispatcher.
293
- // This avoids spawning a full Pi agent process for a plain shell command.
294
- if (params.background === true && config.execEndpoint) {
295
- const send = deps.fetch ?? globalFetch;
296
- let response: Awaited<ReturnType<TaskFetch>>;
297
- try {
298
- response = await send(config.execEndpoint, {
299
- method: "POST",
300
- headers: { "content-type": "application/json", ...(config.token ? { authorization: `Bearer ${config.token}` } : {}) },
301
- body: JSON.stringify({
302
- cmd: params.prompt,
303
- cwd: context.cwd,
304
- conversationId: config.conversationId,
305
- capability: config.capability,
306
- }),
307
- ...(deps.signal ? { signal: deps.signal } : {}),
308
- });
309
- } catch (error) {
310
- return text(renderTaskResult({ id: "unreachable", state: "error", text: `Could not reach the Super Pi server at ${config.execEndpoint}: ${error instanceof Error ? error.message : "unknown error"}` }), true);
311
- }
312
- let payload: { sessionId?: string; status?: string; error?: string };
313
- try { payload = JSON.parse(await response.text()) as typeof payload; }
314
- catch { return text(renderTaskResult({ id: "unreadable", state: "error", text: `The exec endpoint answered ${response.status} with something that is not JSON.` }), true); }
315
- const id = payload.sessionId ?? "refused";
316
- if (payload.status === "running") {
317
- return text(`<task id="${id}" state="running">\n<summary>${params.description.trim() || params.prompt.slice(0, 60)}</summary>\n<task_result>The command is running in the background. You will be notified automatically when it finishes. Do not sleep, poll for progress, or duplicate this work. Continue with non-overlapping work.</task_result>\n</task>`);
318
- }
319
- return text(renderTaskResult({ id, state: "error", text: payload.error ?? `Exec endpoint returned status ${payload.status}` }), true);
320
- }
321
-
322
334
  const send = deps.fetch ?? globalFetch;
323
335
  let response: Awaited<ReturnType<TaskFetch>>;
324
336
  try {
@@ -503,6 +515,65 @@ export function refuseTodo(config: TodoToolConfig, params: TodoParams): string |
503
515
  return undefined;
504
516
  }
505
517
 
518
+ /**
519
+ * Refuses a `run_async` call that cannot work.
520
+ *
521
+ * Two checks, for the same three-case shape `refuseTask` and `refuseTodo` use: the call must reach the
522
+ * server, and the server must be able to attribute it to a conversation. The roster is not consulted
523
+ * because this tool is not a dispatch.
524
+ */
525
+ export function refuseRunAsync(config: TaskToolConfig, params: RunAsyncParams): string | undefined {
526
+ if (!config.execEndpoint) return `The run_async tool is not available in this session: ${TASK_EXEC_ENDPOINT_ENV} is not set, so there is no Super Pi server to run commands on. Do the work yourself with your other tools.`;
527
+ if (!config.conversationId) return `The run_async tool is not available in this session: ${TASK_CONVERSATION_ENV} is not set, so this run cannot be attributed to a conversation. Do the work yourself with your other tools.`;
528
+ if (typeof params.command !== "string" || !params.command.trim()) return "command is required and must not be empty.";
529
+ return undefined;
530
+ }
531
+
532
+ /**
533
+ * Runs one shell command asynchronously and returns the envelope the model reads.
534
+ *
535
+ * Independent of `executeTask` even though both post JSON to the same server: the model reaches for
536
+ * `run_async` when the user wants a background shell command, and reaches for `task` when they want
537
+ * a sub-agent. Routing them through one function would mean sharing a refuse-list, a description,
538
+ * and a wire that a model can never get wrong by accident.
539
+ */
540
+ export async function executeAsync(
541
+ params: RunAsyncParams,
542
+ context: TaskToolContext,
543
+ config: TaskToolConfig,
544
+ deps: { signal?: AbortSignal; fetch?: TaskFetch } = {},
545
+ ): Promise<TaskToolResult> {
546
+ const text = (body: string, isError?: boolean): TaskToolResult => ({ content: [{ type: "text", text: body }], details: undefined, ...(isError ? { isError: true } : {}) });
547
+ const refusal = refuseRunAsync(config, params);
548
+ if (refusal) return text(renderTaskResult({ id: "refused", state: "error", text: refusal }), true);
549
+
550
+ const send = deps.fetch ?? globalFetch;
551
+ let response: Awaited<ReturnType<TaskFetch>>;
552
+ try {
553
+ response = await send(config.execEndpoint, {
554
+ method: "POST",
555
+ headers: { "content-type": "application/json", ...(config.token ? { authorization: `Bearer ${config.token}` } : {}) },
556
+ body: JSON.stringify({
557
+ cmd: params.command,
558
+ cwd: context.cwd,
559
+ conversationId: config.conversationId,
560
+ capability: config.capability,
561
+ }),
562
+ ...(deps.signal ? { signal: deps.signal } : {}),
563
+ });
564
+ } catch (error) {
565
+ return text(renderTaskResult({ id: "unreachable", state: "error", text: `Could not reach the Super Pi server at ${config.execEndpoint}: ${error instanceof Error ? error.message : "unknown error"}` }), true);
566
+ }
567
+ let payload: { sessionId?: string; status?: string; error?: string };
568
+ try { payload = JSON.parse(await response.text()) as typeof payload; }
569
+ catch { return text(renderTaskResult({ id: "unreadable", state: "error", text: `The exec endpoint answered ${response.status} with something that is not JSON.` }), true); }
570
+ const id = payload.sessionId ?? "refused";
571
+ if (payload.status === "running") {
572
+ return text(`<task id="${id}" state="running">\n<summary>${(params.description ?? "").trim() || params.command.slice(0, 60)}</summary>\n<task_result>The command is running in the background. You will be notified automatically when it finishes. Do not sleep, poll for progress, or duplicate this work. Continue with non-overlapping work.</task_result>\n</task>`);
573
+ }
574
+ return text(renderTaskResult({ id, state: "error", text: payload.error ?? `Exec endpoint returned status ${payload.status}` }), true);
575
+ }
576
+
506
577
  /** One HTTP seam, injected so a test can drive the tool without a socket and production uses global fetch. */
507
578
  export type TodoFetch = (url: string, init: { method: string; headers: Record<string, string>; body: string; signal?: AbortSignal }) => Promise<{ ok: boolean; status: number; text(): Promise<string> }>;
508
579
 
@@ -21,6 +21,12 @@ import {
21
21
  readTodoConfig,
22
22
  type TodoParams,
23
23
  } from "./core.ts";
24
+ import {
25
+ RUN_ASYNC_DESCRIPTION,
26
+ RUN_ASYNC_TOOL_NAME,
27
+ executeAsync,
28
+ type RunAsyncParams,
29
+ } from "./core.ts";
24
30
 
25
31
  /**
26
32
  * The Pi adapter for the `task` and `todowrite` tools. Everything with a decision in it is in
@@ -225,6 +231,31 @@ const extension = (pi: ExtensionApi): void => {
225
231
  execute: async (_toolCallId, params, signal) =>
226
232
  await executeWorkflow(params as WorkflowParams, workflowConfig, { signal: signal ?? undefined }),
227
233
  });
234
+
235
+ /**
236
+ * `run_async` - asynchronous shell command execution. NOT a sub-agent dispatch.
237
+ *
238
+ * Registered LAST so a model scanning the tool list reads `task` (the more powerful delegation
239
+ * primitive) before the more specialised background-command tool. A model reaching for the wrong
240
+ * one is the failure this ordering tries to prevent.
241
+ *
242
+ * SHARES THE EXEC ENDPOINT with `task` when it had `background: true`, but is its OWN tool so the
243
+ * description can say "shell command" rather than "sub-agent with a flag" and refuse-task does not
244
+ * have to make the subagent_type optional in a way that confused a model reading the description.
245
+ */
246
+ const RunAsyncParameters = Type.Object({
247
+ command: Type.String({ description: "The shell command to run in the workspace. Runs under bash on Linux/macOS and PowerShell on Windows." }),
248
+ description: Type.Optional(Type.String({ description: "3-5 word label shown in the background tasks panel." })),
249
+ });
250
+ pi.registerTool({
251
+ name: RUN_ASYNC_TOOL_NAME,
252
+ label: "Run Async",
253
+ description: RUN_ASYNC_DESCRIPTION,
254
+ promptSnippet: "run a shell command in the background and deliver its output as a follow-up message",
255
+ parameters: RunAsyncParameters,
256
+ execute: async (_toolCallId, params, signal, _onUpdate, context) =>
257
+ await executeAsync(params as RunAsyncParams, context, config, { signal: signal ?? undefined }),
258
+ });
228
259
  };
229
260
 
230
261
  export default extension;
@@ -201,13 +201,19 @@ export function createHttpServer(options) {
201
201
  return;
202
202
  }
203
203
  response.writeHead(202, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ status: "updating" }));
204
- const command = process.platform === "win32" ? "pi.cmd" : "pi";
205
- const child = spawn(command, ["update", "npm:@evandu/super-pi"], { cwd: rootDir, detached: true, stdio: "ignore", shell: process.platform === "win32", env: process.env });
204
+ // Use npm directly instead of `pi update` to avoid Windows detached+shell exit timing issues.
205
+ // npm's exit code correctly reflects whether the update succeeded.
206
+ const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
207
+ const child = spawn(npmCmd, ["install", "-g", "@evandu/super-pi"], { stdio: "ignore", shell: process.platform === "win32", env: process.env });
206
208
  child.on("close", (code) => {
207
- if (code !== 0)
208
- return;
209
- const restart = spawn(process.execPath, [join(rootDir, "scripts", "restart-server.mjs")], { cwd: rootDir, detached: true, stdio: "ignore", env: process.env });
210
- restart.unref();
209
+ if (code === 0 || code === null) {
210
+ console.log("Super Pi: npm install completed, triggering restart.");
211
+ const restart = spawn(process.execPath, [join(rootDir, "scripts", "restart-server.mjs")], { cwd: rootDir, detached: true, stdio: "ignore", env: process.env });
212
+ restart.unref();
213
+ }
214
+ else {
215
+ console.error(`Super Pi: npm install failed with code ${code}`);
216
+ }
211
217
  });
212
218
  child.on("error", (error) => { console.warn(`Super Pi: update spawn failed: ${error instanceof Error ? error.message : "unknown error"}`); });
213
219
  child.unref();
@@ -220,7 +226,12 @@ export function createHttpServer(options) {
220
226
  }
221
227
  try {
222
228
  const current = checkDiskVersion(rootDir);
223
- const remoteVersion = await fetch("https://pi.dev/api/latest-version").then(result => result.ok ? result.json() : Promise.reject(new Error(`HTTP ${result.status}`)));
229
+ // Query the npm registry for the latest @evandu/super-pi version.
230
+ const remoteVersion = await fetch("https://registry.npmjs.org/@evandu/super-pi/latest").then(async (result) => {
231
+ if (!result.ok)
232
+ throw new Error(`npm registry HTTP ${result.status}`);
233
+ return result.json();
234
+ });
224
235
  const latestVersion = typeof remoteVersion.version === "string" ? remoteVersion.version : undefined;
225
236
  response.writeHead(200, { "content-type": "application/json; charset=utf-8" }).end(JSON.stringify({ ...current, latestVersion, updateAvailable: Boolean(latestVersion && latestVersion !== current.diskVersion) }));
226
237
  }
@@ -371,10 +382,14 @@ export function createHttpServer(options) {
371
382
  json(403, { error: result.message });
372
383
  return;
373
384
  }
374
- // Validate cwd is a known workspace
375
- const workspace = store.snapshot().workspaces.find((w) => w.path === cwd);
385
+ // Validate cwd is a known workspace. Path comparison is normalized so Windows case
386
+ // differences or trailing separators do not produce a false "unknown workspace" failure
387
+ // when the extension hands back the same directory the server handed to it at spawn.
388
+ const normalized = (value) => value.replace(/[\\/]+/g, "/").replace(/\/+$/, "").toLowerCase();
389
+ const normalizedCwd = normalized(cwd);
390
+ const workspace = store.snapshot().workspaces.find((w) => normalized(w.path) === normalizedCwd);
376
391
  if (!workspace) {
377
- json(400, { error: "cwd is not a known workspace" });
392
+ json(400, { error: `cwd is not a known workspace: ${cwd} (known: ${store.snapshot().workspaces.map((w) => w.path).join(" | ")})` });
378
393
  return;
379
394
  }
380
395
  // Set up the background task record BEFORE spawning, so the UI shows it immediately
@@ -6,7 +6,7 @@ import { resolve, join } from "node:path";
6
6
  import { agentSpawnArgs, readAgents } from "./agents.js";
7
7
  import { TASK_CAPABILITY_ENV, mintCapability, releaseCapability } from "./capability.js";
8
8
  import { resolvePiCommand, shellQuote } from "./pi-command.js";
9
- import { TASK_CONVERSATION_ENV, TASK_DEPTH_ENV, TASK_ENDPOINT_ENV, TASK_EXEC_ENDPOINT_ENV, TASK_ROSTER_ENV, TASK_TOOL_NAME, TODO_ENDPOINT_ENV, TODO_TOOL_NAME, WORKFLOW_ENDPOINT_ENV, WORKFLOW_TOOL_NAME, rosterPayload, } from "./task-tool.js";
9
+ import { TASK_CONVERSATION_ENV, TASK_DEPTH_ENV, TASK_ENDPOINT_ENV, TASK_EXEC_ENDPOINT_ENV, TASK_ROSTER_ENV, TASK_TOOL_NAME, TODO_ENDPOINT_ENV, TODO_TOOL_NAME, WORKFLOW_ENDPOINT_ENV, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, rosterPayload, } from "./task-tool.js";
10
10
  import { BLOCKING_UI_METHODS, EXTENSION_UI_METHODS, } from "./types.js";
11
11
  import { loadConversationImages } from "./upload.js";
12
12
  import { describeToolCall, describeToolCallArgs, describeToolCallInProgress, extractPartialResultText, extractToolResultText, readQuestionCall } from "./tool-calls.js";
@@ -766,7 +766,7 @@ export class PiRpcRuntime {
766
766
  // other. They also share the extension's existence - `extensionArgs` is non-empty exactly when
767
767
  // `index.ts` is on disk and an endpoint was configured - so there is no case where one is mounted and
768
768
  // the other is not.
769
- const agentArgs = memoryReview ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates", "--tools", "memory,skill_manage,session_search"] : await agentSpawnArgs(this.agents.promptDir, agent, { extraAllowedTools: [...(extensionArgs.length ? [TASK_TOOL_NAME, TODO_TOOL_NAME, WORKFLOW_TOOL_NAME, "question"] : []), ...(memoryEnabled ? ["memory", "skill_manage", "session_search"] : [])] });
769
+ const agentArgs = memoryReview ? ["--no-extensions", "--no-skills", "--no-mcp", "--no-context-files", "--no-prompt-templates", "--tools", "memory,skill_manage,session_search"] : await agentSpawnArgs(this.agents.promptDir, agent, { extraAllowedTools: [...(extensionArgs.length ? [TASK_TOOL_NAME, TODO_TOOL_NAME, WORKFLOW_TOOL_NAME, RUN_ASYNC_TOOL_NAME, "question"] : []), ...(memoryEnabled ? ["memory", "skill_manage", "session_search"] : [])] });
770
770
  // Refused here, with the reason, rather than handed to the spawn: an ENOENT from `spawn` reads as
771
771
  // "the app is broken" and hides the one fact that matters, which is where it looked for Pi.
772
772
  if (!this.pi.found)
@@ -25,6 +25,15 @@ import type { AgentDefinition, TodoItem } from "./types.js";
25
25
  */
26
26
  /** The name the model calls. Also the key Pi's tool allowlist matches on, and the deny key in `tools:`. */
27
27
  export declare const TASK_TOOL_NAME = "task";
28
+ /**
29
+ * The independent background-command tool. NOT a sub-agent dispatch.
30
+ *
31
+ * Mirrored from `agents/super-pi-task/core.ts` for the same reason `TASK_TOOL_NAME` is: a tool name
32
+ * is a wire between two files Pi's module loader cannot cross, and `scripts/task-tool.test.mjs`
33
+ * asserts the two copies agree. Renaming on one side alone would fail at registerTool time with a
34
+ * message nobody reads.
35
+ */
36
+ export declare const RUN_ASYNC_TOOL_NAME = "run_async";
28
37
  /**
29
38
  * How many task hops are allowed below a primary agent.
30
39
  *
@@ -24,6 +24,15 @@
24
24
  */
25
25
  /** The name the model calls. Also the key Pi's tool allowlist matches on, and the deny key in `tools:`. */
26
26
  export const TASK_TOOL_NAME = "task";
27
+ /**
28
+ * The independent background-command tool. NOT a sub-agent dispatch.
29
+ *
30
+ * Mirrored from `agents/super-pi-task/core.ts` for the same reason `TASK_TOOL_NAME` is: a tool name
31
+ * is a wire between two files Pi's module loader cannot cross, and `scripts/task-tool.test.mjs`
32
+ * asserts the two copies agree. Renaming on one side alone would fail at registerTool time with a
33
+ * message nobody reads.
34
+ */
35
+ export const RUN_ASYNC_TOOL_NAME = "run_async";
27
36
  /**
28
37
  * How many task hops are allowed below a primary agent.
29
38
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evandu/super-pi",
3
- "version": "0.1.57",
3
+ "version": "0.1.59",
4
4
  "description": "Web control plane and companion for Pi",
5
5
  "license": "MIT",
6
6
  "type": "module",