@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/README.md +21 -8
- package/README.zh-CN.md +12 -4
- package/index.ts +139 -328
- package/package.json +17 -8
- package/prompts/implement-and-review.md +29 -0
- package/prompts/wf-review.md +30 -0
- package/sessions/spawn.ts +1 -1
- package/skills/workflow-author/SKILL.md +69 -0
- package/src/agent/dispatch.ts +4 -4
- package/src/format.ts +6 -15
- package/src/library.ts +143 -0
- package/src/runner/index.ts +2 -1
- package/src/runner/stage-executor.ts +28 -1
- package/src/types.ts +9 -12
- package/workflows/review-extension.ts +51 -0
- package/workflows/review-local-diff.ts +64 -0
- package/src/inspect.ts +0 -237
- package/src/ui-groups.ts +0 -43
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fyeeme/pi-dynamic-workflows",
|
|
3
|
-
"version": "0.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-
|
|
48
|
+
"@fyeeme/pi-subagents": "2.1.1"
|
|
40
49
|
},
|
|
41
50
|
"peerDependencies": {
|
|
42
|
-
"@earendil-works/pi-ai": ">=0.84.
|
|
43
|
-
"@earendil-works/pi-coding-agent": ">=0.84.
|
|
44
|
-
"@earendil-works/pi-tui": ">=0.84.
|
|
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.
|
|
51
|
-
"@earendil-works/pi-coding-agent": "0.84.
|
|
52
|
-
"@earendil-works/pi-tui": "0.84.
|
|
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-
|
|
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.
|
package/src/agent/dispatch.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
8
|
-
*
|
|
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
|
+
}
|
package/src/runner/index.ts
CHANGED
|
@@ -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,
|
|
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;
|