pi-taskflow 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,94 +1,238 @@
1
1
  # pi-taskflow
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/pi-taskflow.svg)](https://www.npmjs.com/package/pi-taskflow)
4
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
5
+ [![Pi coding agent](https://img.shields.io/badge/for-Pi%20coding%20agent-7c3aed.svg)](https://pi.dev)
6
+
3
7
  > Lightweight workflow orchestration for the [Pi coding agent](https://pi.dev).
4
8
 
5
- Describe a multi-phase workflow once; let a deterministic runtime orchestrate
6
- isolated subagents to execute it. Intermediate results stay **out of your main
7
- context** only the final answer comes back. Inspired by Claude Code's Dynamic
8
- Workflows, rebuilt as a lightweight, declarative pi extension.
9
+ **Orchestrate your Pi subagents. Not by prompting by declaring.**
10
+
11
+ If you've used the built-in subagent tool's `task` / `tasks` / `chain`, you
12
+ already know the shorthand — your runs just get tracked, resumable, and
13
+ saveable as a one-word `/tf:<name>` command.
9
14
 
10
15
  ```bash
11
16
  pi install npm:pi-taskflow
12
17
  ```
13
18
 
19
+ Fan out one subagent per item, gate the results with an adversarial review, and
20
+ get back only the final report — none of the intermediate transcripts ever touch
21
+ your conversation.
22
+
14
23
  ## Why
15
24
 
16
- `subagent` is great for a single delegated task. But when a job needs **many
17
- coordinated steps**, **fan-out over dozens of items**, **cross-checked review**,
18
- or a **repeatable** pipeline, you want orchestration — without bloating your
19
- conversation with every step's transcript.
25
+ The built-in subagent tool is great for a single delegated task. But when a job
26
+ needs many coordinated steps, fan-out over dozens of items, cross-checked review,
27
+ or a repeatable pipeline, you want orchestration — without the intermediate
28
+ transcripts eating your context window.
20
29
 
21
30
  `pi-taskflow` moves the plan into a small declarative definition. The runtime
22
31
  holds the DAG, the loops, and the intermediate results; your context receives
23
32
  only the final phase's output.
24
33
 
25
- | | `subagent` | `pi-taskflow` |
34
+ | | `subagent` tool | `pi-taskflow` |
26
35
  |---|---|---|
27
36
  | Who drives | the model, turn by turn | the runtime, from a definition |
28
37
  | Intermediate results | in your context window | in the runtime (not your context) |
29
38
  | Reusable | re-described each time | saved as `/tf:<name>` |
30
39
  | Scale | a few tasks | dynamic `map` fan-out |
31
- | Resumable | no | yes (cross-session) |
40
+ | Resumable | no | yes (cross-session, cached phases skip) |
41
+ | Quality gates | no | `gate` phases with `VERDICT: BLOCK / PASS` |
42
+ | Progress visibility | opaque while running | live DAG render with timing + cost |
43
+ | Ergonomics | inline JSON each time | shorthand (`task`/`tasks`/`chain`) or DSL |
44
+
45
+ ## Show me
46
+
47
+ Describe a pipeline once, then run it from a pi session by name:
48
+
49
+ > `/tf:summarize-files dir=src`
50
+
51
+ The runtime fans out one subagent per file, merges the summaries in a `reduce`
52
+ phase, and returns only the final overview. Every intermediate transcript stays
53
+ in the runtime — never in your context window. (Full definition in
54
+ [Quickstart](#then-go-declarative) below.)
55
+
56
+ ## Quickstart
57
+
58
+ ### Shorthand: same effort as `subagent`, but tracked & resumable
59
+
60
+ **Single task** — one agent, one job:
61
+
62
+ ```jsonc
63
+ { "task": "Summarize the architecture of src/", "agent": "explorer" }
64
+ ```
65
+
66
+ **Parallel tasks** — fire several at once, outputs merge:
67
+
68
+ ```jsonc
69
+ { "tasks": [
70
+ { "task": "Audit auth in src/api", "agent": "analyst" },
71
+ { "task": "Audit input validation in src/api", "agent": "analyst" }
72
+ ] }
73
+ ```
32
74
 
33
- ## Concepts
75
+ **Chain** — sequential, each step sees the previous one's output:
76
+
77
+ ```jsonc
78
+ { "chain": [
79
+ { "task": "List the public API of src/lib", "agent": "scout" },
80
+ { "task": "Write docs for:\n{previous.output}", "agent": "writer" }
81
+ ] }
82
+ ```
34
83
 
35
- A **taskflow** is a set of **phases** forming a DAG via `dependsOn`. Each phase
36
- delegates to a subagent (an isolated `pi` process). Phases in the same DAG layer
37
- run concurrently (bounded by `concurrency`).
84
+ `agent` is optional (defaults to the first available agent). Add `name` to label
85
+ the run and enable saving it as a reusable command.
38
86
 
39
- ### Phase types
87
+ Try it inline — tell the model something like:
40
88
 
41
- | type | meaning |
42
- |------|---------|
43
- | `agent` | one subagent runs `task` |
44
- | `parallel` | run `branches[]` concurrently |
45
- | `map` | fan out over an array — one subagent per item, `{item}` bound |
46
- | `gate` | quality/adversarial-review step |
47
- | `reduce` | aggregate several phases' outputs into one |
89
+ > Run a chain: first explore the auth flow, then summarize findings.
48
90
 
49
- ### Interpolation
91
+ The model calls the `taskflow` tool; you get live progress, per-step timing,
92
+ token cost, and a run record. Ask to `save` it and you get `/tf:<name>`.
50
93
 
51
- - `{args.X}` invocation argument
52
- - `{steps.ID.output}` — a prior phase's text output
53
- - `{steps.ID.json}` / `{steps.ID.json.field}` — prior output parsed as JSON
54
- - `{item}` / `{item.field}` — current item inside a `map` phase
55
- - `{previous.output}` — the immediately-upstream phase output
94
+ ### Then go declarative
56
95
 
57
- ## Example
96
+ When your pipeline outgrows the shorthand — when you need dynamic fan-out,
97
+ intermediate JSON routing, or quality gates — graduate to the full DSL:
58
98
 
59
99
  ```jsonc
60
100
  {
61
101
  "name": "summarize-files",
102
+ "description": "Discover files, summarize each, produce a report",
62
103
  "args": { "dir": { "default": "." } },
63
- "concurrency": 4,
104
+ "concurrency": 8,
64
105
  "phases": [
65
106
  { "id": "discover", "type": "agent", "agent": "scout",
66
- "task": "List source files under {args.dir}. Output ONLY a JSON array [{\"file\":\"\"}].",
107
+ "task": "List source files under {args.dir} (non-recursive).\nOutput ONLY a JSON array [{\"file\":\"\"}]. No prose.",
67
108
  "output": "json" },
68
- { "id": "summarize", "type": "map", "over": "{steps.discover.json}", "as": "item",
69
- "agent": "scout", "task": "Read {item.file} and summarize it in one sentence.",
109
+ { "id": "summarize", "type": "map",
110
+ "over": "{steps.discover.json}", "as": "item",
111
+ "agent": "scout",
112
+ "task": "Read {item.file} and give a one-sentence summary.",
70
113
  "dependsOn": ["discover"] },
71
- { "id": "report", "type": "reduce", "from": ["summarize"], "agent": "writer",
114
+ { "id": "report", "type": "reduce", "from": ["summarize"],
115
+ "agent": "writer",
72
116
  "task": "Combine into a short overview:\n{steps.summarize.output}",
73
117
  "dependsOn": ["summarize"], "final": true }
74
118
  ]
75
119
  }
76
120
  ```
77
121
 
78
- ## Usage
122
+ What this does:
123
+
124
+ 1. **`discover`** — an agent lists every file in the directory and outputs a JSON array.
125
+ 2. **`summarize`** — a `map` fans out, spawning one subagent per file in parallel
126
+ (throttled to 8 concurrent). Each gets `{item.file}` bound to its file path.
127
+ 3. **`report`** — a `reduce` merges all summaries into one clean overview.
128
+
129
+ Intermediate outputs never enter your context. The runtime owns them. You get
130
+ only the final report back.
131
+
132
+ Save it once → `/tf:summarize-files` forever.
79
133
 
80
- The model calls the `taskflow` tool; you can also drive it directly:
134
+ ## Watch it run
135
+
136
+ This is the live progress render for a real run — the `self-improve` flow that
137
+ writes and verifies its own test suites, caught here mid-block by a quality gate:
81
138
 
82
139
  ```
83
- /tf list # saved flows
84
- /tf run <name> [args] # run a saved flow
85
- /tf show <name> # print a definition
86
- /tf runs # recent run history
87
- /tf resume <runId> # continue a paused/failed run (cached phases skipped)
88
- /tf:<name> [args] # shortcut per saved flow
140
+ ⊗ taskflow self-improve 6/7 · blocked · $0.095
141
+ discover agent deepseek-v4-flash 10t ↑38k ↓6.7k $0.011
142
+ write-runner-tests agent claude-sonnet-4-6 10t ↑13 ↓6.6k $0.020
143
+ write-store-tests agent claude-sonnet-4-6 10t ↑11 ↓10k $0.018
144
+ write-agents-tests agent claude-sonnet-4-6 10t ↑28 ↓13k $0.030
145
+ fix-stability agent claude-sonnet-4-6 10t ↑13 ↓3.9k $0.012
146
+ ✓ verify gate BLOCK 3 type errors in test files deepseek-v4-flash
147
+ ⊘ report reduce skipped · Gate blocked ↳ fix-stability
89
148
  ```
90
149
 
91
- Tool actions: `run` (inline `define` or saved `name`), `save`, `resume`, `list`.
150
+ **How to read it the layout *is* the DAG:**
151
+
152
+ - **Header** — `⊗` means the flow is blocked (a gate halted it); `6/7` phases
153
+ processed, aggregate cost `$0.095`.
154
+ - **Status icons** — `✓` done, `◐` running, `✗` failed, `⊘` skipped, `○` pending.
155
+ - **Rail `┌ ├ └`** — phases in the same DAG layer, running concurrently. The four
156
+ `write-*`/`fix-stability` tasks all fan out from `discover`. A blank gutter is
157
+ a single-phase layer.
158
+ - **`↳`** — a long (layer-skipping) dependency. `report` depends on `verify` (the
159
+ adjacent layer, implied by position) *and* `fix-stability` two layers back, so
160
+ only that skip edge is annotated.
161
+ - **Gate** — `verify` emitted `VERDICT: BLOCK`, so the runtime skipped `report`
162
+ and ended the run as `blocked`, surfacing the reason.
163
+ - **Detail** — per phase: model, token counts (`↑`in `↓`out), cost, and timing.
164
+ Fan-out phases also show sub-task progress.
165
+
166
+ ## Phase types
167
+
168
+ | type | meaning | required fields |
169
+ |------|---------|-----------------|
170
+ | `agent` | one subagent runs a single task | `task` |
171
+ | `parallel` | run `branches[]` concurrently | `branches` (array of `{task, agent?}`) |
172
+ | `map` | fan out over an array — one subagent per item, `{item}` bound | `over`, `task` |
173
+ | `gate` | quality/review step that can **halt the flow** | `task` |
174
+ | `reduce` | aggregate `from[]` phase outputs into one | `from`, `task` |
175
+
176
+ Every phase needs `id`. Optional fields: `agent`, `dependsOn`, `output`,
177
+ `model`, `thinking`, `tools`, `cwd`, `concurrency`, `final`, `optional`.
178
+
179
+ ### `output` format
180
+
181
+ - `output: "text"` (default) — the raw subagent output.
182
+ - `output: "json"` — the subagent output is parsed as JSON and exposed via
183
+ `{steps.ID.json}` / `{steps.ID.json.field}`. Set this on phases whose output
184
+ a downstream `map` or `reduce` needs to consume as structured data.
185
+
186
+ There is no `output: "file"`. For file-based output, have the agent write to
187
+ disk with a `write` tool call.
188
+
189
+ ### Gate phases (quality control)
190
+
191
+ A `gate` runs an agent to review upstream output and can **block the rest
192
+ of the workflow**. End the gate task's instructions by asking the agent to
193
+ emit a verdict the runtime can read:
194
+
195
+ - a final line `VERDICT: PASS` or `VERDICT: BLOCK` (also accepts `OK`, `FAIL`,
196
+ `STOP`, `REJECT`, `HALT` — last occurrence wins), or
197
+ - JSON like `{"continue": false, "reason": "missing auth checks"}` /
198
+ `{"verdict": "block", "reason": "..."}`.
199
+
200
+ On **BLOCK**, downstream phases are skipped and the run ends as `blocked` with
201
+ the reason surfaced. **Ambiguous output fails open** (treated as PASS) — a gate
202
+ never halts the flow by accident.
203
+
204
+ ```
205
+ Review the audit results below. If any endpoint is missing auth, end with
206
+ "VERDICT: BLOCK" and a one-line reason; otherwise end with "VERDICT: PASS".
207
+
208
+ {steps.audit.output}
209
+ ```
210
+
211
+ ## Interpolation
212
+
213
+ | placeholder | resolves to |
214
+ |---|---|
215
+ | `{args.X}` | invocation argument |
216
+ | `{steps.ID.output}` | a prior phase's text output |
217
+ | `{steps.ID.json}` | prior output parsed as JSON (or `{steps.ID.json.field}`) |
218
+ | `{item}` / `{item.field}` | current item inside a `map` phase |
219
+ | `{previous.output}` | the immediately-upstream phase output |
220
+
221
+ ## Commands
222
+
223
+ Saved flows become CLI shortcuts. All commands work in the pi session:
224
+
225
+ | Command | What it does |
226
+ |---|---|
227
+ | `/tf list` | List all saved flows |
228
+ | `/tf run <name> [args]` | Run a saved flow (e.g. `/tf run summarize-files dir=src`) |
229
+ | `/tf show <name>` | Print a flow's definition |
230
+ | `/tf runs` | Browse recent run history (interactive TUI) |
231
+ | `/tf resume <runId>` | Continue a paused/failed run — cached phases skip automatically |
232
+ | `/tf:<name> [args]` | Shortcut — runs the flow in one tap |
233
+
234
+ Tool actions (used by the model): `run` (inline `define` or saved `name`),
235
+ `save`, `resume`, `list`.
92
236
 
93
237
  ## Storage
94
238
 
@@ -98,30 +242,70 @@ Tool actions: `run` (inline `define` or saved `name`), `save`, `resume`, `list`.
98
242
  .pi/taskflows/runs/<runId>.json # run state (resume); gitignore this
99
243
  ```
100
244
 
245
+ Agent discovery scope (set via `agentScope` in the flow definition):
246
+
247
+ | value | discovers agents from |
248
+ |---|---|
249
+ | `"user"` (default) | `~/.pi/agent/agents/*.md` |
250
+ | `"project"` | `.pi/agents/*.md` (walks up the tree) |
251
+ | `"both"` | user + project; project wins on name collision |
252
+
101
253
  ## Agents
102
254
 
103
- Taskflow reuses your existing pi agents (`~/.pi/agent/agents/*.md`,
104
- `.pi/agents/*.md`) and honors `subagents.agentOverrides` in settings. Reference
105
- agents by `name`.
255
+ Taskflow reuses your existing pi agent files (`~/.pi/agent/agents/*.md`,
256
+ `.pi/agents/*.md`). Reference agents by `name` in a phase or shorthand.
257
+
258
+ When running a phase, the runtime extracts the agent's `systemPrompt` from its
259
+ `.md` frontmatter and passes it via `--append-system-prompt` (written to a temp
260
+ file). Phase-level overrides for `model`, `thinking`, and `tools` are passed as
261
+ `--model` / `--thinking` / `--tools` flags to the subagent invocation.
262
+
263
+ Settings from `~/.pi/agent/settings.json` (the `subagents.agentOverrides` map)
264
+ are honored, letting you tweak model, thinking, or tools per agent across all flows.
265
+
266
+ ## Status & limits
267
+
268
+ - **v0.0.4** — DSL + DAG runtime (`agent`/`parallel`/`map`/`gate`/`reduce`),
269
+ inline + saved flows, cross-session resume, live progress, isolated context.
270
+ Default `concurrency` is 8 (set on the flow; per-phase `concurrency` overrides
271
+ for that phase).
272
+ - A run executes as one streaming tool call (live progress while it runs).
273
+ - `map` requires the upstream phase to emit a JSON array (`output: "json"`).
274
+ - Gate verdicts are **fail-open**: if the agent output contains no recognizable
275
+ verdict marker (`VERDICT: BLOCK/PASS/OK/FAIL/STOP/REJECT/HALT` or
276
+ `{continue: false}` / `{verdict: "block"}`), the gate passes. This prevents
277
+ an accidental missing verdict from blocking your workflow.
278
+
279
+ ### What it doesn't do (yet)
280
+
281
+ - **No detached background execution.** A run needs the pi session to stay open.
282
+ True background execution is on the roadmap.
283
+ - **No `output: "file"`.** Outputs are text/JSON only. Write files via agent
284
+ tool calls if needed.
285
+ - **`map` requires a JSON array.** The `over` field must resolve to
286
+ `{steps.ID.json}` where the upstream phase emitted `output: "json"`. If the
287
+ source is a plain text list, wrap it in a single-agent phase that outputs JSON.
288
+ - **Cycles are rejected at validation.** The DAG must be acyclic.
106
289
 
107
290
  ## Development
108
291
 
109
292
  ```bash
110
293
  npm install
111
294
  npm run typecheck
112
- node --experimental-strip-types --test test/interpolate.test.ts test/schema.test.ts test/runtime.test.ts
295
+ node --experimental-strip-types --test test/interpolate.test.ts \
296
+ test/schema.test.ts test/runtime.test.ts test/runner.test.ts \
297
+ test/store.test.ts test/agents.test.ts test/render.test.ts \
298
+ test/desugar.test.ts
113
299
 
114
300
  # real end-to-end (spawns live subagents; needs model access)
115
301
  PI_TASKFLOW_PI_BIN=pi node --experimental-strip-types test/e2e.mts
116
302
  ```
117
303
 
118
- ## Status & limits
304
+ ## Contributing
119
305
 
120
- - **v0.1**DSL + DAG runtime (`agent`/`parallel`/`map`/`gate`/`reduce`),
121
- inline + saved flows, cross-session resume, live progress, isolated context.
122
- - A run executes as one streaming tool call (live progress while it runs). True
123
- detached background execution is on the roadmap.
124
- - `map` requires the upstream phase to emit a JSON array (`output: "json"`).
306
+ Contributions welcome! This is a young project open an issue or PR on
307
+ [GitHub](https://github.com/heggria/pi-taskflow). Tests live in `test/`, the
308
+ runtime in `extensions/`.
125
309
 
126
310
  ## License
127
311
 
@@ -366,16 +366,19 @@ export async function executeTaskflow(state: RunState, deps: RuntimeDeps): Promi
366
366
  return;
367
367
  }
368
368
 
369
+ const startedAt = Date.now();
369
370
  state.phases[phase.id] = {
370
371
  ...(state.phases[phase.id] ?? { id: phase.id }),
371
372
  id: phase.id,
372
373
  status: "running",
373
- startedAt: Date.now(),
374
+ startedAt,
374
375
  };
375
376
  deps.onProgress?.(state);
376
377
 
377
378
  const ps = await executePhase(phase, state, deps, prior, () => deps.onProgress?.(state));
378
- state.phases[phase.id] = ps;
379
+ // Preserve the phase start time: executePhase returns a fresh PhaseState
380
+ // that omits startedAt (cached/resumed results carry their own).
381
+ state.phases[phase.id] = ps.startedAt ? ps : { ...ps, startedAt };
379
382
  if ((phase.type ?? "agent") === "gate" && ps.gate?.verdict === "block") {
380
383
  gateBlocked = true;
381
384
  gateReason = ps.gate.reason ?? "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-taskflow",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Lightweight workflow orchestration for the Pi coding agent — declarative multi-phase taskflows with dynamic fan-out, isolated subagent context, resumable runs, and saveable commands.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -123,6 +123,20 @@ Review the audit results below. If any endpoint is missing auth, end with
123
123
  3. Reference upstream results explicitly with `{steps.ID...}` and set `dependsOn`.
124
124
  4. Mark the result-bearing phase with `"final": true` (else the last phase wins).
125
125
 
126
+ ## Configuration
127
+
128
+ For the full set of knobs — per-phase `model`/`thinking`/`tools`/`cwd`, the
129
+ two-level concurrency model, model/thinking/tools resolution precedence,
130
+ `agentScope` & agent discovery, `settings.json` overrides, environment
131
+ variables, and storage paths — read `configuration.md` (next to this file).
132
+
133
+ Quick reference:
134
+
135
+ - **Flow:** `name`, `description`, `concurrency` (default 8), `agentScope` (user|project|both), `args`.
136
+ - **Phase:** `model`, `thinking`, `tools` (whitelist), `cwd`, `output:"json"`, `concurrency` (map/parallel fan-out), `final`.
137
+ - **Precedence (model/thinking/tools):** phase value → `settings.subagents.agentOverrides[agent]` → agent frontmatter → global/default.
138
+ - **Concurrency:** same-layer phases use `flow.concurrency`; a `map`/`parallel` phase uses `phase.concurrency ?? flow.concurrency ?? 8`.
139
+
126
140
  ## Actions
127
141
 
128
142
  - `action: "run"` — run inline `define` or a saved `name` (with optional `args`).
@@ -0,0 +1,275 @@
1
+ # Taskflow Configuration Reference
2
+
3
+ Every knob you can set on a taskflow, where it lives, and how the values are
4
+ resolved. Read this when you need fine control over models, concurrency, agent
5
+ discovery, working directories, tool restrictions, or storage.
6
+
7
+ Configuration lives in **five layers**, from most local to most global:
8
+
9
+ | Layer | Where | Sets |
10
+ |-------|-------|------|
11
+ | Phase | a phase object in the DSL | per-step model/thinking/tools/cwd/output/concurrency |
12
+ | Flow | the top-level DSL object | name, args, default concurrency, agent scope |
13
+ | Agent | `~/.pi/agent/agents/*.md`, `.pi/agents/*.md` frontmatter | per-agent default model/thinking/tools + system prompt |
14
+ | Settings | `~/.pi/agent/settings.json` | `subagents.agentOverrides`, global thinking |
15
+ | Environment | shell env | `PI_TASKFLOW_PI_BIN` |
16
+
17
+ ---
18
+
19
+ ## 1. Flow-level options
20
+
21
+ Top-level keys of the taskflow definition object.
22
+
23
+ ```jsonc
24
+ {
25
+ "name": "audit-endpoints", // required — also becomes /tf:<name> when saved
26
+ "description": "Audit API auth", // shown in /tf list and the command palette
27
+ "concurrency": 8, // default max concurrent subagents (default: 8)
28
+ "agentScope": "user", // user | project | both (default: user)
29
+ "args": { /* see §3 */ },
30
+ "phases": [ /* see §2 */ ] // required, at least one phase
31
+ }
32
+ ```
33
+
34
+ | Key | Type | Default | Notes |
35
+ |-----|------|---------|-------|
36
+ | `name` | string | — | **Required.** Saved as `/tf:<name>`. |
37
+ | `description` | string | — | Surfaced in `/tf list` and the slash-command. |
38
+ | `concurrency` | number | `8` | Default fan-out / same-layer parallelism cap. See §4. |
39
+ | `agentScope` | `user`\|`project`\|`both` | `user` | Which agent dirs to load. See §6. |
40
+ | `args` | record | `{}` | Declared invocation arguments. See §3. |
41
+ | `phases` | array | — | **Required.** The phase DAG. See §2. |
42
+ | `version` | number | `1` | ⚠️ Declared in schema but **not yet used** by the runtime. |
43
+
44
+ ---
45
+
46
+ ## 2. Phase-level options
47
+
48
+ Keys of each object in `phases[]`. Some only apply to specific `type`s.
49
+
50
+ ```jsonc
51
+ {
52
+ "id": "audit", // required, unique — referenced via {steps.audit.output}
53
+ "type": "map", // agent | parallel | map | gate | reduce (default: agent)
54
+ "agent": "analyst", // agent name to run this phase
55
+ "task": "Audit {item.route}…",
56
+ "dependsOn": ["discover"],// DAG edges
57
+ "over": "{steps.discover.json}", // [map] array to fan out over
58
+ "as": "item", // [map] loop var name (default: item)
59
+ "branches": [ /* … */ ], // [parallel] static task list
60
+ "from": ["audit"], // [reduce] phase ids to aggregate
61
+ "output": "json", // text | json (default: text)
62
+ "model": "claude-sonnet-4-5", // per-phase model override
63
+ "thinking": "high", // per-phase thinking override
64
+ "tools": ["read","bash"], // restrict tools for this phase's subagent
65
+ "cwd": "packages/api", // working directory for this phase's subagent
66
+ "concurrency": 4, // [map/parallel] fan-out cap for THIS phase
67
+ "final": true // mark this phase's output as the workflow result
68
+ }
69
+ ```
70
+
71
+ | Key | Applies to | Default | Notes |
72
+ |-----|-----------|---------|-------|
73
+ | `id` | all | — | **Required, unique.** Used in `{steps.<id>…}`. |
74
+ | `type` | all | `agent` | One of the 5 phase types. |
75
+ | `agent` | all | first available | Agent name; resolved from the scoped pool. |
76
+ | `task` | agent, gate, map, reduce | — | Prompt; supports interpolation. Required for these types. |
77
+ | `over` | map | — | **Required for map.** Must resolve to an array. |
78
+ | `as` | map | `item` | Loop variable bound per item. |
79
+ | `branches` | parallel | — | **Required for parallel.** `[{task, agent?}]`. |
80
+ | `from` | reduce | — | **Required for reduce.** Phase ids whose outputs are aggregated. |
81
+ | `dependsOn` | all | `[]` | DAG edges. `from` also implies a dependency. |
82
+ | `output` | all | `text` | `json` parses output so `{steps.id.json}` / map `over` work. |
83
+ | `model` | all | agent/global | Per-phase model override. See §5. |
84
+ | `thinking` | all | agent/global | Per-phase thinking level. See §5. |
85
+ | `tools` | all | agent default | Whitelist of tools for the subagent. See §5. |
86
+ | `cwd` | all | flow cwd | Run this phase's subagent in a different directory. |
87
+ | `concurrency` | map, parallel | flow concurrency | Fan-out cap for this phase only. See §4. |
88
+ | `final` | all | last phase | Exactly one phase may be `final`; its output is returned. |
89
+ | `optional` | all | `false` | ⚠️ Declared in schema but **not yet enforced** — a failed phase still skips downstream. |
90
+
91
+ ---
92
+
93
+ ## 3. Declaring & passing arguments
94
+
95
+ Declare arguments on the flow, then reference them with `{args.X}`.
96
+
97
+ ```jsonc
98
+ "args": {
99
+ "dir": { "default": "src", "description": "Directory to scan" },
100
+ "depth": { "default": 2 },
101
+ "token": { "required": true, "description": "API token" }
102
+ }
103
+ ```
104
+
105
+ | Field | Notes |
106
+ |-------|-------|
107
+ | `default` | Used when the caller omits the arg. |
108
+ | `description` | Documentation only. |
109
+ | `required` | ⚠️ Declared but **not enforced** at runtime — treat as documentation for now. |
110
+
111
+ **Resolution:** for each declared arg, the provided value wins, else its
112
+ `default`. Any extra provided keys are also passed through (so undeclared args
113
+ still reach `{args.X}`).
114
+
115
+ **Passing args:**
116
+
117
+ ```
118
+ /tf run audit-endpoints {"dir":"packages/api"} # JSON
119
+ /tf run audit-endpoints dir=packages/api depth=3 # key=value pairs
120
+ /tf run audit-endpoints packages/api # single positional → first declared arg
121
+ ```
122
+
123
+ Via the tool: `{ "action": "run", "name": "audit-endpoints", "args": { "dir": "packages/api" } }`.
124
+
125
+ ---
126
+
127
+ ## 4. Concurrency model
128
+
129
+ There are **two independent concurrency limits**:
130
+
131
+ 1. **Same-layer parallelism** — phases with no dependency between them sit in the
132
+ same topological layer and run concurrently, bounded by **`flow.concurrency`**
133
+ (default `8`).
134
+ 2. **Fan-out within a `map`/`parallel` phase** — bounded by
135
+ **`phase.concurrency ?? flow.concurrency ?? 8`**.
136
+
137
+ ```jsonc
138
+ {
139
+ "concurrency": 6, // ≤6 sibling phases run at once
140
+ "phases": [
141
+ { "id": "scan", "type": "map", "over": "{steps.list.json}",
142
+ "concurrency": 3, // …but this map only fans out 3 at a time
143
+ "task": "…", "dependsOn": ["list"] }
144
+ ]
145
+ }
146
+ ```
147
+
148
+ Set a low `phase.concurrency` to protect rate-limited models or heavy bash work;
149
+ keep `flow.concurrency` higher to let independent phases overlap.
150
+
151
+ ---
152
+
153
+ ## 5. Model, thinking & tools resolution
154
+
155
+ For any phase, the effective value is resolved in this **precedence order**
156
+ (first defined wins):
157
+
158
+ | Setting | Precedence (high → low) |
159
+ |---------|-------------------------|
160
+ | **model** | `phase.model` → `settings.agentOverrides[agent].model` → agent frontmatter `model` → pi default |
161
+ | **thinking** | `phase.thinking` → `settings.agentOverrides[agent].thinking` → agent frontmatter `thinking` → `settings` global thinking → pi default |
162
+ | **tools** | `phase.tools` → `settings.agentOverrides[agent].tools` → agent frontmatter `tools` → all tools |
163
+
164
+ Notes:
165
+ - `tools` is a **whitelist** passed as `--tools a,b,c`. Omit it to allow all.
166
+ - Each phase runs as an isolated process:
167
+ `pi --mode json -p --no-session [--model …] [--thinking …] [--tools …] [--append-system-prompt <agent>] "Task: …"`.
168
+ - The agent's markdown body becomes the subagent's appended system prompt.
169
+
170
+ ---
171
+
172
+ ## 6. Agent discovery & scope
173
+
174
+ `flow.agentScope` controls which agent directories are loaded:
175
+
176
+ | Scope | Loads from |
177
+ |-------|-----------|
178
+ | `user` (default) | `~/.pi/agent/agents/*.md` |
179
+ | `project` | nearest `.pi/agents/*.md` found walking up from cwd |
180
+ | `both` | user **then** project (project overrides on name collision) |
181
+
182
+ - Agents are `.md` files with frontmatter `name` + `description` (required), plus
183
+ optional `model`, `thinking`, `tools`. The body is the system prompt.
184
+ - Reference agents in phases by their `name`. An unknown name fails that phase
185
+ with the list of available agents.
186
+ - If a phase omits `agent`, the **first discovered agent** is used.
187
+
188
+ ---
189
+
190
+ ## 7. settings.json
191
+
192
+ Taskflow shares the subagent settings file at `~/.pi/agent/settings.json`:
193
+
194
+ ```jsonc
195
+ {
196
+ "subagents": {
197
+ "globalThinking": "medium", // fallback thinking for all subagents
198
+ "agentOverrides": {
199
+ "analyst": { "model": "claude-sonnet-4-5", "thinking": "high" },
200
+ "scout": { "tools": ["read", "bash", "grep"] }
201
+ }
202
+ },
203
+ "defaultThinkingLevel": "low" // used if subagents.globalThinking is absent
204
+ }
205
+ ```
206
+
207
+ - `subagents.agentOverrides` — per-agent overrides applied at discovery; they beat
208
+ agent frontmatter but lose to a phase-level value (see §5).
209
+ - `subagents.globalThinking` (or top-level `defaultThinkingLevel`) — global
210
+ thinking fallback.
211
+
212
+ ---
213
+
214
+ ## 8. Environment variables
215
+
216
+ | Variable | Effect |
217
+ |----------|--------|
218
+ | `PI_TASKFLOW_PI_BIN` | Override the `pi` binary used to spawn subagents. Used by tests and unusual launch setups (e.g. `PI_TASKFLOW_PI_BIN=pi`). Normally auto-detected. |
219
+
220
+ ---
221
+
222
+ ## 9. Storage & file locations
223
+
224
+ | What | Path | Commit? |
225
+ |------|------|---------|
226
+ | User-scoped flow | `~/.pi/agent/taskflows/<name>.json` | personal |
227
+ | Project-scoped flow | `<nearest .pi>/taskflows/<name>.json` | ✅ commit to share |
228
+ | Run state (resume) | `<project .pi>/taskflows/runs/<runId>.json` | ❌ gitignore |
229
+
230
+ - `action: "save"` takes `scope: "project"` (default) or `"user"`.
231
+ - Saved flows auto-register as `/tf:<name>` (immediately for the current session,
232
+ and on future `session_start`).
233
+ - Project flows override user flows on a name collision.
234
+ - Add `.pi/taskflows/runs/` to `.gitignore`.
235
+
236
+ ---
237
+
238
+ ## 10. Quick recipes
239
+
240
+ **Pin a strong model only for the review gate:**
241
+ ```jsonc
242
+ { "id": "review", "type": "gate", "agent": "reviewer",
243
+ "model": "claude-opus-4", "thinking": "high",
244
+ "task": "…\nVERDICT:", "dependsOn": ["audit"] }
245
+ ```
246
+
247
+ **Sandbox a phase to read-only in a subdirectory:**
248
+ ```jsonc
249
+ { "id": "scan", "type": "agent", "agent": "scout",
250
+ "cwd": "packages/api", "tools": ["read", "grep", "ls"],
251
+ "task": "List route files. Output ONLY a JSON array.", "output": "json" }
252
+ ```
253
+
254
+ **Throttle a rate-limited fan-out:**
255
+ ```jsonc
256
+ { "id": "summarize", "type": "map", "over": "{steps.scan.json}",
257
+ "concurrency": 2, "agent": "writer",
258
+ "task": "Summarize {item.file}.", "dependsOn": ["scan"] }
259
+ ```
260
+
261
+ **Project-only agents:**
262
+ ```jsonc
263
+ { "name": "ci-audit", "agentScope": "project", "phases": [ /* … */ ] }
264
+ ```
265
+
266
+ ---
267
+
268
+ ## Caveats (declared but not yet enforced)
269
+
270
+ These keys validate but the runtime does **not** act on them yet — don't rely on
271
+ them for behavior:
272
+
273
+ - `phase.optional` — a failed phase still marks downstream phases as skipped.
274
+ - `arg.required` — missing required args are not rejected.
275
+ - `flow.version` — informational only.