@signalridge/pi-workflows 1.3.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,145 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.0
4
+ ### Minor Changes
5
+
6
+ - c1b1741: Protocol v4: workflows use the Agent-tier catalogue directly.
7
+
8
+ The wire contract changes, so the three packages move together: `pi-workflows`
9
+ now declares `"@signalridge/pi-subagents": ">=1.9.0"`, and a peer outside that
10
+ range fails the startup handshake with a diagnostic naming both packages rather
11
+ than failing mid-run.
12
+
13
+ The `small | medium | large` workflow-tier vocabulary and its mapping layer are
14
+ gone. A managed request names an Agent tier — a key in the host's own
15
+ `agentTiers` catalogue — and `resolveAgentTier()` is the single resolver for
16
+ every spawn path. Removed with it: the wire's mapped `agentTier` field, the
17
+ `workflow` settings key (retired with a warning naming it; `agentTiers.defaultTier`
18
+ replaces `workflow.defaultTier`), and the per-call `model`/`thinking` selectors,
19
+ which the validator now rejects rather than accepting and ignoring.
20
+
21
+ Every v4 capability is required and every ping carries the host's tier catalogue,
22
+ so an incomplete peer fails one check instead of five optional ones. Fresh
23
+ installs ship `low`/`medium`/`high` profiles, all inheriting their model, and a
24
+ managed call that names no tier falls back to `medium`, so a workflow runs on an
25
+ unconfigured machine without this package choosing a vendor. Because `medium`
26
+ inherits, that fallback lands on the parent session's model — it commits to an
27
+ effort level, not to a vendor, so it does change the thinking level a managed
28
+ call runs at when the parent session is set higher, and it does not move the
29
+ work anywhere cheaper. What it buys is a named policy with a durable snapshot
30
+ and a scope check; a workspace that wants cheaper managed work names a
31
+ `defaultTier` whose profile pins a model. The fallback is scoped to calls that
32
+ cannot inherit a parent model rather than being installed as
33
+ `agentTiers.defaultTier`: a shipped catalogue default would have applied to
34
+ every ordinary `Agent` spawn as well, silencing `defaultModel` and pinning a
35
+ thinking level on machines that configured neither. The catalogue no longer
36
+ ships a `fast` profile; it was the same (model, thinking) pair as `low`, and the
37
+ one agent that named it (Explore) now names `low`. A user agent file that still
38
+ says `agentTier: fast` is reported by the existing unknown-reference check when
39
+ settings and agents load, rather than failing at its first spawn.
40
+
41
+ `agentTiers.defaultTier` therefore has three states, and `/agents → Settings`
42
+ offers all three rather than rendering two of them as one word: a named tier,
43
+ `unset` (managed calls reach the shipped fallback), and `none` (recorded as
44
+ `noDefaultTier`, which withdraws the fallback so managed calls fail closed).
45
+ `setDefaultAgentTier` takes that choice as a tagged value, so a caller cannot
46
+ express "no default" without saying which of the two it means.
47
+
48
+ `resolveAgentTier` gained `requireTier` for that fallback, and the two things
49
+ pre-resolution callers need are both answered by the resolver rather than
50
+ restated beside it: `agentTierApplies()` for "will a tier own this spawn's
51
+ model?", and `selectAgentTier()` for the tier key itself, which the managed
52
+ path needs to label a tombstone and the lifecycle events before the runner has
53
+ resolved anything.
54
+
55
+ Because a tier now owns model resolution outright, the spawn paths stop
56
+ pre-resolving one — and with it they stopped producing the model name the agent
57
+ UI shows. The resolution callback carries that label back instead, so a tier
58
+ that pins a model still names it in the viewer and the agent list, and a profile
59
+ that inherits correctly shows none. The label rides beside the snapshot rather
60
+ than inside it: the snapshot is a durable policy record a managed tombstone
61
+ persists and revalidates, and a cosmetic string does not belong in it.
62
+
63
+ Workflow resume now keys each cached call on the policy for that call's own tier
64
+ rather than on a whole-catalogue fingerprint, so defining or editing an unrelated
65
+ tier no longer forces a full re-execution — but only for a call that names its
66
+ tier. A call that names none keeps the whole-catalogue key, because the host
67
+ resolves those as `call > agent frontmatter > defaultTier` and frontmatter is
68
+ not on this wire: an agent reaches a managed caller as a name. Keyed on
69
+ `defaultTier`, such a call would be wrong twice over — replaying stale work
70
+ after an edit to the tier its agent actually declares, and re-executing after an
71
+ edit to a default it never reached. `agentTierPolicyIdentity()` therefore
72
+ requires a tier rather than accepting `undefined` and folding the default in, so
73
+ the wrong call cannot be written. Journal schema v4 and managed tombstone schema
74
+ v2 are quarantined from older facts rather than migrated.
75
+
76
+ The tier catalogue belongs to the user, names included. The built-in workflows
77
+ and the ad-hoc script ship with this package, so a tier name they use that the
78
+ host does not define is dropped in favour of the host's default rather than
79
+ failing the run — they cannot assert which names exist on someone else's
80
+ machine. A script the user wrote still fails closed on an undefined tier, since
81
+ that is a typo in a catalogue they own. Shipped-ness travels with the script
82
+ rather than with the frame that called it, so a user script that reaches a
83
+ built-in through `workflow(name)` applies the built-in's rule to the built-in;
84
+ the authoring skill now says outright that `low`/`medium`/`high` are the shipped
85
+ profiles rather than names an authored script may assume.
86
+
87
+ The managed routing policy published on the wire reports the default a managed
88
+ call will actually get, fallback included, so a peer's replay identity cannot
89
+ disagree with the host's selection; its sorts are code-unit rather than
90
+ locale-dependent, so the same catalogue fingerprints identically everywhere. The
91
+ workflow peer re-pings per start and resume instead of pinning the catalogue it
92
+ saw at session start, and reports the host-selected tier back through an
93
+ `onTierResolved` callback rather than by mutating the dispatch options.
94
+
95
+ Fixes: a nested spawn no longer relabels an agent's frontmatter tier as
96
+ caller-requested, which had turned a `scopeModels` warning into a refusal and
97
+ misattributed the choice; the scheduler no longer freezes a frontmatter tier into
98
+ a job, so editing the agent file takes effect at fire time; retired managed spawn
99
+ keys are now bounded instead of growing for the life of a session; a
100
+ scope refusal for a tier whose profile inherits its model now names the model
101
+ that would have run instead of the literal `undefined`; and the retired
102
+ `workflow` key warns once per process rather than on every settings read and
103
+ every project write.
104
+
105
+ The tier-key predicate and its length bound now have a single definition, in
106
+ `pi-subagents-protocol`, which `pi-subagents` re-exports. The wire is the
107
+ narrower of the two gates — a key one accepted and the other rejected could
108
+ never reach a peer — so a second copy could only ever be a way for them to
109
+ disagree. That one definition now also governs `blockedProfiles`, which was
110
+ being validated by a helper that trims first: `" low"` became `"low"` there
111
+ while the same value in `profiles` or `defaultTier` was refused outright. All
112
+ three reject.
113
+
114
+ `/agents → Settings` no longer names the fallback tier in the `unset` row's
115
+ label. Which tier `unset` reaches depends on the catalogue, and a workspace that
116
+ edits or deletes the shipped `medium` profile leaves it reaching nothing — at
117
+ which point `unset` and `none` behave identically. The row's description asks
118
+ the resolver what `unset` would currently resolve to and says that, including
119
+ when the answer is "nothing".
120
+
121
+ `pi-workflows` also gained the session-lifecycle handling this routing work
122
+ needed to sit on. The engine has an explicit dispose lifecycle, so a protocol
123
+ probe or a start that is in flight when a session ends rejects with a disposed
124
+ error instead of resolving against an engine nobody owns. The tool, command and
125
+ widget surfaces register exactly once and resolve the *current* engine on each
126
+ use, rather than closing over the one that existed when they were registered —
127
+ a session replacement previously left them pointing at a disposed engine. Tool
128
+ and command handlers report failures as results rather than throwing out of the
129
+ host's dispatch, and the lifecycle handler accepts `subagents:created` and
130
+ `subagents:started` so a spawn's identity is recorded from the first event that
131
+ carries it rather than only at completion.
132
+
133
+ ### Patch Changes
134
+
135
+ - Updated dependencies [c1b1741]
136
+ - @signalridge/pi-subagents-protocol@1.4.0
137
+
138
+ ## 1.4.0
139
+ ### Minor Changes
140
+
141
+ - 28c8aa1: Strengthen script orchestration with deterministic named dependency graphs, bounded task retries and failure policies, safer fan-out barriers, durable nested-workflow replay, and reliable pause/resume accounting. The natural-language `/workflows run` path now plans, executes, and synthesizes a bounded task graph, while expanded authoring/pattern guidance and the new `workflow-review` skill make routing, recovery, and publication constraints explicit.
142
+
3
143
  ## 1.3.2
4
144
  ### Patch Changes
5
145
 
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  JavaScript orchestration for Pi. A workflow is a raw JavaScript module executed
4
4
  in a determinism-guarded `node:vm` realm; the runtime supplies `agent()`,
5
- `parallel()`, `pipeline()`, `workflow()`, quality helpers (`verify`,
5
+ `parallel()`, `pipeline()`, `orchestrate()`, `workflow()`, quality helpers (`verify`,
6
6
  `judgePanel`, `loopUntilDry`, `completenessCheck`, `retry`, `gate`), human
7
7
  `checkpoint()`, `phase()`, `log()`, `args`, `cwd`, restricted `process`, and
8
8
  `budget`. Every live `agent()` call is dispatched through the additive
@@ -12,8 +12,8 @@ isolation, retry, and session policy.
12
12
 
13
13
  ## Install
14
14
 
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
15
+ `@signalridge/pi-workflows` requires `@signalridge/pi-subagents` `>=1.9.0`
16
+ with protocol v4 managed-spawn, Agent-tier, and managed-policy support. They are separate Pi packages: the peer
17
17
  dependency documents the requirement but intentionally does not auto-load or
18
18
  duplicate the subagent extension.
19
19
 
@@ -54,9 +54,9 @@ export const meta = {
54
54
 
55
55
  phase("scan");
56
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" }),
57
+ () => agent("Check for cross-package imports", { label: "imports", tier: "low" }),
58
+ () => agent("Check for secrets", { label: "secrets", tier: "low" }),
59
+ () => agent("Check for dead code", { label: "dead-code", tier: "low" }),
60
60
  ]);
61
61
 
62
62
  phase("synthesize");
@@ -69,29 +69,42 @@ return report;
69
69
  The meta statement must be the first statement and use only literals — spread,
70
70
  computed keys, methods, template interpolation, and the reserved keys
71
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
72
+ `Math.random()`, and no-argument `new Date()` are unavailable (an AST precheck
73
+ plus in-realm stubs); prompt text containing those words remains valid. Pass
74
+ timestamps and randomness through `args`. The vm is
74
75
  a determinism guardrail, not a security boundary — scripts are generated by the
75
76
  user's own LLM and run on the user's own machine.
76
77
 
77
78
  ### Runtime globals
78
79
 
79
- - `agent(prompt, { label, phase, schema, model, tier, isolation, thread, agentType, toolset, excludeTools, timeoutMs, retries })`
80
+ - `agent(prompt, { label, phase, schema, tier, isolation, thread, agentType, toolset, excludeTools, timeoutMs, retries })`
80
81
  — dispatch one subagent through the managed spawn protocol. Returns text, a
81
- schema-validated value, or recoverable `null` after retries. `schema` is a
82
+ schema-validated value, or recoverable `null` after retries. Replayed journal
83
+ calls consume no real-dispatch cap or token/phase budget. `schema` is a
82
84
  plain JSON Schema; the reply is parsed and validated client-side with bounded
83
85
  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.
86
+ `tier` is the provider-neutral workflow route; `thread` chains sequential turns
87
+ within a named conversation thread. A legacy programmatic `model` option may
88
+ still be accepted by non-managed callers, but workflow meta/phase model or
89
+ thinking fields are rejected and are never a workflow policy source. Pre-schema-v4
90
+ journal runs are quarantined and cannot be resumed.
87
91
  - `parallel(thunks)` — run thunks concurrently, preserve input order. Recoverable
88
92
  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.
93
+ limit) halt the run after the batch barrier settles. Fan-out that breaches
94
+ `maxAgents` cancels only its own batch; each call accepts at most 4096 thunks.
91
95
  - `pipeline(items, ...stages)` — items run concurrently, stages per item run
92
- sequentially with `(previousValue, originalItem, index)`.
96
+ sequentially with `(previousValue, originalItem, index)`; each call accepts at
97
+ most 4096 items and waits for the batch before surfacing fatal failures.
98
+ - `orchestrate(tasks, { onError })` — execute a named dependency graph in
99
+ deterministic declaration-order layers. Each task is `{ id, dependsOn?, phase?,
100
+ retries?, run }`; its callback receives `{ id, attempt, results, statuses }`.
101
+ `onError` is `skip-dependents` (default), `continue`, or `fail-fast`. Results
102
+ are keyed by task id, and task lifecycle events are observable through
103
+ `onRuntimeEvent`.
93
104
  - `workflow(savedName, childArgs?)` — run a saved workflow inline; one nested
94
- level, sharing the limiter, counters, token accounting, and store.
105
+ level, sharing the limiter, counters, token accounting, and store. The child
106
+ result is journaled as one parent call with a generation-scoped namespace, so
107
+ unchanged nested work replays without spawning again after resume.
95
108
  - `verify(item, { reviewers, threshold, lens })`, `judgePanel(attempts,
96
109
  { judges, rubric })`, `loopUntilDry({ round, key, consecutiveEmpty,
97
110
  maxRounds })`, `completenessCheck(taskArgs, results)`, `retry(thunk,
@@ -107,17 +120,35 @@ user's own LLM and run on the user's own machine.
107
120
  ### Resume
108
121
 
109
122
  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,
123
+ live from the first changed or inserted `agent()`/`checkpoint()` call. The
124
+ original invocation `args` and execution limits are frozen and reused on manual
125
+ or automatic resume. Pass `resumeFromRunId` with the edited script; unchanged
126
+ calls report zero tokens,
112
127
  and edited calls rotate their managed spawn key generation so pi-subagents
113
128
  never raises a fingerprint conflict.
114
129
 
130
+ ## Skills
131
+
132
+ The package ships three model-routable skills:
133
+
134
+ - `workflow-authoring` — design, review, and debug custom JavaScript workflows;
135
+ it teaches topology selection, stable work IDs, bounded failure handling, and
136
+ resume-safe authoring.
137
+ - `workflow-patterns` — choose and invoke a reviewed built-in for research,
138
+ adversarial review, code review, multi-perspective analysis, or codebase audit.
139
+ - `workflow-review` — audit an existing workflow for topology, data flow,
140
+ recovery, resume safety, and publication readiness without editing it.
141
+
142
+ Use the pattern skill for a standard task, the authoring skill for a custom
143
+ workflow, and the review skill before accepting a topology or recovery change.
144
+
115
145
  ## Tools and commands
116
146
 
117
147
  - `workflow`: run a script (`script`) or a saved/built-in workflow (`name`),
118
148
  with `args`, `background` (default true), `maxAgents`, `concurrency`,
119
149
  `agentRetries`, `tokenBudget`, `agentTimeoutMs`, and `resumeFromRunId`.
120
150
  - `workflow_control`: `list`, `get`, `pause`, `resume`, `stop`, or `rm` a run; `rm` writes a durable removal tombstone.
151
+ - `pi.events` emits `pi-workflows:runtime` snapshots (`{ runId, event }`) for phase, task, quality, retry, and nested-workflow progress; observer failures are contained.
121
152
  - `/workflows`: TUI navigator plus `run`, `status`, `watch`, `stop`, `pause`,
122
153
  `resume`, `rm`, and `save <name> [runId]` subcommands.
123
154
  - `/workflows-models`: points at pi-subagents' `/agents → Model tiers` — there
@@ -155,12 +186,52 @@ written atomically with a backup.
155
186
 
156
187
  ## Model routing
157
188
 
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.
189
+ `tier` names a key in the host's own `agentTiers` catalogue — the same catalogue an
190
+ ordinary `Agent` call uses. There is no separate workflow-tier vocabulary and no
191
+ mapping layer: a workflow that wants cheap work asks for the tier the user defined
192
+ for cheap work.
193
+
194
+ ```js
195
+ await agent("summarize this diff", { tier: "low" })
196
+ await agent("design the migration", { tier: "high" })
197
+ ```
198
+
199
+ The host's catalogue arrives with the protocol handshake, so a tier this host does
200
+ not define is rejected before any dispatch, naming the ones it does. Resolution
201
+ itself is entirely pi-subagents' `resolveAgentTier()`: it owns the final model,
202
+ thinking level, clamping, availability checks, and the immutable resolution
203
+ snapshot.
204
+
205
+ The catalogue belongs to whoever runs the workflow — the names included. The
206
+ built-in workflows and the ad-hoc script ship with this package, so they cannot
207
+ assert that `low` exists on a machine whose tiers are called `cheap` and `deep`:
208
+ a name they use that the host does not define is dropped, with a log line, and
209
+ the host's own default applies. A script you wrote names tiers from your own
210
+ catalogue, so an undefined one there is a typo and fails closed instead.
211
+
212
+ There is no per-call `model` or `thinking`, and `meta`/phase metadata may not carry
213
+ them either — both are rejected explicitly rather than accepted and ignored. A tier
214
+ is the only model policy a workflow can express, which is what keeps a script from
215
+ pinning a vendor the host did not choose.
216
+
217
+ Omitting `tier` uses the agent's own tier, then `agentTiers.defaultTier`. Fresh
218
+ installs ship `low`/`medium`/`high` profiles, all inheriting their model, and a
219
+ managed call that still names no tier falls back to `medium` — so the built-ins run
220
+ on a new machine without any configuration. That fallback is scoped to managed
221
+ calls: it is not the catalogue's `defaultTier`, so an ordinary `Agent` spawn on the
222
+ same machine keeps falling through to `defaultModel` and the parent session. A host
223
+ whose default has been cleared (`noDefaultTier`) rejects an untiered managed call
224
+ rather than quietly inheriting the parent session's model.
225
+
226
+ The managed wire, invocation record, journal, tombstone, and resume state all carry
227
+ the one resolved tier — including one the host defaulted to, so a run's record says
228
+ what actually ran. Resume keys each cached call on the policy for *that call's*
229
+ tier: editing an unrelated tier does not invalidate work that never used it.
159
230
 
160
231
  ## Intentional adaptations from upstream
161
232
 
162
233
  - **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.
234
+ - **Agent registry and tier catalogue ownership.** `agentType` and the tier catalogue resolve in pi-subagents; the workflow package supplies only a tier key, avoiding a second model/thinking policy.
164
235
  - **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.
165
236
 
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.
237
+ State is persisted as `pi.appendEntry("pi-workflows:journal", ...)` custom entries (schema v4), including frozen invocation args, resolved tier identity, terminal result previews, script revisions, named nested-workflow result boundaries, and durable removals. Interrupted/provider-limited runs replay from the journal; pre-schema-v4 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,6 +1,6 @@
1
1
  {
2
2
  "name": "@signalridge/pi-workflows",
3
- "version": "1.3.2",
3
+ "version": "1.5.0",
4
4
  "description": "Deterministic JavaScript orchestration workflows backed by pi-subagents managed spawning.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -17,10 +17,10 @@
17
17
  "peerDependencies": {
18
18
  "@earendil-works/pi-coding-agent": "^0.84.0",
19
19
  "@earendil-works/pi-tui": "^0.84.0",
20
- "@signalridge/pi-subagents": ">=1.5.0"
20
+ "@signalridge/pi-subagents": ">=1.9.0"
21
21
  },
22
22
  "dependencies": {
23
- "@signalridge/pi-subagents-protocol": "^1.3.0",
23
+ "@signalridge/pi-subagents-protocol": "^1.4.0",
24
24
  "@sinclair/typebox": "^0.34.50",
25
25
  "acorn": "^8.16.0"
26
26
  },
@@ -30,7 +30,8 @@
30
30
  ],
31
31
  "skills": [
32
32
  "skills/workflow-authoring",
33
- "skills/workflow-patterns"
33
+ "skills/workflow-patterns",
34
+ "skills/workflow-review"
34
35
  ]
35
36
  },
36
37
  "repository": {
@@ -1,30 +1,183 @@
1
1
  ---
2
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.
3
+ description: Design, write, review, and debug Pi JavaScript workflows that coordinate multiple subagents. Use when a request needs named dependencies, bounded fan-out, per-item pipelines, quality gates, saved or nested workflows, pause/resume, or workflow progress; do not use it merely to run an existing named workflow.
4
4
  metadata:
5
- version: "1.2.1"
5
+ version: "1.5.0"
6
6
  ---
7
7
 
8
8
  # Workflow authoring
9
9
 
10
- Load this skill when workflow JavaScript changes. Running an existing workflow needs no authoring reference.
10
+ A workflow is a small program that coordinates subagents; it is not a long prompt
11
+ that asks one agent to do every step. Keep semantic work in agents and keep
12
+ identity, ordering, bounds, failure handling, and aggregation deterministic in
13
+ JavaScript.
11
14
 
12
- ## Choose a branch
15
+ ## Operating contract
13
16
 
14
- Read only what the task needs:
17
+ - Use a workflow only when the task benefits from multiple agents, explicit
18
+ stages, independent coverage, or a quality/recovery loop. One or two simple
19
+ delegations should use the ordinary agent surface.
20
+ - Start every script with literal `export const meta = { name, description,
21
+ phases? }`. Return plain JSON data explicitly.
22
+ - Use `orchestrate()` for named dependencies, `parallel()` for a small
23
+ independent barrier, and `pipeline()` for independent items that each need a
24
+ sequential stage chain. Do not encode a dependency graph only in comments.
25
+ - Give every work unit a stable ID and every agent call a short unique label.
26
+ Keep IDs beside results before filtering; `null` is missing coverage, not a
27
+ successful finding.
28
+ - Bound graph tasks, fan-out items, loops, retries, agents, concurrency, and
29
+ evidence size. Invocation-level token and time caps are user constraints, not
30
+ substitutes for algorithmic bounds.
31
+ - Route an agent call with `tier: "<key>"`, naming a tier the destination host
32
+ defines. Omitting `tier` uses the agent's own tier, then the host's configured
33
+ default; a host whose default has been cleared rejects the call rather than
34
+ inheriting the parent model. There is no per-call `model`/`thinking`, and
35
+ neither may appear in workflow meta or phase profiles. Never guess a tier or
36
+ agent-type name—use names supplied by the current environment and follow
37
+ [registry ownership](references/registry-ownership.md).
38
+ - Workflows have no imports, filesystem/network APIs, timers, or unrestricted
39
+ Node APIs. Pass timestamps, randomness, and external decisions through `args`.
15
40
 
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.
41
+ ## The authoring pass
22
42
 
23
- ## Invariants
43
+ 1. **Classify the topology.** Decide whether the work is direct, a named DAG,
44
+ homogeneous fan-out, a per-item pipeline, iterative discovery, or a quality
45
+ gate. Read [pattern selection](references/pattern-selection.md) only for the
46
+ chosen shape.
47
+ 2. **Declare inputs and bounds.** Read `args` once, normalize the input, reject
48
+ or cap untrusted cardinality, and decide what incomplete coverage means.
49
+ 3. **Write the envelope.** Declare only phases that are actually entered. Use
50
+ `phase(title, { budget })` for a soft token sub-budget, not as a scheduler.
51
+ 4. **Build the smallest work ledger.** Store `{ id, status, result, error }`
52
+ (or an equivalent object) for each intended unit. Do not lose identity when
53
+ applying `.filter(Boolean)`.
54
+ 5. **Dispatch with the right primitive.** Await every batch. For a dependent
55
+ prompt, include both the dependency ID and its actual result; do not assume
56
+ the child can see the parent conversation.
57
+ 6. **Add quality only where it changes a decision.** Use a schema before reading
58
+ structured fields, then use `verify`, `judgePanel`, `gate`, or
59
+ `completenessCheck` with explicit bounds.
60
+ 7. **Aggregate and report.** Return a bounded JSON result containing the answer,
61
+ coverage, and important failures. If the workflow runs in the background,
62
+ the run ID is the handle for later control/resume.
63
+ 8. **Review the script.** Use [the review checklist](references/review.md), then
64
+ test with deterministic fake agents when changing topology or recovery.
24
65
 
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.
66
+ ## Topology rules
67
+
68
+ ### Named dependencies: `orchestrate`
69
+
70
+ Use a task graph when a later step consumes a named earlier step:
71
+
72
+ ```js
73
+ const graph = await orchestrate([
74
+ { id: "scan", run: () => agent("Inspect the target", { label: "scan" }) },
75
+ { id: "security", run: () => agent("Check security risks", { label: "security" }) },
76
+ {
77
+ id: "report",
78
+ dependsOn: ["scan", "security"],
79
+ run: ({ results, statuses }) =>
80
+ agent(
81
+ "Synthesize the named findings; mention unavailable coverage.\n\n" +
82
+ JSON.stringify({ results, statuses }),
83
+ { label: "report" },
84
+ ),
85
+ },
86
+ ]);
87
+ return graph;
88
+ ```
89
+
90
+ `orchestrate()` validates IDs, missing dependencies, duplicate edges, and
91
+ cycles before callbacks run. It executes ready tasks in declaration-order
92
+ layers with a barrier between layers and returns `{ results, tasks }`. Each task
93
+ callback receives detached `{ id, attempt, results, statuses }` snapshots.
94
+ `retries` retries ordinary task-callback failures up to three times. The default
95
+ `onError: "skip-dependents"` prevents a failed dependency from feeding a stale
96
+ prompt; use `onError: "continue"` only when the dependent explicitly handles
97
+ failed statuses. `fail-fast` stops discovery of later layers after the current
98
+ layer settles.
99
+
100
+ ### Independent fan-out: `parallel`
101
+
102
+ Pass thunks, not already-started promises. Results preserve input order and a
103
+ recoverable item failure becomes `null`; fatal errors surface after the whole
104
+ batch barrier settles. Keep a ledger with IDs before filtering:
105
+
106
+ ```js
107
+ const work = items.map((item) => ({ id: item.id, item }));
108
+ const outputs = await parallel(work.map(({ item }) => () =>
109
+ agent("Handle item " + item.id, { label: "item-" + item.id }),
110
+ ));
111
+ const ledger = work.map(({ id }, index) => ({ id, result: outputs[index] }));
112
+ ```
113
+
114
+ Each `parallel()` call accepts at most 4096 thunks. It is a barrier: do not
115
+ start synthesis until the returned array is complete.
116
+
117
+ ### Per-item stages: `pipeline`
118
+
119
+ Use `pipeline(items, ...stages)` when every item follows the same chain. Items
120
+ run concurrently, but stages for one item are sequential and receive
121
+ `(previousValue, originalItem, index)`. A stage failure drops that item to
122
+ `null`; guard missing values before the next stage. There is no cross-item
123
+ barrier between stages, and each call accepts at most 4096 items.
124
+
125
+ ### Saved or nested workflows
126
+
127
+ `workflow(name, childArgs)` resolves a context-supplied saved workflow and runs
128
+ it inline with the parent's limiter, agent accounting, token accounting, and
129
+ store. One nested level is allowed; concurrent siblings are allowed. An
130
+ unchanged child is replayed as one parent journal call, while a changed or
131
+ interrupted child receives a generation-scoped managed identity. Do not use
132
+ nesting as an unbounded recursion mechanism.
133
+
134
+ ## Prompt and result discipline
135
+
136
+ Every child prompt must be self-contained: state the role, exact task, relevant
137
+ inputs, expected output shape, and what the child must not do. Prefer a small
138
+ `schema` for values JavaScript will inspect. When a child consumes another
139
+ child's output, include the stable ID and actual bounded data, for example:
140
+
141
+ ```js
142
+ const prompt = "Review result " + JSON.stringify({ id: "scan", result: scan });
143
+ ```
144
+
145
+ Do not pass whole transcripts when a summary or structured fields are enough.
146
+ Use `log()` for progress and return a result that distinguishes completed work,
147
+ missing coverage, and failed/skipped work.
148
+
149
+ ## Failure, limits, and recovery
150
+
151
+ - Recoverable child failures return `null` after agent retries. Fatal workflow
152
+ errors (invalid hooks, exhausted hard limits, unsupported selectors, and
153
+ schema exhaustion) throw and must not be dissolved into a normal result.
154
+ - Runtime `agent({ retries })` retries execution failures. `orchestrate` task
155
+ `retries` retries the callback. Semantic `retry()`/`gate()` attempts are a
156
+ third, intentional layer. Bound and ledger every layer separately.
157
+ - `agent()` schema repair includes concrete validation feedback in the next
158
+ attempt. Do not read fields from an unvalidated text response.
159
+ - `checkpoint()` is headless by default for background runs. Use
160
+ `background: false` only when a human decision must be shown.
161
+ - `budget` and phase budgets are soft pre-call gates; concurrent work may
162
+ overshoot. Do not promise an exact token ceiling unless the caller supplied
163
+ and accepted that policy.
164
+
165
+ ## Resume and publication gate
166
+
167
+ Resume replays the longest unchanged prefix of journaled calls. Keep call order,
168
+ labels, prompts, routing options, and inputs stable. `orchestrate` layer order
169
+ and named dependency results make dynamic plans easier to replay, but arbitrary
170
+ branch/loop call counts can still invalidate the positional prefix. Never use
171
+ `Date.now()`, `Math.random()`, or no-argument `new Date()` in a script.
172
+
173
+ Before shipping a workflow change:
174
+
175
+ - read [lifecycle](references/lifecycle.md) for pause, stop, budget, and resume;
176
+ - read [debugging](references/debugging.md) when reproducing a failure;
177
+ - read [the review checklist](references/review.md) before accepting topology;
178
+ - read [the generated capability index](references/capabilities.md) when a
179
+ signature, default, or support boundary is disputed; remember that a tier
180
+ names a key in the host catalogue while model/thinking resolution stays in
181
+ pi-subagents;
182
+ - ensure every package-relative example/reference link remains inside the
183
+ publishable package.
@@ -0,0 +1,34 @@
1
+ export const meta = {
2
+ name: "named-task-graph",
3
+ description: "Run independent research and synthesize it through named dependencies",
4
+ phases: [{ title: "research" }, { title: "synthesis" }],
5
+ };
6
+
7
+ const graph = await orchestrate([
8
+ {
9
+ id: "architecture",
10
+ phase: "research",
11
+ description: "Inspect the architecture",
12
+ run: () => agent("Inspect the architecture and report concrete design constraints", { label: "architecture" }),
13
+ },
14
+ {
15
+ id: "risk",
16
+ phase: "research",
17
+ description: "Find operational risks",
18
+ run: () => agent("Find operational and recovery risks", { label: "risk" }),
19
+ },
20
+ {
21
+ id: "synthesis",
22
+ phase: "synthesis",
23
+ dependsOn: ["architecture", "risk"],
24
+ description: "Combine the named findings",
25
+ run: ({ results, statuses }) =>
26
+ agent(
27
+ "Combine these findings. Mention any unavailable dependency explicitly.\n\n" +
28
+ JSON.stringify({ results, statuses }),
29
+ { label: "synthesis" },
30
+ ),
31
+ },
32
+ ]);
33
+
34
+ return graph;
@@ -1,8 +1,8 @@
1
1
  <!-- GENERATED from WORKFLOW_CAPABILITY_CONTRACT; do not edit by hand. -->
2
2
  # Workflow capability index
3
3
 
4
- Contract format: `1.0.0`<br>
5
- Contract content / skill / extension: `1.3.2`
4
+ Contract format: `2.0.0`<br>
5
+ Contract content / skill / extension: `1.5.0`
6
6
 
7
7
  This compact generated index covers supported runtime globals and workflow-tool inputs. For constraints, compatibility behavior, internal boundaries, and dynamic-reference ownership, follow the [exhaustive generated facts](capability-details.md).
8
8
 
@@ -11,9 +11,10 @@ This compact generated index covers supported runtime globals and workflow-tool
11
11
  <!-- BEGIN GENERATED SUPPORTED WORKFLOW CAPABILITIES -->
12
12
  | Name | Classification | Signature | Options and defaults |
13
13
  | --- | --- | --- | --- |
14
- | agent | runtime-global | `agent(prompt, options?) => Promise<string \| structured value \| null>` | `label`: string (optional); default: derived from phase and call count<br>`phase`: string (optional); default: current phase<br>`schema`: plain JSON Schema (optional)<br>`model`: string (optional); default: pi-subagents model resolver<br>`tier`: small\|medium\|large (optional); default: pi-subagents workflow tier<br>`isolation`: "worktree" (optional); default: pi-subagents worktree policy<br>`thread`: string (optional); default: fresh session per call<br>`toolset`: string (optional); default: configured host/agent toolset hint<br>`excludeTools`: string[] (optional)<br>`agentType`: string (optional); default: general-purpose<br>`timeoutMs`: number \| null (optional); default: 300000 (5min); null disables<br>`retries`: number (optional); default: run retry count |
14
+ | agent | runtime-global | `agent(prompt, options?) => Promise<string \| structured value \| null>` | `label`: string (optional); default: derived from phase and call count<br>`phase`: string (optional); default: current phase<br>`schema`: plain JSON Schema (optional)<br>`tier`: agent tier key (optional); default: the agent's own tier, else agentTiers.defaultTier<br>`isolation`: "worktree" (optional); default: pi-subagents worktree policy<br>`thread`: string (optional); default: fresh session per call<br>`toolset`: string (optional); default: configured host/agent toolset hint<br>`excludeTools`: string[] (optional)<br>`agentType`: string (optional); default: general-purpose<br>`timeoutMs`: number \| null (optional); default: 300000 (5min); null disables<br>`retries`: number (optional); default: run retry count |
15
15
  | parallel | runtime-global | `parallel(thunks) => Promise<Array<unknown \| null>>` | — |
16
16
  | pipeline | runtime-global | `pipeline(items, ...stages) => Promise<Array<unknown \| null>>` | — |
17
+ | orchestrate | runtime-global | `orchestrate(tasks, options?) => Promise<{ results, tasks }>` | `onError`: "skip-dependents"\|"continue"\|"fail-fast" (optional); default: "skip-dependents" |
17
18
  | workflow | runtime-global | `workflow(savedName, childArgs?) => Promise<unknown>` | — |
18
19
  | verify | runtime-global | `verify(item, options?) => Promise<{ real, realCount, total, votes }>` | `reviewers`: number (optional); default: 2<br>`threshold`: number (optional); default: 0.5<br>`lens`: string \| string[] (optional) |
19
20
  | judgePanel | runtime-global | `judgePanel(attempts, options?) => Promise<{ index, attempt, score, judgments } \| undefined>` | `judges`: number (optional); default: 3<br>`rubric`: string (optional); default: overall quality and correctness |