pi-taskflow 0.0.15 → 0.0.16
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/CHANGELOG.md +135 -0
- package/README.md +14 -3
- package/README.zh-CN.md +645 -0
- package/extensions/agents.ts +69 -7
- package/extensions/index.ts +51 -21
- package/extensions/init.ts +80 -1
- package/extensions/store.ts +29 -2
- package/package.json +6 -3
package/extensions/agents.ts
CHANGED
|
@@ -10,6 +10,59 @@ import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
|
10
10
|
|
|
11
11
|
export type AgentScope = "user" | "project" | "both";
|
|
12
12
|
|
|
13
|
+
export interface TaskflowSettings {
|
|
14
|
+
/** Whether taskflow's package-local built-in agents are available to flows. */
|
|
15
|
+
builtInAgents: boolean;
|
|
16
|
+
/** Whether package-local built-ins are copied into the current project's .pi/agents/. */
|
|
17
|
+
syncBuiltinAgentsToProject: boolean;
|
|
18
|
+
/** Maximum completed/failed runs to keep. 0 disables cleanup. */
|
|
19
|
+
maxKeptRuns: number;
|
|
20
|
+
/** Maximum age (days) for completed/failed runs. 0 disables age cleanup. */
|
|
21
|
+
maxRunAgeDays: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
import { DEFAULT_KEPT_RUNS, DEFAULT_RUN_AGE_DAYS } from "./store.ts";
|
|
25
|
+
|
|
26
|
+
export const DEFAULT_TASKFLOW_SETTINGS: TaskflowSettings = {
|
|
27
|
+
builtInAgents: true,
|
|
28
|
+
syncBuiltinAgentsToProject: false,
|
|
29
|
+
maxKeptRuns: DEFAULT_KEPT_RUNS,
|
|
30
|
+
maxRunAgeDays: DEFAULT_RUN_AGE_DAYS,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function normalizeTaskflowSettings(raw: unknown): TaskflowSettings {
|
|
34
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
35
|
+
return { ...DEFAULT_TASKFLOW_SETTINGS };
|
|
36
|
+
}
|
|
37
|
+
const rec = raw as Record<string, unknown>;
|
|
38
|
+
return {
|
|
39
|
+
builtInAgents:
|
|
40
|
+
typeof rec.builtInAgents === "boolean"
|
|
41
|
+
? rec.builtInAgents
|
|
42
|
+
: DEFAULT_TASKFLOW_SETTINGS.builtInAgents,
|
|
43
|
+
syncBuiltinAgentsToProject:
|
|
44
|
+
typeof rec.syncBuiltinAgentsToProject === "boolean"
|
|
45
|
+
? rec.syncBuiltinAgentsToProject
|
|
46
|
+
: DEFAULT_TASKFLOW_SETTINGS.syncBuiltinAgentsToProject,
|
|
47
|
+
maxKeptRuns:
|
|
48
|
+
typeof rec.maxKeptRuns === "number" && rec.maxKeptRuns >= 0 && Number.isInteger(rec.maxKeptRuns)
|
|
49
|
+
? rec.maxKeptRuns
|
|
50
|
+
: DEFAULT_TASKFLOW_SETTINGS.maxKeptRuns,
|
|
51
|
+
maxRunAgeDays:
|
|
52
|
+
typeof rec.maxRunAgeDays === "number" && rec.maxRunAgeDays >= 0 && Number.isInteger(rec.maxRunAgeDays)
|
|
53
|
+
? rec.maxRunAgeDays
|
|
54
|
+
: DEFAULT_TASKFLOW_SETTINGS.maxRunAgeDays,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function shouldLoadBuiltinAgents(settings: TaskflowSettings = DEFAULT_TASKFLOW_SETTINGS): boolean {
|
|
59
|
+
return settings.builtInAgents;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function shouldSyncBuiltinAgentsToProject(settings: TaskflowSettings = DEFAULT_TASKFLOW_SETTINGS): boolean {
|
|
63
|
+
return settings.builtInAgents && settings.syncBuiltinAgentsToProject;
|
|
64
|
+
}
|
|
65
|
+
|
|
13
66
|
export interface AgentOverride {
|
|
14
67
|
model?: string;
|
|
15
68
|
thinking?: string;
|
|
@@ -122,12 +175,14 @@ export function discoverAgents(
|
|
|
122
175
|
scope: AgentScope,
|
|
123
176
|
overrides?: Record<string, AgentOverride>,
|
|
124
177
|
modelRoles?: Record<string, string>,
|
|
178
|
+
taskflowSettings: TaskflowSettings = DEFAULT_TASKFLOW_SETTINGS,
|
|
125
179
|
): AgentDiscoveryResult {
|
|
126
|
-
// Built-in agents ship with the package (extensions/agents/*.md)
|
|
127
|
-
// PI_TASKFLOW_BUILTIN_AGENTS_DIR
|
|
180
|
+
// Built-in agents ship with the package (extensions/agents/*.md).
|
|
181
|
+
// PI_TASKFLOW_BUILTIN_AGENTS_DIR is kept as a test hook only; user-facing
|
|
182
|
+
// enable/disable lives in settings.json under `taskflow.builtInAgents`.
|
|
128
183
|
const builtInDirEnv = process.env.PI_TASKFLOW_BUILTIN_AGENTS_DIR;
|
|
129
184
|
const builtInDir = builtInDirEnv ? builtInDirEnv : builtInDirEnv === undefined ? path.resolve(import.meta.dirname, "agents") : "";
|
|
130
|
-
const builtInAgents = builtInDir ? loadAgentsFromDir(builtInDir, "built-in") : [];
|
|
185
|
+
const builtInAgents = shouldLoadBuiltinAgents(taskflowSettings) && builtInDir ? loadAgentsFromDir(builtInDir, "built-in") : [];
|
|
131
186
|
|
|
132
187
|
const userDir = path.join(getAgentDir(), "agents");
|
|
133
188
|
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
@@ -165,10 +220,15 @@ export function discoverAgents(
|
|
|
165
220
|
}
|
|
166
221
|
|
|
167
222
|
// Resolve {{role}} model references (e.g. {{fast}} → openrouter/deepseek/v4-flash)
|
|
223
|
+
// Clone before mutating, consistent with the overrides block above.
|
|
168
224
|
if (modelRoles) {
|
|
169
|
-
for (const agent of agentMap.
|
|
225
|
+
for (const [name, agent] of agentMap.entries()) {
|
|
170
226
|
const resolved = resolveModelRole(agent.model, modelRoles);
|
|
171
|
-
if (resolved !== agent.model)
|
|
227
|
+
if (resolved !== agent.model) {
|
|
228
|
+
const mutated: AgentConfig = { ...agent };
|
|
229
|
+
mutated.model = resolved;
|
|
230
|
+
agentMap.set(name, mutated);
|
|
231
|
+
}
|
|
172
232
|
}
|
|
173
233
|
}
|
|
174
234
|
|
|
@@ -179,6 +239,7 @@ export interface SubagentSettings {
|
|
|
179
239
|
agentOverrides?: Record<string, AgentOverride>;
|
|
180
240
|
globalThinking?: string;
|
|
181
241
|
modelRoles?: Record<string, string>;
|
|
242
|
+
taskflow: TaskflowSettings;
|
|
182
243
|
}
|
|
183
244
|
|
|
184
245
|
/**
|
|
@@ -197,15 +258,16 @@ export function resolveModelRole(model: string | undefined, roles?: Record<strin
|
|
|
197
258
|
export function readSubagentSettings(): SubagentSettings {
|
|
198
259
|
try {
|
|
199
260
|
const settingsPath = path.join(getAgentDir(), "settings.json");
|
|
200
|
-
if (!fs.existsSync(settingsPath)) return {};
|
|
261
|
+
if (!fs.existsSync(settingsPath)) return { taskflow: { ...DEFAULT_TASKFLOW_SETTINGS } };
|
|
201
262
|
const raw = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
202
263
|
return {
|
|
203
264
|
agentOverrides: raw.subagents?.agentOverrides,
|
|
204
265
|
globalThinking: raw.subagents?.globalThinking ?? raw.defaultThinkingLevel,
|
|
205
266
|
modelRoles: raw.modelRoles,
|
|
267
|
+
taskflow: normalizeTaskflowSettings(raw.taskflow),
|
|
206
268
|
};
|
|
207
269
|
} catch {
|
|
208
|
-
return {};
|
|
270
|
+
return { taskflow: { ...DEFAULT_TASKFLOW_SETTINGS } };
|
|
209
271
|
}
|
|
210
272
|
}
|
|
211
273
|
|
package/extensions/index.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
runInteractiveInit,
|
|
25
25
|
} from "./init.ts";
|
|
26
26
|
import { Type } from "typebox";
|
|
27
|
-
import { type AgentScope, discoverAgents, readSubagentSettings, syncBuiltinAgentsToProject } from "./agents.ts";
|
|
27
|
+
import { type AgentScope, discoverAgents, readSubagentSettings, shouldSyncBuiltinAgentsToProject, syncBuiltinAgentsToProject } from "./agents.ts";
|
|
28
28
|
import { renderRunResult, summarizeRun } from "./render.ts";
|
|
29
29
|
import { RunHistoryComponent, type RunHistoryResult } from "./runs-view.ts";
|
|
30
30
|
import { executeTaskflow, type ApprovalDecision, type ApprovalRequest, type RuntimeResult } from "./runtime.ts";
|
|
@@ -38,6 +38,8 @@ import {
|
|
|
38
38
|
type RunState,
|
|
39
39
|
saveFlow,
|
|
40
40
|
saveRun,
|
|
41
|
+
DEFAULT_KEPT_RUNS,
|
|
42
|
+
DEFAULT_RUN_AGE_DAYS,
|
|
41
43
|
} from "./store.ts";
|
|
42
44
|
import { CacheStore } from "./cache.ts";
|
|
43
45
|
|
|
@@ -142,11 +144,12 @@ async function runFlow(
|
|
|
142
144
|
|
|
143
145
|
// Throttled persistence: avoid disk writes on every sub-item event.
|
|
144
146
|
let lastPersist = 0;
|
|
147
|
+
const cleanupConfig = { maxKeep: DEFAULT_KEPT_RUNS, maxAgeDays: DEFAULT_RUN_AGE_DAYS };
|
|
145
148
|
const persistThrottled = (s: RunState) => {
|
|
146
149
|
const now = Date.now();
|
|
147
150
|
if (now - lastPersist >= 1000) {
|
|
148
151
|
lastPersist = now;
|
|
149
|
-
saveRun(s);
|
|
152
|
+
saveRun(s, cleanupConfig);
|
|
150
153
|
}
|
|
151
154
|
};
|
|
152
155
|
|
|
@@ -189,8 +192,10 @@ async function runFlow(
|
|
|
189
192
|
// discoverAgents or readSubagentSettings (F-001) is caught and
|
|
190
193
|
// the heartbeat timer is cleared by the finally block below.
|
|
191
194
|
const settings = readSubagentSettings();
|
|
195
|
+
cleanupConfig.maxKeep = settings.taskflow.maxKeptRuns;
|
|
196
|
+
cleanupConfig.maxAgeDays = settings.taskflow.maxRunAgeDays;
|
|
192
197
|
const scope: AgentScope = def.agentScope ?? "user";
|
|
193
|
-
const { agents } = discoverAgents(ctx.cwd, scope, settings.agentOverrides, settings.modelRoles);
|
|
198
|
+
const { agents } = discoverAgents(ctx.cwd, scope, settings.agentOverrides, settings.modelRoles, settings.taskflow);
|
|
194
199
|
|
|
195
200
|
// Hint: if any agent still has unresolved {{role}} references, suggest configuring modelRoles
|
|
196
201
|
const unresolvedRoles = agents
|
|
@@ -225,7 +230,7 @@ async function runFlow(
|
|
|
225
230
|
return result;
|
|
226
231
|
} finally {
|
|
227
232
|
if (heartbeat) clearInterval(heartbeat);
|
|
228
|
-
saveRun(state); // force-persist terminal state
|
|
233
|
+
saveRun(state, cleanupConfig); // force-persist terminal state
|
|
229
234
|
emit(state); // final render reflecting terminal state
|
|
230
235
|
}
|
|
231
236
|
}
|
|
@@ -255,16 +260,44 @@ export default function (pi: ExtensionAPI) {
|
|
|
255
260
|
pi.on("session_start", async (_e, ctx) => {
|
|
256
261
|
registerSavedFlowCommands(ctx);
|
|
257
262
|
|
|
258
|
-
//
|
|
259
|
-
// (and
|
|
260
|
-
//
|
|
263
|
+
// Optional: copy built-in agents into .pi/agents/ so Pi's native
|
|
264
|
+
// subagent tool (and other extensions) can discover them. This is
|
|
265
|
+
// disabled by default to avoid surprising project file creation.
|
|
261
266
|
try {
|
|
262
|
-
|
|
267
|
+
const settings = readSubagentSettings();
|
|
268
|
+
if (shouldSyncBuiltinAgentsToProject(settings.taskflow)) {
|
|
269
|
+
syncBuiltinAgentsToProject(ctx.cwd);
|
|
270
|
+
}
|
|
263
271
|
} catch {
|
|
264
272
|
// Best-effort: a locked or readonly .pi/ directory must not block
|
|
265
273
|
// session startup.
|
|
266
274
|
}
|
|
267
275
|
|
|
276
|
+
// Upgrade hint: if the project already has .pi/agents/ with agent
|
|
277
|
+
// files but no explicit taskflow settings, the user is upgrading
|
|
278
|
+
// from the old default (sync=true) and may be surprised that sync
|
|
279
|
+
// is now disabled by default.
|
|
280
|
+
try {
|
|
281
|
+
const raw = readSettings();
|
|
282
|
+
if (!("taskflow" in raw)) {
|
|
283
|
+
const fs = await import("node:fs");
|
|
284
|
+
const path = await import("node:path");
|
|
285
|
+
const projectAgentsDir = path.join(ctx.cwd, ".pi", "agents");
|
|
286
|
+
try {
|
|
287
|
+
const entries = fs.readdirSync(projectAgentsDir).filter((e: string) => e.endsWith(".md"));
|
|
288
|
+
if (entries.length > 0) {
|
|
289
|
+
console.warn(
|
|
290
|
+
`[taskflow] Note: built-in agents are no longer synced to .pi/agents/ by default. ` +
|
|
291
|
+
`If you rely on this, run /tf init → 'Configure taskflow preferences' to re-enable. ` +
|
|
292
|
+
`(This is a one-time upgrade hint.)`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
} catch { /* .pi/agents/ doesn't exist — no hint needed */ }
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
// Best-effort: settings.json missing or unreadable is not an error.
|
|
299
|
+
}
|
|
300
|
+
|
|
268
301
|
// Hint: prompt to configure model roles if not set
|
|
269
302
|
try {
|
|
270
303
|
const settings = readSubagentSettings();
|
|
@@ -282,7 +315,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
282
315
|
name: "taskflow",
|
|
283
316
|
label: "Taskflow",
|
|
284
317
|
description: [
|
|
285
|
-
"
|
|
318
|
+
"IMPORTANT: Before using this tool for the first time in a session, invoke skill_load('taskflow') to read the full documentation (DSL syntax, examples, best practices). This tool description is a reference, not a tutorial.",
|
|
286
319
|
"Shorthand (same API as subagent): pass `task` (+optional `agent`) for one task, `tasks:[{task,agent?}]` for parallel, or `chain:[{task,agent?}]` for sequential (use {previous.output}).",
|
|
287
320
|
"DSL: use action=run with an inline `define` (you write the DAG) or a saved `name`. Phases (agent, parallel, map, gate, reduce, approval, flow, loop, tournament) form a DAG; intermediate outputs stay out of your context — only the final phase output is returned.",
|
|
288
321
|
"Every delegation is tracked (runId), resumable across sessions, and saveable as /tf:<name> via action=save.",
|
|
@@ -293,7 +326,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
293
326
|
parameters: TaskflowParams,
|
|
294
327
|
promptSnippet: "Orchestrate subagents — single, parallel, chain, or DAG — with tracking, resume, and context isolation. Replaces the subagent tool.",
|
|
295
328
|
promptGuidelines: [
|
|
296
|
-
"
|
|
329
|
+
"BEFORE FIRST USE: invoke skill_load('taskflow') to read the full skill documentation (DSL syntax, phase types, examples, best practices). This tool description is a condensed reference only — the skill is the authoritative guide.\n\nUse taskflow for ALL delegation — single tasks, parallel, chain, or full DAG orchestration. It fully replaces the subagent tool: every delegation is tracked with a runId, resumable across sessions, context-isolated (only final output returns), and saveable as /tf:<name>. Do NOT call the subagent tool directly; use taskflow shorthand (task/tasks/chain) for simple cases instead.",
|
|
297
330
|
"For complex multi-phase work (explore / 审计 / analyze the project, auditing endpoints, reviewing or migrating many files/modules, cross-checked research), use the full DSL with phases. For taskflow map phases, have the upstream phase emit a JSON array and set output:'json'.",
|
|
298
331
|
"For taskflow map phases, have the upstream phase emit a JSON array and set output:'json'.",
|
|
299
332
|
],
|
|
@@ -370,6 +403,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
370
403
|
modelRegistry: ctx.modelRegistry,
|
|
371
404
|
modelList,
|
|
372
405
|
currentRoles: current,
|
|
406
|
+
currentTaskflowSettings: readSubagentSettings().taskflow,
|
|
373
407
|
});
|
|
374
408
|
const text = formatFlowResult(result);
|
|
375
409
|
return { content: [{ type: "text", text }], details: { action } satisfies TaskflowDetails };
|
|
@@ -382,7 +416,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
382
416
|
if (action === "agents") {
|
|
383
417
|
const scope = params.scope ?? "both";
|
|
384
418
|
const settings2 = readSubagentSettings();
|
|
385
|
-
const { agents } = discoverAgents(ctx.cwd, scope as AgentScope, undefined, settings2.modelRoles);
|
|
419
|
+
const { agents } = discoverAgents(ctx.cwd, scope as AgentScope, undefined, settings2.modelRoles, settings2.taskflow);
|
|
386
420
|
const text = agents.length
|
|
387
421
|
? agents
|
|
388
422
|
.map(
|
|
@@ -713,17 +747,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
713
747
|
modelRegistry: ctx.modelRegistry,
|
|
714
748
|
modelList,
|
|
715
749
|
currentRoles,
|
|
750
|
+
currentTaskflowSettings: readSubagentSettings().taskflow,
|
|
716
751
|
});
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
: result.kind === "no-change"
|
|
723
|
-
? "No changes made."
|
|
724
|
-
: "Init cancelled.",
|
|
725
|
-
result.kind === "saved" ? "info" : "info",
|
|
726
|
-
);
|
|
752
|
+
if (result.kind === "cancelled") {
|
|
753
|
+
ctx.ui.notify("Init cancelled.", "info");
|
|
754
|
+
} else {
|
|
755
|
+
ctx.ui.notify(formatFlowResult(result), "info");
|
|
756
|
+
}
|
|
727
757
|
return;
|
|
728
758
|
}
|
|
729
759
|
|
package/extensions/init.ts
CHANGED
|
@@ -17,6 +17,7 @@ import * as path from "node:path";
|
|
|
17
17
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
18
18
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
19
19
|
import type { ExtensionContext, ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import { DEFAULT_TASKFLOW_SETTINGS, normalizeTaskflowSettings, type TaskflowSettings } from "./agents.ts";
|
|
20
21
|
import { writeFileAtomic } from "./store.ts";
|
|
21
22
|
|
|
22
23
|
// ---------------------------------------------------------------------------
|
|
@@ -330,8 +331,23 @@ export function formatDiffReport(
|
|
|
330
331
|
return lines.join("\n");
|
|
331
332
|
}
|
|
332
333
|
|
|
334
|
+
export function formatTaskflowSettingsReport(settings: TaskflowSettings): string {
|
|
335
|
+
return [
|
|
336
|
+
"Taskflow preferences:",
|
|
337
|
+
` Built-in agents: ${settings.builtInAgents ? "enabled" : "disabled"}`,
|
|
338
|
+
` Sync built-ins to project .pi/agents: ${settings.syncBuiltinAgentsToProject ? "enabled" : "disabled"}`,
|
|
339
|
+
].join("\n");
|
|
340
|
+
}
|
|
341
|
+
|
|
333
342
|
export function formatFlowResult(result: InitFlowResult): string {
|
|
334
343
|
if (result.kind === "cancelled") return "Init cancelled.";
|
|
344
|
+
if (result.kind === "preferences-no-change") {
|
|
345
|
+
return "No changes.\n" + formatTaskflowSettingsReport(result.settings);
|
|
346
|
+
}
|
|
347
|
+
if (result.kind === "preferences-saved") {
|
|
348
|
+
const savedPath = formatSettingsPath(result.savedPath);
|
|
349
|
+
return `Saved taskflow preferences to ${savedPath}:\n` + formatTaskflowSettingsReport(result.settings);
|
|
350
|
+
}
|
|
335
351
|
if (result.kind === "no-change") {
|
|
336
352
|
return (
|
|
337
353
|
"No changes.\n" +
|
|
@@ -357,6 +373,8 @@ export function formatFlowResult(result: InitFlowResult): string {
|
|
|
357
373
|
export type InitFlowResult =
|
|
358
374
|
| { kind: "saved"; chosen: Record<string, string>; savedPath: string }
|
|
359
375
|
| { kind: "no-change"; chosen: Record<string, string> }
|
|
376
|
+
| { kind: "preferences-saved"; settings: TaskflowSettings; savedPath: string }
|
|
377
|
+
| { kind: "preferences-no-change"; settings: TaskflowSettings }
|
|
360
378
|
| { kind: "cancelled" };
|
|
361
379
|
|
|
362
380
|
export async function runInteractiveInit(ctx: {
|
|
@@ -366,6 +384,7 @@ export async function runInteractiveInit(ctx: {
|
|
|
366
384
|
modelRegistry: ExtensionContext["modelRegistry"];
|
|
367
385
|
modelList: Model<Api>[];
|
|
368
386
|
currentRoles: Record<string, string>;
|
|
387
|
+
currentTaskflowSettings?: TaskflowSettings;
|
|
369
388
|
}): Promise<InitFlowResult> {
|
|
370
389
|
if (!ctx.hasUI) {
|
|
371
390
|
throw new Error("runInteractiveInit requires an interactive session (hasUI=true).");
|
|
@@ -373,6 +392,7 @@ export async function runInteractiveInit(ctx: {
|
|
|
373
392
|
|
|
374
393
|
const recommended = RECOMMENDED_DEFAULTS;
|
|
375
394
|
const current = ctx.currentRoles;
|
|
395
|
+
const currentTaskflowSettings = ctx.currentTaskflowSettings ?? normalizeTaskflowSettings(readSettings().taskflow);
|
|
376
396
|
const hasCurrent = Object.keys(current).length > 0;
|
|
377
397
|
|
|
378
398
|
// ---- Action menu ----
|
|
@@ -381,10 +401,11 @@ export async function runInteractiveInit(ctx: {
|
|
|
381
401
|
"Use recommended defaults",
|
|
382
402
|
"Configure each role",
|
|
383
403
|
"Edit one role",
|
|
404
|
+
"Configure taskflow preferences",
|
|
384
405
|
"Show current roles",
|
|
385
406
|
"Cancel",
|
|
386
407
|
]
|
|
387
|
-
: ["Use recommended defaults", "Configure each role"];
|
|
408
|
+
: ["Use recommended defaults", "Configure each role", "Configure taskflow preferences"];
|
|
388
409
|
|
|
389
410
|
const action = await ctx.ui.select(
|
|
390
411
|
"What do you want to do with model roles?",
|
|
@@ -416,6 +437,11 @@ export async function runInteractiveInit(ctx: {
|
|
|
416
437
|
// ---- Cancel ----
|
|
417
438
|
if (action === "Cancel") return { kind: "cancelled" };
|
|
418
439
|
|
|
440
|
+
// ---- Configure taskflow preferences ----
|
|
441
|
+
if (action === "Configure taskflow preferences") {
|
|
442
|
+
return configureTaskflowPreferences(ctx, currentTaskflowSettings);
|
|
443
|
+
}
|
|
444
|
+
|
|
419
445
|
// ---- Configure each role ----
|
|
420
446
|
if (action === "Configure each role") {
|
|
421
447
|
const chosen = await collectRolePicks(ctx, current, recommended, undefined);
|
|
@@ -437,6 +463,59 @@ export async function runInteractiveInit(ctx: {
|
|
|
437
463
|
// Internal helpers
|
|
438
464
|
// ---------------------------------------------------------------------------
|
|
439
465
|
|
|
466
|
+
function taskflowSettingsIdentical(a: TaskflowSettings, b: TaskflowSettings): boolean {
|
|
467
|
+
return a.builtInAgents === b.builtInAgents && a.syncBuiltinAgentsToProject === b.syncBuiltinAgentsToProject;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function configureTaskflowPreferences(
|
|
471
|
+
ctx: { signal: AbortSignal; ui: ExtensionUIContext },
|
|
472
|
+
current: TaskflowSettings,
|
|
473
|
+
): Promise<InitFlowResult> {
|
|
474
|
+
const builtInPick = await ctx.ui.select(
|
|
475
|
+
"Taskflow built-in agents",
|
|
476
|
+
[
|
|
477
|
+
`Enable built-in agents${current.builtInAgents ? " (current)" : ""}`,
|
|
478
|
+
`Disable built-in agents${!current.builtInAgents ? " (current)" : ""}`,
|
|
479
|
+
"Back to action menu",
|
|
480
|
+
],
|
|
481
|
+
{ signal: ctx.signal },
|
|
482
|
+
);
|
|
483
|
+
if (builtInPick === undefined || builtInPick === "Back to action menu") return { kind: "cancelled" };
|
|
484
|
+
|
|
485
|
+
const chosen: TaskflowSettings = {
|
|
486
|
+
...DEFAULT_TASKFLOW_SETTINGS,
|
|
487
|
+
builtInAgents: builtInPick.startsWith("Enable"),
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
if (chosen.builtInAgents) {
|
|
491
|
+
const syncPick = await ctx.ui.select(
|
|
492
|
+
"Expose built-in agents to native Pi/project discovery?",
|
|
493
|
+
[
|
|
494
|
+
`Do not copy to project .pi/agents${!current.syncBuiltinAgentsToProject ? " (current)" : ""}`,
|
|
495
|
+
`Copy to project .pi/agents on session start${current.syncBuiltinAgentsToProject ? " (current)" : ""}`,
|
|
496
|
+
"Back to action menu",
|
|
497
|
+
],
|
|
498
|
+
{ signal: ctx.signal },
|
|
499
|
+
);
|
|
500
|
+
if (syncPick === undefined || syncPick === "Back to action menu") return { kind: "cancelled" };
|
|
501
|
+
chosen.syncBuiltinAgentsToProject = syncPick.startsWith("Copy");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (taskflowSettingsIdentical(current, chosen)) {
|
|
505
|
+
return { kind: "preferences-no-change", settings: chosen };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const preview = await ctx.ui.select(
|
|
509
|
+
`Review taskflow preferences:\n\n${formatTaskflowSettingsReport(chosen)}`,
|
|
510
|
+
["Save these preferences", "Cancel"],
|
|
511
|
+
{ signal: ctx.signal },
|
|
512
|
+
);
|
|
513
|
+
if (preview !== "Save these preferences") return { kind: "cancelled" };
|
|
514
|
+
|
|
515
|
+
const savedPath = writeSettings({ ...readSettings(), taskflow: chosen });
|
|
516
|
+
return { kind: "preferences-saved", settings: chosen, savedPath };
|
|
517
|
+
}
|
|
518
|
+
|
|
440
519
|
/** Collect picks for all roles. Returns undefined if user escapes to action menu. */
|
|
441
520
|
async function collectRolePicks(
|
|
442
521
|
ctx: { signal: AbortSignal; ui: ExtensionUIContext; modelList: Model<Api>[] },
|
package/extensions/store.ts
CHANGED
|
@@ -121,6 +121,10 @@ const DEFAULT_MAX_KEPT_TERMINAL = 100;
|
|
|
121
121
|
/** Remove terminal runs older than this (days). */
|
|
122
122
|
const DEFAULT_MAX_AGE_DAYS = 30;
|
|
123
123
|
|
|
124
|
+
// Re-exported for use in TaskflowSettings defaults (agents.ts).
|
|
125
|
+
export const DEFAULT_KEPT_RUNS = DEFAULT_MAX_KEPT_TERMINAL;
|
|
126
|
+
export const DEFAULT_RUN_AGE_DAYS = DEFAULT_MAX_AGE_DAYS;
|
|
127
|
+
|
|
124
128
|
/** Last cleanup timestamp — module-level so it persists across calls. */
|
|
125
129
|
let lastCleanupAt = 0;
|
|
126
130
|
|
|
@@ -460,6 +464,12 @@ function cleanupTerminalRuns(
|
|
|
460
464
|
|
|
461
465
|
if (toRemove.length === 0) return;
|
|
462
466
|
|
|
467
|
+
console.warn(
|
|
468
|
+
`[taskflow] Cleaning up ${toRemove.length} old run(s) ` +
|
|
469
|
+
`(max ${maxKeep} runs, ${maxAgeDays} day age limit). ` +
|
|
470
|
+
`Configure 'taskflow.maxKeptRuns' / 'taskflow.maxRunAgeDays' in settings.json (0 = keep all).`,
|
|
471
|
+
);
|
|
472
|
+
|
|
463
473
|
// Delete run files + lock files (outside the index lock).
|
|
464
474
|
for (const e of toRemove) {
|
|
465
475
|
const filePath = path.join(runsRoot, e.relPath);
|
|
@@ -548,6 +558,8 @@ export function getFlow(cwd: string, name: string): SavedFlow | null {
|
|
|
548
558
|
return listFlows(cwd).find((f) => f.name === name) ?? null;
|
|
549
559
|
}
|
|
550
560
|
|
|
561
|
+
let _piCreationHinted = false;
|
|
562
|
+
|
|
551
563
|
export function saveFlow(
|
|
552
564
|
cwd: string,
|
|
553
565
|
def: Taskflow,
|
|
@@ -558,9 +570,20 @@ export function saveFlow(
|
|
|
558
570
|
const safe = def.name.replace(/[^\w.-]+/g, "_");
|
|
559
571
|
const filePath = path.join(dir, `${safe}.json`);
|
|
560
572
|
writeFileAtomic(filePath, `${JSON.stringify(def, null, 2)}\n`);
|
|
573
|
+
|
|
574
|
+
// One-shot: let the user know we're creating a .pi/ directory on first save.
|
|
575
|
+
if (!_piCreationHinted) {
|
|
576
|
+
_piCreationHinted = true;
|
|
577
|
+
console.warn(
|
|
578
|
+
`[taskflow] Created .pi/taskflows/ for project-scoped flow storage. ` +
|
|
579
|
+
`Add .pi/ to .gitignore if desired.`,
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
561
583
|
return { filePath };
|
|
562
584
|
}
|
|
563
585
|
|
|
586
|
+
|
|
564
587
|
// --- Run state ---
|
|
565
588
|
|
|
566
589
|
function runsDir(cwd: string): string {
|
|
@@ -590,7 +613,7 @@ export function newRunId(flowName: string): string {
|
|
|
590
613
|
* F-009: shallow-clones state before stamping updatedAt to avoid mutating the
|
|
591
614
|
* caller's reference.
|
|
592
615
|
*/
|
|
593
|
-
export function saveRun(state: RunState): void {
|
|
616
|
+
export function saveRun(state: RunState, cleanup?: { maxKeep?: number; maxAgeDays?: number }): void {
|
|
594
617
|
const root = runsDir(state.cwd);
|
|
595
618
|
const flowDir = flowRunDir(root, state.flowName);
|
|
596
619
|
fs.mkdirSync(flowDir, { recursive: true });
|
|
@@ -608,7 +631,11 @@ export function saveRun(state: RunState): void {
|
|
|
608
631
|
});
|
|
609
632
|
|
|
610
633
|
// Opportunistic cleanup — throttled to once per CLEANUP_INTERVAL_MS.
|
|
611
|
-
|
|
634
|
+
const maxKeep = cleanup?.maxKeep ?? DEFAULT_MAX_KEPT_TERMINAL;
|
|
635
|
+
const maxAgeDays = cleanup?.maxAgeDays ?? DEFAULT_MAX_AGE_DAYS;
|
|
636
|
+
if (maxKeep > 0 || maxAgeDays > 0) {
|
|
637
|
+
cleanupTerminalRuns(root, maxKeep, maxAgeDays);
|
|
638
|
+
}
|
|
612
639
|
}
|
|
613
640
|
|
|
614
641
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-taskflow",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.16",
|
|
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",
|
|
@@ -31,12 +31,14 @@
|
|
|
31
31
|
"skills",
|
|
32
32
|
"examples",
|
|
33
33
|
"README.md",
|
|
34
|
+
"README.zh-CN.md",
|
|
35
|
+
"CHANGELOG.md",
|
|
34
36
|
"DESIGN.md",
|
|
35
37
|
"LICENSE"
|
|
36
38
|
],
|
|
37
39
|
"scripts": {
|
|
38
40
|
"typecheck": "tsc --noEmit",
|
|
39
|
-
"test": "PI_TASKFLOW_BUILTIN_AGENTS_DIR= node --experimental-strip-types --test test/interpolate.test.ts test/condition.test.ts test/schema.test.ts test/usage.test.ts test/runtime.test.ts test/features.test.ts test/runner.test.ts test/store.test.ts test/agents.test.ts test/init.test.ts test/render.test.ts test/desugar.test.ts test/cache.test.ts test/loop.test.ts test/tournament.test.ts test/verify.test.ts test/gate-eval.test.ts",
|
|
41
|
+
"test": "PI_TASKFLOW_BUILTIN_AGENTS_DIR= node --experimental-strip-types --test test/interpolate.test.ts test/condition.test.ts test/schema.test.ts test/usage.test.ts test/runtime.test.ts test/features.test.ts test/runner.test.ts test/store.test.ts test/agents.test.ts test/init.test.ts test/render.test.ts test/desugar.test.ts test/cache.test.ts test/loop.test.ts test/tournament.test.ts test/verify.test.ts test/gate-eval.test.ts test/transient-error.test.ts test/runtime-branches.test.ts test/interpolate-extended.test.ts test/store-extended.test.ts",
|
|
40
42
|
"test:e2e": "PI_TASKFLOW_PI_BIN=pi node --experimental-strip-types test/e2e.mts",
|
|
41
43
|
"test:dogfood-cache": "node --experimental-strip-types test/dogfood-cache.mts"
|
|
42
44
|
},
|
|
@@ -46,7 +48,8 @@
|
|
|
46
48
|
],
|
|
47
49
|
"skills": [
|
|
48
50
|
"./skills"
|
|
49
|
-
]
|
|
51
|
+
],
|
|
52
|
+
"image": "https://raw.githubusercontent.com/heggria/pi-taskflow/main/assets/social-preview.png"
|
|
50
53
|
},
|
|
51
54
|
"peerDependencies": {
|
|
52
55
|
"@earendil-works/pi-agent-core": "*",
|