@tranhoangnguyen0310/pi-flow-external 4.0.0-external.0 → 5.0.0-external.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/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  All notable changes to pi-flow external are documented here.
4
4
 
5
+ ## [5.0.0-external.0] - 2026-10-15
6
+
7
+ ### Breaking
8
+ - Renamed the orchestration tool **`workflow` → `external_workflow`**, following PR #86's direct-tool rename precedent. There is no alias or automatic translation. Update `tools.workflow(...)` → `tools.external_workflow(...)`, `describeTool("workflow")` → `describeTool("external_workflow")`, exact-name hooks, tool allow-lists, and receipt checks. A stale call may reach another extension's `workflow` tool rather than fail.
9
+ - New workflow receipts report `tool: "external_workflow"`. Envelope `contractVersion: 1`, `action: "run"`, run `kind: "workflow"`, `wf_...` IDs, saved scripts, and script helpers such as `agent()` remain unchanged. Settings stay at version 5; no conversion or saved-run rewrite is required. Help topic `workflow`, extension option `{ workflow: false }`, and E2E `--workflow` are unchanged.
10
+ - Update Flow's `pi-cc-extensions` `excludeRenderers` entry from `"workflow"` to `"external_workflow"` (alongside `"external_delegate"` and `"external_runs"`), preserve unrelated entries, and restart Pi. See the [workflow tool migration guide](docs/migration-workflow-to-external-workflow.md).
11
+
12
+ ### Changed
13
+ - Parent guidance requires a plain-language pre-launch briefing covering purpose, assignments, parallel/sequential order, allowed changes, and deliverable, with the same roadmap in `meta.phases` and actual transitions marked by `phase()`. This follows the user's existing approval policy; it is not a runtime-verified chat-message gate.
14
+ - Workflow cards now display existing planned-phase details, including future stages. Help and examples explain the roadmap, actual progress, and explicit launch inspection for retrieving recorded plans during background runs and after completion. Phases remain optional for existing saved scripts; declared intent is not proof of execution.
15
+
16
+ - Foreground/background guidance now puts the selection rule and ownership up front: foreground also supports parallel work; background is for independent work or an explicit request, has no automatic completion delivery, and requires collection in the owning session. The runtime guide includes outcome-aware, paged collection instead of teaching immediate background-then-wait. Permission descriptions distinguish native tool restrictions from OS sandboxing.
17
+
18
+ ### Fixed
19
+ - Phase explanations use the existing secret redaction before display, and supplied phase details must be strings rather than silently rendering malformed values.
20
+
5
21
  ## [4.0.0-external.0] - 2026-10-13
6
22
 
7
23
  ### Breaking
package/README.md CHANGED
@@ -24,7 +24,7 @@ You ask Pi to delegate work in plain chat. Every delegation combines three eleme
24
24
 
25
25
  > ⚠️ **Security & Defaults:**
26
26
  > Roles describe **intent**; `permission` defines **authority**. Saying "read-only" in prompt text does not restrict an agent.
27
- > Out of the box, the default harness is `agy` and the default permission is `danger` (full host access). Antigravity rejects `readonly` and `edit`. For enforced sandboxing, ask for a harness that supports it (such as `claude` or `codex`) and explicit read-only access.
27
+ > Out of the box, the default harness is `agy` and the default permission is `danger` (full host access). Antigravity rejects `readonly` and `edit`. For restricted access, request a supported permission tier explicitly. Claude uses native tool restrictions; Codex uses an OS sandbox. See the permission matrix below.
28
28
 
29
29
  ---
30
30
 
@@ -71,7 +71,7 @@ Pi recognizes the request and delegates:
71
71
 
72
72
  - **Role:** `explorer`
73
73
  - **Harness:** `claude`
74
- - **Permission:** `readonly` (enforced sandbox: Claude cannot edit files or run shell commands)
74
+ - **Permission:** `readonly` (native tool restrictions: Claude cannot edit files or run shell commands; not an OS sandbox)
75
75
 
76
76
  *(Programmatic / Codemode equivalent):*
77
77
 
@@ -140,34 +140,27 @@ await tools.external_delegate({
140
140
  });
141
141
  ```
142
142
 
143
- ### 2. Background Runs & Supervision
143
+ ### 2. Foreground or Background?
144
144
 
145
- For long audits or explorations you don't want to wait for:
145
+ **Use foreground when you need the result next.** Use background when useful independent work can proceed, or you explicitly want to collect the result later. A long task alone does not need background mode.
146
146
 
147
- > **You:** "Have Claude run a security audit of our API routes in the background while we continue working."
147
+ Both `external_delegate` and `external_workflow` support these modes:
148
148
 
149
- Pi launches the run in the background, gives you a run ID, and keeps your chat session free.
149
+ | | Foreground (default) | Background (`background: true`) |
150
+ |---|---|---|
151
+ | Original call returns | When the work finishes or fails | After registration, with a run ID; work may already have finished |
152
+ | Progress | Live execution card | A static launch receipt; inspect with `/external runs` or ask Pi for status |
153
+ | Next step | Pi checks the returned outcome | Pi keeps the run ID, does independent work, then collects and checks the outcome |
150
154
 
151
- - Check live progress or inspect output anytime with `/external runs`.
152
- - Or ask Pi: *"How is that background security audit going?"*
155
+ > **You:** "Have Claude audit the API in the background while we review the release checklist. Then bring me the findings."
153
156
 
154
- *(Codemode):*
157
+ - **No automatic completion message:** background work does not notify or wake the parent. Pi owns collection unless you explicitly ask it to defer; you should not have to manage every run yourself.
158
+ - **Keep Pi open:** background work belongs to its originating session, not a persistent service. Orderly shutdown requests cancellation with bounded cleanup; restart does not adopt unfinished work.
159
+ - **Stopping is different from waiting:** interrupting the original foreground call cancels it. Interrupting `external_runs wait` only stops waiting; use `/external runs` or `external_runs cancel` to stop background work.
155
160
 
156
- ```ts
157
- const receipt = await tools.external_delegate({
158
- description: "Audit security-sensitive endpoints",
159
- role: "reviewer",
160
- harness: "claude",
161
- permission: "readonly",
162
- prompt: "Audit security-sensitive endpoints.",
163
- background: true,
164
- });
161
+ Both modes allow parallel workflow children under the same concurrency limit. Background does not change permissions, enforce new budget limits, or make work inherently faster or cheaper. Prompt instructions such as “do not edit” express intent; the supported `permission` tier defines enforced restrictions.
165
162
 
166
- if (receipt.ok) {
167
- const runId = receipt.data.run.runId;
168
- await tools.external_runs({ action: "wait", runIds: [runId], mode: "all" });
169
- }
170
- ```
163
+ See [launch → independent work → collect](docs/supervision-and-runtime.md#background-example-launch--independent-work--collect) for the operational example, result handling, and cancellation details.
171
164
 
172
165
  ### 3. Multi-Agent Workflows (Parallel Delegation)
173
166
 
@@ -175,13 +168,17 @@ For tasks that benefit from multiple perspectives at once:
175
168
 
176
169
  > **You:** "Run a parallel review: have Claude check for architectural issues while Codex audits test coverage."
177
170
 
178
- Pi runs a trusted JavaScript workflow that runs both subagents simultaneously under a shared concurrency limiter:
171
+ Pi first explains who will do what, what runs in parallel, what may change, and the expected result. It then uses `external_workflow` to run both subagents under a shared concurrency limiter. The live card shows the roadmap and actual child activity; `/external runs` provides inspection and cancellation.
179
172
 
180
173
  *(Workflow script):*
181
174
 
182
175
  ```js
183
- export const meta = { apiVersion: 1, name: "code-review", description: "Parallel review" };
176
+ export const meta = {
177
+ apiVersion: 1, name: "code-review", description: "Parallel review",
178
+ phases: [{ title: "Review", detail: "Claude checks architecture while Codex checks test coverage; both use readonly access." }],
179
+ };
184
180
 
181
+ phase("Review");
185
182
  const [arch, coverage] = await parallel([
186
183
  () => agent("Review architectural integrity read-only", { role: "reviewer", harness: "claude", permission: "readonly" }),
187
184
  () => agent("Audit test coverage read-only", { role: "qa", harness: "codex", permission: "readonly" }),
@@ -227,6 +224,7 @@ The direct delegation tool `Agent` is now `external_delegate`, with no alias. Up
227
224
  - **[Harness Reference](docs/harness-reference.md):** Deep CLI flags, OpenCode 2 standalone server details, Muse session resume, Grok sandbox notes, and Pi in-process presets.
228
225
  - **[Configuration Reference](docs/configuration-reference.md):** Settings v5 JSON schema, sparse inheritance, role overrides, and full command reference.
229
226
  - **[Supervision & Runtime](docs/supervision-and-runtime.md):** `external_runs` actions, cursor pagination, batch inspect, and byte budgets.
230
- - **[Workflows](docs/workflows.md):** Scripting API, error handling (`ChildRunError`), and replay cache.
227
+ - **[Workflows](docs/workflows.md):** `external_workflow`, plain-language plans, live phases, error handling (`ChildRunError`), and replay cache.
228
+ - **[Workflow Tool Rename](docs/migration-workflow-to-external-workflow.md):** `workflow` → `external_workflow`, with no alias.
231
229
  - **[Public Contract](docs/public-contract.md):** Contract envelope definitions for Pi codemode consumers.
232
230
  - **[Troubleshooting](docs/troubleshooting.md):** Common errors, authentication checks, and `pi-cc-extensions` renderer compatibility.
package/docs/README.md CHANGED
@@ -18,6 +18,7 @@ Core reference guides for configuring and using the extension with Pi:
18
18
  | **[Troubleshooting](troubleshooting.md)** | Diagnostic recipes, authentication checks, and `pi-cc-extensions` renderer compatibility. |
19
19
  | **[Migration Guide (v4 → v5)](migration-v4-to-v5.md)** | Upgrading from older configurations, backups, command mappings, and safe file purging. |
20
20
  | **[Tool Rename Migration (`Agent` → `external_delegate`)](migration-agent-to-external-delegate.md)** | Updating codemode calls, hooks, receipt checks, and renderer exclusions after the direct tool rename. |
21
+ | **[Workflow Tool Rename (`workflow` → `external_workflow`)](migration-workflow-to-external-workflow.md)** | Updating workflow callers, hooks, receipt checks, and renderer exclusions; script helpers are unchanged. |
21
22
 
22
23
  ---
23
24
 
@@ -26,7 +27,7 @@ Specifications for programmatic subagent delegation and tool outputs:
26
27
 
27
28
  | Document | Status | Purpose |
28
29
  |---|---|---|
29
- | **[Public Contract v1](public-contract.md)** | `Normative Contract` | Schema definitions, envelope shapes (`{ ok, data, error }`), and delivery invariants for `external_delegate`, `workflow`, and `external_runs`. |
30
+ | **[Public Contract v1](public-contract.md)** | `Normative Contract` | Schema definitions, envelope shapes (`{ ok, data, error }`), and delivery invariants for `external_delegate`, `external_workflow`, and `external_runs`. |
30
31
 
31
32
  ---
32
33
 
@@ -55,6 +56,6 @@ Non-normative proposals, design investigations, and dated implementation records
55
56
 
56
57
  ## Document Status Legend
57
58
 
58
- - **`Normative Guide` / `Normative Contract`:** Authoritative documentation reflecting current production behavior in version 3.x.
59
+ - **`Normative Guide` / `Normative Contract`:** Authoritative documentation reflecting the current implementation.
59
60
  - **`Design Direction`:** Approved future UX/architectural targets; does not claim current implementation satisfies it.
60
61
  - **`Historical Record` / `Archive`:** Preserved investigation notes and dated planning documents. Superseded where newer contracts exist.
@@ -44,26 +44,12 @@ return receipt.data.run;
44
44
  - **Envelope:** `contractVersion` is still `1`. `action` is still `"delegate"`, and runs still report `kind: "agent"`. `ok`, `data`, `error`, and `warnings` keep their meaning. See [Public Contract v1](public-contract.md).
45
45
  - **Settings and saved runs:** settings stay at version 5. No settings conversion or saved-run rewrite is needed.
46
46
  - **Workflows:** the workflow `agent()` function keeps its name. It exists only inside workflow scripts and never appears in a tool list.
47
- - **Other tools:** `workflow`, `external_runs`, and `external_help` keep their names.
47
+ - **Other tools at the time of this rename:** `workflow`, `external_runs`, and `external_help` kept their names. In the subsequent release, `workflow` became [`external_workflow`](migration-workflow-to-external-workflow.md); use that name for current callers.
48
48
 
49
49
  ---
50
50
 
51
51
  ## Background Runs Still Need Collecting
52
52
 
53
- A background run does not notify or resume the session when it finishes. Ending the turn does not bring the result back. Collect it with `external_runs`:
53
+ Foreground is the default, including parallel workflows. Use background for useful independent work or an explicit user request—not routinely followed by an immediate wait. Background completion does not notify or wake the parent. Retain the ID and collect/check the outcome unless the user explicitly defers; keep the owning session open.
54
54
 
55
- ```ts
56
- const receipt = await tools.external_delegate({
57
- description: "Audit endpoints",
58
- role: "reviewer",
59
- prompt: "Audit the endpoints read-only.",
60
- background: true,
61
- });
62
- if (!receipt.ok) return receipt.error;
63
- const runId = receipt.data.run.runId;
64
-
65
- await tools.external_runs({ action: "wait", runIds: [runId], mode: "all" });
66
- const final = await tools.external_runs({ action: "inspect", runIds: [runId], view: "final" });
67
- ```
68
-
69
- `final` holds only a verified successful answer. For a failed run, read the outcome from `wait` and the details with `view: "diagnostics"`.
55
+ Use inline successful output when available; otherwise inspect `final` and follow pagination. Failed runs need outcomes and `diagnostics`, not a successful final answer. See the [launch → independent work → collect example](supervision-and-runtime.md#background-example-launch--independent-work--collect) for the complete sequence and interruption rules.
@@ -0,0 +1,48 @@
1
+ # Migration Guide: `workflow` Tool Renamed to `external_workflow`
2
+
3
+ The orchestration tool formerly named `workflow` is now `external_workflow`, alongside `external_delegate`, `external_runs`, and `external_help`. This follows the direct tool rename in [PR #86](https://github.com/tranhoangnguyen03/pi-flow-external/pull/86).
4
+
5
+ ## No Alias
6
+
7
+ Flow no longer registers `workflow`. Old calls are not translated. A stale call fails, or reaches another extension's `workflow` tool if one is registered. Update every caller, then reload or restart Pi.
8
+
9
+ ## What to Update
10
+
11
+ | Where | Before | After |
12
+ |---|---|---|
13
+ | Codemode calls | `tools.workflow({...})` | `tools.external_workflow({...})` |
14
+ | Codemode discovery | `describeTool("workflow")` | `describeTool("external_workflow")` |
15
+ | Exact-name hooks (`tool_call`, `tool_result`, `event.toolName`), handlers, and tool allow-lists such as `--tools` | `"workflow"` | `"external_workflow"` |
16
+ | Result discriminator: envelope `tool` field | `tool: "workflow"` | `tool: "external_workflow"` |
17
+ | `pi-cc-extensions` `excludeRenderers` | `"workflow"` | `"external_workflow"`, then restart Pi. Keep `"workflow"` only if another extension needs it. |
18
+
19
+ ```ts
20
+ const receipt = await tools.external_workflow({
21
+ script: `export const meta = {
22
+ apiVersion: 1, name: "review", description: "Review the change without edits",
23
+ phases: [{ title: "Review", detail: "Claude reviews the change with readonly access." }],
24
+ };
25
+ phase("Review");
26
+ return await agent("Review the change without editing files", {
27
+ description: "Review change", role: "reviewer", harness: "claude", permission: "readonly",
28
+ });`,
29
+ });
30
+ if (!receipt.ok) return receipt.error;
31
+ return receipt.data.run;
32
+ ```
33
+
34
+ ## What Does Not Change
35
+
36
+ - Inputs: `script`, `name`, `scriptPath`, `args`, `background`, and `resumeFromRunId` are unchanged.
37
+ - Script API: `meta.apiVersion: 1`, `agent()`, `parallel()`, `pipeline()`, `phase()`, and `log()` keep their names. Existing saved scripts do not need rewriting.
38
+ - Envelope: `contractVersion: 1`, `action: "run"`, run `kind: "workflow"`, and `wf_...` run IDs are unchanged.
39
+ - Settings stay at version 5. No settings conversion or saved-run rewrite is needed. The extension option `{ workflow: false }`, help topic `workflow`, workflow directories, and E2E `--workflow` flag are unchanged.
40
+ - `external_delegate`, `external_runs`, and `external_help` keep their names. No bridge-compatibility fix is claimed by this rename.
41
+
42
+ ## Explain Work Before Launch
43
+
44
+ Parent guidance now requires a short plain-language briefing: purpose, assignments, parallel/sequential order, allowed changes, and deliverable. Put the roadmap in the existing optional `meta.phases` and use `phase()` at actual transitions. Phase details now display on the live card.
45
+
46
+ This is an instruction to the agent, not a runtime check that a chat message was sent. Planned phases describe intent; child states and activity describe observed execution. Existing saved scripts without phases remain valid. Follow the user's existing approval policy rather than treating every briefing as a new approval gate.
47
+
48
+ Background work still needs collecting through `external_runs`; its launch receipt is not a live monitor. Inspect `view: "launch"` to retrieve the recorded purpose and phase plan during or after execution. That explicit view also contains sensitive script/argument evidence. See [Workflows](workflows.md) for details.
@@ -1,6 +1,6 @@
1
- # Public contract v1: external_delegate, workflow and external_runs
1
+ # Public contract v1: external_delegate, external_workflow and external_runs
2
2
 
3
- `external_delegate`, `workflow` and `external_runs` declare `outputSchema` and return matching `structuredContent`. Human `content` and renderer `details` stay separate. Native Pi codemode receives the structured result, including failures. **Check `ok`**, not just whether the call threw.
3
+ `external_delegate`, `external_workflow` and `external_runs` declare `outputSchema` and return matching `structuredContent`. Human `content` and renderer `details` stay separate. Native Pi codemode receives the structured result, including failures. **Check `ok`**, not just whether the call threw.
4
4
 
5
5
  ```js
6
6
  const receipt = await tools.external_delegate({ role: "worker", description: "Check build", prompt: "Run the build and report." });
@@ -23,7 +23,7 @@ Every result carries:
23
23
  | Tool | `action` | Success `data` | Failure `data` |
24
24
  |---|---|---|---|
25
25
  | `external_delegate` | `delegate` | `{run}` | `{run}`, or `{run: null}` before registration |
26
- | `workflow` | `run` | `{run}` | `{run}`, or `{run: null}` before registration |
26
+ | `external_workflow` | `run` | `{run}` | `{run}`, or `{run: null}` before registration |
27
27
  | `external_runs` | the requested action | the action's payload (below) | `null` |
28
28
 
29
29
  A failure that has a run keeps that run's evidence. Read-only supervision never returns partial data on failure.
@@ -31,35 +31,78 @@ Workspace /path/to/project
31
31
 
32
32
  ---
33
33
 
34
- ## Background Runs & Lifecycle
34
+ ## Foreground, Background & Lifecycle
35
35
 
36
- By default, calls to `external_delegate` and `workflow` are blocking: the caller waits until the child process completes.
36
+ **Foreground is the default:** omit `background` or set it to `false`. The original `external_delegate` or `external_workflow` call waits for settlement and returns the result or failure. Its live card carries progress while the parent waits.
37
37
 
38
- Setting `background: true` queues and starts the work asynchronously while returning an immediate receipt containing a stable handle:
38
+ **Background returns a handle:** set `background: true` when the parent has useful independent work before collection, or the user explicitly requests background execution. The receipt confirms registration, not completion; a fast run may already be settled. The launch receipt is not a live monitor. Use `/external runs` to browse, or `external_runs` to inspect, wait, or cancel.
39
39
 
40
- ```ts
41
- const receipt = await tools.external_delegate({
42
- description: "Audit security-sensitive endpoints",
43
- role: "reviewer",
44
- harness: "claude",
45
- permission: "readonly",
46
- prompt: "Audit security-sensitive endpoints.",
40
+ Use foreground when the result is needed next—even for a long workflow or parallel children. Do not routinely launch background then immediately wait. Both modes support `parallel()` under the same concurrency limiter; neither changes permission boundaries or budget enforcement. Prompt instructions express intent, not an enforced restriction.
41
+
42
+ ### Background Example: Launch → Independent Work → Collect
43
+
44
+ Before launch, tell the user: “Claude will audit the API with readonly permission. While it runs, I’ll read the release checklist, then compare the findings.” For a workflow, also explain assignments and ordering as described in [Workflows](workflows.md#explain-the-plan-before-launch).
45
+
46
+ This codemode example assumes both paths exist; replace them with your project's absolute paths. It uses `external_delegate`, but collection is the same for `external_workflow` and its `wf_...` ID.
47
+
48
+ ```js
49
+ const launched = await tools.external_delegate({
50
+ description: "Audit API routes",
51
+ role: "reviewer", harness: "claude", permission: "readonly",
52
+ prompt: "Audit /absolute/project/api for security defects. Do not edit files.",
47
53
  background: true,
48
54
  });
55
+ if (!launched.ok) throw new Error(launched.error.message);
56
+ let run = launched.data.run; // Retain this handle and its runId.
57
+
58
+ // Useful independent work, not an immediate wait:
59
+ const checklist = await tools.read({ path: "/absolute/project/RELEASE.md" });
60
+
61
+ if (!run.state.outcome) {
62
+ const waited = await tools.external_runs({
63
+ action: "wait", runIds: [run.runId], mode: "all",
64
+ });
65
+ if (!waited.ok) throw new Error(waited.error.message);
66
+ const settled = waited.data.completed.find(item => item.runId === run.runId);
67
+ if (!settled) return { pending: waited.data.pending, checklist };
68
+ run = settled;
69
+ }
49
70
 
50
- if (!receipt.ok) {
51
- throw new Error(`Failed to launch: ${receipt.error.message}`);
71
+ if (run.state.outcome === "succeeded" && run.output.delivery === "inline") {
72
+ return { run, result: run.output.value, checklist }; // null/false/0 are valid.
52
73
  }
53
74
 
54
- const runId = receipt.data.run.runId; // e.g. "run_..."
75
+ const view = run.state.outcome === "succeeded" ? "final" : "diagnostics";
76
+ let cursor;
77
+ let text = "";
78
+ let encoding;
79
+ do {
80
+ const inspected = await tools.external_runs({
81
+ action: "inspect", runIds: [run.runId], view, ...(cursor ? { cursor } : {}),
82
+ });
83
+ if (!inspected.ok) throw new Error(inspected.error.message);
84
+ if (view === "final" && !inspected.data.finalAvailable) {
85
+ throw new Error("No verified final answer is available");
86
+ }
87
+ text += inspected.data.page.text;
88
+ encoding = inspected.data.page.encoding;
89
+ cursor = inspected.data.page.nextCursor;
90
+ } while (cursor);
91
+ return { run, view, encoding, text, checklist };
55
92
  ```
56
93
 
57
- Background runs do not notify or resume this session when they finish. Collect them with `external_runs`: `wait`, then `inspect` with view `final` for a successful answer. For a failed run, read the `wait` outcome and `inspect` view `diagnostics`.
94
+ - **Check the run, not just `ok`.** `wait.ok` means supervision worked, even if a child failed. Read each `completed[i].state.outcome` and all `pending` IDs. With multiple targets, a failed workflow can return early even in `mode: "all"`; pending work continues. Keep collecting it unless the user defers, or cancel it explicitly.
95
+ - **Avoid redundant calls.** If settlement is already known, inspect directly rather than wait again. Successful inline output needs no second fetch. A successful workflow can still have `children.failed` when the script handled child errors; check the returned reports.
96
+ - **Read every page.** Otherwise use `final` for verified success or `diagnostics`/`output` for failure. `final` is empty without verified success. Follow `page.nextCursor`, concatenate text, and only then parse if `encoding` is `json`. Workflow final JSON wraps the value in `{ runId, result, finalAvailable }`; agent finals are text.
97
+ - **Keep ownership.** The parent retains IDs and collects/checks results unless the user explicitly defers collection. A script's local variables do not survive a separate codemode call; pass the retained handle or ID explicitly. If independent work or collection fails, report any still-running IDs rather than abandoning them silently. Never retry a child automatically.
98
+
99
+ ### Interruption, Cancellation & Session Ownership
58
100
 
59
- ### Ownership Invariants
60
- - **Session-Owned:** Background children belong to the active Pi session, not a persistent OS daemon.
61
- - **Orderly Shutdown:** Closing Pi gracefully signals cancellation and waits for bounded cleanup.
62
- - **Unconfirmed Exits:** Host crashes or force-kills leave unfinished evidence marked `interrupted_or_uncertain`. A restarted session never re-attaches to live orphan processes.
101
+ - **Foreground interruption:** interrupting the original blocking call cancels that work.
102
+ - **Wait interruption:** interrupting `external_runs wait` only stops waiting; it does not cancel the selected runs. Use `external_runs cancel` to stop background work. Cancellation requests are not proof of completed cleanup; check settlement.
103
+ - **Workflow cancellation:** cancelling the root stops its active children. Cancelling one child throws a catchable child error; a script that handles it may continue.
104
+ - **No automatic delivery:** background completion does not notify, wake, or resume the parent. Ending a turn is not the same as closing the session; work can continue between turns in the owning session.
105
+ - **Not a persistent service:** keep the owning Pi session open. Orderly shutdown requests cancellation and waits for bounded cleanup. Crashes or force-kills leave unfinished evidence `interrupted_or_uncertain`; restarting Pi never adopts live orphan work.
63
106
 
64
107
  ---
65
108
 
@@ -110,10 +153,11 @@ Block until one or more targets reach terminal state:
110
153
  const waitResult = await external_runs({
111
154
  action: "wait",
112
155
  runIds: [runId1, runId2],
113
- mode: "all", // "all" waits for every run; "any" returns after the first settles
156
+ mode: "all", // A failed workflow can return early; always check data.pending.
114
157
  limitBytes: 32768,
115
158
  });
116
159
  ```
160
+ - **Wait Modes:** `any` returns after a selected target settles; `all` waits for all, except an unsuccessful workflow returns early. Check `completed` outcomes and `pending` IDs; `ok: true` does not mean every run succeeded.
117
161
  - **Live Progress Heartbeat:** While waiting, the tool emits bounded progress updates (watched targets, completed/pending counts, recent activity) through Pi's update channel.
118
162
  - **Shared Budget:** The `limitBytes` cap is spent across settled results in requested order. If a result exceeds remaining space, it returns `resultTruncated: true` with references to inspect the rest.
119
163
 
@@ -131,7 +175,7 @@ await external_runs({
131
175
 
132
176
  ## Operational Evidence & Run Storage
133
177
 
134
- Every normal run writes durable, private evidence to disk:
178
+ Normal runs write private, best-effort evidence to disk; unavailable storage does not discard a settled in-memory result:
135
179
 
136
180
  ```text
137
181
  ~/.pi/agent/pi-flow-external/runs/<run-id>/
@@ -59,14 +59,14 @@ Solutions to common issues, authentication questions, and environment quirks whe
59
59
 
60
60
  ## Third-Party UI Compatibility: `pi-cc` / `ccstyle`
61
61
 
62
- If using [`pi-cc-extensions`](https://www.npmjs.com/package/pi-cc-extensions), its default rendering overrides can cause `workflow` and `wait` progress cards to show a generic `Pending…` message.
62
+ If using [`pi-cc-extensions`](https://www.npmjs.com/package/pi-cc-extensions), its default rendering overrides can cause `external_workflow` and `external_runs` wait progress cards to show a generic `Pending…` message.
63
63
 
64
64
  ### Resolution
65
65
  Add `pi-flow-external` tool names to `excludeRenderers` in `~/.pi/agent/pi-cc-extensions.json`:
66
66
 
67
67
  ```json
68
68
  {
69
- "excludeRenderers": ["external_delegate", "workflow", "external_runs"]
69
+ "excludeRenderers": ["external_delegate", "external_workflow", "external_runs"]
70
70
  }
71
71
  ```
72
72
 
package/docs/workflows.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Workflows
2
2
 
3
- Workflows provide trusted JavaScript orchestration across external AI harnesses and in-process Pi models, enabling parallel fan-out, multi-stage pipelines, and structured failure handling.
3
+ The `external_workflow` tool provides trusted JavaScript orchestration across external AI harnesses and in-process Pi models, enabling parallel fan-out, multi-stage pipelines, and structured failure handling. The former `workflow` tool name is not an alias; see the [rename migration guide](migration-workflow-to-external-workflow.md).
4
4
 
5
5
  ---
6
6
 
@@ -13,8 +13,13 @@ export const meta = {
13
13
  apiVersion: 1,
14
14
  name: "code-review",
15
15
  description: "Parallel architecture and QA review",
16
+ phases: [
17
+ { title: "Review", detail: "Claude checks architecture while Codex checks tests; both use readonly access." },
18
+ { title: "Collect", detail: "Return both reports for the parent to compare." },
19
+ ],
16
20
  };
17
21
 
22
+ phase("Review");
18
23
  // Orchestrate subagents:
19
24
  const [arch, qa] = await parallel([
20
25
  () => agent("Review architecture read-only", {
@@ -29,11 +34,38 @@ const [arch, qa] = await parallel([
29
34
  }),
30
35
  ]);
31
36
 
37
+ phase("Collect");
32
38
  return { arch, qa };
33
39
  ```
34
40
 
35
41
  ---
36
42
 
43
+ ## Explain the Plan Before Launch
44
+
45
+ Before calling `external_workflow`, the parent agent must tell the user, in plain language:
46
+
47
+ - The purpose and which agent/harness handles each assignment.
48
+ - What runs in parallel and what waits for earlier results.
49
+ - What may change and what deliverable the user will receive.
50
+
51
+ For example: “Claude will review the architecture while Codex checks test coverage, both with readonly access. Then I’ll compare their reports and return one prioritized list.” Follow the user's existing approval policy; a briefing is not an extra approval gate. Explain material changes to the plan.
52
+
53
+ Put the same roadmap in `meta.phases`, an optional ordered array of `{ title, detail? }`, and call `phase(title)` when each stage actually begins. Short phase details explain the work on the live card; future stages appear as planned. Existing saved scripts without phases remain valid.
54
+
55
+ **A plan is intent, not proof of execution.** The briefing is an agent instruction, not a runtime-verified chat message. Runtime validation checks phase metadata, not whether the script follows its prose. Child states and activity are the source of truth for observed execution. Do not invent completion estimates or call a quiet agent healthy merely because a spinner moves.
56
+
57
+ Choose foreground by default, including parallel workflows. Use background only for useful independent work or an explicit user request; it does not make children more parallel. While a foreground call runs, the live card carries progress; the parent cannot reliably send chat updates from inside that call. A background receipt is not a live monitor, and completion does not wake the parent. Keep the owning session open and collect/check results unless the user explicitly defers. See [execution and collection](supervision-and-runtime.md#foreground-background--lifecycle) for the complete lifecycle and interruption rules.
58
+
59
+ The recorded purpose and phase details remain accessible in explicit launch inspection, during execution and after completion:
60
+
61
+ ```ts
62
+ await tools.external_runs({ action: "inspect", runIds: [runId], view: "launch" });
63
+ ```
64
+
65
+ Launch evidence also includes the script and arguments and may contain sensitive information. Follow `nextCursor` and concatenate pages when the response is paged.
66
+
67
+ ---
68
+
37
69
  ## Global Functions & Environment
38
70
 
39
71
  The workflow sandbox exposes these helpers:
@@ -41,7 +73,7 @@ The workflow sandbox exposes these helpers:
41
73
  - **`agent(prompt, options)`:** Delegates one subagent task.
42
74
  - `options.role`: Role name (`explorer`, `reviewer`, `implementer`, etc.).
43
75
  - `options.harness`: Optional harness override (`claude`, `codex`, `agy`, `grok`, `muse`, `opencode`, or named `pi-*`).
44
- - `options.permission`: Sandbox tier (`readonly`, `edit`, or `danger`).
76
+ - `options.permission`: Permission tier (`readonly`, `edit`, or `danger`).
45
77
  - `options.context`: Context sharing (`{ mode: "recent", turns: 5 }` or `{ mode: "full" }`).
46
78
  - `options.description`: User-facing task label in UI.
47
79
  - `options.schema`: Optional JSON / TypeBox schema for structured return values (supported on Pi subagents and native CLI schemas).
@@ -50,7 +82,7 @@ The workflow sandbox exposes these helpers:
50
82
  - **`pipeline(items, ...stages)`:** Feeds items sequentially through transformation stages.
51
83
  - **`phase(title)`:** Marks milestone boundaries in live progress presentation.
52
84
  - **`log(message)`:** Appends diagnostic notes to the workflow journal.
53
- - **`args`:** Input JSON arguments passed via `workflow({ args: { ... } })`.
85
+ - **`args`:** Input JSON arguments passed via `external_workflow({ args: { ... } })`.
54
86
  - **`cwd`:** Current working directory of the Pi session.
55
87
 
56
88
  ---
@@ -96,7 +128,7 @@ When a workflow begins, it freezes an immutable snapshot of all settings, role i
96
128
  When executing a saved workflow file from disk, you can pass `resumeFromRunId` to replay:
97
129
 
98
130
  ```ts
99
- await workflow({
131
+ await tools.external_workflow({
100
132
  scriptPath: "/path/to/workflow.js",
101
133
  resumeFromRunId: "wf_previous_run_id",
102
134
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tranhoangnguyen0310/pi-flow-external",
3
- "version": "4.0.0-external.0",
3
+ "version": "5.0.0-external.0",
4
4
  "description": "External Claude Code, Codex CLI, Antigravity, Grok Build CLI, Muse Code, and OpenCode delegation for pi.",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -21,6 +21,7 @@
21
21
  "docs/troubleshooting.md",
22
22
  "docs/migration-v4-to-v5.md",
23
23
  "docs/migration-agent-to-external-delegate.md",
24
+ "docs/migration-workflow-to-external-workflow.md",
24
25
  "docs/public-contract.md"
25
26
  ],
26
27
  "keywords": [
@@ -25,7 +25,7 @@ export function contractEnvelope<S extends TSchema, F extends TSchema>(tool: str
25
25
  return Type.Union([successVariant(tool, action, success), failureVariant(tool, action, failure)]);
26
26
  }
27
27
 
28
- /** Builds one envelope as plain JSON. `redact` re-redacts the whole envelope (Agent and workflow receipts). */
28
+ /** Builds one envelope as plain JSON. `redact` re-redacts the whole envelope (external_delegate and external_workflow receipts). */
29
29
  export function envelope({ tool, action, data, warnings = [], error, redact = false }: {
30
30
  tool: string;
31
31
  action: string;
@@ -1,11 +1,12 @@
1
1
  import { Type } from "typebox";
2
+ import { EXTERNAL_WORKFLOW_TOOL } from "../tool-names.ts";
2
3
  import type { RegisteredRunEntry } from "../core/run-registry.ts";
3
4
  import { contractEnvelope, INLINE_RESULT_BYTES, type PublicError } from "./envelope.ts";
4
5
  import { liveWorkflowRun, workflowRunSchema, type EvidenceIntegrity, type PublicRun } from "./run.ts";
5
6
 
6
7
  const workflowData = Type.Object({ run: Type.Union([Type.Null(), workflowRunSchema]) });
7
8
  /** `ok` describes the workflow operation: a background launch is accepted, a foreground one settled successfully. */
8
- export const workflowOutputSchema = contractEnvelope("workflow", Type.Literal("run"), workflowData, workflowData);
9
+ export const workflowOutputSchema = contractEnvelope(EXTERNAL_WORKFLOW_TOOL, Type.Literal("run"), workflowData, workflowData);
9
10
 
10
11
  export const HANDLED_CHILD_FAILURES = "handled_child_failures";
11
12
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * A shared in-flight concurrency limiter (semaphore + FIFO wait-queue).
3
3
  *
4
- * One instance is shared across the `Agent` tool and the `workflow` tool so the
4
+ * One instance is shared across the `external_delegate` tool and the `external_workflow` tool so the
5
5
  * global cap bounds the total number of concurrent subagents across both entry
6
6
  * points — not per-tool.
7
7
  *
@@ -9,7 +9,7 @@
9
9
  * - `tryAcquire()` — synchronous, non-blocking. Returns a release fn if a slot
10
10
  * is free, else `null`. Kept for direct semaphore-style callers/tests.
11
11
  * - `acquire(signal?)` — async. Resolves with a release fn when a slot frees.
12
- * Used by both the `Agent` tool and workflow `agent()` calls so excess
12
+ * Used by both the `external_delegate` tool and workflow `agent()` calls so excess
13
13
  * subagents queue and drain under the shared cap. Honors an AbortSignal.
14
14
  *
15
15
  * Correctness / no over-subscription: `active` is incremented only on the fast
package/src/core/spawn.ts CHANGED
@@ -45,18 +45,18 @@ import { runRecordsDirectory } from "./retention.ts";
45
45
  import { createTimeoutSignal, markSubagentTimedOut } from "./timeout.ts";
46
46
  import type { PermissionResolution } from "./permissions.ts";
47
47
  import { effectivePiResourcePreset, isValidThinkingLevel, VALID_THINKING_LEVELS } from "../harnesses.ts";
48
- import { EXTERNAL_DELEGATE_TOOL } from "../tool-names.ts";
48
+ import { EXTERNAL_DELEGATE_TOOL, EXTERNAL_WORKFLOW_TOOL } from "../tool-names.ts";
49
49
 
50
50
  /**
51
51
  * The delegation tools a spawned child must never receive, so subagents cannot
52
52
  * recursively fan out. Owned by the spawn core and used as the default exclude
53
53
  * list so no caller can accidentally under-specify the nesting block.
54
54
  */
55
- export const CHILD_EXCLUDED_TOOLS: readonly string[] = [EXTERNAL_DELEGATE_TOOL, "workflow"];
55
+ export const CHILD_EXCLUDED_TOOLS: readonly string[] = [EXTERNAL_DELEGATE_TOOL, EXTERNAL_WORKFLOW_TOOL];
56
56
 
57
57
  /**
58
58
  * Parameters for a single subagent run. This is the shared spawn primitive used
59
- * by both the `Agent` tool and the `workflow` tool's `agent()` global.
59
+ * by both the `external_delegate` tool and the `external_workflow` tool's `agent()` global.
60
60
  * Concurrency accounting lives in the callers, not here; callers acquire a slot
61
61
  * before invoking spawnSubagent, so the runtime timeout below excludes queue time.
62
62
  */
@@ -17,8 +17,8 @@ export const RICH_SUBAGENT_ACTIVE_LIMIT = 4;
17
17
  * initializes (`initTheme()`); a headless/RPC render, an uninitialized test
18
18
  * harness, or any other caller without that startup step must still produce
19
19
  * a readable row instead of crashing the tool card. This is the single
20
- * shared "render a result body" primitive for `Agent` (subagent-render.ts),
21
- * `workflow` (its own final-output block), and `external_runs`'s output/
20
+ * shared "render a result body" primitive for `external_delegate` (subagent-render.ts),
21
+ * `external_workflow` (its own final-output block), and `external_runs`'s output/
22
22
  * final views — the last mile of the shared-rendering fix for issue #52's
23
23
  * "still plain Text for output rather than rendered Markdown" gap.
24
24
  */
@@ -90,33 +90,37 @@ function validateHarnessFilter(harness: string | undefined, configuredPiHarnesse
90
90
  }
91
91
 
92
92
  function workflowHelp(workflowsEnabled: boolean, workflows: ReturnType<typeof listSavedWorkflows>): string {
93
- const availability = workflowsEnabled ? "The workflow tool is enabled." : "The workflow tool is disabled for this extension instance.";
93
+ const availability = workflowsEnabled ? "The external_workflow tool is enabled." : "The external_workflow tool is disabled for this extension instance.";
94
94
  return `${availability} It runs trusted JavaScript, not a sandbox.
95
95
 
96
96
  Provide exactly one workflow source: name, scriptPath, or script. Every script starts with the literal current declaration export const meta = { apiVersion: 1, name, description }, calls agent() at least once, awaits every started call, and returns JSON-serializable data. Missing/unsupported API versions fail before any child launches. Project .pi/workflows are visible only when the project is trusted.
97
97
 
98
+ Before calling external_workflow, explain the purpose, assignments, parallel/sequential order, allowed changes, and deliverable in plain language. Add the same roadmap as meta.phases: [{ title: "Review", detail: "Who checks what and whether files may change" }, ...], and call phase("Review") when that stage actually starts. Planned phases and details remain visible on the live card; they describe intent, while child states/activity show observed execution. Explain material plan changes and follow the user's existing approval policy. The briefing is an agent instruction, not a runtime-verified chat message; phases remain optional for existing saved scripts. Use external_runs inspect view launch to retrieve the recorded purpose and plan (also includes sensitive script/args); background receipts are not live monitors.
99
+
98
100
  APIs: agent(prompt, { description, label, role, harness, subagent_type, permission, max_budget_usd, resume, context, schema, phase }); parallel(thunks); pipeline(items, ...stages); phase(title); log(message). Globals: args and cwd. Agent options other than the profile selector are optional. "description" is the common task-name option shared with the direct external_delegate tool; "label" is a compatible alias — set only one, or both to the same value. Choose role/harness or legacy exact subagent_type, never both. Use unique descriptions/labels and clear task prompts. An agent() succeeds with its value or throws ChildRunError { runId, outcome, message, outputRef, diagnosticsRef }; catch optional failures explicitly. Uncaught failures terminate the workflow and drain siblings. Helpers never convert failures to null. Context options: {mode:"none"} (default), {mode:"recent",turns:N} (positive integer, includes current user turn), or {mode:"full"} (available context after compaction). All children share invocation-time context/settings; earlier child results must still be passed explicitly. Context excludes thinking/system instructions and pending calls; images and snapshots over 1 MiB fail without truncation. Context sharing cannot be combined with resume. Use schema for results that control branching or aggregation. Imports, filesystem globals, Date APIs, and Math.random() are unavailable.
99
101
 
100
- Background and supervision: blocking by default — an ordinary call waits for its child and returns the result, run everything this way unless the parent has other work to do first. Set background:true only when the parent can proceed before completion, then use external_runs on the returned stable run ID while session-owned work continues. Background runs do not notify or resume this session; collect them with external_runs (wait, then inspect with view final). Check failed outcomes and diagnostics; view final only supplies verified successful answers. Parallel same-harness work (e.g. three agy workers) runs concurrently under the shared maxConcurrentSubagents cap: call parallel([() => agent(...), ...]) in a workflow, or issue separate external_delegate calls with background:true in one turn, then external_runs wait/inspect/cancel. Awaiting agent() calls sequentially stays serial by construction. external_runs selects targets with runIds. list takes optional workflowRunId, cursor, workflowCursor, and limit. inspect with one id reads any view (summary, output, diagnostics, or final) and pages with an opaque cursor and limitBytes; several ids (up to 20) batch summary only. wait takes runIds in mode any or all (up to 100). cancel takes one id and an optional reason. Follow nextCursor/nextWorkflowCursor for complete results. Wait returns selected terminal outcomes and pending IDs, never cancels pending work, and returns an unsuccessful workflow early; interrupting wait stops only the wait. Cancelling a workflow stops active children, while cancelling one child is a catchable workflow error. Blocking-call interruption cancels that call; background work survives tool return but is cancelled on orderly owning-session shutdown. It is not a daemon: crashes leave unfinished evidence interrupted/uncertain, restart does not adopt work, and live steering is unavailable.
102
+ Execution mode: foreground is blocking by default, including parallel workflows. Use background:true only for useful independent work before collection or an explicit user request; do not routinely launch background then immediately wait. Both modes support parallel([() => agent(...), ...]) under the same maxConcurrentSubagents cap; sequential awaits stay serial. Background changes neither permissions nor budget enforcement. Its stable handle means accepted, not completed; the receipt is not a live monitor. Retain the ID and own collection unless the user explicitly defers it. No completion notification wakes this session. Keep the session open: orderly shutdown requests cancellation with bounded cleanup; crashes leave uncertain evidence and restart does not adopt work. Topic usage contains the worked collection guidance.
103
+
104
+ Supervision: external_runs selects targets with runIds. list takes optional workflowRunId, cursor, workflowCursor, and limit. inspect with one id reads summary/output/diagnostics/final/launch and pages with cursor and limitBytes; several ids (up to 20) batch summary only. wait takes up to 100 runIds in mode any or all; check ok for the operation, then each completed run's outcome and pending IDs. It never cancels pending work and can return early on a failed workflow even in all mode. If settlement is already known, skip wait. Use inline successful output when available; otherwise inspect final and follow nextCursor. Failures need diagnostics/output, not final. Follow nextWorkflowCursor for workflow listings too. Foreground-call interruption cancels that call; interrupting wait only stops waiting. Use cancel with one id and an optional reason to stop background work. Cancelling a workflow stops active children; cancelling one child is a catchable workflow error. There is no live steering.
101
105
 
102
106
  Replay: resumeFromRunId works only with persisted scriptPath and starts an explicit new attempt. It reuses the longest unchanged prefix of successful child calls; the first changed/failed/cancelled/timed-out call and its suffix run again. Script recomposition is cheap, but child reruns can cost money or repeat side effects. There is no automatic repaired-script replay.
103
107
 
104
- Background is a workflow tool call parameter ({ script, background: true }), never a meta field — meta.background is silently ignored and does not run the workflow in the background.
108
+ Background is an external_workflow tool call parameter ({ script, background: true }), never a meta field — meta.background is silently ignored and does not run the workflow in the background.
105
109
 
106
- Background example (same script as below; call the workflow tool with background:true):
107
- export const meta = { apiVersion: 1, name: "review", description: "Map and review a repository" };
108
- const [map, review] = await parallel([
109
- () => agent("Map /absolute/repo read-only.", { label: "map", role: "explorer" }),
110
- () => agent("Review /absolute/repo read-only.", { label: "review", role: "reviewer", harness: "codex" }),
111
- ]);
112
- return { map, review };
113
-
114
- Foreground example:
115
- export const meta = { apiVersion: 1, name: "review", description: "Map and review a repository" };
110
+ Example (blocking by default; pass background:true on the external_workflow call only when the parent has other work):
111
+ export const meta = {
112
+ apiVersion: 1, name: "review", description: "Map and review a repository",
113
+ phases: [
114
+ { title: "Check", detail: "The default harness maps the repository while Codex reviews it; neither is assigned edits." },
115
+ { title: "Collect", detail: "Return both reports for the parent to compare." },
116
+ ],
117
+ };
118
+ phase("Check");
116
119
  const [map, review] = await parallel([
117
120
  () => agent("Map /absolute/repo read-only.", { label: "map", role: "explorer" }),
118
121
  () => agent("Review /absolute/repo read-only.", { label: "review", role: "reviewer", harness: "codex" }),
119
122
  ]);
123
+ phase("Collect");
120
124
  return { map, review };
121
125
 
122
126
  ${formatSavedWorkflows(workflows)}`;
@@ -76,7 +76,7 @@ export const agentToolParameters = Type.Object({
76
76
  description: "The task briefing to send to the subagent.",
77
77
  }),
78
78
  background: Type.Optional(Type.Boolean({
79
- description: "Return a stable run handle after registration while the session-owned child continues. Defaults to false.",
79
+ description: "Defaults to false: wait for settlement. True returns a stable session-owned run handle after registration; work may already be settled. Use for independent work or an explicit background request. Retain the ID and collect with external_runs; no completion notification. Keep the owning session open.",
80
80
  })),
81
81
  role: Type.Optional(Type.String({
82
82
  minLength: 1,
@@ -36,13 +36,13 @@ A role is shared authoring only: one markdown file under pi-flow-external/roles/
36
36
 
37
37
  Describe read-only or editing intent in the instructions. Do not ask for a permission tier: a role does not grant authority. The caller passes permission, or the global defaultPermission applies. Do not ask about backend, model, or thinking: shared roles never pin those, and backend-specific execution settings belong in /external config role set <role> --harness <name>; instruction replacements use /external config role edit <role> --harness <name>. When ready, summarize once and call ${ROLE_TOOL_NAME}.
38
38
 
39
- Do not write files yourself and do not run external_delegate or workflow. The tool will show what will be created for review, request confirmation, and write it offline without a backend smoke test. A role is not an authenticated connection; readiness smoke testing belongs to harness registration.`;
39
+ Do not write files yourself and do not run external_delegate or external_workflow. The tool will show what will be created for review, request confirmation, and write it offline without a backend smoke test. A role is not an authenticated connection; readiness smoke testing belongs to harness registration.`;
40
40
 
41
41
  export const HARNESS_INTERVIEW_PROMPT = `Help me register one named Pi harness configuration through an AI-assisted interview.
42
42
 
43
43
  Ask for a "pi-<label>" name, a provider/model id (validated live against the model registry), an optional thinking level (off/minimal/low/medium/high/xhigh; default off), and a resource preset. The preset is a simple choice of minimal or skills. minimal is the default and loads no skills. skills loads installed skills; project skills load only when the project is trusted. The six canonical roles (explorer, planner, implementer, reviewer, qa, worker) become automatically available on this harness the moment it is registered — no per-role file needed. When ready, summarize once and call ${HARNESS_TOOL_NAME}.
44
44
 
45
- Do not write files yourself and do not run external_delegate or workflow in this branch. The tool will show what will be registered for review, request confirmation, stage it, run a real in-process pi smoke test, and either install it or roll it back.`;
45
+ Do not write files yourself and do not run external_delegate or external_workflow in this branch. The tool will show what will be registered for review, request confirmation, stage it, run a real in-process pi smoke test, and either install it or roll it back.`;
46
46
 
47
47
  const roleParameters = Type.Object({
48
48
  name: Type.String({ description: "Lowercase shared role name, such as security-reviewer. No backend prefix; reviewer replaces the built-in reviewer." }),
package/src/profiles.ts CHANGED
@@ -264,7 +264,7 @@ export function loadExternalCatalog(agentDir = getAgentDir(), options: { legacyI
264
264
  const { frontmatter } = parseFrontmatter<Record<string, unknown>>(content);
265
265
  const obsolete = ["permission", "capabilitySet"].filter((key) => Object.prototype.hasOwnProperty.call(frontmatter, key));
266
266
  if (obsolete.length) {
267
- throw new Error(`obsolete metadata ${obsolete.join(", ")} does not grant authority. Remove it. A role describes intent; pass permission on the external_delegate or workflow call, or set defaultPermission. Pi skills follow the harness preset.`);
267
+ throw new Error(`obsolete metadata ${obsolete.join(", ")} does not grant authority. Remove it. A role describes intent; pass permission on the external_delegate or external_workflow call, or set defaultPermission. Pi skills follow the harness preset.`);
268
268
  }
269
269
  if (Object.keys(frontmatter).some((key) => key !== "description")) throw new Error("shared roles support description only; use an exact override for execution settings");
270
270
  } else {
package/src/prompts.ts CHANGED
@@ -92,13 +92,15 @@ export function buildCoordinatorPrompt(
92
92
 
93
93
  ${formatExternalRoleCatalog(profiles, defaultHarness, configuredHarnessNames)}
94
94
 
95
- Own the result and verify it yourself. Do the work directly unless a separate harness, a fresh context, or a bounded parallel review earns the overhead. A role is the job. A harness (${EXTERNAL_HARNESSES.join(", ")}, or a registered pi-* name) is the environment. Prefer the configured default harness unless the user asks for another or the task needs a capability the default lacks. Select both through external_delegate or workflow. Follow the user's own approval policy before widening a delegation. This extension has no fixed multi-agent approval threshold.
95
+ Own the result and verify it yourself. Do the work directly unless a separate harness, a fresh context, or a bounded parallel review earns the overhead. A role is the job. A harness (${EXTERNAL_HARNESSES.join(", ")}, or a registered pi-* name) is the environment. Prefer the configured default harness unless the user asks for another or the task needs a capability the default lacks. Select both through external_delegate or external_workflow. Follow the user's own approval policy before widening a delegation. This extension has no fixed multi-agent approval threshold.
96
+
97
+ Before external_workflow, explain the purpose, who does what, which tasks run in parallel or in sequence, allowed changes, and the expected deliverable in plain language. Put the same roadmap in meta.phases with short titles and details, and use phase() to mark actual transitions. Explain material plan changes; follow the user's existing approval policy. Planned phases describe intent, not proof of execution: actual child states and activity are authoritative.
96
98
 
97
99
  Give every child an objective, the context it needs, the changes it may make, a deliverable, and the check you will run. Use absolute paths and say whether the task is read-only or may edit. Permission is the call's tier, otherwise defaultPermission (danger unless changed). Omit it when the child should keep that default, including a reviewer that may use a shell and still must not edit. A danger tier still has to stay inside the authorization the user gave you. agy accepts only danger and rejects readonly and edit.
98
100
 
99
101
  Choose a fresh child or resume on purpose. Omit context for independent work. context {mode:"recent", turns:N} shares the last N user turns, including the current turn; {mode:"full"} shares the available post-compaction conversation. Snapshots drop thinking, system instructions, and pending calls, reject images and payloads over 1 MiB, and are sent to that harness. resume continues the same CLI child and cannot be combined with sharing. A named pi-* harness has no persisted session and no enforced budget. Prefer resume for the same task and a new child for an independent review.
100
102
 
101
- Calls block unless background:true. Background runs do not notify or resume this session; collect them with external_runs (wait, then inspect with view "final"). Check failed outcomes and diagnostics instead of expecting a final answer from a failed run. Same-harness parallel work shares one maxConcurrentSubagents cap: parallel([() => agent(...), ...]) or separate background external_delegate calls in one turn. Sequential awaits stay serial. Report milestones. Retry a failed run, or switch its model or harness, only when the user authorized that step. agy alone may make one disclosed infrastructure retry. Nested agents may start in another workspace.
103
+ Default to foreground (blocking), including parallel workflows. Use background:true only for useful independent work before collection or an explicit user request; do not routinely background then immediately wait. Retain the run ID and own collection unless the user explicitly defers it. Foreground has a live card; a background receipt is not a live monitor and completion never notifies or wakes this session. Use external_runs wait only when needed; check each run's outcome and pending IDs. Read inline successful output or inspect view final; inspect failures with diagnostics. Interrupting foreground cancels that call; interrupting wait only stops waiting. Explicit cancel stops background work. Keep the owning session open: orderly shutdown requests cancellation; restart does not adopt work. Prefer one foreground external_workflow with parallel() for coordinated parallel tasks; all children share maxConcurrentSubagents. Sequential awaits stay serial. Report milestones. Retry a failed run, or switch its model or harness, only when the user authorized that step. agy alone may make one disclosed infrastructure retry. Nested agents may start in another workspace.
102
104
 
103
105
  Catalog availability reflects enabled harnesses and built-in/authored roles, not CLI installation or authentication. Disabled harnesses reject calls without fallback; manage them through /external config. Harness and role lifecycle commands live under /external config harness and /external config role. Role selection never falls back. external_help topic usage is the playbook; roles, permissions, and workflow are the references.`;
104
106
  }
@@ -187,10 +189,12 @@ export function usageWorkflowScript(): string {
187
189
  }
188
190
  }`);
189
191
  return [
190
- 'export const meta = { apiVersion: 1, name: "parallel-review", description: "Review in parallel and keep a sibling failure" };',
192
+ 'export const meta = { apiVersion: 1, name: "parallel-review", description: "Review in parallel and keep a sibling failure", phases: [{ title: "Review", detail: "Claude maps the repository while Codex reviews concurrency; neither is assigned to edit files." }, { title: "Collect", detail: "Return both findings, including any failed check." }] };',
193
+ 'phase("Review");',
191
194
  "const settled = await parallel([",
192
195
  branches.join(",\n"),
193
196
  "]);",
197
+ 'phase("Collect");',
194
198
  "return { settled };",
195
199
  "",
196
200
  ].join("\n");
@@ -203,7 +207,7 @@ function jsonBlock(value: unknown): string {
203
207
  /** Worked playbook served by external_help topic "usage". Examples are the exported constants. */
204
208
  export function formatUsagePlaybook(): string {
205
209
  const script = usageWorkflowScript();
206
- return `Practical delegation playbook. A role is the work. A harness is the environment: ${EXTERNAL_HARNESSES.join(", ")}, or a registered pi-* name. external_delegate and workflow select every one of those harnesses the same way. Prefer the configured default harness. Name another when the user asks for it or the task needs a capability the default lacks. Do the task directly unless a separate environment, an independent context, or bounded parallel work is worth the coordination. You still own the outcome: read the result and check the deliverable. Follow the user's approval policy before widening a delegation. There is no fixed team-size threshold here.
210
+ return `Practical delegation playbook. A role is the work. A harness is the environment: ${EXTERNAL_HARNESSES.join(", ")}, or a registered pi-* name. external_delegate and external_workflow select every one of those harnesses the same way. Prefer the configured default harness. Name another when the user asks for it or the task needs a capability the default lacks. Do the task directly unless a separate environment, an independent context, or bounded parallel work is worth the coordination. You still own the outcome: read the result and check the deliverable. Follow the user's approval policy before widening a delegation. There is no fixed team-size threshold here.
207
211
 
208
212
  A named pi-* harness runs in-process on the SDK's built-in tools. Preset minimal leaves skills unloaded. Preset skills loads installed skills, and project skills only when the project is trusted. Those presets select skill loading. They are not this extension's tools and they are not web search. Resume does not continue a pi-* child, and a budget there is recorded without enforcement.
209
213
 
@@ -211,7 +215,9 @@ Tell each child the objective, the context it needs, what it may change, the del
211
215
 
212
216
  Choose fresh context or resume deliberately. Omit context, or pass {"mode":"none"}, for independent work such as a review. {"mode":"recent","turns":N} and {"mode":"full"} share parent conversation and cannot be combined with resume. resume continues one prior CLI run that recorded a session id, on the same backend: ${EXTERNAL_HARNESSES.join(", ")}. A named pi-* harness has no persisted session, so resume does not continue it. Prefer resume for the same task and a new child when you want an independent judgment.
213
217
 
214
- Calls block until the child finishes. background true returns a run id while the originating session still owns the work. Background runs do not notify or resume this session. Use external_runs to wait, then inspect with view final, and judge that answer yourself. For failed runs, check the outcome and diagnostics; view final only provides verified successful answers. A failed or aborted run stays failed. Retry it, or switch model or harness, only when the user authorized that step. agy may make one disclosed infrastructure retry; that exception is not yours to extend.
218
+ Choose foreground by default, even for a long or parallel workflow: the original call waits for settlement and its live card carries progress. Use background:true for useful independent work or an explicit user request, not routinely followed by an immediate wait. The returned handle means accepted, not completed; the run may already be settled. Keep its ID and collect/check the outcome yourself unless the user explicitly defers collection. No completion notification wakes the session. Keep Pi open: orderly shutdown requests cancellation with bounded cleanup; crashes leave uncertain evidence and restart does not adopt work.
219
+
220
+ After independent work, use external_runs wait if settlement is not already known. Check ok for the operation, then each completed run's state.outcome and pending IDs; a failed workflow can return wait early even in mode all. Use successful output when delivery is inline (null/false/0 are valid); otherwise inspect view final, following cursor pages. For failures inspect diagnostics or output; final is empty without verified success. A successful workflow may still have children.failed when its script caught child errors; read the reports. Interrupting the original foreground call cancels its work. Interrupting wait only stops waiting; cancel explicitly to stop background work. Cancelling a workflow stops active children; cancelling one child throws a catchable child error. Both modes share permissions, budget enforcement, and concurrency limits. A failed or aborted run stays failed. Retry it, or switch model or harness, only when the user authorized that step. agy may make one disclosed infrastructure retry; that exception is not yours to extend.
215
221
 
216
222
  One external_delegate call. Permission stays omitted; the prompt carries the no-edit intent:
217
223
  ${jsonBlock(USAGE_ONE_AGENT)}
@@ -219,11 +225,13 @@ ${jsonBlock(USAGE_ONE_AGENT)}
219
225
  Independent review. Fresh context, a reviewer role, the default tier, no resume:
220
226
  ${jsonBlock(USAGE_INDEPENDENT_REVIEW)}
221
227
 
222
- Background work, then the verified final answer. A one-element runIds list is valid for view final:
228
+ Background launch. Save data.run.runId from a successful receipt, then do useful independent work before collection:
229
+
223
230
  ${jsonBlock(USAGE_BACKGROUND_AGENT)}
231
+ Later, wait if needed and check the outcome. If succeeded, use inline output; otherwise the following is the final-inspection shape (substitute the retained ID and follow nextCursor). For a failed run choose diagnostics instead:
224
232
  ${jsonBlock(USAGE_RUNS_FINAL)}
225
233
 
226
- Parallel workflow. Catch the child error inside each branch so one failure does not drain its sibling. Pass this string as the workflow tool's script:
234
+ Parallel workflow. First explain the plan in plain language: purpose, assignments, order, allowed changes, and deliverable. Put the same roadmap in meta.phases and mark transitions with phase(). The live card carries progress while a blocking call runs; background receipts are not live monitors, so collect and inspect background work with external_runs. Catch the child error inside each branch so one failure does not drain its sibling. Pass this string as the external_workflow tool's script:
227
235
  ${script}
228
236
  Resume, only for the same task on a CLI harness that stored a session id. Do not combine it with context:
229
237
  ${jsonBlock(USAGE_RESUME_AGENT)}
package/src/tool-names.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  /** Public tool names owned by pi-flow-external. */
2
2
  export const EXTERNAL_DELEGATE_TOOL = "external_delegate" as const;
3
+ export const EXTERNAL_WORKFLOW_TOOL = "external_workflow" as const;
package/src/types.ts CHANGED
@@ -61,18 +61,18 @@ export interface SubagentExtensionOptions {
61
61
  * Maximum number of subagents allowed to run concurrently across the whole
62
62
  * agent run (a global in-flight cap, not a per-level fan-out width). A slot is
63
63
  * taken when a subagent launches and released when it completes, fails, or is
64
- * aborted. The cap is shared by the `Agent` tool and the `workflow` tool.
64
+ * aborted. The cap is shared by the `external_delegate` tool and the `external_workflow` tool.
65
65
  */
66
66
  maxConcurrentSubagents?: number;
67
67
  /**
68
68
  * Maximum wall-clock runtime for each launched subagent, in milliseconds.
69
69
  * Defaults to a generous global guardrail. Set to 0 to disable. The limit is
70
- * shared by direct `Agent` calls and workflow `agent()` calls, and can also
70
+ * shared by direct `external_delegate` calls and workflow `agent()` calls, and can also
71
71
  * be overridden with `--subagent-timeout-ms`.
72
72
  */
73
73
  subagentTimeoutMs?: number;
74
74
  /**
75
- * Register the dynamic `workflow` tool alongside `Agent`. Defaults to true:
75
+ * Register the dynamic `external_workflow` tool alongside `external_delegate`. Defaults to true:
76
76
  * one product, two entry points. Set to false for a subagents-only surface.
77
77
  */
78
78
  workflow?: boolean;
@@ -231,6 +231,9 @@ function validateMeta(meta: unknown): asserts meta is WorkflowMeta {
231
231
  if (!phase || typeof phase !== "object" || typeof (phase as WorkflowMetaPhase).title !== "string") {
232
232
  throw new Error("each meta phase must have a title string");
233
233
  }
234
+ if (phase.detail !== undefined && typeof phase.detail !== "string") {
235
+ throw new Error("meta phase detail must be a string");
236
+ }
234
237
  }
235
238
  }
236
239
  }
@@ -20,13 +20,14 @@ import type { WorkflowCachedAgentResult, WorkflowMetaPhase } from "./types.ts";
20
20
 
21
21
  export const workflowToolParameters = Type.Object({
22
22
  background: Type.Optional(Type.Boolean({
23
- description: "Return a stable workflow handle after registration while session-owned execution continues. Defaults to false.",
23
+ description: "Defaults to false: wait for settlement. True returns a stable session-owned workflow handle after registration; work may already be settled. Use for independent work or an explicit background request. Retain the ID and collect with external_runs; no completion notification. Keep the owning session open.",
24
24
  })),
25
25
  script: Type.Optional(
26
26
  Type.String({
27
27
  description: [
28
28
  "Raw JavaScript workflow script (no Markdown fences) for an ad-hoc workflow.",
29
29
  "First statement: export const meta = { apiVersion: 1, name: 'short_name', description: 'non-empty' }.",
30
+ "Explain the plan in plain language before launch. Add meta.phases: [{ title, detail }] for the roadmap and call phase(title) at actual transitions.",
30
31
  "Use agent(prompt, opts), parallel(thunks), pipeline(items, ...stages), phase(title), log(message), args, cwd. Must call agent() at least once and return a JSON-serializable value. Results are canonicalized to JSON; non-plain objects are rejected.",
31
32
  "Provide exactly one of `script`, `name`, or `scriptPath`.",
32
33
  ].join(" "),
@@ -20,8 +20,9 @@ import { runRecordsDirectory } from "../core/retention.ts";
20
20
  import { captureParentContext } from "../core/parent-context.ts";
21
21
  import { RunRegistry, type RegisteredRunHandle } from "../core/run-registry.ts";
22
22
  import { ExpectedFlowError, type FlowErrorCode } from "../core/errors.ts";
23
- import { withContract } from "../contract/envelope.ts";
23
+ import { redactedText, withContract } from "../contract/envelope.ts";
24
24
  import { workflowOutputSchema, workflowReceipt } from "../contract/workflow.ts";
25
+ import { EXTERNAL_WORKFLOW_TOOL } from "../tool-names.ts";
25
26
  import { loadWorkflowJournal } from "./journal.ts";
26
27
  import { OUTPUT_PREVIEW_CHARS } from "../core/progress.ts";
27
28
  import { loadExternalCatalog, resolveExternalProfile, selectorHarness, unknownExternalProfileMessage } from "../profiles.ts";
@@ -135,14 +136,14 @@ export function createWorkflowTool(
135
136
  options: CreateWorkflowToolOptions,
136
137
  ): ToolDefinition<typeof workflowToolParameters, WorkflowToolDetails> {
137
138
  return defineTool({
138
- name: "workflow",
139
- label: "Workflow",
140
- description: "Run a saved, persisted, or ad-hoc trusted JavaScript workflow that orchestrates external roles.",
139
+ name: EXTERNAL_WORKFLOW_TOOL,
140
+ label: EXTERNAL_WORKFLOW_TOOL,
141
+ description: "Run a saved, persisted, or ad-hoc trusted JavaScript workflow that orchestrates external roles. Before launch, explain purpose, assignments, parallel/sequential order, allowed changes, and deliverable in plain language.",
141
142
  promptSnippet: WORKFLOW_PROMPT_SNIPPET,
142
143
  parameters: workflowToolParameters,
143
144
  outputSchema: workflowOutputSchema,
144
145
  async execute(toolCallId, params, signal, onUpdate, ctx) {
145
- return await withContract<WorkflowToolDetails>({ tool: "workflow", action: "run", failureData: { run: null }, redact: true }, async () => {
146
+ return await withContract<WorkflowToolDetails>({ tool: EXTERNAL_WORKFLOW_TOOL, action: "run", failureData: { run: null }, redact: true }, async () => {
146
147
  const parentMessages = captureParentContext(ctx.sessionManager);
147
148
  const sessionId = ctx.sessionManager?.getSessionId?.() ?? `unpersisted:${toolCallId}`;
148
149
  const sessionVersion = options.registry.sessionVersion(sessionId);
@@ -783,6 +784,8 @@ function renderPhaseTree(container: Container, details: WorkflowToolDetails, the
783
784
  0,
784
785
  ),
785
786
  );
787
+ const detail = details.plannedPhases?.find((planned) => planned.title === phase)?.detail;
788
+ if (detail) container.addChild(new Text(` ${theme.fg("muted", redactedText(detail))}`, 0, 0));
786
789
  const shown = selectAgentsForRender(agents);
787
790
  for (const agent of shown) {
788
791
  container.addChild(renderSubagentNode(agent, theme, frame, runningCount, " ", Date.now(), expanded, false));