pi-taskflow 0.0.5 → 0.0.7

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/DESIGN.md CHANGED
@@ -204,6 +204,19 @@ pi-taskflow/
204
204
  | `map` | 对上游数组**动态 fan-out**,每项一个 agent | ≤concurrency |
205
205
  | `gate` | 质量门 / 对抗 review(可决定是否继续) | 1+ |
206
206
  | `reduce` | 把多结果聚合为一(synthesize) | 1 |
207
+ | `approval` | **人在环**:暂停等待 approve / reject / edit | 1 |
208
+ | `flow` | 把一个**已保存的 taskflow** 当作单个 phase 运行(组合复用) | 子流程并发 |
209
+
210
+ ### 3.3b 控制流 / 可靠性字段(任意 phase)
211
+
212
+ | 字段 | 语义 |
213
+ |------|------|
214
+ | `when` | 条件守卫:表达式为假则 skip 该 phase。支持 `{refs}`、`== != < > <= >=`、`&& \|\| !`、括号、字符串/数字字面量。解析失败 fail-open(仍运行) |
215
+ | `join` | 依赖 join:`all`(默认,等全部 dep)/ `any`(OR-join,任一 dep 完成即运行) |
216
+ | `retry` | `{max, backoffMs, factor}`:失败重试,延迟 = `backoffMs * factor^attempt` |
217
+ | `use` / `with` | `flow` 子流程的名字与入参(入参字符串值会插值) |
218
+
219
+ 顶层 `budget: {maxUSD, maxTokens}`:累计成本/token 超限即停(剩余 phase skip,运行态 `blocked`)。
207
220
 
208
221
  ### 3.4 模板插值
209
222
 
@@ -293,7 +306,8 @@ export async function runTaskflow(def, args, ctx): Promise<TaskflowResult>
293
306
  | **v0.1** | DSL + schema + runtime(agent/parallel/map/reduce)+ `taskflow` 工具 + `/tf run` + 内存隔离 + 流式进度 | ✅ 已发布 (npm 0.0.1) |
294
307
  | **v0.2** | 保存/动态命令注册 + 跨 session 恢复 + `gate` 真门控 + run 历史交互 TUI | ✅ 已完成 (npm 0.0.3) |
295
308
  | **v0.3** | examples + SKILL.md(教 LLM 写定义)+ YAML 支持 + 发布 npm | 🚧 examples/SKILL/npm 已做;YAML 待办 |
296
- | **v0.4** | 真·后台执行(detached + 轮询)+ 成本预估/上限 + 内置 `deep-research` 工作流 | 待办 |
309
+ | **v0.6** | 控制流 & 可靠性:`when` 条件分支 + `join:any` OR-join + 声明式 `retry` + `approval` 人在环 + `flow` 子流程组合 + `budget` 成本上限 | 已完成 |
310
+ | **v0.7+** | 真·后台执行(detached + 轮询)+ 事件/cron 触发 + 成本**预估** + mermaid DAG 导出 + 内置 `deep-research` 工作流 | ⏳ 待办 |
297
311
 
298
312
  ---
299
313
 
package/README.md CHANGED
@@ -1,8 +1,14 @@
1
- # pi-taskflow
1
+ <div align="center">
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)
3
+ <img src="./assets/hero.png" alt="pi-taskflow — declarative, multi-phase subagent workflows" width="880">
4
+
5
+ <p>
6
+ <a href="https://www.npmjs.com/package/pi-taskflow"><img src="https://img.shields.io/npm/v/pi-taskflow?style=flat-square&color=B692FF&label=npm" alt="npm version"></a>
7
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-43D9AD?style=flat-square" alt="MIT license"></a>
8
+ <a href="https://pi.dev"><img src="https://img.shields.io/badge/for-Pi%20coding%20agent-6E8BFF?style=flat-square" alt="for the Pi coding agent"></a>
9
+ </p>
10
+
11
+ </div>
6
12
 
7
13
  > Lightweight workflow orchestration for the [Pi coding agent](https://pi.dev).
8
14
 
@@ -16,9 +22,10 @@ saveable as a one-word `/tf:<name>` command.
16
22
  pi install npm:pi-taskflow
17
23
  ```
18
24
 
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.
25
+ Fan out one subagent per item, route on results, retry the flaky ones, pause for
26
+ human approval, cap the spend, and gate the output with an adversarial review
27
+ all from one declarative definition. Only the final report reaches your
28
+ conversation; every intermediate transcript stays in the runtime.
22
29
 
23
30
  ## Why
24
31
 
@@ -39,6 +46,11 @@ only the final phase's output.
39
46
  | Scale | a few tasks | dynamic `map` fan-out |
40
47
  | Resumable | no | yes (cross-session, cached phases skip) |
41
48
  | Quality gates | no | `gate` phases with `VERDICT: BLOCK / PASS` |
49
+ | Conditional routing | no | `when` guards + `join: any` OR-joins |
50
+ | Fault tolerance | no | per-phase `retry` with backoff |
51
+ | Human-in-the-loop | no | `approval` phases (approve / reject / edit) |
52
+ | Cost control | no | run-wide `budget` (USD / token caps) |
53
+ | Composition | no | `flow` phases run saved sub-flows |
42
54
  | Progress visibility | opaque while running | live DAG render with timing + cost |
43
55
  | Ergonomics | inline JSON each time | shorthand (`task`/`tasks`/`chain`) or DSL |
44
56
 
@@ -131,6 +143,36 @@ only the final report back.
131
143
 
132
144
  Save it once → `/tf:summarize-files` forever.
133
145
 
146
+ ### Route, gate, and guard
147
+
148
+ Phases also **branch, retry, pause for a human, and respect a budget** — still
149
+ declaratively, no scripting:
150
+
151
+ ```jsonc
152
+ {
153
+ "name": "triage-and-fix",
154
+ "budget": { "maxUSD": 1.5 },
155
+ "phases": [
156
+ { "id": "triage", "type": "agent", "agent": "analyst", "output": "json",
157
+ "task": "Classify the bug. Output ONLY {\"severity\":\"high\"} or {\"severity\":\"low\"}." },
158
+ { "id": "deep", "when": "{steps.triage.json.severity} == high", "dependsOn": ["triage"],
159
+ "agent": "executor_code", "task": "Root-cause and patch it.",
160
+ "retry": { "max": 2, "backoffMs": 500 } },
161
+ { "id": "quick", "when": "{steps.triage.json.severity} == low", "dependsOn": ["triage"],
162
+ "agent": "executor_fast", "task": "Apply the quick fix." },
163
+ { "id": "approve", "type": "approval", "join": "any", "dependsOn": ["deep", "quick"],
164
+ "task": "Review the fix before it ships." },
165
+ { "id": "ship", "type": "agent", "dependsOn": ["approve"],
166
+ "task": "Open a PR with the change.", "final": true }
167
+ ]
168
+ }
169
+ ```
170
+
171
+ - **`when`** routes to `deep` *or* `quick` from the triage JSON; the other branch is skipped.
172
+ - **`join: "any"`** lets `approve` run as soon as whichever branch fired completes.
173
+ - **`retry`** re-runs a flaky patch with backoff; **`budget`** halts the whole run if it gets too expensive.
174
+ - **`approval`** pauses for a human (approve / reject / edit) before the final `ship`.
175
+
134
176
  ## Watch it run
135
177
 
136
178
  This is the live progress render for a real run — the `self-improve` flow that
@@ -172,9 +214,51 @@ writes and verifies its own test suites, caught here mid-block by a quality gate
172
214
  | `map` | fan out over an array — one subagent per item, `{item}` bound | `over`, `task` |
173
215
  | `gate` | quality/review step that can **halt the flow** | `task` |
174
216
  | `reduce` | aggregate `from[]` phase outputs into one | `from`, `task` |
217
+ | `approval` | **human-in-the-loop** pause — approve / reject / edit before continuing | — |
218
+ | `flow` | run a **saved sub-flow** as one phase (composition/reuse) | `use` |
219
+
220
+ ### Common phase fields
175
221
 
176
- Every phase needs `id`. Optional fields: `agent`, `dependsOn`, `output`,
177
- `model`, `thinking`, `tools`, `cwd`, `concurrency`, `final`, `optional`.
222
+ Every phase needs a unique `id` and a `type` (defaults to `agent`). On top of the
223
+ per-type fields above:
224
+
225
+ | Field | Meaning |
226
+ |---|---|
227
+ | `agent` | Agent to run (defaults to the first discovered agent) |
228
+ | `dependsOn` | Phase ids this phase waits for — builds the DAG |
229
+ | `join` | `"all"` (default) waits for every dep; `"any"` is an OR-join |
230
+ | `when` | Conditional guard — skip unless the expression is truthy |
231
+ | `retry` | `{ max, backoffMs?, factor? }` — retry a failing subagent |
232
+ | `output` | `"text"` (default) or `"json"` (exposes `{steps.ID.json}`) |
233
+ | `model` / `thinking` / `tools` | Per-phase overrides for the subagent |
234
+ | `cwd` | Working directory for the subagent |
235
+ | `concurrency` | Fan-out cap for `map` / `parallel` (overrides the flow default) |
236
+ | `final` | Marks the result-bearing phase (else the last phase wins) |
237
+ | `optional` | A failure here does **not** abort the run |
238
+ | `use` / `with` | (`flow`) saved sub-flow name + its args |
239
+
240
+ Flow-level keys: `name`, `description`, `args`, `concurrency` (default 8),
241
+ `agentScope`, and `budget: { maxUSD?, maxTokens? }`.
242
+
243
+ ### Control flow & reliability
244
+
245
+ - **`when`** — skip a phase unless an expression is truthy. Supports `{refs}`,
246
+ `== != < > <= >=`, `&& || !`, parentheses, and quoted strings/numbers, e.g.
247
+ `"when": "{steps.triage.json.route} == deep"`. Pair with `join: "any"` on the
248
+ merge phase to build real if/else routing. Parse errors **fail open**.
249
+ - **`join: "any"`** — an OR-join: the phase runs as soon as *one* dependency
250
+ completes (default `"all"` waits for every dep).
251
+ - **`retry`** — `{ "max": 2, "backoffMs": 500, "factor": 2 }` retries a failing
252
+ subagent with fixed (`factor:1`) or exponential backoff; usage is summed and
253
+ the attempt count shows as `↻N` in the TUI.
254
+ - **`approval`** — pause for a human (`select`: Approve / Reject / Edit). Reject
255
+ halts the flow; Edit injects the typed note as the phase output for downstream
256
+ steps. Non-interactive runs auto-approve.
257
+ - **`flow`** — `{ "type": "flow", "use": "deep-research", "with": { "topic": "{item}" } }`
258
+ runs a saved flow as a phase (recursion is detected and rejected).
259
+ - **`budget`** — a run-wide `{maxUSD, maxTokens}` ceiling; once exceeded, pending
260
+ phases are skipped (and in-flight fan-out stops spawning) and the run is
261
+ `blocked`.
178
262
 
179
263
  ### `output` format
180
264
 
@@ -263,9 +347,26 @@ file). Phase-level overrides for `model`, `thinking`, and `tools` are passed as
263
347
  Settings from `~/.pi/agent/settings.json` (the `subagents.agentOverrides` map)
264
348
  are honored, letting you tweak model, thinking, or tools per agent across all flows.
265
349
 
350
+ ## Examples
351
+
352
+ Ready-to-read definitions live in [`examples/`](./examples):
353
+
354
+ | File | Demonstrates |
355
+ |---|---|
356
+ | [`summarize-files.json`](./examples/summarize-files.json) | discover → `map` fan-out → `reduce` |
357
+ | [`conditional-research.json`](./examples/conditional-research.json) | `when` routing + `join: any` + `gate` + `budget` |
358
+ | [`guarded-refactor.json`](./examples/guarded-refactor.json) | `approval` (human-in-the-loop) + `retry` + `gate` |
359
+
360
+ To use one, copy it into `.pi/taskflows/<name>.json` (or
361
+ `~/.pi/agent/taskflows/`) and it registers as `/tf:<name>` — or just point the
362
+ model at the definition.
363
+
266
364
  ## Status & limits
267
365
 
268
- - **v0.0.4** — DSL + DAG runtime (`agent`/`parallel`/`map`/`gate`/`reduce`),
366
+ - **v0.0.6** — control flow & reliability: conditional `when` guards, `join: any`
367
+ OR-joins, declarative `retry`/backoff, `approval` (human-in-the-loop) phases,
368
+ `flow` (saved sub-flow composition), and run-wide `budget` caps — on top of the
369
+ DSL + DAG runtime (`agent`/`parallel`/`map`/`gate`/`reduce`),
269
370
  inline + saved flows, cross-session resume, live progress, isolated context.
270
371
  Default `concurrency` is 8 (set on the flow; per-phase `concurrency` overrides
271
372
  for that phase).
@@ -279,7 +380,8 @@ are honored, letting you tweak model, thinking, or tools per agent across all fl
279
380
  ### What it doesn't do (yet)
280
381
 
281
382
  - **No detached background execution.** A run needs the pi session to stay open.
282
- True background execution is on the roadmap.
383
+ True background execution (and event/cron triggers on top of it) is on the
384
+ roadmap.
283
385
  - **No `output: "file"`.** Outputs are text/JSON only. Write files via agent
284
386
  tool calls if needed.
285
387
  - **`map` requires a JSON array.** The `over` field must resolve to
@@ -292,13 +394,10 @@ are honored, letting you tweak model, thinking, or tools per agent across all fl
292
394
  ```bash
293
395
  npm install
294
396
  npm run typecheck
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
397
+ npm test # unit tests — no network, no process spawning
299
398
 
300
399
  # real end-to-end (spawns live subagents; needs model access)
301
- PI_TASKFLOW_PI_BIN=pi node --experimental-strip-types test/e2e.mts
400
+ npm run test:e2e
302
401
  ```
303
402
 
304
403
  ## Contributing
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "conditional-research",
3
+ "description": "Triage a topic, route to a deep or quick research branch (conditional `when` + OR-join), gate the result, and report.",
4
+ "version": 1,
5
+ "args": {
6
+ "topic": { "description": "The subject to research" }
7
+ },
8
+ "concurrency": 4,
9
+ "agentScope": "user",
10
+ "budget": { "maxUSD": 1.0 },
11
+ "phases": [
12
+ {
13
+ "id": "triage",
14
+ "type": "agent",
15
+ "agent": "analyst",
16
+ "task": "Decide how much research the topic \"{args.topic}\" needs. Output ONLY JSON: {\"route\": \"deep\"} for broad/ambiguous topics, or {\"route\": \"quick\"} for narrow/well-defined ones.",
17
+ "output": "json"
18
+ },
19
+ {
20
+ "id": "deep",
21
+ "type": "agent",
22
+ "agent": "explorer",
23
+ "when": "{steps.triage.json.route} == deep",
24
+ "dependsOn": ["triage"],
25
+ "task": "Do a thorough, multi-angle investigation of \"{args.topic}\". Cover background, key players, trade-offs, and open questions.",
26
+ "retry": { "max": 2, "backoffMs": 500, "factor": 2 }
27
+ },
28
+ {
29
+ "id": "quick",
30
+ "type": "agent",
31
+ "agent": "explorer",
32
+ "when": "{steps.triage.json.route} == quick",
33
+ "dependsOn": ["triage"],
34
+ "task": "Give a concise, focused answer on \"{args.topic}\" with the 3 most important points.",
35
+ "retry": { "max": 2, "backoffMs": 500, "factor": 2 }
36
+ },
37
+ {
38
+ "id": "review",
39
+ "type": "gate",
40
+ "agent": "critic",
41
+ "join": "any",
42
+ "from": ["deep", "quick"],
43
+ "dependsOn": ["deep", "quick"],
44
+ "task": "Review the research below for unsupported claims. If it is solid, end with 'VERDICT: PASS'; if it is too thin, end with 'VERDICT: BLOCK' and one reason.\n\n{steps.deep.output}{steps.quick.output}"
45
+ },
46
+ {
47
+ "id": "report",
48
+ "type": "reduce",
49
+ "from": ["review"],
50
+ "dependsOn": ["review"],
51
+ "agent": "doc-writer",
52
+ "task": "Write a clean markdown brief on \"{args.topic}\" from the validated research:\n\n{steps.deep.output}{steps.quick.output}",
53
+ "final": true
54
+ }
55
+ ]
56
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "guarded-refactor",
3
+ "description": "Plan a change, pause for human approval before touching code (HITL), then implement with retries and a final review gate. Budget-capped.",
4
+ "version": 1,
5
+ "args": {
6
+ "target": { "default": "src", "description": "Directory or module to refactor" },
7
+ "goal": { "description": "What the refactor should achieve" }
8
+ },
9
+ "concurrency": 4,
10
+ "agentScope": "user",
11
+ "budget": { "maxUSD": 2.0 },
12
+ "phases": [
13
+ {
14
+ "id": "plan",
15
+ "type": "agent",
16
+ "agent": "planner",
17
+ "task": "Produce a concrete, step-by-step refactor plan for {args.target} to achieve: {args.goal}. List the files you would change and the risks."
18
+ },
19
+ {
20
+ "id": "approve",
21
+ "type": "approval",
22
+ "dependsOn": ["plan"],
23
+ "task": "Review the refactor plan above. Approve to proceed, reject to abort, or edit to add constraints the implementer must follow."
24
+ },
25
+ {
26
+ "id": "implement",
27
+ "type": "agent",
28
+ "agent": "executor_code",
29
+ "dependsOn": ["approve"],
30
+ "task": "Implement the approved plan for {args.target}.\nPlan:\n{steps.plan.output}\nExtra human guidance (if any):\n{steps.approve.output}",
31
+ "retry": { "max": 1, "backoffMs": 1000 }
32
+ },
33
+ {
34
+ "id": "review",
35
+ "type": "gate",
36
+ "agent": "reviewer",
37
+ "dependsOn": ["implement"],
38
+ "task": "Review the implementation report below. If it is correct and complete, end with 'VERDICT: PASS'; otherwise 'VERDICT: BLOCK' with reasons.\n\n{steps.implement.output}"
39
+ },
40
+ {
41
+ "id": "summary",
42
+ "type": "reduce",
43
+ "from": ["review"],
44
+ "dependsOn": ["review"],
45
+ "agent": "doc-writer",
46
+ "task": "Write a short changelog entry summarizing what was done:\n\n{steps.implement.output}",
47
+ "final": true
48
+ }
49
+ ]
50
+ }
@@ -55,7 +55,14 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
55
55
  continue;
56
56
  }
57
57
 
58
- const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
58
+ const { frontmatter, body } = (() => {
59
+ try {
60
+ return parseFrontmatter<Record<string, string>>(content);
61
+ } catch {
62
+ // A single malformed agent file must not break discovery for every flow.
63
+ return { frontmatter: {} as Record<string, string>, body: "" };
64
+ }
65
+ })();
59
66
  if (!frontmatter.name || !frontmatter.description) continue;
60
67
 
61
68
  const tools = frontmatter.tools
@@ -18,8 +18,8 @@ import { Type } from "typebox";
18
18
  import { type AgentScope, discoverAgents, readSubagentSettings } from "./agents.ts";
19
19
  import { renderRunResult, summarizeRun } from "./render.ts";
20
20
  import { RunHistoryComponent, type RunHistoryResult } from "./runs-view.ts";
21
- import { executeTaskflow, type RuntimeResult } from "./runtime.ts";
22
- import { finalPhase, type Taskflow, validateTaskflow, desugar, isShorthand } from "./schema.ts";
21
+ import { executeTaskflow, type ApprovalDecision, type ApprovalRequest, type RuntimeResult } from "./runtime.ts";
22
+ import { finalPhase, resolveArgs, type Taskflow, validateTaskflow, desugar, isShorthand } from "./schema.ts";
23
23
  import {
24
24
  getFlow,
25
25
  listFlows,
@@ -86,17 +86,6 @@ const TaskflowParams = Type.Object({
86
86
  ),
87
87
  });
88
88
 
89
- function resolveArgs(def: Taskflow, provided: Record<string, unknown> | undefined): Record<string, unknown> {
90
- const args: Record<string, unknown> = {};
91
- for (const [key, spec] of Object.entries(def.args ?? {})) {
92
- if (provided && key in provided) args[key] = provided[key];
93
- else if (spec.default !== undefined) args[key] = spec.default;
94
- }
95
- // also pass through any extra provided args
96
- if (provided) for (const [k, v] of Object.entries(provided)) if (!(k in args)) args[k] = v;
97
- return args;
98
- }
99
-
100
89
  function makeRunState(def: Taskflow, args: Record<string, unknown>, cwd: string): RunState {
101
90
  return {
102
91
  runId: newRunId(def.name),
@@ -153,6 +142,29 @@ async function runFlow(
153
142
  (heartbeat as { unref?: () => void }).unref?.();
154
143
  }
155
144
 
145
+ // Human-in-the-loop approver — only when an interactive UI is available.
146
+ const requestApproval = ctx.hasUI
147
+ ? async (req: ApprovalRequest): Promise<ApprovalDecision> => {
148
+ if (req.upstream?.trim()) {
149
+ const snip = req.upstream.replace(/\s+/g, " ").trim();
150
+ ctx.ui.notify(`[${def.name}/${req.phaseId}] ${snip.length > 280 ? `${snip.slice(0, 280)}…` : snip}`, "info");
151
+ }
152
+ const choice = await ctx.ui.select(
153
+ `Taskflow approval — ${req.phaseId}: ${req.message}`,
154
+ ["Approve", "Reject", "Edit / add guidance"],
155
+ { signal },
156
+ );
157
+ if (!choice || choice === "Reject") return { decision: "reject" };
158
+ if (choice.startsWith("Edit")) {
159
+ const note = await ctx.ui.input("Guidance passed downstream as this phase's output", "type guidance…", {
160
+ signal,
161
+ });
162
+ return { decision: "edit", note: note ?? "" };
163
+ }
164
+ return { decision: "approve" };
165
+ }
166
+ : undefined;
167
+
156
168
  try {
157
169
  const result = await executeTaskflow(state, {
158
170
  cwd: ctx.cwd,
@@ -160,6 +172,8 @@ async function runFlow(
160
172
  globalThinking: settings.globalThinking,
161
173
  signal,
162
174
  persist: persistThrottled,
175
+ requestApproval,
176
+ loadFlow: (name: string) => getFlow(ctx.cwd, name)?.def,
163
177
  });
164
178
  return result;
165
179
  } finally {
@@ -199,11 +213,12 @@ export default function (pi: ExtensionAPI) {
199
213
  label: "Taskflow",
200
214
  description: [
201
215
  "Orchestrate a multi-phase workflow of subagents from a declarative definition.",
202
- "Phases (agent, parallel, map, gate, reduce) form a DAG; intermediate outputs stay out of your context — only the final phase output is returned.",
216
+ "Phases (agent, parallel, map, gate, reduce, approval, flow) form a DAG; intermediate outputs stay out of your context — only the final phase output is returned.",
203
217
  "Use action=run with an inline `define` (you write the DSL) or a saved `name`.",
204
218
  "For simple non-DAG delegations (like the subagent tool) skip the DSL: pass `task` (+optional `agent`) for one task, `tasks:[{task,agent?}]` to run in parallel, or `chain:[{task,agent?}]` to run sequentially (reference the prior step with {previous.output}).",
205
219
  "Use action=save to persist a definition as a reusable /tf:<name> command. action=resume continues a paused run. action=list shows saved flows.",
206
- "DSL: {name, args?, concurrency?, phases:[{id, type, agent, task, dependsOn?, over?(map), as?(map), branches?(parallel), from?(reduce), output?:'json', final?}]}.",
220
+ "DSL: {name, args?, concurrency?, budget?:{maxUSD,maxTokens}, phases:[{id, type, agent, task, dependsOn?, join?:'all'|'any', when?, retry?:{max,backoffMs,factor}, over?(map), as?(map), branches?(parallel), from?(reduce), use?(flow), with?(flow), output?:'json', final?}]}.",
221
+ "Phase types: agent (one subagent), parallel (static branches), map (dynamic fan-out over an array), gate (VERDICT: PASS/BLOCK quality gate), reduce (aggregate from N phases), approval (human-in-the-loop pause), flow (run a saved sub-flow). join:'any' is an OR-join; when is a conditional guard; retry adds backoff; budget caps run cost.",
207
222
  "Interpolation: {args.X}, {steps.ID.output}, {steps.ID.json}, {item} (map), {previous.output}.",
208
223
  ].join(" "),
209
224
  parameters: TaskflowParams,
@@ -286,19 +301,28 @@ export default function (pi: ExtensionAPI) {
286
301
  );
287
302
  },
288
303
  });
304
+ const warningText = v.warnings.length ? `\n\nWarnings:\n- ${v.warnings.join("\n- ")}` : "";
289
305
  return {
290
306
  content: [
291
- { type: "text", text: `Saved taskflow '${def.name}' → ${filePath}\nRun it with /tf:${def.name} or action=run.` },
307
+ { type: "text", text: `Saved taskflow '${def.name}' → ${filePath}\nRun it with /tf:${def.name} or action=run.${warningText}` },
292
308
  ],
293
309
  details: { action, message: filePath } satisfies TaskflowDetails,
294
310
  };
295
311
  }
296
312
 
297
313
  // run
298
- const v = validateTaskflow(def);
299
- if (!v.ok) return errorResult(action, `Invalid taskflow:\n- ${v.errors.join("\n- ")}`);
300
314
  const args = resolveArgs(def, params.args);
315
+ const v = validateTaskflow(def, { args, cwd: ctx.cwd });
316
+ if (!v.ok) return errorResult(action, `Invalid taskflow:\n- ${v.errors.join("\n- ")}`);
317
+ for (const w of v.warnings) {
318
+ console.warn(`[taskflow:${def.name}] ${w}`);
319
+ }
301
320
  const result = await runFlow(def, args, ctx, signal, onUpdate as any);
321
+ // Surface the validation warnings in the tool result so the model
322
+ // can acknowledge or fix them, and the user sees them in the chat.
323
+ if (v.warnings.length) {
324
+ result.finalOutput = `${result.finalOutput}\n\nWarnings:\n- ${v.warnings.join("\n- ")}`;
325
+ }
302
326
  return finalResult(action, result);
303
327
  },
304
328