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.
- package/extensions/index.ts +71 -10
- package/extensions/render.ts +52 -6
- package/extensions/runner.ts +1 -1
- package/extensions/runtime.ts +11 -3
- package/extensions/schema.ts +82 -0
- package/extensions/store.ts +1 -1
- package/package.json +2 -2
- package/skills/taskflow/SKILL.md +31 -2
package/extensions/index.ts
CHANGED
|
@@ -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.
|
|
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
|
|
239
|
+
// resolve the definition: inline `define` / shorthand (single|parallel|chain), else saved `name`.
|
|
213
240
|
let def: Taskflow | undefined;
|
|
214
|
-
|
|
215
|
-
|
|
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 =
|
|
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)
|
|
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
|
-
|
|
260
|
-
let
|
|
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)
|
|
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
|
|
package/extensions/render.ts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
package/extensions/runner.ts
CHANGED
package/extensions/runtime.ts
CHANGED
|
@@ -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
|
-
|
|
267
|
-
|
|
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 {
|
package/extensions/schema.ts
CHANGED
|
@@ -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
|
// ---------------------------------------------------------------------------
|
package/extensions/store.ts
CHANGED
|
@@ -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
|
+
"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": {
|
package/skills/taskflow/SKILL.md
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|