pi-agent-flow 1.4.4 → 1.6.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
@@ -81,8 +81,8 @@ The result is faster, cheaper, and cleaner delegation: the main agent remains un
81
81
  | Flow | Purpose | Tools | Tier |
82
82
  |------|---------|-------|------|
83
83
  | `[scout]` | Discover files, trace code paths, map architecture | `batch`, `bash`, `find`, `grep`, `ls` | `lite` |
84
- | `[debug]` | Investigate logs, errors, stack traces, root causes | `batch`, `bash`, `find`, `grep`, `ls` | `lite` |
85
- | `[build]` | Implement features, fix bugs, write tests, ship | `batch`, `bash`, `find`, `grep`, `ls` | `flash` |
84
+ | `[debug]` | Investigate logs, errors, stack traces, root causes, and update relevant troubleshooting docs | `batch`, `bash`, `find`, `grep`, `ls` | `lite` |
85
+ | `[build]` | Implement features, fix bugs, write tests, update relevant docs, ship | `batch`, `bash`, `find`, `grep`, `ls` | `flash` |
86
86
  | `[craft]` | Plan structure, break down requirements, design solutions | `batch`, `bash`, `find`, `grep`, `ls` | `full` |
87
87
  | `[audit]` | Audit security, quality, correctness; fix issues autonomously | `batch`, `bash`, `find`, `grep`, `ls` | `flash` |
88
88
  | `[ideas]` | Generate ideas and explore possibilities with inherited context | `batch`, `bash` | `full` |
@@ -91,6 +91,8 @@ The result is faster, cheaper, and cleaner delegation: the main agent remains un
91
91
 
92
92
  > **Clean slate:** Set `inheritContext: false` in a custom flow's front-matter so it receives only the intent, ideal for unbiased creative work.
93
93
 
94
+ > **Docs hygiene:** Bundled `build` and `debug` flows are instructed to update relevant documentation after their work when the findings or implementation change developer or operational knowledge. If no docs apply, they should state why in the final report.
95
+
94
96
  ### Session modes
95
97
 
96
98
  Each flow call may set `sessionMode` to choose the child-agent time budget:
package/agents/build.md CHANGED
@@ -17,8 +17,9 @@ During this build flow — your mission is to implement and verify changes. Be a
17
17
  4. Execute — implement changes following core principles.
18
18
  5. Refactor — clean up only if the change is working.
19
19
  6. Verify — run tests and relevant checks before considering the work done.
20
- 7. Shipcommit, push, monitor CI/CD, and fix failures until green when shipping is in scope.
21
- 8. Finalizeconfirm implementation, tests, and CI/CD status.
20
+ 7. Documentupdate relevant docs after the implementation and verification are settled; if no docs apply, state why.
21
+ 8. Shipcommit, push, monitor CI/CD, and fix failures until green when shipping is in scope.
22
+ 9. Finalize — confirm implementation, docs, tests, and CI/CD status.
22
23
 
23
24
  ## Rules
24
25
 
@@ -28,6 +29,7 @@ During this build flow — your mission is to implement and verify changes. Be a
28
29
  - If already on a feature/fix branch, continue on it.
29
30
  - Commit with a clear conventional message such as `feat:`, `fix:`, or `refactor:` when committing is in scope.
30
31
  - Push only after local verification passes when shipping is in scope.
32
+ - Update relevant documentation after finishing the work; if no docs changed, explain why in the final report.
31
33
  - If CI/CD fails, diagnose, fix, commit, push, and repeat until green.
32
34
  - If an unexpected error or trace is needed, recommend [debug] rather than guessing.
33
35
 
package/agents/debug.md CHANGED
@@ -16,6 +16,8 @@ During this debug flow — your mission is to investigate root cause. Be forensi
16
16
  3. Check changes — inspect recent diffs, configuration, dependencies, and environment differences.
17
17
  4. Identify root cause — state exactly what is broken and why.
18
18
  5. Recommend fix — propose the smallest safe correction only after evidence confirms the cause.
19
+ 6. Document — update relevant docs, runbooks, or troubleshooting notes after finishing the investigation; if no docs apply, state why.
20
+ 7. Finalize — confirm root cause, evidence, documentation updates, and recommended next steps.
19
21
 
20
22
  ## Rules
21
23
 
@@ -23,7 +25,8 @@ During this debug flow — your mission is to investigate root cause. Be forensi
23
25
  - Read logs and symptoms before reading broad code areas.
24
26
  - Use `batch` with `o: "read"`, `s: <offset>`, and `l: <limit>` for targeted file reading instead of bash `sed`/`head`/`tail`.
25
27
  - Do not suggest fixes until root cause is confirmed.
26
- - Do not implement changes from this flow unless explicitly requested.
28
+ - Documentation-only updates are required after finishing the work when relevant and safe; if no docs changed, explain why in the final report.
29
+ - Do not implement product-code changes from this flow unless explicitly requested.
27
30
 
28
31
  ## Note
29
32
  Treat this as a clean-slate system rewrite, unless explicitly mentioned in the requirements. Perform a comprehensive migration with zero requirements for backwards compatibility. You must ensure that all residual code, variable names, test suites, and documentation are fully refactored and perfectly aligned with the new architecture.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-flow",
3
- "version": "1.4.4",
3
+ "version": "1.6.0",
4
4
  "description": "Flow-state delegation extension for Pi coding agent.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/agents.ts CHANGED
@@ -56,6 +56,18 @@ export function getFlowTier(flowName: string): FlowTier {
56
56
  }
57
57
  }
58
58
 
59
+ /** Return the bundled flow names associated with a given tier. */
60
+ export function getTierFlowNames(tier: FlowTier): string[] {
61
+ switch (tier) {
62
+ case "lite":
63
+ return ["scout", "debug"];
64
+ case "flash":
65
+ return ["build", "audit"];
66
+ case "full":
67
+ return ["ideas", "craft"];
68
+ }
69
+ }
70
+
59
71
  // ---------------------------------------------------------------------------
60
72
  // Internal helpers
61
73
  // ---------------------------------------------------------------------------
package/src/config.ts CHANGED
@@ -9,8 +9,8 @@ import * as fs from "node:fs";
9
9
  import * as os from "node:os";
10
10
  import * as path from "node:path";
11
11
  import { parseAgentSessionMode, type AgentSessionMode } from "./session-mode.js";
12
+ import { type FlowTier } from "./agents.js";
12
13
 
13
- export type FlowTier = "lite" | "flash" | "full";
14
14
 
15
15
  export interface FlowModelTierConfig {
16
16
  primary?: string;
@@ -369,3 +369,23 @@ export function resolveFlowModelCandidates(opts: {
369
369
 
370
370
  return { primary: candidates[0], candidates };
371
371
  }
372
+
373
+ export function formatFlowModelStrategy(modeName: string, strategy: FlowModelStrategy): string {
374
+ const tiers: FlowTier[] = ["lite", "flash", "full"];
375
+ const parts: string[] = [];
376
+ for (const tier of tiers) {
377
+ const config = strategy[tier];
378
+ const hasPrimary = Boolean(config?.primary);
379
+ const hasFailover = config?.failover && config.failover.length > 0;
380
+ let value: string;
381
+ if (hasPrimary) {
382
+ value = config!.primary!;
383
+ } else if (hasFailover) {
384
+ value = `failover: ${config!.failover!.join(", ")}`;
385
+ } else {
386
+ value = "(default)";
387
+ }
388
+ parts.push(`${tier}: ${value}`);
389
+ }
390
+ return `mode: ${modeName} | ${parts.join(" · ")}`;
391
+ }
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  resolveFlowModelCandidates,
18
18
  selectFlowModelStrategy,
19
19
  writeGlobalFlowMode,
20
+ formatFlowModelStrategy,
20
21
  type LoadedFlowModelConfigs,
21
22
  } from "./config.js";
22
23
  import { getInheritedCliArgs } from "./cli-args.js";
@@ -727,8 +728,10 @@ export default function (pi: ExtensionAPI) {
727
728
  );
728
729
  } else {
729
730
  try {
730
- const writeResult = writeGlobalFlowMode(requestedFlowMode);
731
- console.warn(`[pi-agent-flow] Flow mode switched to "${requestedFlowMode}" in ${writeResult.path}.`);
731
+ writeGlobalFlowMode(requestedFlowMode);
732
+ const strategy = loadedFlowModelConfigs.configs[requestedFlowMode] ?? {};
733
+ const strategyDescription = formatFlowModelStrategy(requestedFlowMode, strategy);
734
+ console.warn(strategyDescription);
732
735
  } catch (error) {
733
736
  const message = error instanceof Error ? error.message : String(error);
734
737
  console.warn(`[pi-agent-flow] ${message}`);
@@ -1091,7 +1094,12 @@ flow [type] accomplished
1091
1094
  pi.emit("pi-agent-flow:ready", pluginApi);
1092
1095
  }
1093
1096
 
1094
- // Register cleanup on process exit (once)
1097
+ // Register cleanup on process exit (once).
1098
+ // We intentionally do NOT hook SIGINT/SIGTERM here. A plugin must not
1099
+ // override the host process signal handling; doing so can cut off the
1100
+ // host's terminal-cleanup logic and leave escape sequences or status
1101
+ // lines visible on Ctrl+C. The 'exit' event fires for normal exits,
1102
+ // including when the host handles the signal and calls process.exit().
1095
1103
  if (!(globalThis as any).__pi_agent_flow_shutdown_registered) {
1096
1104
  (globalThis as any).__pi_agent_flow_shutdown_registered = true;
1097
1105
  const emitShutdown = () => {
@@ -1100,8 +1108,6 @@ flow [type] accomplished
1100
1108
  }
1101
1109
  };
1102
1110
  process.on("exit", emitShutdown);
1103
- process.on("SIGINT", () => { emitShutdown(); process.exit(130); });
1104
- process.on("SIGTERM", () => { emitShutdown(); process.exit(143); });
1105
1111
  }
1106
1112
 
1107
1113
  }
@@ -47,27 +47,30 @@ export function formatCompactTokenPair(usage: Partial<UsageStats>): string {
47
47
  return `↑ ${formatFixedTokens(usage.input || 0)} · ↓ ${formatFixedTokens(usage.output || 0)}`;
48
48
  }
49
49
 
50
- export function formatCompactStats(usage: Partial<UsageStats>, model?: string, maxWidth?: number): string {
51
- const parts: string[] = [];
52
- parts.push(`↑ ${formatFixedTokens(usage.input || 0)}`);
53
- parts.push(`↓ ${formatFixedTokens(usage.output || 0)}`);
54
- parts.push(`tps: ${formatTps(usage.smoothedTps)}`);
55
- parts.push(`ctx: ${formatFixedTokens(usage.contextTokens || 0)}`);
50
+ export function formatCompactStats(
51
+ usage: Partial<UsageStats>,
52
+ model?: string,
53
+ maxWidth?: number,
54
+ options: { skipTokens?: boolean } = {},
55
+ ): string {
56
+ const tokenParts = [`↑ ${formatFixedTokens(usage.input || 0)}`, `↓ ${formatFixedTokens(usage.output || 0)}`];
57
+ const runtimeParts = [`tps: ${formatTps(usage.smoothedTps)}`, `ctx: ${formatFixedTokens(usage.contextTokens || 0)}`];
58
+ const parts = options.skipTokens ? runtimeParts : [...tokenParts, ...runtimeParts];
56
59
 
57
60
  const displayModel = model ? model.replace(/^[^/]+\//, "") : undefined;
58
61
  let result = parts.join(" · ") + (displayModel ? ` · ${displayModel}` : "");
59
62
  if (maxWidth && visibleLength(result) > maxWidth) {
60
- // Drop model first
63
+ // Drop model first.
61
64
  let narrow = parts.join(" · ");
62
65
  if (visibleLength(narrow) <= maxWidth) return narrow;
63
66
 
64
- // Drop context tokens
65
- const narrowParts = parts.slice(0, 3); // up to tps
66
- narrow = narrowParts.join(" · ");
67
+ // Drop context tokens next.
68
+ const withoutContext = parts.filter((part) => !part.startsWith("ctx:"));
69
+ narrow = withoutContext.join(" · ");
67
70
  if (visibleLength(narrow) <= maxWidth) return narrow;
68
71
 
69
- // Bare minimum (just input/output)
70
- narrow = `${parts[0]} · ${parts[1]}`;
72
+ // Bare minimum: token pair for normal stats, tps for token-free headers.
73
+ narrow = options.skipTokens ? runtimeParts[0] : tokenParts.join(" · ");
71
74
  if (visibleLength(narrow) <= maxWidth) return narrow;
72
75
 
73
76
  return truncateChars(result, maxWidth);
@@ -105,12 +108,15 @@ export function getTruncationBudget(prefixLength: number): number {
105
108
  /** Fixed content budget for collapsed-line text (dir/act/log). */
106
109
  export const CONTENT_MAX = 60;
107
110
 
111
+ /** Longer budget for detail lines (act/msg) to show more content. */
112
+ export const DETAIL_CONTENT_MAX = 80;
113
+
108
114
  /**
109
115
  * Compute how many visible chars of content fit after a prefix,
110
- * using the fixed CONTENT_MAX budget. Floor of 8 to keep things readable.
116
+ * using the given budget (defaults to CONTENT_MAX). Floor of 8 to keep things readable.
111
117
  */
112
- export function contentBudget(prefixVisibleLen: number): number {
113
- return Math.max(CONTENT_MAX - prefixVisibleLen, 8);
118
+ export function contentBudget(prefixVisibleLen: number, max = CONTENT_MAX): number {
119
+ return Math.max(max - prefixVisibleLen, 8);
114
120
  }
115
121
 
116
122
  /**
package/src/render.ts CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  isFlowError,
23
23
  isFlowSuccess,
24
24
  } from "./types.js";
25
- import { formatCompactStats, formatCompactTokenPair, formatCountdown, formatFlowTypeName, truncateChars, tailText, contentBudget, visibleLength } from "./render-utils.js";
25
+ import { formatCompactStats, formatCompactTokenPair, formatCountdown, formatFlowTypeName, truncateChars, tailText, contentBudget, visibleLength, DETAIL_CONTENT_MAX } from "./render-utils.js";
26
26
 
27
27
  function shortenPath(p: string): string {
28
28
  const home = os.homedir();
@@ -33,6 +33,12 @@ type ThemeFg = (color: string, text: string) => string;
33
33
  type ThemeBg = (color: string, text: string) => string;
34
34
  type FlowTheme = { fg: ThemeFg; bold: (s: string) => string; bg: ThemeBg };
35
35
 
36
+ const COLLAPSED_FLOW_HEADER_TYPE_WIDTH = 8;
37
+
38
+ function formatCollapsedFlowHeaderTypeName(type: string): string {
39
+ return formatFlowTypeName(type).padEnd(COLLAPSED_FLOW_HEADER_TYPE_WIDTH, " ");
40
+ }
41
+
36
42
  function formatFlowToolCall(toolName: string, args: Record<string, unknown>, fg: ThemeFg): string {
37
43
  const pathArg = (args.file_path || args.path || "...") as string;
38
44
 
@@ -319,9 +325,9 @@ function renderFlowCollapsed(
319
325
  ): Container {
320
326
  const container = new Container();
321
327
  const maxWidth = process.stdout.columns ?? 80;
322
- const stats = formatCompactStats(r.usage, r.model, maxWidth);
323
- const typeName = formatFlowTypeName(r.type);
324
- let header = `${theme.fg("accent", theme.bold(typeName))} ${theme.fg("dim", stats)}`;
328
+ const stats = formatCompactStats(r.usage, r.model, maxWidth, { skipTokens: true });
329
+ const typeName = formatCollapsedFlowHeaderTypeName(r.type);
330
+ let header = `${theme.fg("accent", theme.bold(typeName))}${theme.fg("dim", `· ${stats}`)}`;
325
331
  if (error && r.stopReason) header += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
326
332
  container.addChild(new TruncatedText(header, 0, 0));
327
333
 
@@ -337,13 +343,13 @@ function renderFlowCollapsed(
337
343
  if (lastTool) {
338
344
  const actStr = formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme));
339
345
  const actPrefix = `├─ act: [${r.usage.toolCalls}] - `;
340
- const actContent = truncateChars(actStr, contentBudget(visibleLength(actPrefix)));
346
+ const actContent = truncateChars(actStr, contentBudget(visibleLength(actPrefix), DETAIL_CONTENT_MAX));
341
347
  container.addChild(new TruncatedText(`${theme.fg("dim", actPrefix)}${actContent}`, 0, 0));
342
348
  }
343
349
 
344
350
  // msg: line (last assistant text or streaming)
345
351
  const msgPrefix = formatMsgLinePrefix("└─", r);
346
- const msgBudget = contentBudget(visibleLength(msgPrefix));
352
+ const msgBudget = contentBudget(visibleLength(msgPrefix), DETAIL_CONTENT_MAX);
347
353
  if (r.exitCode === -1 && streamingText) {
348
354
  const logContent = tailText(streamingText, msgBudget);
349
355
  container.addChild(new TruncatedText(`${theme.fg("dim", msgPrefix)}${theme.fg("dim", logContent)}`, 0, 0));
@@ -451,13 +457,13 @@ function renderActivityPanel(
451
457
  for (let i = 0; i < results.length; i++) {
452
458
  const r = results[i];
453
459
  const isLast = i === results.length - 1;
454
- const stats = formatCompactStats(r.usage, r.model, maxWidth);
460
+ const stats = formatCompactStats(r.usage, r.model, maxWidth, { skipTokens: true });
455
461
  const error = isFlowError(r);
456
- const typeName = formatFlowTypeName(r.type);
462
+ const typeName = formatCollapsedFlowHeaderTypeName(r.type);
457
463
 
458
464
  // Header line
459
465
  const headerPrefix = isLast ? "└─" : "├─";
460
- let headerLine = `${theme.fg("dim", headerPrefix)} ${theme.fg("accent", theme.bold(typeName))} ${theme.fg("dim", stats)}`;
466
+ let headerLine = `${theme.fg("dim", headerPrefix)} ${theme.fg("accent", theme.bold(typeName))}${theme.fg("dim", `· ${stats}`)}`;
461
467
  if (error && r.stopReason) {
462
468
  headerLine += ` ${theme.fg("error", `[${r.stopReason}]`)}`;
463
469
  }
@@ -478,13 +484,13 @@ function renderActivityPanel(
478
484
  if (lastTool) {
479
485
  const actStr = formatFlowToolCall(lastTool.name, lastTool.args, theme.fg.bind(theme));
480
486
  const actPrefix = `${indent}├─ act: [${r.usage.toolCalls}] - `;
481
- const actContent = truncateChars(actStr, contentBudget(visibleLength(actPrefix)));
487
+ const actContent = truncateChars(actStr, contentBudget(visibleLength(actPrefix), DETAIL_CONTENT_MAX));
482
488
  container.addChild(new TruncatedText(`${theme.fg("dim", actPrefix)}${actContent}`, 0, 0));
483
489
  }
484
490
 
485
491
  // msg: line (live streaming text or last assistant text)
486
492
  const msgPrefix = formatMsgLinePrefix(indent + "└─", r);
487
- const msgBudget = contentBudget(visibleLength(msgPrefix));
493
+ const msgBudget = contentBudget(visibleLength(msgPrefix), DETAIL_CONTENT_MAX);
488
494
  const liveText = r.exitCode === -1 ? r.streamingText : undefined;
489
495
  const lastText = liveText || getLastAssistantText(r.messages);
490
496
  if (lastText) {