pi-taskflow 0.0.3 → 0.0.4

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.
@@ -19,7 +19,7 @@ import { type AgentScope, discoverAgents, readSubagentSettings } from "./agents.
19
19
  import { renderRunResult, summarizeRun } from "./render.ts";
20
20
  import { RunHistoryComponent, type RunHistoryResult } from "./runs-view.ts";
21
21
  import { executeTaskflow, type RuntimeResult } from "./runtime.ts";
22
- import { finalPhase, type Taskflow, validateTaskflow } from "./schema.ts";
22
+ import { finalPhase, type Taskflow, validateTaskflow, desugar, isShorthand } from "./schema.ts";
23
23
  import {
24
24
  getFlow,
25
25
  listFlows,
@@ -41,6 +41,14 @@ interface TaskflowDetails {
41
41
  /** pi reads `isError` at runtime to mark tool failures; it is not in the public type. */
42
42
  type ToolResult = AgentToolResult<TaskflowDetails> & { isError?: boolean };
43
43
 
44
+ const ShorthandStep = Type.Object(
45
+ {
46
+ agent: Type.Optional(Type.String({ description: "Agent for this step (defaults to the first available agent)" })),
47
+ task: Type.String({ description: "Task prompt for this step (supports {previous.output} in chains)" }),
48
+ },
49
+ { additionalProperties: false },
50
+ );
51
+
44
52
  const TaskflowParams = Type.Object({
45
53
  action: StringEnum(["run", "save", "resume", "list"] as const, {
46
54
  description: "What to do: run a flow, save a definition, resume a paused run, or list saved flows",
@@ -53,6 +61,24 @@ const TaskflowParams = Type.Object({
53
61
  "Inline taskflow definition (JSON object matching the taskflow DSL). Use to run or save a new flow.",
54
62
  }),
55
63
  ),
64
+ // --- Shorthand (non-DAG) modes, like the subagent tool. No DSL required. ---
65
+ agent: Type.Optional(
66
+ Type.String({ description: "Shorthand single mode: agent to run with `task` (like subagent single mode)" }),
67
+ ),
68
+ task: Type.Optional(
69
+ Type.String({ description: "Shorthand single mode: the task prompt (like subagent single mode)" }),
70
+ ),
71
+ tasks: Type.Optional(
72
+ Type.Array(ShorthandStep, {
73
+ description: "Shorthand parallel mode: run these tasks concurrently and merge results (like subagent parallel)",
74
+ }),
75
+ ),
76
+ chain: Type.Optional(
77
+ Type.Array(ShorthandStep, {
78
+ description:
79
+ "Shorthand chain mode: run sequentially; reference the prior step with {previous.output} (like subagent chain)",
80
+ }),
81
+ ),
56
82
  args: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Invocation arguments for the flow" })),
57
83
  runId: Type.Optional(Type.String({ description: "Run id to resume (for action=resume)" })),
58
84
  scope: Type.Optional(
@@ -175,6 +201,7 @@ export default function (pi: ExtensionAPI) {
175
201
  "Orchestrate a multi-phase workflow of subagents from a declarative definition.",
176
202
  "Phases (agent, parallel, map, gate, reduce) form a DAG; intermediate outputs stay out of your context — only the final phase output is returned.",
177
203
  "Use action=run with an inline `define` (you write the DSL) or a saved `name`.",
204
+ "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}).",
178
205
  "Use action=save to persist a definition as a reusable /tf:<name> command. action=resume continues a paused run. action=list shows saved flows.",
179
206
  "DSL: {name, args?, concurrency?, phases:[{id, type, agent, task, dependsOn?, over?(map), as?(map), branches?(parallel), from?(reduce), output?:'json', final?}]}.",
180
207
  "Interpolation: {args.X}, {steps.ID.output}, {steps.ID.json}, {item} (map), {previous.output}.",
@@ -183,7 +210,7 @@ export default function (pi: ExtensionAPI) {
183
210
  promptSnippet: "Orchestrate many subagents over a whole codebase/many items (declarative DAG with map fan-out)",
184
211
  promptGuidelines: [
185
212
  "Prefer taskflow whenever a request spans a whole project/codebase or many items — e.g. 'explore / 探索 / 审计 / analyze the project', auditing endpoints, reviewing or migrating many files/modules, or cross-checked research. It fans out to many subagents across phases and aggregates the result, keeping intermediate work out of your context.",
186
- "Choose taskflow over ad-hoc parallel subagents when the work has multiple phases (discover → work → review → report), needs dynamic fan-out over a discovered list, or should be saved and rerun. Use the plain subagent tool only for a single delegated task.",
213
+ "Choose taskflow over ad-hoc parallel subagents when the work has multiple phases (discover → work → review → report), needs dynamic fan-out over a discovered list, or should be saved and rerun. For simple single/parallel/chain delegations use the shorthand `task`/`tasks`/`chain` (no DSL) when you want the run tracked, resumable, or saveable; otherwise the plain subagent tool is fine.",
187
214
  "For taskflow map phases, have the upstream phase emit a JSON array and set output:'json'.",
188
215
  ],
189
216
 
@@ -209,18 +236,39 @@ export default function (pi: ExtensionAPI) {
209
236
  return finalResult(action, result);
210
237
  }
211
238
 
212
- // resolve the definition (inline define wins, else saved name)
239
+ // resolve the definition: inline `define` / shorthand (single|parallel|chain), else saved `name`.
213
240
  let def: Taskflow | undefined;
214
- if (params.define) {
215
- const v = validateTaskflow(params.define);
241
+
242
+ // A shorthand spec can come from `define` (no phases) or top-level params.
243
+ const shorthandSpec: unknown =
244
+ params.define ??
245
+ (params.chain
246
+ ? { chain: params.chain, name: params.name }
247
+ : params.tasks
248
+ ? { tasks: params.tasks, name: params.name }
249
+ : params.task
250
+ ? { task: params.task, agent: params.agent, name: params.name }
251
+ : undefined);
252
+
253
+ if (shorthandSpec !== undefined) {
254
+ let candidate: unknown = shorthandSpec;
255
+ if (isShorthand(candidate)) {
256
+ try {
257
+ candidate = desugar(candidate);
258
+ } catch (e) {
259
+ return errorResult(action, `Invalid shorthand: ${e instanceof Error ? e.message : String(e)}`);
260
+ }
261
+ }
262
+ const v = validateTaskflow(candidate);
216
263
  if (!v.ok) return errorResult(action, `Invalid taskflow:\n- ${v.errors.join("\n- ")}`);
217
- def = params.define as Taskflow;
264
+ def = candidate as Taskflow;
218
265
  } else if (params.name) {
219
266
  const saved = getFlow(ctx.cwd, params.name);
220
267
  if (!saved) return errorResult(action, `Saved flow not found: ${params.name}`);
221
268
  def = saved.def;
222
269
  }
223
- if (!def) return errorResult(action, "Provide 'define' (inline) or 'name' (saved).");
270
+ if (!def)
271
+ return errorResult(action, "Provide 'define' (DSL), shorthand 'task'/'tasks'/'chain', or 'name' (saved).");
224
272
 
225
273
  // save
226
274
  if (action === "save") {
@@ -256,10 +304,23 @@ export default function (pi: ExtensionAPI) {
256
304
 
257
305
  renderCall(args, theme) {
258
306
  const action = args.action ?? "run";
259
- const name = args.name || (args.define as any)?.name || "(inline)";
260
- let text = theme.fg("toolTitle", theme.bold("taskflow ")) + theme.fg("accent", `${action} `) + theme.fg("muted", name);
307
+ let label = args.name || (args.define as { name?: string } | undefined)?.name;
308
+ let suffix = "";
261
309
  const phases = (args.define as Taskflow | undefined)?.phases;
262
- if (phases) text += theme.fg("dim", ` (${phases.length} phases)`);
310
+ if (phases) suffix = ` (${phases.length} phases)`;
311
+ else if (args.chain) {
312
+ label ||= "chain";
313
+ suffix = ` (${(args.chain as unknown[]).length} steps)`;
314
+ } else if (args.tasks) {
315
+ label ||= "parallel";
316
+ suffix = ` (${(args.tasks as unknown[]).length} tasks)`;
317
+ } else if (args.task) {
318
+ label ||= "task";
319
+ }
320
+ label ||= "(inline)";
321
+ let text =
322
+ theme.fg("toolTitle", theme.bold("taskflow ")) + theme.fg("accent", `${action} `) + theme.fg("muted", label);
323
+ if (suffix) text += theme.fg("dim", suffix);
263
324
  return new Text(text, 0, 0);
264
325
  },
265
326
 
@@ -9,7 +9,7 @@ import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
9
9
  import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
10
10
  import { formatTokens, type UsageStats } from "./runner.ts";
11
11
  import type { PhaseState, RunState } from "./store.ts";
12
- import type { Phase } from "./schema.ts";
12
+ import { dependenciesOf, type Phase, topoLayers } from "./schema.ts";
13
13
 
14
14
  // Single-width glyphs (Geometric Shapes / check marks) — keep columns aligned.
15
15
  const ICON: Record<PhaseState["status"], { ch: string; color: string }> = {
@@ -223,35 +223,81 @@ function headerLine(state: RunState, theme: Theme): string {
223
223
  return line;
224
224
  }
225
225
 
226
- /** The full dense progress block (header + aligned phase rows). */
226
+ /**
227
+ * Left-gutter rail glyph for a phase at `i` within a parallel group of `size`.
228
+ * A group is a topological layer with >1 phase (they run concurrently); the
229
+ * bracket (┌ ├ └) visually fans them out from the preceding layer. Single-phase
230
+ * layers get a blank gutter so the column stays quiet.
231
+ */
232
+ function railGlyph(i: number, size: number): string {
233
+ if (size <= 1) return " ";
234
+ if (i === 0) return "┌";
235
+ if (i === size - 1) return "└";
236
+ return "├";
237
+ }
238
+
239
+ /** The full dense progress block (header + DAG-ordered phase rows). */
227
240
  export function renderProgress(state: RunState, theme: Theme): string {
228
241
  const phases = state.def.phases;
229
242
  const idW = Math.max(...phases.map((p) => p.id.length), 2);
230
243
  const typeW = Math.max(...phases.map((p) => (p.type ?? "agent").length), 4);
244
+ const defIndex = new Map(phases.map((p, i) => [p.id, i]));
245
+
246
+ // Render in topological order: each layer is a set of phases that can run
247
+ // concurrently; later layers depend on earlier ones. This makes the DAG's
248
+ // flow legible top-to-bottom without drawing a full graph.
249
+ const layers = topoLayers(phases);
250
+ const rendered = new Set<string>();
231
251
 
232
252
  let text = headerLine(state, theme);
233
- for (const phase of phases) {
253
+
254
+ const renderRow = (phase: Phase, rail: string, prevLayerIds: Set<string>) => {
234
255
  const ps = state.phases[phase.id];
235
256
  const status = ps?.status ?? "pending";
236
257
  const id = phase.id.padEnd(idW);
237
258
  const type = (phase.type ?? "agent").padEnd(typeW);
238
259
  const detail = phaseDetail(phase, ps, theme);
260
+
261
+ // Annotate only "long" edges — dependencies that skip past the adjacent
262
+ // layer. Edges into the immediately-preceding layer are implied by position
263
+ // (and the rail), so showing them would just add noise.
264
+ const longEdges = dependenciesOf(phase).filter((d) => !prevLayerIds.has(d));
265
+ const dep = longEdges.length
266
+ ? theme.fg("dim", ` ↳ ${longEdges.join(", ")}`)
267
+ : "";
268
+
269
+ const gutter = rail === " " ? " " : theme.fg("borderMuted", rail);
239
270
  text +=
240
- `\n ${icon(status, theme)} ` +
271
+ `\n ${gutter} ${icon(status, theme)} ` +
241
272
  theme.fg(status === "pending" ? "dim" : "text", id) +
242
273
  " " +
243
274
  theme.fg("dim", type) +
244
275
  " " +
245
- detail;
276
+ detail +
277
+ dep;
246
278
 
247
279
  // Live activity sub-line (only while running, only if we have a message).
248
280
  if (status === "running" && ps?.liveText) {
249
- const indent = " ".repeat(2 + 2 + idW + 2);
281
+ const indent = " ".repeat(2 + 2 + 2 + idW + 2);
250
282
  const msg = ps.liveText.replace(/\s+/g, " ").trim();
251
283
  const snip = msg.length > 88 ? `${msg.slice(0, 88)}…` : msg;
252
284
  text += `\n${indent}${theme.fg("dim", "› ")}${theme.fg("muted", snip)}`;
253
285
  }
286
+ rendered.add(phase.id);
287
+ };
288
+
289
+ let prevLayerIds = new Set<string>();
290
+ for (const layer of layers) {
291
+ const ordered = [...layer].sort((a, b) => (defIndex.get(a.id) ?? 0) - (defIndex.get(b.id) ?? 0));
292
+ ordered.forEach((phase, i) => renderRow(phase, railGlyph(i, ordered.length), prevLayerIds));
293
+ prevLayerIds = new Set(ordered.map((p) => p.id));
254
294
  }
295
+
296
+ // Safety net: render any phase a malformed DAG left out of the layering.
297
+ for (const phase of phases) {
298
+ if (!rendered.has(phase.id)) renderRow(phase, " ", prevLayerIds);
299
+ }
300
+
255
301
  return text;
256
302
  }
257
303
 
@@ -289,7 +289,7 @@ export async function runAgentTask(
289
289
  }
290
290
  if (tmpPromptDir) {
291
291
  try {
292
- fs.rmdirSync(tmpPromptDir);
292
+ fs.rmSync(tmpPromptDir, { recursive: true, force: true });
293
293
  } catch {
294
294
  /* ignore */
295
295
  }
@@ -259,12 +259,20 @@ async function executePhase(
259
259
 
260
260
  /** Resolve a `{steps.x.json}`-style ref directly to its parsed value (bypassing stringify). */
261
261
  function directRef(over: string, state: RunState): unknown {
262
- const m = over.match(/^\{steps\.([a-zA-Z0-9_]+)\.(output|json)\}$/);
262
+ const m = over.match(/^\{steps\.([a-zA-Z0-9_]+)\.(output|json)(?:\.([a-zA-Z0-9_]+(?:\.[a-zA-Z0-9_]+)*))?\}$/);
263
263
  if (!m) return undefined;
264
264
  const step = state.phases[m[1]];
265
265
  if (!step || step.status !== "done") return undefined;
266
- if (m[2] === "json") return step.json ?? safeParse(step.output ?? "");
267
- return safeParse(step.output ?? "");
266
+ let value: unknown;
267
+ if (m[2] === "json") value = step.json ?? safeParse(step.output ?? "");
268
+ else value = safeParse(step.output ?? "");
269
+ if (m[3]) {
270
+ for (const key of m[3].split(".")) {
271
+ if (value == null || typeof value !== "object") return undefined;
272
+ value = (value as Record<string, unknown>)[key];
273
+ }
274
+ }
275
+ return value;
268
276
  }
269
277
 
270
278
  function lastCompletedOutput(state: RunState, phase: Phase): string | undefined {
@@ -90,6 +90,88 @@ export type Phase = Static<typeof PhaseSchema>;
90
90
  export type Taskflow = Static<typeof TaskflowSchema>;
91
91
  export type ArgSpec = Static<typeof ArgSpecSchema>;
92
92
 
93
+ // ---------------------------------------------------------------------------
94
+ // Shorthand (non-DAG) specs — subagent-style ergonomics
95
+ // ---------------------------------------------------------------------------
96
+ //
97
+ // For simple delegations you should not have to author a phases DAG. A
98
+ // shorthand spec mirrors the subagent tool's modes and is desugared into a
99
+ // full Taskflow before validation/execution:
100
+ //
101
+ // { task, agent? } → one `agent` phase (single)
102
+ // { tasks: [{task, agent?}, ...] } → one `parallel` phase (parallel)
103
+ // { chain: [{task, agent?}, ...] } → sequential `agent` phases (chain)
104
+ //
105
+ // Chain steps reference the prior step's output with {previous.output}, exactly
106
+ // like the subagent tool's {previous} placeholder.
107
+
108
+ export interface ShorthandStep {
109
+ agent?: string;
110
+ task: string;
111
+ }
112
+
113
+ /** True when `def` is a shorthand spec (no `phases`, but a task/tasks/chain field). */
114
+ export function isShorthand(def: unknown): boolean {
115
+ if (typeof def !== "object" || def === null) return false;
116
+ const d = def as Record<string, unknown>;
117
+ if (Array.isArray(d.phases)) return false;
118
+ return Array.isArray(d.chain) || Array.isArray(d.tasks) || typeof d.task === "string";
119
+ }
120
+
121
+ function readStep(s: unknown): ShorthandStep {
122
+ if (typeof s === "string") return { task: s };
123
+ if (s && typeof s === "object") {
124
+ const o = s as Record<string, unknown>;
125
+ return { agent: typeof o.agent === "string" ? o.agent : undefined, task: String(o.task ?? "") };
126
+ }
127
+ return { task: "" };
128
+ }
129
+
130
+ /**
131
+ * Desugar a shorthand spec into a full Taskflow DAG. Throws if no recognizable
132
+ * shorthand field is present. Carries through optional name/description/
133
+ * concurrency/agentScope/args.
134
+ */
135
+ export function desugar(def: unknown): Taskflow {
136
+ if (typeof def !== "object" || def === null) throw new Error("Shorthand spec must be an object");
137
+ const d = def as Record<string, unknown>;
138
+
139
+ const meta: Partial<Taskflow> = {};
140
+ if (typeof d.description === "string") meta.description = d.description;
141
+ if (typeof d.concurrency === "number") meta.concurrency = d.concurrency;
142
+ if (d.agentScope === "user" || d.agentScope === "project" || d.agentScope === "both") meta.agentScope = d.agentScope;
143
+ if (d.args && typeof d.args === "object") meta.args = d.args as Taskflow["args"];
144
+ const nameOf = (fallback: string) => (typeof d.name === "string" && d.name.trim() ? d.name.trim() : fallback);
145
+
146
+ // chain → sequential agent phases
147
+ if (Array.isArray(d.chain) && d.chain.length > 0) {
148
+ const steps = d.chain.map(readStep);
149
+ const phases: Phase[] = steps.map((s, i) => {
150
+ const phase: Phase = { id: `step${i + 1}`, type: "agent", task: s.task };
151
+ if (s.agent) phase.agent = s.agent;
152
+ if (i > 0) phase.dependsOn = [`step${i}`];
153
+ if (i === steps.length - 1) phase.final = true;
154
+ return phase;
155
+ });
156
+ return { name: nameOf("chain"), ...meta, phases };
157
+ }
158
+
159
+ // tasks → one parallel phase (fan-out + merge), no extra aggregation agent
160
+ if (Array.isArray(d.tasks) && d.tasks.length > 0) {
161
+ const branches: ParallelTask[] = d.tasks.map(readStep).map((s) => (s.agent ? { task: s.task, agent: s.agent } : { task: s.task }));
162
+ return { name: nameOf("parallel"), ...meta, phases: [{ id: "parallel", type: "parallel", branches, final: true }] };
163
+ }
164
+
165
+ // single task → one agent phase
166
+ if (typeof d.task === "string") {
167
+ const phase: Phase = { id: "main", type: "agent", task: d.task, final: true };
168
+ if (typeof d.agent === "string") phase.agent = d.agent;
169
+ return { name: nameOf("task"), ...meta, phases: [phase] };
170
+ }
171
+
172
+ throw new Error("Shorthand spec needs one of: 'task' (single), 'tasks' (parallel), or 'chain' (sequential)");
173
+ }
174
+
93
175
  // ---------------------------------------------------------------------------
94
176
  // Validation (beyond schema: DAG integrity, phase-type requirements)
95
177
  // ---------------------------------------------------------------------------
@@ -114,7 +114,7 @@ export function saveFlow(
114
114
  def: Taskflow,
115
115
  scope: "user" | "project" = "project",
116
116
  ): { filePath: string } {
117
- const dir = scope === "user" ? userFlowsDir() : findProjectFlowsDir(cwd, true)!;
117
+ const dir = scope === "user" ? userFlowsDir() : (findProjectFlowsDir(cwd, true) ?? path.join(cwd, ".pi", "taskflows"));
118
118
  fs.mkdirSync(dir, { recursive: true });
119
119
  const safe = def.name.replace(/[^\w.-]+/g, "_");
120
120
  const filePath = path.join(dir, `${safe}.json`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-taskflow",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Lightweight workflow orchestration for the Pi coding agent — declarative multi-phase taskflows with dynamic fan-out, isolated subagent context, resumable runs, and saveable commands.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -36,7 +36,7 @@
36
36
  ],
37
37
  "scripts": {
38
38
  "typecheck": "tsc --noEmit",
39
- "test": "node --experimental-strip-types --test test/interpolate.test.ts test/schema.test.ts test/runtime.test.ts",
39
+ "test": "node --experimental-strip-types --test test/interpolate.test.ts test/schema.test.ts test/runtime.test.ts test/runner.test.ts test/store.test.ts test/agents.test.ts test/render.test.ts test/desugar.test.ts",
40
40
  "test:e2e": "PI_TASKFLOW_PI_BIN=pi node --experimental-strip-types test/e2e.mts"
41
41
  },
42
42
  "pi": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: taskflow
3
- description: Orchestrate multi-phase subagent workflows with pi-taskflow. Use whenever a request spans a whole project or many items — deeply exploring / 探索 / auditing / 审计 / analyzing a codebase, reviewing or migrating many files or modules in parallel, cross-checked/adversarial review, codebase-wide research, or any repeatable orchestration you want to save and rerun. Prefer this over ad-hoc parallel subagents when the work has multiple phases or dynamic fan-out over a discovered list. Not for a single delegated task use the subagent tool for that.
3
+ description: Orchestrate multi-phase subagent workflows with pi-taskflow. Use whenever a request spans a whole project or many items — deeply exploring / 探索 / auditing / 审计 / analyzing a codebase, reviewing or migrating many files or modules in parallel, cross-checked/adversarial review, codebase-wide research, or any repeatable orchestration you want to save and rerun. Prefer this over ad-hoc parallel subagents when the work has multiple phases or dynamic fan-out over a discovered list. Also supports subagent-style shorthand (single / parallel / chain) for simple non-DAG delegations you want tracked, resumable, or saveable.
4
4
  ---
5
5
 
6
6
  # Taskflow
@@ -16,7 +16,36 @@ the final answer — not every step's transcript.
16
16
  - You want **cross-checked / adversarial review** before reporting.
17
17
  - You want a **repeatable** orchestration saved as a `/tf:<name>` command.
18
18
 
19
- For a single delegated task, use the `subagent` tool instead.
19
+ For a single quick delegation you can use the **shorthand modes** below (no DSL),
20
+ or the plain `subagent` tool. Use the shorthand when you want the run tracked,
21
+ resumable, or saveable as a `/tf` command.
22
+
23
+ ## Shorthand (non-DAG) — like the subagent tool
24
+
25
+ Skip the DSL entirely for simple delegations. The runtime desugars these into a
26
+ proper flow, so you still get progress, persistence, resume, and `save`.
27
+
28
+ ```jsonc
29
+ // single — one agent, one task
30
+ { "task": "Summarize the architecture of src/", "agent": "explorer" }
31
+
32
+ // parallel — run several tasks at once, outputs merged
33
+ { "tasks": [
34
+ { "task": "Audit auth in src/api", "agent": "analyst" },
35
+ { "task": "Audit input validation in src/api", "agent": "analyst" }
36
+ ] }
37
+
38
+ // chain — run sequentially; reference the prior step with {previous.output}
39
+ { "chain": [
40
+ { "task": "List the public API of src/lib", "agent": "scout" },
41
+ { "task": "Write docs for:\n{previous.output}", "agent": "writer" }
42
+ ] }
43
+ ```
44
+
45
+ - `agent` is optional (defaults to the first available agent).
46
+ - Add `name` to label the run (and to `save` it as a `/tf:<name>` command).
47
+ - Precedence if several are given: `chain` > `tasks` > `task`.
48
+ - You can pass these as top-level tool params **or** inside `define`.
20
49
 
21
50
  ## How to author a taskflow
22
51