@signalridge/pi-workflows 1.2.1 → 1.3.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +142 -17
  3. package/package.json +14 -8
  4. package/skills/workflow-authoring/SKILL.md +30 -0
  5. package/skills/workflow-authoring/examples/adversarial-verification.js +63 -0
  6. package/skills/workflow-authoring/examples/bounded-semantic-retry.js +56 -0
  7. package/skills/workflow-authoring/examples/classify-and-act.js +67 -0
  8. package/skills/workflow-authoring/examples/defensive-json-parsing.js +63 -0
  9. package/skills/workflow-authoring/examples/fan-out-and-synthesize.js +47 -0
  10. package/skills/workflow-authoring/examples/generate-and-filter.js +68 -0
  11. package/skills/workflow-authoring/examples/loop-until-done.js +68 -0
  12. package/skills/workflow-authoring/examples/phased-budgets.js +75 -0
  13. package/skills/workflow-authoring/examples/saved-nested-workflows.js +44 -0
  14. package/skills/workflow-authoring/examples/structured-output.js +37 -0
  15. package/skills/workflow-authoring/examples/tournament.js +81 -0
  16. package/skills/workflow-authoring/examples/validated-gate.js +63 -0
  17. package/skills/workflow-authoring/references/capabilities.md +44 -0
  18. package/skills/workflow-authoring/references/capability-details.md +294 -0
  19. package/skills/workflow-authoring/references/common-helpers.md +4 -0
  20. package/skills/workflow-authoring/references/debugging.md +29 -0
  21. package/skills/workflow-authoring/references/focused-recipes.md +12 -0
  22. package/skills/workflow-authoring/references/helpers.md +9 -0
  23. package/skills/workflow-authoring/references/lifecycle.md +37 -0
  24. package/skills/workflow-authoring/references/pattern-selection.md +15 -0
  25. package/skills/workflow-authoring/references/quality-helpers.md +8 -0
  26. package/skills/workflow-authoring/references/registry-ownership.md +17 -0
  27. package/skills/workflow-authoring/references/retry-helper.md +5 -0
  28. package/skills/workflow-authoring/references/review.md +43 -0
  29. package/skills/workflow-authoring/references/runtime.md +29 -0
  30. package/skills/workflow-authoring/references/specialized-helpers.md +19 -0
  31. package/skills/workflow-authoring/references/versions.md +13 -0
  32. package/skills/workflow-patterns/SKILL.md +51 -0
  33. package/src/arming.ts +69 -0
  34. package/src/builtins.ts +331 -0
  35. package/src/capabilities.ts +318 -0
  36. package/src/code-review-scope.ts +144 -0
  37. package/src/engine.ts +1158 -2561
  38. package/src/errors.ts +136 -0
  39. package/src/index.ts +750 -103
  40. package/src/journal.ts +594 -1001
  41. package/src/rpc-client.ts +95 -24
  42. package/src/runtime.ts +1708 -0
  43. package/src/saved-workflows.ts +121 -0
  44. package/src/shared-store.ts +132 -0
  45. package/src/ui.ts +77 -40
  46. package/src/workflow-settings.ts +100 -0
  47. package/src/schema.ts +0 -215
  48. package/src/state-machine.ts +0 -79
  49. package/src/synthesis.ts +0 -140
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.1
4
+ ### Patch Changes
5
+
6
+ - 7d2c6c9: Keep generated workflow capability references synchronized when Changesets versions the package.
7
+
8
+ ## 1.3.0
9
+ ### Minor Changes
10
+
11
+ - bae8689: Complete the managed workflow parity and hardening pass. Workflows now forward and validate exact model/thinking, toolset, denylist, thread, and per-call worktree intent while pi-subagents remains the policy and lifecycle owner. Script resume accepts edited revisions, workflow removal is durable, stale lifecycle events are rejected, provider-limit errors pause runs, and the live progress/effort/trigger settings are persisted and bounded. Run identity is durable before dispatch, pause/dispose stop owned agents and reject waiters, and every background start/resume delivers its result. The protocol adds only optional fields and capability metadata, preserving older policy-free managed-spawn consumers; workflows fail closed with a diagnostic when the peer does not advertise managed policy support.
12
+ - b6cf242: Rewrite `pi-workflows` from a declarative DAG engine to a JavaScript orchestration runtime.
13
+
14
+ - **Scripts, not DAGs**: a workflow is now a raw JavaScript module executed in a determinism-guarded `node:vm` realm. The script declares `export const meta = { name, description, phases }` first and uses runtime globals `agent()`, `parallel()`, `pipeline()`, `workflow()`, `verify()`, `judgePanel()`, `loopUntilDry()`, `completenessCheck()`, `retry()`, `gate()`, `checkpoint()`, `phase()`, `log()`, `args`, `cwd`, restricted `process`, and `budget`. The `workflow` tool takes `script`/`name`/`args`/`background`/`maxAgents`/`concurrency`/`agentRetries`/`tokenBudget`/`agentTimeoutMs`/`resumeFromRunId` instead of `tasks`/`phases`/`synthesis`.
15
+ - **Determinism guardrail**: `Date.now()`, `Math.random()`, and no-argument `new Date()` are unavailable (regex precheck plus in-realm stubs); `new Date(arg)`, `Date.UTC`, and `Date.parse` still work. The meta contract rejects spread, computed keys, methods, template interpolation, and `__proto__`/`constructor`/`prototype`.
16
+ - **Resume with cache replay**: an edited script replays the longest unchanged prefix from the journal (zero tokens for replayed calls) and runs live from the first changed or inserted call. Journal schema v2→v3: `run_created` carries the script text, hash, and meta; call identity is the lexical call index; each completed `agent()`/`checkpoint()` call journals `{ index, runId, callHash, result, storeDelta }`. Schema-v2 (declarative) journals are quarantined rather than replayed.
17
+ - **Spawn-key generation rotation (A4)**: live dispatches use `${runId}/call-${callIndex}/attempt-${generation}`; a resume of an edited call rotates the generation so pi-subagents never raises a fingerprint conflict. Per-call store deltas replay additively in call order.
18
+ - **Command surface expanded to 11+**: `/workflows` now supports `run`, `status`, `watch`, `stop`, `pause`, `resume`, `rm`, and `save <name> [runId]` subcommands; new `/workflows-models`, `/workflows-trigger` (persisted keyword arming via an input hook), `/workflows-progress`, `/effort`, `/ultracode`, the five built-in workflow commands (`/deep-research`, `/adversarial-review`, `/code-review`, `/multi-perspective`, `/codebase-audit`), and one command per saved workflow registered at `session_start` with name-clash checks. `stop` and `rm` are destructive and take no implicit target — a bare `/workflows rm` asks for a run id rather than acting on the first listed run; the reversible `pause` and `resume` still default to it. Each builtin command routes its free text to the `args` key its own script reads (`question`, `task`, `topic`, `scope`) rather than a generic `prompt` no script consumes, and a builtin whose name another extension already registered is skipped rather than clobbered, since Pi cannot unregister commands.
19
+ - **`/code-review` resolves its own diff**: no argument auto-scopes `git diff HEAD` with generated and vendored artifacts excluded; a numeric argument reads `gh pr diff <n>`; an argument containing `..` reads that revision range; anything else is a path. It degrades to the unscoped `git diff HEAD` when the auto scope fails or when the exclusions emptied a non-empty diff, reports a genuinely clean tree as clean rather than as a degradation, and caps the diff before handing it to the review agents. Commands run as argv arrays, never shell strings.
20
+ - **Keyword arming authorizes the tool rather than opening UI**: typing the bounded word `workflow`/`workflows` annotates the message to tell the model the `workflow` tool is available for this turn, with an explicit escape hatch so a conversational or trivial turn is still answered directly. Matching uses token boundaries that treat identifier, path, and flag punctuation as part of the token, so `myworkflow`, `workflow_name`, `WorkflowEngine`, `--workflow-id`, `src/workflow-editor.ts`, `workflow.ts`, and the `/workflows` command never arm — while a sentence-ending dot ("run a workflow.") still does. `/workflows-trigger` takes `set <word>` (matched exactly; only the default word also matches its plural), `off`, and `on`. Extension-submitted text never arms, and an already-annotated message cannot stack a second directive.
21
+ - **Saved workflows (A7)**: project scope takes precedence over user scope, persisted under the package-owned workflow directories with atomic writes and backups; nested `workflow(savedName, args)` calls are one level deep.
22
+ - **UI (A10)**: the navigator renders runs → phases → calls → detail; a live widget shows active runs and clears when idle; background-run results deliver through a session-bound endpoint, falling back to a durable marker when no endpoint is bound yet. Child-agent output still passes through `safe-text.ts` before truncation.
23
+ - **Capability contract (A11)**: a machine-readable declaration is the single source of truth for the runtime surface; the runtime validates its bindings against it at run start, so docs and reality cannot drift.
24
+
25
+ Intentional adaptations from upstream (documented in the README): no host-side web tools (agents reach the web via configured tools / MCP), no duplicate agent registry (agentType resolves in pi-subagents), and no second model-tier catalogue (tier routing uses pi-subagents' `workflow.tiers`). The managed protocol now carries optional exact model/thinking, toolset, denylist, thread, and worktree requests while pi-subagents retains final policy ownership.
26
+
27
+ ### Patch Changes
28
+
29
+ - b6cf242: Peer dependency ranges now name the versions actually validated against, so an untested host combination fails at install time instead of silently at runtime: `@earendil-works/pi-coding-agent`, `pi-ai`, `pi-tui`, and `pi-agent-core` move from `"*"` to `^0.84.0`, and `typebox` from `"*"` to `^1.3.11`.
30
+
31
+ Shared dependencies now carry ONE declared range across every package that uses them, and `bun run check:shared-deps` keeps it that way.
32
+
33
+ `@narumitw/pi-tui-kit` was declared at three disjoint floors — `^0.54.0`, `^0.51.0`, and `^0.49.1` across nine packages — and the lockfile duly resolved three copies (0.54.0, 0.51.0, 0.49.3) installed side by side. For a shared rendering surface drawing into one terminal inside one host process, that means a theme rendering one way in one extension and another way in the next, with nothing failing at install to say so. All nine now declare `^0.54.0` and the install resolves a single copy. `@sinclair/typebox` likewise converges on `^0.34.50`.
34
+
35
+ The new check covers `dependencies` and `peerDependencies` and compares range strings rather than their semantics: two ranges that merely overlap are still a finding, because the goal is one intentional answer per dependency rather than an accidental intersection. `devDependencies` are deliberately out of scope — a build tool is not a shared surface, and `pi-subagents` intentionally carries its own toolchain. `docs/package-boundaries.md` documents the Kit as a shared surface for the first time.
36
+ - Updated dependencies [bae8689]
37
+ - @signalridge/pi-subagents-protocol@1.3.0
38
+
3
39
  ## 1.2.1
4
40
  ### Patch Changes
5
41
 
package/README.md CHANGED
@@ -1,10 +1,21 @@
1
1
  # @signalridge/pi-workflows
2
2
 
3
- Declarative DAG orchestration for Pi. The `workflow` tool validates phases, task dependencies, cycles, bounded synthesis input, and optional `small`/`medium`/`large` semantic tiers, then submits ready tasks through the additive `subagents:rpc:spawn-managed` event-bus protocol. `pi-subagents` remains the only owner of model, thinking, queue, concurrency, tools, skills, cwd, isolation, retry, and session policy.
3
+ JavaScript orchestration for Pi. A workflow is a raw JavaScript module executed
4
+ in a determinism-guarded `node:vm` realm; the runtime supplies `agent()`,
5
+ `parallel()`, `pipeline()`, `workflow()`, quality helpers (`verify`,
6
+ `judgePanel`, `loopUntilDry`, `completenessCheck`, `retry`, `gate`), human
7
+ `checkpoint()`, `phase()`, `log()`, `args`, `cwd`, restricted `process`, and
8
+ `budget`. Every live `agent()` call is dispatched through the additive
9
+ `subagents:rpc:spawn-managed` event-bus protocol — `pi-subagents` remains the
10
+ only owner of model, thinking, queue, concurrency, tools, skills, cwd,
11
+ isolation, retry, and session policy.
4
12
 
5
13
  ## Install
6
14
 
7
- `@signalridge/pi-workflows` requires `@signalridge/pi-subagents` `>=1.0.0` with protocol v3 managed-spawn support. They are separate Pi packages: the peer dependency documents the requirement but intentionally does not auto-load or duplicate the subagent extension.
15
+ `@signalridge/pi-workflows` requires `@signalridge/pi-subagents` `>=1.5.0`
16
+ with protocol v3 managed-spawn, workflow-tier, and managed-policy support. They are separate Pi packages: the peer
17
+ dependency documents the requirement but intentionally does not auto-load or
18
+ duplicate the subagent extension.
8
19
 
9
20
  ```bash
10
21
  pi install npm:@signalridge/pi-subagents
@@ -13,29 +24,143 @@ pi install npm:@signalridge/pi-workflows
13
24
 
14
25
  ## Use from this checkout
15
26
 
16
- From the repository root, load both local package directories once:
27
+ From the repository root, load both local package directories once. Disable
28
+ extension discovery so the npm-installed copies do not also load — an npm copy
29
+ of one alongside a local checkout of the other loads both, and the duplicate
30
+ registers its tools twice:
17
31
 
18
32
  ```bash
19
- pi -e ./packages/pi-subagents
20
- pi -e ./packages/pi-workflows
33
+ pi -ne -e ./packages/pi-subagents -e ./packages/pi-workflows
21
34
  ```
22
35
 
23
- Pi activates each package from its own `pi.extensions` manifest. Install `pi-workflows` and `pi-subagents` from the same source — an npm copy of one alongside a local checkout of the other loads both, and the duplicate registers its tools twice.
36
+ `-ne` skips the installed extension catalogue while keeping explicit `-e`
37
+ paths; without it, first remove the installed copies (`pi remove
38
+ @signalridge/pi-subagents @signalridge/pi-workflows`) or accept duplicate tool
39
+ registration.
24
40
 
25
- At `session_start`, this extension sends `subagents:rpc:ping` and verifies protocol version 3 plus the `managedSpawn`, `lifecycleOwner`, owner-scoped `ownedStop`, `ownedQuiescence`, and `workflowTiers` capabilities. The package floor and capability check prevent older v3 peers from receiving tiered requests they cannot validate. If the fork is missing, old, or duplicated incorrectly, workflow execution returns a diagnostic naming the required packages instead of dispatching agents. The legacy unowned stop RPC remains available to older external callers, but workflow cleanup never falls back to it.
41
+ Pi activates each package from its own `pi.extensions` manifest.
26
42
 
27
- ## Tools and command
43
+ ## Writing a workflow
28
44
 
29
- - `workflow`: run a foreground or background workflow. `depends_on` is a scheduling barrier only. A task may additionally declare `inputs`, a duplicate-free subset of its own `depends_on`, to have those dependencies' journaled results appended to its dispatch prompt: each result is capped at 6,000 characters, the appended section at 24,000, and the whole prompt at the protocol's 100,000-character ceiling. When the task prompt leaves less than 512 characters of headroom, nothing is appended and the prompt dispatches unchanged. Omitting `inputs` dispatches exactly the authored prompt.
30
- - `workflow_control`: `list`, `get`, `pause`, `resume`, or `stop` a run. `get` returns bounded task and synthesis details including status, agent ID, compactions, and result/error previews.
31
- - `/workflows`: TUI run list/detail navigator. Live agent conversation navigation remains in pi-subagents FleetView.
45
+ A workflow script starts with the only legal export — a literal-only meta
46
+ contract — and then runs inside an async function:
32
47
 
33
- Composed input sections are a pure function of the journaled prefix — declared `inputs` order, journaled result fields only — so a replay reproduces the same prompt and the managed spawn fingerprint stays stable across a branch rewind.
48
+ ```js
49
+ export const meta = {
50
+ name: "example",
51
+ description: "Fan out three checks and synthesize",
52
+ phases: [{ title: "scan" }, { title: "synthesize" }],
53
+ };
34
54
 
35
- State is persisted as `pi.appendEntry("pi-workflows:journal", ...)` custom entries, which do not enter model context. Results and synthesis instructions are capped before journaling or dispatch. Interrupted runs reset unreconciled dispatches to a recoverable state and reuse their managed spawn keys; completed task facts are not dispatched again. A task failure stops remaining owned agents, journals their terminal callbacks, blocks dependents, and settles the workflow as failed.
36
- Restore rotation is journaled atomically per run (`run_recovery`) so a failed append leaves no partial generation; older multi-entry recovery prefixes remain replayable and are migrated with independent `attempt_recovery` entries.
37
- Each atomic recovery also carries a deterministic bounded `recoveryId` derived from its branch/run and attempt rotations. Replay treats an exact normalized duplicate as a no-op, quarantines same-ID conflicts, and accepts older schema-v2 atomic entries without an ID only through deterministic legacy normalization.
38
- Managed lifecycle calls are owner-scoped to the exact `{ runId, nodeId, attemptId }` generation. Branch replacement stops/quiesces owned agents and tracks in-flight dispatch RPCs; if a peer cannot settle, the run is quarantined conservatively and late lifecycle or spawn replies are ignored. Foreground `AbortSignal`s cancel only the caller's wait. The session signal may cancel an outstanding RPC wait during shutdown, but neither signal is used to cancel a managed child; explicit stop/quiescence owns child cancellation.
55
+ phase("scan");
56
+ const findings = await parallel([
57
+ () => agent("Check for cross-package imports", { label: "imports", tier: "small" }),
58
+ () => agent("Check for secrets", { label: "secrets", tier: "small" }),
59
+ () => agent("Check for dead code", { label: "dead-code", tier: "small" }),
60
+ ]);
39
61
 
62
+ phase("synthesize");
63
+ const report = await agent("Summarize the findings: " + JSON.stringify(findings), {
64
+ label: "report",
65
+ });
66
+ return report;
67
+ ```
68
+
69
+ The meta statement must be the first statement and use only literals — spread,
70
+ computed keys, methods, template interpolation, and the reserved keys
71
+ `__proto__`/`constructor`/`prototype` are rejected. `Date.now()`,
72
+ `Math.random()`, and no-argument `new Date()` are unavailable (a regex precheck
73
+ plus in-realm stubs); pass timestamps and randomness through `args`. The vm is
74
+ a determinism guardrail, not a security boundary — scripts are generated by the
75
+ user's own LLM and run on the user's own machine.
76
+
77
+ ### Runtime globals
78
+
79
+ - `agent(prompt, { label, phase, schema, model, tier, isolation, thread, agentType, toolset, excludeTools, timeoutMs, retries })`
80
+ — dispatch one subagent through the managed spawn protocol. Returns text, a
81
+ schema-validated value, or recoverable `null` after retries. `schema` is a
82
+ plain JSON Schema; the reply is parsed and validated client-side with bounded
83
+ repair across attempts, and exhaustion throws `SCHEMA_NONCOMPLIANCE`.
84
+ Explicit `model` and `isolation: "worktree"` are resolved and enforced by pi-subagents; `tier` remains the provider-neutral route.
85
+ `tier`; see "Model routing"). `thread` chains sequential turns within a named
86
+ conversation thread.
87
+ - `parallel(thunks)` — run thunks concurrently, preserve input order. Recoverable
88
+ thunk failures become `null`; non-recoverable failures (token budget, agent
89
+ limit) halt the run. Fan-out that breaches `maxAgents` cancels only its own
90
+ batch.
91
+ - `pipeline(items, ...stages)` — items run concurrently, stages per item run
92
+ sequentially with `(previousValue, originalItem, index)`.
93
+ - `workflow(savedName, childArgs?)` — run a saved workflow inline; one nested
94
+ level, sharing the limiter, counters, token accounting, and store.
95
+ - `verify(item, { reviewers, threshold, lens })`, `judgePanel(attempts,
96
+ { judges, rubric })`, `loopUntilDry({ round, key, consecutiveEmpty,
97
+ maxRounds })`, `completenessCheck(taskArgs, results)`, `retry(thunk,
98
+ { attempts, until })`, `gate(thunk, validator, { attempts })` — quality
99
+ helpers built purely on `agent()`/`parallel()` so call sequencing stays
100
+ stable and resume keeps working.
101
+ - `checkpoint(prompt, { default, headless, kind, choices, timeoutMs })` —
102
+ journaled human confirmation. Foreground runs thread the real UI; background
103
+ runs are headless and take the declared `default`.
104
+ - `phase(title, { budget })`, `log(message)`, `args`, `cwd`, `process.cwd()`,
105
+ `budget` (`{ total, spent(), remaining() }`).
106
+
107
+ ### Resume
108
+
109
+ A script edit replays the longest unchanged prefix from the journal and runs
110
+ live from the first changed or inserted `agent()`/`checkpoint()` call. Pass
111
+ `resumeFromRunId` with the edited script; unchanged calls report zero tokens,
112
+ and edited calls rotate their managed spawn key generation so pi-subagents
113
+ never raises a fingerprint conflict.
114
+
115
+ ## Tools and commands
116
+
117
+ - `workflow`: run a script (`script`) or a saved/built-in workflow (`name`),
118
+ with `args`, `background` (default true), `maxAgents`, `concurrency`,
119
+ `agentRetries`, `tokenBudget`, `agentTimeoutMs`, and `resumeFromRunId`.
120
+ - `workflow_control`: `list`, `get`, `pause`, `resume`, `stop`, or `rm` a run; `rm` writes a durable removal tombstone.
121
+ - `/workflows`: TUI navigator plus `run`, `status`, `watch`, `stop`, `pause`,
122
+ `resume`, `rm`, and `save <name> [runId]` subcommands.
123
+ - `/workflows-models`: points at pi-subagents' `/agents → Model tiers` — there
124
+ is deliberately no second tier configuration file.
125
+ - `/effort` and `/ultracode`: standing `off|high|ultra` effort guidance that auto-arms substantive messages.
126
+ - `/workflows-progress`: `compact|detailed|status|max <N>` controls the persisted live panel.
127
+ - `/deep-research`, `/adversarial-review`, `/code-review`,
128
+ `/multi-perspective`, `/codebase-audit`: the five built-in workflows. A
129
+ saved workflow with the same name takes precedence.
130
+ - `/workflows-trigger`: `set <keyword>`, `off`, or `on`. See keyword arming below.
131
+ - `/<saved-name>`: every saved workflow registers its own slash command at
132
+ `session_start` (Pi cannot unregister commands, so registration happens there
133
+ rather than at factory time, and existing commands are never overwritten).
134
+
135
+ ### Keyword arming
136
+
137
+ Typing the bounded word `workflow` or `workflows` in an ordinary message counts
138
+ as an explicit opt-in to multi-agent orchestration: the message is annotated to
139
+ tell the model the `workflow` tool is authorized for this turn.
140
+
141
+ Arming **authorizes; it does not compel**. The annotation says the model may run
142
+ a workflow and may still decline, so "how do workflows work?" stays an ordinary
143
+ question with an ordinary answer. Nothing is swallowed and no UI opens.
144
+
145
+ Only a standalone word arms. A workflow is also a thing people write code about,
146
+ so none of `myworkflow`, `workflow_name`, `WorkflowEngine`, `--workflow-id`,
147
+ `src/workflow-editor.ts`, `workflow.ts`, or the `/workflows` command matches. A
148
+ sentence-ending dot is not a filename dot: "please run a workflow." arms, and
149
+ `workflow.json` does not. Set a different word with `/workflows-trigger set
150
+ <word>` (matched exactly — only the default word also matches its plural) or
151
+ turn it off entirely with `/workflows-trigger off`.
152
+
153
+ Saved workflows use project scope first and a user scope fallback; writes are atomic and names are path-safe.
154
+ written atomically with a backup.
155
+
156
+ ## Model routing
157
+
158
+ The managed protocol carries semantic tiers plus optional exact model/thinking, toolset, denylist, thread, and worktree requests. pi-subagents resolves model references, validates scope, creates/cleans worktrees, and owns tool/session policy. Workflows fail closed with a diagnostic when the peer does not advertise managed-policy support; explicit selectors otherwise fail closed when unavailable, and workflows never bypass the companion's policy.
159
+
160
+ ## Intentional adaptations from upstream
161
+
162
+ - **No host-side web tools.** Agents reach the web through configured pi-web-access/MCP tools or an agent type's tool policy.
163
+ - **Agent registry and tier catalogue ownership.** `agentType` and `small|medium|large` profiles resolve in pi-subagents, avoiding a second registry/config schema.
164
+ - **Schema validation is client-side.** The managed request does not assume a structured-output tool; the runtime asks for JSON, parses, validates (including `additionalProperties`), and repairs across bounded attempts.
40
165
 
41
- Interrupted-attempt recovery is intentionally bounded to three generations per task, including synthesis. Normal interruptions retry with a fresh managed attempt; if the same node is interrupted repeatedly beyond that bound, the workflow fails safely instead of creating an unbounded outer retry loop.
166
+ State is persisted as `pi.appendEntry("pi-workflows:journal", ...)` custom entries (schema v3), including script revisions and durable removals. Interrupted/provider-limited runs replay from the journal; schema-v2 declarative journals are quarantined rather than replayed. Foreground `AbortSignal`s stop owned children through explicit owner-scoped stop/quiescence, and dispose/branch replacement reject stale waiters.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@signalridge/pi-workflows",
3
- "version": "1.2.1",
4
- "description": "Declarative DAG workflows backed by pi-subagents managed spawning.",
3
+ "version": "1.3.1",
4
+ "description": "Deterministic JavaScript orchestration workflows backed by pi-subagents managed spawning.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "private": false,
@@ -15,17 +15,22 @@
15
15
  "lifecycle": "stable"
16
16
  },
17
17
  "peerDependencies": {
18
- "@earendil-works/pi-coding-agent": "*",
19
- "@earendil-works/pi-tui": "*",
20
- "@signalridge/pi-subagents": ">=1.0.0"
18
+ "@earendil-works/pi-coding-agent": "^0.84.0",
19
+ "@earendil-works/pi-tui": "^0.84.0",
20
+ "@signalridge/pi-subagents": ">=1.5.0"
21
21
  },
22
22
  "dependencies": {
23
- "@sinclair/typebox": "^0.34.49",
24
- "@signalridge/pi-subagents-protocol": "^1.0.0"
23
+ "@signalridge/pi-subagents-protocol": "^1.3.0",
24
+ "@sinclair/typebox": "^0.34.50",
25
+ "acorn": "^8.16.0"
25
26
  },
26
27
  "pi": {
27
28
  "extensions": [
28
29
  "./src/index.ts"
30
+ ],
31
+ "skills": [
32
+ "skills/workflow-authoring",
33
+ "skills/workflow-patterns"
29
34
  ]
30
35
  },
31
36
  "repository": {
@@ -44,7 +49,8 @@
44
49
  "README.md",
45
50
  "LICENSE",
46
51
  "CHANGELOG.md",
47
- "src"
52
+ "src",
53
+ "skills"
48
54
  ],
49
55
  "scripts": {
50
56
  "test": "vitest run test --config vitest.config.ts",
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: workflow-authoring
3
+ description: Guidance for writing, editing, reviewing, and debugging JavaScript workflow code for @signalridge/pi-workflows. Use when authoring or changing workflow scripts; not for merely running an existing workflow.
4
+ metadata:
5
+ version: "1.2.1"
6
+ ---
7
+
8
+ # Workflow authoring
9
+
10
+ Load this skill when workflow JavaScript changes. Running an existing workflow needs no authoring reference.
11
+
12
+ ## Choose a branch
13
+
14
+ Read only what the task needs:
15
+
16
+ - **Write or edit:** start with [runtime](references/runtime.md). Add [pattern selection](references/pattern-selection.md) for topology, [lifecycle](references/lifecycle.md) for limits or resume, and [focused recipes](references/focused-recipes.md) for the matching concern.
17
+ - **Helper task:** read [quality helpers](references/quality-helpers.md) only for `verify` or `judgePanel`, the [retry helper](references/retry-helper.md) only for `retry`, and [specialized helpers](references/specialized-helpers.md) only for `completenessCheck`, `loopUntilDry`, `gate`, or `checkpoint`.
18
+ - **Review:** use the [review checklist](references/review.md), plus only the matching [quality](references/quality-helpers.md) or [specialized](references/specialized-helpers.md) helper contracts.
19
+ - **Debug:** use the [debugging map](references/debugging.md).
20
+ - **Routing:** read [registry ownership](references/registry-ownership.md) before using `model`, `tier`, phase models, or `agentType`; use environment-specific names only when context supplies them.
21
+ - **Exact lookup or portability:** start with the generated [capability index](references/capabilities.md). Follow its exhaustive-facts pointer only for constraints or support boundaries. Use [versions](references/versions.md) when moving scripts between installations.
22
+
23
+ ## Invariants
24
+
25
+ - Start with literal `export const meta = { name, description }`; declare phases as an array of used `{ title }` objects and enter each named phase.
26
+ - Call `agent()` at least once, give every call a short unique `label`, and return plain JSON data explicitly.
27
+ - Pair ordered results with stable work IDs before filtering. When one agent consumes another's selected result, include both its stable ID and actual data in the downstream prompt. Treat recoverable `null` as missing coverage and report it.
28
+ - Bound fan-out, loops, retries, agents, and concurrency to the task. Treat invocation-level token and time caps as opt-in user constraints, not defaults.
29
+ - Use `log()` for new code; `console` is compatibility-only.
30
+ - Write plain JavaScript without imports or filesystem modules. Pass nondeterminism through `args`; `Date.now()`, `Math.random()`, and no-argument `new Date()` are unavailable.
@@ -0,0 +1,63 @@
1
+ export const meta = {
2
+ name: "adversarial_verification",
3
+ description: "Produce claims and challenge them in separate skeptical contexts",
4
+ phases: [{ title: "Produce" }, { title: "Verify" }],
5
+ };
6
+
7
+ // ADAPT: validate and bound topics; define the evidence standard and schemas.
8
+ const topics =
9
+ args && Array.isArray(args.topics) ? args.topics : [{ id: "sample", topic: "Verify one sample claim" }];
10
+ const rubric = args && typeof args.rubric === "string" ? args.rubric : "Evidence is direct and sufficient";
11
+ const productionSchema = {
12
+ type: "object",
13
+ properties: {
14
+ claim: { type: "string" },
15
+ evidence: { type: "array", items: { type: "string" } },
16
+ },
17
+ required: ["claim", "evidence"],
18
+ };
19
+ const verdictSchema = {
20
+ type: "object",
21
+ properties: { upheld: { type: "boolean" }, reason: { type: "string" } },
22
+ required: ["upheld", "reason"],
23
+ };
24
+
25
+ phase("Produce");
26
+ const productions = await parallel(
27
+ topics.map((topic, index) => () =>
28
+ agent(`Develop a claim and cite its evidence.\n\n${JSON.stringify(topic)}`, {
29
+ label: `produce:${index}:${String(topic.id)}`,
30
+ schema: productionSchema,
31
+ }),
32
+ ),
33
+ );
34
+ const producerFailures = topics.flatMap((topic, index) =>
35
+ productions[index] === null ? [String(topic.id)] : [],
36
+ );
37
+ const reviewable = topics.flatMap((topic, index) =>
38
+ productions[index] === null ? [] : [{ topic, production: productions[index] }],
39
+ );
40
+
41
+ phase("Verify");
42
+ // INVARIANT: each skeptic is a fresh agent call, never the producer reviewing itself.
43
+ const verdicts = await parallel(
44
+ reviewable.map(({ topic, production }, index) => () =>
45
+ agent(
46
+ `Act as a skeptic. Try to refute this production under the rubric.\nRubric: ${rubric}\nProduction: ${JSON.stringify(production)}`,
47
+ { label: `skeptic:${index}:${String(topic.id)}`, schema: verdictSchema },
48
+ ),
49
+ ),
50
+ );
51
+ const skepticFailures = reviewable.flatMap(({ topic }, index) =>
52
+ verdicts[index] === null ? [String(topic.id)] : [],
53
+ );
54
+
55
+ return {
56
+ reviewed: reviewable.flatMap(({ topic, production }, index) =>
57
+ verdicts[index] === null
58
+ ? []
59
+ : [{ id: String(topic.id), production, verdict: verdicts[index] }],
60
+ ),
61
+ failed: { producers: producerFailures, skeptics: skepticFailures },
62
+ complete: producerFailures.length === 0 && skepticFailures.length === 0,
63
+ };
@@ -0,0 +1,56 @@
1
+ export const meta = {
2
+ name: "bounded_semantic_retry",
3
+ description: "Separate recoverable transport retries from a visible bounded semantic attempt ledger",
4
+ phases: [{ title: "Attempt" }],
5
+ };
6
+
7
+ // ADAPT: define task-owned acceptance, prompts, bounds, and structured result fields.
8
+ const maxSemanticAttempts =
9
+ args && Number.isInteger(args.maxSemanticAttempts) ? Math.max(1, Math.min(args.maxSemanticAttempts, 5)) : 3;
10
+ const transportRetries =
11
+ args && Number.isInteger(args.transportRetries) ? Math.max(0, Math.min(args.transportRetries, 3)) : 0;
12
+ const resultSchema = {
13
+ type: "object",
14
+ properties: {
15
+ accepted: { type: "boolean" },
16
+ answer: { type: "string" },
17
+ feedback: { type: "string" },
18
+ },
19
+ required: ["accepted", "answer", "feedback"],
20
+ };
21
+ const attempts = [];
22
+ let feedback = "";
23
+ let acceptedResult = null;
24
+
25
+ phase("Attempt");
26
+ for (let attempt = 1; attempt <= maxSemanticAttempts; attempt++) {
27
+ const result = await agent(
28
+ `Produce an acceptable answer.${feedback ? ` Address this prior feedback: ${feedback}` : ""}`,
29
+ {
30
+ label: `semantic-attempt:${attempt}`,
31
+ schema: resultSchema,
32
+ // Runtime retries repeat this same logical call after recoverable execution failures.
33
+ retries: transportRetries,
34
+ },
35
+ );
36
+ if (result === null) {
37
+ attempts.push({ attempt, status: "missing", result: null });
38
+ feedback = "The previous logical attempt produced no usable coverage.";
39
+ continue;
40
+ }
41
+ const status = result.accepted ? "accepted" : "rejected";
42
+ attempts.push({ attempt, status, result });
43
+ if (result.accepted) {
44
+ acceptedResult = result;
45
+ break;
46
+ }
47
+ feedback = result.feedback;
48
+ }
49
+
50
+ // INVARIANT: semantic exhaustion is returned visibly; it is not presented as success or thrown away.
51
+ return {
52
+ ok: acceptedResult !== null,
53
+ exhausted: acceptedResult === null && attempts.length === maxSemanticAttempts,
54
+ result: acceptedResult,
55
+ attempts,
56
+ };
@@ -0,0 +1,67 @@
1
+ export const meta = {
2
+ name: "classify_and_act",
3
+ description: "Classify bounded work before routing each item to an appropriate actor",
4
+ phases: [{ title: "Classify" }, { title: "Act" }],
5
+ };
6
+
7
+ // ADAPT: validate and bound items, categories, prompts, and schemas for the task.
8
+ const items =
9
+ args && Array.isArray(args.items) ? args.items : [{ id: "sample", task: "Classify one sample item" }];
10
+ const categories = ["direct", "parallel", "iterative"];
11
+ const instructionsByCategory = {
12
+ direct: "Handle directly in one focused pass.",
13
+ parallel: "Divide independent parts, account for each part, then combine them.",
14
+ iterative: "Work in bounded rounds and stop when the task-specific condition is met.",
15
+ };
16
+ const classificationSchema = {
17
+ type: "object",
18
+ properties: {
19
+ category: { type: "string", enum: categories },
20
+ reason: { type: "string" },
21
+ },
22
+ required: ["category", "reason"],
23
+ };
24
+ const actionSchema = {
25
+ type: "object",
26
+ properties: { outcome: { type: "string" } },
27
+ required: ["outcome"],
28
+ };
29
+
30
+ phase("Classify");
31
+ const classifications = await parallel(
32
+ items.map((item, index) => () =>
33
+ agent(`Classify this item as ${categories.join(", ")}.\n\n${JSON.stringify(item)}`, {
34
+ label: `classify:${index}:${String(item.id)}`,
35
+ schema: classificationSchema,
36
+ }),
37
+ ),
38
+ );
39
+ const failedClassification = items.flatMap((item, index) =>
40
+ classifications[index] === null ? [String(item.id)] : [],
41
+ );
42
+ const routed = items.flatMap((item, index) =>
43
+ classifications[index] === null ? [] : [{ item, classification: classifications[index] }],
44
+ );
45
+
46
+ // INVARIANT: no routed action starts until the complete classification set exists.
47
+ phase("Act");
48
+ const actions = await parallel(
49
+ routed.map(({ item, classification }, index) => () =>
50
+ agent(
51
+ `${instructionsByCategory[classification.category]}\n\n${JSON.stringify(item)}`,
52
+ { label: `act:${index}:${String(item.id)}`, schema: actionSchema },
53
+ ),
54
+ ),
55
+ );
56
+ const failedAction = routed.flatMap(({ item }, index) => (actions[index] === null ? [String(item.id)] : []));
57
+
58
+ // INVARIANT: retain identities for failures in either stage.
59
+ return {
60
+ handled: routed.flatMap(({ item, classification }, index) =>
61
+ actions[index] === null
62
+ ? []
63
+ : [{ id: String(item.id), category: classification.category, result: actions[index] }],
64
+ ),
65
+ failed: { classification: failedClassification, action: failedAction },
66
+ complete: failedClassification.length === 0 && failedAction.length === 0,
67
+ };
@@ -0,0 +1,63 @@
1
+ export const meta = {
2
+ name: "defensive_json_parsing",
3
+ description: "Parse agent() text output as JSON defensively when schema isn't used, flagging unparseable results instead of reading undefined fields",
4
+ phases: [{ title: "Extract" }],
5
+ };
6
+
7
+ // Asking a model to "return STRICT JSON" in the prompt does not change what
8
+ // agent() gives back: without `schema`, the result is always the assistant's
9
+ // raw text. Reading a field off unparsed text (or off a plain-text agent()
10
+ // call in general) fails silently — `result.verdict` is `undefined`, not an
11
+ // error, and a fleet of such calls can look fully "successful" while every
12
+ // aggregator downstream gets undefined values. Prefer the `schema` option
13
+ // (see structured-output.js) whenever the shape matters; use this pattern
14
+ // only when a schema genuinely isn't available (e.g. an agentType/model that
15
+ // cannot be schema-validated).
16
+ function parseOrFlag(text, requiredKeys) {
17
+ if (typeof text !== "string") return { ok: false, raw: text };
18
+ const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
19
+ const candidate = fence ? fence[1] : text;
20
+ let value;
21
+ try {
22
+ value = JSON.parse(candidate.trim());
23
+ } catch {
24
+ return { ok: false, raw: text };
25
+ }
26
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return { ok: false, raw: text };
27
+ for (const key of requiredKeys) {
28
+ if (!(key in value)) return { ok: false, raw: text };
29
+ }
30
+ return { ok: true, value };
31
+ }
32
+
33
+ // ADAPT: validate and bound work, then keep required fields as small as downstream JavaScript needs.
34
+ const work = args && Array.isArray(args.work) ? args.work.slice(0, 8) : [{ id: "sample" }];
35
+ const requiredKeys = ["verdict", "reason"];
36
+ const outputs = [];
37
+ const missing = [];
38
+ const unparseable = [];
39
+
40
+ phase("Extract");
41
+ for (let index = 0; index < work.length; index++) {
42
+ const item = work[index];
43
+ const id = String(item.id);
44
+ const result = await agent(
45
+ `Review this item and return STRICT JSON only, no prose: {"verdict": "pass" | "fail", "reason": string}. Item: ${JSON.stringify(item)}`,
46
+ { label: `defensive:${index}:${id}` },
47
+ );
48
+ if (result === null) {
49
+ missing.push(id);
50
+ outputs.push({ id, status: "missing", verdict: null });
51
+ continue;
52
+ }
53
+ const parsed = parseOrFlag(result, requiredKeys);
54
+ if (!parsed.ok) {
55
+ unparseable.push(id);
56
+ outputs.push({ id, status: "unparseable", verdict: null });
57
+ continue;
58
+ }
59
+ // INVARIANT: field access happens only after parseOrFlag confirms the required keys exist.
60
+ outputs.push({ id, status: "complete", verdict: parsed.value.verdict });
61
+ }
62
+
63
+ return { outputs, missing, unparseable, complete: missing.length === 0 && unparseable.length === 0 };
@@ -0,0 +1,47 @@
1
+ export const meta = {
2
+ name: "fan_out_and_synthesize",
3
+ description: "Run bounded independent work, retain a complete coverage ledger, then synthesize",
4
+ phases: [{ title: "Fan out" }, { title: "Synthesize" }],
5
+ };
6
+
7
+ // ADAPT: validate and bound args.work for the task before invoking this workflow.
8
+ const work = args && Array.isArray(args.work) ? args.work : [];
9
+
10
+ phase("Fan out");
11
+ const fanOutResults = await parallel(
12
+ work.map((unit, index) => () =>
13
+ agent(
14
+ `Complete this independent work unit. Return only evidence relevant to it.\n\n${JSON.stringify(unit)}`,
15
+ // INVARIANT: index plus a stable task-owned id keeps labels unique.
16
+ { label: `fanout:${index}:${String(unit.id)}` },
17
+ ),
18
+ ),
19
+ );
20
+
21
+ // INVARIANT: preserve every intended identity before filtering or synthesis.
22
+ const ledger = work.map((unit, index) => ({
23
+ id: String(unit.id),
24
+ status: fanOutResults[index] === null ? "failed" : "complete",
25
+ result: fanOutResults[index],
26
+ }));
27
+
28
+ phase("Synthesize");
29
+ const synthesis = await agent(
30
+ `Synthesize the complete fan-out ledger below. Distinguish covered work from failed/missing coverage; do not invent results.\n\n${JSON.stringify(ledger)}`,
31
+ {
32
+ label: "synthesize-complete-set",
33
+ // ADAPT: keep the schema small and aligned with downstream field access.
34
+ schema: {
35
+ type: "object",
36
+ properties: {
37
+ summary: { type: "string" },
38
+ coveredIds: { type: "array", items: { type: "string" } },
39
+ failedIds: { type: "array", items: { type: "string" } },
40
+ },
41
+ required: ["summary", "coveredIds", "failedIds"],
42
+ },
43
+ },
44
+ );
45
+
46
+ // INVARIANT: return plain serializable data, including missing-coverage identities.
47
+ return { ledger, synthesis };