@fyeeme/pi-dynamic-workflows 0.1.1 → 2.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fyeeme/pi-dynamic-workflows",
3
- "version": "0.1.1",
3
+ "version": "2.0.1",
4
4
  "description": "Deterministic TypeScript workflow orchestration for pi. Fuses the pi-dynamic-workflows design (declarative graph, 10 step primitives, heuristic planner, outcome collectors) with Claude Code's workflow engine coordination mechanisms (deterministic sandbox, cache-key resume, per-agent abort map, dynamic budget, runaway caps).",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -22,6 +22,9 @@
22
22
  "*.ts",
23
23
  "src/**/*.ts",
24
24
  "sessions/**/*.ts",
25
+ "workflows/**/*.ts",
26
+ "skills/**/*.md",
27
+ "prompts/**/*.md",
25
28
  "README.md",
26
29
  "README.zh-CN.md",
27
30
  "LICENSE"
@@ -29,6 +32,12 @@
29
32
  "pi": {
30
33
  "extensions": [
31
34
  "./index.ts"
35
+ ],
36
+ "skills": [
37
+ "skills/workflow-author"
38
+ ],
39
+ "prompts": [
40
+ "prompts"
32
41
  ]
33
42
  },
34
43
  "scripts": {
@@ -36,20 +45,20 @@
36
45
  "typecheck": "tsc"
37
46
  },
38
47
  "dependencies": {
39
- "@fyeeme/pi-subagent-core": "^0.3.2"
48
+ "@fyeeme/pi-subagents": "2.1.1"
40
49
  },
41
50
  "peerDependencies": {
42
- "@earendil-works/pi-ai": ">=0.84.1",
43
- "@earendil-works/pi-coding-agent": ">=0.84.1",
44
- "@earendil-works/pi-tui": ">=0.84.1",
51
+ "@earendil-works/pi-ai": ">=0.84.4",
52
+ "@earendil-works/pi-coding-agent": ">=0.84.4",
53
+ "@earendil-works/pi-tui": ">=0.84.4",
45
54
  "jiti": ">=2.0.0",
46
55
  "typebox": ">=1.0.0",
47
56
  "typescript": ">=5.0.0"
48
57
  },
49
58
  "devDependencies": {
50
- "@earendil-works/pi-ai": "0.84.1",
51
- "@earendil-works/pi-coding-agent": "0.84.1",
52
- "@earendil-works/pi-tui": "0.84.1",
59
+ "@earendil-works/pi-ai": "0.84.4",
60
+ "@earendil-works/pi-coding-agent": "0.84.4",
61
+ "@earendil-works/pi-tui": "0.84.4",
53
62
  "@types/node": "22.19.19",
54
63
  "jiti": "2.7.0",
55
64
  "typebox": "1.1.38",
@@ -0,0 +1,29 @@
1
+ ---
2
+ description: Implement → review → fix loop via successive subagent calls
3
+ argument-hint: <what to implement>
4
+ ---
5
+ Execute this three-stage loop with the `subagent` tool — one call per stage.
6
+ You are the coordinator: pass each stage's complete output text into the next
7
+ stage's `task` yourself (chain mode no longer exists).
8
+
9
+ 1. **Implement** — call `subagent` with agent `worker`, task: $@ plus any repo
10
+ context it needs. Ask for the final implementation summary in the result.
11
+
12
+ 2. **Review** — call `subagent` with agent `reviewer`. Its task must begin:
13
+ "Review the following implementation." followed by the worker's COMPLETE
14
+ result verbatim, plus the files/areas touched.
15
+
16
+ 3. **Fix** — if and only if the reviewer reported issues, one more `worker`
17
+ call: "Apply this feedback to your earlier implementation." followed by the
18
+ reviewer's findings verbatim.
19
+
20
+ Rules:
21
+
22
+ - Forward results between stages verbatim — never paraphrase or summarize;
23
+ each subagent has zero memory of its predecessor.
24
+ - Run stages strictly sequentially; a later stage always needs the earlier
25
+ stage's full output as input.
26
+ - Skip stages 2–3 only for trivial changes (< ~20 lines, no behavior change);
27
+ state explicitly that you did so and why.
28
+ - Finish by reporting: what was implemented, the review verdict, and anything
29
+ still open.
@@ -0,0 +1,30 @@
1
+ ---
2
+ description: Heavy review pipeline via deterministic workflow (budget + resume + adversarial verify)
3
+ ---
4
+ Run the heavy review pipeline now. Target: $@
5
+
6
+ First resolve the diff yourself (run `git diff` against the appropriate
7
+ range — upstream merge-base if configured, else HEAD; pass the diff text as
8
+ the workflow `input`).
9
+
10
+ Then call `run_workflow` with `source: "library"`:
11
+
12
+ - For a plain local diff → name `review-local-diff` (5 finders at
13
+ parallelism 4, adversarial verify judges 3 / minPass 2, budget 12 agents /
14
+ 2M tokens).
15
+ - For changed packages under an extensions submodule → name
16
+ `review-extension` (one reviewer per changed package at parallelism 3).
17
+
18
+ Budget guidance: pass a per-call `budget` override on the run_workflow call —
19
+ halve `maxAgents` when the diff exceeds ~150 files; without an override the
20
+ workflow's own declared budget applies.
21
+
22
+ Resume behavior: on journal keys already present from a prior partial run
23
+ the engine skips completed steps — do not re-dispatch them yourself.
24
+
25
+ When the run completes, report the merged, verified findings via
26
+ `review_report` (if available) or as a markdown list ranked most-severe
27
+ first.
28
+
29
+ For tuning the pipeline itself (steps, rubric, determinism constraints),
30
+ load the workflow-author skill.
package/sessions/spawn.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Thin barrel over src/agent/dispatch.ts: the spawn registry + per-agent
5
5
  * abort primitives. The core spawn implementation lives in
6
- * `@fyeeme/pi-subagent-core`; skip/retry (workflows-specific) stay in
6
+ * `@fyeeme/pi-subagents`; skip/retry (workflows-specific) stay in
7
7
  * dispatch.ts.
8
8
  */
9
9
  export {
@@ -0,0 +1,69 @@
1
+ ---
2
+ name: workflow-author
3
+ description: "Author and tune deterministic workflow definitions for run_workflow: step-type selection, rubric writing, the single-file determinism constraint, budget/parallelism as declared data, and resume semantics."
4
+ ---
5
+
6
+ # Authoring workflow-library definitions
7
+
8
+ A library workflow is a single `.ts` file under `.pi/workflows/lib/`
9
+ (project) or the package's bundled `workflows/` (reference examples:
10
+ `review-local-diff`, `review-extension`). It exports a workflow and becomes
11
+ runnable by name (`run_workflow` with `source: "library"`) on the next call —
12
+ dropping the file in IS the registration; no code change, no restart.
13
+
14
+ ## The hard constraint: single-file determinism
15
+
16
+ The loader runs the ast determinism guard on the entry source and REJECTS
17
+ `Date.now()`, `Math.random()`, and `new Date()` **before** executing. This is
18
+ not a style rule — cache-key resume is only sound when the same workflow
19
+ source produces the same cache keys. The guard scans **only the entry file**:
20
+ a helper imported from another file could smuggle in non-determinism and
21
+ silently break resume. Therefore: **library workflows must stay
22
+ single-file.** Inline everything; if you need `now`, the engine passes it
23
+ (the `now` run parameter / `ctx` plumbing) — never read the clock yourself.
24
+
25
+ ## Choosing step types
26
+
27
+ | Need | Step | Notes |
28
+ |---|---|---|
29
+ | One LLM call | `agent` | prompt may be a function of `ctx.input` / `ctx.step(id)` |
30
+ | N parallel calls over a list | `fan_out` | `over` may be dynamic (`ctx => …`); declare `parallelism`; `merge` folds results |
31
+ | Produce + judge | `adversarial` | rubric criteria become judge prompts; `judges`/`minPass` (majority default) |
32
+ | Best-of-N | `tournament` | candidates × judges |
33
+ | Route by classification | `classify_route` | classifier replies `{category}`; routes + fallback |
34
+ | Iterate to convergence | `loop_until` | `until` condition + `maxIterations` (TS-only — library files only, not inline JSON) |
35
+ | Zero-token narrative | `log` | journal annotation only |
36
+
37
+ ## Writing rubrics (adversarial / tournament)
38
+
39
+ A rubric entry is a judge prompt, not a checkbox. Write each criterion as a
40
+ falsifiable, self-contained sentence: `"finding is concretely actionable
41
+ (file:line present)"` — not `"quality"`. 2–4 criteria; a judge that cannot
42
+ verify a criterion from the material in front of it will guess, and guessing
43
+ flattens the verdict distribution.
44
+
45
+ ## Budget and parallelism are data
46
+
47
+ `budget: { maxAgents, maxTokens, maxDurationMs }` at the workflow level and
48
+ `parallelism` on fan-out steps are declared fields the engine enforces
49
+ (fan-out pre-checks the batch fits; a step exceeding budget follows its
50
+ `onBudgetExhaust` policy — throw, or degrade to null). Tune them by editing
51
+ the file; orchestration prompts may also pass tighter values per call.
52
+
53
+ ## Resume semantics
54
+
55
+ Every dispatched call is cache-keyed (workflow source + prompts + inputs).
56
+ Re-running with unchanged definition and inputs skips completed steps from
57
+ the journal — do not "help" by re-dispatching; change an input only when you
58
+ want a step actually re-run. Editing the workflow source changes the keys:
59
+ that is the intended way to invalidate stale cache.
60
+
61
+ ## Checklist before shipping a library workflow
62
+
63
+ 1. Single file, and imports type-only: `import type { WorkflowDefinition } from ...` (erased at load — nothing outside the file executes, so the entry-only guard scan covers everything). NEVER value-import the engine by relative path: from `.pi/workflows/lib/` it does not resolve (bricking the whole library), and any value import puts un-scanned code on the load path. Plain `export const workflow = { name, steps }` with no import at all works too (the loader validates shape, not `defineWorkflow`).
64
+ 2. No `Date.now` / `Math.random` / `new Date` anywhere in the source.
65
+ 3. `name` is unique and descriptive (it is the invocation key); `description`
66
+ one line — it appears in the unknown-name listing.
67
+ 4. Budget and parallelism declared, sized to the worst expected input.
68
+ 5. Verified locally: `run_workflow` with `source: "library"`, then re-run to
69
+ see journal hits skip completed steps.
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * The core spawn primitive (`spawnAgent`, `mapWithConcurrencyLimit`,
5
5
  * `createSpawnRegistry`, `abortAgent`, `getPiInvocation` + the registry/
6
- * options/result types) lives in the shared `@fyeeme/pi-subagent-core`
6
+ * options/result types) lives in the shared `@fyeeme/pi-subagents`
7
7
  * package — extracted from the duplicate copies that used to live here and
8
8
  * in pi-review. This module keeps the workflows-specific layer on top:
9
9
  * `skipAgent`/`retryAgent` (with `AbortReason` semantics) and the lifecycle
@@ -19,7 +19,7 @@
19
19
  */
20
20
  import type { AgentLifecycleListeners } from "../lifecycle.ts";
21
21
  import { notifyRetry, notifySkip } from "../lifecycle.ts";
22
- import type { AgentSpawnRegistry } from "@fyeeme/pi-subagent-core";
22
+ import type { AgentSpawnRegistry } from "@fyeeme/pi-subagents";
23
23
 
24
24
  // Re-export the core dispatch surface so existing importers of this module
25
25
  // (`../agent/dispatch.ts`) keep working unchanged.
@@ -29,7 +29,7 @@ export {
29
29
  getPiInvocation,
30
30
  mapWithConcurrencyLimit,
31
31
  spawnAgent,
32
- } from "@fyeeme/pi-subagent-core";
32
+ } from "@fyeeme/pi-subagents";
33
33
  export type {
34
34
  AgentAbortMap,
35
35
  AgentCallId,
@@ -37,7 +37,7 @@ export type {
37
37
  AgentSpawnRegistry,
38
38
  AgentSpawnResult,
39
39
  AgentUsage,
40
- } from "@fyeeme/pi-subagent-core";
40
+ } from "@fyeeme/pi-subagents";
41
41
 
42
42
  export type AbortReason = "user-skip" | "user-retry";
43
43
 
package/src/format.ts CHANGED
@@ -1,22 +1,13 @@
1
1
  /**
2
- * src/format.ts — shared display/parsing helpers (progress widget + /wf-inspect + runner).
2
+ * src/format.ts — shared parsing helper used by the runner.
3
3
  *
4
- * fmtTokens + ANSI color helpers: used by `buildProgressWidget` (index.ts) and
5
- * `WorkflowInspect` (src/inspect.ts) — one copy so the two UIs cannot drift.
6
4
  * stepIdOf: callId → step-id attribution, used by the runner (degraded-step
7
- * accounting) and the widget (grouping by step). Extracted from the verbatim
8
- * duplicates that used to live in each file.
5
+ * accounting, sibling abort scoping) and by the engine's dispatchOpts (the
6
+ * monitor `displayName` for the shared sub-agent UI). The former ANSI color
7
+ * helpers + fmtTokens were progress-widget/`/wf-inspect` rendering aids and
8
+ * were removed together with those surfaces (live progress is now rendered by
9
+ * the shared @fyeeme/pi-subagents extension).
9
10
  */
10
- export const GREEN = (s: string): string => `\x1b[32m${s}\x1b[0m`;
11
- export const RED = (s: string): string => `\x1b[31m${s}\x1b[0m`;
12
- export const YELLOW = (s: string): string => `\x1b[33m${s}\x1b[0m`;
13
- export const DIM = (s: string): string => `\x1b[2m${s}\x1b[0m`;
14
- export const CYAN = (s: string): string => `\x1b[36m${s}\x1b[0m`;
15
- export const BOLD = (s: string): string => `\x1b[1m${s}\x1b[0m`;
16
-
17
- export function fmtTokens(n: number): string {
18
- return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
19
- }
20
11
 
21
12
  /** Extract the step id from a callId of the form `${stepId}#${n}` (e.g.
22
13
  * "fan#2", "adv#produce", "cr#classify"). Falls back to the whole callId when
package/src/library.ts ADDED
@@ -0,0 +1,143 @@
1
+ /**
2
+ * src/library.ts — named workflow library: the persistence layer for
3
+ * workflows (PR3 of the sandwich refactor).
4
+ *
5
+ * Workflows are single-file `.ts` definitions (full step set including the
6
+ * TS-only steps like loop_until) discovered from:
7
+ *
8
+ * 1. project — nearest `.pi/workflows/lib/` walking up from cwd
9
+ * 2. bundled — this package's own `workflows/` directory (seed workflows)
10
+ *
11
+ * The project entry overrides a bundled entry of the same name. Discovery and
12
+ * loading go through the existing loader (jiti + the ast determinism guard),
13
+ * so a library workflow is rejected before execution when its source
14
+ * smuggles in non-deterministic APIs — the precondition cache-key resume
15
+ * relies on.
16
+ *
17
+ * The library directory is deliberately distinct from the journal output
18
+ * tree (`.pi/workflows/runs/<runId>/`): definitions and run state never
19
+ * collide. Adding a workflow = dropping a file; no code change.
20
+ */
21
+ import * as fs from "node:fs";
22
+ import * as path from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+ import { loadWorkflowModule } from "./loader.ts";
25
+ import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
26
+ import type { WorkflowDefinition } from "./types.ts";
27
+
28
+ /** This file lives at <pkg>/src/ → the bundled library is <pkg>/workflows. */
29
+ const PKG_ROOT = fs.realpathSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
30
+ const BUNDLED_LIB_DIR = path.join(PKG_ROOT, "workflows");
31
+
32
+ /** Project-level library directory, relative to the project root. */
33
+ const PROJECT_LIB_REL = path.join(CONFIG_DIR_NAME, "workflows", "lib");
34
+
35
+ /** One library entry, after loading the module. */
36
+ export interface LibraryEntry {
37
+ readonly name: string;
38
+ readonly description?: string;
39
+ readonly filePath: string;
40
+ readonly workflow: WorkflowDefinition;
41
+ }
42
+
43
+ function isDirectory(p: string): boolean {
44
+ try {
45
+ return fs.statSync(p).isDirectory();
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ /** Nearest project library dir walking up from cwd (like agent discovery). */
52
+ export function findProjectLibDir(cwd: string): string | null {
53
+ let dir = path.resolve(cwd);
54
+ for (;;) {
55
+ const candidate = path.join(dir, PROJECT_LIB_REL);
56
+ if (isDirectory(candidate)) return candidate;
57
+ const parent = path.dirname(dir);
58
+ if (parent === dir) return null;
59
+ dir = parent;
60
+ }
61
+ }
62
+
63
+ /** Library directories in priority order (later wins on name collision). */
64
+ function libDirs(cwd: string): string[] {
65
+ const dirs = [BUNDLED_LIB_DIR];
66
+ const project = findProjectLibDir(cwd);
67
+ if (project) dirs.push(project);
68
+ return dirs;
69
+ }
70
+
71
+ function isValidWorkflow(value: unknown): value is WorkflowDefinition {
72
+ return (
73
+ !!value &&
74
+ typeof value === "object" &&
75
+ typeof (value as WorkflowDefinition).name === "string" &&
76
+ Array.isArray((value as WorkflowDefinition).steps)
77
+ );
78
+ }
79
+
80
+ /** Load one workflow file: ast-guard + jiti import + shape validation. */
81
+ async function loadEntry(filePath: string): Promise<LibraryEntry | null> {
82
+ try {
83
+ const mod = (await loadWorkflowModule({ filePath })) as {
84
+ workflow?: unknown;
85
+ default?: unknown;
86
+ };
87
+ const wf = mod.workflow ?? mod.default;
88
+ if (!isValidWorkflow(wf)) return null;
89
+ return {
90
+ name: wf.name,
91
+ description: wf.description,
92
+ filePath,
93
+ workflow: wf,
94
+ };
95
+ } catch (err) {
96
+ // Fail-fast, deliberately: a library file that fails the determinism
97
+ // guard or errors at import must NOT silently disappear from the
98
+ // available list (a poisoned entry re-appearing would break cache-key
99
+ // resume). The error names the offending file; remove or fix the file
100
+ // to restore discovery. Shape-invalid files (below) are the exception
101
+ // — they are skipped silently as non-workflows.
102
+ throw new Error(`library workflow ${path.basename(filePath)} failed to load: ${err instanceof Error ? err.message : String(err)}`);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Discover the library: load every `.ts` workflow in the library dirs and
108
+ * index by workflow name (later dirs override earlier same-name entries).
109
+ * A file whose module import or the determinism guard check FAILS aborts the
110
+ * whole discovery with an error naming the file (fail-fast — see loadEntry);
111
+ * a file that imports cleanly but is not a valid workflow shape is skipped
112
+ * silently.
113
+ */
114
+ export async function discoverWorkflowLibrary(cwd: string): Promise<Map<string, LibraryEntry>> {
115
+ const byName = new Map<string, LibraryEntry>();
116
+ for (const dir of libDirs(cwd)) {
117
+ let entries: fs.Dirent[];
118
+ try {
119
+ entries = fs.readdirSync(dir, { withFileTypes: true });
120
+ } catch {
121
+ continue; // unreadable/missing dir — skip
122
+ }
123
+ for (const entry of entries) {
124
+ if (!entry.name.endsWith(".ts") || entry.name.startsWith(".")) continue;
125
+ if (!entry.isFile() && !entry.isSymbolicLink()) continue;
126
+ const loaded = await loadEntry(path.join(dir, entry.name));
127
+ if (loaded) byName.set(loaded.name, loaded);
128
+ }
129
+ }
130
+ return byName;
131
+ }
132
+
133
+ /** Resolve one workflow by name, or null when not in the library. */
134
+ export async function loadLibraryWorkflow(
135
+ name: string,
136
+ cwd: string,
137
+ ): Promise<LibraryEntry | null> {
138
+ // Fast path: only load the files that could define the name. Files export
139
+ // the workflow's name, so discovery is required either way — but skipping
140
+ // the project/bundled distinction keeps override semantics identical.
141
+ const lib = await discoverWorkflowLibrary(cwd);
142
+ return lib.get(name) ?? null;
143
+ }
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import * as fs from "node:fs";
16
16
  import * as path from "node:path";
17
+ import { CONFIG_DIR_NAME } from "@earendil-works/pi-coding-agent";
17
18
  import { BudgetPool } from "../budget/index.ts";
18
19
  import { Journal, type RunManifest } from "../cache/index.ts";
19
20
  import type { AgentLifecycleListeners } from "../lifecycle.ts";
@@ -81,7 +82,7 @@ export async function runWorkflow(opts: RunWorkflowOptions): Promise<RunResult>
81
82
  const budget = opts.budget ?? workflow.budget ?? {};
82
83
  const journalDir =
83
84
  opts.journalDir ??
84
- path.join(cwd, ".pi", "workflows", sanitizeWorkflowName(workflow.name));
85
+ path.join(cwd, CONFIG_DIR_NAME, "workflows", sanitizeWorkflowName(workflow.name));
85
86
 
86
87
  await fs.promises.mkdir(journalDir, { recursive: true });
87
88
  const journal = new Journal({ dir: journalDir });
@@ -985,6 +985,11 @@ function dispatchOpts(
985
985
  systemPrompt: spec.systemPrompt,
986
986
  signal,
987
987
  allowChildRecursion,
988
+ // UI display name for the shared sub-agent widget/FleetView (purely
989
+ // observational metadata consumed by the pi-subagents monitor): the
990
+ // step id ("fan", "adv"), so workflow rows are distinguishable from other
991
+ // agents. Cache hits never spawn, so they never appear — zero dispatch.
992
+ displayName: stepIdOf(callId),
988
993
  // C3: bridge the spawn's streamed deltas to the lifecycle onUpdate listener,
989
994
  // attributed to this callId. When no listener is registered, the subprocess
990
995
  // drops the deltas (its onUpdate stays undefined — same as before).
@@ -1013,6 +1018,12 @@ function usageStats(res: AgentSpawnResult, durationMs: number, ok: boolean): Ste
1013
1018
  durationMs,
1014
1019
  agents: 1,
1015
1020
  failures: ok ? 0 : 1,
1021
+ usage: {
1022
+ input: res.usage.input,
1023
+ output: res.usage.output,
1024
+ cacheRead: res.usage.cacheRead,
1025
+ cacheWrite: res.usage.cacheWrite,
1026
+ },
1016
1027
  };
1017
1028
  }
1018
1029
 
@@ -1023,6 +1034,20 @@ function addStats(a: StepStats, b: StepStats): StepStats {
1023
1034
  durationMs: a.durationMs + b.durationMs,
1024
1035
  agents: a.agents + b.agents,
1025
1036
  failures: a.failures + b.failures,
1037
+ usage: mergeUsage(a.usage, b.usage),
1038
+ };
1039
+ }
1040
+
1041
+ function mergeUsage(
1042
+ a: StepStats["usage"],
1043
+ b: StepStats["usage"],
1044
+ ): StepStats["usage"] {
1045
+ if (!a && !b) return undefined;
1046
+ return {
1047
+ input: (a?.input ?? 0) + (b?.input ?? 0),
1048
+ output: (a?.output ?? 0) + (b?.output ?? 0),
1049
+ cacheRead: (a?.cacheRead ?? 0) + (b?.cacheRead ?? 0),
1050
+ cacheWrite: (a?.cacheWrite ?? 0) + (b?.cacheWrite ?? 0),
1026
1051
  };
1027
1052
  }
1028
1053
 
@@ -1033,14 +1058,16 @@ export function aggregateStats(stats: readonly StepStats[], durationMs: number):
1033
1058
  let agents = 0;
1034
1059
  let failures = 0;
1035
1060
  let dur = 0;
1061
+ let usage: StepStats["usage"];
1036
1062
  for (const s of stats) {
1037
1063
  tokens += s.tokens;
1038
1064
  cost += s.cost;
1039
1065
  agents += s.agents;
1040
1066
  failures += s.failures;
1041
1067
  dur += s.durationMs;
1068
+ usage = mergeUsage(usage, s.usage);
1042
1069
  }
1043
- return { tokens, cost, durationMs: durationMs > 0 ? durationMs : dur, agents, failures };
1070
+ return { tokens, cost, durationMs: durationMs > 0 ? durationMs : dur, agents, failures, usage };
1044
1071
  }
1045
1072
 
1046
1073
  function withDuration(stats: StepStats, start: number): StepStats {
package/src/types.ts CHANGED
@@ -55,6 +55,15 @@ export interface StepStats {
55
55
  readonly durationMs: number;
56
56
  readonly agents: number;
57
57
  readonly failures: number;
58
+ /** Nested-LLM usage split, populated for agent-dispatch steps and surfaced
59
+ * as `usage` on the run_workflow tool result (pi usage accounting). Absent
60
+ * for zero-dispatch steps (code/log) and journals written before this field. */
61
+ readonly usage?: {
62
+ readonly input: number;
63
+ readonly output: number;
64
+ readonly cacheRead: number;
65
+ readonly cacheWrite: number;
66
+ };
58
67
  }
59
68
 
60
69
  export interface StepResult<T = unknown> {
@@ -247,23 +256,11 @@ export interface StepRetry {
247
256
  * spec revision; add `retryStage` back with implementation when ready. */
248
257
  }
249
258
 
250
- /** A phase groups related steps for UI progress-tree rendering.
251
- * Steps not assigned to any phase render under an implicit default group. */
252
- export interface PhaseDefinition {
253
- readonly title: string;
254
- readonly detail?: string;
255
- readonly stepIds: readonly string[];
256
- /** Optional model override for all agents in this phase. */
257
- readonly model?: string;
258
- }
259
-
260
259
  export interface WorkflowDefinition {
261
260
  readonly name: string;
262
261
  readonly description?: string;
263
262
  readonly steps: readonly StepDefinition[];
264
263
  readonly budget?: Budget;
265
- /** Optional phase groupings for progress-tree UI rendering. */
266
- readonly phases?: readonly PhaseDefinition[];
267
264
  }
268
265
 
269
266
  /** Typed identity helper: gives a workflow literal full union checking. */
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Seed library workflow: review-extension — the extensions-submodule review
3
+ * fan-out distilled from this repository's review-local-extensions-fanout
4
+ * pipeline: scope the changed packages first, then one reviewer per changed
5
+ * extension directory (parallelism 3), then merge.
6
+ *
7
+ * Input: newline-separated list of changed paths under packages/extensions
8
+ * (or a single path). Prompts use prompt functions (ctx.input /
9
+ * ctx.step(id)) — the TS API does not substitute {{...}} tokens.
10
+ * Runtime-single-file: `import type` is erased at load (see
11
+ * review-local-diff.ts).
12
+ */
13
+ import type { WorkflowDefinition } from "../src/index.ts";
14
+
15
+ export const workflow: WorkflowDefinition = {
16
+ name: "review-extension",
17
+ description: "Review each changed extension package: one reviewer per package (parallelism 3), merged findings",
18
+ budget: { maxAgents: 9, maxTokens: 2_000_000 },
19
+ steps: [
20
+ {
21
+ id: "scope",
22
+ type: "agent",
23
+ prompt: (ctx) =>
24
+ "The input lists changed paths under packages/extensions. Reduce it to the set of " +
25
+ "distinct top-level extension directories (e.g. \"pi-review\", \"pi-subagents\"). " +
26
+ "Return ONLY the directory names, one per line, no commentary.\n\n" +
27
+ String(ctx.input),
28
+ },
29
+ {
30
+ id: "reviewers",
31
+ type: "fan_out",
32
+ over: (ctx) =>
33
+ String(ctx.step("scope").results)
34
+ .split("\n")
35
+ .map((l) => l.trim())
36
+ .filter(Boolean),
37
+ parallelism: 3,
38
+ agent: (pkg, _index, ctx) => ({
39
+ prompt:
40
+ `Review the uncommitted changes in packages/extensions/${pkg} (run ` +
41
+ `\`git -C packages/extensions/${pkg} diff\` yourself; read the touched files for ` +
42
+ `context). Report findings as \`file:line\` — one-line summary — the concrete cost. ` +
43
+ `Cover correctness, reuse, simplification, efficiency, altitude. Report only.\n\n` +
44
+ `Changed paths:\n${ctx.input}`,
45
+ }),
46
+ merge: (results) => results.join("\n---\n"),
47
+ },
48
+ ],
49
+ };
50
+
51
+ export default workflow;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Seed library workflow: review-local-diff — the local-diff review pipeline
3
+ * actually run in this repository (distilled from the .pi/workflows journals
4
+ * of review-local-changes / review-local-extensions-fanout runs).
5
+ *
6
+ * Input: the unified diff to review (run input). Fan out one finder per
7
+ * angle (parallelism 4), adversarially verify a merged candidate list, and
8
+ * return the verified findings. Budget and parallelism are declared here as
9
+ * data — the engine enforces them.
10
+ *
11
+ * Prompts reference run context via prompt FUNCTIONS (ctx.input /
12
+ * ctx.step(id)) — the {{...}} template tokens are the inline-JSON tool
13
+ * surface only; the TS API never substitutes them in plain strings.
14
+ *
15
+ * Runtime-single-file: `import type` is erased at load, so nothing outside
16
+ * this file executes and the ast determinism guard's entry-only scan covers
17
+ * everything that runs (a VALUE import from another file would not be
18
+ * scanned — keep imports type-only).
19
+ */
20
+ import type { WorkflowDefinition } from "../src/index.ts";
21
+
22
+ const ANGLES = ["correctness", "reuse", "simplification", "efficiency", "altitude"] as const;
23
+
24
+ export const workflow: WorkflowDefinition = {
25
+ name: "review-local-diff",
26
+ description: "Fan out diff-scoped finders, adversarially verify the merged candidates, return verified findings",
27
+ budget: { maxAgents: 12, maxTokens: 2_000_000 },
28
+ steps: [
29
+ {
30
+ id: "finders",
31
+ type: "fan_out",
32
+ over: () => [...ANGLES],
33
+ parallelism: 4,
34
+ agent: (angle, _index, ctx) => ({
35
+ prompt:
36
+ `You are a code-review finder for the "${angle}" angle. Review the diff below. ` +
37
+ `Report each finding as \`file:line\` — one-line summary — the concrete cost ` +
38
+ `(for correctness: the input/state that triggers it → wrong output). Report only; ` +
39
+ `an empty list is a valid answer.\n\nDiff:\n${ctx.input}`,
40
+ }),
41
+ merge: (results) => results.join("\n---\n"),
42
+ },
43
+ {
44
+ id: "verify",
45
+ type: "adversarial",
46
+ produce: {
47
+ prompt: (ctx) =>
48
+ "Dedup and merge these code-review findings into a numbered candidate list " +
49
+ "(same defect + same location + same reason → keep one; different reasons for " +
50
+ "the same line are NOT duplicates — keep both). Return only the numbered list.\n\n" +
51
+ String(ctx.step("finders").results),
52
+ },
53
+ rubric: [
54
+ "finding is concretely actionable (file:line present)",
55
+ "the failure scenario or cost is stated and realistic",
56
+ "not a duplicate of another kept finding",
57
+ ],
58
+ judges: 3,
59
+ minPass: 2,
60
+ },
61
+ ],
62
+ };
63
+
64
+ export default workflow;