pi-workflow-engine 1.0.0 → 1.2.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 (59) hide show
  1. package/.pi/extensions/pi-workflow-engine/index.ts +236 -94
  2. package/.pi/extensions/pi-workflow-engine/src/advisory-evidence.ts +9 -6
  3. package/.pi/extensions/pi-workflow-engine/src/agent-runner-types.ts +10 -3
  4. package/.pi/extensions/pi-workflow-engine/src/agent-runner.ts +18 -4
  5. package/.pi/extensions/pi-workflow-engine/src/agent-session.ts +71 -23
  6. package/.pi/extensions/pi-workflow-engine/src/background-workflow-tool.ts +12 -3
  7. package/.pi/extensions/pi-workflow-engine/src/background-workflows.ts +51 -8
  8. package/.pi/extensions/pi-workflow-engine/src/cancellation.ts +14 -0
  9. package/.pi/extensions/pi-workflow-engine/src/classify.ts +107 -0
  10. package/.pi/extensions/pi-workflow-engine/src/codemode-slots.ts +73 -0
  11. package/.pi/extensions/pi-workflow-engine/src/config-file.ts +52 -0
  12. package/.pi/extensions/pi-workflow-engine/src/discovery.ts +127 -58
  13. package/.pi/extensions/pi-workflow-engine/src/dynamax.ts +123 -40
  14. package/.pi/extensions/pi-workflow-engine/src/engine.ts +78 -18
  15. package/.pi/extensions/pi-workflow-engine/src/filesystem-error.ts +5 -0
  16. package/.pi/extensions/pi-workflow-engine/src/host-tools.ts +76 -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/model-ref.ts +19 -0
  21. package/.pi/extensions/pi-workflow-engine/src/pi-compat.ts +2 -2
  22. package/.pi/extensions/pi-workflow-engine/src/progress-types.ts +1 -1
  23. package/.pi/extensions/pi-workflow-engine/src/progress.ts +32 -4
  24. package/.pi/extensions/pi-workflow-engine/src/prompt-sections.ts +15 -0
  25. package/.pi/extensions/pi-workflow-engine/src/research-evidence.ts +8 -6
  26. package/.pi/extensions/pi-workflow-engine/src/resume-context.ts +1 -1
  27. package/.pi/extensions/pi-workflow-engine/src/tool-capabilities.ts +15 -3
  28. package/.pi/extensions/pi-workflow-engine/src/types.ts +68 -9
  29. package/.pi/extensions/pi-workflow-engine/src/ui/kit.ts +1 -0
  30. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-format.ts +9 -3
  31. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-inspector.ts +69 -18
  32. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-runs-browser.ts +17 -6
  33. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-tool-renderer.ts +8 -3
  34. package/.pi/extensions/pi-workflow-engine/src/ui/workflow-widget.ts +16 -8
  35. package/.pi/extensions/pi-workflow-engine/src/usage.ts +20 -2
  36. package/.pi/extensions/pi-workflow-engine/src/workflow-advisory-utils.ts +123 -9
  37. package/.pi/extensions/pi-workflow-engine/src/workflow-catalog.ts +79 -0
  38. package/.pi/extensions/pi-workflow-engine/src/workflow-execution.ts +6 -2
  39. package/.pi/extensions/pi-workflow-engine/src/workflow-input.ts +13 -0
  40. package/.pi/extensions/pi-workflow-engine/src/workflow-module.ts +21 -4
  41. package/.pi/extensions/pi-workflow-engine/src/workflow-result-context.ts +26 -6
  42. package/.pi/extensions/pi-workflow-engine/src/workflow-run-background.ts +2 -0
  43. package/.pi/extensions/pi-workflow-engine/src/workflow-run-controller.ts +69 -13
  44. package/.pi/extensions/pi-workflow-engine/src/workflow-run-history.ts +23 -4
  45. package/.pi/extensions/pi-workflow-engine/src/workflow-run-record.ts +27 -1
  46. package/.pi/extensions/pi-workflow-engine/src/workflow-run-seal.ts +83 -0
  47. package/.pi/extensions/pi-workflow-engine/src/workflow-run-store.ts +30 -4
  48. package/.pi/extensions/pi-workflow-engine/src/workflow-script-file.ts +50 -0
  49. package/.pi/extensions/pi-workflow-engine/src/workflow-settings.ts +147 -0
  50. package/.pi/extensions/pi-workflow-engine/src/workflow-tool-request.ts +61 -0
  51. package/.pi/extensions/pi-workflow-engine/workflows/code-review.ts +1 -0
  52. package/.pi/extensions/pi-workflow-engine/workflows/diagnose.ts +1 -0
  53. package/.pi/extensions/pi-workflow-engine/workflows/perf-review.ts +1 -0
  54. package/.pi/extensions/pi-workflow-engine/workflows/refactor-scout.ts +1 -0
  55. package/.pi/extensions/pi-workflow-engine/workflows/research.ts +12 -4
  56. package/README.md +12 -4
  57. package/USAGE.md +167 -23
  58. package/package.json +13 -4
  59. package/skills/workflow-authoring/SKILL.md +160 -0
@@ -0,0 +1,61 @@
1
+ export interface WorkflowToolRequestParams {
2
+ readonly name?: string;
3
+ readonly script?: string;
4
+ readonly scriptPath?: string;
5
+ readonly resumeFromRunId?: string;
6
+ readonly background?: boolean;
7
+ }
8
+
9
+ export type WorkflowToolRequest =
10
+ | { readonly kind: "named"; readonly name: string }
11
+ | { readonly kind: "inline"; readonly script: string }
12
+ | { readonly kind: "script-path"; readonly scriptPath: string }
13
+ | { readonly kind: "error"; readonly error: "invalid_workflow_invocation"; readonly message: string };
14
+
15
+ export const INVALID_WORKFLOW_INVOCATION_MESSAGE = "Provide exactly one of a workflow name, an inline workflow script, or a scriptPath.";
16
+
17
+ export function normalizeWorkflowToolRequest(params: WorkflowToolRequestParams): WorkflowToolRequest {
18
+ const name = params.name?.trim() ?? "";
19
+ const script = params.script?.trim() ?? "";
20
+ const scriptPath = params.scriptPath?.trim() ?? "";
21
+ const provided = [name, script, scriptPath].filter((value) => value.length > 0).length;
22
+ if (provided !== 1) return { kind: "error", error: "invalid_workflow_invocation", message: INVALID_WORKFLOW_INVOCATION_MESSAGE };
23
+ if (name) return { kind: "named", name };
24
+ return script ? { kind: "inline", script } : { kind: "script-path", scriptPath };
25
+ }
26
+
27
+ /** pi names MCP tools `mcp__<server>__<tool>`; such a word contains its server's `mcp__<server>` namespace. */
28
+ const MCP_TOOL_REFERENCE = /mcp__[A-Za-z0-9_-]+/g;
29
+ /** `api.workflow(...)` or a destructured `workflow(...)`; prose that happens to match only costs one wait. */
30
+ const SUB_WORKFLOW_CALL = /\bworkflow\s*\(/;
31
+
32
+ /**
33
+ * Which tools a `workflow` tool call may reach, for pi's `reachesTools`, read from the same request the
34
+ * call executes. Before the call runs, pi's MCP extension waits for every server whose namespace a
35
+ * returned word contains, or for all of them on `"all"`. Without that wait a synchronous run early in a
36
+ * session can start before MCP tools register, and agents that name them then fail at the host-tools bridge.
37
+ *
38
+ * - A background run never gets the bridge, so it reaches nothing.
39
+ * - A named workflow is opaque, so it reaches `"all"`. Each server's readiness settles once, so this
40
+ * costs at most one wait at session start.
41
+ * - An inline script reaches the MCP tools it names, or `"all"` when it asks for `external-search`
42
+ * tools or composes a sub-workflow, which is as opaque as a named one.
43
+ * - A script file reaches `"all"` unread. pi asks synchronously on its main thread, where reading a FIFO,
44
+ * a terminal or a device named as `scriptPath` would block or never end.
45
+ * - An invalid call fails before running anything, so it reaches nothing.
46
+ */
47
+ export function workflowToolReach(input: Record<string, unknown>): "all" | readonly string[] {
48
+ if (input.background === true) return [];
49
+ const request = normalizeWorkflowToolRequest({ name: text(input.name), script: text(input.script), scriptPath: text(input.scriptPath) });
50
+ if (request.kind === "inline") return scriptReach(request.script);
51
+ return request.kind === "error" ? [] : "all";
52
+ }
53
+
54
+ function scriptReach(script: string): "all" | readonly string[] {
55
+ if (script.includes("external-search") || SUB_WORKFLOW_CALL.test(script)) return "all";
56
+ return [...new Set(script.match(MCP_TOOL_REFERENCE) ?? [])];
57
+ }
58
+
59
+ function text(value: unknown): string | undefined {
60
+ return typeof value === "string" ? value : undefined;
61
+ }
@@ -23,6 +23,7 @@ import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
23
23
  export const meta: WorkflowMeta = {
24
24
  name: "code-review",
25
25
  description: "Fan-out review of the branch's open PR (or branch vs main): scope → per-angle find → independent verify → synthesize.",
26
+ whenToUse: "Use to review a PR, branch diff, ref range, or focused target for correctness bugs and cleanups, with every finding independently verified; args are the target or review instructions, blank auto-detects the diff.",
26
27
  phases: [{ title: "Scope" }, { title: "Find" }, { title: "Verify" }, { title: "Challenge" }, { title: "Synthesize" }],
27
28
  };
28
29
 
@@ -16,6 +16,7 @@ import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
16
16
  export const meta: WorkflowMeta = {
17
17
  name: "diagnose",
18
18
  description: "Advisory-only bug diagnosis: scope symptoms → competing hypotheses → independent verify → synthesize likely root causes.",
19
+ whenToUse: "Use when a bug, failing command, flaky test, or regression has an unclear cause and competing hypotheses should be checked independently before anyone edits code; args describe the symptom.",
19
20
  phases: [{ title: "Scope" }, { title: "Hypothesize" }, { title: "Verify" }, { title: "Challenge" }, { title: "Synthesize" }],
20
21
  };
21
22
 
@@ -17,6 +17,7 @@ import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
17
17
  export const meta: WorkflowMeta = {
18
18
  name: "perf-review",
19
19
  description: "Advisory-only performance review: scope slow path → per-lens bottleneck hypotheses → verify evidence → synthesize measurements and safe optimizations.",
20
+ whenToUse: "Use for a slow path, startup cost, or performance concern where measured bottlenecks and safe optimizations matter more than guesses; args name the path, workload, or command.",
20
21
  phases: [{ title: "Scope" }, { title: "Find" }, { title: "Verify" }, { title: "Challenge" }, { title: "Synthesize" }],
21
22
  };
22
23
 
@@ -18,6 +18,7 @@ import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
18
18
  export const meta: WorkflowMeta = {
19
19
  name: "refactor-scout",
20
20
  description: "Advisory-only refactor scout: scope → per-lens find → independent verify → synthesize safe refactor opportunities.",
21
+ whenToUse: "Use to find small, behaviour-preserving refactors (duplication, complexity, type safety, boundaries, dead code) in a path or module without changing it; args name the target.",
21
22
  phases: [{ title: "Scope" }, { title: "Find" }, { title: "Verify" }, { title: "Challenge" }, { title: "Synthesize" }],
22
23
  };
23
24
 
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  MAX_RESEARCH_LANES,
3
+ MAX_VERIFICATION_CLAIMS,
3
4
  ResearchLaneResultSchema,
4
5
  ResearchPlanSchema,
5
6
  ResearchReportSchema,
@@ -23,6 +24,7 @@ import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
23
24
  export const meta: WorkflowMeta = {
24
25
  name: "research",
25
26
  description: "Source-grounded external research: decompose → gather direct-page evidence → independently verify claims → cited synthesis.",
27
+ whenToUse: "Use for questions that need current external sources and cited, independently verified claims; needs an installed web search or browsing tool; args are the question and any scope constraints.",
26
28
  phases: [{ title: "Plan" }, { title: "Gather" }, { title: "Verify" }, { title: "Synthesize" }],
27
29
  };
28
30
 
@@ -105,10 +107,11 @@ export default async function run(api: WorkflowApi): Promise<ResearchReport> {
105
107
  const gathered = gatherResults.flatMap((result) => (result.ok ? [result.value] : []));
106
108
  const failedLanes = lanes.filter((_, index) => !gatherResults[index]!.ok);
107
109
 
108
- const candidates = buildClaimCandidates(gathered);
110
+ const { candidates, overCap } = buildClaimCandidates(gathered);
109
111
  if (candidates.length === 0) return unavailableResearchReport(failedLanes.length > 0 ? "lanes-failed" : "no-evidence");
110
112
  progress({ type: "counter", key: "research.claims", label: "claims to verify", value: candidates.length });
111
113
  log(`${candidates.length} bounded claim(s) selected for independent verification`);
114
+ if (overCap > 0) log(`Dropped ${overCap} lower-priority claim(s) over the verification cap of ${MAX_VERIFICATION_CLAIMS}`);
112
115
 
113
116
  phase("Verify");
114
117
  const verificationResults = await parallel(
@@ -162,9 +165,14 @@ export default async function run(api: WorkflowApi): Promise<ResearchReport> {
162
165
  }
163
166
 
164
167
  const report = sanitizeResearchReport(synthesis, synthesisInputs);
165
- if (failedLanes.length === 0) return report;
166
- const laneGap = `${failedLanes.length} of ${lanes.length} research lane(s) failed (${failedLanes.map((lane) => lane.title).join(", ")}); coverage is incomplete.`;
167
- return { ...report, limitations: [...report.limitations, laneGap] };
168
+ const gaps: string[] = [];
169
+ if (failedLanes.length > 0) {
170
+ gaps.push(`${failedLanes.length} of ${lanes.length} research lane(s) failed (${failedLanes.map((lane) => lane.title).join(", ")}); coverage is incomplete.`);
171
+ }
172
+ if (overCap > 0) {
173
+ gaps.push(`${overCap} lower-priority claim(s) were not independently verified; verification is capped at ${MAX_VERIFICATION_CLAIMS} claims.`);
174
+ }
175
+ return gaps.length === 0 ? report : { ...report, limitations: [...report.limitations, ...gaps] };
168
176
  }
169
177
 
170
178
  function verificationPrompt(question: string, candidate: ResearchClaimCandidate): string {
package/README.md CHANGED
@@ -15,7 +15,7 @@ one-off workflow for the task.
15
15
 
16
16
  ## Quick start
17
17
 
18
- Requires **pi 0.80.10 or newer**.
18
+ Requires **pi 1.0.0 or newer**.
19
19
 
20
20
  ```bash
21
21
  pi install npm:pi-workflow-engine
@@ -72,15 +72,21 @@ dynamax investigate this flaky test using separate timing, fixture, diff, and ex
72
72
  dynamax argue both implementation options, then have another agent challenge the recommendation
73
73
  ```
74
74
 
75
- The token enables Dynamax for one turn. Sticky mode remains active for the pi
76
- session:
75
+ The token enables Dynamax for that one request. Sticky mode instead makes
76
+ workflow orchestration the default for every substantive task in the pi session;
77
+ `/workflow:dynamax` with no argument toggles it:
77
78
 
78
79
  ```text
80
+ /workflow:dynamax
79
81
  /workflow:dynamax on
80
82
  /workflow:dynamax off
81
83
  /workflow:dynamax status
82
84
  ```
83
85
 
86
+ `/workflow:size small|medium|large|off` sets how many agents pi aims to keep
87
+ each authored workflow under (5, 10 by default, or 25); see the
88
+ [size guideline](USAGE.md#workflow-size-guideline).
89
+
84
90
  In the TUI, a standalone `dynamax` token receives a short moving-shine sweep and
85
91
  then stays highlighted. Use `PI_DYNAMAX_EFFECT=static` or
86
92
  `PI_DYNAMAX_EFFECT=off` to reduce or disable the cue; the
@@ -108,7 +114,9 @@ covers review actions and recent run history.
108
114
  ## Create your own workflows
109
115
 
110
116
  Saved workflows are TypeScript modules for repeatable orchestration; inline
111
- workflows are useful for one-off Dynamax tasks. The
117
+ workflows are useful for one-off Dynamax tasks. An inline script that proves
118
+ useful can be saved as a `.js` file in a project's `.pi/workflows/` or in
119
+ `~/.pi/agent/workflows/` and run by name. The
112
120
  [workflow authoring guide](USAGE.md#author-a-saved-workflow) covers the API,
113
121
  typed handoffs, model profiles, tool access, composition, and registration.
114
122
 
package/USAGE.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  `pi-workflow-engine` adds zero-dependency Dynamax workflows to pi: opt into live, parallel subagents for the tasks that need a custom investigation instead of one long prompt. Most users only need these surfaces:
4
4
 
5
- This release requires **pi 0.80.10 or newer**. pi remains host-provided rather
5
+ This release requires **pi 1.0.0 or newer**. pi remains host-provided rather
6
6
  than bundled by this package, and the extension rejects older runtimes at load
7
7
  time with an update message.
8
8
 
@@ -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
@@ -109,16 +114,20 @@ command instead of selecting another model.
109
114
  as external web search, browsing, or HTTP(S) page extraction. Local grep/find
110
115
  tools do not satisfy this check. The workflow does not bundle a scraper or a
111
116
  paid API; install or enable the web capability you prefer, then restart or
112
- reload pi so subagent sessions can see it.
117
+ reload pi so subagent sessions can see it. An MCP web search server also
118
+ qualifies when the host agent runs `research` through the `workflow` tool,
119
+ because only those runs can reach the host's MCP tools.
113
120
 
114
121
  The workflow bounds decomposition to four lanes and verification to twelve
115
122
  claims, while the engine's concurrency, total-agent, timeout, cancellation, and
116
- token-budget limits still apply. Gatherers retain direct page titles and URLs;
117
- search-results URLs are rejected, sources are canonicalized and deduplicated,
118
- and separate verifier agents classify claims as supported, conflicting,
119
- uncertain, inferred, or rejected before synthesis. External research calls use
120
- `resume: "off"` because live web evidence, time, and service state are outside
121
- 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.
122
131
 
123
132
  ## Use Dynamax for custom workflows
124
133
 
@@ -139,6 +148,7 @@ dynamax use parallel agents to review this design and report the risks
139
148
  Sticky mode and inspector shortcut:
140
149
 
141
150
  ```text
151
+ /workflow:dynamax # toggle sticky mode
142
152
  /workflow:dynamax on
143
153
  /workflow:dynamax off
144
154
  /workflow:dynamax status
@@ -146,7 +156,7 @@ Sticky mode and inspector shortcut:
146
156
 
147
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.
148
158
 
149
- When only the literal `dynamax` token is used, the opt-in is one-shot: the next agent run receives the workflow permission reminder, 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.
150
160
 
151
161
  ### Prompt editor cue
152
162
 
@@ -160,7 +170,7 @@ The default effect is `shine`. Set `PI_DYNAMAX_EFFECT=static` for a motionless
160
170
  cue or `PI_DYNAMAX_EFFECT=off` to disable it. A non-empty `NO_COLOR` also
161
171
  disables the default cue; an explicit `PI_DYNAMAX_EFFECT` value takes precedence.
162
172
 
163
- The implementation follows pi 0.80.10's [official `CustomEditor`
173
+ The implementation follows pi's [official `CustomEditor`
164
174
  pattern](https://pi.dev/docs/latest/extensions#custom-editor): it decorates only
165
175
  the editor's rendered lines, retaining the wrapped editor's input, cursor,
166
176
  deletion, paste, multiline, undo, completion, IME, and app-keybinding behavior.
@@ -189,6 +199,24 @@ Set either shortcut to `null` to disable it while keeping `/workflow:inspector`
189
199
 
190
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.
191
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
+
192
220
  ### Adaptive multi-pass authoring
193
221
 
194
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:
@@ -247,6 +275,8 @@ During a run, a widget above the editor shows the workflow's progress bar, finis
247
275
 
248
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.
249
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
+
250
280
  ### Durable run records
251
281
 
252
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.
@@ -255,11 +285,17 @@ Record replacement is atomic, corrupt or unsupported-version files are ignored i
255
285
 
256
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.
257
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
+
258
292
  ### Background workflows
259
293
 
260
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.
261
295
 
262
- 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.
263
299
 
264
300
  Provider usage-window continuation is a separate, explicit background option:
265
301
 
@@ -273,7 +309,7 @@ Provider usage-window continuation is a separate, explicit background option:
273
309
  }
274
310
  ```
275
311
 
276
- 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.
277
313
 
278
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.
279
315
 
@@ -283,7 +319,7 @@ A graceful pi session shutdown aborts active background work and records it as `
283
319
 
284
320
  ### Recent runs and lifecycle actions
285
321
 
286
- `/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.
287
323
 
288
324
  The same operations are available without the selection UI:
289
325
 
@@ -295,7 +331,7 @@ The same operations are available without the selection UI:
295
331
  /workflow:inspector <run-id>
296
332
  ```
297
333
 
298
- 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.
299
335
 
300
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.
301
337
 
@@ -318,6 +354,8 @@ With edited-workflow resume enabled, unchanged calls are reused while changed or
318
354
 
319
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.
320
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
+
321
359
  Replay is explicit for agents that share the workflow directory and automatic for isolated patch-producing agents:
322
360
 
323
361
  | Agent call | Default during resume | Override |
@@ -331,9 +369,9 @@ Journals use replay contract v2. A hit requires the same prompt/options and exec
331
369
 
332
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.
333
371
 
334
- 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.
335
373
 
336
- 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.
337
375
 
338
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.
339
377
 
@@ -349,6 +387,11 @@ import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
349
387
  export const meta: WorkflowMeta = {
350
388
  name: "my-workflow",
351
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
+ ],
352
395
  };
353
396
 
354
397
  const Finding = Type.Object({
@@ -387,6 +430,8 @@ export default async function run({ agent, parallel, phase, args }: WorkflowApi)
387
430
  }
388
431
  ```
389
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
+
390
435
  Core primitives:
391
436
 
392
437
  | Primitive | What it does |
@@ -399,6 +444,10 @@ Core primitives:
399
444
  | `progress(event)` | Emits counters, summaries, and lane items. |
400
445
  | `workflow(ref, args?)` | Runs another workflow inline as a sub-step and returns its result. |
401
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
+
402
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.
403
452
 
404
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.
@@ -423,6 +472,11 @@ overrides its profile.
423
472
 
424
473
  `tools` is a strict allowlist. If you set `tools: ["read", "bash"]`, extension tools such as `ast-grep`, `mgrep`, `fffind`, or `ffgrep` are hidden from that subagent. Add `toolHints: ["search"]` to dynamically expose installed grep/find/search-like tools while keeping the concrete base allowlist portable. Use `toolHints: ["external-search"]` for installed web search, browsing, and URL-extraction tools; `requireToolHints: true` fails before prompting unless each requested capability matched. The built-in advisory workflows use `tools: ["read", "bash", "grep", "find", "ls"]` plus `toolHints: ["search"]`.
425
474
 
475
+ Subagents are SDK sessions, so pi's built-in codemode and MCP extensions do not load in them. Two opt-ins cover that:
476
+
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.
479
+
426
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.
427
481
 
428
482
  Set `model` only when a subagent should use a specific model. Bare ids keep the Anthropic shorthand; `provider/id` targets built-in, custom, or local providers. Explicit models override a requested profile; calls with neither inherit the host/session default. Malformed or unknown explicit refs fail fast.
@@ -450,16 +504,36 @@ Cleanup is a required finalizer. The engine attempts every registered worktree r
450
504
 
451
505
  ### Compose workflows
452
506
 
453
- `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.
454
508
 
455
509
  ```ts
456
510
  export default async function run({ workflow }: WorkflowApi) {
457
511
  const review = await workflow("code-review", "HEAD~3");
458
- return { review };
512
+ const audit = await workflow({ scriptPath: "scripts/audit.js" }, { paths: ["src/"], strict: true });
513
+ return { review, audit };
459
514
  }
460
515
  ```
461
516
 
462
- 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.
518
+
519
+ ### Classify between stages
520
+
521
+ `classify(context, opts?)` answers typed `choice`, `score`, or `bool` questions about JSON `state` with one of pi's classifier models (for example TypeSafe Jev or Cloudflare Clef), using the host's credentials and no chat session. It suits cheap gates between stages, such as deduplicating findings or deciding which items need a follow-up agent.
522
+
523
+ ```ts
524
+ const answers = await api.classify(
525
+ {
526
+ state: { finding },
527
+ questions: {
528
+ followUp: { type: "bool", instructions: "Does this finding need verification?", criteria: { true: "Plausible but unproven", false: "Clearly settled" } },
529
+ },
530
+ },
531
+ { label: `triage:${finding.id}` },
532
+ );
533
+ if (answers.followUp?.type === "bool" && answers.followUp.probability > 0.5) { /* verify it */ }
534
+ ```
535
+
536
+ Without `opts.model` ("provider/id"), the first classifier with working credentials is used. Calls share the run's concurrency cap, count toward usage and the budget, and are not journaled, so a resumed run classifies again. When no classifier is configured the call rejects with a recoverable error, so guard it or let `parallel()` turn it into `null`.
463
537
 
464
538
  ### Where workflows live
465
539
 
@@ -469,12 +543,19 @@ Guaranteed built-ins are statically registered:
469
543
  2. Import it in `.pi/extensions/pi-workflow-engine/src/workflows.ts`.
470
544
  3. Add a `defineBuiltinWorkflow(mod, "<name>.ts")` entry to `BUILTIN_WORKFLOW_DEFINITIONS`.
471
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
+
472
553
  Drop-in workflows are also discovered best-effort from:
473
554
 
474
555
  - `~/.pi/agent/workflows/*.ts`: the user-level drop-in directory, which survives `pi update`.
475
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`.
476
557
 
477
- 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.
478
559
 
479
560
  ## Author an inline workflow
480
561
 
@@ -501,6 +582,7 @@ Inline rules:
501
582
 
502
583
  - start with `export const meta = { ... }`;
503
584
  - default-export an async workflow function;
585
+ - write plain JavaScript: the script compiles in-process, so TypeScript annotations do not parse;
504
586
  - no imports or dynamic `import()`;
505
587
  - use the injected `Type` object for schemas;
506
588
  - request `profile: "small"`, `"medium"`, or `"big"` instead of hard-coding provider models when the stage is portable;
@@ -509,6 +591,10 @@ Inline rules:
509
591
 
510
592
  Inline workflows run in-process with extension permissions, so treat them as trusted task automation.
511
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
+
512
598
  ## Runtime knobs
513
599
 
514
600
  Only tune these when a workflow is too slow, too expensive, or too noisy:
@@ -517,7 +603,7 @@ Only tune these when a workflow is too slow, too expensive, or too noisy:
517
603
  | --- | --- |
518
604
  | `--concurrency=N` / `PI_WORKFLOW_CONCURRENCY=N` | Cap concurrent subagents. Default is `min(8, max(2, CPU count))`. |
519
605
  | `--parallel-limit=N` / `PI_WORKFLOW_PARALLEL_SUBMISSION_LIMIT=N` | Limit eager `parallel()` submission. |
520
- | `--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. |
521
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`. |
522
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`. |
523
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. |
@@ -533,6 +619,38 @@ and abort scope. Usage from failed attempts is retained, so it can exhaust the
533
619
  budget before the next retry begins. Host cancellation takes precedence over
534
620
  limit and timeout failures.
535
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, a `workflow` tool call without its own `budget` runs
640
+ with the pool's remainder as its budget (clamped to the tool's budget range),
641
+ and each synchronous run's output tokens are subtracted once it finishes,
642
+ failed or not. When less than one token remains, the tool refuses the call and
643
+ tells the agent the turn's budget is spent. An explicit `budget` on the call
644
+ can only lower that ceiling, is refused the same way once the pool is spent, and
645
+ its spend is subtracted too. The pool clears when the run that
646
+ received the prompt settles, so it still covers the run after pi retries a
647
+ provider error or compacts an overflowing context within that run.
648
+
649
+ Background runs receive the current remainder as their budget but do not draw
650
+ the pool down, because their usage lands after the turn. Parallel `workflow`
651
+ calls in one assistant message each see the same remainder before any of them
652
+ finishes. Per-run budget accounting is unchanged either way.
653
+
536
654
  Transient provider failures are retried in two layers. First, each subagent
537
655
  inherits pi's session-level automatic retry from your pi `retry` settings
538
656
  (enabled by default: 3 retries with 2s, 4s, 8s backoff). It retries the failed
@@ -575,6 +693,32 @@ fails, the result retains verified records and reports incomplete coverage.
575
693
  Synthesis selects or merges IDs; the workflow reconstructs evidence-bearing
576
694
  fields from those records.
577
695
 
696
+ Each lens verifies at most the per-lens candidate count its finder prompt
697
+ states (six per `code-review` angle, four or five for the other workflows).
698
+ When a finder returns more, the workflow logs how many it cut from that lens.
699
+ `stats.candidates` counts everything the finders returned, and `stats.dropped`
700
+ counts the candidates never verified: those over a lens cap, duplicates, and
701
+ out-of-scope candidates.
702
+
703
+ ### Duplicate candidates
704
+
705
+ Lenses often report the same defect in different words. When a TypeSafe Jev
706
+ classifier is reachable (TypeSafe directly, OpenRouter, Cloudflare Workers AI,
707
+ Vercel AI Gateway, or OpenCode Zen), the four advisory workflows ask it whether
708
+ each pair of candidates in the same file describes one defect. Before
709
+ verification, a candidate joins a group only when the classifier is at least 80%
710
+ sure it matches every member, so each defect gets one verifier. Merged
711
+ candidates keep all of their source IDs, locations, impacts, and evidence, and
712
+ the verifier sees the other wordings as "Also reported as" evidence. If the
713
+ first comparison fails (no Jev credentials, a bad key, a rate limit), the merge
714
+ is skipped.
715
+
716
+ Measured on GPT-6.1 Sol code reviews with planted bugs, the merge cut verifier
717
+ agents and cost by about 40% (e.g. 29 → 18 agents, $0.60 → $0.37 list price on
718
+ a small diff; 22 → 14 agents, $1.74 → $0.89 on a 160 KB diff). In every
719
+ instrumented run, each planted bug that a finder proposed reached the final
720
+ report, with or without merging.
721
+
578
722
  ### Selective adversarial challenges
579
723
 
580
724
  Add `--challenge` to an advisory workflow to challenge up to three uncertain or