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.
- package/LICENSE +21 -0
- package/README.md +447 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/bin.d.ts +31 -0
- package/dist/mcp/bin.d.ts.map +1 -0
- package/dist/mcp/bin.js +39 -0
- package/dist/mcp/bin.js.map +1 -0
- package/dist/mcp/server.d.ts +16 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +30 -0
- package/dist/mcp/server.js.map +1 -0
- package/package.json +60 -0
- package/plugin/assets/taskflow-small.svg +14 -0
- package/plugin/assets/taskflow.svg +17 -0
- package/plugin/hermes.config.snippet.yaml +27 -0
- package/plugin/skills/taskflow/SKILL.md +692 -0
- package/plugin/skills/taskflow/advanced.md +267 -0
- package/plugin/skills/taskflow/configuration.md +598 -0
- package/plugin/skills/taskflow/library.md +105 -0
- package/plugin/skills/taskflow/patterns.md +348 -0
|
@@ -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.
|