pi-agent-flow 0.2.8 → 0.3.0

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- <p align="center"><code>pi install /path/to/pi-agent-flow</code></p>
1
+ <p align="center"><code>pi install npm:pi-agent-flow</code></p>
2
2
  <p align="center"><strong>Pi Agent Flow</strong> is a flow-state delegation extension for the <a href="https://pi.dev">Pi coding agent</a> that runs locally in your terminal.</p>
3
3
  <p align="center">
4
4
  <img src="https://github.com/user-attachments/assets/pi-agent-flow-demo.png" alt="Pi Agent Flow demo" width="80%" />
@@ -11,11 +11,10 @@
11
11
 
12
12
  ### Installing Pi Agent Flow
13
13
 
14
- Install via the Pi CLI with your local path:
14
+ Install via the Pi CLI from npm:
15
15
 
16
16
  ```shell
17
- # Install from a local path
18
- pi install /path/to/pi-agent-flow
17
+ pi install npm:pi-agent-flow
19
18
  ```
20
19
 
21
20
  Or add it to your Pi settings:
@@ -24,7 +23,7 @@ Or add it to your Pi settings:
24
23
  # ~/.pi/agent/settings.json
25
24
  {
26
25
  "packages": [
27
- "./path/to/pi-agent-flow"
26
+ "npm:pi-agent-flow"
28
27
  ]
29
28
  }
30
29
  ```
@@ -32,9 +31,10 @@ Or add it to your Pi settings:
32
31
  Then start Pi and delegate tasks using flow states.
33
32
 
34
33
  <details>
35
- <summary>You can also clone this repository and use it directly.</summary>
34
+ <summary>You can also install from a local path.</summary>
36
35
 
37
36
  ```shell
37
+ # Install from a local clone
38
38
  git clone https://github.com/your-org/pi-agent-flow.git
39
39
  cd pi-agent-flow
40
40
  pi install .
@@ -56,6 +56,21 @@ pi install .
56
56
 
57
57
  ---
58
58
 
59
+ ## Why Flow Style?
60
+
61
+ Flow-style delegation is designed for **context efficiency**. Instead of launching every sub-agent with the full, ever-growing conversation history, each flow receives only what it needs: your intent and (when appropriate) a session snapshot.
62
+
63
+ This approach delivers four concrete benefits:
64
+
65
+ 1. **Avoid duplicate tool calls** — every sub-agent launch no longer re-runs the same `read`, `grep`, or `bash` probes that the parent already performed.
66
+ 2. **Prevent context bloat** — long transcripts with repeated file listings and command outputs are kept out of the main conversation thread.
67
+ 3. **Eliminate unnecessary noise** — the parent agent sees only structured results (`[Summary]`, `[Done]`, `[Not Done]`, `[Next Steps]`) instead of pages of intermediate reasoning.
68
+ 4. **Preserve focus** — each flow stays locked on its intent because it isn't distracted by unrelated earlier messages.
69
+
70
+ The result is faster, cheaper, and cleaner delegation: the main agent remains uncluttered while specialized flows do the heavy lifting in isolated contexts.
71
+
72
+ ---
73
+
59
74
  ## Flow Definitions
60
75
 
61
76
  Create `.md` files in `~/.pi/agent/agents/` or `.pi/agents/`:
package/agents/code.md CHANGED
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: code
3
- description: Implement features, fix bugs, write tests
3
+ description: Implement features, fix bugs, write tests, deploy, and ship
4
4
  tools: read, write, edit, bash, find, grep, ls
5
5
  maxDepth: 2
6
6
  ---
7
7
 
8
- You are the code flow — your mission is to build. Be a craftsman: verify first, then ship. The conversation history above provides background context; treat it as reference only and do not let it distract from your objective.
8
+ You are the code flow — your mission is to build and ship. Be a craftsman: verify first, then ship. The conversation history above provides background context; treat it as reference only and do not let it distract from your objective.
9
9
 
10
10
  Core Principles:
11
11
  - SOLID: Single Responsibility, Open/Closed
@@ -19,7 +19,16 @@ Workflow:
19
19
  4. Execute — implement changes following core principles (green)
20
20
  5. Refactor — clean up only if the change is working (optional)
21
21
  6. Verify — run tests and any relevant checks before considering done
22
- 7. Finalizeall tests pass, implementation verified
22
+ 7. Shipcommit, push, monitor CI/CD pipeline, diagnose and fix failures until green
23
+ 8. Finalize — all tests pass, CI/CD green, implementation verified
24
+
25
+ Shipping Guidelines:
26
+ - Commit with a clear, conventional message (feat:, fix:, refactor:, etc.)
27
+ - Push to the target branch after local verification passes
28
+ - Monitor CI/CD pipeline status after pushing
29
+ - If CI/CD fails: diagnose the failure, fix it, commit, push, and repeat until green
30
+ - Only report back if there are serious conflicts or issues you cannot resolve autonomously
31
+ - You own the full ship cycle: implement → test → commit → push → monitor CI → fix if needed
23
32
 
24
33
  If you hit an unexpected error or need to trace execution, delegate to [debug] rather than guessing.
25
34
 
@@ -32,6 +41,7 @@ flow [code] accomplished
32
41
  [Done]
33
42
  - changes made with file:line references
34
43
  - tests written or run
44
+ - CI/CD status (committed, pushed, pipeline status)
35
45
 
36
46
  [Not Done]
37
47
  - incomplete items and reasons
package/agents.ts CHANGED
@@ -110,7 +110,7 @@ function parseFlowFile(filePath: string, source: "user" | "project" | "bundled")
110
110
  const frontmatter = parsed.frontmatter ?? {};
111
111
  const body = parsed.body ?? "";
112
112
 
113
- const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
113
+ const name = typeof frontmatter.name === "string" ? frontmatter.name.trim().toLowerCase() : "";
114
114
  const description = typeof frontmatter.description === "string" ? frontmatter.description.trim() : "";
115
115
  if (!name || !description) return null;
116
116
 
@@ -197,7 +197,7 @@ function loadFlowsFromDir(dir: string, source: "user" | "project" | "bundled"):
197
197
  function mergeFlows(...groups: FlowConfig[][]): FlowConfig[] {
198
198
  const flowMap = new Map<string, FlowConfig>();
199
199
  for (const group of groups) {
200
- for (const flow of group) flowMap.set(flow.name, flow);
200
+ for (const flow of group) flowMap.set(flow.name.toLowerCase(), flow);
201
201
  }
202
202
  return Array.from(flowMap.values());
203
203
  }
@@ -227,7 +227,7 @@ export function discoverFlows(cwd: string, scope: FlowScope): FlowDiscoveryResul
227
227
  return { flows: mergeFlows(bundledFlows, userFlows), projectFlowsDir };
228
228
  }
229
229
  if (scope === "project") {
230
- return { flows: projectFlows, projectFlowsDir };
230
+ return { flows: mergeFlows(projectFlows), projectFlowsDir };
231
231
  }
232
232
  return {
233
233
  flows: mergeFlows(bundledFlows, userFlows, projectFlows),
package/flow.ts CHANGED
@@ -195,11 +195,12 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
195
195
  makeDetails,
196
196
  } = opts;
197
197
 
198
- const flow = flows.find((f) => f.name === flowName);
198
+ const normalizedFlowName = flowName.toLowerCase();
199
+ const flow = flows.find((f) => f.name === normalizedFlowName);
199
200
  if (!flow) {
200
201
  const available = flows.map((f) => `"${f.name}"`).join(", ") || "none";
201
202
  return {
202
- type: flowName,
203
+ type: normalizedFlowName,
203
204
  agentSource: "unknown",
204
205
  intent,
205
206
  exitCode: 1,
@@ -210,7 +211,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
210
211
  }
211
212
 
212
213
  const result: SingleResult = {
213
- type: flowName,
214
+ type: normalizedFlowName,
214
215
  agentSource: flow.source,
215
216
  intent,
216
217
  exitCode: -1,
@@ -255,7 +256,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
255
256
  const exitCode = await new Promise<number>((resolve) => {
256
257
  const nextDepth = Math.max(0, Math.floor(parentDepth)) + 1;
257
258
  const propagatedMaxDepth = Math.max(0, Math.floor(maxDepth));
258
- const propagatedStack = [...parentFlowStack, flowName];
259
+ const propagatedStack = [...parentFlowStack, normalizedFlowName];
259
260
  const { command, prefixArgs } = resolveFlowSpawn();
260
261
  const proc = spawn(command, [...prefixArgs, ...piArgs], {
261
262
  cwd: taskCwd ?? cwd,
package/index.ts CHANGED
@@ -36,7 +36,7 @@ const FLOW_PREVENT_CYCLES_ENV = "PI_FLOW_PREVENT_CYCLES";
36
36
 
37
37
  const FlowItem = Type.Object({
38
38
  type: Type.String({
39
- description: "Flow type. Must match an available flow name exactly: explore, debug, code, architect, review, brainstorm.",
39
+ description: "Flow type. Matching is case-insensitive. Must correspond to an available flow name such as explore, debug, code, architect, review, or brainstorm.",
40
40
  }),
41
41
  intent: Type.String({
42
42
  description: "Clear, specific mission for this flow.",
@@ -137,7 +137,7 @@ function parseFlowStack(raw: unknown): string[] | null {
137
137
  if (!Array.isArray(parsed)) return null;
138
138
  if (!parsed.every((value) => typeof value === "string")) return null;
139
139
  return parsed
140
- .map((value) => value.trim())
140
+ .map((value) => value.trim().toLowerCase())
141
141
  .filter((value) => value.length > 0);
142
142
  }
143
143
 
@@ -303,7 +303,7 @@ function getRequestedProjectFlows(
303
303
  requestedNames: Set<string>,
304
304
  ): FlowConfig[] {
305
305
  return Array.from(requestedNames)
306
- .map((name) => flows.find((f) => f.name === name))
306
+ .map((name) => flows.find((f) => f.name === name.toLowerCase()))
307
307
  .filter((f): f is FlowConfig => f?.source === "project");
308
308
  }
309
309
 
@@ -405,10 +405,26 @@ flow [type] accomplished
405
405
  label: "Flow",
406
406
  description: [
407
407
  "Delegate work to flow states running in isolated pi processes.",
408
- "Each flow receives a snapshot of your current session context.",
408
+ "Each flow receives a forked snapshot of your current session context.",
409
409
  "All flows run in parallel — batch independent tasks into one call.",
410
410
  "",
411
411
  'Usage: { "flow": [{ "type": "explore", "intent": "..." }, ...] }',
412
+ "",
413
+ "Each flow type is a specialized agent configuration. Standard types:",
414
+ "\u2022 explore \u2014 Discovery, trace code paths, map architecture",
415
+ "\u2022 debug \u2014 Root-cause analysis, investigate errors and stack traces",
416
+ "\u2022 code \u2014 Implementation, tests, builds, deploy, git commit",
417
+ "\u2022 architect \u2014 Structural design, planning before building",
418
+ "\u2022 review \u2014 Read-only audit for security, quality, correctness",
419
+ "\u2022 brainstorm \u2014 Fresh ideas from a clean slate (no inherited context)",
420
+ "",
421
+ "Custom flows can be defined as .md files in .pi/agents/ (project)",
422
+ "or ~/.pi/agent/agents/ (user). They override bundled flows by name.",
423
+ "",
424
+ "Each flow returns: [Summary], [Done], [Not Done], [Next Steps]",
425
+ "",
426
+ "Guards: Max delegation depth and cycle prevention are enforced.",
427
+ "Blocked flows return an error with the reason.",
412
428
  ].join("\n"),
413
429
  parameters: FlowParams,
414
430
 
@@ -423,7 +439,7 @@ flow [type] accomplished
423
439
  );
424
440
 
425
441
  // Collect all requested flow names
426
- const requested = new Set(params.flow.map((f) => f.type));
442
+ const requested = new Set(params.flow.map((f) => f.type.toLowerCase()));
427
443
 
428
444
  // Cycle check
429
445
  if (preventCycles) {
@@ -483,58 +499,52 @@ flow [type] accomplished
483
499
  }
484
500
 
485
501
  let lastStreamingText = "";
502
+ let lastEmittedText: string | undefined;
486
503
  const emitProgress = (streamingText?: string) => {
487
504
  if (!onUpdate) return;
488
- if (streamingText) lastStreamingText = streamingText;
505
+ if (streamingText !== undefined) lastStreamingText = streamingText;
506
+ const text = lastStreamingText || "";
507
+ if (text === lastEmittedText) return;
508
+ lastEmittedText = text;
489
509
  onUpdate({
490
- content: [{ type: "text", text: lastStreamingText || "" }],
510
+ content: [{ type: "text", text }],
491
511
  details: makeDetails([...allResults]),
492
512
  });
493
513
  };
494
514
 
495
- let heartbeat: NodeJS.Timeout | undefined;
496
- if (onUpdate) {
497
- emitProgress();
498
- heartbeat = setInterval(() => {
499
- if (allResults.some((r) => r.exitCode === -1)) emitProgress();
500
- }, 1000);
501
- }
502
-
503
- let results: SingleResult[];
504
- try {
505
- results = await mapFlowConcurrent(params.flow, 4, async (item, index) => {
506
- const targetFlow = flows.find((f) => f.name === item.type);
507
- const effectiveMaxDepth =
508
- targetFlow?.maxDepth !== undefined ? targetFlow.maxDepth : maxDepth;
509
-
510
- const shouldInheritContext = targetFlow?.inheritContext !== false;
511
- const result = await runFlow({
512
- cwd: ctx.cwd,
513
- flows,
514
- flowName: item.type,
515
- intent: item.intent,
516
- taskCwd: item.cwd,
517
- forkSessionSnapshotJsonl: shouldInheritContext ? forkSessionSnapshotJsonl : null,
518
- parentDepth: currentDepth,
519
- parentFlowStack: ancestorFlowStack,
520
- maxDepth: effectiveMaxDepth,
521
- preventCycles,
522
- signal,
523
- onUpdate: (partial) => {
524
- if (partial.details?.results[0]) {
525
- allResults[index] = partial.details.results[0];
526
- emitProgress(partial.content?.[0]?.text);
527
- }
528
- },
529
- makeDetails,
530
- });
531
- allResults[index] = result;
532
- emitProgress();
533
- return result;
515
+ if (onUpdate) emitProgress();
516
+
517
+ const results = await mapFlowConcurrent(params.flow, 4, async (item, index) => {
518
+ const normalizedType = item.type.toLowerCase();
519
+ const targetFlow = flows.find((f) => f.name === normalizedType);
520
+ const effectiveMaxDepth =
521
+ targetFlow?.maxDepth !== undefined ? targetFlow.maxDepth : maxDepth;
522
+
523
+ const shouldInheritContext = targetFlow?.inheritContext !== false;
524
+ const result = await runFlow({
525
+ cwd: ctx.cwd,
526
+ flows,
527
+ flowName: normalizedType,
528
+ intent: item.intent,
529
+ taskCwd: item.cwd,
530
+ forkSessionSnapshotJsonl: shouldInheritContext ? forkSessionSnapshotJsonl : null,
531
+ parentDepth: currentDepth,
532
+ parentFlowStack: ancestorFlowStack,
533
+ maxDepth: effectiveMaxDepth,
534
+ preventCycles,
535
+ signal,
536
+ onUpdate: (partial) => {
537
+ if (partial.details?.results[0]) {
538
+ allResults[index] = partial.details.results[0];
539
+ emitProgress(partial.content?.[0]?.text);
540
+ }
541
+ },
542
+ makeDetails,
534
543
  });
535
- } finally {
536
- if (heartbeat) clearInterval(heartbeat);
537
- }
544
+ allResults[index] = result;
545
+ emitProgress();
546
+ return result;
547
+ });
538
548
 
539
549
  // Build tool result with FULL flow output — no truncation
540
550
  const successCount = results.filter((r) => isFlowSuccess(r)).length;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-flow",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
4
4
  "description": "Flow-state delegation extension for Pi coding agent.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
package/render-utils.ts CHANGED
@@ -48,14 +48,14 @@ export function formatFixedTokens(count: number): string {
48
48
  }
49
49
 
50
50
  /**
51
- * Format flow type name to fixed width (10 chars) in uppercase with dot padding.
52
- * Examples: "debug" → "DEBUG.....", "architect" → "ARCHITECT.", "brainstorm" → "BRAINSTORM"
51
+ * Format flow type name to fixed width (10 chars) in lowercase with dot padding.
52
+ * Examples: "debug" → "debug.....", "architect" → "architect.", "brainstorm" → "brainstorm"
53
53
  */
54
54
  export function formatFlowTypeName(type: string): string {
55
- const upper = type.toUpperCase();
55
+ const lower = type.toLowerCase();
56
56
  const targetWidth = 10;
57
- if (upper.length >= targetWidth) return upper.slice(0, targetWidth);
58
- return upper + ".".repeat(targetWidth - upper.length);
57
+ if (lower.length >= targetWidth) return lower.slice(0, targetWidth);
58
+ return lower + ".".repeat(targetWidth - lower.length);
59
59
  }
60
60
 
61
61
  export function formatCompactStats(usage: Partial<UsageStats>, model?: string): string {
@@ -157,6 +157,7 @@ function truncateAnsi(text: string, max: number): string {
157
157
  }
158
158
 
159
159
  export function truncateChars(text: string, max: number): string {
160
+ text = text.replace(/[\n\r\t]+/g, " ").replace(/ +/g, " ").trim();
160
161
  if (visibleLength(text) <= max) return text;
161
162
  return truncateAnsi(text, max);
162
163
  }