pi-taskflow 0.0.1

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 ADDED
@@ -0,0 +1,324 @@
1
+ # pi-taskflow — 设计与可行性方案
2
+
3
+ > 轻量工作流编排框架 for [pi coding agent](https://pi.dev)
4
+ > 灵感来自 Claude Code Dynamic Workflows(2026-05-28 发布),适配 pi extension 生态。
5
+
6
+ ---
7
+
8
+ ## 0. 一句话定位
9
+
10
+ **让 LLM(或用户)用声明式 DSL 描述一个多阶段工作流,由确定性 runtime 编排 subagent 执行,中间结果不污染主 context,最终只回收结论;工作流可保存为命令、可复用、可恢复。**
11
+
12
+ ---
13
+
14
+ ## 1. 市场调研结论
15
+
16
+ ### 1.1 命名
17
+
18
+ | 名字 | 状态 | 说明 |
19
+ |------|------|------|
20
+ | `pi-workflow` | ❌ 已占 | VSCode GUI 扩展(聊天面板/侧栏),**非编排框架**,不冲突 |
21
+ | **`pi-taskflow`** | ✅ 可用 | 本项目 |
22
+
23
+ ### 1.2 竞品分析(pi 生态无同类)
24
+
25
+ | 包 | 模式 | 与 pi-taskflow 差异 |
26
+ |----|------|------|
27
+ | `pi-pipeline` | SPEC→PLAN→TASKS→VERIFY 固定流水线 | 固定流程,非动态可定义 DSL |
28
+ | `pi-agent-flow` | fork subagent 并行调用器(scout/audit…) | 一次性并行调用,无 DAG / 无保存 / 无恢复 |
29
+ | `pi-crew` | 重型多 agent 编排 + worktree + 异步 | 太重,用户已弃用 |
30
+ | `pi-loop` | planner-worker-judge 固定循环 | 固定架构 |
31
+ | `pi-subagents`(官方) | single/parallel/chain 即时调用 | 无持久化工作流定义、无 fan-out scale、无恢复 |
32
+
33
+ **结论:声明式、可保存、可恢复、支持动态 fan-out 的轻量编排框架在 pi 生态是空白。**
34
+
35
+ ### 1.3 Claude Code Dynamic Workflows 借鉴要点
36
+
37
+ | 特性 | Claude Code | pi-taskflow 对应 |
38
+ |------|-------------|------------------|
39
+ | 计划进代码 | Claude 写 JS 脚本 | LLM 产出 **声明式 JSON DSL**(更轻、可审、更安全) |
40
+ | 中间结果隔离 | 脚本变量 | runtime 内存 Map,不进 context |
41
+ | 规模 | 16 并发 / 1000 agent | 可配置并发上限 + `map` 动态 fan-out |
42
+ | 可复用 | 保存为 `/command` | 保存到 `.pi/taskflows/`,注册为 `/tf:<name>` |
43
+ | 可恢复 | 同 session 缓存 | run 状态落盘,**跨 session 可恢复**(超越 CC) |
44
+ | 质量模式 | 对抗式 review | `gate` / `review` 阶段类型 |
45
+
46
+ ---
47
+
48
+ ## 2. 深度可行性验证(逐项对照 pi 真实 API)
49
+
50
+ > 全部基于阅读 `@earendil-works/pi-coding-agent` 的 extensions.md / packages.md / json.md / skills.md / prompt-templates.md / development.md + 现有 `~/.pi/agent/extensions/subagent/` 源码。
51
+
52
+ ### ✅ V1. 生成隔离上下文的 subagent,并拿到结构化输出
53
+ - **机制**:`spawn("pi", ["--mode","json","-p","--no-session", ...])`,逐行解析 JSON 事件(`message_end` / `tool_result_end`)。
54
+ - **证据**:现有 subagent extension 的 `runSingleAgent()` 已完整实现,含 usage 统计、stopReason、错误处理、abort 信号。
55
+ - **结论**:**直接复用**,零风险。
56
+
57
+ ### ✅ V2. 并发控制(matching CC 的 scale)
58
+ - **机制**:`mapWithConcurrencyLimit(items, concurrency, fn)`。
59
+ - **证据**:subagent extension 已有该函数(worker pool 实现)。
60
+ - **结论**:复用 + 提高默认上限(CC=16),新增 `map` 阶段做动态 fan-out。
61
+
62
+ ### ✅ V3. 中间结果不进 context window
63
+ - **机制**:phase 结果存 runtime 内存 `Map<phaseName, PhaseResult>`;只有最终 phase 的 output 写进 tool `content`;完整轨迹放 `details`(默认不送 LLM,仅 TUI 渲染)。
64
+ - **证据**:tool result 的 `content` vs `details` 分离(json.md / 现有 subagent)。
65
+ - **结论**:可行,这是相对"裸 subagent 串联"的核心优势。
66
+
67
+ ### ⚠️ V4. 后台执行(session 保持响应)—— 已知约束 + 取舍
68
+ - **pi 现实**:工具调用在一个 agent turn 内是**同步阻塞**的;没有 CC 那种独立 workflow runtime 进程。
69
+ - **可用手段**:
70
+ - 工具 `onUpdate(partial)` 回调可**实时流式**推进度(subagent parallel 模式已验证)。
71
+ - `ctx.ui.setStatus()` / `ctx.ui.setWidget()` footer/widget 进度。
72
+ - **取舍**:
73
+ - **v1(采用)**:工作流作为**单次长工具调用**执行,期间实时流式进度。session 在该 turn 内"忙",但有完整 phase 进度可视化 —— 与 subagent 现有体验一致,符合"轻量"。
74
+ - **v2(路线图)**:detached 子进程 + 文件状态轮询 + `/tf status` 命令实现**真后台**。复杂度高,非首版。
75
+ - **结论**:v1 可行,体验对标 subagent;真后台留作演进。诚实记录此约束。
76
+
77
+ ### ✅ V5. 保存工作流 → 可复用命令
78
+ - **三条可用路径**(均已读文档确认):
79
+ 1. `pi.registerCommand()` —— 文档明确支持**运行时注册**(与 registerTool 同源刷新)。
80
+ 2. `resources_discover` 事件 —— 动态贡献 prompt/skill 路径(dynamic-resources 示例验证)。
81
+ 3. prompt templates(`.pi/prompts/*.md`)—— `/name` 展开为文本。
82
+ - **采用方案**:
83
+ - 工作流定义存 `.pi/taskflows/<name>.json`(项目级)/ `~/.pi/agent/taskflows/<name>.json`(用户级)。
84
+ - `session_start` 时扫描目录,为每个工作流 `registerCommand("tf:<name>")`。
85
+ - 始终提供通用 `taskflow` 工具(LLM 调用)+ `/tf run <name> [args]` 命令(用户调用)。
86
+ - 保存新工作流后 `registerCommand` 立即生效(同 session 可用),无需 reload。
87
+ - **结论**:可行,比 prompt-template 方案更强(命令直接驱动 runtime)。
88
+
89
+ ### ✅ V6. 状态持久化 / 恢复
90
+ - **机制**:
91
+ - `pi.appendEntry(customType, data)` —— 会话内持久化(survive reload)。
92
+ - run 状态额外落盘 `.pi/taskflows/runs/<runId>.json` —— **跨 session 恢复**。
93
+ - 恢复逻辑:按 `phaseName + inputHash` 缓存结果;重跑跳过已完成 phase(与 CC "cached results" 一致)。
94
+ - **证据**:todo.ts 示例(从 session entries 重建状态);appendEntry API(extensions.md)。
95
+ - **结论**:可行,且跨 session 恢复**超越 CC**(CC 仅同 session)。
96
+
97
+ ### ✅ V7. 进度可视化(TUI)
98
+ - **机制**:复用 subagent 的 `renderCall` / `renderResult`;新增 phase 进度条 / DAG 状态。`ctx.ui.custom()` 做全屏 run 视图(todo.ts 模式)。
99
+ - **结论**:可行,有现成范式。
100
+
101
+ ### ✅ V8. 打包发布
102
+ - **机制**:`package.json` + `pi` manifest + `pi-package` keyword;pi 核心走 `peerDependencies`;`extensions/` 约定目录。`pi install npm:pi-taskflow`。
103
+ - **证据**:packages.md。
104
+ - **结论**:可行。
105
+
106
+ ### ✅ V9. Agent 复用
107
+ - **机制**:复用 `discoverAgents(cwd, scope, overrides)`,从 `~/.pi/agent/agents/*.md` + `.pi/agents/*.md` 加载;工作流按 agent 名引用;支持 settings.json 的 `subagents.agentOverrides`。
108
+ - **结论**:与现有 subagent 体系无缝衔接。
109
+
110
+ ### 可行性总评
111
+
112
+ | 项 | 结论 |
113
+ |----|------|
114
+ | 核心编排(spawn/并发/隔离) | ✅ 复用现成代码,零风险 |
115
+ | 保存/命令/恢复 | ✅ API 齐全 |
116
+ | 真·后台执行 | ⚠️ v1 用流式长调用替代,v2 演进 |
117
+ | TUI/打包/agent | ✅ 有范式 |
118
+
119
+ **整体:高度可行。唯一妥协是"真后台"留 v2,v1 用流式长工具调用,体验对标现有 subagent。**
120
+
121
+ ---
122
+
123
+ ## 3. 架构设计
124
+
125
+ ### 3.1 包结构
126
+
127
+ ```
128
+ pi-taskflow/
129
+ ├── package.json # pi manifest + peerDeps + pi-package keyword
130
+ ├── tsconfig.json
131
+ ├── README.md
132
+ ├── DESIGN.md # 本文件
133
+ ├── extensions/
134
+ │ ├── index.ts # 入口:注册 tool + commands + 事件
135
+ │ ├── runtime.ts # 编排引擎(DAG 解析 + 调度 + 恢复)
136
+ │ ├── runner.ts # subagent spawn(复用/移植 runSingleAgent)
137
+ │ ├── agents.ts # agent discovery(移植自 subagent/agents.ts)
138
+ │ ├── schema.ts # Taskflow DSL typebox schema + 校验
139
+ │ ├── store.ts # 工作流定义/run 状态读写(.pi/taskflows/)
140
+ │ ├── interpolate.ts # 模板插值 {steps.x.output} / {args.y}
141
+ │ └── render.ts # TUI renderCall/renderResult + 进度视图
142
+ ├── skills/
143
+ │ └── taskflow/
144
+ │ └── SKILL.md # 教 LLM 何时/如何写 taskflow 定义
145
+ └── examples/
146
+ ├── audit-endpoints.json
147
+ ├── deep-research.json
148
+ └── migrate-files.json
149
+ ```
150
+
151
+ ### 3.2 DSL(声明式工作流定义)
152
+
153
+ ```jsonc
154
+ {
155
+ "name": "audit-endpoints",
156
+ "description": "审计 src/routes/ 下所有 API 端点的认证检查",
157
+ "version": 1,
158
+ "args": { // 调用时传入,{args.dir}
159
+ "dir": { "default": "src/routes" }
160
+ },
161
+ "concurrency": 8, // 默认并发上限
162
+ "phases": [
163
+ {
164
+ "id": "discover",
165
+ "type": "agent", // 单 agent
166
+ "agent": "analyst",
167
+ "task": "列出 {args.dir} 下所有 API 端点,输出 JSON 数组 [{file, route}]",
168
+ "output": "json" // 解析为结构化数据供 map 用
169
+ },
170
+ {
171
+ "id": "audit",
172
+ "type": "map", // ★ 动态 fan-out(scale 核心)
173
+ "over": "{steps.discover.output}", // 对数组每项起一个 agent
174
+ "as": "item",
175
+ "agent": "analyst",
176
+ "task": "审计端点 {item.route}(文件 {item.file})的认证检查,列出风险",
177
+ "dependsOn": ["discover"]
178
+ },
179
+ {
180
+ "id": "review",
181
+ "type": "gate", // ★ 对抗式质量门
182
+ "agent": "reviewer",
183
+ "task": "复核以下审计结果,剔除误报,标注置信度:\n{steps.audit.output}",
184
+ "dependsOn": ["audit"]
185
+ },
186
+ {
187
+ "id": "report",
188
+ "type": "agent",
189
+ "agent": "planner",
190
+ "task": "汇总成最终报告:\n{steps.review.output}",
191
+ "dependsOn": ["review"],
192
+ "final": true // 该 phase 输出回收到主 session
193
+ }
194
+ ]
195
+ }
196
+ ```
197
+
198
+ ### 3.3 Phase 类型
199
+
200
+ | type | 语义 | 并发 |
201
+ |------|------|------|
202
+ | `agent` | 单 subagent 调用 | 1 |
203
+ | `parallel` | 静态多任务并行(固定 task 列表) | ≤concurrency |
204
+ | `map` | 对上游数组**动态 fan-out**,每项一个 agent | ≤concurrency |
205
+ | `gate` | 质量门 / 对抗 review(可决定是否继续) | 1+ |
206
+ | `reduce` | 把多结果聚合为一(synthesize) | 1 |
207
+
208
+ ### 3.4 模板插值
209
+
210
+ | 占位符 | 含义 |
211
+ |--------|------|
212
+ | `{args.X}` | 调用参数 |
213
+ | `{steps.ID.output}` | 某 phase 的最终输出(字符串) |
214
+ | `{steps.ID.json}` | 某 phase 输出解析为 JSON |
215
+ | `{item}` / `{item.field}` | map 阶段当前项 |
216
+ | `{previous.output}` | 上一 phase 输出(链式简写) |
217
+
218
+ ### 3.5 执行引擎(runtime.ts)
219
+
220
+ ```
221
+ 1. 校验 DSL(schema.ts)
222
+ 2. 拓扑排序 phases(dependsOn 建 DAG,检测环)
223
+ 3. 按层调度:
224
+ - 同层无依赖 phase 并行
225
+ - map 阶段展开为 N 子任务,受 concurrency 限流
226
+ 4. 每个 phase:
227
+ - 插值 task
228
+ - 命中缓存(phaseName+inputHash 在 run 状态里)→ 跳过
229
+ - 否则 spawn subagent(runner.ts),流式 onUpdate
230
+ - 存结果到内存 Map + 落盘 run 状态
231
+ 5. gate 阶段可返回 {continue:false} 中止
232
+ 6. final phase(或最后一个)输出 → tool content 回主 session
233
+ 7. 全程 details 累积完整轨迹供 TUI
234
+ ```
235
+
236
+ ### 3.6 对外接口
237
+
238
+ **(a) LLM 工具:`taskflow`**
239
+ ```jsonc
240
+ // 内联定义直接跑(LLM 动态生成工作流 —— 对标 CC "Claude 写脚本")
241
+ { "define": { /* 完整 DSL */ }, "args": { "dir": "src/api" } }
242
+
243
+ // 跑已保存的工作流
244
+ { "run": "audit-endpoints", "args": { "dir": "src/api" } }
245
+
246
+ // 保存定义为可复用命令
247
+ { "save": "audit-endpoints", "define": { /* DSL */ } }
248
+
249
+ // 从中断处恢复
250
+ { "resume": "<runId>" }
251
+ ```
252
+
253
+ **(b) 用户命令**
254
+ | 命令 | 作用 |
255
+ |------|------|
256
+ | `/tf list` | 列出已保存工作流 + 最近 run |
257
+ | `/tf run <name> [args]` | 运行 |
258
+ | `/tf:<name> [args]` | 每个保存的工作流自动注册的快捷命令 |
259
+ | `/tf resume <runId>` | 恢复中断的 run |
260
+ | `/tf show <name>` | 查看定义 |
261
+ | `/tf runs` | 全屏 run 历史/状态视图(ctx.ui.custom) |
262
+
263
+ **(c) 编程接口(供其他 extension)**
264
+ ```ts
265
+ export async function runTaskflow(def, args, ctx): Promise<TaskflowResult>
266
+ ```
267
+
268
+ ### 3.7 存储布局
269
+
270
+ ```
271
+ .pi/taskflows/ # 项目级定义(可入库共享)
272
+ audit-endpoints.json
273
+ ~/.pi/agent/taskflows/ # 用户级定义
274
+ deep-research.json
275
+ .pi/taskflows/runs/ # run 状态(恢复用,gitignore)
276
+ <runId>.json # {def, args, phases:{id:{status,output,usage,hash}}}
277
+ ```
278
+
279
+ ---
280
+
281
+ ## 4. 与现有 subagent 的关系
282
+
283
+ - **不替代,是上层编排**。subagent = 即时调用;taskflow = 可定义/保存/恢复的编排。
284
+ - 复用其 spawn / 并发 / usage / TUI 代码(移植进 `runner.ts`,避免硬依赖一个非 npm 的本地扩展)。
285
+ - 共享 agent 体系(`~/.pi/agent/agents/*.md` + settings `subagents.agentOverrides`)。
286
+
287
+ ---
288
+
289
+ ## 5. 路线图
290
+
291
+ | 版本 | 范围 |
292
+ |------|------|
293
+ | **v0.1** | DSL + schema + runtime(agent/parallel/map/reduce)+ `taskflow` 工具 + `/tf run` + 内存隔离 + 流式进度 |
294
+ | **v0.2** | 保存/动态命令注册 + 跨 session 恢复 + `gate` 阶段 + run 历史 TUI |
295
+ | **v0.3** | examples + SKILL.md(教 LLM 写定义)+ YAML 支持 + 发布 npm |
296
+ | **v0.4** | 真·后台执行(detached + 轮询)+ 成本预估/上限 + 内置 `deep-research` 工作流 |
297
+
298
+ ---
299
+
300
+ ## 6. 风险与缓解
301
+
302
+ | 风险 | 缓解 |
303
+ |------|------|
304
+ | 真后台执行 v1 缺失 | 流式长调用 + 明确文档;v4 补 |
305
+ | map 依赖上游输出结构化 JSON | `output:"json"` + 容错解析 + schema 提示 agent |
306
+ | spawn pi 路径解析(bun/node/standalone) | 移植 subagent 的 `getPiInvocation()`(已处理三种运行时) |
307
+ | 并发过高耗 token/限流 | concurrency 上限 + 成本预估(v4) |
308
+ | 运行时命令注册兼容性 | session_start 扫描注册兜底;保存即注册为增强 |
309
+ | DSL 过度复杂 | 保持声明式、5 种 phase 封顶;JS 逃生舱不做(保持"轻量") |
310
+
311
+ ---
312
+
313
+ ## 7. 下一步
314
+
315
+ 1. 创建 `package.json` + `tsconfig.json` + 骨架目录
316
+ 2. 实现 `schema.ts`(DSL 校验)+ `interpolate.ts`
317
+ 3. 移植 `runner.ts` / `agents.ts`(自 subagent)
318
+ 4. 实现 `runtime.ts`(DAG 调度 + map fan-out)
319
+ 5. `index.ts` 接线 tool + `/tf` 命令
320
+ 6. 本地 `pi -e ./extensions/index.ts` 联调
321
+ 7. examples + SKILL.md + README
322
+ 8. 发布 `npm publish` → `pi install npm:pi-taskflow`
323
+ </content>
324
+ </invoke>
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 heggria
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # pi-taskflow
2
+
3
+ > Lightweight workflow orchestration for the [Pi coding agent](https://pi.dev).
4
+
5
+ Describe a multi-phase workflow once; let a deterministic runtime orchestrate
6
+ isolated subagents to execute it. Intermediate results stay **out of your main
7
+ context** — only the final answer comes back. Inspired by Claude Code's Dynamic
8
+ Workflows, rebuilt as a lightweight, declarative pi extension.
9
+
10
+ ```bash
11
+ pi install npm:pi-taskflow
12
+ ```
13
+
14
+ ## Why
15
+
16
+ `subagent` is great for a single delegated task. But when a job needs **many
17
+ coordinated steps**, **fan-out over dozens of items**, **cross-checked review**,
18
+ or a **repeatable** pipeline, you want orchestration — without bloating your
19
+ conversation with every step's transcript.
20
+
21
+ `pi-taskflow` moves the plan into a small declarative definition. The runtime
22
+ holds the DAG, the loops, and the intermediate results; your context receives
23
+ only the final phase's output.
24
+
25
+ | | `subagent` | `pi-taskflow` |
26
+ |---|---|---|
27
+ | Who drives | the model, turn by turn | the runtime, from a definition |
28
+ | Intermediate results | in your context window | in the runtime (not your context) |
29
+ | Reusable | re-described each time | saved as `/tf:<name>` |
30
+ | Scale | a few tasks | dynamic `map` fan-out |
31
+ | Resumable | no | yes (cross-session) |
32
+
33
+ ## Concepts
34
+
35
+ A **taskflow** is a set of **phases** forming a DAG via `dependsOn`. Each phase
36
+ delegates to a subagent (an isolated `pi` process). Phases in the same DAG layer
37
+ run concurrently (bounded by `concurrency`).
38
+
39
+ ### Phase types
40
+
41
+ | type | meaning |
42
+ |------|---------|
43
+ | `agent` | one subagent runs `task` |
44
+ | `parallel` | run `branches[]` concurrently |
45
+ | `map` | fan out over an array — one subagent per item, `{item}` bound |
46
+ | `gate` | quality/adversarial-review step |
47
+ | `reduce` | aggregate several phases' outputs into one |
48
+
49
+ ### Interpolation
50
+
51
+ - `{args.X}` — invocation argument
52
+ - `{steps.ID.output}` — a prior phase's text output
53
+ - `{steps.ID.json}` / `{steps.ID.json.field}` — prior output parsed as JSON
54
+ - `{item}` / `{item.field}` — current item inside a `map` phase
55
+ - `{previous.output}` — the immediately-upstream phase output
56
+
57
+ ## Example
58
+
59
+ ```jsonc
60
+ {
61
+ "name": "summarize-files",
62
+ "args": { "dir": { "default": "." } },
63
+ "concurrency": 4,
64
+ "phases": [
65
+ { "id": "discover", "type": "agent", "agent": "scout",
66
+ "task": "List source files under {args.dir}. Output ONLY a JSON array [{\"file\":\"\"}].",
67
+ "output": "json" },
68
+ { "id": "summarize", "type": "map", "over": "{steps.discover.json}", "as": "item",
69
+ "agent": "scout", "task": "Read {item.file} and summarize it in one sentence.",
70
+ "dependsOn": ["discover"] },
71
+ { "id": "report", "type": "reduce", "from": ["summarize"], "agent": "writer",
72
+ "task": "Combine into a short overview:\n{steps.summarize.output}",
73
+ "dependsOn": ["summarize"], "final": true }
74
+ ]
75
+ }
76
+ ```
77
+
78
+ ## Usage
79
+
80
+ The model calls the `taskflow` tool; you can also drive it directly:
81
+
82
+ ```
83
+ /tf list # saved flows
84
+ /tf run <name> [args] # run a saved flow
85
+ /tf show <name> # print a definition
86
+ /tf runs # recent run history
87
+ /tf resume <runId> # continue a paused/failed run (cached phases skipped)
88
+ /tf:<name> [args] # shortcut per saved flow
89
+ ```
90
+
91
+ Tool actions: `run` (inline `define` or saved `name`), `save`, `resume`, `list`.
92
+
93
+ ## Storage
94
+
95
+ ```
96
+ .pi/taskflows/<name>.json # project-scoped definitions (commit to share)
97
+ ~/.pi/agent/taskflows/<name>.json # user-scoped definitions
98
+ .pi/taskflows/runs/<runId>.json # run state (resume); gitignore this
99
+ ```
100
+
101
+ ## Agents
102
+
103
+ Taskflow reuses your existing pi agents (`~/.pi/agent/agents/*.md`,
104
+ `.pi/agents/*.md`) and honors `subagents.agentOverrides` in settings. Reference
105
+ agents by `name`.
106
+
107
+ ## Development
108
+
109
+ ```bash
110
+ npm install
111
+ npm run typecheck
112
+ node --experimental-strip-types --test test/interpolate.test.ts test/schema.test.ts test/runtime.test.ts
113
+
114
+ # real end-to-end (spawns live subagents; needs model access)
115
+ PI_TASKFLOW_PI_BIN=pi node --experimental-strip-types test/e2e.mts
116
+ ```
117
+
118
+ ## Status & limits
119
+
120
+ - **v0.1** — DSL + DAG runtime (`agent`/`parallel`/`map`/`gate`/`reduce`),
121
+ inline + saved flows, cross-session resume, live progress, isolated context.
122
+ - A run executes as one streaming tool call (live progress while it runs). True
123
+ detached background execution is on the roadmap.
124
+ - `map` requires the upstream phase to emit a JSON array (`output: "json"`).
125
+
126
+ ## License
127
+
128
+ MIT
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "summarize-files",
3
+ "description": "Scan a directory, then summarize each file in parallel and produce a combined report.",
4
+ "version": 1,
5
+ "args": {
6
+ "dir": { "default": ".", "description": "Directory to scan" }
7
+ },
8
+ "concurrency": 4,
9
+ "agentScope": "user",
10
+ "phases": [
11
+ {
12
+ "id": "discover",
13
+ "type": "agent",
14
+ "agent": "scout",
15
+ "task": "List the source files directly under {args.dir} (non-recursive). Output ONLY a JSON array of objects like [{\"file\": \"path\"}]. No prose.",
16
+ "output": "json"
17
+ },
18
+ {
19
+ "id": "summarize",
20
+ "type": "map",
21
+ "over": "{steps.discover.json}",
22
+ "as": "item",
23
+ "agent": "scout",
24
+ "task": "Read the file {item.file} and give a one-sentence summary of what it does.",
25
+ "dependsOn": ["discover"]
26
+ },
27
+ {
28
+ "id": "report",
29
+ "type": "reduce",
30
+ "from": ["summarize"],
31
+ "agent": "writer",
32
+ "task": "Combine these per-file summaries into a short markdown overview of the directory:\n\n{steps.summarize.output}",
33
+ "dependsOn": ["summarize"],
34
+ "final": true
35
+ }
36
+ ]
37
+ }
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Agent discovery and configuration.
3
+ * Adapted from the pi subagent extension so taskflow shares the same agent
4
+ * pool (~/.pi/agent/agents/*.md, .pi/agents/*.md) and settings overrides.
5
+ */
6
+
7
+ import * as fs from "node:fs";
8
+ import * as path from "node:path";
9
+ import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
10
+
11
+ export type AgentScope = "user" | "project" | "both";
12
+
13
+ export interface AgentOverride {
14
+ model?: string;
15
+ thinking?: string;
16
+ tools?: string[];
17
+ }
18
+
19
+ export interface AgentConfig {
20
+ name: string;
21
+ description: string;
22
+ tools?: string[];
23
+ model?: string;
24
+ thinking?: string;
25
+ systemPrompt: string;
26
+ source: "user" | "project";
27
+ filePath: string;
28
+ }
29
+
30
+ export interface AgentDiscoveryResult {
31
+ agents: AgentConfig[];
32
+ projectAgentsDir: string | null;
33
+ }
34
+
35
+ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
36
+ const agents: AgentConfig[] = [];
37
+ if (!fs.existsSync(dir)) return agents;
38
+
39
+ let entries: fs.Dirent[];
40
+ try {
41
+ entries = fs.readdirSync(dir, { withFileTypes: true });
42
+ } catch {
43
+ return agents;
44
+ }
45
+
46
+ for (const entry of entries) {
47
+ if (!entry.name.endsWith(".md")) continue;
48
+ if (!entry.isFile() && !entry.isSymbolicLink()) continue;
49
+
50
+ const filePath = path.join(dir, entry.name);
51
+ let content: string;
52
+ try {
53
+ content = fs.readFileSync(filePath, "utf-8");
54
+ } catch {
55
+ continue;
56
+ }
57
+
58
+ const { frontmatter, body } = parseFrontmatter<Record<string, string>>(content);
59
+ if (!frontmatter.name || !frontmatter.description) continue;
60
+
61
+ const tools = frontmatter.tools
62
+ ?.split(",")
63
+ .map((t) => t.trim())
64
+ .filter(Boolean);
65
+
66
+ agents.push({
67
+ name: frontmatter.name,
68
+ description: frontmatter.description,
69
+ tools: tools && tools.length > 0 ? tools : undefined,
70
+ model: frontmatter.model,
71
+ thinking: frontmatter.thinking,
72
+ systemPrompt: body,
73
+ source,
74
+ filePath,
75
+ });
76
+ }
77
+ return agents;
78
+ }
79
+
80
+ function isDirectory(p: string): boolean {
81
+ try {
82
+ return fs.statSync(p).isDirectory();
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+
88
+ function findNearestProjectAgentsDir(cwd: string): string | null {
89
+ let currentDir = cwd;
90
+ while (true) {
91
+ const candidate = path.join(currentDir, ".pi", "agents");
92
+ if (isDirectory(candidate)) return candidate;
93
+ const parentDir = path.dirname(currentDir);
94
+ if (parentDir === currentDir) return null;
95
+ currentDir = parentDir;
96
+ }
97
+ }
98
+
99
+ export function discoverAgents(
100
+ cwd: string,
101
+ scope: AgentScope,
102
+ overrides?: Record<string, AgentOverride>,
103
+ ): AgentDiscoveryResult {
104
+ const userDir = path.join(getAgentDir(), "agents");
105
+ const projectAgentsDir = findNearestProjectAgentsDir(cwd);
106
+
107
+ const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
108
+ const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
109
+
110
+ const agentMap = new Map<string, AgentConfig>();
111
+ if (scope === "both") {
112
+ for (const a of userAgents) agentMap.set(a.name, a);
113
+ for (const a of projectAgents) agentMap.set(a.name, a);
114
+ } else if (scope === "user") {
115
+ for (const a of userAgents) agentMap.set(a.name, a);
116
+ } else {
117
+ for (const a of projectAgents) agentMap.set(a.name, a);
118
+ }
119
+
120
+ if (overrides) {
121
+ for (const [name, override] of Object.entries(overrides)) {
122
+ const agent = agentMap.get(name);
123
+ if (agent) {
124
+ if (override.model !== undefined) agent.model = override.model;
125
+ if (override.thinking !== undefined) agent.thinking = override.thinking;
126
+ if (override.tools !== undefined) agent.tools = override.tools;
127
+ }
128
+ }
129
+ }
130
+
131
+ return { agents: Array.from(agentMap.values()), projectAgentsDir };
132
+ }
133
+
134
+ export interface SubagentSettings {
135
+ agentOverrides?: Record<string, AgentOverride>;
136
+ globalThinking?: string;
137
+ }
138
+
139
+ /** Read subagent overrides from ~/.pi/agent/settings.json (shared with the subagent extension). */
140
+ export function readSubagentSettings(): SubagentSettings {
141
+ try {
142
+ const settingsPath = path.join(getAgentDir(), "settings.json");
143
+ if (!fs.existsSync(settingsPath)) return {};
144
+ const raw = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
145
+ return {
146
+ agentOverrides: raw.subagents?.agentOverrides,
147
+ globalThinking: raw.subagents?.globalThinking ?? raw.defaultThinkingLevel,
148
+ };
149
+ } catch {
150
+ return {};
151
+ }
152
+ }