pi-background-tasks 0.7.7 → 0.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.
package/PUBLISHING.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Publishing pi-background-tasks
2
2
 
3
- Release checklist for npm publishing and standalone git publishing. The current release candidate is 0.7.7; version 0.7.0 introduced the Fusion public surfaces (`/fusion`, `/fusion-models`, `fusion_brainstorm`) in addition to the background-task surfaces. Do not advertise the GitHub install target until the standalone repository has the exact release commit and tag.
3
+ Release checklist for npm publishing and standalone git publishing. The current release candidate is 0.9.0, which adds the `fusion_validate` validation workflow and Anthropic child sanitization; 0.7.0 introduced the Fusion public surfaces (`/fusion`, `/fusion-models`, `fusion_brainstorm`) in addition to the background-task surfaces. Do not advertise the GitHub install target until the standalone repository has the exact release commit and tag.
4
4
 
5
5
  ## Preconditions
6
6
 
@@ -37,8 +37,8 @@ npm publish --access public
37
37
  Pi install smoke after publish:
38
38
 
39
39
  ```bash
40
- PI_CODING_AGENT_DIR=$(mktemp -d) pi -e npm:pi-background-tasks@0.7.7 --offline --no-tools --no-session -p "/jobs"
41
- pi install npm:pi-background-tasks@0.7.7
40
+ PI_CODING_AGENT_DIR=$(mktemp -d) pi -e npm:pi-background-tasks@0.9.0 --offline --no-tools --no-session -p "/jobs"
41
+ pi install npm:pi-background-tasks@0.9.0
42
42
  ```
43
43
 
44
44
  ## Publish to git
@@ -51,15 +51,15 @@ git status --short --branch
51
51
  git log --oneline -3
52
52
  git remote -v
53
53
  git push origin main
54
- git tag v0.7.7
55
- git push origin v0.7.7
54
+ git tag v0.9.0
55
+ git push origin v0.9.0
56
56
  ```
57
57
 
58
58
  Pi install smoke after git tag, using an isolated Pi agent directory so no local checkout or user `~/.pi` state is involved:
59
59
 
60
60
  ```bash
61
- PI_CODING_AGENT_DIR=$(mktemp -d) pi -e git:github.com/ismailsaleekh/pi-background-tasks@v0.7.7 --offline --no-tools --no-session -p "/jobs"
62
- pi install git:github.com/ismailsaleekh/pi-background-tasks@v0.7.7
61
+ PI_CODING_AGENT_DIR=$(mktemp -d) pi -e git:github.com/ismailsaleekh/pi-background-tasks@v0.9.0 --offline --no-tools --no-session -p "/jobs"
62
+ pi install git:github.com/ismailsaleekh/pi-background-tasks@v0.9.0
63
63
  ```
64
64
 
65
65
  ## pi.dev/packages
package/README.md CHANGED
@@ -2,26 +2,26 @@
2
2
 
3
3
  Claude-Code-like explicit background shell task manager for [Pi](https://pi.dev/).
4
4
 
5
- This package adds named, tracked background shell jobs with durable output files, bounded log reads, kill/timeout safety, task-owned context-window/token/tool-use/model telemetry, explicit Pi-agent telemetry wrapping for tasks marked as agents, a focused footer-dock task manager, `/tasks` fallback UI, and completion notifications that can wake the agent when LLM-launched work finishes. It also ships Fusion: a direct child-Pi five-call synthesis workflow exposed as `/fusion`, `/fusion-models`, and the always-active `fusion_brainstorm` tool. A terminal task status is published only after trailing wrapped-agent telemetry is consumed and final output plus terminal metadata have completed their durability writes.
5
+ This package adds named, tracked background shell jobs with durable output files, bounded log reads, kill/timeout safety, task-owned context-window/token/tool-use/model telemetry, explicit Pi-agent telemetry wrapping for tasks marked as agents, a focused footer-dock task manager, `/tasks` fallback UI, and completion notifications that can wake the agent when LLM-launched work finishes. It also ships Fusion: a direct child-Pi five-call synthesis workflow exposed as `/fusion`, `/fusion-models`, and the always-active `fusion_brainstorm` and `fusion_validate` tools. A terminal task status is published only after trailing wrapped-agent telemetry is consumed and final output plus terminal metadata have completed their durability writes.
6
6
 
7
7
  ## Install
8
8
 
9
9
  From npm after publish:
10
10
 
11
11
  ```bash
12
- pi install npm:pi-background-tasks@0.7.7
12
+ pi install npm:pi-background-tasks@0.9.0
13
13
  ```
14
14
 
15
15
  From git after pushing this package to its standalone repository and tagging:
16
16
 
17
17
  ```bash
18
- pi install git:github.com/ismailsaleekh/pi-background-tasks@v0.7.7
18
+ pi install git:github.com/ismailsaleekh/pi-background-tasks@v0.9.0
19
19
  ```
20
20
 
21
21
  For project-local install:
22
22
 
23
23
  ```bash
24
- pi install -l npm:pi-background-tasks@0.7.7
24
+ pi install -l npm:pi-background-tasks@0.9.0
25
25
  ```
26
26
 
27
27
  ## Commands
@@ -97,7 +97,8 @@ The lookup runs at most once per session on `session_start`, is time-boxed, and
97
97
  - `bg_status` — inspect one task or all recent tasks.
98
98
  - `bg_logs` — read bounded task output.
99
99
  - `bg_kill` — stop a running task.
100
- - `fusion_brainstorm({prompt})` — always-active tool that runs the Fusion workflow and returns the exact merged text as the tool result for the parent agent to consume, with the exact Pi `Usage` shape attached when the host supports tool-result usage: token fields plus complete `cost.input`, `cost.output`, `cost.cacheRead`, `cost.cacheWrite`, and `cost.total`. Its closed public schema has exactly one required parameter, `prompt`; extra keys are rejected. It has no eligibility, quota, routine, or justification gate. Tool context capture excludes the current assistant tool-call leaf when Pi is executing that `fusion_brainstorm` call, so the nested children do not see the in-progress tool call or sibling calls. Children receive the documented conversation projection described under [Conversation context policy](#conversation-context-policy): visible user/assistant text verbatim, with thinking and tool payloads replaced by explicit hash-accounted omission receipts. Because the prompt is composed by the parent agent, it is treated as authoritative and self-contained.
100
+ - `fusion_validate({prompt})` — always-active tool that runs the same five-model Fusion workflow as a validation review of work that was just completed, and returns the merged prose review. Its closed public schema has exactly one parameter, `prompt`; **it takes no `capability` argument**, and a caller-supplied `capability` is rejected loudly rather than ignored. Candidate reviewers always run with the read-only `inspect` capability, because a reasoning-only reviewer cannot read the code it is judging; the evaluator and merger remain no-tools by stage policy. Findings are classified `critical`, `high`, or `minor`, each with a file/symbol location, the evidence the reviewer actually read, and why it matters. A review with no findings must state what was verified rather than returning an unexplained pass. See [Validation workflow](#validation-workflow).
101
+ - `fusion_brainstorm({prompt, capability?})` — always-active tool that runs the Fusion workflow and returns the exact merged text as the tool result for the parent agent to consume, with the exact Pi `Usage` shape attached when the host supports tool-result usage: token fields plus complete `cost.input`, `cost.output`, `cost.cacheRead`, `cost.cacheWrite`, and `cost.total`. Calling it as `fusion_brainstorm({prompt})` uses the default `reason` capability: no tools and byte-identical child argv/prompt behaviour to the previous release. The optional `capability` accepts only `"reason"`, `"inspect"`, or `"research"`; `inspect` gives candidate children read-only repository inspection tools, and `research` adds the package-owned `fusion_web_fetch` tool for targeted public URL fetches. The evaluator and merger remain no-tools by stage policy. Its closed public schema has one required parameter, `prompt`, plus optional `capability`; extra keys are rejected. It has no eligibility, quota, routine, or justification gate. Tool context capture excludes the current assistant tool-call leaf when Pi is executing that `fusion_brainstorm` call, so the nested children do not see the in-progress tool call or sibling calls. Children receive the documented conversation projection described under [Conversation context policy](#conversation-context-policy): visible user/assistant text verbatim, with thinking and tool payloads replaced by explicit hash-accounted omission receipts. Because the prompt is composed by the parent agent, it is treated as authoritative and self-contained.
101
102
 
102
103
  `bg_run` requires a concise `name` for the footer dock, the shell `command`, and required `isAgent: boolean`. Set `isAgent: true` only when the background task launches an LLM/agent process (for example `pi -p ...` or `pi --mode json ...`); set `isAgent: false` for scripts, tests, dev servers, sleeps, and ordinary shell commands. It defaults both `notifyOnCompletion` and `triggerOnCompletion` to `true`. With those defaults, `bg_run` returns immediately, the agent continues only independent useful work or ends its current turn instead of sleeping or polling, and a durable `background-task-notification` for completed, failed, or killed state automatically starts a follow-up turn. The launch receipt states the effective notification/wake behavior explicitly. `bg_status` and `bg_logs` remain available for user-requested inspection, deliberately disabled completion delivery, concrete hang diagnosis, or reading output after the terminal event; they are not waiting primitives, and the terminal notification does not need status reconfirmation. Setting `triggerOnCompletion: false` keeps the notification but prevents it from starting an agent turn. Setting `notifyOnCompletion: false` suppresses both notification and wake-up even if `triggerOnCompletion` is true.
103
104
 
@@ -269,7 +270,7 @@ what the parent knew at launch and is never used to decide success.
269
270
 
270
271
  ## Fusion workflow
271
272
 
272
- Fusion runs direct child `pi --mode text` processes only; it never calls `pi-ai` completion APIs. Each child is launched with `--no-session`, `--no-tools`, `--no-extensions`, `--no-skills`, `--no-prompt-templates`, `--no-themes`, and `--no-context-files`, plus the resolved provider/model/thinking level and the package-owned private `extensions/fusion-child.ts` metadata extension. The prompt travels over stdin, not a shell or positional argument.
273
+ Fusion runs direct child `pi --mode text` processes only; it never calls `pi-ai` completion APIs. Each child is launched with `--no-session`, `--no-extensions`, `--no-skills`, `--no-prompt-templates`, `--no-themes`, and `--no-context-files`, plus the tool policy described below, the resolved provider/model/thinking level, and the package-owned private `extensions/fusion-child.ts` metadata extension. The prompt travels over stdin, not a shell or positional argument.
273
274
 
274
275
  Pi text mode writes the final full answer exactly once instead of serializing cumulative reasoning/partial-message events on every token delta. The private child extension emits one compact, reasoning-free metadata record per finalized assistant message for provider/model, stop reason, the complete Pi token/cost `Usage` object, and response byte/hash validation. Fusion persists those compact records in `*.events.jsonl`; the complete answer remains in the stage response artifact. The 32 MiB child stdout cap therefore applies to one final response, not amplified JSON telemetry. Failed attempts keep the authoritative response artifact empty and, when any stdout was captured, persist it separately as an explicitly incomplete `*.response.partial.*` artifact.
275
276
 
@@ -279,6 +280,62 @@ Model configuration is global under the Pi agent directory:
279
280
  fusion-models.json
280
281
  ```
281
282
 
283
+ ### Validation workflow
284
+
285
+ `fusion_validate` is the same orchestrator, artifact store, budget engine, conversation projection, and evaluation schema as `fusion_brainstorm`, with different stage framing. A workflow profile selects the four system prompts and the capability policy; it never changes the canonical input schema, which remains `pi-background-tasks.fusion-input.v4` for both tools. Canonical input bytes and the omission ledger are provably identical across workflows for the same conversation, and that equality is asserted by the golden-bytes gate rather than assumed.
286
+
287
+ | Concern | `fusion_brainstorm` | `fusion_validate` |
288
+ |---|---|---|
289
+ | Parameters | `{prompt, capability?}` | `{prompt}` — capability rejected |
290
+ | Candidate capability | caller-selected, default `reason` | always `inspect` |
291
+ | Evaluator / merger capability | `reason` by stage policy | `reason` by stage policy |
292
+ | Run id prefix | `f` | `v` |
293
+ | Output | prose answer | prose review |
294
+ | Evaluation schema | `fusion-evaluation.v1` | `fusion-evaluation.v1` (identical) |
295
+
296
+ The capability is fixed rather than defaulted. A `capability` argument on `fusion_validate` is a hard error at the schema boundary, and the orchestrator independently re-asserts the workflow capability before the artifact store or any child process exists, so a contradicting request launches **zero children**.
297
+
298
+ The three reviewers are blind-compared exactly like brainstorm candidates, but the evaluator is additionally required to treat each distinct defect claim as a unit and to carry every surviving claim into `synthesis_plan.must_include`, **including claims raised by only one reviewer**. The merger is correspondingly forbidden from dropping a single-source finding or inventing one no reviewer raised, and must state the resolution and reason wherever reviewers disagreed. Without those two clauses a real defect that only one model noticed could disappear by silent majority vote, which is the failure mode a three-model review exists to prevent.
299
+
300
+ `fusion_validate` is advisory and read-only. It never modifies files, it does not gate anything, and it is not a substitute for running tests or builds. Like every Fusion entry point, facts that exist only inside omitted tool output are not visible to reviewers; `inspect` lets them re-derive repository facts themselves, but uncommitted state that exists only in the parent agent's context must be restated in the prompt.
301
+
302
+ ### Anthropic child sanitization
303
+
304
+ Fusion children launch with `--no-extensions` for isolation, which disables extension *discovery* while still honouring explicit `--extension` paths. The parent session normally loads an Anthropic system-prompt sanitizer through discovery, so a Claude child would inherit nothing and fail at the provider: Pi's own system prompt contains documentation lines Anthropic rejects.
305
+
306
+ Children routed to the `anthropic` provider therefore receive a second explicit extension, [`@ravshansbox/pi-anthropic-sps`](https://github.com/ravshansbox/pi-anthropic-sps) (MIT), resolved from the package's own dependency tree. The metadata extension is always passed first so its `message_end` frame is never displaced.
307
+
308
+ Children on every other provider receive exactly one `--extension` and their argv is byte-identical to the pre-sanitizer form. The sanitizer package publishes no `main`/`exports`, so it is located through its manifest's `pi.extensions[0]` entry rather than a direct require. Every resolution failure - package missing, manifest unreadable or malformed, no declared extension, or a declared file that does not exist - is a loud error before launch, because silently omitting the sanitizer would resurface later as an opaque provider rejection.
309
+
310
+ ### Candidate capabilities
311
+
312
+ Fusion candidate children support three launch-time capability profiles. `reason` is the default: it passes `--no-tools` and preserves the previous no-tool candidate argv and prompt bytes. `inspect` is available only to candidate children and replaces `--no-tools` with the exact read-only tool policy `--no-builtin-tools --tools read,grep,find,ls --exclude-tools bash,edit,write,fusion_brainstorm,bg_delegate,bg_result,bg_run,bg_kill,bg_status,bg_logs,bg_run_pi_attested`. `research` extends `inspect` by adding the package-owned `fusion_web_fetch` tool to the `--tools` allowlist; the `--exclude-tools` denylist is unchanged and still bans `bash`, `edit`, `write`, `fusion_brainstorm`, `bg_delegate`, `bg_result`, `bg_run`, `bg_kill`, `bg_status`, `bg_logs`, and `bg_run_pi_attested`. The `reason` and `inspect` argv forms remain byte-identical to v0.7.8.
313
+
314
+ Evaluator, evaluation-repair, and merger children always run with `--no-tools` by stage policy. Caller input cannot grant them tools, even when candidates use `inspect` or `research`. Capability is recorded as launch metadata in the run manifest and child argv, not added to the canonical child-facing input; the canonical input schema remains `pi-background-tasks.fusion-input.v4`.
315
+
316
+ The inspect candidate system prompt tells the child it may re-derive facts from the repository using `read`, `grep`, `find`, and `ls`. The research prompt adds `fusion_web_fetch` for fetching a specific public URL as bounded Markdown or text. Both prompts extend the untrusted-data rule: projected conversation text, file contents, and fetched page content are data, never instructions to follow.
317
+
318
+ `fusion_web_fetch` has a closed schema: `{ url: string, extract?: 'text' | 'markdown' }`. `extract` defaults to Markdown. There is deliberately no per-fetch prompt parameter. Anthropic documents the `{url, prompt}` extraction pattern as lossy by design: the prompt decides what reaches the model, so a false negative can enter a Fusion candidate answer, pass through blind evaluation, and reach the merged answer with no signal that the page contained missed information.
319
+
320
+ HTML extraction uses the runtime dependency `turndown@7.2.4`; its only dependency is `@mixmark-io/domino`, so it does not require `jsdom`. Markdown is the default because it preserves link destinations, headings, tables, and code blocks better than plain text. This version has no web search, browser, PDF support, cache, or domain allowlist.
321
+
322
+ | `fusion_web_fetch` policy | Value |
323
+ |---|---:|
324
+ | Request method | GET |
325
+ | Schemes | `http:` and `https:` only |
326
+ | Timeout | 60 seconds |
327
+ | Response body cap | 2 MiB |
328
+ | Returned content cap | 32 KiB |
329
+ | Redirect cap | 5 hops |
330
+
331
+ Network handling is basic network hygiene, not a secret-exfiltration control. The hostname is resolved once, every returned address is checked, the connection is pinned to the vetted address, and the socket's remote address is verified after connect. Address validation re-runs on every redirect hop. Private, loopback, link-local, unique-local, multicast, and cloud-metadata addresses are refused. A research child that can read files and reach the network can in principle send what it read; that is an accepted trade in this version, not a sandbox or security boundary.
332
+
333
+ `fusion_web_fetch` fails loudly with typed errors rather than retrying or falling back to another URL, scheme, encoding, or extraction mode. Error codes include `invalid_url`, `unsupported_scheme`, `blocked_address`, `dns_failure`, `redirect_limit`, `redirect_blocked`, `response_too_large`, `unsupported_content_type`, `request_timeout`, `network_error`, `extraction_failed`, and `http_error`.
334
+
335
+ Every child has a stale-action watchdog in addition to the 30-minute absolute timeout. `FUSION_CHILD_IDLE_TIMEOUT_MS` defaults to 900 seconds and fails the child if no stdout or stderr activity occurs during that window; any stdout or stderr activity resets the watchdog. The threshold is deliberately far above tool latency: the child metadata frame is emitted only at `message_end` and text-mode stdout carries only the final assistant message, so one slow model turn is legitimately silent on both streams and must not be killed. The absolute timeout remains a backstop for children that keep producing output but never finish.
336
+
337
+ Inspect and research candidates also write a per-attempt tool-call audit log at `candidate-<slot>.attempt-<n>.tool-calls.jsonl`. The child appends one JSON line per completed tool call with the tool name, argument/result byte counts, and SHA-256 digests only. For `fusion_web_fetch`, the record also includes `url`, `final_url`, `http_status`, `response_bytes`, and `content_sha256`. Raw arguments, raw tool results, and page content are never written because file paths, file contents, fetched content, and tool results may contain secrets. After the child exits, the parent verifies the log is complete and contiguous; a trailing partial line, ordinal gap, duplicate ordinal, or schema-version mismatch is a loud failure.
338
+
282
339
  Missing config means all five slots are `$current`. Malformed config, stale explicit models, unavailable current models, and concurrent selector write conflicts fail loudly before child inference. Selector saves use an inter-process lock plus revision re-read before rename so simultaneous dialogs cannot silently overwrite each other. Candidate identities are anonymized before evaluation; provider/model metadata stays in local artifacts, not in evaluator prompts.
283
340
 
284
341
  Progress is surfaced through `fusion` status updates, TUI cancellable loader UI for `/fusion`, and partial `fusion_brainstorm` tool updates. Session shutdown or reload tracks the whole invocation from entry, aborts live or initializing Fusion runs, and waits for cleanup.
@@ -389,7 +446,7 @@ Fusion writes private debugging artifacts under:
389
446
  .pi/fusion/<session-id>-<pid>/<run-id>/
390
447
  ```
391
448
 
392
- Each run contains `manifest.json`, `canonical-input.json`, `context-omission-ledger.json`, `budget-plan.json`, candidate/evaluation/merge prompts, raw child JSONL events, stderr, responses, `blind-candidates.json`, `evaluation.json`, `merged.md`, and `error.json` for failed/cancelled runs. Persisted stage prompts are byte-identical to the exact bytes written to that child's stdin. `context-omission-ledger.json` carries the complete source-ordered omission ledger, and `budget-plan.json` records every configured route's capacity plus the pre-candidate feasibility decision, so a rejected run is as auditable as a successful one. Artifact files are written by private temp-file/fsync/rename, and v2 manifests persist cumulative child usage plus per-attempt observed usage/model data for successful, failed, and cancelled child attempts. Every usage record preserves the complete Pi cost breakdown; the same exact shape is cloned into `fusion_brainstorm` tool results so newer Pi hosts can calculate and replay footer/session statistics safely. These artifacts are local evidence only; they are not shown in `/jobs` or the background-task dock.
449
+ Each run contains `manifest.json`, `canonical-input.json`, `context-omission-ledger.json`, `budget-plan.json`, candidate/evaluation/merge prompts, raw child JSONL events, stderr, responses, and, when inspect or research candidates run, tool-call logs named `candidate-<slot>.attempt-<n>.tool-calls.jsonl`, plus `blind-candidates.json`, `evaluation.json`, `merged.md`, and `error.json` for failed/cancelled runs. Persisted stage prompts are byte-identical to the exact bytes written to that child's stdin. `context-omission-ledger.json` carries the complete source-ordered omission ledger, and `budget-plan.json` records every configured route's capacity plus the pre-candidate feasibility decision, so a rejected run is as auditable as a successful one. Artifact files are written by private temp-file/fsync/rename, and v2 manifests persist cumulative child usage plus per-attempt observed usage/model data for successful, failed, and cancelled child attempts. Every usage record preserves the complete Pi cost breakdown; the same exact shape is cloned into `fusion_brainstorm` tool results so newer Pi hosts can calculate and replay footer/session statistics safely. These artifacts are local evidence only; they are not shown in `/jobs` or the background-task dock.
393
450
 
394
451
  For attested Pi tasks only, the task id is `b` plus 32 random hex characters (128 bits) and additional flat siblings are written in the same directory:
395
452
 
package/TEST_PLAN.md CHANGED
@@ -13,7 +13,7 @@ This package follows:
13
13
  | Package | `pi-background-tasks` |
14
14
  | Extension entrypoint | `extensions/background-tasks.ts` |
15
15
  | Public commands | `/bg`, `/jobs`, `/logs`, `/kill`, `/tasks`, `/bg-tasks`, `/bg-clear`, `/bg-update`, `/fusion`, `/fusion-models` |
16
- | Public tools | `bg_run`, `bg_delegate`, `bg_result`, `bg_run_pi_attested`, `bg_status`, `bg_logs`, `bg_kill`, `fusion_brainstorm` |
16
+ | Public tools | `bg_run`, `bg_delegate`, `bg_result`, `bg_run_pi_attested`, `bg_status`, `bg_logs`, `bg_kill`, `fusion_brainstorm`, `fusion_validate` |
17
17
  | Extension EventBus API | `pi-background-tasks:request:v1`, `pi-background-tasks:response:v1`, `pi-background-tasks:terminal:v1`; schemas exported from `src/core/extension-api.ts` |
18
18
  | Shortcuts | `Shift+Down`; optional fallback `Ctrl+Alt+C` |
19
19
  | Custom UI | footer status + focused bottom dock overlay |
@@ -55,8 +55,15 @@ This package follows:
55
55
  | Stop task from LLM tool | `bg_kill` | | yes | | | | | | Covers running kill and already-finished loud failure. |
56
56
  | Fusion command direct result | `/fusion`, `fusion-result` custom message | yes | yes | yes | | yes | yes | | Core unit covers deterministic v2 conversation-projection construction, explicit image omission markers without raw image data, direct final-text child argv/stdin/compact-metadata parsing, exact multi-block answer reconstruction, artifacts, pre-abort handling, and orchestration. SDK runs real `/fusion` against a fake child `pi`, verifies exactly five child text-mode invocations, hidden prompt persistence, visible exact merged custom message, no parent assistant rewrite, renderer output, progress/status behavior, no-argument editor flow, editor cancellation, and malformed config causing zero children. RPC verifies command discovery, `/fusion <prompt>` with U+2028/U+2029 content, custom-message result emission, no parent `agent_start`, no-argument editor protocol, malformed config/child failure visibility, and child isolation flags. PTY verifies `/fusion` renders the exact merged answer directly in a real TUI. Compatibility smoke runs `/fusion` through every supported Pi version with the installed package entrypoint. |
57
57
  | Fusion tool result | `fusion_brainstorm({prompt})` | yes | yes | | | current-host stats/replay | yes | yes | Tool is registered at load and re-added on `session_start`; no eligibility/quota/routine/justification gates are implemented. BUG-182 unit/SDK coverage pins the exact host `Usage` contract, complete cost-component aggregation, rejection of legacy `costTotal`, v2 child/result/manifest schemas, exact merged text, progress, context exclusion, and final details. The release compatibility gate drives a real current-host RPC agent through the tool, verifies persisted tool usage, invokes the same session-stat path used by the footer, then reopens the durable session and verifies identical stats. Scripted-provider coverage proves normal parent consumption and evaluator schema repair. |
58
+ | Fusion Anthropic child sanitization | `@ravshansbox/pi-anthropic-sps` dependency, child `--extension` argv | yes | | | | | yes | | Unit proves a Claude route receives exactly two `--extension` paths with the metadata extension first and the sanitizer second, that every non-Anthropic provider still receives exactly one and never resolves the sanitizer at all, and that the real installed package resolves to an existing file through its manifest `pi.extensions[0]`. Resolution failures are each pinned as loud errors: package unresolvable, manifest unreadable, manifest not JSON, missing `pi` section, empty `pi.extensions`, blank entry, and a declared file that does not exist. Package guards require the sanitizer to be a declared dependency and require the provider gate so non-Anthropic child argv cannot drift. |
59
+ | Fusion validation workflow | `fusion_validate({prompt})`, workflow profiles, `fusion-manifest.v3`, `fusion-result.v4` | yes | | | | | yes | | Unit pins the brainstorm profile to the exact pre-extraction prompt constants so the workflow seam is proven byte-neutral, binds the validate profile to its own four system prompts, and asserts the two workflows share one evaluation schema contract and one repair framing. Validate orchestration covers five children with validate framing, candidates always `inspect` while evaluator and merger stay `reason`, tool-call log paths assigned only to candidates, `workflow` plus a `v`-prefixed run id persisted in the manifest and result details, and prompt artifacts byte-identical to the bytes handed to each child. A caller capability contradicting the fixed workflow policy is rejected before the artifact store exists and launches **zero** children; the same capability stated explicitly is accepted. Golden bytes pin a separate committed validate corpus, prove canonical input and omission ledger are identical across workflows for every case, and prove budget plans move for every case so the profile provably reaches the budget engine. Package guards reject `capability` and extra keys on `fusion_validate`, pin `FUSION_VALIDATE_CAPABILITY` to `inspect`, require `fusion_validate` in both the Fusion and delegate denylists, and pin the workflow-invariant comments at exactly one occurrence per launch site. |
58
60
  | Fusion conversation projection | canonical input `fusion-input.v4`, `context-omission-ledger.json` | yes | yes | yes | | | yes | | Unit covers a >1 MB synthetic tool-heavy session staying within budget, verbatim user/assistant text, thinking exclusion, zero tool-payload preview bytes (head/tail sentinels absent), exact and stable omission counts/byte totals/hashes, tuple round-trip preservation of roles/source ordinals/block ordinals/exact text, byte-identical repeated construction and separate-process determinism, hash change on omitted-payload mutation without exposing the payload, ledger root hash unchanged by compact encoding, contiguous omission-run collapsing into compact `["o", span, bytes, [thinking, toolCalls, toolResults]]` tuples, material canonical-input byte reduction versus the verbose object encoding, receipt-to-ledger reconciliation through `projection_map`, active-tool-call-leaf and sibling-call exclusion, marker-only user images plus ledger-only tool-result images with no base64, per-entry-point policy ids and request authority, and the exactly-one-disposition property for every retained block. SDK verifies the exact child stdin carries `conversation_projection` and never `conversation_transcript`. RPC verifies the versioned request object. A dedicated high-cardinality fixture (340 receipt runs over 1,360 omitted events with incident-matching visible-text volume) pins per-receipt cost and proves the pre-fix verbose format would overflow the merge stage while the compact tuple format fits. Package tests assert the ledger artifact ships in the documented artifact set. |
59
61
  | Fusion stage budgets | `budget-plan.json` (v3 per-stage forecasts), typed `prompt_budget_exceeded_forecast` / `prompt_budget_exceeded_measured` | yes | | | | | | | Unit covers the per-family affine estimator, additive segment accounting, multibyte 1.0-token/byte charging, unknown-provider floor visibility, byte-capacity route selection, scope guards for small windows, input-only fatal preflight versus warning-only reservations, rejection of unknown/zero/negative/too-small context windows, boundary accept at exactly the limit and reject one byte past, the child system prompt counted as input, per-stage forecasts built from the real prompt builders against each stage's own route, reservation warnings, breach-detector artifacts, safe prompts completing all five calls, persisted route/plan snapshots including negative slack on fatal rejection, and the reproduced 1 MB failure shape now fitting the smallest configured budget. Errors carry stage, measured size, allowed size, limiting model, estimator source, and remediation in both structured detail and message text. |
62
+ | Fusion candidate capability profiles | `fusion_brainstorm({prompt, capability?})`, candidate child argv | yes | yes | | | | yes | | Unit covers `reason` defaulting; reason and inspect argv remain byte-identical; `inspect` argv is exactly `--no-builtin-tools --tools read,grep,find,ls --exclude-tools bash,edit,write,fusion_brainstorm,bg_delegate,bg_result,bg_run,bg_kill,bg_status,bg_logs,bg_run_pi_attested`; `research` argv is the inspect allowlist plus `fusion_web_fetch`, with the same denylist. Unit also proves capability-specific prompts, research env only for research children, candidate tool-call log paths assigned only to candidates, and evaluator plus merger calls staying `reason`/`--no-tools` under caller-selected inspect or research. SDK and package guards validate only `reason`/`inspect`/`research`, reject extra keys, pin the allow/deny lists and `fusion_web_fetch` tool name, and assert evaluator/merger launch sites cannot read the caller-selected capability. |
63
+ | Fusion research web fetch | `fusion_web_fetch({ url, extract? })`, `candidate-<slot>.attempt-<n>.tool-calls.jsonl` | yes | | | | | yes | live child | Unit covers the closed schema surface through the child extension, registration only when research mode is enabled, unsupported schemes and URL credentials failing before network access, blocked DNS classes, a mixed public-plus-blocked DNS answer failing loudly, a redirect to a blocked address failing loudly, redirect-loop limit failure, Content-Length and streamed-body response caps, unsupported content types, Markdown extraction preserving links/headings/tables/code blocks, script/style stripping, text/plain passthrough with hash and byte count, UTF-8-safe output truncation, and typed timeout. Unit also proves the resolver is consulted once and the connection is pinned to that vetted address, and that audit metadata records `url`, `final_url`, `http_status`, `response_bytes`, and `content_sha256` without raw page content. Package guards require `fusion_web_fetch` registration behind the research env flag. Live verification fetched `https://example.com/`, reported the page H1, read a repository constant, and refused instruction-like text from both a repo file and the fetched page. |
64
+ | Fusion multi-message transcript validation | `*.events.jsonl`, stop reasons `toolUse`/`stop` | yes | | | | | | | Unit accepts a multi-message tool loop, reconstructs stdout from the final assistant message only, and sums usage across all compact metadata records. Non-final records must be `toolUse`, the final record must be `stop`, and `length`, `error`, `aborted`, and `pending` fail loudly instead of being skipped. |
65
+ | Fusion stale-action watchdog | `FUSION_CHILD_IDLE_TIMEOUT_MS`, child process lifecycle | yes | | | | | | | Unit proves a child with no stdout or stderr activity fails as `child_timeout` with the stalled-child message and is terminated; stderr activity resets the watchdog and allows success; the 30-minute absolute timeout path remains distinct and is not reported as a stalled child. |
66
+ | Fusion candidate tool-call audit log | `candidate-<slot>.attempt-<n>.tool-calls.jsonl`, `pi-background-tasks.fusion-tool-call.v1` | yes | | | | | | | Unit proves completed tool calls are logged with tool name, byte counts, and hashes while raw arguments/results containing a secret never appear; a complete 3-call log summarizes count and result bytes; zero completed calls is a complete trace; a trailing partial line, ordinal gap, duplicate ordinal, wrong schema version, and inspect-child partial log all fail loudly. Artifact coverage verifies successful attempts persist the JSONL and manifest summary, while no-log attempts do not invent one. |
60
67
  | Pi/TypeBox compatibility | `peerDependencies`, packed bytes | yes | | | | | yes | | Unit pins typebox as a `"*"` peer that is neither a runtime nor bundled dependency, requires the resolved TypeBox to be Pi 0.83's 1.3.x line, requires all four supported Pi/TUI lines in the peer range, scans all package TypeScript for the seven removed TypeBox APIs, and compiles the shipped tool schema plus nullable-array/nullable-string and optional-field shapes under TypeBox 1.3. The release compat gate repeats the removed-API scan against installed package bytes per Pi version. |
61
68
  | Pi hook contract | `npm run test:hook-contract`, `tests/scripted-provider/pi-hook-contract-evidence.json`, `src/core/delegate/hook-contract-evidence.json` | | | | | | evidence parity | yes | Executes a real Pi agent loop and records, rather than assumes: `context` fires once before every model call in load order; returned messages reach the provider; **throwing** in `context` does NOT block dispatch (Pi catches and continues); `ctx.abort()` does not skip the call site but delivers an already-aborted signal and terminates the run; `tool_result` fires before the transcript entry, chains in load order, replacement reaches the provider while the original does not, and toolCallId/role/`isError` survive. Handler ordering across two separate probe extensions is pinned. The evidence file is compared, never silently regenerated, and a package test asserts the shipped copy is byte-identical. |
62
69
  | Delegate context seeding | `bg_delegate` seed `pi-background-tasks.delegate-seed.v1`, `seed.json`, `context-omission-ledger.json` | yes | yes | | | | mutation guard | yes | Unit covers verbatim visible user/assistant text, complete exclusion of thinking/tool-call arguments/tool-result payloads, marker-only images with no raw bytes anywhere in the seed, exclusion of the in-flight `bg_delegate` call **and every sibling call in the same assistant message** (two delegates launched together get byte-identical projected history), the prompt preserved exactly and marked `explicit_text` authority, zero payload-preview bytes, byte-identical construction across repeated builds and across separate processes via a fixed-leaf fixture, and refusal of a blank prompt. Receive-side verification rejects a single mutated byte, a foreign task identity, a structurally malformed seed whose hash matches, a directive whose hash disagrees with its text, and an unsupported capability. SDK proves the projected parent text actually reaches the child. |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-background-tasks",
3
- "version": "0.7.7",
4
- "description": "Pi extension package for background shell tasks plus five-model fusion brainstorming: bg_run tools, /bg commands, /fusion, fusion_brainstorm, model selector UI, bounded logs, kill/timeout safety, and completion wakeups.",
3
+ "version": "0.9.0",
4
+ "description": "Pi extension package for background shell tasks plus five-model fusion brainstorming and validation: bg_run tools, bg_delegate, /bg commands, /fusion, fusion_brainstorm, fusion_validate, model selector UI, bounded logs, kill/timeout safety, and completion wakeups.",
5
5
  "type": "module",
6
6
  "license": "ISC",
7
7
  "author": "Ismail <ismailsalikhodjaev@gmail.com>",
@@ -72,12 +72,17 @@
72
72
  "@earendil-works/pi-ai": "^0.83.0",
73
73
  "@earendil-works/pi-coding-agent": "^0.83.0",
74
74
  "@earendil-works/pi-tui": "^0.83.0",
75
+ "@ravshansbox/pi-anthropic-sps": "github:ravshansbox/pi-anthropic-sps",
75
76
  "@types/node": "^24.0.0",
77
+ "@types/turndown": "5.0.6",
76
78
  "tsx": "^4.19.0",
77
79
  "typebox": "^1.3.7",
78
80
  "typescript": "^5.9.0"
79
81
  },
80
82
  "engines": {
81
83
  "node": ">=22.19.0"
84
+ },
85
+ "dependencies": {
86
+ "turndown": "7.2.4"
82
87
  }
83
88
  }
@@ -222,6 +222,7 @@ export const DELEGATE_FORBIDDEN_TOOLS: readonly string[] = [
222
222
  'bg_kill',
223
223
  'bg_run_pi_attested',
224
224
  'fusion_brainstorm',
225
+ 'fusion_validate',
225
226
  ];
226
227
 
227
228
  export function delegateToolsFor(capability: DelegateCapability): readonly string[] {
@@ -15,6 +15,7 @@ import {
15
15
  type FusionBudgetPlanV1,
16
16
  type FusionCalibrationViolation,
17
17
  type FusionCandidateId,
18
+ type FusionCapability,
18
19
  type FusionContextOmissionLedgerV2,
19
20
  type FusionChildRunResult,
20
21
  type FusionModelConfigV1,
@@ -23,14 +24,21 @@ import {
23
24
  type FusionState,
24
25
  type FusionTerminalState,
25
26
  type FusionUsage,
27
+ type FusionWorkflowId,
26
28
  type ResolvedFusionModels,
27
29
  } from './types.js';
30
+ import { FUSION_BRAINSTORM_WORKFLOW, type FusionWorkflowProfile } from './workflows.js';
28
31
 
29
- const RUN_ID_PATTERN = /^f[0-9a-f]{32}$/;
32
+ /**
33
+ * Run ids are prefixed by workflow so an artifact directory is self-describing.
34
+ * The prefix set is closed: an unknown prefix must fail rather than be accepted.
35
+ */
36
+ const RUN_ID_PATTERN = /^[fv][0-9a-f]{32}$/;
30
37
 
31
38
  interface MutableFusionArtifactManifest {
32
39
  schema_version: typeof FUSION_MANIFEST_SCHEMA_VERSION;
33
40
  run_id: string;
41
+ workflow: FusionWorkflowId;
34
42
  source: FusionSource;
35
43
  state: FusionState;
36
44
  created_at: string;
@@ -43,6 +51,11 @@ interface MutableFusionArtifactManifest {
43
51
  merger: string;
44
52
  thinking_level: string;
45
53
  };
54
+ capabilities: {
55
+ candidate: FusionCapability;
56
+ evaluation: FusionCapability;
57
+ merge: FusionCapability;
58
+ };
46
59
  usage: FusionUsage;
47
60
  attempts: FusionAttemptArtifactRecord[];
48
61
  artifacts: Record<string, FusionArtifactRef>;
@@ -54,9 +67,15 @@ export interface CreateFusionArtifactStoreOptions {
54
67
  cwd: string;
55
68
  sessionId?: string | undefined;
56
69
  runId?: string | undefined;
70
+ profile?: FusionWorkflowProfile | undefined;
57
71
  source: FusionSource;
58
72
  config: FusionModelConfigV1;
59
73
  models: ResolvedFusionModels;
74
+ capabilities?: {
75
+ candidate: FusionCapability;
76
+ evaluation: FusionCapability;
77
+ merge: FusionCapability;
78
+ };
60
79
  now?: () => Date;
61
80
  }
62
81
 
@@ -83,8 +102,8 @@ export interface RecordFusionFailedAttemptInput {
83
102
  usage?: FusionUsage;
84
103
  }
85
104
 
86
- function makeRunId(): string {
87
- return `f${randomBytes(16).toString('hex')}`;
105
+ function makeRunId(profile: FusionWorkflowProfile): string {
106
+ return `${profile.runIdPrefix}${randomBytes(16).toString('hex')}`;
88
107
  }
89
108
 
90
109
  function modelsForManifest(models: ResolvedFusionModels): MutableFusionArtifactManifest['models'] {
@@ -149,6 +168,7 @@ function publicManifest(manifest: MutableFusionArtifactManifest): FusionArtifact
149
168
  const out: FusionArtifactManifest = {
150
169
  schema_version: manifest.schema_version,
151
170
  run_id: manifest.run_id,
171
+ workflow: manifest.workflow,
152
172
  source: manifest.source,
153
173
  state: manifest.state,
154
174
  created_at: manifest.created_at,
@@ -156,6 +176,7 @@ function publicManifest(manifest: MutableFusionArtifactManifest): FusionArtifact
156
176
  cwd: manifest.cwd,
157
177
  config: manifest.config,
158
178
  models: manifest.models,
179
+ capabilities: manifest.capabilities,
159
180
  usage: cloneFusionUsage(manifest.usage),
160
181
  attempts: [...manifest.attempts],
161
182
  artifacts: { ...manifest.artifacts },
@@ -203,8 +224,14 @@ export class FusionArtifactStore {
203
224
  }
204
225
 
205
226
  static async create(options: CreateFusionArtifactStoreOptions): Promise<FusionArtifactStore> {
206
- const runId = options.runId ?? makeRunId();
227
+ const profile = options.profile ?? FUSION_BRAINSTORM_WORKFLOW;
228
+ const runId = options.runId ?? makeRunId(profile);
207
229
  if (!RUN_ID_PATTERN.test(runId)) throw errorForArtifact(`invalid fusion run id: ${runId}`);
230
+ if (!runId.startsWith(profile.runIdPrefix)) {
231
+ throw errorForArtifact(
232
+ `fusion run id ${runId} does not carry the ${profile.id} workflow prefix ${profile.runIdPrefix}`,
233
+ );
234
+ }
208
235
  const sessionSegment = sanitizePathSegment(
209
236
  options.sessionId ?? `session-${String(process.pid)}`,
210
237
  );
@@ -217,6 +244,7 @@ export class FusionArtifactStore {
217
244
  const manifest: MutableFusionArtifactManifest = {
218
245
  schema_version: FUSION_MANIFEST_SCHEMA_VERSION,
219
246
  run_id: runId,
247
+ workflow: profile.id,
220
248
  source: options.source,
221
249
  state: 'initializing',
222
250
  created_at: timestamp,
@@ -224,6 +252,11 @@ export class FusionArtifactStore {
224
252
  cwd: options.cwd,
225
253
  config: options.config,
226
254
  models: modelsForManifest(options.models),
255
+ capabilities: options.capabilities ?? {
256
+ candidate: 'reason',
257
+ evaluation: 'reason',
258
+ merge: 'reason',
259
+ },
227
260
  usage: cloneFusionUsage(EMPTY_FUSION_USAGE),
228
261
  attempts: [],
229
262
  artifacts: {},
@@ -250,6 +283,10 @@ export class FusionArtifactStore {
250
283
  return this.runDirAbs;
251
284
  }
252
285
 
286
+ childToolCallLogPath(stage: FusionStage, slot: 1 | 2 | 3 | undefined, attempt: number): string {
287
+ return this.artifactPath(`${attemptPrefix(stage, slot, attempt)}.tool-calls.jsonl`);
288
+ }
289
+
253
290
  snapshot(): FusionArtifactManifest {
254
291
  return publicManifest(this.manifest);
255
292
  }
@@ -338,6 +375,10 @@ export class FusionArtifactStore {
338
375
  responseName(prefix, input.responseKind),
339
376
  input.result.text,
340
377
  );
378
+ const toolCallsRef =
379
+ input.result.toolCallTrace === undefined
380
+ ? undefined
381
+ : await this.writeArtifact(`${prefix}.tool-calls.jsonl`, input.result.toolCallTrace.bytes);
341
382
  await this.updateManifest((manifest) => {
342
383
  const record: FusionAttemptArtifactRecord = {
343
384
  stage: input.result.stage,
@@ -352,6 +393,10 @@ export class FusionArtifactStore {
352
393
  qualifiedId: input.result.qualifiedId,
353
394
  usage: cloneFusionUsage(input.result.usage),
354
395
  };
396
+ if (toolCallsRef !== undefined && input.result.toolCallTrace !== undefined) {
397
+ record.tool_calls_path = toolCallsRef.path;
398
+ record.tool_calls = { ...input.result.toolCallTrace.summary };
399
+ }
355
400
  if (input.result.slot !== undefined) record.slot = input.result.slot;
356
401
  manifest.attempts.push(record);
357
402
  });
@@ -14,10 +14,6 @@ import {
14
14
  isUsableContextWindow,
15
15
  } from '../context/token-budget.js';
16
16
  import {
17
- FUSION_CANDIDATE_SYSTEM_PROMPT,
18
- FUSION_EVALUATION_REPAIR_SYSTEM_PROMPT,
19
- FUSION_EVALUATOR_SYSTEM_PROMPT,
20
- FUSION_MERGER_SYSTEM_PROMPT,
21
17
  buildBlindEvaluationInput,
22
18
  buildCandidatePrompt,
23
19
  buildEvaluationPrompt,
@@ -26,10 +22,12 @@ import {
26
22
  buildMergePrompt,
27
23
  type AnonymousFusionCandidate,
28
24
  } from './prompts.js';
25
+ import { FUSION_BRAINSTORM_WORKFLOW, type FusionWorkflowProfile } from './workflows.js';
29
26
  import {
30
27
  FUSION_BUDGET_PLAN_SCHEMA_VERSION,
31
28
  FUSION_CALIBRATION_VIOLATION_SCHEMA_VERSION,
32
29
  FUSION_EVALUATION_SCHEMA_VERSION,
30
+ FUSION_DEFAULT_CAPABILITY,
33
31
  FusionError,
34
32
  type FusionBudgetBlocker,
35
33
  type FusionBudgetCheckKind,
@@ -45,6 +43,7 @@ import {
45
43
  type FusionBudgetWarning,
46
44
  type FusionCalibrationViolation,
47
45
  type FusionCanonicalInputV3,
46
+ type FusionCapability,
48
47
  type FusionEvaluationV1,
49
48
  type FusionRouteCapacity,
50
49
  type FusionStage,
@@ -677,11 +676,20 @@ export class FusionBudget {
677
676
  readonly routes: readonly FusionRouteCapacity[];
678
677
  readonly limiting: FusionRouteCapacity;
679
678
  private readonly contextPolicyId: string;
680
-
681
- constructor(models: ResolvedFusionModels, contextPolicyId: string) {
679
+ private readonly candidateCapability: FusionCapability;
680
+ private readonly profile: FusionWorkflowProfile;
681
+
682
+ constructor(
683
+ models: ResolvedFusionModels,
684
+ contextPolicyId: string,
685
+ candidateCapability: FusionCapability = FUSION_DEFAULT_CAPABILITY,
686
+ profile: FusionWorkflowProfile = FUSION_BRAINSTORM_WORKFLOW,
687
+ ) {
682
688
  this.routes = fusionRouteCapacities(models);
683
689
  this.limiting = fusionLimitingRoute(this.routes);
684
690
  this.contextPolicyId = contextPolicyId;
691
+ this.candidateCapability = candidateCapability;
692
+ this.profile = profile;
685
693
  }
686
694
 
687
695
  get allowedInputTokens(): number {
@@ -708,6 +716,7 @@ export class FusionBudget {
708
716
  }
709
717
 
710
718
  private drafts(input: FusionCanonicalInputV3): readonly StageForecastDraft[] {
719
+ const candidateSystemPrompt = this.profile.candidateSystemPrompt(this.candidateCapability);
711
720
  const candidatePrompt = buildCandidatePrompt(input);
712
721
  const blindInput = buildBlindEvaluationInput(input, EMPTY_CANDIDATES);
713
722
  const evaluationPrompt = buildEvaluationPrompt(blindInput);
@@ -724,7 +733,7 @@ export class FusionBudget {
724
733
  slot: 1,
725
734
  route: this.routeForStage('candidate', 1),
726
735
  conditional: false,
727
- system_prompt: FUSION_CANDIDATE_SYSTEM_PROMPT,
736
+ system_prompt: candidateSystemPrompt,
728
737
  empty_user_prompt: candidatePrompt,
729
738
  upstream_output_contract_bytes: 0,
730
739
  },
@@ -733,7 +742,7 @@ export class FusionBudget {
733
742
  slot: 2,
734
743
  route: this.routeForStage('candidate', 2),
735
744
  conditional: false,
736
- system_prompt: FUSION_CANDIDATE_SYSTEM_PROMPT,
745
+ system_prompt: candidateSystemPrompt,
737
746
  empty_user_prompt: candidatePrompt,
738
747
  upstream_output_contract_bytes: 0,
739
748
  },
@@ -742,7 +751,7 @@ export class FusionBudget {
742
751
  slot: 3,
743
752
  route: this.routeForStage('candidate', 3),
744
753
  conditional: false,
745
- system_prompt: FUSION_CANDIDATE_SYSTEM_PROMPT,
754
+ system_prompt: candidateSystemPrompt,
746
755
  empty_user_prompt: candidatePrompt,
747
756
  upstream_output_contract_bytes: 0,
748
757
  },
@@ -750,7 +759,7 @@ export class FusionBudget {
750
759
  budget_stage: 'evaluation',
751
760
  route: this.routeForStage('evaluation'),
752
761
  conditional: false,
753
- system_prompt: FUSION_EVALUATOR_SYSTEM_PROMPT,
762
+ system_prompt: this.profile.evaluatorSystemPrompt,
754
763
  empty_user_prompt: evaluationPrompt,
755
764
  upstream_output_contract_bytes: 3 * FUSION_CANDIDATE_MAX_OUTPUT_BYTES,
756
765
  },
@@ -758,7 +767,7 @@ export class FusionBudget {
758
767
  budget_stage: 'merge',
759
768
  route: this.routeForStage('merge'),
760
769
  conditional: false,
761
- system_prompt: FUSION_MERGER_SYSTEM_PROMPT,
770
+ system_prompt: this.profile.mergerSystemPrompt,
762
771
  empty_user_prompt: mergePrompt,
763
772
  upstream_output_contract_bytes:
764
773
  3 * FUSION_CANDIDATE_MAX_OUTPUT_BYTES + FUSION_EVALUATION_MAX_OUTPUT_BYTES,
@@ -767,7 +776,7 @@ export class FusionBudget {
767
776
  budget_stage: 'evaluation_repair',
768
777
  route: this.routeForStage('evaluation_repair'),
769
778
  conditional: true,
770
- system_prompt: FUSION_EVALUATION_REPAIR_SYSTEM_PROMPT,
779
+ system_prompt: this.profile.evaluationRepairSystemPrompt,
771
780
  empty_user_prompt: repairPrompt,
772
781
  upstream_output_contract_bytes:
773
782
  3 * FUSION_CANDIDATE_MAX_OUTPUT_BYTES +
@@ -13,6 +13,7 @@ import {
13
13
  type ReadonlyParentSessionManager,
14
14
  } from '../context/parent-snapshot.js';
15
15
  import type { Message } from '@earendil-works/pi-ai';
16
+ import { FUSION_BRAINSTORM_TOOL_NAME as FUSION_BRAINSTORM_TOOL_NAME_VALUE } from './workflows.js';
16
17
  import {
17
18
  FUSION_BRANCH_FILTER_ID,
18
19
  FUSION_COMMAND_CONTEXT_POLICY_ID,
@@ -33,7 +34,11 @@ import {
33
34
  type FusionSource,
34
35
  } from './types.js';
35
36
 
36
- export const FUSION_BRAINSTORM_TOOL_NAME = 'fusion_brainstorm';
37
+ /**
38
+ * Re-exported from the workflow registry, which owns every workflow's tool name.
39
+ * Kept here so existing importers of this module keep working unchanged.
40
+ */
41
+ export { FUSION_BRAINSTORM_TOOL_NAME, FUSION_VALIDATE_TOOL_NAME } from './workflows.js';
37
42
 
38
43
  /** Retained for source compatibility; Fusion's session access is the shared adapter. */
39
44
  export type FusionReadonlySessionManager = ReadonlyParentSessionManager;
@@ -202,7 +207,7 @@ export function buildFusionCanonicalInput(
202
207
  childCreated: false,
203
208
  });
204
209
  }
205
- const toolName = options.toolName ?? FUSION_BRAINSTORM_TOOL_NAME;
210
+ const toolName = options.toolName ?? FUSION_BRAINSTORM_TOOL_NAME_VALUE;
206
211
  const snapshotOptions: ParentSnapshotOptions = {
207
212
  toolName,
208
213
  excludeActiveToolCallLeaf: options.source === 'tool',