pi-workflow-engine 0.8.0 → 0.10.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.
@@ -14,7 +14,10 @@ import {
14
14
  primaryLocation,
15
15
  recordVerdictProgress,
16
16
  verdictConfidence,
17
+ DEFAULT_ADVISORY_TOOL_HINTS,
18
+ DEFAULT_ADVISORY_TOOLS,
17
19
  } from "../src/workflow-advisory-utils.ts";
20
+ import { compactResults } from "../src/concurrency.ts";
18
21
  import { captureDiff } from "../src/diff-capture.ts";
19
22
  import type { ReviewContext } from "../src/review/review-issues.ts";
20
23
  import type { WorkflowApi, WorkflowMeta, WorkflowRunStats } from "../src/types.ts";
@@ -55,7 +58,8 @@ const ANGLES: Angle[] = [
55
58
  { label: "conventions", kind: "cleanup", text: "Violations of the project conventions noted in scope (naming, idioms, banned patterns)." },
56
59
  ];
57
60
 
58
- const TOOLS = ["read", "bash"];
61
+ const TOOLS = DEFAULT_ADVISORY_TOOLS;
62
+ const TOOL_HINTS = DEFAULT_ADVISORY_TOOL_HINTS;
59
63
  const PER_ANGLE = 6;
60
64
  const DIFF_EMBED_CAP = 60_000;
61
65
 
@@ -123,7 +127,7 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
123
127
  "Then: list the changed files, summarize the change in one paragraph (mention the PR if one was found), " +
124
128
  "and read any relevant AGENTS.md or project docs noting conventions a reviewer should know.\n" +
125
129
  "Return diffCommand exactly as a reviewer should run it. Structured output only.",
126
- { phase: "Scope", label: "scope", tools: TOOLS, thinkingLevel: "medium", schema: ScopeSchema },
130
+ { phase: "Scope", label: "scope", tools: TOOLS, toolHints: TOOL_HINTS, thinkingLevel: "medium", schema: ScopeSchema },
127
131
  );
128
132
 
129
133
  if (!scope) {
@@ -195,7 +199,7 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
195
199
  `Surface up to ${PER_ANGLE} candidates. Use category exactly "${angle.kind}". Each candidate must include a one-line summary, ` +
196
200
  "locations with the changed file and a line that appears in the diff, and impact describing the concrete failure or maintenance scenario. " +
197
201
  "Pass through anything with a nameable impact — a separate verifier judges them next. Structured output only.",
198
- { phase: "Find", label: `find:${angle.label}`, tools: TOOLS, thinkingLevel: "low", schema: AdvisoryCandidatesSchema },
202
+ { phase: "Find", label: `find:${angle.label}`, tools: TOOLS, toolHints: TOOL_HINTS, thinkingLevel: "low", schema: AdvisoryCandidatesSchema },
199
203
  );
200
204
  const raw = (found?.candidates ?? []).slice(0, PER_ANGLE);
201
205
  rawCandidateCount += raw.length;
@@ -224,7 +228,7 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
224
228
  }),
225
229
  );
226
230
 
227
- const novel = perAngle.flatMap(({ angle, candidates }) =>
231
+ const novel = compactResults(perAngle).flatMap(({ angle, candidates }) =>
228
232
  candidates
229
233
  .filter((candidate) => {
230
234
  const key = dedupKey(candidate);
@@ -249,6 +253,7 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
249
253
  phase: "Verify",
250
254
  label: `verify:${location.file.split("/").pop() ?? location.file}`,
251
255
  tools: TOOLS,
256
+ toolHints: TOOL_HINTS,
252
257
  thinkingLevel: "low",
253
258
  schema: AdvisoryVerdictSchema,
254
259
  },
@@ -259,7 +264,7 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
259
264
  }),
260
265
  );
261
266
 
262
- const verified = verdicts.filter((value): value is Verified => value !== null);
267
+ const verified = compactResults(verdicts);
263
268
  const surviving = verified.filter((finding) => finding.verdict !== "REFUTED");
264
269
  const stats = makeStats(verified.length, surviving.length);
265
270
  progress({ type: "counter", key: "verified", label: "verified", value: verified.length });
@@ -14,7 +14,10 @@ import {
14
14
  formatLocation,
15
15
  primaryLocation,
16
16
  recordVerdictProgress,
17
+ DEFAULT_ADVISORY_TOOL_HINTS,
18
+ DEFAULT_ADVISORY_TOOLS,
17
19
  } from "../src/workflow-advisory-utils.ts";
20
+ import { compactResults } from "../src/concurrency.ts";
18
21
  import type { WorkflowApi, WorkflowMeta, WorkflowRunStats } from "../src/types.ts";
19
22
 
20
23
  export const meta: WorkflowMeta = {
@@ -57,7 +60,8 @@ const HYPOTHESIS_LENSES: HypothesisLens[] = [
57
60
  { label: "test-fixture", category: "test-fixture", text: "The failure is caused by test setup, fixtures, mocks, generated files, or stale local state rather than product code." },
58
61
  ];
59
62
 
60
- const TOOLS = ["read", "bash"];
63
+ const TOOLS = DEFAULT_ADVISORY_TOOLS;
64
+ const TOOL_HINTS = DEFAULT_ADVISORY_TOOL_HINTS;
61
65
  const PER_LENS = 4;
62
66
 
63
67
  export default async function run(api: WorkflowApi): Promise<unknown> {
@@ -85,7 +89,7 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
85
89
  "Inspect relevant files, package/test configuration, and safe diagnostic commands. " +
86
90
  "Safe commands are read-only commands such as status, grep, listing files, typecheck/test commands, or commands explicitly requested by the user. " +
87
91
  "Do not run mutation, install, commit, network, or destructive commands. Return scoped files, observations, and constraints. Structured output only.",
88
- { phase: "Scope", label: "scope", tools: TOOLS, thinkingLevel: "medium", schema: ScopeSchema },
92
+ { phase: "Scope", label: "scope", tools: TOOLS, toolHints: TOOL_HINTS, thinkingLevel: "medium", schema: ScopeSchema },
89
93
  );
90
94
 
91
95
  if (!scope) {
@@ -117,7 +121,7 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
117
121
  `Consider ONLY this hypothesis lens:\n${lens.text}\n\n` +
118
122
  `Surface up to ${PER_LENS} root-cause hypotheses. Use category exactly "${lens.category}". ` +
119
123
  "Each hypothesis must include a one-line summary, locations, impact explaining how it produces the symptom, and an optional recommendation for the next validation step. Structured output only.",
120
- { phase: "Hypothesize", label: `hypothesize:${lens.label}`, tools: TOOLS, thinkingLevel: "low", schema: AdvisoryCandidatesSchema },
124
+ { phase: "Hypothesize", label: `hypothesize:${lens.label}`, tools: TOOLS, toolHints: TOOL_HINTS, thinkingLevel: "low", schema: AdvisoryCandidatesSchema },
121
125
  );
122
126
  const candidates = (found?.candidates ?? []).slice(0, PER_LENS).map((candidate) => ({ ...candidate, lens }));
123
127
  rawCandidateCount += candidates.length;
@@ -136,13 +140,13 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
136
140
  }),
137
141
  );
138
142
 
139
- const hypotheses = dedupe(perLens.flat(), (dropped) => {
143
+ const hypotheses = dedupe(compactResults(perLens).flat(), (dropped) => {
140
144
  droppedCandidateCount += dropped;
141
145
  progress({ type: "counter_delta", key: "dropped", label: "dropped", delta: dropped });
142
146
  });
143
147
 
144
148
  phase("Verify");
145
- const verified = (
149
+ const verified = compactResults(
146
150
  await parallel(
147
151
  hypotheses.map((hypothesis) => async (): Promise<Verified | null> => {
148
152
  const location = primaryLocation(hypothesis);
@@ -157,6 +161,7 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
157
161
  phase: "Verify",
158
162
  label: `verify:${location.file.split("/").pop() ?? location.file}`,
159
163
  tools: TOOLS,
164
+ toolHints: TOOL_HINTS,
160
165
  thinkingLevel: "low",
161
166
  schema: AdvisoryVerdictSchema,
162
167
  },
@@ -167,8 +172,8 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
167
172
  });
168
173
  return { ...hypothesis, verdict: judged.verdict, evidence: judged.evidence, confidence: judged.confidence };
169
174
  }),
170
- )
171
- ).filter((value): value is Verified => value !== null);
175
+ ),
176
+ );
172
177
 
173
178
  const surviving = verified.filter((finding) => finding.verdict !== "REFUTED");
174
179
  const refuted = verified.filter((finding) => finding.verdict === "REFUTED");
@@ -241,4 +246,3 @@ function rank(finding: Verified): number {
241
246
  if (finding.verdict === "CONFIRMED") return 0;
242
247
  return 1;
243
248
  }
244
-
@@ -10,6 +10,8 @@ import {
10
10
  formatEvidence,
11
11
  formatLocation,
12
12
  runLensVerificationPipeline,
13
+ DEFAULT_ADVISORY_TOOL_HINTS,
14
+ DEFAULT_ADVISORY_TOOLS,
13
15
  } from "../src/workflow-advisory-utils.ts";
14
16
  import type { WorkflowApi, WorkflowMeta, WorkflowRunStats } from "../src/types.ts";
15
17
 
@@ -51,7 +53,8 @@ const PERF_LENSES: PerfLens[] = [
51
53
  { label: "measurement", category: "measurement", text: "Missing, misleading, noisy, or insufficient benchmark/measurement design." },
52
54
  ];
53
55
 
54
- const TOOLS = ["read", "bash"];
56
+ const TOOLS = DEFAULT_ADVISORY_TOOLS;
57
+ const TOOL_HINTS = DEFAULT_ADVISORY_TOOL_HINTS;
55
58
  const PER_LENS = 4;
56
59
 
57
60
  export default async function run(api: WorkflowApi): Promise<unknown> {
@@ -77,7 +80,7 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
77
80
  "Inspect repository structure, scripts, likely hot-path files, and any existing benchmark or measurement commands. " +
78
81
  "Prefer identifying what to measure before claiming bottlenecks. Return files, commands, summary, and known measurements or the lack of them. " +
79
82
  `This workflow will fan out across ${PERF_LENSES.length} lenses with up to ${PER_LENS} candidates per lens. Structured output only.`,
80
- { phase: "Scope", label: "scope", tools: TOOLS, thinkingLevel: "medium", schema: ScopeSchema },
83
+ { phase: "Scope", label: "scope", tools: TOOLS, toolHints: TOOL_HINTS, thinkingLevel: "medium", schema: ScopeSchema },
81
84
  );
82
85
 
83
86
  if (!scope || scope.files.length === 0) {
@@ -107,6 +110,7 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
107
110
  schedulingMode: "finder-barrier",
108
111
  perLens: PER_LENS,
109
112
  tools: TOOLS,
113
+ toolHints: TOOL_HINTS,
110
114
  finderPrompt: (lens) =>
111
115
  `## Perf-review finder — ${lens.label}\n\n${scopeBlock}\n` +
112
116
  "This workflow is advisory-only: identify bottleneck hypotheses, measurement gaps, and safe optimization directions, but do not edit files.\n" +
@@ -10,6 +10,8 @@ import {
10
10
  formatEvidence,
11
11
  formatLocation,
12
12
  runLensVerificationPipeline,
13
+ DEFAULT_ADVISORY_TOOL_HINTS,
14
+ DEFAULT_ADVISORY_TOOLS,
13
15
  } from "../src/workflow-advisory-utils.ts";
14
16
  import type { WorkflowApi, WorkflowMeta, WorkflowRunStats } from "../src/types.ts";
15
17
 
@@ -50,7 +52,8 @@ const REFACTOR_LENSES: RefactorLens[] = [
50
52
  { label: "conventions", category: "conventions", text: "Departures from project conventions, naming, dependency rules, or local idioms." },
51
53
  ];
52
54
 
53
- const TOOLS = ["read", "bash"];
55
+ const TOOLS = DEFAULT_ADVISORY_TOOLS;
56
+ const TOOL_HINTS = DEFAULT_ADVISORY_TOOL_HINTS;
54
57
  const PER_LENS = 5;
55
58
 
56
59
  export default async function run(api: WorkflowApi): Promise<unknown> {
@@ -76,7 +79,7 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
76
79
  "Inspect repository structure, the target path or module, and relevant AGENTS.md / project docs conventions. " +
77
80
  "Return the concrete files that should be considered, a short summary, and any conventions that affect refactor advice. " +
78
81
  `This workflow will fan out across ${REFACTOR_LENSES.length} lenses with up to ${PER_LENS} candidates per lens. Structured output only.`,
79
- { phase: "Scope", label: "scope", tools: TOOLS, thinkingLevel: "medium", schema: ScopeSchema },
82
+ { phase: "Scope", label: "scope", tools: TOOLS, toolHints: TOOL_HINTS, thinkingLevel: "medium", schema: ScopeSchema },
80
83
  );
81
84
 
82
85
  if (!scope || scope.files.length === 0) {
@@ -104,6 +107,7 @@ export default async function run(api: WorkflowApi): Promise<unknown> {
104
107
  schedulingMode: "finder-barrier",
105
108
  perLens: PER_LENS,
106
109
  tools: TOOLS,
110
+ toolHints: TOOL_HINTS,
107
111
  finderPrompt: (lens) =>
108
112
  `## Refactor-scout finder — ${lens.label}\n\n${scopeBlock}\n` +
109
113
  "This workflow is advisory-only: do not edit files and do not propose broad rewrites.\n" +
package/README.md CHANGED
@@ -8,7 +8,7 @@ Use `dynamax` when a task needs several independent passes: review from differen
8
8
 
9
9
  The package ships no bundled runtime dependencies. It uses the pi SDK and pi's bundled core packages, so installing it does not pull in a second agent stack.
10
10
 
11
- The built-in workflows are advisory. They inspect, verify, and report; they do not edit files.
11
+ The built-in workflows are advisory by default. They inspect, verify, and report; they do not edit files. Workflow authors can opt individual subagents into disposable git worktrees when they want reviewable patches instead of direct edits.
12
12
 
13
13
  ## Install
14
14
 
@@ -134,9 +134,13 @@ Useful flags:
134
134
  /workflow code-review --result-viewer
135
135
  /workflow code-review --no-result-viewer
136
136
  /workflow code-review --concurrency=4
137
+ /workflow code-review --budget=50000
138
+ /workflow code-review --resume <run-id>
137
139
  /workflow code-review --refresh
138
140
  ```
139
141
 
142
+ Every workflow result includes a run id. If a long run is interrupted, rerun the same workflow with `--resume <run-id>` to replay matching completed `agent()` calls from the journal and run only the first changed or missing call onward. Resume is deterministic-prefix based: keep prompts and agent options stable if you want cache hits.
143
+
140
144
  The code-review workflow can open an interactive findings viewer. Select findings and hand them back to the parent agent for minimal fixes, or ask it to raise GitHub PR comments when `gh` is authenticated and PR context is available.
141
145
 
142
146
  For the full command guide, see [USAGE.md](USAGE.md).
@@ -202,9 +206,15 @@ Inline workflows are passed to the `workflow` tool as a script string. They are
202
206
  - Each `agent()` runs an in-process pi `AgentSession` with `SessionManager.inMemory()`.
203
207
  - Workflow results aggregate the finalized assistant usage from those subagent sessions before disposal and show token/cost totals separately from pi's host-session footer accounting.
204
208
  - Structured output uses a terminating tool whose `parameters` is your schema. pi validates the call; the engine captures the args. There is no JSON scraping.
209
+ - A schema agent that finishes without calling `final_answer` is re-prompted once before returning `null`.
210
+ - Runs write completed `agent()` results to `.pi/.workflow-runs/<run-id>.jsonl`; `--resume <run-id>` replays the longest unchanged prefix and writes a fresh journal for the resumed run.
211
+ - `agent(..., { isolation: "worktree" })` runs in a disposable git worktree and returns `{ result, patch, changed }`; the user working tree is not mutated.
205
212
  - A single run-level semaphore caps concurrent agents, so nested `parallel`, `pipeline`, and `workflow()` calls stay bounded.
213
+ - `parallel()` and `pipeline()` are fail-soft: recoverable branch failures, including budget backstops, become `null` slots while survivors continue. A genuine run abort still rejects.
206
214
  - Built-in workflows stay statically imported so they share pi's bundled `typebox` identity.
207
215
  - Set `thinkingLevel` per fan-out agent. Otherwise many subagents can inherit an expensive global reasoning level.
216
+ - Subagents receive no skills by default. Opt in per agent with `skills: ["skill-name"]`; clear prompt text like `include skill name` also works when `skills` is omitted.
217
+ - `tools` is a strict allowlist for subagents. Use `toolHints: ["search"]` to dynamically expose installed grep/find/search-like tools such as `ast-grep`, `mgrep`, `ffgrep`, or `fffind` without hard-coding a specific extension.
208
218
  - Set `model` per agent only when needed: bare ids are Anthropic shorthand, and `provider/id` targets other providers.
209
219
  Omitted models inherit the host/session default; malformed or unknown explicit refs fail fast.
210
220
 
package/USAGE.md CHANGED
@@ -18,7 +18,7 @@ From inside a repo:
18
18
  /workflow perf-review "workflow startup latency"
19
19
  ```
20
20
 
21
- The built-in workflows are advisory. They inspect, verify, and report; they do **not** edit files.
21
+ The built-in workflows are advisory by default. They inspect, verify, and report; they do **not** edit files. Saved or inline workflows can opt individual agents into disposable git worktrees when they need reviewable patches.
22
22
 
23
23
  Useful flags:
24
24
 
@@ -28,6 +28,8 @@ Useful flags:
28
28
  /workflow code-review --result-viewer # explicitly open the findings viewer
29
29
  /workflow code-review --review-viewer # alias for --result-viewer
30
30
  /workflow code-review --concurrency=4 # cap concurrent subagents
31
+ /workflow code-review --budget=50000 # output-token ceiling for subagents
32
+ /workflow code-review --resume <run-id> # replay matching completed agent calls
31
33
  /workflow code-review --refresh # rediscover newly added workflow files
32
34
  ```
33
35
 
@@ -104,12 +106,25 @@ During a run, pi shows live phases and subagent status. Use `--inspect` if you w
104
106
 
105
107
  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, press `enter` to expand/collapse the nicely formatted finding text, and use `1`-`9` to jump directly to a visible finding.
106
108
 
109
+ ### Resume a run
110
+
111
+ Every workflow result includes a run id. Resume with:
112
+
113
+ ```text
114
+ /workflow code-review --resume <run-id> HEAD~3
115
+ ```
116
+
117
+ The `workflow` tool exposes the same feature as `resumeFromRunId`. Resume replays completed `agent()` results from `.pi/.workflow-runs/<run-id>.jsonl` while the agent call index and prompt/options hash still match. After the first missing or changed call, the rest of the run executes live and writes a new journal under the new run id. Cached results do not add usage or budget spend for the resumed run.
118
+
119
+ Resume only caches completed `agent()` calls. It does not snapshot arbitrary workflow local variables or in-flight tool work. Keep prompts and agent options deterministic across reruns if you want cache hits.
120
+
107
121
  ## Author a saved workflow
108
122
 
109
123
  Saved workflows are TypeScript modules with `meta` plus a default async function:
110
124
 
111
125
  ```ts
112
126
  import { Type } from "typebox";
127
+ import { compactResults } from "../src/concurrency.ts";
113
128
  import type { WorkflowApi, WorkflowMeta } from "../src/types.ts";
114
129
 
115
130
  export const meta: WorkflowMeta = {
@@ -120,13 +135,17 @@ export const meta: WorkflowMeta = {
120
135
  const Finding = Type.Object({
121
136
  summary: Type.String(),
122
137
  });
138
+ const SEARCH_TOOLS = ["read", "bash", "grep", "find", "ls"];
139
+ const SEARCH_TOOL_HINTS = ["search"] as const;
123
140
 
124
141
  export default async function run({ agent, parallel, phase, args }: WorkflowApi) {
125
142
  phase("Find");
126
- const findings = await parallel([
127
- () => agent(`Find correctness issues: ${args}`, { schema: Finding, tools: ["read", "bash"], thinkingLevel: "low" }),
128
- () => agent(`Find edge cases: ${args}`, { schema: Finding, tools: ["read", "bash"], thinkingLevel: "low" }),
129
- ]);
143
+ const findings = compactResults(
144
+ await parallel([
145
+ () => agent(`Find correctness issues: ${args}`, { schema: Finding, tools: SEARCH_TOOLS, toolHints: SEARCH_TOOL_HINTS, thinkingLevel: "low" }),
146
+ () => agent(`Find edge cases: ${args}`, { schema: Finding, tools: SEARCH_TOOLS, toolHints: SEARCH_TOOL_HINTS, thinkingLevel: "low" }),
147
+ ]),
148
+ );
130
149
 
131
150
  phase("Synthesize");
132
151
  return agent(`Summarize: ${JSON.stringify(findings)}`, { thinkingLevel: "medium" });
@@ -138,16 +157,39 @@ Core primitives:
138
157
  | Primitive | What it does |
139
158
  | --- | --- |
140
159
  | `agent(prompt, opts)` | Runs one isolated subagent. With `schema`, returns validated structured data. |
141
- | `parallel(thunks)` | Runs thunks concurrently and waits for all. |
142
- | `pipeline(items, ...stages)` | Runs each item through stages independently. |
160
+ | `parallel(thunks)` | Runs thunks concurrently and waits for all; recoverable failures become `null` slots. |
161
+ | `pipeline(items, ...stages)` | Runs each item through stages independently; a failed item becomes `null`. |
143
162
  | `phase(title)` / `log(message)` | Updates workflow progress UI. |
144
163
  | `progress(event)` | Emits counters, summaries, and lane items. |
145
164
  | `workflow(ref, args?)` | Runs another workflow inline as a sub-step and returns its result. |
146
165
 
147
166
  Set `thinkingLevel` on fan-out agents. Otherwise many subagents can inherit an expensive global reasoning level.
148
167
 
168
+ `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. The built-in advisory workflows use `tools: ["read", "bash", "grep", "find", "ls"]` plus `toolHints: ["search"]`.
169
+
170
+ 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.
171
+
149
172
  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. Omitted models inherit the host/session default; malformed or unknown explicit refs fail fast.
150
173
 
174
+ ### Mutating agents in worktrees
175
+
176
+ Use `isolation: "worktree"` only for agents that should edit files. The agent runs in a detached git worktree, the engine captures its net patch, then the worktree is removed. The user's working tree is not changed.
177
+
178
+ ```ts
179
+ const edit = await agent("Rename this helper and update its call sites.", {
180
+ tools: ["read", "bash", "edit", "grep", "find", "ls"],
181
+ isolation: "worktree",
182
+ thinkingLevel: "medium",
183
+ });
184
+
185
+ if (edit.changed) {
186
+ return { summary: "Patch ready for review.", patch: edit.patch };
187
+ }
188
+ return { summary: edit.result };
189
+ ```
190
+
191
+ Isolated agents require the workflow `cwd` to be inside a git work tree. Outside git, the agent fails fast rather than silently mutating the shared directory. The return value is `{ result, patch, changed }`, where `result` is the normal text or structured `agent()` result and `patch` is a `git diff HEAD` patch. Worktree setup costs disk and git process time, so keep it opt-in for mutating/parallel-edit stages.
192
+
151
193
  ### Compose workflows
152
194
 
153
195
  `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, abort signal, and perf timing, and its phases nest under `<name> ▸ <phase>` in the live UI.
@@ -159,7 +201,7 @@ export default async function run({ workflow }: WorkflowApi) {
159
201
  }
160
202
  ```
161
203
 
162
- Nesting is one level only: calling `workflow()` from inside a sub-workflow rejects. Resolution throws on an unknown name — wrap the call in `try/catch` if a missing sub-workflow should be non-fatal (note `parallel()` rejects the whole batch on the first error, so catch inside each thunk for per-branch resilience).
204
+ 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.
163
205
 
164
206
  ### Where workflows live
165
207
 
@@ -203,6 +245,7 @@ Inline rules:
203
245
  - default-export an async workflow function;
204
246
  - no imports or dynamic `import()`;
205
247
  - use the injected `Type` object for schemas;
248
+ - add `skills: ["skill-name"]` per `agent()` call when a subagent should use a pi skill; no skills are exposed by default;
206
249
  - no extra exports or code after the default export.
207
250
 
208
251
  Inline workflows run in-process with extension permissions, so treat them as trusted task automation.
@@ -215,6 +258,7 @@ Only tune these when a workflow is too slow, too expensive, or too noisy:
215
258
  | --- | --- |
216
259
  | `--concurrency=N` / `PI_WORKFLOW_CONCURRENCY=N` | Cap concurrent subagents. Default is `min(8, max(2, CPU count))`. |
217
260
  | `--parallel-limit=N` / `PI_WORKFLOW_PARALLEL_SUBMISSION_LIMIT=N` | Limit eager `parallel()` submission. |
261
+ | `--budget=N` / `PI_WORKFLOW_BUDGET=N` | Set an output-token ceiling for completed subagents. `agent()` throws `WorkflowBudgetExceededError` before starting another agent once the ceiling is reached; agents already running may overshoot because the engine does not reserve per-agent estimates. |
218
262
  | `--perf` / `PI_WORKFLOW_PERF=1` | Include internal timing aggregates. Usage/cost totals are reported separately from perf. |
219
263
  | `PI_WORKFLOW_LANE_ITEM_LIMIT=N` | Cap retained progress lane items. |
220
264
 
@@ -223,4 +267,5 @@ Only tune these when a workflow is too slow, too expensive, or too noisy:
223
267
  - **Unknown workflow**: confirm the extension is loaded with `pi list`; use `--refresh` for new drop-ins.
224
268
  - **Inline compile error**: check the inline rules above.
225
269
  - **Slow run**: lower fan-out, set `thinkingLevel`, or reduce `--concurrency`.
270
+ - **Budget exhausted**: narrow the target, raise `--budget`, reduce fan-out/concurrency, or guard custom loops with `api.budget.remaining()`.
226
271
  - **Duplicate command/tool warnings while developing**: avoid loading both the global package and the working copy.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-workflow-engine",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "Zero-dependency Dynamax workflows for pi: live parallel subagents, typed handoffs, synthesized results.",
6
6
  "keywords": [
@@ -49,14 +49,14 @@
49
49
  "prepublishOnly": "tsgo --noEmit && bun run test"
50
50
  },
51
51
  "peerDependencies": {
52
- "@earendil-works/pi-agent-core": "*",
53
- "@earendil-works/pi-ai": "*",
54
- "@earendil-works/pi-coding-agent": "*",
55
- "@earendil-works/pi-tui": "*",
56
- "typebox": "*"
52
+ "@earendil-works/pi-agent-core": "0.79.10",
53
+ "@earendil-works/pi-ai": "0.79.10",
54
+ "@earendil-works/pi-coding-agent": "0.79.10",
55
+ "@earendil-works/pi-tui": "0.79.10",
56
+ "typebox": "1.3.0"
57
57
  },
58
58
  "devDependencies": {
59
- "@types/node": "^22",
60
- "@typescript/native-preview": "^7.0.0-dev.20260615.1"
59
+ "@types/node": "^22.20.0",
60
+ "@typescript/native-preview": "^7.0.0-dev.20260623.1"
61
61
  }
62
62
  }