pi-agent-flow 1.6.0 → 1.7.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/README.md CHANGED
@@ -31,7 +31,7 @@ Then start Pi and delegate tasks using flow states.
31
31
 
32
32
  ```shell
33
33
  # Install from a local clone
34
- git clone https://github.com/your-org/pi-agent-flow.git
34
+ git clone https://github.com/tuanhung303/pi-agent-flow.git
35
35
  cd pi-agent-flow
36
36
  pi install .
37
37
  ```
@@ -45,18 +45,25 @@ pi install .
45
45
  - **Flow-state delegation** — six bundled specialist flows (`scout`, `debug`, `build`, `craft`, `audit`, `ideas`) plus custom flows via Markdown front-matter
46
46
  - **Isolated forked context** — each flow runs as an isolated `pi` child process with a session snapshot (or clean slate when configured)
47
47
  - **Parallel execution** — batch independent flows into one call with bounded concurrency
48
- - **Structured reports** — every flow returns `[Summary]`, `[Done]`, `[Not Done]`, `[Next Steps]`
48
+ - **Structured reports** — every flow returns `[Summary]`, `[Done]`, `[Not Done]`, `[Next Steps]`; optional JSON schema with files, actions, commands, and reasoning
49
+ - **Mechanically enriched commands** — bash commands in structured output are replaced with exact verbatim tool-call strings and annotated with `executionTime`
49
50
  - **Depth guards** — configurable max delegation depth (default: `3`)
50
- - **Session timeout modes** — child flows use controlled budgets: `fast` (300s), `default` (600s), or `long` (900s)
51
+ - **Session timeout modes** — child flows use controlled budgets: `fast` (300s), `default` (600s), `long` (900s), or `extreme_long` (1200s)
52
+ - **Two-stage timeout awareness** — flows receive deadline hints in their prompt; the parent UI shows live countdowns and injects warning reminders before hard kill
53
+ - **Graceful shutdown** — parent `SIGINT`/`SIGTERM` propagates to all child process groups; orphaned sub-agents are force-killed after a grace period
51
54
  - **Cycle prevention** — blocks re-entering flows already in the ancestor stack
52
55
  - **Model tiering & failover** — flows map to `lite` / `flash` / `full` tiers with primary + failover model chains
56
+ - **Persistent flow mode** — switch global model strategies with `--flow-mode`; written to `settings.json` and remembered across sessions
57
+ - **Flow-mode notification** — concise (`mode: name | lite: model · flash: model · full: model`) or verbose (with per-tier flow-name labels) startup message
58
+ - **Auto-transition** — opt-in automatic queuing of follow-up flows based on the declarative transition matrix
53
59
  - **Unified batch tools** — `batch` (read/write/edit/delete) and `batch_read` replace separate file tools for cross-cutting work
54
60
  - **Web tool** — built-in `web` search (Brave + DuckDuckGo) and page fetch with HTML→Markdown conversion
55
61
  - **Sliding system prompt** — lightweight routing reminder injected before each user message, stripped from child snapshots to avoid duplication
56
- - **Session snapshot sanitization** — removes sliding prompts, reasoning/thinking artifacts, and non-inheritable content before forking
62
+ - **Session snapshot sanitization** — removes sliding prompts, reasoning/thinking artifacts, and non-inheritable content before forking; compresses prior flow results into compact context maps
63
+ - **Shared context inheritance** — child flows receive the parent's sanitized session automatically; write forward-looking intents and let the child pick up context from its inherited snapshot
57
64
  - **Project flow confirmation** — prompts before running project-local flows from `.pi/agents/` for security
58
65
  - **Post-flow hooks** — automatic advisory messages suggesting follow-up flows (e.g., `build → audit`)
59
- - **Rich TUI rendering** — collapsed activity-panel view with per-flow stats, plus expanded view with full reports and tool traces
66
+ - **Rich TUI rendering** — collapsed activity-panel view with per-flow stats, live countdowns, and expanded view with full reports and tool traces
60
67
  - **Smooth streaming metrics** — token counters and smoothed TPS increment tick-by-tick during active streaming
61
68
 
62
69
  ---
@@ -76,15 +83,46 @@ The result is faster, cheaper, and cleaner delegation: the main agent remains un
76
83
 
77
84
  ---
78
85
 
86
+ ## Shared Context
87
+
88
+ When you delegate to a flow, the child agent receives an automatic **sanitized fork** of your current session. This lets you write concise intents that focus on **what new work to do** rather than restating the full problem.
89
+
90
+ ### How it works
91
+
92
+ 1. **Snapshot serialized** — your conversation (files read, commands run, prior flow results) is serialized into a JSONL snapshot.
93
+ 2. **Sanitized** — sliding prompts, reasoning/thinking artifacts, and other non-inheritable content are stripped.
94
+ 3. **Compressed** — prior flow tool results are compacted into short summaries: files touched, commands used, outcome status.
95
+ 4. **Forked** — the child agent loads this snapshot via `--session` at startup.
96
+
97
+ ### Writing good intents
98
+
99
+ The child already sees what you've done. Write intents that say **what to do next**, not what context it needs:
100
+
101
+ | ❌ Bad intent | ✅ Good intent |
102
+ |---|---|
103
+ | `"The auth module uses JWT tokens… inspect src/auth/ for security issues"` | `"Audit the auth module for security issues"` |
104
+ | `"We already found the bug… run the failing test and fix it"` | `"Fix the failing test in tests/auth.test.ts"` |
105
+ | `"The file structure is… implement the feature described in the PRD"` | `"Implement the feature described in the PRD"` |
106
+
107
+ ### Clean slate
108
+
109
+ Set `inheritContext: false` in a custom flow's front-matter to start with a clean slate. The child receives only your `intent` — no inherited session. This is ideal for unbiased creative work in `ideas` flows.
110
+
111
+ ### What the child sees
112
+
113
+ The child's `<context-seal>` prompt tells it: *"The conversation above is sealed — it is your session history for situational awareness only."* The child can reference files and findings already in context but shouldn't act as if it's still in the parent's conversation.
114
+
115
+ ---
116
+
79
117
  ## Bundled Flows
80
118
 
81
119
  | Flow | Purpose | Tools | Tier |
82
120
  |------|---------|-------|------|
83
121
  | `[scout]` | Discover files, trace code paths, map architecture | `batch`, `bash`, `find`, `grep`, `ls` | `lite` |
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` |
122
+ | `[debug]` | Investigate logs, errors, stack traces, root causes | `batch`, `bash`, `find`, `grep`, `ls` | `lite` |
123
+ | `[build]` | Implement features, fix bugs, write tests, update docs, ship | `batch`, `bash`, `find`, `grep`, `ls` | `flash` |
86
124
  | `[craft]` | Plan structure, break down requirements, design solutions | `batch`, `bash`, `find`, `grep`, `ls` | `full` |
87
- | `[audit]` | Audit security, quality, correctness; fix issues autonomously | `batch`, `bash`, `find`, `grep`, `ls` | `flash` |
125
+ | `[audit]` | Audit security, quality, correctness; fix safe issues autonomously | `batch`, `bash`, `find`, `grep`, `ls` | `flash` |
88
126
  | `[ideas]` | Generate ideas and explore possibilities with inherited context | `batch`, `bash` | `full` |
89
127
 
90
128
  > **Note:** All bundled flows have `maxDepth: 0`, meaning they do not delegate further by default. Custom flows can override this via front-matter.
@@ -102,6 +140,7 @@ Each flow call may set `sessionMode` to choose the child-agent time budget:
102
140
  | `fast` | 300s | quick scouting, narrow checks, small design passes |
103
141
  | `default` | 600s | normal flow work; this is the default |
104
142
  | `long` | 900s | large builds, full test runs, broad refactors, complex debugging |
143
+ | `extreme_long` | 1200s | very large refactors, extensive audits, or multi-step debugging sessions |
105
144
 
106
145
  Example:
107
146
 
@@ -120,6 +159,16 @@ Example:
120
159
 
121
160
  The public interface is mode-based; arbitrary per-flow numeric timeouts are not exposed.
122
161
 
162
+ ### Timeout behavior
163
+
164
+ Flows are aware of their deadline from the moment they start:
165
+
166
+ - **Prompt injection** — the activation block includes the exact time budget and warns the agent to wrap up before the deadline.
167
+ - **Parent UI countdown** — a live `MM:SS` countdown is shown next to the flow's aim while it runs.
168
+ - **Two-stage warnings** — at 2 minutes before hard timeout a warning is injected into the child's reminder stream; at 2 minutes 15 seconds a final urge demands the agent stop all tool use and output structured findings.
169
+ - **Grace period** — after the hard timeout fires, the agent gets a 90-second reporting grace to finish its summary before the process is force-killed.
170
+ - **Graceful shutdown** — when the parent receives `SIGINT` or `SIGTERM`, the signal propagates to every child process group so sub-agents terminate cleanly instead of becoming orphans.
171
+
123
172
  ---
124
173
 
125
174
  ## Flow Definitions
@@ -165,24 +214,72 @@ flow [myflow] accomplished
165
214
  | `thinking` | `string` | Thinking budget (e.g., `"low"`, `"medium"`, `"high"`) |
166
215
  | `maxDepth` | `number` | How many more delegation levels this flow may spawn |
167
216
  | `inheritContext` | `boolean` | Whether to fork parent session snapshot (`true`) or start clean (`false`) |
217
+ | `tier` | `string` | Explicit tier override: `lite`, `flash`, or `full` |
168
218
 
169
219
  ---
170
220
 
171
- ## Post-Flow Hooks
221
+ ## Structured Output
222
+
223
+ When `structuredOutput` is enabled (default), flows are instructed to append a JSON code block to their final response. The block is mechanically validated and enriched:
224
+
225
+ - **Bash commands** are replaced with the exact verbatim strings from the actual tool calls, fixing the common LLM behaviour of paraphrasing `curl -s -X POST …` as `"curl GAWA baseline"`.
226
+ - **Execution time** is captured from the timed-bash wrapper and attached to each bash command entry.
172
227
 
173
- When certain flows complete successfully, the system injects advisory messages suggesting follow-up flows. This keeps the agent on the optimal path without requiring the user to manually chain flows.
228
+ Schema:
174
229
 
175
- ### Built-in Hooks
230
+ ```json
231
+ {
232
+ "version": "1.0",
233
+ "status": "complete",
234
+ "summary": "2-3 sentence summary",
235
+ "files": [
236
+ { "path": "relative/path", "role": "read", "description": "why it matters", "snippet": "short excerpt", "ranges": [{ "start": 10, "end": 25, "label": "bug" }] }
237
+ ],
238
+ "actions": [
239
+ { "type": "read", "description": "what was done", "target": "file.ts", "result": "success", "evidence": "output or proof" }
240
+ ],
241
+ "commands": [
242
+ { "command": "curl -s -X POST https://api.example.com/v1/data", "tool": "bash", "executionTime": "1.2s (normal)" }
243
+ ],
244
+ "notDone": [
245
+ { "item": "unfinished work", "reason": "why it was not completed", "blocker": "blocking issue", "nextStep": "specific follow-up" }
246
+ ],
247
+ "nextSteps": ["recommended follow-up action"],
248
+ "reasoning": ["key hypothesis or inference"],
249
+ "notes": ["observation or warning"]
250
+ }
251
+ ```
252
+
253
+ Only include fields that have data. Omit empty arrays; missing array fields are acceptable.
176
254
 
177
- | Hook | Trigger | Advice |
178
- |------|---------|--------|
179
- | `build audit` | A `[build]` flow succeeds | *"Consider running an [audit] flow to audit the changes…"* |
180
- | `debug → build` | A `[debug]` flow succeeds | *"The root cause has been identified. Consider running a [build] flow to implement the fix."* |
181
- | `audit scout` | An `[audit]` flow succeeds | *"Audit complete. Consider running a [scout] flow to trace the audit findings across the codebase."* |
255
+ ---
256
+
257
+ ## Post-Flow Hooks & Auto-Transitions
258
+
259
+ When certain flows complete, the system injects advisory messages suggesting follow-up flows. This keeps the agent on the optimal path without requiring the user to manually chain flows.
260
+
261
+ ### Built-in transition matrix
262
+
263
+ | Source | Target | Condition | Advice |
264
+ |--------|--------|-----------|--------|
265
+ | `scout` | `build` | success | Context mapped. Consider running a [build] flow to implement changes, or [debug] if investigating an issue. |
266
+ | `scout` | `debug` | success | Context mapped. Consider running a [debug] flow if investigating an issue. |
267
+ | `debug` | `build` | success | The root cause has been identified. Consider running a [build] flow to implement the fix. |
268
+ | `debug` | `audit` | success | Root cause identified. Consider running an [audit] flow to verify the fix area for related issues. |
269
+ | `build` | `audit` | success | Consider running an [audit] flow to audit the changes for security, correctness, and code quality. |
270
+ | `build` | `debug` | failure | Build failed. Consider running a [debug] flow to investigate the root cause. |
271
+ | `audit` | `scout` | success | Audit complete. Consider running a [scout] flow to trace the audit findings across the codebase. |
272
+ | `audit` | `build` | failure | Audit found issues. Consider running a [build] flow to fix them. |
273
+ | `craft` | `build` | success | Plan ready. Consider running a [build] flow to implement the design. |
274
+ | `ideas` | `craft` | success | Ideas explored. Consider running a [craft] flow to design the approach, or [build] to implement directly. |
182
275
 
183
276
  Hooks are smart: if the agent already included the suggested flow in the same batch, the advisory is suppressed to avoid redundancy.
184
277
 
185
- ### Extending
278
+ ### Auto-transition
279
+
280
+ Enable `autoTransition: true` in `flowSettings` (or `--auto-transition`) to automatically queue follow-up flows without manual intervention. The system checks that the target flow exists, was not already requested, and would not create a cycle before queuing it.
281
+
282
+ ### Extending hooks
186
283
 
187
284
  Hooks are registered via `registerHook()` in `hooks.ts`. Each hook defines a trigger (flow type + success requirement) and an action that returns advisory text.
188
285
 
@@ -220,7 +317,7 @@ registerHook({
220
317
  }
221
318
  ```
222
319
 
223
- ### Override working directory for a flow
320
+ ### Override working directory or confirm project flows
224
321
 
225
322
  ```json
226
323
  {
@@ -230,13 +327,26 @@ registerHook({
230
327
  }
231
328
  ```
232
329
 
330
+ Suppress the confirmation prompt before running project-local flows:
331
+
332
+ ```json
333
+ {
334
+ "flow": [
335
+ { "type": "scout", "intent": "Map packages/ui", "aim": "Map UI package" }
336
+ ],
337
+ "confirmProjectFlows": false
338
+ }
339
+ ```
340
+
341
+
342
+
233
343
  ---
234
344
 
235
345
  ## Tools
236
346
 
237
347
  ### `flow` — delegate to flow states
238
348
 
239
- The core delegation tool. Accepts an array of flow tasks and runs them in parallel.
349
+ The core delegation tool. Accepts an array of flow tasks and runs them in parallel with bounded concurrency (default: 4, capped to CPU count).
240
350
 
241
351
  ### `batch` / `batch_read` — unified file operations
242
352
 
@@ -289,16 +399,27 @@ Use `flowModelConfigs` in your Pi settings to define tiered model strategies. Ea
289
399
 
290
400
  Settings are merged: project `.pi/settings.json` overrides global `~/.pi/agent/settings.json`.
291
401
 
402
+ ### Persistent flow mode switch
403
+
292
404
  Switch the global active strategy quickly with `--flow-mode`:
293
405
 
294
406
  ```bash
295
407
  pi --flow-mode balance
296
408
  pi --flow-mode quality
297
- pi --flow-mode mimo
298
409
  ```
299
410
 
300
411
  `--flow-mode` updates `flowModelConfig` in global `~/.pi/agent/settings.json` (or `$PI_CODING_AGENT_DIR/settings.json`) and applies the mode immediately for the current invocation. The mode must already exist in the merged `flowModelConfigs`; project `.pi/settings.json` can still override global settings on later no-flag runs.
301
412
 
413
+ On startup, the selected mode is printed in a compact notification:
414
+
415
+ ```
416
+ mode: balance | lite: gpt-5.4-mini · flash: gpt-5.5 · full: gpt-5.5
417
+ ```
418
+
419
+ Failover-only tiers are shown as `failover: model-a, model-b`. Verbose mode includes the flow names associated with each tier.
420
+
421
+ ### Flow settings
422
+
302
423
  You can also set flow runtime defaults under `flowSettings`:
303
424
 
304
425
  ```json
@@ -313,7 +434,15 @@ You can also set flow runtime defaults under `flowSettings`:
313
434
  }
314
435
  ```
315
436
 
316
- `flowSettings.sessionMode` accepts `fast`, `default`, or `long`. Session mode precedence is:
437
+ | Setting | Default | Description |
438
+ |---------|---------|-------------|
439
+ | `sessionMode` | `default` | Default child-flow session mode: `fast`, `default`, `long`, or `extreme_long` |
440
+ | `maxConcurrency` | `4` | Maximum parallel flows (capped to CPU count) |
441
+ | `toolOptimize` | `true` | Use unified `batch`/`batch_read` instead of separate read/write/edit |
442
+ | `structuredOutput` | `true` | Inject JSON structured-output instructions into flow prompts |
443
+ | `autoTransition` | `false` | Automatically queue follow-up flows based on the transition matrix |
444
+
445
+ Session mode precedence is:
317
446
 
318
447
  ```txt
319
448
  per-flow sessionMode > --flow-session-mode > PI_FLOW_SESSION_MODE > flowSettings.sessionMode > default
@@ -331,9 +460,13 @@ per-flow sessionMode > --flow-session-mode > PI_FLOW_SESSION_MODE > flowSettings
331
460
  | `--flow-lite-model [model]` | Override the lite-tier model | — |
332
461
  | `--flow-flash-model [model]` | Override the flash-tier model | — |
333
462
  | `--flow-full-model [model]` | Override the full-tier model | — |
334
- | `--flow-session-mode [mode]` | Default child-flow session mode: `fast`, `default`, or `long` | `default` |
335
- | `--tool-optimize` | Use unified `batch`/`batch_read` instead of separate read/write/edit | `true` |
336
- | `--no-tool-optimize` | Disable tool optimization; use legacy read/write/edit tools | |
463
+ | `--flow-session-mode [mode]` | Default child-flow session mode | `default` |
464
+ | `--flow-max-concurrency [n]` | Maximum parallel flows | `4` |
465
+ | `--tool-optimize` | Use unified `batch`/`batch_read` | `true` |
466
+ | `--no-tool-optimize` | Disable tool optimization; use legacy read/write/edit | — |
467
+ | `--auto-transition` | Automatically queue follow-up flows | `false` |
468
+ | `--structured-output` | Inject structured JSON output instructions | `true` |
469
+ | `--no-structured-output` | Disable structured output injection | — |
337
470
 
338
471
  ### Environment variables
339
472
 
@@ -344,7 +477,9 @@ per-flow sessionMode > --flow-session-mode > PI_FLOW_SESSION_MODE > flowSettings
344
477
  | `PI_FLOW_STACK` | JSON array of ancestor flow names |
345
478
  | `PI_FLOW_PREVENT_CYCLES` | `"1"` or `"0"` |
346
479
  | `PI_FLOW_TOOL_OPTIMIZE` | `"1"` or `"0"` (overrides default tool optimization) |
347
- | `PI_FLOW_SESSION_MODE` | Default child-flow session mode: `fast`, `default`, or `long` |
480
+ | `PI_FLOW_SESSION_MODE` | Default child-flow session mode: `fast`, `default`, `long`, or `extreme_long` |
481
+ | `PI_FLOW_MAX_CONCURRENCY` | Maximum parallel flows |
482
+ | `PI_FLOW_SPAWN_COMMAND` | Override the spawn command for exotic runtime environments (e.g. bundled with pkg/nexe) |
348
483
 
349
484
  ---
350
485
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-flow",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "description": "Flow-state delegation extension for Pi coding agent.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
package/src/flow.ts CHANGED
@@ -25,10 +25,11 @@ import { DEFAULT_AGENT_SESSION_MODE, getAgentSessionTimeoutMs, type AgentSession
25
25
 
26
26
  const isWindows = process.platform === "win32";
27
27
  const SIGKILL_TIMEOUT_MS = 5000;
28
+ const FINISH_KILL_GRACE_MS = 5_000; // wait 5s after finish() before force-killing the child process
28
29
  const AGENT_END_GRACE_MS = 2000;
29
30
  const FLOW_TIME_BUDGET_WARNING_MS = 2 * 60 * 1000; // warn 2 min before kill
30
- const FLOW_FINAL_URGE_MS = 30 * 1000; // final urge 30 s before kill
31
- const REPORTING_GRACE_MS = 10_000; // grace period after timeout for agent to report findings
31
+ const FLOW_FINAL_URGE_MS = 135 * 1000; // final urge 135 s (2m15s) before kill (increased from 30s for wider summary window)
32
+ const REPORTING_GRACE_MS = 90_000; // grace period after timeout for agent to report findings (increased from 10s to 90s)
32
33
  const FLOW_TOOL_SUMMARY_GRACE_MS = FLOW_FINAL_URGE_MS; // bash/tool abort lead time so the agent can summarize
33
34
  const FLOW_DEPTH_ENV = "PI_FLOW_DEPTH";
34
35
  const FLOW_MAX_DEPTH_ENV = "PI_FLOW_MAX_DEPTH";
@@ -38,6 +39,54 @@ const FLOW_DEADLINE_ENV = "PI_FLOW_DEADLINE_MS";
38
39
  const FLOW_TOOL_SUMMARY_GRACE_ENV = "PI_FLOW_TOOL_SUMMARY_GRACE_MS";
39
40
  export const FLOW_TOOL_OPTIMIZE_ENV = "PI_FLOW_TOOL_OPTIMIZE";
40
41
  const PI_OFFLINE_ENV = "PI_OFFLINE";
42
+ const FLOW_REMINDER_FILE_ENV = "PI_FLOW_REMINDER_FILE";
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Global child process group tracking for signal propagation
46
+ // ---------------------------------------------------------------------------
47
+
48
+ /** Track child process groups so we can kill them on parent exit / signal. */
49
+ const runningChildGroups = new Map<number, { groupPid: number; name: string }>();
50
+
51
+ /** Register a child process group for cleanup on shutdown. */
52
+ export function registerChildGroup(pid: number, name: string): void {
53
+ if (pid > 0 && !isWindows) {
54
+ runningChildGroups.set(pid, { groupPid: pid, name });
55
+ }
56
+ }
57
+
58
+ /** Unregister a completed/stopped child process group. */
59
+ export function unregisterChildGroup(pid: number): void {
60
+ runningChildGroups.delete(pid);
61
+ }
62
+
63
+ /**
64
+ * Terminate all registered child process groups via SIGTERM (then SIGKILL after timeout).
65
+ * Called on parent exit, SIGINT, SIGTERM, or pi-agent-flow:shutdown.
66
+ */
67
+ export function terminateAllChildGroups(): void {
68
+ if (runningChildGroups.size === 0) return;
69
+ const pids = Array.from(runningChildGroups.keys());
70
+ for (const pid of pids) {
71
+ try {
72
+ process.kill(-pid, "SIGTERM");
73
+ } catch {
74
+ try { process.kill(pid, "SIGTERM"); } catch { /* gone */ }
75
+ }
76
+ }
77
+ // Hard kill after timeout
78
+ const sigkillTimer = setTimeout(() => {
79
+ for (const pid of pids) {
80
+ try {
81
+ process.kill(-pid, "SIGKILL");
82
+ } catch {
83
+ try { process.kill(pid, "SIGKILL"); } catch { /* gone */ }
84
+ }
85
+ }
86
+ runningChildGroups.clear();
87
+ }, 5000);
88
+ sigkillTimer.unref();
89
+ }
41
90
 
42
91
  type FlowUpdateCallback = (partial: AgentToolResult<FlowDetails>) => void;
43
92
 
@@ -109,6 +158,24 @@ function cleanupFlowTempDir(dir: string | null): void {
109
158
  }
110
159
  }
111
160
 
161
+ // ---------------------------------------------------------------------------
162
+ // Reminder file helpers
163
+ // ---------------------------------------------------------------------------
164
+
165
+ /**
166
+ * Write a reminder message to the reminder file so the child agent can see it
167
+ * via the timed-bash wrapper before its next tool call.
168
+ * Creates the file if it doesn't exist; appends the message.
169
+ */
170
+ function writeReminderFile(reminderFilePath: string | null, message: string): void {
171
+ if (!reminderFilePath) return;
172
+ try {
173
+ fs.writeFileSync(reminderFilePath, message + "\n", { encoding: "utf-8", flag: "a" });
174
+ } catch {
175
+ /* best-effort */
176
+ }
177
+ }
178
+
112
179
  // ---------------------------------------------------------------------------
113
180
  // Build pi CLI arguments (fork-only)
114
181
  // ---------------------------------------------------------------------------
@@ -428,6 +495,15 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
428
495
  forkSessionTmpPath = forkTmp.filePath;
429
496
  }
430
497
 
498
+ // Create a temp dir for the reminder file so the child agent can read timeout warnings
499
+ // via the timed-bash wrapper before its next tool call.
500
+ let reminderTmpDir: string | null = null;
501
+ let reminderFilePath: string | null = null;
502
+ if (effectiveTimeout > 0) {
503
+ reminderTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-flow-reminder-"));
504
+ reminderFilePath = path.join(reminderTmpDir, "reminder.txt");
505
+ }
506
+
431
507
  try {
432
508
  const piArgs = buildFlowArgs(
433
509
  flow,
@@ -472,13 +548,20 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
472
548
  ...(effectiveTimeout > 0 ? {
473
549
  [FLOW_DEADLINE_ENV]: String(deadlineAtMs),
474
550
  [FLOW_TOOL_SUMMARY_GRACE_ENV]: String(toolSummaryGraceMs),
551
+ [FLOW_REMINDER_FILE_ENV]: reminderFilePath ?? "",
475
552
  } : {
476
553
  [FLOW_DEADLINE_ENV]: "",
477
554
  [FLOW_TOOL_SUMMARY_GRACE_ENV]: "0",
555
+ [FLOW_REMINDER_FILE_ENV]: "",
478
556
  }),
479
557
  },
480
558
  });
481
559
 
560
+ // Register the child process group for global cleanup on signal/exit
561
+ if (proc.pid !== undefined && !isWindows) {
562
+ registerChildGroup(proc.pid, normalizedFlowName);
563
+ }
564
+
482
565
  let stdinEnded = false;
483
566
  const endStdin = () => {
484
567
  if (stdinEnded) return;
@@ -497,6 +580,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
497
580
  let timeoutFired = false;
498
581
  let semanticCompletionTimer: NodeJS.Timeout | undefined;
499
582
  let countdownTimer: NodeJS.Timeout | undefined;
583
+ let finishKillTimer: NodeJS.Timeout | undefined;
500
584
 
501
585
  const clearSemanticCompletionTimer = () => {
502
586
  if (semanticCompletionTimer) {
@@ -534,6 +618,13 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
534
618
  sigkillTimer.unref();
535
619
  };
536
620
 
621
+ const clearFinishKillTimer = () => {
622
+ if (finishKillTimer) {
623
+ clearTimeout(finishKillTimer);
624
+ finishKillTimer = undefined;
625
+ }
626
+ };
627
+
537
628
  const finish = (code: number) => {
538
629
  if (settled) return;
539
630
  settled = true;
@@ -543,6 +634,15 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
543
634
  if (signal && abortHandler) {
544
635
  signal.removeEventListener("abort", abortHandler);
545
636
  }
637
+ // Soft-kill: give the child a short grace to exit naturally after stdin close.
638
+ // If it hasn't closed by then, force-kill to prevent orphaned processes.
639
+ clearFinishKillTimer();
640
+ finishKillTimer = setTimeout(() => {
641
+ if (!didClose) {
642
+ terminateChild();
643
+ }
644
+ }, FINISH_KILL_GRACE_MS);
645
+ finishKillTimer.unref();
546
646
  resolve(code);
547
647
  };
548
648
 
@@ -596,6 +696,10 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
596
696
 
597
697
  proc.on("close", (code) => {
598
698
  didClose = true;
699
+ clearFinishKillTimer();
700
+ if (proc.pid !== undefined) {
701
+ unregisterChildGroup(proc.pid);
702
+ }
599
703
  if (buffer.trim()) flushBufferedLines(buffer);
600
704
  finish(code ?? 0);
601
705
  });
@@ -630,13 +734,15 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
630
734
  const remainingSec = Math.round(FLOW_TIME_BUDGET_WARNING_MS / 1000);
631
735
  const warnMsg = `\n[Flow warning] ${remainingSec}s remaining before hard timeout. The agent should wrap up now.`;
632
736
  result.stderr += warnMsg;
737
+ // Write to reminder file so the child agent sees it on its next bash call.
738
+ writeReminderFile(reminderFilePath, `[Flow warning] ${remainingSec}s remaining before hard timeout. Wrap up your work and output structured findings.`);
633
739
  // Force an update so the parent UI shows the warning immediately.
634
740
  emitUpdate();
635
741
  }, warningMs);
636
742
  warnTimer.unref();
637
743
  }
638
744
 
639
- // Final urge timer: stronger warning 30 s before hard timeout
745
+ // Final urge timer: stronger warning 45 s before hard timeout
640
746
  const urgeMs = effectiveTimeout - FLOW_FINAL_URGE_MS;
641
747
  if (urgeMs > 0) {
642
748
  const urgeTimer = setTimeout(() => {
@@ -644,6 +750,8 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
644
750
  const remainingSec = Math.round(FLOW_FINAL_URGE_MS / 1000);
645
751
  const urgeMsg = `\n[Flow warning] ${remainingSec}s remaining before hard timeout. Stop all work and output your structured findings.`;
646
752
  result.stderr += urgeMsg;
753
+ // Write to reminder file so the child agent sees it on its next bash call.
754
+ writeReminderFile(reminderFilePath, `[Flow urge] ${remainingSec}s remaining before hard timeout. STOP all tool use and output your structured findings NOW.`);
647
755
  emitUpdate();
648
756
  }, urgeMs);
649
757
  urgeTimer.unref();
@@ -692,6 +800,7 @@ export async function runFlow(opts: RunFlowOptions): Promise<SingleResult> {
692
800
  return normalized;
693
801
  } finally {
694
802
  cleanupFlowTempDir(forkSessionTmpDir);
803
+ cleanupFlowTempDir(reminderTmpDir);
695
804
  }
696
805
  }
697
806
 
package/src/index.ts CHANGED
@@ -24,7 +24,7 @@ import { getInheritedCliArgs } from "./cli-args.js";
24
24
  import { renderFlowCall, renderFlowResult } from "./render.js";
25
25
  import { getFlowSummaryText } from "./runner-events.js";
26
26
  import { runHooks, runHooksDetailed, getRegisteredHooks, registerHook, unregisterHook } from "./hooks.js";
27
- import { mapFlowConcurrent, runFlow } from "./flow.js";
27
+ import { mapFlowConcurrent, runFlow, terminateAllChildGroups } from "./flow.js";
28
28
  import { executeFlows } from "./executor.js";
29
29
  import {
30
30
  type SingleResult,
@@ -100,8 +100,9 @@ const FlowItem = Type.Object({
100
100
  Type.Literal("fast"),
101
101
  Type.Literal("default"),
102
102
  Type.Literal("long"),
103
+ Type.Literal("extreme_long"),
103
104
  ], {
104
- description: "Agent session budget for this flow: fast=300s, default=600s, long=900s. Use long only for large builds, broad refactors, full test runs, or complex debugging.",
105
+ description: "Agent session budget for this flow: fast=300s, default=600s, long=900s, extreme_long=1200s. Use long or extreme_long only when the work genuinely needs the larger budget.",
105
106
  }),
106
107
  ),
107
108
  });
@@ -110,7 +111,7 @@ const FlowParams = Type.Object({
110
111
  flow: Type.Array(FlowItem, {
111
112
  description:
112
113
  "Array of flow tasks to execute. Each runs in its own forked process. " +
113
- "Optional sessionMode selects the child-agent budget: fast=300s, default=600s, long=900s. " +
114
+ "Optional sessionMode selects the child-agent budget: fast=300s, default=600s, long=900s, extreme_long=1200s. " +
114
115
  'Example: { flow: [{ type: "scout", "intent": "Find all authentication-related code and trace JWT validation", "aim": "Find auth code and trace JWT", "sessionMode": "fast" }, { type: "build", "intent": "Fix the bug in user registration", "aim": "Fix registration bug", "sessionMode": "long" }] }',
115
116
  minItems: 1,
116
117
  }),
@@ -933,6 +934,14 @@ flow [type] accomplished
933
934
 
934
935
  ### Guards
935
936
  - Depth: ${currentDepth}/${maxDepth} | Cycles: ${preventCycles ? "blocked" : "off"} | Stack: ${ancestorFlowStack.length > 0 ? ancestorFlowStack.join(" -> ") : "(root)"}
937
+
938
+ ### Shared Context
939
+ Child flows fork your session automatically:
940
+
941
+ - They receive a sanitized snapshot of your conversation — files read, commands run, prior flow results.
942
+ - Prior flow tool results are **compressed** into compact summaries (files touched, commands used, status).
943
+ - Write 'intent' as a **forward-looking mission** — reference what the child already sees, don't re-describe it.
944
+ - Set inheritContext: false in a custom flow's front-matter to start with a **clean slate** (no inherited context).
936
945
  `,
937
946
  };
938
947
  });
@@ -997,7 +1006,7 @@ flow [type] accomplished
997
1006
  "",
998
1007
  "Flow states are isolated \u03c0 processes with forked session snapshots. They run in parallel.",
999
1008
  'Invoke: { "flow": [{ "type": "scout", "intent": "...", "aim": "...", "sessionMode": "default" }, ...] }',
1000
- "Session modes: fast=300s, default=600s, long=900s. Use long only when the work genuinely needs the larger budget.",
1009
+ "Session modes: fast=300s, default=600s, long=900s, extreme_long=1200s. Use long or extreme_long only when the work genuinely needs the larger budget.",
1001
1010
  "States: scout, debug, build, craft, audit, ideas.",
1002
1011
  "Custom states configs in (create if not exists): .md files in .pi/agents/ or ~/.pi/agent/agents/.",
1003
1012
  ].join("\n"),
@@ -1095,14 +1104,20 @@ flow [type] accomplished
1095
1104
  }
1096
1105
 
1097
1106
  // 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().
1107
+ // We use prependListener on SIGINT/SIGTERM to propagate to child processes
1108
+ // before the host's own signal handler runs. This avoids orphaned sub-agents.
1109
+ // The host handler still runs afterward and handles terminal cleanup.
1103
1110
  if (!(globalThis as any).__pi_agent_flow_shutdown_registered) {
1104
1111
  (globalThis as any).__pi_agent_flow_shutdown_registered = true;
1112
+
1113
+ // Propagate signals to child process groups so sub-agents don't become orphans.
1114
+ // We use prependListener so our handler runs first, before the host's cleanup.
1115
+ process.prependListener("SIGINT", terminateAllChildGroups);
1116
+ process.prependListener("SIGTERM", terminateAllChildGroups);
1117
+
1118
+ // Also handle the 'exit' event, which fires when the host calls process.exit().
1105
1119
  const emitShutdown = () => {
1120
+ terminateAllChildGroups();
1106
1121
  if (typeof pi.emit === "function") {
1107
1122
  pi.emit("pi-agent-flow:shutdown", { reason: "process-exit" });
1108
1123
  }
@@ -1,15 +1,16 @@
1
- export const AGENT_SESSION_MODES = ["fast", "default", "long"] as const;
1
+ export const AGENT_SESSION_MODES = ["fast", "default", "long", "extreme_long"] as const;
2
2
 
3
3
  export type AgentSessionMode = typeof AGENT_SESSION_MODES[number];
4
4
 
5
5
  export const DEFAULT_AGENT_SESSION_MODE: AgentSessionMode = "default";
6
- export const MAX_AGENT_SESSION_TIMEOUT_MS = 900_000;
6
+ export const MAX_AGENT_SESSION_TIMEOUT_MS = 1_200_000;
7
7
  export const PI_FLOW_SESSION_MODE_ENV = "PI_FLOW_SESSION_MODE";
8
8
 
9
9
  export const AGENT_SESSION_TIMEOUTS_MS: Record<AgentSessionMode, number> = {
10
10
  fast: 300_000,
11
11
  default: 600_000,
12
- long: MAX_AGENT_SESSION_TIMEOUT_MS,
12
+ long: 900_000,
13
+ extreme_long: MAX_AGENT_SESSION_TIMEOUT_MS,
13
14
  };
14
15
 
15
16
  export function parseAgentSessionMode(value: unknown): AgentSessionMode | undefined {
@@ -22,6 +22,7 @@ export const SLIDING_PROMPT =
22
22
  `You are operating with pi-agent-flow routing.\n` +
23
23
  `If the answer is already in context, answer directly; otherwise delegate to the appropriate flow.\n` +
24
24
  `For git, bash, CLI, or terminal tasks, delegate to [build].\n` +
25
+ `Child flows inherit your session context — write intents that focus on new work rather than restating what the child already sees.\n` +
25
26
  `${SLIDING_PROMPT_CLOSE_TAG}`;
26
27
 
27
28
  const SLIDING_PROMPT_RE = new RegExp(
package/src/timed-bash.ts CHANGED
@@ -13,6 +13,7 @@
13
13
  * still active.
14
14
  */
15
15
 
16
+ import * as fs from "node:fs";
16
17
  import { createBashToolDefinition } from "@mariozechner/pi-coding-agent";
17
18
 
18
19
  export type TimingTier =
@@ -30,6 +31,7 @@ export interface TimingReport {
30
31
 
31
32
  const FLOW_DEADLINE_ENV = "PI_FLOW_DEADLINE_MS";
32
33
  const FLOW_TOOL_SUMMARY_GRACE_ENV = "PI_FLOW_TOOL_SUMMARY_GRACE_MS";
34
+ const FLOW_REMINDER_FILE_ENV = "PI_FLOW_REMINDER_FILE";
33
35
  const DEFAULT_FLOW_TOOL_SUMMARY_GRACE_MS = 30_000;
34
36
 
35
37
  /** Classify duration into user-defined tiers with actionable feedback. */
@@ -87,6 +89,30 @@ function getFlowToolSummaryGraceMs(): number {
87
89
  return parseNonNegativeSafeInteger(process.env[FLOW_TOOL_SUMMARY_GRACE_ENV]) ?? DEFAULT_FLOW_TOOL_SUMMARY_GRACE_MS;
88
90
  }
89
91
 
92
+ function getFlowReminderFilePath(): string | null {
93
+ const raw = process.env[FLOW_REMINDER_FILE_ENV];
94
+ return typeof raw === "string" && raw.trim() ? raw.trim() : null;
95
+ }
96
+
97
+ /**
98
+ * Read any pending reminder from the reminder file set by the parent runner.
99
+ * Returns the reminder text (without trailing newline), or null if no reminder exists.
100
+ * Clears the file after reading so the agent only sees each reminder once.
101
+ */
102
+ export function readAndClearReminderFile(): string | null {
103
+ const filePath = getFlowReminderFilePath();
104
+ if (!filePath) return null;
105
+ try {
106
+ if (!fs.existsSync(filePath)) return null;
107
+ const content = fs.readFileSync(filePath, { encoding: "utf-8" }).trim();
108
+ // Clear the file after reading so each reminder is only injected once.
109
+ try { fs.writeFileSync(filePath, "", { encoding: "utf-8" }); } catch { /* best-effort */ }
110
+ return content || null;
111
+ } catch {
112
+ return null;
113
+ }
114
+ }
115
+
90
116
  function formatDeadlineAppendix(): string {
91
117
  return "\n\n[Flow timeout] Bash command was interrupted to preserve time for the final flow summary. Stop running tools and return structured findings now.";
92
118
  }
@@ -189,6 +215,12 @@ export function createTimedBashToolDefinition(
189
215
  ) {
190
216
  const start = Date.now();
191
217
  const deadlineSignal = createDeadlineSignal(signal);
218
+
219
+ // Check for pending reminder from parent runner before executing bash.
220
+ // This allows the parent to inject timeout warnings into the agent's context
221
+ // via the tool result, since stdin is closed after spawn.
222
+ const reminderPreamble = readAndClearReminderFile();
223
+
192
224
  try {
193
225
  const result = await original.execute(
194
226
  toolCallId,
@@ -201,6 +233,10 @@ export function createTimedBashToolDefinition(
201
233
  const report = classifyDuration(duration);
202
234
  const appendix = formatTimingAppendix(report);
203
235
 
236
+ // Inject reminder preamble before timing info so it's the first thing the agent sees.
237
+ if (reminderPreamble) {
238
+ appendTextToToolResult(result, `\n\n[REMINDER FROM PARENT] ${reminderPreamble}`);
239
+ }
204
240
  appendTextToToolResult(result, appendix);
205
241
  if (deadlineSignal.wasDeadlineAbort()) {
206
242
  appendTextToToolResult(result, formatDeadlineAppendix());