@pi-unipi/background-tasks 2.6.1

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 (116) hide show
  1. package/README.md +87 -0
  2. package/extensions/anthropic-attribution.ts +1 -0
  3. package/extensions/delegate-child.ts +1 -0
  4. package/extensions/fusion-child.ts +1 -0
  5. package/package.json +40 -0
  6. package/src/__tests__/anthropic-attribution.test.ts +195 -0
  7. package/src/__tests__/config.test.ts +137 -0
  8. package/src/__tests__/core.test.ts +493 -0
  9. package/src/__tests__/delegate-artifacts.test.ts +528 -0
  10. package/src/__tests__/delegate-budget.test.ts +456 -0
  11. package/src/__tests__/delegate-launch.test.ts +676 -0
  12. package/src/__tests__/delegate-result-package.test.ts +350 -0
  13. package/src/__tests__/delegate-seed.test.ts +392 -0
  14. package/src/__tests__/durable-fs.test.ts +559 -0
  15. package/src/__tests__/extension-api.test.ts +579 -0
  16. package/src/__tests__/fusion-artifacts.test.ts +1039 -0
  17. package/src/__tests__/fusion-budget.test.ts +1356 -0
  18. package/src/__tests__/fusion-claude-cache.test.ts +320 -0
  19. package/src/__tests__/fusion-config.test.ts +335 -0
  20. package/src/__tests__/fusion-context-prompts.test.ts +670 -0
  21. package/src/__tests__/fusion-evaluation.test.ts +315 -0
  22. package/src/__tests__/fusion-extraction-equivalence.test.ts +58 -0
  23. package/src/__tests__/fusion-golden-bytes.test.ts +35 -0
  24. package/src/__tests__/fusion-high-cardinality.test.ts +192 -0
  25. package/src/__tests__/fusion-model-selector.test.ts +205 -0
  26. package/src/__tests__/fusion-orchestrator.test.ts +1194 -0
  27. package/src/__tests__/fusion-rpc.test.ts +369 -0
  28. package/src/__tests__/fusion-sdk.test.ts +1226 -0
  29. package/src/__tests__/fusion-v5-core.test.ts +219 -0
  30. package/src/__tests__/fusion-validate-orchestrator.test.ts +240 -0
  31. package/src/__tests__/fusion-web-fetch.test.ts +485 -0
  32. package/src/__tests__/fusion-workflows.test.ts +59 -0
  33. package/src/__tests__/helpers/delegate-deterministic-seed.ts +109 -0
  34. package/src/__tests__/helpers/delegate-seed-subprocess.ts +10 -0
  35. package/src/__tests__/helpers/fusion-canonical-subprocess.ts +21 -0
  36. package/src/__tests__/helpers/fusion-canonical.ts +140 -0
  37. package/src/__tests__/helpers/fusion-fake-pi.ts +279 -0
  38. package/src/__tests__/helpers/fusion-golden-corpus.ts +500 -0
  39. package/src/__tests__/helpers/fusion-high-cardinality.ts +140 -0
  40. package/src/__tests__/helpers/normalize.ts +22 -0
  41. package/src/__tests__/helpers/pi-hook-contract-evidence.json +18 -0
  42. package/src/__tests__/pi-launch.test.ts +202 -0
  43. package/src/__tests__/registry.test.ts +1580 -0
  44. package/src/__tests__/scripted-provider/delegate-ambient-provider.test.ts +130 -0
  45. package/src/__tests__/scripted-provider/delegate-child-guard.test.ts +631 -0
  46. package/src/__tests__/scripted-provider/delegate-guard-provider.ts +403 -0
  47. package/src/__tests__/scripted-provider/follow-up.test.ts +448 -0
  48. package/src/__tests__/scripted-provider/fusion-output-recovery.test.ts +132 -0
  49. package/src/__tests__/scripted-provider/fusion-reason.test.ts +310 -0
  50. package/src/__tests__/scripted-provider/fusion-runtime-guard.test.ts +163 -0
  51. package/src/__tests__/scripted-provider/hook-contract-provider.ts +179 -0
  52. package/src/__tests__/scripted-provider/hook-probe-a.ts +3 -0
  53. package/src/__tests__/scripted-provider/hook-probe-b.ts +3 -0
  54. package/src/__tests__/scripted-provider/hook-probe-extension.ts +126 -0
  55. package/src/__tests__/scripted-provider/output-recovery-provider.ts +153 -0
  56. package/src/__tests__/scripted-provider/pi-hook-contract-evidence.json +18 -0
  57. package/src/__tests__/scripted-provider/pi-hook-contract.test.ts +477 -0
  58. package/src/__tests__/scripted-provider/runtime-guard-probe.ts +28 -0
  59. package/src/__tests__/scripted-provider/runtime-guard-provider.ts +49 -0
  60. package/src/__tests__/scripted-provider/scripted-provider-extension.ts +408 -0
  61. package/src/__tests__/task-manager.test.ts +479 -0
  62. package/src/__tests__/windows-taskkill.test.ts +161 -0
  63. package/src/anthropic-attribution-path.ts +21 -0
  64. package/src/anthropic-attribution.ts +1983 -0
  65. package/src/attested-pi-run.ts +612 -0
  66. package/src/child-process.ts +55 -0
  67. package/src/common.ts +8 -0
  68. package/src/config.ts +292 -0
  69. package/src/context-parent-snapshot.ts +142 -0
  70. package/src/context-token-budget.ts +903 -0
  71. package/src/context-visible-conversation-v2.ts +551 -0
  72. package/src/delegate/artifacts.ts +487 -0
  73. package/src/delegate/budget.ts +415 -0
  74. package/src/delegate/hook-contract-evidence.json +18 -0
  75. package/src/delegate/hook-contract.ts +154 -0
  76. package/src/delegate/launch.ts +497 -0
  77. package/src/delegate/result-package.ts +459 -0
  78. package/src/delegate/runner.ts +449 -0
  79. package/src/delegate/seed.ts +423 -0
  80. package/src/delegate/types.ts +323 -0
  81. package/src/delegate-child-extension.ts +978 -0
  82. package/src/delegate-extension.ts +806 -0
  83. package/src/durable-fs.ts +386 -0
  84. package/src/extension-api.ts +548 -0
  85. package/src/fixtures/delegate-context-incident.json +17 -0
  86. package/src/fixtures/fusion-golden-bytes.json +310 -0
  87. package/src/fixtures/fusion-validate-golden-bytes.json +282 -0
  88. package/src/fusion/artifacts.ts +967 -0
  89. package/src/fusion/budget.ts +1162 -0
  90. package/src/fusion/child-protocol.ts +305 -0
  91. package/src/fusion/claude-cache.ts +207 -0
  92. package/src/fusion/clean-context.ts +91 -0
  93. package/src/fusion/config.ts +449 -0
  94. package/src/fusion/context.ts +265 -0
  95. package/src/fusion/evaluation.ts +800 -0
  96. package/src/fusion/orchestrator.ts +1288 -0
  97. package/src/fusion/output-contract.ts +34 -0
  98. package/src/fusion/pi-child.ts +2373 -0
  99. package/src/fusion/prompts.ts +345 -0
  100. package/src/fusion/result-package.ts +959 -0
  101. package/src/fusion/source-policy.ts +257 -0
  102. package/src/fusion/types.ts +1139 -0
  103. package/src/fusion/web-fetch.ts +1060 -0
  104. package/src/fusion/workflows.ts +184 -0
  105. package/src/fusion-child-extension.ts +1052 -0
  106. package/src/fusion-extension.ts +1293 -0
  107. package/src/index.ts +295 -0
  108. package/src/pi-launch.ts +225 -0
  109. package/src/registry.ts +2424 -0
  110. package/src/settings-overlay.ts +208 -0
  111. package/src/task-manager.ts +774 -0
  112. package/src/tools.ts +530 -0
  113. package/src/turndown.d.ts +15 -0
  114. package/src/types.ts +963 -0
  115. package/src/ui/fusion-model-selector.ts +322 -0
  116. package/src/windows-taskkill.ts +250 -0
package/src/tools.ts ADDED
@@ -0,0 +1,530 @@
1
+ /**
2
+ * @pi-unipi/background-tasks — Tools & commands (Phase 2)
3
+ *
4
+ * Ported from pi-background-tasks src/extension.ts tool/command registration.
5
+ * Commands live in OUR /unipi:* namespace; tools keep reference names.
6
+ */
7
+
8
+ import { Text } from "@earendil-works/pi-tui";
9
+ import { getInstalledPackageVersion } from "@pi-unipi/core";
10
+ import { dirname } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { Type, type Static } from "typebox";
13
+ import {
14
+ COMMAND_PREVIEW_CHARS,
15
+ DEFAULT_LOG_BYTES,
16
+ MAX_LOG_BYTES,
17
+ deriveCompletionDeliveryGuidance,
18
+ deriveTaskNameFromCommand,
19
+ formatSnapshotList,
20
+ normalizeMaxBytes,
21
+ normalizeTaskName,
22
+ parseBgCommandArgs,
23
+ taskDisplayName,
24
+ truncateChars,
25
+ type BgKillDetails,
26
+ type BgLogsDetails,
27
+ type BgRunDetails,
28
+ type BgStatusDetails,
29
+ type BgTaskSnapshot,
30
+ type StartAttestedPiTaskOptions,
31
+ type StartTaskOptions,
32
+ } from "./types.js";
33
+ import type { BackgroundTaskRegistry } from "./registry.js";
34
+
35
+ export function textContent(text: string): Array<{ type: "text"; text: string }> {
36
+ return [{ type: "text" as const, text }];
37
+ }
38
+
39
+ // ── Tool parameter schemas ──────────────────────────────────────────────────
40
+
41
+ export const BgRunParams = Type.Object({
42
+ name: Type.String({
43
+ description:
44
+ "Short human-readable task name shown in the bg footer dock. Required; use 2-6 words, not the raw command.",
45
+ }),
46
+ command: Type.String({ description: "Shell command to start in the background" }),
47
+ isAgent: Type.Boolean({
48
+ description:
49
+ "True ONLY when the command launches an LLM/agent process (enables telemetry wrapping). False for scripts, tests, servers, sleeps.",
50
+ }),
51
+ description: Type.Optional(Type.String({ description: "Longer human-readable description" })),
52
+ timeoutSeconds: Type.Optional(
53
+ Type.Number({ description: "Kill the task after this many seconds (optional)" }),
54
+ ),
55
+ notifyOnCompletion: Type.Optional(
56
+ Type.Boolean({
57
+ description:
58
+ "Deliver a durable terminal notification when the task finishes. Default true. Do not disable unless opting out of completion handling.",
59
+ }),
60
+ ),
61
+ triggerOnCompletion: Type.Optional(
62
+ Type.Boolean({
63
+ description:
64
+ "Start a follow-up agent turn on terminal notification. Default true for bg_run. Requires notifyOnCompletion.",
65
+ }),
66
+ ),
67
+ });
68
+
69
+ export const BgStatusParams = Type.Object({
70
+ taskId: Type.Optional(
71
+ Type.String({ description: "Task ID or unambiguous prefix. Omit to list all tasks." }),
72
+ ),
73
+ });
74
+
75
+ export const BgLogsParams = Type.Object({
76
+ taskId: Type.String({ description: "Task ID or unambiguous prefix" }),
77
+ maxBytes: Type.Optional(
78
+ Type.Number({
79
+ description: `Maximum bytes of output to return (1-${String(MAX_LOG_BYTES)}). Default ${String(DEFAULT_LOG_BYTES)}.`,
80
+ }),
81
+ ),
82
+ tail: Type.Optional(
83
+ Type.Boolean({ description: "Read from the end (tail, default true) or the beginning (head)" }),
84
+ ),
85
+ });
86
+
87
+ export const BgKillParams = Type.Object({
88
+ taskId: Type.String({ description: "Task ID or unambiguous prefix of a running task" }),
89
+ });
90
+
91
+ export const BgPiAttestedParams = Type.Object({
92
+ name: Type.String({ description: "Short human-readable task name" }),
93
+ provider: Type.String({ description: "Pi provider id (e.g. anthropic)" }),
94
+ model: Type.String({ description: "Model id under the provider" }),
95
+ prompt: Type.String({ description: "Final user prompt for the child Pi run" }),
96
+ reportPath: Type.String({
97
+ description: "Relative path (inside cwd) where the child must write its report before exit",
98
+ }),
99
+ extraPiArgs: Type.Optional(
100
+ Type.Array(Type.String(), { description: "Extra literal pi CLI args (restricted)" }),
101
+ ),
102
+ thinking: Type.Optional(Type.String({ description: "Thinking level passed as --thinking" })),
103
+ timeoutSeconds: Type.Optional(Type.Number({ description: "Kill after this many seconds" })),
104
+ });
105
+
106
+ // ── Registration ────────────────────────────────────────────────────────────
107
+
108
+ export interface RegisterSurfaceOptions {
109
+ pi: import("@earendil-works/pi-coding-agent").ExtensionAPI;
110
+ registry: BackgroundTaskRegistry;
111
+ startTask: (ctx: any, command: string, options?: StartTaskOptions) => Promise<any>;
112
+ startAttestedPiTask: (
113
+ ctx: any,
114
+ options: StartAttestedPiTaskOptions,
115
+ ) => Promise<any>;
116
+ openTaskManager: (ctx: any, initialTaskId?: string) => Promise<void>;
117
+ clearFinishedNotices: (ctx: any) => number;
118
+ openSettings: (ctx: any) => Promise<void>;
119
+ }
120
+
121
+ function renderPlainResult(
122
+ result: { content: ReadonlyArray<{ type: string; text?: string }> },
123
+ _options: unknown,
124
+ theme: any,
125
+ ): Text {
126
+ const text = result.content
127
+ .map((content) => (content.type === "text" ? (content.text ?? "") : "[image content]"))
128
+ .join("\n");
129
+ return new Text(theme.fg("toolOutput", text), 0, 0);
130
+ }
131
+
132
+ /** Register all bg_* tools + /unipi:* commands + shortcuts. */
133
+ export function registerToolsAndCommands(options: RegisterSurfaceOptions): void {
134
+ const { pi, registry } = options;
135
+
136
+ // ── Commands (/unipi:* namespace — ours) ──────────────────────────────────
137
+
138
+ // Update info lives with OUR updater module; this only reports versions.
139
+ pi.registerCommand("unipi:bg-update", {
140
+ description: "Show the installed background-tasks version and how to update",
141
+ handler: (_args, ctx) => {
142
+ const here = dirname(fileURLToPath(import.meta.url));
143
+ const current = getInstalledPackageVersion(here, "@pi-unipi/background-tasks");
144
+ const lines = [
145
+ `@pi-unipi/background-tasks ${current} is installed.`,
146
+ "Background tasks ship inside the @pi-unipi/unipi umbrella package.",
147
+ "Update from npm:",
148
+ " pi install npm:@pi-unipi/unipi@latest",
149
+ "Or use /unipi:updater-settings to check for updates.",
150
+ "This command only prints update instructions; it does not install or self-update.",
151
+ ];
152
+ ctx.ui.notify(lines.join("\n"), "info");
153
+ return Promise.resolve();
154
+ },
155
+ });
156
+
157
+ pi.registerCommand("unipi:bg", {
158
+ description:
159
+ 'Start a shell command as a tracked background task: /unipi:bg [--agent] [--name "Task name"] <command>',
160
+ handler: async (args, ctx) => {
161
+ try {
162
+ const parsed = parseBgCommandArgs(args);
163
+ const taskOptions: StartTaskOptions = {
164
+ isAgent: parsed.isAgent,
165
+ notifyOnCompletion: true,
166
+ triggerOnCompletion: false,
167
+ };
168
+ if (parsed.name !== undefined) taskOptions.name = parsed.name;
169
+ const task = await options.startTask(ctx, parsed.command, taskOptions);
170
+ ctx.ui.notify(
171
+ `Started ${taskDisplayName(task)} (${task.id})\nOutput: ${task.outputPath}\nCommand: ${task.command}`,
172
+ "info",
173
+ );
174
+ } catch (error) {
175
+ ctx.ui.notify(
176
+ `Background task failed to start: ${error instanceof Error ? error.message : String(error)}`,
177
+ "error",
178
+ );
179
+ }
180
+ },
181
+ });
182
+
183
+ pi.registerCommand("unipi:tasks", {
184
+ description: "Open the background task manager UI",
185
+ handler: async (args, ctx) => {
186
+ const taskId = typeof args === "string" ? args.trim() : "";
187
+ await options.openTaskManager(ctx, taskId || undefined);
188
+ },
189
+ });
190
+
191
+ pi.registerCommand("unipi:bg-tasks", {
192
+ description: "Open the background task manager UI",
193
+ handler: async (args, ctx) => {
194
+ const taskId = typeof args === "string" ? args.trim() : "";
195
+ await options.openTaskManager(ctx, taskId || undefined);
196
+ },
197
+ });
198
+
199
+ pi.registerCommand("unipi:bg-clear", {
200
+ description: "Clear finished background task footer notices",
201
+ handler: (_args, ctx) => {
202
+ options.clearFinishedNotices(ctx);
203
+ return Promise.resolve();
204
+ },
205
+ });
206
+
207
+ pi.registerCommand("unipi:jobs", {
208
+ description: "List running and recent background tasks",
209
+ handler: (_args, ctx) => {
210
+ ctx.ui.notify(
211
+ formatSnapshotList(registry.allTasks().map((task) => registry.snapshot(task))),
212
+ "info",
213
+ );
214
+ return Promise.resolve();
215
+ },
216
+ });
217
+
218
+ pi.registerCommand("unipi:logs", {
219
+ description: "Show bounded output from a background task: /unipi:logs <id> [maxBytes]",
220
+ getArgumentCompletions: (prefix: string) => {
221
+ const matches = registry
222
+ .allTasks()
223
+ .filter((task) => task.id.startsWith(prefix.trim()))
224
+ .slice(0, 20)
225
+ .map((task) => ({
226
+ value: task.id,
227
+ label: `${task.id} ${taskDisplayName(task)}`,
228
+ description: `${task.status} — ${truncateChars(task.command, 60)}`,
229
+ }));
230
+ return matches.length > 0 ? matches : null;
231
+ },
232
+ handler: async (args, ctx) => {
233
+ try {
234
+ const [id, bytes] = args.trim().split(/\s+/, 2);
235
+ const task = registry.resolveTask(id ?? "");
236
+ const maxBytes = normalizeMaxBytes(Number(bytes), DEFAULT_LOG_BYTES);
237
+ const logs = await registry.getTaskLogs(task, maxBytes, true);
238
+ ctx.ui.notify(logs.text, "info");
239
+ } catch (error) {
240
+ ctx.ui.notify(
241
+ `Background logs error: ${error instanceof Error ? error.message : String(error)}`,
242
+ "error",
243
+ );
244
+ }
245
+ },
246
+ });
247
+
248
+ pi.registerCommand("unipi:kill", {
249
+ description: "Stop a running background task: /unipi:kill <id>",
250
+ getArgumentCompletions: (prefix: string) => {
251
+ const matches = registry
252
+ .allTasks()
253
+ .filter((task) => task.status === "running" && task.id.startsWith(prefix.trim()))
254
+ .slice(0, 20)
255
+ .map((task) => ({
256
+ value: task.id,
257
+ label: `${task.id} ${taskDisplayName(task)}`,
258
+ description: truncateChars(task.command, 70),
259
+ }));
260
+ return matches.length > 0 ? matches : null;
261
+ },
262
+ handler: async (args, ctx) => {
263
+ try {
264
+ const task = registry.resolveTask(args.trim());
265
+ await registry.stopTask(task, "user");
266
+ ctx.ui.notify(
267
+ `Killed ${taskDisplayName(task)} (${task.id}). Output: ${task.outputPath}`,
268
+ "info",
269
+ );
270
+ } catch (error) {
271
+ ctx.ui.notify(
272
+ `Background kill error: ${error instanceof Error ? error.message : String(error)}`,
273
+ "error",
274
+ );
275
+ }
276
+ },
277
+ });
278
+
279
+ // Shortcuts (same keys as reference; documented in our README)
280
+ pi.registerCommand("unipi:bg-settings", {
281
+ description: "Open background-tasks settings (master toggle, defaults)",
282
+ handler: async (_args, ctx) => {
283
+ await options.openSettings(ctx);
284
+ },
285
+ });
286
+
287
+ pi.registerShortcut("shift+down" as never, {
288
+ description: "Open focused background task footer dock",
289
+ handler: async (ctx) => {
290
+ await options.openTaskManager(ctx);
291
+ },
292
+ });
293
+ pi.registerShortcut("ctrl+alt+c" as never, {
294
+ description: "Clear finished background task footer notices",
295
+ handler: (ctx) => {
296
+ options.clearFinishedNotices(ctx);
297
+ },
298
+ });
299
+
300
+ // ── Notification renderer ────────────────────────────────────────────────
301
+
302
+ pi.registerMessageRenderer<BgTaskSnapshot>(
303
+ "background-task-notification",
304
+ (message: { details?: BgTaskSnapshot }, _options: unknown, theme: any) => {
305
+ const task = message.details;
306
+ const status = task?.status ?? "completed";
307
+ const color: string =
308
+ status === "completed" ? "success" : status === "failed" ? "error" : status === "killed" ? "warning" : "accent";
309
+ const id = task?.id ?? "background task";
310
+ const name = task ? taskDisplayName(task) : "Background task";
311
+ const output = task?.outputPath ? `\n${theme.fg("dim", `Output: ${task.outputPath}`)}` : "";
312
+ const error = task?.error ? `\n${theme.fg("error", task.error)}` : "";
313
+ return new Text(
314
+ `${theme.fg(color, `[bg ${status}]`)} ${theme.fg("accent", name)} ${theme.fg("dim", `(${id})`)}${output}${error}`,
315
+ 0,
316
+ 0,
317
+ );
318
+ },
319
+ );
320
+
321
+ // ── Tools (reference names kept) ─────────────────────────────────────────
322
+
323
+ pi.registerTool<typeof BgRunParams, BgRunDetails>({
324
+ name: "bg_run",
325
+ label: "Background Run",
326
+ description: `Start a named long-running shell command in the background and return immediately with a task ID and output path. By default, terminal state is delivered automatically as <background-task-notification> and starts a follow-up agent turn; do not sleep or poll merely to wait. Output is written under the OS temp root and model-visible logs are bounded.`,
327
+ promptSnippet:
328
+ "Start a named long-running shell command; default terminal notification wakes a follow-up turn, so yield instead of polling",
329
+ promptGuidelines: [
330
+ "Use bg_run instead of bash for commands expected to run for a long time, such as test suites, dev servers, watchers, or builds.",
331
+ "Set isAgent:true only when the background task launches an LLM/agent process; false for scripts, tests, dev servers, sleeps.",
332
+ "Always set name to a concise 2-6 word human-readable label; do not use the raw command as the name.",
333
+ "After a default bg_run launch, continue only independent useful work; otherwise end the turn and wait for <background-task-notification>.",
334
+ "Treat <background-task-notification> as durable terminal truth. Do not call bg_status to reconfirm it.",
335
+ "Do not set notifyOnCompletion:false or triggerOnCompletion:false unless intentionally opting out.",
336
+ ],
337
+ parameters: BgRunParams,
338
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
339
+ if (typeof params.isAgent !== "boolean") {
340
+ throw new Error(
341
+ "bg_run requires isAgent boolean. Set true only for LLM/agent tasks; set false for scripts, tests, servers, sleeps.",
342
+ );
343
+ }
344
+ const taskOptions: StartTaskOptions = {
345
+ name: params.name,
346
+ isAgent: params.isAgent,
347
+ notifyOnCompletion: params.notifyOnCompletion ?? true,
348
+ triggerOnCompletion: params.triggerOnCompletion ?? true,
349
+ };
350
+ if (params.description !== undefined) taskOptions.description = params.description;
351
+ if (params.timeoutSeconds !== undefined) taskOptions.timeoutSeconds = params.timeoutSeconds;
352
+ const task = await options.startTask(ctx, params.command, taskOptions);
353
+ const completionDelivery = deriveCompletionDeliveryGuidance(
354
+ task.notifyOnCompletion,
355
+ task.triggerOnCompletion,
356
+ );
357
+ return {
358
+ content: textContent(
359
+ `Started background task ${taskDisplayName(task)} (${task.id})\nStatus: ${task.status}\nPID: ${String(task.pid ?? "unknown")}\nOutput: ${task.outputPath}\n${completionDelivery.text}`,
360
+ ),
361
+ details: { task: registry.snapshot(task) },
362
+ };
363
+ },
364
+ renderCall(args: Static<typeof BgRunParams>, theme: any) {
365
+ return new Text(
366
+ `${theme.fg("toolTitle", theme.bold("bg_run "))}${theme.fg("muted", truncateChars(taskDisplayName(args), COMMAND_PREVIEW_CHARS))}`,
367
+ 0,
368
+ 0,
369
+ );
370
+ },
371
+ renderResult(result: { details: BgRunDetails }, _options: unknown, theme: any) {
372
+ const { task } = result.details;
373
+ return new Text(
374
+ `${theme.fg("success", "✓ started")} ${theme.fg("accent", taskDisplayName(task))} ${theme.fg("dim", `(${task.id})`)}\n${theme.fg("dim", `Output: ${task.outputPath}`)}`,
375
+ 0,
376
+ 0,
377
+ );
378
+ },
379
+ });
380
+
381
+ pi.registerTool<typeof BgStatusParams, BgStatusDetails>({
382
+ name: "bg_status",
383
+ label: "Background Status",
384
+ description:
385
+ "Inspect one background task or list all running/recent background tasks. Point-in-time inspection tool, not a waiting primitive.",
386
+ promptSnippet:
387
+ "Inspect point-in-time status for one or all background tasks; never poll it as a wait loop",
388
+ promptGuidelines: [
389
+ "Use bg_status for deliberate point-in-time inspection, not as a waiting primitive.",
390
+ "A running result is not an instruction to poll again.",
391
+ "Use bg_status when the user explicitly requests an update, automatic completion handling was disabled, or there is concrete evidence a task is hung.",
392
+ ],
393
+ parameters: BgStatusParams,
394
+ execute(_toolCallId, params) {
395
+ const selected = params.taskId ? [registry.resolveTask(params.taskId)] : registry.allTasks();
396
+ const snapshots = selected.map((task) => registry.snapshot(task));
397
+ return Promise.resolve({
398
+ content: textContent(formatSnapshotList(snapshots)),
399
+ details: { tasks: snapshots },
400
+ });
401
+ },
402
+ renderCall(args: Static<typeof BgStatusParams>, theme: any) {
403
+ return new Text(
404
+ `${theme.fg("toolTitle", theme.bold("bg_status"))}${args.taskId ? ` ${theme.fg("accent", args.taskId)}` : ""}`,
405
+ 0,
406
+ 0,
407
+ );
408
+ },
409
+ renderResult: renderPlainResult,
410
+ });
411
+
412
+ pi.registerTool<typeof BgLogsParams, BgLogsDetails>({
413
+ name: "bg_logs",
414
+ label: "Background Logs",
415
+ description:
416
+ "Read bounded output from a background task for deliberate inspection; not a waiting primitive. Output is capped for model safety and points to the full output file when truncated.",
417
+ promptSnippet: "Read bounded task output when needed; never tail it repeatedly as a wait loop",
418
+ promptGuidelines: [
419
+ "Use bg_logs with a modest maxBytes value only when task output is needed.",
420
+ "Do not repeatedly call bg_logs to wait for completion while an automatic terminal notification is pending.",
421
+ ],
422
+ parameters: BgLogsParams,
423
+ async execute(_toolCallId, params) {
424
+ const task = registry.resolveTask(params.taskId);
425
+ const logs = await registry.getTaskLogs(
426
+ task,
427
+ normalizeMaxBytes(params.maxBytes),
428
+ params.tail ?? true,
429
+ );
430
+ return {
431
+ content: textContent(logs.text),
432
+ details: logs.details,
433
+ };
434
+ },
435
+ renderCall(args: Static<typeof BgLogsParams>, theme: any) {
436
+ return new Text(
437
+ `${theme.fg("toolTitle", theme.bold("bg_logs "))}${theme.fg("accent", args.taskId)}`,
438
+ 0,
439
+ 0,
440
+ );
441
+ },
442
+ renderResult(result: { details: BgLogsDetails; content: ReadonlyArray<{ type: string; text?: string }> }, options: { expanded?: boolean }, theme: any) {
443
+ const details = result.details;
444
+ let text = `${theme.fg("accent", taskDisplayName(details.task))} ${theme.fg("dim", `(${details.task.id})`)} ${theme.fg("muted", details.tail ? "tail" : "head")} ${details.bytesRead}`;
445
+ if (details.truncated) text += theme.fg("warning", " (truncated)");
446
+ text += `\n${theme.fg("dim", `Full output: ${details.path}`)}`;
447
+ if (options.expanded) {
448
+ const output = result.content
449
+ .map((content) => (content.type === "text" ? (content.text ?? "") : "[image content]"))
450
+ .join("\n");
451
+ text += `\n${theme.fg("toolOutput", output.split("\n").slice(0, 30).join("\n"))}`;
452
+ }
453
+ return new Text(text, 0, 0);
454
+ },
455
+ });
456
+
457
+ pi.registerTool<typeof BgKillParams, BgKillDetails>({
458
+ name: "bg_kill",
459
+ label: "Background Kill",
460
+ description:
461
+ "Stop a running background task by ID. Fails loudly if the task is unknown or already finished.",
462
+ promptSnippet: "Stop a running background task by ID",
463
+ promptGuidelines: [
464
+ "Use bg_kill when the user asks to stop a background task or when a bg_run command is no longer needed.",
465
+ ],
466
+ parameters: BgKillParams,
467
+ async execute(_toolCallId, params) {
468
+ const task = registry.resolveTask(params.taskId);
469
+ await registry.stopTask(task, "user");
470
+ const message = `Killed background task ${taskDisplayName(task)} (${task.id}). Output: ${task.outputPath}`;
471
+ return {
472
+ content: textContent(message),
473
+ details: { task: registry.snapshot(task), message },
474
+ };
475
+ },
476
+ renderCall(args: Static<typeof BgKillParams>, theme: any) {
477
+ return new Text(
478
+ `${theme.fg("toolTitle", theme.bold("bg_kill "))}${theme.fg("accent", args.taskId)}`,
479
+ 0,
480
+ 0,
481
+ );
482
+ },
483
+ renderResult(result: { details: BgKillDetails }, _options: unknown, theme: any) {
484
+ const { task } = result.details;
485
+ return new Text(
486
+ `${theme.fg("warning", "■ killed")} ${theme.fg("accent", taskDisplayName(task))} ${theme.fg("dim", `(${task.id})`)}\n${theme.fg("dim", `Output: ${task.outputPath}`)}`,
487
+ 0,
488
+ 0,
489
+ );
490
+ },
491
+ });
492
+
493
+ pi.registerTool<typeof BgPiAttestedParams, BgRunDetails>({
494
+ name: "bg_run_pi_attested",
495
+ label: "Attested Pi Run",
496
+ description:
497
+ "Opt-in evidence-oriented direct Pi spawn. Launches exactly one `pi --mode json` child, records raw events/stderr, hashes prompt/report/output, observes OAuth through ModelRegistry, and emits a strict attestation sidecar only after successful completion.",
498
+ promptSnippet: "Start an attested direct Pi agent task and return its task ID plus output path",
499
+ promptGuidelines: [
500
+ "Use only when the user explicitly asks for an attested Pi evidence-producing task; ordinary background work should use bg_run unchanged.",
501
+ "Provide provider/model as structured fields and a relative reportPath that the child Pi prompt will write before exit.",
502
+ "Do not provide channel, auth, route, or hash claims; the producer observes those facts itself and fails loudly if it cannot attest them.",
503
+ ],
504
+ parameters: BgPiAttestedParams,
505
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
506
+ const task = await options.startAttestedPiTask(ctx, params);
507
+ return {
508
+ content: textContent(
509
+ `Started attested Pi task ${taskDisplayName(task)} (${task.id})\nStatus: ${task.status}\nPID: ${String(task.pid ?? "unknown")}\nOutput: ${task.outputPath}\nAttestation: ${task.attestationPath ?? "pending until completion"}`,
510
+ ),
511
+ details: { task: registry.snapshot(task) },
512
+ };
513
+ },
514
+ renderCall(args: Static<typeof BgPiAttestedParams>, theme: any) {
515
+ return new Text(
516
+ `${theme.fg("toolTitle", theme.bold("bg_run_pi_attested "))}${theme.fg("muted", truncateChars(args.name, COMMAND_PREVIEW_CHARS))}`,
517
+ 0,
518
+ 0,
519
+ );
520
+ },
521
+ renderResult(result: { details: BgRunDetails }, _options: unknown, theme: any) {
522
+ const { task } = result.details;
523
+ return new Text(
524
+ `${theme.fg("success", "✓ started")} ${theme.fg("accent", taskDisplayName(task))} ${theme.fg("dim", `(${task.id})`)}\n${theme.fg("dim", `Output: ${task.outputPath}`)}\n${theme.fg("dim", `Attestation: ${task.attestationPath ?? "pending"}`)}`,
525
+ 0,
526
+ 0,
527
+ );
528
+ },
529
+ });
530
+ }
@@ -0,0 +1,15 @@
1
+ /** Turndown has no bundled type declarations; we use a narrow surface. */
2
+ declare module "turndown" {
3
+ interface TurndownService {
4
+ turndown(html: string): string;
5
+ addRule(key: string, rule: unknown): TurndownService;
6
+ remove(tags: string | string[]): TurndownService;
7
+ keep(tags: string | string[]): TurndownService;
8
+ use(plugin: unknown): TurndownService;
9
+ }
10
+ interface TurndownConstructor {
11
+ new (options?: Record<string, unknown>): TurndownService;
12
+ }
13
+ const TurndownService: TurndownConstructor;
14
+ export default TurndownService;
15
+ }