pi-workflow-engine 1.1.0 → 1.3.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 (55) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +199 -85
  2. package/.pi/extensions/pi-workflow-engine/src/advisory-challenge.ts +5 -2
  3. package/.pi/extensions/pi-workflow-engine/src/advisory-schema.ts +8 -0
  4. package/.pi/extensions/pi-workflow-engine/src/agent-limits.ts +4 -0
  5. package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +3 -1
  6. package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +18 -4
  7. package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +34 -7
  8. package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +9 -3
  9. package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +51 -8
  10. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +14 -0
  11. package/.pi/extensions/pi-workflow-engine/src/codemode-slots.ts +73 -0
  12. package/.pi/extensions/pi-workflow-engine/src/config-file.ts +52 -0
  13. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +127 -58
  14. package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +112 -31
  15. package/.pi/extensions/pi-workflow-engine/src/engine.ts +75 -18
  16. package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +5 -0
  17. package/.pi/extensions/pi-workflow-engine/src/inline-workflow.ts +7 -1
  18. package/.pi/extensions/pi-workflow-engine/src/journal.ts +22 -4
  19. package/.pi/extensions/pi-workflow-engine/src/model-profiles.ts +9 -47
  20. package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +1 -1
  21. package/.pi/extensions/pi-workflow-engine/src/progress.ts +32 -4
  22. package/.pi/extensions/pi-workflow-engine/src/prompt-sections.ts +15 -0
  23. package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +4 -4
  24. package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +1 -1
  25. package/.pi/extensions/pi-workflow-engine/src/types.ts +54 -9
  26. package/.pi/extensions/pi-workflow-engine/src/ui/kit.ts +1 -0
  27. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +9 -3
  28. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +69 -18
  29. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-runs-browser.ts +17 -6
  30. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-tool-renderer.ts +8 -3
  31. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +16 -8
  32. package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +82 -23
  33. package/.pi/extensions/pi-workflow-engine/src/workflow-catalog.ts +79 -0
  34. package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +6 -2
  35. package/.pi/extensions/pi-workflow-engine/src/workflow-input.ts +13 -0
  36. package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +21 -4
  37. package/.pi/extensions/pi-workflow-engine/src/workflow-result-context.ts +26 -6
  38. package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +2 -0
  39. package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +69 -13
  40. package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +23 -4
  41. package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +27 -1
  42. package/.pi/extensions/pi-workflow-engine/src/workflow-run-seal.ts +83 -0
  43. package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +30 -4
  44. package/.pi/extensions/pi-workflow-engine/src/workflow-script-file.ts +50 -0
  45. package/.pi/extensions/pi-workflow-engine/src/workflow-settings.ts +147 -0
  46. package/.pi/extensions/pi-workflow-engine/src/workflow-tool-request.ts +61 -0
  47. package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +59 -22
  48. package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +7 -4
  49. package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +7 -4
  50. package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +7 -4
  51. package/.pi/extensions/pi-workflow-engine/workflows/research.ts +12 -4
  52. package/README.md +11 -3
  53. package/USAGE.md +124 -23
  54. package/package.json +13 -4
  55. package/skills/workflow-authoring/SKILL.md +162 -0
package/USAGE.md CHANGED
@@ -40,10 +40,11 @@ Useful flags:
40
40
  /workflow code-review --budget=50000 # output-token ceiling for subagents
41
41
  /workflow code-review --resume <run-id> # replay matching completed agent calls
42
42
  /workflow code-review --resume <run-id> --resume-edited # reuse unchanged calls after a workflow edit
43
- /workflow code-review --refresh # rediscover newly added workflow files
43
+ /workflow code-review --refresh # reload .ts drop-ins (saved .js scripts are always reread)
44
44
  /workflow:runs # browse recent durable project runs
45
45
  /workflow:inspector <run-id> # inspect one retained run directly
46
46
  /workflow:models # inspect small/medium/big model routes
47
+ /workflow:size # show or set the workflow size guideline
47
48
  ```
48
49
 
49
50
  ## Workflow model profiles
@@ -83,7 +84,11 @@ secrets stay in pi's provider configuration. The engine never ranks models by
83
84
  price, model-name substrings, or assumed capability.
84
85
 
85
86
  Routing is resolved per field: an agent's explicit `model` or `thinkingLevel`
86
- wins, then its requested profile, then host inheritance. A configured profile
87
+ wins, then its requested profile, then the profile of its declared phase, then
88
+ host inheritance. A phase profile comes from `meta.phases` (`{ title, profile }`)
89
+ and applies only to an agent that passes that title as its `phase` option and
90
+ sets neither `model` nor `profile`; the global `phase()` call never selects one,
91
+ because concurrent branches share it. A configured profile
87
92
  without `thinkingLevel` inherits the host effort. An entirely unconfigured tier
88
93
  uses the host model and the built-in bounded effort for that tier (`low`,
89
94
  `medium`, or `high` respectively); `/workflow:models` labels that route as a host
@@ -115,12 +120,14 @@ because only those runs can reach the host's MCP tools.
115
120
 
116
121
  The workflow bounds decomposition to four lanes and verification to twelve
117
122
  claims, while the engine's concurrency, total-agent, timeout, cancellation, and
118
- token-budget limits still apply. Gatherers retain direct page titles and URLs;
119
- search-results URLs are rejected, sources are canonicalized and deduplicated,
120
- and separate verifier agents classify claims as supported, conflicting,
121
- uncertain, inferred, or rejected before synthesis. External research calls use
122
- `resume: "off"` because live web evidence, time, and service state are outside
123
- the journal's deterministic replay surface.
123
+ token-budget limits still apply. When gatherers find more than twelve distinct
124
+ claims, the most important are verified, and both the run log and the report's
125
+ limitations say how many lower-priority claims the cap left unverified. Gatherers retain direct page
126
+ titles and URLs; search-results URLs are rejected, sources are canonicalized and
127
+ deduplicated, and separate verifier agents classify claims as supported,
128
+ conflicting, uncertain, inferred, or rejected before synthesis. External
129
+ research calls use `resume: "off"` because live web evidence, time, and service
130
+ state are outside the journal's deterministic replay surface.
124
131
 
125
132
  ## Use Dynamax for custom workflows
126
133
 
@@ -141,6 +148,7 @@ dynamax use parallel agents to review this design and report the risks
141
148
  Sticky mode and inspector shortcut:
142
149
 
143
150
  ```text
151
+ /workflow:dynamax # toggle sticky mode
144
152
  /workflow:dynamax on
145
153
  /workflow:dynamax off
146
154
  /workflow:dynamax status
@@ -148,7 +156,7 @@ Sticky mode and inspector shortcut:
148
156
 
149
157
  The workflow inspector and code-review results viewer are also registered as first-class shortcuts shown by `/hotkeys`. The defaults are `ctrl+shift+m` for the inspector and `ctrl+shift+r` for the latest review results. `ctrl+o` is intentionally not used because pi already uses it for tool-output expansion and tree filtering.
150
158
 
151
- When only the literal `dynamax` token is used, the opt-in is one-shot: the next agent run receives a `<dynamax>` system prompt section that permits the workflow tool (pi appends it as a section patch, so the cached prompt head is kept), stays visibly active for that run, then clears after the run ends. When `/workflow:dynamax on` is used, the opt-in is sticky for the current pi session until `/workflow:dynamax off`. The footer status states the mode compactly: `◆ dynamax` while sticky, `◆ dynamax · next prompt` while a one-shot waits for its prompt, and `◆ dynamax · this turn` while that run is active; off mode clears it. A running workflow names itself in its own status next to it.
159
+ When only the literal `dynamax` token is used, the opt-in is one-shot: the next agent run receives a `<dynamax>` system prompt section that permits the workflow tool (pi appends it as a section patch, so the cached prompt head is kept), stays visibly active for that run, then clears after the run ends. When `/workflow:dynamax on` (or `/workflow:dynamax` while sticky mode is off) is used, the opt-in is sticky for the current pi session until `/workflow:dynamax off` (or `/workflow:dynamax` again). Sticky mode is a standing default rather than a one-request permission: its `<dynamax>` section tells the host agent to orchestrate every substantive task with the workflow tool, scouting inline first and then running one single-phase workflow per phase (understand, design, implement, review), reading each result before choosing the next, and to work without a workflow only for conversational turns and trivial or already-verified edits. It applies to every run in the session, including runs that a custom message starts, such as a background workflow waking the agent; a one-shot is consumed only by the user prompt that carried it. The footer status states the mode compactly: `◆ dynamax` while sticky, `◆ dynamax · next prompt` while a one-shot waits for its prompt, and `◆ dynamax · this turn` while that run is active; off mode clears it. A running workflow names itself in its own status next to it.
152
160
 
153
161
  ### Prompt editor cue
154
162
 
@@ -191,6 +199,24 @@ Set either shortcut to `null` to disable it while keeping `/workflow:inspector`
191
199
 
192
200
  With Dynamax enabled, the host agent usually calls the `workflow` tool with `script`: a one-off inline workflow tailored to your prompt. It can still call a saved workflow by `name` when one already fits.
193
201
 
202
+ Every run that can call the `workflow` tool also carries a `<workflows>` system prompt section: the catalog of registered workflows, one line each with its name, `description`, and `whenToUse`, sorted by name. It is built once per pi session from workflow discovery and reused, so its text is identical on every turn and keeps the provider's prompt cache; start a new session or `/reload` to list workflows added since.
203
+
204
+ ### Workflow size guideline
205
+
206
+ The catalog ends with one line telling the host agent how large to make each workflow it authors:
207
+
208
+ ```text
209
+ /workflow:size # show the current guideline and where it comes from
210
+ /workflow:size small # keep each workflow under 5 agents
211
+ /workflow:size medium # under 10 agents (the default)
212
+ /workflow:size large --project # under 25 agents, for this project only
213
+ /workflow:size off # leave the line out
214
+ ```
215
+
216
+ The line reads, for example, `Workflow size guideline: medium — keep each workflow under 10 agents unless the user asks for more; the user changes this with /workflow:size.` The setting is stored like the model profiles: `~/.pi/agent/workflow-settings.json` for the user, overridden by `<project>/.pi/workflow-settings.json`; setting a size writes the user file unless `--project` is given. A minimal file is `{ "size": "small" }`. Changing it rebuilds the current session's catalog once.
217
+
218
+ The guideline only shapes what the host agent writes. The `maxAgents` run limit (default `64`, see [Runtime knobs](#runtime-knobs)) stays the separate hard cap: once a run has started that many live agents, further `agent()` calls fail with `WorkflowAgentLimitError`, whatever the guideline says.
219
+
194
220
  ### Adaptive multi-pass authoring
195
221
 
196
222
  A single fan-out is still the default when it can answer the question. When the first pass may reveal gaps, conflicts, weak claims, or missing evidence, an inline workflow can inspect those intermediate results and commission only the follow-up work the LLM says is needed:
@@ -249,6 +275,8 @@ During a run, a widget above the editor shows the workflow's progress bar, finis
249
275
 
250
276
  The inspector has Overview, Agents, Findings, and Logs sections, plus Result for a retained run opened from `/workflow:runs`. Switch sections with `tab`/`shift+tab`, `←`/`→`, or `1`–`5`; move with `↑`/`↓` or `j`/`k`, jump with `g`/`G`, expand an agent or finding with `enter`, and close with `q` or `esc`. Expanded agents show the full error, last tool, model, and token usage.
251
277
 
278
+ While a run is live, `s` on the Agents tab skips the selected running agent: press it twice on the same agent (like stopping a run in the runs browser); the key is listed in the footer only while the selection can take it. Skipping aborts only that agent's session; the run and its sibling agents carry on. The skipped `agent()` call rejects with a recoverable `WorkflowAgentSkippedError` (code `WORKFLOW_AGENT_SKIPPED`), so `parallel()` and `pipeline()` turn it into a `null` slot while a direct `await` sees the error. A skipped agent is never restarted by `agentRetries` and never written to the replay journal, so a resumed run asks it again; the tokens it already spent still count toward usage and the budget. The widget, inspector and run record show the agent as `skipped`. Skipping works for a background run too when you open its live inspector with `/workflow:inspector` or the shortcut; a retained run opened from `/workflow:runs` is read-only.
279
+
252
280
  ### Durable run records
253
281
 
254
282
  Every workflow run writes a versioned project-local record to `.pi/.workflow-runs/<run-id>.run.json`; its agent replay journal remains `.pi/.workflow-runs/<run-id>.jsonl`. The same run ID appears in execution metadata, progress snapshots, result details, and both filenames. Records distinguish `queued`, `running`, `completed`, `failed`, `stopped`, and `paused` lifecycle states and retain resolved scalar options, timestamps, compact progress, summary usage, and the final result or error.
@@ -257,11 +285,17 @@ Record replacement is atomic, corrupt or unsupported-version files are ignored i
257
285
 
258
286
  Persistence is intentionally bounded and private by default. Raw workflow args, prompts, subagent transcripts, progress logs, lane details, agent errors, environment data, credentials, and tool-output-shaped fields are not stored. Final results are limited to bounded plain JSON; credential- and transcript-shaped fields are redacted, and unsupported or cyclic results are marked unavailable instead of executing accessors or breaking the workflow. Loading history only parses these data files and never imports or executes workflow code.
259
287
 
288
+ An inline run (a tool `script` or `scriptPath`) also saves its script to `.pi/.workflow-runs/<run-id>.script.js`, a separate file the record only references, because the record never holds prompts. The script holds nothing new: pi's session transcript already contains it as the tool call's arguments. A pruned record takes its script with it. Whenever the engine creates `.pi/.workflow-runs/`, it adds a `.gitignore` containing `*` so run files stay out of the project's git history; an existing `.gitignore` there is never replaced.
289
+
290
+ The result handed to the host agent names the run's files after the run ID: `Script:` for an inline run, and `Journal:` with a reminder to read the journal before diagnosing an empty or unexpected result. A failed inline run lists the same lines after its error, a background start names the script too, and a background delivery names the record, script, and journal (see [Background workflows](#background-workflows)). To iterate, edit the script and rerun it with `scriptPath` (see [Author an inline workflow](#author-an-inline-workflow)).
291
+
260
292
  ### Background workflows
261
293
 
262
294
  The host-facing `workflow` tool accepts `background: true`. It returns as soon as the run ID and origin metadata are durably recorded, while the normal workflow engine continues in-process with the same concurrency, budget, worktree, progress, usage, and resume rules. Synchronous execution remains the default, and the `/workflow` slash command remains synchronous.
263
295
 
264
- Background runs are detached from the initiating tool-call signal, so the host agent can finish that turn and continue the conversation. While one runs, it shows a single line above the editor (`◆ background · code-review 56b57550 · Find 6/10 · 12s`) instead of the full live widget. When the run completes or fails, the extension waits for the originating session to become idle and inserts one concise `workflow-result` message containing the run ID, terminal state, and summary. Delivery state is stored with the run record; on reload, the extension checks both that state and the session message history before retrying, which suppresses duplicates even if persistence failed immediately after message insertion.
296
+ Background runs are detached from the initiating tool-call signal, so the host agent can finish that turn and continue the conversation. While one runs, it shows a single line above the editor (`◆ background · code-review 56b57550 · Find 6/10 · 12s`) instead of the full live widget. When the run completes or fails, the extension waits for the originating session to become idle and inserts one concise `workflow-result` message containing the run ID, terminal state, and summary, followed by the run's files: `Record:`, `Script:` for an inline run, and `Journal:` with the reminder to read it before diagnosing an empty or unexpected result, so a later turn knows where the retained result lives. Delivery state is stored with the run record; on reload, the extension checks both that state and the session message history before retrying, which suppresses duplicates even if persistence failed immediately after message insertion.
297
+
298
+ If Dynamax was active when the host agent started the run (sticky mode, or the turn a `dynamax` one-shot covered), the delivery also starts a new agent turn, so the agent wakes to read the result and choose its next step without waiting for you; with sticky mode on, that turn carries the sticky `<dynamax>` section. pi gives the model only a message's text, so a waking delivery carries the full result in place of the summary, rendered and bounded exactly as a synchronous `workflow` call returns it (a review's findings, every other result field, and the usage line), or the full retained message of a failed, stopped, or paused run, before the same file lines. The run record keeps this as `background.origin.wakeOnComplete`, so a delivery after a restart, including the notice for a run paused by shutdown, still wakes the agent, and a resume, restart, or provider-limit auto-resume of that run wakes it too. A run you stopped does not wake the agent, nor does a provider-limit pause whose automatic resume is scheduled: that resume's own delivery wakes it. Other background runs keep the silent delivery above. There is no separate flag: Dynamax being active is the signal that the agent is orchestrating.
265
299
 
266
300
  Provider usage-window continuation is a separate, explicit background option:
267
301
 
@@ -275,7 +309,7 @@ Provider usage-window continuation is a separate, explicit background option:
275
309
  }
276
310
  ```
277
311
 
278
- It is off by default and schedules only registered file workflows whose invocation had no arguments; inline scripts and argument-bearing runs still pause safely but require an explicit manual decision because their executable input is not persisted. A recognized provider usage or quota failure aborts the current fan-out before queued agents can open new sessions, preserves the replay journal, and records a sanitized provider message, provider identity, reset hint, total attempt number, and next eligible time. Transport failures and ordinary provider errors keep the existing per-agent `agentRetries` behavior and do not create a provider-limit pause.
312
+ It is off by default and schedules only registered file workflows and inline runs whose script was persisted, and only when the invocation had no arguments; argument-bearing runs still pause safely but require an explicit manual decision because their arguments are not persisted. A recognized provider usage or quota failure aborts the current fan-out before queued agents can open new sessions, preserves the replay journal, and records a sanitized provider message, provider identity, reset hint, total attempt number, and next eligible time. Transport failures and ordinary provider errors keep the existing per-agent `agentRetries` behavior and do not create a provider-limit pause.
279
313
 
280
314
  Reset parsing accepts provider duration hints such as `retry-after: 60` and `x-ratelimit-reset-tokens: 6m0s`, plus future RFC3339 timestamps. Missing, past, or malformed hints use a 60-second fallback; delays are clamped to at least 5 seconds and at most `usageLimitMaxDelayMs`. The default maximum is three total run attempts, including the initial run. When the eligible time arrives, the extension starts a new background run with `resumeFromRunId`; after a process restart, it re-arms only the remaining delay. `/workflow:runs resume` and `/workflow:runs stop` cancel any pending timer, and the attempt cap prevents an infinite resume loop.
281
315
 
@@ -285,7 +319,7 @@ A graceful pi session shutdown aborts active background work and records it as `
285
319
 
286
320
  ### Recent runs and lifecycle actions
287
321
 
288
- `/workflow:runs` opens a runs browser in the TUI: one aligned row per recent project run (state glyph and word, workflow, duration, age, cost when known, short run ID) above a details pane for the selected run with its wall-clock times, phases, agent and token counts, resume lineage, pause details, outcome, and why a relaunch is unavailable when it is. Keys act on the selected run: `enter` or `i` inspect, `r` resume, `s` stop (press twice), `R` restart; only the actions currently valid for that run are listed in the footer, and the others explain why they are unavailable. The browser refreshes while a run is active and reopens on the same run after an action. RPC sessions keep pi's native two-step selection (run, then action). The background activity line is present only while this session owns active background work.
322
+ `/workflow:runs` opens a runs browser in the TUI: one aligned row per recent project run (state glyph and word, workflow, tagged `inline` when the run was an inline script, duration, age, cost when known, short run ID) above a details pane for the selected run with its wall-clock times, phases, agent and token counts, resume lineage, pause details, outcome, and why a relaunch is unavailable when it is. Keys act on the selected run: `enter` or `i` inspect, `r` resume, `s` stop (press twice), `R` restart; only the actions currently valid for that run are listed in the footer, and the others explain why they are unavailable. The browser refreshes while a run is active and reopens on the same run after an action. RPC sessions keep pi's native two-step selection (run, then action). The background activity line is present only while this session owns active background work.
289
323
 
290
324
  The same operations are available without the selection UI:
291
325
 
@@ -297,7 +331,7 @@ The same operations are available without the selection UI:
297
331
  /workflow:inspector <run-id>
298
332
  ```
299
333
 
300
- Inspect is always safe. Stop is available for a background run active in the current session and for a retained paused run with a pending resume timer. Resume is limited to paused registered workflows whose source fingerprint is unchanged and whose invocation had no redacted arguments; it starts a new background run ID with `resumeFromRunId` journal replay. A paused run already resumed by a queued, running, paused, or completed run is listed as `resumed as <run-id>` and offers no second resume; once every resume of it has failed or been stopped, manual resume is offered again, while automatic provider-limit resume never fires twice for the same paused run. Restart is limited to completed, failed, or stopped registered workflows that had no arguments and also creates a new background run ID. Inline workflows and argument-bearing invocations are intentionally not relaunched because their executable input was not persisted. In `print` and `json` modes, list and inspect return formatted text without opening TUI components, while resume/restart report that background execution requires TUI or RPC mode.
334
+ Inspect is always safe. Stop is available for a background run active in the current session and for a retained paused run with a pending resume timer. Resume is limited to paused runs whose source fingerprint is unchanged and whose invocation had no redacted arguments; it starts a new background run ID with `resumeFromRunId` journal replay. A paused run already resumed by a queued, running, paused, or completed run is listed as `resumed as <run-id>` and offers no second resume; once every resume of it has failed or been stopped, manual resume is offered again, while automatic provider-limit resume never fires twice for the same paused run. Restart is limited to completed, failed, or stopped runs that had no arguments and also creates a new background run ID. A registered workflow relaunches by name; an inline run relaunches from its persisted `<run-id>.script.js`, so it is relaunchable only while that script is retained, and fails with a clear message if the file was deleted. Because `.pi/.workflow-runs/` sits in the project, a cloned repository could ship a record and script of its own (the `*` ignore file does not untrack committed files), so the engine seals each inline run: its record holds an HMAC of the run ID and script fingerprint under a key that pi creates once per install at `~/.pi/agent/workflow-run-seal.key` (inside `PI_CODING_AGENT_DIR` when set). Before reading the script, a resume or restart checks the seal against this install's key, then requires the script's fingerprint to match the record; a record from elsewhere, or a script edited in place since the run, is refused with a hint to rerun it with the `workflow` tool's `scriptPath`. A run of a saved `.pi/workflows` or user script is sealed the same way, without a copy: it relaunches by name only while the script still matches the content that ran, since a repository could also ship a record naming a script of its own; otherwise run it again by name. A record without a seal, including every inline record written before sealing existed, offers neither action. Inline runs are listed as `<name> (inline)` and tagged `inline` in the browser, so such a record cannot pass for a registered workflow's run. Argument-bearing invocations are intentionally not relaunched because their arguments were not persisted. In `print` and `json` modes, list and inspect return formatted text without opening TUI components, while resume/restart report that background execution requires TUI or RPC mode.
301
335
 
302
336
  Code-review findings are rendered as a formatted result message by default. pi no longer asks whether to open the findings viewer. Use `--result-viewer` or `--review-viewer` when you want to inspect findings interactively. The viewer lists findings most severe first with the selected finding's full detail beside the list (below it in narrow terminals); `space` or `enter` selects a finding, `a` toggles all, `f` generates patch previews and `c` posts PR comments for the selection, and `pgup`/`pgdn` scroll a long detail. The viewer is sized to its content and shows scroll hints only when something is clipped. `/workflow:results` or `ctrl+shift+r` reopens the most recent validated code-review report in the current pi session without rerunning the workflow; selections reset when it reopens.
303
337
 
@@ -320,6 +354,8 @@ With edited-workflow resume enabled, unchanged calls are reused while changed or
320
354
 
321
355
  This option can return a result produced before an orchestration edit when that call's explicit identity did not change. Use it only when the unchanged call is still semantically valid in the edited workflow, and prefer stable `cacheKey` values for repeated logical calls. Omit `--resume-edited` or `resumeEditedWorkflow` for the fail-closed default.
322
356
 
357
+ A tool call with `scriptPath` and `resumeFromRunId` implies `resumeEditedWorkflow: true`, because rerunning an edited script file is what `scriptPath` is for. Only the whole-script fingerprint check is relaxed; each call's own replay identity still decides whether its cached result is reused.
358
+
323
359
  Replay is explicit for agents that share the workflow directory and automatic for isolated patch-producing agents:
324
360
 
325
361
  | Agent call | Default during resume | Override |
@@ -333,9 +369,9 @@ Journals use replay contract v2. A hit requires the same prompt/options and exec
333
369
 
334
370
  Cached values are revalidated before use: text must still be text, structured output must satisfy the current typebox schema, and an isolated patch must have consistent metadata and pass `git apply --check --binary` against its fresh baseline worktree. A malformed or stale value runs live.
335
371
 
336
- Invalidations appear in progress output with a concise reason. Legacy v1 journals and early v2 entries without effective-session identity are parsed for compatibility but always miss. Git repositories with no commits and non-Git directories remain supported. Git control data and engine-owned workflow journals cannot be declared as inputs; if a Git-visible or explicit surface exceeds safety bounds, that call runs live.
372
+ Invalidations appear in progress output with a concise reason. Legacy v1 journals and early v2 entries without effective-session identity are parsed for compatibility but always miss. Journals written by pi-workflow-engine 1.1.0 or earlier miss once with `effective system prompt changed`, because later versions append the return-value instruction to every subagent's system prompt; the resumed run records fresh entries. Git repositories with no commits and non-Git directories remain supported. Git control data and engine-owned workflow journals cannot be declared as inputs; if a Git-visible or explicit surface exceeds safety bounds, that call runs live.
337
373
 
338
- Statically loaded built-ins fingerprint and revalidate their bounded extension source tree so imported helper changes invalidate replay, while inline workflows use the compiler-provided script fingerprint. Dynamically discovered and programmatic modules are intentionally non-replayable because their transitive runtime imports or captured closures cannot be bound to an immutable source snapshot.
374
+ Statically loaded built-ins fingerprint and revalidate their bounded extension source tree so imported helper changes invalidate replay, while inline workflows and saved `.js` scripts use the compiler-provided script fingerprint. Dynamically discovered `.ts` and programmatic modules are intentionally non-replayable because their transitive runtime imports or captured closures cannot be bound to an immutable source snapshot.
339
375
 
340
376
  Resume only caches completed `agent()` calls. It does not snapshot arbitrary workflow local variables, in-flight tool work, environment or clock state, external services, or generated inputs outside the captured repository/workflow/skill/tool surfaces. Use `resume: "off"` for calls that depend on those values, and keep prompts and options deterministic when you want cache hits.
341
377
 
@@ -351,6 +387,11 @@ import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
351
387
  export const meta: WorkflowMeta = {
352
388
  name: "my-workflow",
353
389
  description: "Find and summarize something important.",
390
+ whenToUse: "Use when the user wants the important thing found and summarized; args name the target.",
391
+ phases: [
392
+ { title: "Find", profile: "small" },
393
+ { title: "Synthesize", profile: "medium" },
394
+ ],
354
395
  };
355
396
 
356
397
  const Finding = Type.Object({
@@ -389,6 +430,8 @@ export default async function run({ agent, parallel, phase, args }: WorkflowApi)
389
430
  }
390
431
  ```
391
432
 
433
+ `meta.name` is required. `description` and `whenToUse` describe the workflow in the picker and in the host's workflow catalog, so write `whenToUse` as the situation that should make a model choose it and what `args` it expects. `phases` declares the progress phases up front; each may add a default `profile` for agents that pass that phase title as their `phase` option.
434
+
392
435
  Core primitives:
393
436
 
394
437
  | Primitive | What it does |
@@ -401,6 +444,10 @@ Core primitives:
401
444
  | `progress(event)` | Emits counters, summaries, and lane items. |
402
445
  | `workflow(ref, args?)` | Runs another workflow inline as a sub-step and returns its result. |
403
446
 
447
+ Every subagent's system prompt states that its return value goes to the workflow program as data, not to a person: a text agent's final message is returned verbatim, so it replies with only the output its task asks for (no greeting, preamble, narration, or offer of further help), and a schema agent is told the same about its `final_answer` arguments. The instruction is appended after any `APPEND_SYSTEM.md` pi discovers, which subagents keep loading.
448
+
449
+ `args` holds the invocation's arguments as text and `input` holds them verbatim. A slash command passes a string, so both are that string. The `workflow` tool's `args` field and `workflow(ref, args)` also accept any JSON value: `input` is the value and `args` its JSON text, so workflows written for string arguments keep working. Omitted arguments are `""` in both. Like string arguments, JSON arguments are never written to the run record, so a run that had any cannot be resumed or restarted from `/workflow:runs`.
450
+
404
451
  Schema agents accept structured output only through the validated, terminating `final_answer` tool. If the first response omits it, the engine makes at most **two repair attempts** (three prompt attempts total). Repair prompts expose only `final_answer` when the active pi session supports tool restriction. Clean exhaustion throws `WorkflowStructuredOutputError` with code `WORKFLOW_STRUCTURED_OUTPUT_NONCOMPLIANCE`; provider failures and host cancellation still propagate unchanged.
405
452
 
406
453
  Use settled mode when the workflow must distinguish a successful `null` from a failed branch. It preserves input order and returns `{ ok: true, value }` or `{ ok: false, error: { name?, message, code?, details? } }` for every thunk. Default parallel mode still turns recoverable structured-output exhaustion into a `null` slot, while settled mode retains the typed code and attempt details. Genuine workflow cancellation still rejects the whole `parallel()` call and cancels siblings.
@@ -427,8 +474,8 @@ overrides its profile.
427
474
 
428
475
  Subagents are SDK sessions, so pi's built-in codemode and MCP extensions do not load in them. Two opt-ins cover that:
429
476
 
430
- - **Codemode.** Add `"codemode"` to an agent's `tools` to let it batch or filter many tool calls in one script, for example `tools: ["read", "grep", "codemode"]`. Scripts can call only that agent's other allowed tools, and never `final_answer`. Classifier calls a script makes count toward the run's usage and budget.
431
- - **Host MCP tools.** In runs started by the `workflow` tool, subagents reach the host's MCP tools over its existing connections and through its permission handlers. Name a tool in `tools` (for example `"mcp__linear__create_issue"`), or use `toolHints: ["external-search"]` to pick up MCP web research tools; servers that declare a tool not read-only or closed-world are excluded from that hint. Bridged tools act in the host session, so the local `search` hint never bridges them, and an isolated worktree agent that names a local MCP tool sees the main checkout through it, not its worktree. `/workflow` commands and background runs outlive the host tool call that makes this possible: an agent there that names an MCP tool fails before prompting rather than running without it.
477
+ - **Codemode.** Add `"codemode"` to an agent's `tools` to let it batch or filter many tool calls in one script, for example `tools: ["read", "grep", "codemode"]`. Scripts can call only that agent's other allowed tools, and never `final_answer`. Classifier calls a script makes count toward the run's usage and budget. pi runs at most two codemode scripts per process, shared with the host, and fails a third instead of waiting; subagents' codemode calls therefore queue for one of those two slots, so a wide fan-out of codemode agents runs their scripts two at a time. A codemode agent runs its tool calls one at a time, and so do its scripts' own calls: by default pi checks every call in a message before it starts any, and a codemode call takes its slot in that check, so three codemode calls in one message would hold both slots with calls that cannot start while the third waits for one. To batch calls, put them in one script, which still makes them all in a single model turn. pi frees a slot only once the script's worker has exited, which can lag its result, so an agent can occasionally still get pi's "Codemode worker limit reached" error and retry. The host's own codemode calls do not queue.
478
+ - **Host MCP tools.** In runs started by the `workflow` tool, subagents reach the host's MCP tools over its existing connections and through its permission handlers. Name a tool in `tools` (for example `"mcp__linear__create_issue"`), or use `toolHints: ["external-search"]` to pick up MCP web research tools; servers that declare a tool not read-only or closed-world are excluded from that hint. Bridged tools act in the host session, so the local `search` hint never bridges them, and an isolated worktree agent that names a local MCP tool sees the main checkout through it, not its worktree. `/workflow` commands and background runs outlive the host tool call that makes this possible: an agent there that names an MCP tool fails before prompting rather than running without it. So that a run started early in a session does not race servers that are still connecting, the `workflow` tool tells pi (1.2 or newer) which servers a call may reach, and pi waits for them before the run starts: the servers of the `mcp__…` tools an inline script names, or every server for a named workflow, a script that uses `external-search` hints or `workflow()`, or any `scriptPath`. pi asks before the call runs and on its main thread, so a `scriptPath` file is not read then; the call itself reads it only if it is a regular file of at most 1 MiB. A background call waits for none. Each server becomes ready once, so the wait costs at most one pause at session start.
432
479
 
433
480
  Subagents receive no skills by default. Opt in per agent with `skills: ["skill-name"]`; if `tools` is also restricted, the engine automatically keeps `read` available so the subagent can load the selected `SKILL.md`. When `skills` is omitted, clear prompt text such as `/skill:name`, `include skill name`, or `use the name skill` is also treated as an opt-in. Pass `skills: []` to suppress that inference.
434
481
 
@@ -457,16 +504,17 @@ Cleanup is a required finalizer. The engine attempts every registered worktree r
457
504
 
458
505
  ### Compose workflows
459
506
 
460
- `workflow(ref, args?)` runs a registered workflow by name as a sub-step, returning its result. The child shares the parent run's concurrency cap, total-agent limit, per-agent timeout, abort signal, and perf timing, and its phases nest under `<name> ▸ <phase>` in the live UI.
507
+ `workflow(ref, args?)` runs a registered workflow by name, or an inline-format script file via `{ scriptPath }`, as a sub-step, returning its result. `args` is a string or any JSON value, which the child reads as `input` and, as text, `args`. The child shares the parent run's concurrency cap, total-agent limit, per-agent timeout, abort signal, and perf timing, and its phases nest under `<name> ▸ <phase>` in the live UI.
461
508
 
462
509
  ```ts
463
510
  export default async function run({ workflow }: WorkflowApi) {
464
511
  const review = await workflow("code-review", "HEAD~3");
465
- return { review };
512
+ const audit = await workflow({ scriptPath: "scripts/audit.js" }, { paths: ["src/"], strict: true });
513
+ return { review, audit };
466
514
  }
467
515
  ```
468
516
 
469
- Nesting is one level only: calling `workflow()` from inside a sub-workflow rejects. Resolution throws on an unknown name. Inside `parallel()` or `pipeline()`, recoverable branch errors become `null` results, so filter nulls before synthesis; a genuine run abort still rejects.
517
+ A `scriptPath` is resolved against the workflow cwd and must name a regular file of at most 1 MiB; the file follows the [inline workflow rules](#author-an-inline-workflow) and is fingerprinted by content like an inline script. Nesting is one level only: calling `workflow()` from inside a sub-workflow rejects. Resolution throws on an unknown name, an unreadable `scriptPath`, or a script that does not compile. Inside `parallel()` or `pipeline()`, recoverable branch errors become `null` results, so filter nulls before synthesis; a genuine run abort still rejects.
470
518
 
471
519
  ### Classify between stages
472
520
 
@@ -495,12 +543,19 @@ Guaranteed built-ins are statically registered:
495
543
  2. Import it in `.pi/extensions/pi-workflow-engine/src/workflows.ts`.
496
544
  3. Add a `defineBuiltinWorkflow(mod, "<name>.ts")` entry to `BUILTIN_WORKFLOW_DEFINITIONS`.
497
545
 
546
+ Saved scripts are workflows in the [inline script format](#author-an-inline-workflow) kept as `*.js` files:
547
+
548
+ - `<project>/.pi/workflows/*.js`: project workflows, shared through the repository.
549
+ - `~/.pi/agent/workflows/*.js` (pi's configured agent directory): user workflows, next to the `.ts` drop-ins.
550
+
551
+ They compile like an inline `script`, with the injected `Type` and no imports, so they share pi's typebox, unlike `.ts` drop-ins, and they are fingerprinted by content, so `--resume` and `/workflow:runs` resume and restart them like built-ins. Project scripts load only when pi reports the project as trusted (`ctx.isProjectTrusted()`). pi itself does not ask for trust because of `.pi/workflows` alone, so a folder with no other protected project resources counts as trusted; review a repository's `.pi/workflows` before running them. Files starting with `_` or `.` and scripts that do not compile are skipped with a stderr note.
552
+
498
553
  Drop-in workflows are also discovered best-effort from:
499
554
 
500
555
  - `~/.pi/agent/workflows/*.ts`: the user-level drop-in directory, which survives `pi update`.
501
556
  - The extension's own `workflows/*.ts` directory: `.pi/extensions/pi-workflow-engine/workflows/` in a working copy or a project-vendored copy. Files added to an installed package are replaced by `pi update`.
502
557
 
503
- Use `/workflow <name> --refresh` after adding a drop-in file.
558
+ When two sources define the same name, the first wins: built-ins, project scripts, the extension's `.ts` drop-ins, user scripts, then user `.ts` drop-ins. Saved scripts are read again on every lookup, so a script added or edited mid-session runs by name (tool `name`, `/workflow <name>`, `workflow("<name>")`, `/workflow:runs`) with its current content at once. The `<workflows>` catalog stays as it was built for the session, to keep the provider's prompt cache, so a new script joins it from the next session. `.ts` drop-ins are loaded once per process: use `/workflow <name> --refresh` after adding or editing one.
504
559
 
505
560
  ## Author an inline workflow
506
561
 
@@ -527,6 +582,7 @@ Inline rules:
527
582
 
528
583
  - start with `export const meta = { ... }`;
529
584
  - default-export an async workflow function;
585
+ - write plain JavaScript: the script compiles in-process, so TypeScript annotations do not parse;
530
586
  - no imports or dynamic `import()`;
531
587
  - use the injected `Type` object for schemas;
532
588
  - request `profile: "small"`, `"medium"`, or `"big"` instead of hard-coding provider models when the stage is portable;
@@ -535,6 +591,10 @@ Inline rules:
535
591
 
536
592
  Inline workflows run in-process with extension permissions, so treat them as trusted task automation.
537
593
 
594
+ The host agent's reference for writing these scripts is the bundled `workflow-authoring` skill (`skills/workflow-authoring/SKILL.md`). The `workflow` tool's guidelines stay short (the opt-in rule, the target and `Type` rules, profiles, the worktree return shape, host MCP reach, and the run-mode flags) and tell the model to read the skill before it writes or edits a script. The skill covers the full API, why fan-out uses `api.parallel` rather than `Promise.all` and defaults to `api.pipeline`, verification and completeness patterns, budget loops, worktree edits, resume, and saving a reusable workflow.
595
+
596
+ The tool also takes `scriptPath` in place of `script`: a regular file of at most 1 MiB holding an inline script, relative to the working directory. Each tool call gives exactly one of `name`, `script`, or `scriptPath`. Every inline run saves its script as `.pi/.workflow-runs/<run-id>.script.js` and names it in the result, so iterating does not need the script pasted again: edit that file in place, then call the tool with `scriptPath` set to it and `resumeFromRunId` set to the previous run. Replayable agent calls (see [Resume a run](#resume-a-run)) that the edit left unchanged are reused; edited and new calls run live. Edit the persisted file rather than a copy: `.pi/.workflow-runs/` is excluded from the repository state that shared `resume: "read-only"` calls are bound to, while editing any other copy inside the repository, or a saved `.pi/workflows/*.js` script, changes that state, so every shared read-only call misses and runs live.
597
+
538
598
  ## Runtime knobs
539
599
 
540
600
  Only tune these when a workflow is too slow, too expensive, or too noisy:
@@ -543,10 +603,10 @@ Only tune these when a workflow is too slow, too expensive, or too noisy:
543
603
  | --- | --- |
544
604
  | `--concurrency=N` / `PI_WORKFLOW_CONCURRENCY=N` | Cap concurrent subagents. Default is `min(8, max(2, CPU count))`. |
545
605
  | `--parallel-limit=N` / `PI_WORKFLOW_PARALLEL_SUBMISSION_LIMIT=N` | Limit eager `parallel()` submission. |
546
- | `--max-agents=N` / `PI_WORKFLOW_MAX_AGENTS=N` | Cap live model calls across the complete run, including sub-workflows. Default `64`; values are clamped to `1`–`10000`. Replay hits do not consume the cap. The `workflow` tool field is `maxAgents`. |
606
+ | `--max-agents=N` / `PI_WORKFLOW_MAX_AGENTS=N` | Cap live model calls across the complete run, including sub-workflows. Default `64`; values are clamped to `1`–`10000`. Replay hits do not consume the cap. The `workflow` tool field is `maxAgents`. This is independent of the `/workflow:size` prompt guideline. |
547
607
  | `--agent-timeout-ms=N` / `PI_WORKFLOW_AGENT_TIMEOUT_MS=N` | Abort one live agent after this duration. Default `1800000` (30 minutes); values are clamped to `1000`–`86400000`. The session is aborted and its isolated worktree is cleaned up. The `workflow` tool field is `agentTimeoutMs`. |
548
608
  | `--agent-retries=N` / `PI_WORKFLOW_AGENT_RETRIES=N` | Restart the whole agent after a transient provider or transport failure that pi's in-session turn retry could not recover. Default `0`; values are clamped to `0`–`10`. Backoff starts at one second, doubles, and caps at 30 seconds. The `workflow` tool field is `agentRetries`. |
549
- | `--budget=N` / `PI_WORKFLOW_BUDGET=N` | Set an output-token ceiling across recorded subagent attempts, including failed provider attempts. `agent()` throws `WorkflowBudgetExceededError` before starting another model request once the ceiling is reached; agents already running may overshoot because the engine does not reserve per-agent estimates. |
609
+ | `--budget=N` / `PI_WORKFLOW_BUDGET=N` | Set an output-token ceiling across recorded subagent attempts, including failed provider attempts. `agent()` throws `WorkflowBudgetExceededError` before starting another model request once the ceiling is reached; agents already running may overshoot because the engine does not reserve per-agent estimates. The `workflow` tool has no budget field; see [Turn budget directive](#turn-budget-directive). |
550
610
  | `--perf` / `PI_WORKFLOW_PERF=1` | Include internal timing aggregates. Usage/cost totals are reported separately from perf. |
551
611
  | `PI_WORKFLOW_LANE_ITEM_LIMIT=N` | Cap retained progress lane items. |
552
612
 
@@ -559,6 +619,40 @@ and abort scope. Usage from failed attempts is retained, so it can exhaust the
559
619
  budget before the next retry begins. Host cancellation takes precedence over
560
620
  limit and timeout failures.
561
621
 
622
+ ### Turn budget directive
623
+
624
+ Put a standalone `+N` token in a prompt to give that turn's workflow runs one
625
+ shared output-token pool:
626
+
627
+ ```text
628
+ dynamax +500k review this branch and verify every finding
629
+ +1.5m map every caller of the old API, then propose the migration
630
+ ```
631
+
632
+ The token takes `k` or `m` suffixes in either case (`+500k`, `+1.5m`, `+2M`) or
633
+ a plain count (`+250000`). It must follow the start of the prompt or whitespace
634
+ and end at whitespace, punctuation, or the end, so `C++`, `a+500k`, or `+500kb`
635
+ do not count, and values under 1,000 tokens are ignored so a `+1` in ordinary
636
+ prose never caps a run. The first such token in a typed prompt wins; prompts
637
+ that extensions send are not read.
638
+
639
+ While the pool is active, every `workflow` tool call runs with the pool's
640
+ remainder as its budget (clamped to the run budget range), and each
641
+ synchronous run's output tokens are subtracted once it finishes, failed or not.
642
+ When less than one token remains, the tool refuses the call and tells the agent
643
+ the turn's budget is spent. The pool clears when the run that
644
+ received the prompt settles, so it still covers the run after pi retries a
645
+ provider error or compacts an overflowing context within that run.
646
+
647
+ The `workflow` tool has no `budget` field, so the host agent cannot choose a
648
+ ceiling: one it guesses can run out before a run's final stage. Without a pool,
649
+ a tool run takes `PI_WORKFLOW_BUDGET` if it is set and is otherwise unbudgeted.
650
+
651
+ Background runs receive the current remainder as their budget but do not draw
652
+ the pool down, because their usage lands after the turn. Parallel `workflow`
653
+ calls in one assistant message each see the same remainder before any of them
654
+ finishes. Per-run budget accounting is unchanged either way.
655
+
562
656
  Transient provider failures are retried in two layers. First, each subagent
563
657
  inherits pi's session-level automatic retry from your pi `retry` settings
564
658
  (enabled by default: 3 retries with 2s, 4s, 8s backoff). It retries the failed
@@ -601,6 +695,13 @@ fails, the result retains verified records and reports incomplete coverage.
601
695
  Synthesis selects or merges IDs; the workflow reconstructs evidence-bearing
602
696
  fields from those records.
603
697
 
698
+ Each lens verifies at most the per-lens candidate count its finder prompt
699
+ states (six per `code-review` angle, four or five for the other workflows).
700
+ When a finder returns more, the workflow logs how many it cut from that lens.
701
+ `stats.candidates` counts everything the finders returned, and `stats.dropped`
702
+ counts the candidates never verified: those over a lens cap, duplicates, and
703
+ out-of-scope candidates.
704
+
604
705
  ### Duplicate candidates
605
706
 
606
707
  Lenses often report the same defect in different words. When a TypeSafe Jev
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-workflow-engine",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "type": "module",
5
5
  "description": "Zero-dependency Dynamax workflows for pi: live parallel subagents, typed handoffs, synthesized results.",
6
6
  "keywords": [
@@ -29,8 +29,12 @@
29
29
  "LICENSE"
30
30
  ],
31
31
  "pi": {
32
- "extensions": [".pi/extensions/pi-workflow-engine/index.ts"],
33
- "skills": ["skills"],
32
+ "extensions": [
33
+ ".pi/extensions/pi-workflow-engine/index.ts"
34
+ ],
35
+ "skills": [
36
+ "skills"
37
+ ],
34
38
  "image": "https://raw.githubusercontent.com/timbrinded/pi-workflow-engine/master/assets/preview.png"
35
39
  },
36
40
  "scripts": {
@@ -55,7 +59,12 @@
55
59
  "typebox": "*"
56
60
  },
57
61
  "devDependencies": {
58
- "@types/node": "^22.20.0",
62
+ "@types/node": "^22.20.5",
59
63
  "typescript": "^7.0.2"
64
+ },
65
+ "allowScripts": {
66
+ "esbuild": true,
67
+ "protobufjs": true,
68
+ "@google/genai": true
60
69
  }
61
70
  }
@@ -0,0 +1,162 @@
1
+ ---
2
+ name: workflow-authoring
3
+ description: "Reference for writing pi-workflow-engine workflow scripts: the inline script contract, the injected api (agent, parallel, pipeline, classify, budget, workflow), quality patterns, resume, and saving a reusable workflow. Read before writing or editing an inline script for the workflow tool. It does not itself authorize running a workflow."
4
+ ---
5
+
6
+ # Writing workflow scripts
7
+
8
+ Read this before you write or edit a script for the `workflow` tool. It does not grant permission to run one: call the tool only when the user opted in (the `dynamax` token, sticky `/workflow:dynamax`, an explicit request to run or author a workflow, or a command or skill instruction).
9
+
10
+ A workflow is plain code that fans work out to subagents, passes validated data between stages, and returns one result. Prefer a registered workflow (`name`) when one fits; write a `script` when none does.
11
+
12
+ ## Script contract
13
+
14
+ ```js
15
+ export const meta = {
16
+ name: "auth-audit",
17
+ description: "Audit the auth middleware for bypasses.",
18
+ phases: [{ title: "Find" }, { title: "Verify" }],
19
+ };
20
+
21
+ export default async function run(api) {
22
+ const Finding = Type.Object({ file: Type.String(), summary: Type.String() });
23
+ const findings = await api.agent(`Find auth bypasses in ${api.args || "src/"}`, {
24
+ label: "find", phase: "Find", profile: "small", schema: Type.Object({ items: Type.Array(Finding, { maxItems: 20 }) }),
25
+ });
26
+ return { summary: `${findings.items.length} candidate bypasses`, findings: findings.items };
27
+ }
28
+ ```
29
+
30
+ - The script is plain JavaScript compiled in-process: no TypeScript annotations, `as` casts, or interfaces.
31
+ - It must start with `export const meta = { ... };`, a pure literal: strings, numbers, booleans, `null`, arrays, and objects only. No variables, calls, template literals, spreads, computed keys, or raw newlines inside strings. `name` is required; `description`, `whenToUse` (the situation that should make a model pick it), and `phases` are optional. Each phase is `{ title, profile? }`.
32
+ - Exactly one `export default` follows the meta: an `async function` or `async (api) => { ... }` with a block body. Nothing may follow it but `;` and comments. Put helpers and constants inside the function body.
33
+ - No `import` statements, no dynamic `import()`, no other exports.
34
+ - `Type` (TypeBox) is an injected global: write `Type.Object(...)` directly. It is not a property of `api`. Using the injected `Type` keeps schemas on pi's bundled TypeBox, so validation works.
35
+ - The return value becomes the tool result. Return a string or an object with a short `summary` first; the result the host model sees is truncated when it is very large, so return conclusions, not raw transcripts.
36
+
37
+ ## The api
38
+
39
+ | Member | What it does |
40
+ | --- | --- |
41
+ | `agent(prompt, opts?)` | Runs one subagent session. `prompt` must be a plain string (build it with a template string first). Without `schema` it resolves to the final assistant text, a string. With `schema` it resolves to an object already validated against the schema. |
42
+ | `parallel(thunks)` | Runs `() => Promise` thunks concurrently and waits for all. A recoverable failure becomes a `null` slot; the other slots keep their results. |
43
+ | `parallel(thunks, { settled: true })` | Same, but every slot is `{ ok: true, value }` or `{ ok: false, error: { name?, message, code?, details? } }`, in input order. |
44
+ | `pipeline(items, ...stages)` | Runs every item through all stages independently. Each stage is `(prev, item, index) => Promise`; the first stage receives the item as `prev`. A failed stage turns that item's slot into `null` and skips its later stages. |
45
+ | `classify(context, opts?)` | Answers typed `choice`/`score`/`bool` questions about JSON `state` with a classifier model and no chat session. |
46
+ | `workflow(nameOrRef, args?)` | Runs another workflow as a sub-step and returns its result: a registered workflow by name, or an inline-format script file via `{ scriptPath }`, relative to the workflow cwd. `args` is a string or any JSON value, which the child reads as `input` and `args`. One level of nesting only. Rejects on an unknown name, an unreadable `scriptPath`, or a script that does not compile. |
47
+ | `phase(title)` / `log(message)` / `progress(event)` | Drive the live progress tree, logs, counters, and lanes. |
48
+ | `budget` | `{ total, spent(), remaining() }` in output tokens; `total` is `null` when the run has no budget. |
49
+ | `agents` | `{ max, remaining() }`: the run's live-agent limit and the admissions left before `agent()` throws. It is a live reading, not a reservation: agents running alongside, and retries, take admissions too, and journal replays take none, so a resumed run reads more. |
50
+ | `input` | The arguments verbatim: the `/workflow` command's string, or the JSON value the tool call or `workflow()` passed; `""` when none was given. |
51
+ | `args`, `cwd`, `signal` | The arguments as text (a string as is, any other JSON value as JSON text, `""` when none), the host working directory, and the run's abort signal. |
52
+
53
+ Agent options:
54
+
55
+ | Option | Meaning |
56
+ | --- | --- |
57
+ | `profile` | `"small"`, `"medium"`, or `"big"`: the user's central model routes. Set one on every agent. |
58
+ | `label`, `phase` | Row label and phase group in the progress tree. |
59
+ | `schema` | TypeBox schema; the agent must answer through a validated `final_answer` tool. Two repair attempts, then the call rejects with code `WORKFLOW_STRUCTURED_OUTPUT_NONCOMPLIANCE`. |
60
+ | `tools` | Strict allowlist of tool names, for example `["read", "bash", "grep", "find", "ls"]`. `[]` gives a tool-free agent. |
61
+ | `toolHints` | `["search"]` adds installed grep/find/code-search tools; `["external-search"]` adds web search, browsing, and URL-extraction tools. `requireToolHints: true` fails before prompting when a hint matched nothing. |
62
+ | `skills` | Pi skills to expose to this subagent; none by default. |
63
+ | `isolation: "worktree"` | Run in a disposable git worktree and return the patch (see below). |
64
+ | `model`, `thinkingLevel` | Intentional per-call overrides of the profile. `model` is `"provider/id"`. |
65
+ | `resume`, `resumeInputs`, `cacheKey` | Replay controls (see Resume). |
66
+
67
+ Add `"codemode"` to an agent's `tools` when it should batch or filter many tool calls in one script; those scripts can call only the agent's other allowed tools. Host MCP tools reach subagents only in synchronous `workflow` tool runs: name them in `tools` (for example `"mcp__linear__create_issue"`) or use `toolHints: ["external-search"]`. Background runs and `/workflow` commands cannot bridge them, and an agent that names one fails before prompting.
68
+
69
+ ## Profiles, labels, and phases
70
+
71
+ - Set `profile` on every `agent()` call: `small` for scouting, finding, and verifying; `medium` for judgement and synthesis; `big` for the hardest reasoning. Use `model`/`thinkingLevel` only for a deliberate override.
72
+ - A phase declared as `{ title: "Verify", profile: "small" }` routes agents that pass `phase: "Verify"` and set neither `profile` nor `model`. Routing order: the agent's `model`, its `profile`, its phase's `profile`, then the host model.
73
+ - Inside `parallel` and `pipeline`, pass `phase` and `label` in each agent's options. The global `phase()` is one shared value that every concurrent branch reads and writes, so calling it inside fan-out branches races and misfiles rows. Call `phase()` only between sequential stages.
74
+
75
+ ## Concurrency rules
76
+
77
+ **Use `api.parallel`, never `Promise.all` or `Promise.allSettled` over agents.** `api.parallel` turns a recoverable failure (schema exhaustion, an agent timeout, the budget or agent limit, a provider error) into a `null` slot and the survivors continue; `Promise.all` rejects the whole stage on the first failure and discards every sibling result. `api.parallel` also applies the run's submission limit and cancels siblings on a genuine abort. Filter the nulls before using the results:
78
+
79
+ ```js
80
+ const found = (await api.parallel(lenses.map((lens) => () =>
81
+ api.agent(`Review ${api.args} for ${lens}`, { label: `find:${lens}`, phase: "Find", profile: "small", schema: Findings }),
82
+ ))).filter((result) => result !== null);
83
+ ```
84
+
85
+ Use settled mode when the error detail matters, for example to report which branches failed and why instead of silently shrinking coverage.
86
+
87
+ **Default to `api.pipeline(items, ...stages)`.** With no barrier between stages, item A can be verified while item B is still being found, and the run's concurrency cap still bounds the number of live agents, so it is safe to start every item at once:
88
+
89
+ ```js
90
+ const verified = await api.pipeline(
91
+ files,
92
+ (file) => api.agent(`Find defects in ${file}`, { label: `find:${file}`, phase: "Find", profile: "small", schema: Findings }),
93
+ (found, file) => api.parallel(found.items.map((finding, i) => () =>
94
+ api.agent(`Try to refute this finding in ${file}: ${JSON.stringify(finding)}`, {
95
+ label: `verify:${file}:${i}`, phase: "Verify", profile: "small", schema: Verdict,
96
+ }),
97
+ )),
98
+ );
99
+ ```
100
+
101
+ Barrier smell test: before writing `await api.parallel(...)` followed by another fan-out over its results, ask whether the next stage needs every result of the previous one. A barrier is justified only for cross-item dedupe or merge, an early exit on a zero count, or a stage that compares against all prior results. Flatten, map, or filter alone is not a reason: do it inside a pipeline stage.
102
+
103
+ Host-written scripts in practice reached for `Promise.all` and never for `pipeline`; both habits lose results or wall-clock time. Check every fan-out against these two rules.
104
+
105
+ ## Quality patterns
106
+
107
+ - **Adversarial verify.** Give each finding to a separate verifier told to refute it, and keep only what survives. Thin evidence means not substantiated, not confirmed.
108
+ - **Filter after discovery, not during it.** When the user sets a reporting bar (evidence or experiment requirements, severity or MVP floors, net-LOC rules, dedupe against posted comments), give finders only where to look and ask for every candidate with a nameable impact; apply the bar in verify and synthesis. Finders told that returning nothing is preferred filter out their own candidates before any verifier sees them, and the run reports nothing.
109
+ - **Perspective-diverse verify.** For findings that matter, use two or three verifiers with different methods (read the callers, reproduce it, check the tests) and require agreement.
110
+ - **Judge panel.** Generate several independent answers or designs, then have a judge (or several) pick or merge them against stated criteria.
111
+ - **Loop until dry.** Repeat discovery rounds, deduping each round against everything seen so far, not only the last round. Stop when a round adds nothing new, or at a round cap or budget guard.
112
+ - **Multi-modal sweep.** Cover the space from different angles (per file, per lens, per symptom, search-driven and reading-driven) and take the union.
113
+ - **Completeness critic.** After synthesis, have an agent check the result against the original request or work list and name the gaps; follow up only on material ones.
114
+ - **No silent caps.** Whenever you slice a list, cap rounds, or skip items, `api.log()` what was dropped and report it in the result, for example in a `gaps` or `skipped` field.
115
+ - **Adaptive multi-pass, only when needed.** A single-pass fan-out is the default when it can answer the question. When the first pass may expose gaps, conflicts, weak claims, or missing evidence: run the bounded first-pass agents; give their surviving results to a structured gap-analysis agent so the LLM decides what needs follow-up; put a hard `maxItems` bound on LLM-authored task arrays and defensively slice them before fan-out; use ordinary conditionals or bounded loops to launch follow-up agents only when gaps exist; synthesize the first-pass and follow-up results together. Do not generate a second pass when the first pass is sufficient, and do not invent iteration, quorum, graph, reduction, or retry primitives for this.
116
+
117
+ ## Plan the run
118
+
119
+ - **Scout inline first, then orchestrate.** Use your own tools to find the work list (the files, failing tests, or findings) before writing the script, then fan out over that concrete list. Embed the list in the script or pass it as a JSON `args` value and read it from `api.input`.
120
+ - **One single-phase workflow per turn.** For multi-phase work (understand, design, implement, review), run one workflow per phase and read each result before choosing the next, rather than one script that guesses every phase in advance.
121
+ - **Scale to the request and the size guideline.** A focused question needs a few agents, a broad sweep more. The `<workflows>` section's size guideline line (set by the user with `/workflow:size`) bounds each workflow unless the user asks for more. The run's `maxAgents` limit (default 64) is a separate hard backstop, not a target. Under a tight limit, size a fan-out from `api.agents.remaining()` and keep the agents later stages need, such as a final synthesize, rather than letting the fan-out spend them; the built-in review workflows batch verification this way.
122
+ - **Use `classify()` as a cheap gate** between stages: dedupe, triage, or route items without a full agent. It rejects when no classifier model is configured, so call it inside `api.parallel` or a `try`/`catch` and fall back to an agent or to keeping the item. Results are not journaled, so a resumed run classifies again.
123
+
124
+ ```js
125
+ const answers = await api.classify(
126
+ { state: { finding }, questions: { real: { type: "bool", instructions: "Is this a real defect?", criteria: { true: "Concrete and reachable", false: "Speculative" } } } },
127
+ { label: `triage:${finding.id}` },
128
+ );
129
+ const keep = answers.real?.type === "bool" ? answers.real.probability > 0.5 : true;
130
+ ```
131
+
132
+ ## Budget
133
+
134
+ `api.budget.total` is the output-token ceiling (`null` when unset); `spent()` and `remaining()` are live. Only the user sets a budget: the `workflow` tool takes none. When the user's prompt carries a `+500k`-style directive, every run that turn is capped at the remaining pool, and synchronous runs draw the pool down; once it is spent the tool refuses further runs that turn. Once the ceiling is reached `agent()` rejects with `WorkflowBudgetExceededError` (a `null` slot inside `parallel`); agents already running can overshoot. Scale fleets from `total`, and guard every loop with it so an unbudgeted run still terminates:
135
+
136
+ ```js
137
+ const width = api.budget.total ? Math.max(2, Math.min(8, Math.floor(api.budget.total / 40_000))) : 4;
138
+ for (let round = 0; round < 5 && (api.budget.total === null || api.budget.remaining() > 30_000); round++) {
139
+ // one discovery round
140
+ }
141
+ ```
142
+
143
+ ## Edits in worktrees
144
+
145
+ Give `isolation: "worktree"` only to agents that edit files. The agent runs in a disposable git worktree, and `agent()` resolves to `{ result, patch, changed, baselineOid }`: `result` is the usual text or schema object, `patch` is its `git diff`, `changed` says whether it touched anything. The user's checkout is never modified; return the patches so the host can review and apply them. It requires the workflow `cwd` to be inside a git work tree.
146
+
147
+ ## Resume and diagnosis
148
+
149
+ - Every result names its run ID. Pass `resumeFromRunId: "<run id>"` to the tool to replay completed agent calls whose prompt, options, and execution identity still match; the rest run live.
150
+ - Shared-workspace agents replay only with `resume: "read-only"` (use it for advisory agents); isolated agents replay by default. `resume: "off"` forces a live call. Give repeated identical calls a distinct `cacheKey`, such as `` `verify:${finding.id}` ``.
151
+ - To iterate on an inline run, edit the persisted script its result names (`.pi/.workflow-runs/<runId>.script.js`) in place, then call the tool with `scriptPath` set to that file and `resumeFromRunId`. `scriptPath` implies `resumeEditedWorkflow`, so unchanged replayable calls are reused and edited or new ones run live. Edit that file, not a copy: it is excluded from the repository fingerprint, while editing any other copy in the repository, or a saved `.pi/workflows` script, changes repository state, so every shared `read-only` call reruns live.
152
+ - After editing the script, `resumeEditedWorkflow: true` (with `resumeFromRunId`) reuses calls that did not change. Use it only when the user accepts that.
153
+ - The journal at `.pi/.workflow-runs/<runId>.jsonl` holds one line per completed replayable agent call (`resume: "read-only"` or isolated) with its result; `<runId>.run.json` beside it holds the run state, compact progress, and the final result or error. Read them before diagnosing an empty or unexpected result: a replayable call missing from the journal failed or never ran, while calls without a replay policy are never journaled.
154
+
155
+ ## Run modes
156
+
157
+ - `background: true` only when the user wants the run to continue after this tool call; the tool returns a run ID and the result arrives later as a message naming the run's record and journal (and script, for an inline run). Under Dynamax that message carries the full result or error and starts a new turn, so you wake to read it and choose the next step; otherwise it arrives silently for the next turn with only the summary, and the record holds the rest. `autoResumeOnUsageLimit: true` only for such a background run when the user wants bounded automatic continuation after a provider usage window.
158
+ - Every call provides exactly one of `name`, `script`, or `scriptPath`.
159
+
160
+ ## Save a reusable workflow
161
+
162
+ When a script is worth keeping, write it to `.pi/workflows/<name>.js` with the same contract (pure-literal `meta`, one default export, no imports, injected `Type`). Saved workflows are discovered by name, so later runs can use `name: "<name>"` or `/workflow <name>`; `scriptPath: ".pi/workflows/<name>.js"` runs the file directly. The file is read again on every run by name, so a script saved or edited this session runs with its current content right away, but it appears in the `<workflows>` catalog only from the next session.