@pi-unipi/unipi 2.16.0 → 2.17.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 (95) hide show
  1. package/CHANGELOG.md +24 -1
  2. package/package.json +26 -23
  3. package/packages/ask-user/package.json +2 -2
  4. package/packages/autocomplete/package.json +1 -1
  5. package/packages/autocomplete/src/constants.ts +11 -18
  6. package/packages/background-tasks/README.md +21 -27
  7. package/packages/background-tasks/package.json +3 -4
  8. package/packages/background-tasks/src/cards.ts +76 -0
  9. package/packages/background-tasks/src/child-process.ts +1 -1
  10. package/packages/background-tasks/src/config.ts +0 -42
  11. package/packages/background-tasks/src/context-visible-conversation-v2.ts +1 -1
  12. package/packages/background-tasks/src/delegate/artifacts.ts +1 -1
  13. package/packages/background-tasks/src/delegate/hook-contract-evidence.json +18 -0
  14. package/packages/background-tasks/src/delegate/launch.ts +17 -30
  15. package/packages/background-tasks/src/delegate/result-package.ts +1 -1
  16. package/packages/background-tasks/src/delegate/runner.ts +1 -20
  17. package/packages/background-tasks/src/delegate/seed.ts +1 -1
  18. package/packages/background-tasks/src/delegate-extension.ts +16 -168
  19. package/packages/background-tasks/src/fixtures/delegate-context-incident.json +17 -0
  20. package/packages/background-tasks/src/index.ts +53 -25
  21. package/packages/background-tasks/src/json-utils.ts +56 -0
  22. package/packages/background-tasks/src/package-assets.ts +51 -0
  23. package/packages/background-tasks/src/registry.ts +8 -459
  24. package/packages/background-tasks/src/task-manager.ts +13 -2
  25. package/packages/background-tasks/src/tools.ts +4 -189
  26. package/packages/background-tasks/src/types.ts +17 -70
  27. package/packages/btw/package.json +2 -2
  28. package/packages/compactor/package.json +3 -3
  29. package/packages/core/fusion-status.ts +39 -0
  30. package/packages/core/index.ts +2 -0
  31. package/packages/core/package.json +1 -1
  32. package/packages/core/spinner-line.ts +99 -0
  33. package/packages/core/tui-overlay.ts +32 -0
  34. package/packages/footer/package.json +3 -3
  35. package/packages/footer/src/glance-editor.ts +22 -3
  36. package/packages/footer/src/index.ts +3 -1
  37. package/packages/fusion/README.md +131 -0
  38. package/packages/fusion/package.json +54 -0
  39. package/packages/fusion/src/index.ts +378 -0
  40. package/packages/fusion/src/picker.ts +572 -0
  41. package/packages/fusion/src/preset-editor.ts +194 -0
  42. package/packages/fusion/src/preset.ts +259 -0
  43. package/packages/fusion/src/prompts.ts +96 -0
  44. package/packages/fusion/src/savings.ts +22 -0
  45. package/packages/fusion/src/sidekick-runtime.ts +301 -0
  46. package/packages/fusion/src/slider.ts +37 -0
  47. package/packages/fusion/src/tools.ts +163 -0
  48. package/packages/image/package.json +2 -2
  49. package/packages/info-screen/package.json +2 -2
  50. package/packages/input-shortcuts/package.json +2 -2
  51. package/packages/kanboard/package.json +2 -2
  52. package/packages/mcp/package.json +2 -2
  53. package/packages/memory/package.json +3 -3
  54. package/packages/milestone/package.json +2 -2
  55. package/packages/notify/README.md +16 -0
  56. package/packages/notify/events.ts +3 -1
  57. package/packages/notify/package.json +2 -2
  58. package/packages/notify/settings.ts +1 -0
  59. package/packages/notify/summarize.ts +23 -1
  60. package/packages/notify/types.ts +9 -0
  61. package/packages/ralph/package.json +3 -3
  62. package/packages/subagents/package.json +2 -2
  63. package/packages/subagents/src/widget.ts +11 -1
  64. package/packages/trajectory/package.json +1 -1
  65. package/packages/unipi/bundled.js +4518 -12504
  66. package/packages/unipi/index.ts +2 -0
  67. package/packages/updater/package.json +2 -2
  68. package/packages/utility/package.json +2 -2
  69. package/packages/web-api/package.json +2 -2
  70. package/packages/workflow/package.json +2 -2
  71. package/packages/background-tasks/extensions/anthropic-attribution.ts +0 -1
  72. package/packages/background-tasks/extensions/fusion-child.ts +0 -1
  73. package/packages/background-tasks/src/anthropic-attribution-path.ts +0 -21
  74. package/packages/background-tasks/src/anthropic-attribution.ts +0 -1983
  75. package/packages/background-tasks/src/attested-pi-run.ts +0 -612
  76. package/packages/background-tasks/src/fusion/artifacts.ts +0 -967
  77. package/packages/background-tasks/src/fusion/budget.ts +0 -1162
  78. package/packages/background-tasks/src/fusion/child-protocol.ts +0 -305
  79. package/packages/background-tasks/src/fusion/claude-cache.ts +0 -207
  80. package/packages/background-tasks/src/fusion/clean-context.ts +0 -91
  81. package/packages/background-tasks/src/fusion/config.ts +0 -449
  82. package/packages/background-tasks/src/fusion/context.ts +0 -265
  83. package/packages/background-tasks/src/fusion/evaluation.ts +0 -800
  84. package/packages/background-tasks/src/fusion/orchestrator.ts +0 -1288
  85. package/packages/background-tasks/src/fusion/output-contract.ts +0 -34
  86. package/packages/background-tasks/src/fusion/pi-child.ts +0 -2373
  87. package/packages/background-tasks/src/fusion/prompts.ts +0 -345
  88. package/packages/background-tasks/src/fusion/result-package.ts +0 -959
  89. package/packages/background-tasks/src/fusion/source-policy.ts +0 -257
  90. package/packages/background-tasks/src/fusion/types.ts +0 -1139
  91. package/packages/background-tasks/src/fusion/web-fetch.ts +0 -1060
  92. package/packages/background-tasks/src/fusion/workflows.ts +0 -184
  93. package/packages/background-tasks/src/fusion-child-extension.ts +0 -1052
  94. package/packages/background-tasks/src/fusion-extension.ts +0 -1293
  95. package/packages/background-tasks/src/ui/fusion-model-selector.ts +0 -322
package/CHANGELOG.md CHANGED
@@ -6,7 +6,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- ## [2.16.0] — 2026-09-03
9
+ ## [2.17.0] — 2026-09-15
10
+
11
+ ### Added
12
+
13
+ - `fusion`: **Local Fusion runtime — a real lead + sidekick pair.** Picking a Fusion preset now wires up a persistent sidekick subagent, not just a status label, via two new agent tools:
14
+ - `sidekick` — hand off implementation/verification work to a persistent sidekick child process, spawned in pi's RPC mode (`--mode rpc`) with the active pair's sidekick model + effort and its own session file under `~/.unipi/state/fusion/sidekick/<session>.jsonl`. Blocking dispatch is the default (`block:true` waits and returns the report); `block:false` returns immediately and later delivers a `<subagent_completion_notification>` with the report. Calling `sidekick` again while a handoff is running injects the message as an interrupt instead of spawning a second process.
15
+ - `read_subagent` — read a handoff report by `agent_id` (or the latest); `block:true` waits (default 2700s), `block:false` returns a live progress snapshot (tool-call count, recent tools, text tail).
16
+ - The runtime (`sidekick-runtime.ts`) keeps one persistent child per session, streams progress (`◆ sidekick working · N tool calls · 12.3s`), accumulates usage (input/output/cache-read/cache-write tokens + cost), and returns a structured `HandoffReport` (`status`, `text`, `usage`, `toolCalls`, `durationMs`).
17
+ - A delegate-by-default policy (`leadPolicy()` in `prompts.ts`) is appended to the lead's system prompt on every turn while Fusion is active, the sidekick child gets a dedicated role prompt (`sidekickSystemPrompt()`), and a one-time `FIRST_EDIT_NUDGE` reminds the lead to brief the sidekick instead of editing when a change is larger than trivial.
18
+ - `fusion`: **`/unipi:fusion-stats`** command — reports sidekick token usage, estimated savings, handoff count, and runtime liveness.
19
+ - `fusion`: **savings estimation** (`estimateSavings()` in `savings.ts`) — prices sidekick usage at both the sidekick and lead model rates and reports the difference; surfaced in the stats command and the footer.
20
+ - `footer`: **the glance frame now renders the active Fusion pair** in the model slot — `Fusion · <lead> <effort> ◆ <sidekick>` with the lead lit and the sidekick muted, plus `· saved $X.XX` once savings pass half a cent; the separate thinking slot is suppressed while a pair is active.
21
+ - `core`: new `fusion-status` module (`setSharedFusionStatus`/`getSharedFusionStatus`) — a `Symbol.for` shared registry (same pattern as background-tasks) that lets `fusion` publish the active pair and `footer` render it without a hard dependency.
22
+
23
+ ### Changed
24
+
25
+ - `fusion`: **now depends on `@pi-unipi/subagents`** — the sidekick child is spawned via `getPiSpawnCommand` from `@pi-unipi/subagents/src/pi-spawn.js`, reusing the subagents package's spawn logic instead of re-implementing it.
26
+ - `fusion`: the picker shows **model badges** (from the preset), decouples effort selection, and renders a **rainbow price slider** (`slider.ts`) placing each model on a log-scaled cost gradient; confirming a Fusion pair now records it as the preset default.
27
+
28
+ ## [2.16.1] — 2026-09-11
29
+
30
+ ### Fixed
31
+
32
+ - `notify`: **recap no longer silently degrades on thinking models** (GitHub [#36](https://github.com/Neuron-Mr-White/UniPi/issues/36)) — recap summarizes with a 100-token budget, and a thinking model served by llama.cpp/vLLM could burn the entire budget on reasoning tokens and return no visible content, falling back to the raw 100-character truncation. New `recap.disableThinking` config flag (default `false`) sends `chat_template_kwargs: { enable_thinking: false, preserve_thinking: false }` with recap requests so chat-template servers skip reasoning entirely. Opt-in because strict OpenAI-compatible endpoints reject unknown body params; the Anthropic path is unaffected. 5 new tests in `src/__tests__/summarize.test.ts`.
10
33
 
11
34
  ### Added
12
35
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/unipi",
3
- "version": "2.16.0",
3
+ "version": "2.17.0",
4
4
  "description": "All-in-one extension suite for Pi coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,7 +35,9 @@
35
35
  "docs/prefix-cache-architecture.md",
36
36
  "packages/*/package.json",
37
37
  "packages/*/*.ts",
38
+ "packages/*/tests/**/*.ts",
38
39
  "packages/*/src/**/*.ts",
40
+ "packages/*/src/**/*.json",
39
41
  "packages/*/extensions/**/*.ts",
40
42
  "packages/*/bridge/**/*.py",
41
43
  "packages/*/platforms/*",
@@ -82,28 +84,29 @@
82
84
  "typebox": "^1.1.38"
83
85
  },
84
86
  "dependencies": {
85
- "@pi-unipi/ask-user": "2.16.0",
86
- "@pi-unipi/background-tasks": "2.16.0",
87
- "@pi-unipi/btw": "2.16.0",
88
- "@pi-unipi/command-enchantment": "2.16.0",
89
- "@pi-unipi/compactor": "2.16.0",
90
- "@pi-unipi/core": "2.16.0",
91
- "@pi-unipi/footer": "2.16.0",
92
- "@pi-unipi/image": "2.16.0",
93
- "@pi-unipi/info-screen": "2.16.0",
94
- "@pi-unipi/input-shortcuts": "2.16.0",
95
- "@pi-unipi/kanboard": "2.16.0",
96
- "@pi-unipi/mcp": "2.16.0",
97
- "@pi-unipi/memory": "2.16.0",
98
- "@pi-unipi/milestone": "2.16.0",
99
- "@pi-unipi/notify": "2.16.0",
100
- "@pi-unipi/ralph": "2.16.0",
101
- "@pi-unipi/subagents": "2.16.0",
102
- "@pi-unipi/trajectory": "2.16.0",
103
- "@pi-unipi/updater": "2.16.0",
104
- "@pi-unipi/utility": "2.16.0",
105
- "@pi-unipi/web-api": "2.16.0",
106
- "@pi-unipi/workflow": "2.16.0"
87
+ "@pi-unipi/ask-user": "2.17.0",
88
+ "@pi-unipi/background-tasks": "2.17.0",
89
+ "@pi-unipi/btw": "2.17.0",
90
+ "@pi-unipi/command-enchantment": "2.17.0",
91
+ "@pi-unipi/compactor": "2.17.0",
92
+ "@pi-unipi/core": "2.17.0",
93
+ "@pi-unipi/footer": "2.17.0",
94
+ "@pi-unipi/image": "2.17.0",
95
+ "@pi-unipi/fusion": "2.17.0",
96
+ "@pi-unipi/info-screen": "2.17.0",
97
+ "@pi-unipi/input-shortcuts": "2.17.0",
98
+ "@pi-unipi/kanboard": "2.17.0",
99
+ "@pi-unipi/mcp": "2.17.0",
100
+ "@pi-unipi/memory": "2.17.0",
101
+ "@pi-unipi/milestone": "2.17.0",
102
+ "@pi-unipi/notify": "2.17.0",
103
+ "@pi-unipi/ralph": "2.17.0",
104
+ "@pi-unipi/subagents": "2.17.0",
105
+ "@pi-unipi/trajectory": "2.17.0",
106
+ "@pi-unipi/updater": "2.17.0",
107
+ "@pi-unipi/utility": "2.17.0",
108
+ "@pi-unipi/web-api": "2.17.0",
109
+ "@pi-unipi/workflow": "2.17.0"
107
110
  },
108
111
  "devDependencies": {
109
112
  "@earendil-works/pi-agent-core": "^0.84.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/ask-user",
3
- "version": "2.16.0",
3
+ "version": "2.17.0",
4
4
  "description": "Structured user input tool for Pi coding agent — single-select, multi-select, freeform",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -40,7 +40,7 @@
40
40
  "access": "public"
41
41
  },
42
42
  "dependencies": {
43
- "@pi-unipi/core": "2.16.0"
43
+ "@pi-unipi/core": "2.17.0"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@earendil-works/pi-coding-agent": "^0.84.0",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/command-enchantment",
3
- "version": "2.16.0",
3
+ "version": "2.17.0",
4
4
  "description": "Enhanced TUI autocomplete for /unipi:* commands — colored, sorted, and grouped by package",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -37,6 +37,7 @@ export const PACKAGE_ORDER: string[] = [
37
37
  "image",
38
38
  "subagents",
39
39
  "background-tasks",
40
+ "fusion",
40
41
  ];
41
42
 
42
43
  // ─── Package Colors ──────────────────────────────────────────────────
@@ -62,6 +63,7 @@ export const PACKAGE_COLORS: Record<string, string> = {
62
63
  image: `${ESC}[35m`, // Magenta
63
64
  subagents: `${ESC}[34m`, // Blue
64
65
  "background-tasks": `${ESC}[91m`, // Bright Red
66
+ fusion: `${ESC}[96m`, // Bright Cyan
65
67
  };
66
68
 
67
69
  // ─── Command Registry ────────────────────────────────────────────────
@@ -143,17 +145,13 @@ export const COMMAND_REGISTRY: Record<string, string> = {
143
145
 
144
146
  // background-tasks (8 commands)
145
147
  "unipi:bg": "background-tasks",
146
- "unipi:bg-clear": "background-tasks",
147
148
  "unipi:bg-tasks": "background-tasks",
148
149
  "unipi:bg-settings": "background-tasks",
149
- "unipi:bg-update": "background-tasks",
150
- "unipi:tasks": "background-tasks",
151
- "unipi:jobs": "background-tasks",
152
- "unipi:kill": "background-tasks",
153
- "unipi:logs": "background-tasks",
154
- "unipi:claude-cache": "background-tasks",
155
- "unipi:fusion": "background-tasks",
156
- "unipi:fusion-models": "background-tasks",
150
+
151
+ // fusion (3 commands)
152
+ "unipi:model": "fusion",
153
+ "unipi:fusion-preset": "fusion",
154
+ "unipi:fusion-stats": "fusion",
157
155
 
158
156
  // info (2 commands)
159
157
  "unipi:info": "info",
@@ -316,17 +314,11 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
316
314
  "unipi:stash-settings": "Open input shortcuts settings — customize keybindings",
317
315
 
318
316
  "unipi:bg": "Start a shell command as a tracked background task",
319
- "unipi:bg-clear": "Clear finished background task footer notices",
320
317
  "unipi:bg-tasks": "Open the background task manager UI",
321
318
  "unipi:bg-settings": "Open background-tasks settings (master toggle, defaults)",
322
- "unipi:bg-update": "Show installed background-tasks version and update instructions",
323
- "unipi:tasks": "Open the background task manager UI",
324
- "unipi:jobs": "List running and recent background tasks",
325
- "unipi:kill": "Stop a running background task: /unipi:kill <id>",
326
- "unipi:logs": "Show bounded output from a background task",
327
- "unipi:claude-cache": "Show or set Claude cache retention for this session",
328
- "unipi:fusion": "Run the Fusion reason workflow with a prompt",
329
- "unipi:fusion-models": "Open the Fusion five-slot model selector",
319
+ "unipi:model": "Pick a model or Fusion lead+sidekick pair (Devin-style picker)",
320
+ "unipi:fusion-preset": "Curate the model preset used by /unipi:model",
321
+ "unipi:fusion-stats": "Estimated Fusion savings (sidekick tokens priced at lead rates)",
330
322
  "unipi:subagents-fleet": "Open the subagents fleet view",
331
323
  "unipi:subagents-doctor": "Diagnose subagents configuration",
332
324
  "unipi:subagents-guide": "Show the subagents usage guide",
@@ -355,4 +347,5 @@ export const PACKAGE_LABELS: Record<string, string> = {
355
347
  image: "image",
356
348
  subagents: "subagents",
357
349
  "background-tasks": "background-tasks",
350
+ fusion: "fusion",
358
351
  };
@@ -1,10 +1,11 @@
1
1
  # @pi-unipi/background-tasks
2
2
 
3
- Background tasks for UniPi — a full adoption of
4
- [pi-background-tasks](https://github.com/ismailsaleekh/pi-background-tasks) into the
5
- unipi conventions: durable background shell jobs, read-only delegated agents,
6
- attested Pi runs, fixed-purpose multi-model Fusion workflows, and package-owned
7
- Anthropic subscription attribution.
3
+ Background tasks for UniPi — durable background shell jobs plus one read-only
4
+ delegated background agent, with a footer dock the user can manage while the
5
+ agent keeps working. Originally adopted from
6
+ [pi-background-tasks](https://github.com/ismailsaleekh/pi-background-tasks);
7
+ the Fusion council, attested Pi runs, and Anthropic attribution surfaces were
8
+ removed in 2.17.0 so the module does exactly one thing.
8
9
 
9
10
  ## Master toggle
10
11
 
@@ -19,37 +20,37 @@ One config key disables the entire module — no tools, no commands, no hooks, n
19
20
  ```
20
21
 
21
22
  Open `/unipi:bg-settings` for the interactive settings overlay (master toggle,
22
- defaults, output caps, delegate/fusion defaults).
23
+ defaults, output caps, delegate defaults).
23
24
 
24
25
  ## Surfaces
25
26
 
26
- ### Tools (reference names kept)
27
+ ### Tools
27
28
 
28
29
  | Tool | Purpose |
29
30
  | --- | --- |
30
31
  | `bg_run` | Start a named long-running shell command; terminal notification wakes a follow-up turn by default |
31
32
  | `bg_status` / `bg_logs` / `bg_kill` | Point-in-time inspection, bounded log reads, stop — never polling primitives |
32
33
  | `bg_delegate` + `bg_result` | One read-only child Pi agent seeded with a frozen projection of this conversation; hash-verified answer retrieval |
33
- | `bg_run_pi_attested` | Evidence-oriented direct Pi spawn with local hashes and route attestation |
34
- | `fusion_reason` | 3 candidates → blind evaluator → merger on the session projection |
35
- | `fusion_investigate` / `fusion_research` / `fusion_validate` | Fixed-purpose multi-model workflows on clean-task input (inspect tools / caller-URL research / advisory review) |
36
34
 
37
- ### Commands (our namespace)
35
+ ### Commands
38
36
 
39
- `/unipi:bg`, `/unipi:bg-clear`, `/unipi:bg-tasks`, `/unipi:bg-update`,
40
- `/unipi:tasks`, `/unipi:jobs`, `/unipi:kill`, `/unipi:logs`,
41
- `/unipi:bg-settings`, `/unipi:fusion`, `/unipi:fusion-models`,
42
- `/unipi:claude-cache`.
37
+ `/unipi:bg` (start a shell task), `/unipi:bg-tasks` (open the dock),
38
+ `/unipi:bg-settings`.
43
39
 
44
40
  Shortcuts: `Shift↓` opens the task manager dock; `Ctrl+Alt+C` clears finished notices.
45
41
 
42
+ ## What the user sees
43
+
44
+ - **Launch card** — `bg_run` results render as a tinted card (`● bg started <name> · wakes agent on completion`) so the start of a task is visible in the transcript.
45
+ - **Pending-wake line** — while the agent is idle but a task that will wake it is still running, a spinner line sits above the editor: `⠋ waiting on 1 bg task · <name> 12s — agent resumes automatically when done`. Without this the UI looks finished and users assume the turn is over.
46
+ - **Completion card** — the terminal notification renders as a tinted card (`✓ bg done <name> · exit 0 · 25s · agent woken`) with the last three output lines. Failed / stopped tasks use the error tint.
47
+ - **Dock** (`Shift↓`) — rows show `⏰ wakes agent` for tasks that will resume the agent, and the last output line for running shell tasks.
48
+
46
49
  ## Storage layout (ours — never `.pi/`)
47
50
 
48
51
  - Runtime artifacts (task output/metadata): `$TMPDIR/unipi-bg-tasks/<session>-<pid>-<nonce>/`
49
52
  - Durable delegate artifacts: `<workspace>/.unipi/delegate/<session>-<pid>/<task-id>/`
50
- - Durable fusion artifacts: `<workspace>/.unipi/fusion/<session>-<pid>/<run-id>/`
51
53
  - Config: `~/.unipi/config/background-tasks.json` + workspace override
52
- - Fusion model config: five-slot selector persisted through `/unipi:fusion-models`
53
54
 
54
55
  ## Environment
55
56
 
@@ -67,13 +68,6 @@ Shortcuts: `Shift↓` opens the task manager dock; `Ctrl+Alt+C` clears finished
67
68
 
68
69
  Treat `<background-task-notification>` as durable terminal truth — do not poll.
69
70
 
70
- ## Attribution
71
-
72
- `extensions/anthropic-attribution.ts` loads first (provider-gated): Claude Code
73
- subscription OAuth attribution, exact-match system-prompt sanitization, and
74
- cache-retention policy for Anthropic routes. Duplicate installed copies resolve
75
- ownership through an EventBus claim; later copies go inert.
76
-
77
71
  ## Shared registry (for sibling extensions)
78
72
 
79
73
  Other packages can read the live task registry synchronously — no events, no polling:
@@ -94,10 +88,10 @@ treat that as "no data", e.g. the footer's glance process line does.
94
88
 
95
89
  - Commands live in the `/unipi:*` namespace; env prefix is `UNIPI_BG_*`.
96
90
  - Runtime artifacts under the OS temp root (per-registry nonce) and durable
97
- delegate/fusion artifacts under workspace `.unipi/` — never `.pi/`.
91
+ delegate artifacts under workspace `.unipi/` — never `.pi/`.
98
92
  - The reference's `update-check` footer surface is dropped (unipi's updater
99
93
  module owns updates).
100
- - `agent_settled` / `before_provider_headers` hooks register defensively on our
101
- pi SDK with an `agent_end` settlement fallback.
94
+ - Removed: attested Pi runs, the five-slot Fusion council tools, and the
95
+ Anthropic attribution provider override.
102
96
 
103
97
  ISC-licensed reference: Copyright Ismail <ismailsalikhodjaev@gmail.com>.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pi-unipi/background-tasks",
3
- "version": "2.16.0",
4
- "description": "Background tasks for UniPi — durable shell jobs, delegated agents, attested Pi runs, and fixed-purpose Fusion workflows",
3
+ "version": "2.17.0",
4
+ "description": "Background tasks for UniPi — durable shell jobs and delegated background agents with a footer dock",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "types": "src/index.ts",
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@earendil-works/pi-ai": "^0.84.0",
13
- "@pi-unipi/core": "2.16.0",
13
+ "@pi-unipi/core": "2.17.0",
14
14
  "turndown": "^7.2.4"
15
15
  },
16
16
  "devDependencies": {
@@ -31,7 +31,6 @@
31
31
  ],
32
32
  "pi": {
33
33
  "extensions": [
34
- "./extensions/anthropic-attribution.ts",
35
34
  "./src/index.ts"
36
35
  ],
37
36
  "skills": [],
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @pi-unipi/background-tasks — tinted chat cards
3
+ *
4
+ * Background-task launch/completion messages were plain single lines that got
5
+ * lost in the transcript. These helpers render them as padded, background-
6
+ * tinted boxes (same visual language as pi's tool cards) so a user scanning
7
+ * the chat can spot "a bg task started here" / "it finished here" at a glance.
8
+ */
9
+ import { Box, Text } from "@earendil-works/pi-tui";
10
+ import { formatDuration, taskDisplayName, type BgTaskSnapshot } from "./types.js";
11
+
12
+ type ThemeLike = {
13
+ fg: (color: string, text: string) => string;
14
+ bg: (color: string, text: string) => string;
15
+ bold: (text: string) => string;
16
+ };
17
+
18
+ type CardTone = "pending" | "success" | "error";
19
+
20
+ const BG_FOR_TONE: Record<CardTone, string> = {
21
+ pending: "toolPendingBg",
22
+ success: "toolSuccessBg",
23
+ error: "toolErrorBg",
24
+ };
25
+
26
+ function toneFor(status: BgTaskSnapshot["status"]): CardTone {
27
+ if (status === "running") return "pending";
28
+ if (status === "completed") return "success";
29
+ return "error";
30
+ }
31
+
32
+ function statusGlyph(theme: ThemeLike, status: BgTaskSnapshot["status"]): string {
33
+ if (status === "running") return theme.fg("accent", "●");
34
+ if (status === "completed") return theme.fg("success", "✓");
35
+ if (status === "killed") return theme.fg("warning", "■");
36
+ return theme.fg("error", "✗");
37
+ }
38
+
39
+ function card(theme: ThemeLike, tone: CardTone, lines: string[]): Box {
40
+ const box = new Box(1, 0, (text) => theme.bg(BG_FOR_TONE[tone], text));
41
+ box.addChild(new Text(lines.join("\n"), 0, 0));
42
+ return box;
43
+ }
44
+
45
+ /** `bg_run` result card: "● bg started <name> (id) · will wake agent" */
46
+ export function renderLaunchCard(theme: ThemeLike, task: BgTaskSnapshot): Box {
47
+ const wake = task.triggerOnCompletion
48
+ ? theme.fg("dim", " · wakes agent on completion")
49
+ : task.notifyOnCompletion
50
+ ? theme.fg("dim", " · notifies on completion")
51
+ : theme.fg("dim", " · silent");
52
+ const head = `${statusGlyph(theme, "running")} ${theme.fg("accent", theme.bold("bg started"))} ${theme.fg("accent", taskDisplayName(task))} ${theme.fg("dim", `(${task.id})`)}${wake}`;
53
+ const cmd = theme.fg("dim", `$ ${task.command.length > 120 ? `${task.command.slice(0, 120)}…` : task.command}`);
54
+ return card(theme, "pending", [head, cmd]);
55
+ }
56
+
57
+ /** Completion notification card: "✓ bg done <name> · exit 0 · 25s" + last output lines */
58
+ export function renderCompletionCard(theme: ThemeLike, task: BgTaskSnapshot | undefined): Box {
59
+ if (task === undefined) {
60
+ return card(theme, "success", [`${theme.fg("success", "✓")} ${theme.fg("accent", theme.bold("bg done"))}`]);
61
+ }
62
+ const tone = toneFor(task.status);
63
+ const label =
64
+ task.status === "completed" ? "bg done" : task.status === "killed" ? "bg stopped" : "bg failed";
65
+ const meta: string[] = [];
66
+ if (task.exitCode !== undefined && task.exitCode !== null) meta.push(`exit ${String(task.exitCode)}`);
67
+ if (task.endTime !== undefined) meta.push(formatDuration(task.endTime - task.startTime));
68
+ if (task.triggerOnCompletion) meta.push("agent woken");
69
+ const head = `${statusGlyph(theme, task.status)} ${theme.fg(tone === "error" ? "error" : "success", theme.bold(label))} ${theme.fg("accent", taskDisplayName(task))} ${theme.fg("dim", `(${task.id})`)}${meta.length > 0 ? theme.fg("dim", ` · ${meta.join(" · ")}`) : ""}`;
70
+ const lines = [head];
71
+ if (task.error) lines.push(theme.fg("error", task.error));
72
+ const tail = task.outputTail ?? [];
73
+ for (const line of tail) lines.push(theme.fg("toolOutput", ` ${line}`));
74
+ lines.push(theme.fg("dim", ` ${task.outputPath}`));
75
+ return card(theme, tone, lines);
76
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @pi-unipi/background-tasks — Shared child-process & context interfaces
3
3
  *
4
- * Consumed by types.ts, registry.ts, attested-pi-run.ts, and tests.
4
+ * Consumed by types.ts, registry.ts, and tests.
5
5
  */
6
6
 
7
7
  import type { SpawnOptions } from "node:child_process";
@@ -34,13 +34,6 @@ export interface BackgroundTasksConfig {
34
34
  maxToolCalls: number;
35
35
  timeoutSeconds: number;
36
36
  };
37
- /** Fusion defaults. */
38
- fusion: {
39
- /** Five-slot model selections ('' = $current). */
40
- candidates: [string, string, string];
41
- evaluator: string;
42
- merger: string;
43
- };
44
37
  }
45
38
 
46
39
  export const DEFAULT_CONFIG: BackgroundTasksConfig = {
@@ -57,11 +50,6 @@ export const DEFAULT_CONFIG: BackgroundTasksConfig = {
57
50
  maxToolCalls: 120,
58
51
  timeoutSeconds: 900,
59
52
  },
60
- fusion: {
61
- candidates: ["", "", ""],
62
- evaluator: "",
63
- merger: "",
64
- },
65
53
  };
66
54
 
67
55
  function getGlobalConfigPath(): string {
@@ -181,28 +169,6 @@ export function validateBackgroundTasksConfig(config: unknown): string[] {
181
169
  }
182
170
  }
183
171
  }
184
- if (c.fusion !== undefined) {
185
- if (typeof c.fusion !== "object" || c.fusion === null) {
186
- problems.push("fusion must be an object");
187
- } else {
188
- const f = c.fusion as Record<string, unknown>;
189
- for (const key of ["candidates", "evaluator", "merger"] as const) {
190
- const value = f[key];
191
- if (value === undefined) continue;
192
- if (key === "candidates") {
193
- if (
194
- !Array.isArray(value) ||
195
- value.length !== 3 ||
196
- value.some((v) => typeof v !== "string")
197
- ) {
198
- problems.push("fusion.candidates must be an array of exactly 3 strings");
199
- }
200
- } else if (typeof value !== "string") {
201
- problems.push(`fusion.${key} must be a string`);
202
- }
203
- }
204
- }
205
- }
206
172
  return problems;
207
173
  }
208
174
 
@@ -272,14 +238,6 @@ export function loadBackgroundTasksConfig(cwd: string): LoadedBackgroundTasksCon
272
238
  ? merged.delegate.autoDeliver
273
239
  : "when_small",
274
240
  },
275
- fusion: {
276
- candidates:
277
- Array.isArray(merged.fusion?.candidates) && merged.fusion.candidates.length === 3
278
- ? ([...merged.fusion.candidates] as [string, string, string])
279
- : DEFAULT_CONFIG.fusion.candidates,
280
- evaluator: typeof merged.fusion?.evaluator === "string" ? merged.fusion.evaluator : "",
281
- merger: typeof merged.fusion?.merger === "string" ? merged.fusion.merger : "",
282
- },
283
241
  };
284
242
  return { config, warnings };
285
243
  }
@@ -1,6 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import type { Message } from '@earendil-works/pi-ai';
3
- import { canonicalJson } from './attested-pi-run.js';
3
+ import { canonicalJson } from './json-utils.js';
4
4
 
5
5
  /**
6
6
  * Frozen `visible-conversation-ledger-v2` transform.
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { existsSync } from 'node:fs';
3
3
  import { chmod, mkdir, open, readFile, rename, rm } from 'node:fs/promises';
4
4
  import { basename, isAbsolute, join, relative, sep } from 'node:path';
5
- import { canonicalJson } from '../attested-pi-run.js';
5
+ import { canonicalJson } from '../json-utils.js';
6
6
  import { sanitizePathSegment } from '../types.js';
7
7
  import { replaceFileDurable, writeFileDurable } from '../durable-fs.js';
8
8
  import {
@@ -0,0 +1,18 @@
1
+ {
2
+ "schema_version": "pi-background-tasks.delegate-hook-contract.v1",
3
+ "contract_id": "context-measure-abort-v1+tool-result-spill-v1",
4
+ "guarantees": {
5
+ "context_fires_before_every_model_call": true,
6
+ "context_result_messages_reach_provider": true,
7
+ "context_abort_blocks_provider_call": true,
8
+ "context_abort_skips_stream_invocation": false,
9
+ "context_abort_terminates_run": true,
10
+ "context_throw_blocks_provider_call": false,
11
+ "context_throw_isolated_to_throwing_handler": true,
12
+ "tool_result_fires_before_transcript_entry": true,
13
+ "tool_result_replacement_reaches_provider": true,
14
+ "tool_result_replacement_preserves_identity": true,
15
+ "tool_result_chains_in_load_order": true,
16
+ "handlers_run_in_extension_load_order": true
17
+ }
18
+ }
@@ -1,5 +1,6 @@
1
1
  import { randomBytes } from 'node:crypto';
2
2
  import { existsSync } from 'node:fs';
3
+ import { packageAssetSearchHint, resolvePackageAsset } from '../package-assets.js';
3
4
  import { mkdir } from 'node:fs/promises';
4
5
  import { dirname, join, resolve } from 'node:path';
5
6
  import { fileURLToPath } from 'node:url';
@@ -184,15 +185,20 @@ export function resolveDelegateChildExtensionPath(
184
185
  moduleUrl = import.meta.url,
185
186
  pathExists: (path: string) => boolean = existsSync,
186
187
  ): string {
187
- const modulePath = fileURLToPath(moduleUrl);
188
- const extension = modulePath.endsWith('.ts') ? 'delegate-child.ts' : 'delegate-child.js';
189
- const candidate = resolve(dirname(modulePath), '../../extensions', extension);
190
- if (!pathExists(candidate)) {
191
- throw new DelegateError(`delegate child extension is missing: ${candidate}`, {
192
- code: 'delegate_isolation_unsupported',
193
- childCreated: false,
194
- remediation: ['Reinstall the package; the child guard extension is required to spawn.'],
195
- });
188
+ // Prefer the TypeScript guard (pi loads extensions through jiti); fall back
189
+ // to a compiled copy when one exists.
190
+ const candidate =
191
+ resolvePackageAsset('extensions/delegate-child.ts', { moduleUrl, pathExists }) ??
192
+ resolvePackageAsset('extensions/delegate-child.js', { moduleUrl, pathExists });
193
+ if (candidate === undefined) {
194
+ throw new DelegateError(
195
+ `delegate child extension is missing: ${packageAssetSearchHint('extensions/delegate-child.ts')}`,
196
+ {
197
+ code: 'delegate_isolation_unsupported',
198
+ childCreated: false,
199
+ remediation: ['Reinstall the package; the child guard extension is required to spawn.'],
200
+ },
201
+ );
196
202
  }
197
203
  return candidate;
198
204
  }
@@ -201,7 +207,7 @@ export function resolveDelegateChildExtensionPath(
201
207
  * Tools the inspect capability permits.
202
208
  *
203
209
  * v1 is read-only by construction: no shell, no network, no edit/write, no
204
- * recursive delegation, no Fusion. The list is passed to `--tools`, so it is
210
+ * recursive delegation. The list is passed to `--tools`, so it is
205
211
  * enforced by the child's tool registry rather than by prompt text.
206
212
  */
207
213
  export const DELEGATE_INSPECT_TOOLS: readonly string[] = [
@@ -223,12 +229,6 @@ export const DELEGATE_FORBIDDEN_TOOLS: readonly string[] = [
223
229
  'bg_kill',
224
230
  'bg_status',
225
231
  'bg_logs',
226
- 'bg_run_pi_attested',
227
- 'fusion_brainstorm',
228
- 'fusion_reason',
229
- 'fusion_investigate',
230
- 'fusion_research',
231
- 'fusion_validate',
232
232
  ];
233
233
 
234
234
  export function delegateToolsFor(capability: DelegateCapability): readonly string[] {
@@ -256,7 +256,6 @@ export interface DelegateChildArgvInput {
256
256
  childSessionId: string;
257
257
  childSessionDir: string;
258
258
  childExtensionPath: string;
259
- attributionExtensionPath?: string | undefined;
260
259
  systemPrompt: string;
261
260
  }
262
261
 
@@ -282,19 +281,7 @@ export function buildDelegateChildArgv(input: DelegateChildArgvInput): string[]
282
281
  );
283
282
  }
284
283
  }
285
- const extensionPaths =
286
- input.route.provider === 'anthropic'
287
- ? [
288
- input.attributionExtensionPath ??
289
- (() => {
290
- throw new DelegateError(
291
- 'Anthropic delegate launch requires the package attribution extension',
292
- { code: 'delegate_isolation_unsupported', childCreated: false },
293
- );
294
- })(),
295
- input.childExtensionPath,
296
- ]
297
- : [input.childExtensionPath];
284
+ const extensionPaths = [input.childExtensionPath];
298
285
 
299
286
  return [
300
287
  '--mode',
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { canonicalJson } from '../attested-pi-run.js';
2
+ import { canonicalJson } from '../json-utils.js';
3
3
  import {
4
4
  DELEGATE_RECEIPT_SCHEMA_VERSION,
5
5
  DELEGATE_RESULT_PACKAGE_SCHEMA_VERSION,