hermes-taskflow 0.2.9

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.
@@ -0,0 +1,598 @@
1
+ <!-- GENERATED FILE — do not edit. Source: skills-src/taskflow/configuration.md (npm run build:skills) -->
2
+
3
+ # Taskflow Configuration Reference
4
+
5
+ Every knob you can set on a taskflow, where it lives, and how the values are
6
+ resolved. Read this when you need fine control over models, concurrency, agent
7
+ discovery, working directories, tool restrictions, or storage.
8
+
9
+ > Companion files: `SKILL.md` (core DSL + actions), `patterns.md` (flow
10
+ > archetypes + production checklist), `advanced.md` (context sharing, dynamic
11
+ > sub-flows, workspace isolation, incremental recompute).
12
+
13
+ Configuration lives in **five layers**, from most local to most global:
14
+
15
+ | Layer | Where | Sets |
16
+ |-------|-------|------|
17
+ | Phase | a phase object in the DSL | per-step model/thinking/tools/cwd/output/concurrency |
18
+ | Flow | the top-level DSL object | name, args, default concurrency, agent scope |
19
+ | Agent | `~/.pi/agent/agents/*.md`, `.pi/agents/*.md` frontmatter | per-agent default model/thinking/tools + system prompt |
20
+ | Settings | `~/.pi/agent/settings.json` | `modelRoles`, global thinking |
21
+ | Environment | shell env | `PI_TASKFLOW_PI_BIN` |
22
+
23
+ ---
24
+
25
+ ## 1. Flow-level options
26
+
27
+ Top-level keys of the taskflow definition object.
28
+
29
+ ```jsonc
30
+ {
31
+ "name": "audit-endpoints", // required — also becomes /tf:<name> when saved
32
+ "description": "Audit API auth", // shown in /tf list and the command palette
33
+ "concurrency": 8, // default max concurrent subagents (default: 8)
34
+ "agentScope": "user", // user | project | both (default: user)
35
+ "args": { /* see §3 */ },
36
+ // 0.2.7: optional terminal hooks (summary payload only — never transcripts)
37
+ // "hooks": { "onComplete": [{ "type": "file", "path": ".taskflow/hooks/last.json" }] },
38
+ "phases": [ /* see §2 */ ] // required, at least one phase
39
+ }
40
+ ```
41
+
42
+ | Key | Type | Default | Notes |
43
+ |-----|------|---------|-------|
44
+ | `name` | string | — | **Required.** Saved as `/tf:<name>`. |
45
+ | `description` | string | — | Surfaced in `/tf list` and the slash-command. |
46
+ | `concurrency` | number | `8` | Default fan-out / same-layer parallelism cap. See §4. |
47
+ | `idleTimeout` | number | host default (`300000`) | Flow-level idle watchdog in ms (≥ 1000, or `0` to disable) for all agent-running phases that don't set their own. `0` disables the watchdog but then **every** agent-running phase MUST declare a finite wall `timeout` (≥ 1000) so the flow can never hang. A per-phase `idleTimeout` overrides this. |
48
+ | `agentScope` | `user`\|`project`\|`both` | `user` | Which agent dirs to load. See §6. |
49
+ | `args` | record | `{}` | Declared invocation arguments. See §3. |
50
+ | `hooks` | object | — | **0.2.7.** Terminal fire-and-forget notifications: `onComplete` / `onFail` / `onBlocked` arrays of `{type:"webhook"\|"file"\|"command", …}`. Payload is summary-only (`taskflow.hook.v1`) — never transcripts. Hook failure never changes run status. `https` or `http://127.0.0.1\|localhost` for webhooks; `command.run` is argv-only (no shell string). |
51
+ | `phases` | array | — | **Required.** The phase DAG. See §2. |
52
+ | `version` | number | `1` | Informational metadata in 0.2.x; it does not select runtime semantics or migrate a flow. |
53
+
54
+ ---
55
+
56
+ ## 2. Phase-level options
57
+
58
+ Keys of each object in `phases[]`. Some only apply to specific `type`s.
59
+
60
+ ```jsonc
61
+ {
62
+ "id": "audit", // required, unique — referenced via {steps.audit.output}
63
+ "type": "map", // agent | parallel | map | gate | reduce | approval | flow | loop | tournament | script | race | expand (default: agent)
64
+ "agent": "analyst", // agent name to run this phase
65
+ "task": "Audit {item.route}…",
66
+ "dependsOn": ["discover"],// DAG edges
67
+ "over": "{steps.discover.json}", // [map] array to fan out over
68
+ "as": "item", // [map] loop var name (default: item)
69
+ "branches": [ /* … */ ], // [parallel|race] static task list
70
+ "from": ["audit"], // [reduce] phase ids to aggregate
71
+ "def": "{steps.plan.json}", // [expand|flow] inline fragment / dynamic sub-flow
72
+ "expandMode": "nested", // [expand] nested | graft
73
+ "output": "json", // text | json (default: text)
74
+ "model": "claude-sonnet-4-5", // per-phase model override
75
+ "thinking": "high", // per-phase thinking override
76
+ "tools": ["read","bash"], // restrict tools for this phase's subagent
77
+ "cwd": "packages/api", // working directory for this phase's subagent
78
+ "concurrency": 4, // [map/parallel] fan-out cap for THIS phase
79
+ "final": true // mark this phase's output as the workflow result
80
+ }
81
+ ```
82
+
83
+ | Key | Applies to | Default | Notes |
84
+ |-----|-----------|---------|-------|
85
+ | `id` | all | — | **Required, unique.** Used in `{steps.<id>…}`. |
86
+ | `type` | all | `agent` | One of the **12** phase types (agent, parallel, map, gate, reduce, approval, flow, loop, tournament, script, **race**, **expand**). |
87
+ | `agent` | all | first available | Agent name; resolved from the scoped pool. |
88
+ | `task` | agent, gate, map, reduce | — | Prompt; supports interpolation. Required for these types. |
89
+ | `over` | map | — | **Required for map.** Must resolve to an array. |
90
+ | `as` | map | `item` | Loop variable bound per item. |
91
+ | `branches` | parallel, race | — | **Required** (≥1 for parallel; ≥2 for race). `[{task, agent?}]`. |
92
+ | `cancelLosers` | race | `true` | Abort in-flight losers after first **success** (best-effort AbortSignal). |
93
+ | `from` | reduce | — | **Required for reduce.** Phase ids whose outputs are aggregated. `{previous.output}` resolves to **all completed `from[]` outputs** in from-array order (one → raw; many → `### <id>\n\n<output>` sections joined by `\n\n---\n\n`). |
94
+ | `reduceStrategy` | reduce | `one-shot` | `one-shot` = a single reducer call over all aggregated inputs. `tree` = batched intermediate reducer rounds (see `batchSize`); useful when aggregated input would exceed one prompt. `tree` forces the imperative runtime (event kernel falls back). |
95
+ | `batchSize` | reduce | — | With `reduceStrategy: "tree"`, max inputs per intermediate reducer call (integer ≥ 2). Ignored for one-shot. A phase may start at most 256 tree-reducer calls; increase `batchSize` or split the reduction if validation would exceed the cap. |
96
+ | `def` | expand, flow | — | **Required for expand.** Fragment Taskflow / phases array / `{steps.X.json}`. |
97
+ | `expandMode` | expand | `nested` | `nested` = isolated sub-flow; `graft` = promote children as `<expandId>-<childId>`. |
98
+ | `maxNodes` | expand | `50` | Cap fragment phase count (1..100). |
99
+ | `run` | script | — | **Required for script.** Shell command: a string (runs in a shell) or an array (direct exec, no shell). A string with an interpolation placeholder is rejected (injection guard). |
100
+ | `input` | script | — | Text piped to the command's stdin; supports interpolation. |
101
+ | `timeout` | script | `60000` | Max run time in ms (1000–300000). On timeout: SIGTERM → SIGKILL, phase fails. For agent-running phases: caps EACH subagent call (≥ 1000 ms); expiry aborts + fails with `timedOut` (never retried). Not supported for approval/flow. |
102
+ | `timeoutMs` | approval | — | **0.2.7.** Max wait for a human decision in ms (≥ 1000). Omit for infinite wait (legacy). On expiry apply `onExpire`. Distinct from agent wall `timeout`. |
103
+ | `onExpire` | approval | `reject` | **0.2.7.** When `timeoutMs` elapses: `reject` \| `fail` \| `approve` (explicit footgun — auto-continues without review). No-op without `timeoutMs`. |
104
+ | `idleTimeout` | agent, gate, reduce, map, parallel, loop, tournament | host default (`300000`) | Idle watchdog in ms (≥ 1000, or `0` to disable). If a subagent produces no output for this long it is killed as stalled. `0` disables the watchdog but then a finite wall `timeout` (≥ 1000) is **required** on that phase so it can never hang. Per-phase overrides the flow-level `idleTimeout`; absent → flow-level or host default. |
105
+ | `dependsOn` | all | `[]` | DAG edges. `from` also implies a dependency. |
106
+ | `output` | all | `text` | `json` parses output so `{steps.id.json}` / map `over` work. |
107
+ | `model` | all | agent/global | Per-phase model override. See §5. |
108
+ | `thinking` | all | agent/global | Per-phase thinking level. See §5. |
109
+ | `tools` | all | agent default | Whitelist of tools for the subagent. See §5. |
110
+ | `cwd` | all | flow cwd | Run this phase's subagent in a different directory. |
111
+ | `concurrency` | map, parallel | flow concurrency | Fan-out cap for this phase only. See §4. |
112
+ | `context` | all | — | File paths / `{steps.X}` refs to **pre-read and inject** before the task. See §2.1. |
113
+ | `contextLimit` | all | `8000` | Max characters read **per file** in `context`. See §2.1. |
114
+ | `cache` | all | `run-only` | Per-phase cache policy (`scope`/`ttl`/`fingerprint`). See §11. |
115
+ | `final` | all | last phase | Exactly one phase may be `final`; its output is returned. |
116
+
117
+ > Gate-only control fields (`eval`, `onBlock`, score), the loop/tournament control
118
+ > fields (`until`/`maxIterations`/`convergence`, `variants`/`judge`/`judgeAgent`/`mode`),
119
+ > the script fields (`run`/`input`/`timeout`), race/expand fields above, and the
120
+ > cross-phase contract fields (`expect`, `timeout`, `optional`, `strictInterpolation`)
121
+ > are documented in `SKILL.md` next to their phase types. `shareContext` and the
122
+ > workspace `cwd` keywords (`temp`/`dedicated`/`worktree`) are in `advanced.md`.
123
+
124
+ ---
125
+
126
+ ## 2.1 Context pre-reading (`context` / `contextLimit`)
127
+
128
+ Instead of making a subagent *discover* files by exploring (an O(N²) turn-cost
129
+ spiral), you can **pre-read** known files and inject their contents ahead of the
130
+ task prompt. List file paths and/or `{steps.X}` refs in `context`; the runtime
131
+ resolves interpolated refs first, then reads each file and prepends labeled
132
+ blocks to the task.
133
+
134
+ ```jsonc
135
+ {
136
+ "id": "review",
137
+ "type": "agent",
138
+ "agent": "reviewer",
139
+ "context": ["src/auth.ts", "src/middleware.ts", "{steps.spec.output}"],
140
+ "contextLimit": 12000,
141
+ "task": "Review the auth flow against the spec above. VERDICT: PASS or BLOCK.",
142
+ "dependsOn": ["spec"]
143
+ }
144
+ ```
145
+
146
+ **Behavior & limits (all enforced in the runtime):**
147
+
148
+ | Aspect | Rule |
149
+ |--------|------|
150
+ | Resolution order | interpolate `{steps.X}` / `{args.X}` refs **first**, then read file paths. |
151
+ | Per-file cap | `contextLimit` characters per file (default **8000**); longer files are truncated with a marker. |
152
+ | Total cap | the combined injected block is hard-capped at **200,000 chars**; overflow is truncated with a notice. |
153
+ | Unreadable file | skipped with a `console.warn` (never aborts the phase). |
154
+ | JSON-looking entry | a value that looks like a JSON blob (not a path) is diagnosed and skipped, not read as a file. |
155
+
156
+ Use `context` for **known, bounded** inputs (a handful of source files, an
157
+ upstream phase's output). For large/unknown exploration, let the agent use its
158
+ `read`/`grep` tools instead — pre-reading hundreds of files just hits the total
159
+ cap.
160
+
161
+ ---
162
+
163
+ ## 3. Declaring & passing arguments
164
+
165
+ Declare arguments on the flow, then reference them with `{args.X}`.
166
+
167
+ ```jsonc
168
+ "args": {
169
+ "dir": { "type": "relative-path", "default": "src", "description": "Directory to scan" },
170
+ "depth": { "type": "number", "default": 2, "minimum": 1 },
171
+ "format": { "type": "enum", "values": ["text", "json"], "default": "text" },
172
+ "token": { "type": "string", "required": true, "description": "API token" }
173
+ }
174
+ ```
175
+
176
+ | Field | Notes |
177
+ |-------|-------|
178
+ | `type` | Optional: `string`, `relative-path`, `number`, `boolean`, or `enum`. Legacy untyped args keep interpolation compatibility but cannot select resources. Flow `version` is informational and does not change this rule. |
179
+ | `default` | Used when the caller omits the arg. |
180
+ | `description` | Documentation only. |
181
+ | `required` | Enforced for typed args when no invocation value or default exists; advisory on legacy untyped declarations. |
182
+ | `minimum` / `maximum` / `values` | Type-specific constraints, validated for defaults and invocation values. |
183
+
184
+ **Resolution:** for each declared arg, the provided value wins, else its
185
+ `default`. Any extra provided keys are also passed through (so undeclared args
186
+ still reach `{args.X}`).
187
+
188
+ ### Typed relative cwd bridge (0.2.1)
189
+
190
+ An author-written reusable flow can select one existing directory below its
191
+ invocation root without accepting arbitrary cwd interpolation:
192
+
193
+ ```jsonc
194
+ {
195
+ "args": { "package": { "type": "relative-path", "required": true } },
196
+ "phases": [
197
+ { "id": "review", "type": "agent", "cwd": "{args.package}", "task": "Review this package." }
198
+ ]
199
+ }
200
+ ```
201
+
202
+ The whole `cwd` must be exactly one `{args.X}` reference. Absolute paths,
203
+ concatenation, `{steps.*}`, dot segments, missing directories, files, and
204
+ symlink escapes are rejected during binding. Runtime-generated sub-flows cannot use this
205
+ bridge. Because the current 0.2.x runtime does not yet ship a cross-host filesystem sandbox, it is
206
+ disabled by default. A host operator—not flow JSON—may explicitly accept the
207
+ lower resolver-only guarantee by launching the host with:
208
+
209
+ ```bash
210
+ TASKFLOW_CWD_BRIDGE_MODE=resolve-only
211
+ ```
212
+
213
+ Resolver-only performs a time-of-check canonical-path validation and constrains
214
+ cwd selection, but it has no no-follow handle and does **not** stop a command or agent
215
+ tool from deliberately accessing other filesystem paths. The runtime marks this
216
+ in phase warnings and disables cache/resume reuse for the affected flow tree.
217
+ Saved-flow definitions are snapshotted once per execution; resume verifies the
218
+ persisted canonical root identity. A bridge-selected child flow receives a
219
+ non-expanding boundary, so nested literal cwd/context paths may narrow but never
220
+ escape it, including through symlinks.
221
+
222
+ All resolve-only writers admitted by one invocation are serialized before
223
+ durable lease acquisition. This keeps parallel/map/race/tournament fan-out from
224
+ contending with itself while preserving cross-process exclusion. It deliberately
225
+ trades same-workspace write parallelism for a provable phase boundary until a
226
+ native broker/snapshot backend exists.
227
+
228
+ Argument-selected cwd phases cannot set `retry.max > 0`. A failed resolve-only
229
+ writer may already have changed files, so Taskflow records the scope as
230
+ `dirty-unknown` and requires an explicit workspace reconciliation before any
231
+ new write instead of replaying side effects automatically.
232
+
233
+ Inspect or repair the current tree first. Then MCP hosts can call
234
+ `taskflow_reconcile_workspace` with acknowledgement exactly
235
+ `I acknowledge the current workspace state`; Pi can use
236
+ `action: "reconcile-workspace"` with the same acknowledgement or
237
+ `/tf reconcile-workspace --ack [reason]`. Model-callable MCP/Pi reconciliation
238
+ is disabled unless the host operator separately launches Taskflow with
239
+ `TASKFLOW_WORKSPACE_RECONCILE_MODE=explicit`; this host-only authority is
240
+ stripped from subagent environments and cannot be enabled by flow arguments.
241
+ The direct Pi slash command is already a user control-plane action and does not
242
+ require that environment variable. Reconciliation accepts the current state and
243
+ advances its generation; it does not restore files or certify them as correct.
244
+
245
+ **Passing args:**
246
+
247
+ Via the MCP tool: `taskflow_run` with `{ "name": "audit-endpoints", "args": { "dir": "packages/api" } }`.
248
+
249
+ ---
250
+
251
+ ## 4. Concurrency model
252
+
253
+ There are **two independent concurrency limits**:
254
+
255
+ 1. **Same-layer parallelism** — phases with no dependency between them sit in the
256
+ same topological layer and run concurrently, bounded by **`flow.concurrency`**
257
+ (default `8`).
258
+ 2. **Fan-out within a `map`/`parallel` phase** — bounded by
259
+ **`phase.concurrency ?? flow.concurrency ?? 8`**.
260
+
261
+ ```jsonc
262
+ {
263
+ "concurrency": 6, // ≤6 sibling phases run at once
264
+ "phases": [
265
+ { "id": "scan", "type": "map", "over": "{steps.list.json}",
266
+ "concurrency": 3, // …but this map only fans out 3 at a time
267
+ "task": "…", "dependsOn": ["list"] }
268
+ ]
269
+ }
270
+ ```
271
+
272
+ Set a low `phase.concurrency` to protect rate-limited models or heavy bash work;
273
+ keep `flow.concurrency` higher to let independent phases overlap.
274
+
275
+ ---
276
+
277
+ ## 5. Model, thinking & tools resolution
278
+
279
+ For any phase, the effective value is resolved in this **precedence order**
280
+ (first defined wins):
281
+
282
+ | Setting | Precedence (high → low) |
283
+ |---------|-------------------------|
284
+ | **model** | `phase.model` → agent frontmatter `model` (resolved via `modelRoles`) → pi default |
285
+ | **thinking** | `phase.thinking` → agent frontmatter `thinking` → `settings` global thinking → pi default |
286
+ | **tools** | `phase.tools` → agent frontmatter `tools` → host default capability policy |
287
+
288
+ Notes:
289
+ - `tools` expresses the requested capability set, but enforcement is
290
+ host-specific. It is a literal whitelist on Pi; Codex maps it to an OS
291
+ sandbox profile, while the other hosts use their own permission contracts.
292
+ Omit it to request the host's default capability policy.
293
+ - Each phase runs as an isolated `hermes chat -q <prompt> -Q --source tool`
294
+ session. Quiet mode writes the final answer to stdout and `session_id:`
295
+ metadata to stderr; stdout is preserved verbatim, including blank lines and
296
+ answer text that happens to begin with `session_id:`. Unresolved `{{placeholder}}`s are dropped;
297
+ pi thinking suffixes (`:xhigh`) are stripped from `-m`. Effective thinking
298
+ maps to `--reasoning` (`off` → `none`). Read-only phases use
299
+ local-read tools → `taskflow_readonly_files` (read_file+search_files). Opt-in network via
300
+ `PI_TASKFLOW_HERMES_READONLY_WEB=1` → `web,search`. Never attach Hermes `file`
301
+ under RO (writable). Children use ephemeral HERMES_HOME with an inference-only
302
+ filtered `auth.json`, an exact inference-provider dotenv allowlist, non-secret
303
+ model/fallback routing, and no parent skills/MCP/memory; both `read_file` and
304
+ `search_files` are confined to the resolved phase cwd. Mutating/default-capable
305
+ phases fail closed unless `PI_TASKFLOW_HERMES_UNSAFE_YOLO=1`, which enables
306
+ `--yolo`; their default surface is local `file,terminal`, while explicit web
307
+ aliases may add `web`. Delegation, skills, memory, browser, cron, and other
308
+ control-plane toolsets are denied in 0.2.9. Optional
309
+ `PI_TASKFLOW_HERMES_MAX_TURNS` caps child loops (default 64). Quiet mode
310
+ does not stream token/cost accounting, so budgeted flows fail closed at the
311
+ MCP adapter the same way other non-accounting hosts do when costs are
312
+ unobservable. Children inherit only platform/proxy/CA and common provider
313
+ variables; generic `HERMES_*` control-plane state and unrelated secrets are removed.
314
+
315
+ For Codex, OpenCode, Grok, or Hermes, an operator can intentionally pass additional
316
+ task-specific environment variables by listing their names in the
317
+ comma-separated `PI_TASKFLOW_CHILD_ENV_ALLOW` setting.
318
+ - The agent's markdown body becomes the subagent's appended system prompt.
319
+
320
+ ---
321
+
322
+ ## 6. Agent discovery & scope
323
+
324
+ `flow.agentScope` controls which agent directories are loaded:
325
+
326
+ | Scope | Loads from |
327
+ |-------|-----------|
328
+ | `user` (default) | `~/.pi/agent/agents/*.md` |
329
+ | `project` | nearest `.pi/agents/*.md` found walking up from cwd |
330
+ | `both` | user **then** project (project overrides on name collision) |
331
+
332
+ - Agents are `.md` files with frontmatter `name` + `description` (required), plus
333
+ optional `model`, `thinking`, `tools`. The body is the system prompt.
334
+ - Reference agents in phases by their `name`. An unknown name fails that phase
335
+ with the list of available agents.
336
+ - If a phase omits `agent`, the **first discovered agent** is used.
337
+
338
+ ---
339
+
340
+ ## 7. settings.json
341
+
342
+ Taskflow shares the subagent settings file at `~/.pi/agent/settings.json`:
343
+
344
+ ```jsonc
345
+ {
346
+ "modelRoles": {
347
+ "steward": "openrouter/anthropic/claude-fable-5",
348
+ "expert": "openrouter/anthropic/claude-opus-5",
349
+ "builder": "openrouter/anthropic/claude-sonnet-5",
350
+ "scout": "openrouter/anthropic/claude-haiku-4.5"
351
+ },
352
+ "subagents": {
353
+ "globalThinking": "medium" // fallback thinking for all subagents
354
+ },
355
+ "defaultThinkingLevel": "low" // used if subagents.globalThinking is absent
356
+ }
357
+ ```
358
+
359
+ - `modelRoles` — maps the semantic `{{steward}}`, `{{expert}}`,
360
+ `{{builder}}`, and `{{scout}}` responsibilities in agent frontmatter to
361
+ concrete model identifiers. Legacy role keys from 0.2.4 remain valid for
362
+ custom agents and as exact built-in fallbacks until the corresponding new
363
+ role is configured.
364
+ - `subagents.globalThinking` (or top-level `defaultThinkingLevel`) — global
365
+ thinking fallback.
366
+
367
+ ---
368
+
369
+ ## 8. Cross-run caching (`cache`)
370
+
371
+ By default every phase is **`run-only`**: completed phases are reused only when
372
+ you *resume the same run* (the historical behavior). Opt a phase into the
373
+ persistent **cross-run** memoization store to reuse an identical-input result
374
+ from *any prior run* — instant, zero tokens. See `docs/rfc-cross-run-memoization.md`
375
+ for the design.
376
+
377
+ ```jsonc
378
+ {
379
+ "id": "summarize-deps",
380
+ "type": "agent",
381
+ "agent": "writer",
382
+ "task": "Summarize the dependency tree of this repo.",
383
+ "cache": {
384
+ "scope": "cross-run",
385
+ "ttl": "6h",
386
+ "fingerprint": ["git:HEAD", "file:package-lock.json"]
387
+ }
388
+ }
389
+ ```
390
+
391
+ ### `scope`
392
+
393
+ | Value | Meaning |
394
+ |-------|---------|
395
+ | `run-only` (default) | Reuse only within a resumed run — exactly the historical behavior. |
396
+ | `cross-run` | Reuse an identical-input result from **any** prior run (the persistent store). |
397
+ | `off` | Never reuse, even within a run (force re-execution every time). |
398
+
399
+ ### Flow-wide opt-in: `incremental`
400
+
401
+ Rather than annotating every phase with `cache: { "scope": "cross-run" }`, set
402
+ `incremental: true` at the **flow** level (or pass `incremental: true` as the
403
+ `run` tool argument) to default *every* phase to cross-run reuse:
404
+
405
+ ```jsonc
406
+ {
407
+ "name": "audit",
408
+ "incremental": true, // ← every phase defaults to scope:"cross-run"
409
+ "phases": [ /* ... */ ]
410
+ }
411
+ ```
412
+
413
+ Precedence: the invocation `incremental` argument wins over the flow's
414
+ `incremental` field, which is in turn overridden by any **per-phase** `cache`
415
+ setting. The cross-run-blocked phase types (`gate`/`approval`/`loop`/
416
+ `tournament`/`script`/`race`/`expand`) and all per-phase soundness fallbacks still apply. The default
417
+ remains `run-only` (each run starts fresh unless something opts in), because
418
+ cross-run reuse silently persists outputs and can serve stale results for phases
419
+ whose agents read files at runtime.
420
+
421
+ ### `ttl` (cross-run only)
422
+
423
+ Max age before a cross-run hit is treated as a miss: e.g. `"30m"`, `"6h"`, `"7d"`.
424
+ Omit for no time bound. A hit older than the TTL re-executes the phase. Cross-run cache entries are hard-evicted after 90 days regardless of per-entry TTL. This ceiling is not configurable.
425
+
426
+ ### `fingerprint` (cross-run only)
427
+
428
+ The cache key is normally `phaseId + agent + model + interpolated-task`. A
429
+ fingerprint folds **“did the world change?”** signals into that key, so an
430
+ external change becomes a cache **miss** even when the task text is identical.
431
+ Each entry is one of:
432
+
433
+ | Entry | Becomes a miss when… | Resolves to |
434
+ |-------|----------------------|-------------|
435
+ | `git:HEAD` / `git:<ref>` | the commit moves | the resolved SHA (30s timeout → `<timeout>`; no git → `<no-git>`) |
436
+ | `glob:<pattern>` | the **set of matching paths** or their metadata changes | sorted path list with size + mtime (content-hashed globs use `glob!:` instead, which is mtime-independent) |
437
+ | `glob!:<pattern>` | the **contents** of matching files change | content hashes (capped at 5000 matches) |
438
+ | `file:<path>` | that file's content changes | sha256 of the file (>10 MB or missing → `<skip>`/`<missing>`) |
439
+ | `env:<NAME>` | the env var changes | the env value |
440
+
441
+ ### What is cached, and when
442
+
443
+ - Only phases whose **`status` is `done`** and that **were not themselves a cache
444
+ hit** are written to the store (no re-storing a value just read).
445
+ - The store is keyed by the full input hash + fingerprint, tagged with
446
+ `flowName`/`phaseId`/`runId`/`model` for inspection and LRU eviction.
447
+ - Cross-run reuse is **safe by construction**: a different agent, model, task, or
448
+ fingerprint produces a different key, so stale results are never served.
449
+
450
+ > **When to use it:** expensive, deterministic phases whose inputs rarely change
451
+ > (dependency summaries, doc generation, repeated audits of the same tree). For
452
+ > phases that *should* re-run every time (anything reading live external state
453
+ > without a fingerprint), leave the default `run-only` or set `off`.
454
+
455
+ ---
456
+
457
+ ## 9. Environment variables
458
+
459
+ | Variable | Effect |
460
+ |----------|--------|
461
+ | `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. |
462
+ | `PI_TASKFLOW_CODEX_BIN` | Override the `codex` binary used to spawn Codex subagents. |
463
+ | `PI_TASKFLOW_CHILD_ENV_ALLOW` | Comma-separated names of extra task-specific environment variables to pass intentionally to Codex/OpenCode/Grok children. Unlisted application secrets are removed. |
464
+ | `PI_TASKFLOW_CLAUDE_BIN` | Override the `claude` binary used to spawn Claude Code subagents. |
465
+ | `PI_TASKFLOW_CLAUDE_UNSAFE_BYPASS=1` | Explicitly allow trusted Claude phases requesting known mutating tools to use narrow `--tools` + `bypassPermissions`; unknown names always fail closed. |
466
+ | `PI_TASKFLOW_OPENCODE_BIN` | Override the `opencode` binary used to spawn OpenCode subagents. |
467
+ | `PI_TASKFLOW_OPENCODE_MODEL` | Override the default OpenCode model for OpenCode executor e2e tests (e.g. `opencode/deepseek-v4-flash-free`). |
468
+ | `PI_TASKFLOW_OPENCODE_UNSAFE_AUTO=1` | Explicitly permit trusted OpenCode mutating/default phases to use unsandboxed `--auto`; otherwise they fail before spawn. All OpenCode children still use `--pure`. |
469
+ | `PI_TASKFLOW_GROK_BIN` | Override the `grok` binary used to spawn Grok Build subagents. |
470
+ | `PI_TASKFLOW_GROK_MUTATING_SANDBOX_PROFILE` | Required for Grok mutating/no-whitelist phases. Must name a custom profile from `.grok/sandbox.toml` or `~/.grok/sandbox.toml`; built-in profiles are rejected because they may fail open on unsupported hosts. |
471
+ | `PI_TASKFLOW_GROK_READONLY_SANDBOX_PROFILE` | Required for Grok read-only phases. Must name a custom profile from `.grok/sandbox.toml` or `~/.grok/sandbox.toml` extending `read-only`; built-in names are rejected so hooks/plugins remain kernel-contained if the host cannot enforce a built-in profile. |
472
+
473
+ ---
474
+
475
+ ## 10. Storage & file locations
476
+
477
+ | What | Path | Commit? |
478
+ |------|------|---------|
479
+ | User-scoped flow | `~/.pi/agent/taskflows/<name>.json` | personal |
480
+ | Project-scoped flow | `<nearest .pi>/taskflows/<name>.json` | ✅ commit to share |
481
+ | Run state (resume) | `<project .pi>/taskflows/runs/<flowName>/<runId>.json` | ❌ gitignore |
482
+
483
+ - `action: "save"` takes `scope: "project"` (default) or `"user"`.
484
+ - Project flows override user flows on a name collision.
485
+ - Add `.pi/taskflows/runs/` to `.gitignore`.
486
+
487
+ ---
488
+
489
+ ## 11. Quick recipes
490
+
491
+ **Pin a strong model only for the review gate:**
492
+ ```jsonc
493
+ { "id": "review", "type": "gate", "agent": "reviewer",
494
+ "model": "claude-opus-4", "thinking": "high",
495
+ "task": "…\nVERDICT:", "dependsOn": ["audit"] }
496
+ ```
497
+
498
+ **Sandbox a phase to read-only in a subdirectory:**
499
+ ```jsonc
500
+ { "id": "scan", "type": "agent", "agent": "scout",
501
+ "cwd": "packages/api", "tools": ["read", "grep", "ls"],
502
+ "task": "List route files. Output ONLY a JSON array.", "output": "json" }
503
+ ```
504
+
505
+ **Throttle a rate-limited fan-out:**
506
+ ```jsonc
507
+ { "id": "summarize", "type": "map", "over": "{steps.scan.json}",
508
+ "concurrency": 2, "agent": "writer",
509
+ "task": "Summarize {item.file}.", "dependsOn": ["scan"] }
510
+ ```
511
+
512
+ **Project-only agents:**
513
+ ```jsonc
514
+ { "name": "ci-audit", "agentScope": "project", "phases": [ /* … */ ] }
515
+ ```
516
+
517
+ ---
518
+
519
+ ## 9. TypeScript DSL CLI (`taskflow-dsl` / S4)
520
+
521
+ Author flows as **`.tf.ts`** (compile-time runes), then run the emitted JSON
522
+ through existing `taskflow_*` tools. JSON remains first-class (escape hatch).
523
+
524
+ ```bash
525
+ # From a monorepo checkout (dev):
526
+ node --conditions=development --experimental-strip-types \
527
+ packages/taskflow-dsl/src/cli.ts new audit
528
+ # edit audit.tf.ts
529
+ node --conditions=development --experimental-strip-types \
530
+ packages/taskflow-dsl/src/cli.ts check audit.tf.ts
531
+ # Fast rune/static-only pass (skip the default full tsc Program check):
532
+ node --conditions=development --experimental-strip-types \
533
+ packages/taskflow-dsl/src/cli.ts check audit.tf.ts --no-typecheck
534
+ node --conditions=development --experimental-strip-types \
535
+ packages/taskflow-dsl/src/cli.ts build audit.tf.ts --emit both
536
+ # → audit.taskflow.json (+ audit.flowir.json)
537
+ # Then: taskflow_verify / taskflow_run with defineFile=audit.taskflow.json
538
+ ```
539
+
540
+ | Command | Purpose |
541
+ |---------|---------|
542
+ | `new [name]` | ≤5-line hello skeleton (`.tf.ts` or `--json-escape` JSON) |
543
+ | `check <file>` | Erase + `validateTaskflow` + tsc (use `--no-typecheck` for a faster static-only pass) |
544
+ | `build <file>` | Erase → Taskflow JSON; optional FlowIR hash (`--emit taskflow\|flowir\|both`) |
545
+ | `decompile <file>` | Taskflow JSON → readable `.tf.ts` (semantic, not literal) |
546
+
547
+ Output commands are create-only by default: pass `--force` to replace an
548
+ existing regular file. Outputs are `--cwd`-contained, reject destination
549
+ symlinks, and commit atomically; `--emit both` preflights both destinations.
550
+
551
+ **Authoring notes (kinds ↔ runes)**
552
+
553
+ Import: `import { flow, agent, map, … } from "taskflow-dsl"`. Runes erase to Taskflow
554
+ JSON kinds (single source: `PHASE_TYPES` in core + `erase/kinds/*` registry).
555
+
556
+ | JSON `type` | DSL rune(s) | Notes |
557
+ |-------------|-------------|--------|
558
+ | `agent` | `agent(task, opts?)` | templates → `{steps.*}` / `{item.*}` |
559
+ | `parallel` | `parallel([agent…])` | waits for all branches |
560
+ | `map` | `map(source, item => agent…)` | `over` + `as` |
561
+ | `gate` | `gate(up, opts?, task?)` · `gate.automated` · `gate.scored` | sugar → `eval` / `score` |
562
+ | `reduce` | `reduce([…], () => agent…)` | `from` |
563
+ | `approval` | `approval({ request })` | |
564
+ | `flow` | `subflow("name")` · `subflow.def(plan)` | use vs def |
565
+ | `loop` | `loop({ task, until?, … })` | |
566
+ | `tournament` | `tournament({ branches/variants, judge, … })` | |
567
+ | `script` | `script(run, opts?)` | string or argv array |
568
+ | `race` | `race([agent…], { cancelLosers? })` | first **success** wins; cooperative loser usage is counted |
569
+ | `expand` | `expand` / `expand.nested` / `expand.graft` | `def` + `expandMode` |
570
+
571
+ - `const [a,b] = parallel([agent(...), agent(...)])` desugars to **two real agent phases** (`a`, `b`) that run concurrently (no `dependsOn` between them). Prefer this when you need `{steps.a.output}`.
572
+ - `race([...])` does **not** support array destructure — bind as one phase: `const winner = race([...])`.
573
+ - Unbuilt `.tf.ts` must **not** be executed as a Node program (runes throw `TFDSL_ERASE_ONLY`).
574
+ - Modular erase: new kind → `packages/taskflow-dsl/src/build/erase/kinds/<kind>.ts` + registry entry (see `docs/internal/modularization-0.2.0.md`).
575
+
576
+ Design docs: `docs/rfc-0.2.0-s4-mvp.md`, `docs/rfc-0.2.0-dsl-phases-horizon.md`.
577
+
578
+ ---
579
+
580
+ ## Caveats (declared but not yet enforced / partial)
581
+
582
+ These keys validate but the runtime does **not** fully act on them yet — don't
583
+ rely on them for behavior:
584
+
585
+ - Typed `arg.required` is enforced before execution. On legacy untyped arg
586
+ declarations it remains advisory for backward compatibility; add an explicit
587
+ `type` when absence must block.
588
+ - `flow.version` — informational only; it does not select runtime semantics.
589
+ - **Event kernel** (`eventKernel` / `PI_TASKFLOW_EVENT_KERNEL=1`) — opt-in; does
590
+ **not** run `race`/`expand`; score gates, `retry`, `expect`, reflexion,
591
+ cross-run cache, and Shared Context Tree force the **imperative** path.
592
+ - **`taskflow-dsl decompile`** — generates safe, readable TypeScript whose
593
+ rebuilt Taskflow/FlowIR is semantically equivalent for supported constructs;
594
+ dependencies are emitted before consumers even when input JSON is out of
595
+ order;
596
+ it does **not** reproduce original variable names, formatting, comments, or
597
+ source spelling. Unsupported/lossy constructs fail rather than silently
598
+ promising a literal round-trip.