@signalridge/pi-workflows 1.4.0 → 1.6.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,254 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.6.0
4
+ ### Minor Changes
5
+
6
+ - fabe89a: A workflow now speaks one routing word of its own: **strength**.
7
+
8
+ `agent({ tier })` is gone. A script names `strength: "low" | "medium" | "high"` —
9
+ this package's own word for how much effort a step deserves — and the new
10
+ `strengths` setting is the only thing that binds one to an Agent tier. A strength
11
+ no table defines dispatches with **no tier at all** and takes the agent's ordinary
12
+ default, so nothing is inferred from spelling at dispatch: the binding is always a
13
+ table entry.
14
+
15
+ That inference was the bug. The built-ins ask for `low`, and so does the `Explore`
16
+ agent and everything that names no tier of its own, so pointing `low` at a cheaper
17
+ model to make a 26-agent fan-out affordable dragged every ordinary search spawn
18
+ with it. Pointing a *strength* elsewhere does not.
19
+
20
+ A host that has configured nothing gets a shipped default table rather than no
21
+ mappings: each strength on the catalogue tier of the same name, wherever the host
22
+ defines one — identity on a stock install, empty on a host whose tiers are called
23
+ `cheap`/`deep`. It is computed against the live catalogue, so it is never an
24
+ assertion about someone else's tier names and never complains at run start about
25
+ configuration nobody wrote. Without it every shipped script's `low`/`medium`
26
+ distinction would collapse onto pi-subagents' managed `medium` fallback — the
27
+ *more* expensive rung — and an unconfigured machine would run its fan-outs dearer
28
+ than before this change.
29
+
30
+ The distinction that makes that safe is that it is a **table, not a fallback
31
+ rule**. A rule would be unremovable, and re-pricing a fan-out would again mean
32
+ editing the shared tier. A table is replaced by writing one, a written table that
33
+ omits `low` leaves it unmapped even where a tier called `low` exists, and `{}` is
34
+ a real table meaning "map nothing".
35
+
36
+ This is not the retired `workflow.tiers` key. That one carried its own `model` and
37
+ `thinking`, making it a second model policy behind a second resolver. A value here
38
+ is a key in the host's one catalogue: pi-subagents still owns every model, every
39
+ thinking level, and the only `resolveAgentTier()`, and cannot tell a mapped call
40
+ apart from a spawn that named the key itself. No protocol change.
41
+
42
+ The vocabulary is closed, which buys back what the passthrough used to provide.
43
+ A word outside the three is a typo, not a strength nobody configured, so it fails
44
+ before dispatch. With no way for a script name to reach the catalogue, the
45
+ shipped-vs-user tier rule (`shippedScript`) had nothing left to decide and is
46
+ removed, along with its `run_created` journal field.
47
+
48
+ The same argument applies one level up, so `agent()` and `checkpoint()` now
49
+ reject any option name they do not read instead of ignoring it. `tier`, `model`,
50
+ and `thinking` answer by naming their replacement; anything else lists the legal
51
+ keys, and a non-object options value is refused rather than reaching a bare
52
+ `TypeError`. An unknown key is dropped before the call hash is built, so it moves
53
+ neither the dispatch nor the resume identity: `agent({ strenght: "low" })` would
54
+ have run every step untiered and replayed that way, and
55
+ `checkpoint({ headles: "abort" })` would have auto-approved in headless mode
56
+ instead of aborting. Neither leaves a log line. A script is plain JavaScript
57
+ authored against this contract, so nothing else was going to catch it. Both key
58
+ lists are pinned to their interfaces by a `satisfies Record<keyof …>` clause, in
59
+ both directions.
60
+
61
+ There is deliberately no default strength: a call that names none dispatches with
62
+ no tier, exactly as an unmapped one does, and the host resolves it the way it
63
+ resolves every other spawn. A default was tried and is wrong — a tier a call
64
+ requests outranks the agent type's own, so on a stock install (`medium → medium`)
65
+ every unlabelled dispatch would have pinned `medium` over whatever the agent
66
+ declared, overruling `Explore`'s shipped `tier: low` and re-pricing in the wrong
67
+ direction the one agent this indirection exists to leave alone. Nothing is lost:
68
+ a script that named no strength stated no opinion, and pi-subagents' ordinary
69
+ precedence is still user-owned policy.
70
+
71
+ What the table must reach is every call a script did label, and the shipped
72
+ scripts label all of theirs. The three helpers that dispatch on the script's
73
+ behalf — `verify`, `judgePanel`, `completenessCheck` — took no strength at all
74
+ and so could never be re-routed; they now accept one, defaulting to `low`, `low`,
75
+ and `medium`.
76
+
77
+ Those defaults are an opinion where there was none, and an opinion outranks the
78
+ host's: a tier a call requests beats `agentTiers.defaultTier`, so on a machine
79
+ whose default is `high` a `completenessCheck` now runs at whatever `medium` maps
80
+ to rather than at `high`. That is the price of making the helpers steerable at
81
+ all — a helper that names no tier can never be pointed anywhere — and the new
82
+ option is how a script takes it back.
83
+
84
+ Fixes a re-spend bug: `strengths` is deliberately never frozen onto a run, but the
85
+ resumes the engine starts itself (`/workflows resume`, the provider-limit retry)
86
+ passed no table, and since a call's identity keys on the tier it requested, every
87
+ finished call missed its journal entry and re-ran untabled. The engine now reads
88
+ the current table for those.
89
+
90
+ `/workflows-models` is gone; the table is edited with `/workflows strength`
91
+ (`[<low|medium|high> <tier>|off]`), which also prints the effective table and the
92
+ tiers this host defines — the one place both halves are visible. It validates the
93
+ strength and the target before writing and reports the resolved result rather than
94
+ echoing the write, so it can no longer claim a change the settings layer dropped.
95
+ `off` yields to a real host tier of that name, and says so when it does, since a
96
+ user who typed the documented keyword and got a mapping has to be told which
97
+ reading won. The view also names where a machine-wide table lives, and — while
98
+ the shipped default is in force — that the default is identity, so workflow work
99
+ is still sharing the tiers ordinary spawns use until a tier is defined for it.
100
+
101
+ Settings commands now write project scope instead of the merged view. Previously
102
+ the first `/effort` or `/workflows-progress` in a project copied every global value
103
+ and default into the project file, after which that project stopped tracking the
104
+ global one.
105
+
106
+ `strengths` needs the opposite treatment, because it replaces rather than merges at
107
+ each level, so `/workflows strength` seeds its write from the table a run would
108
+ actually use — project, else global, else the shipped default. Built on project
109
+ scope alone, setting one entry would have discarded an inherited global table, and
110
+ `off` against an entry the project never wrote would have written nothing and left
111
+ the mapping standing.
112
+
113
+ One caution the docs now carry: the provider-limit retry resumes on a timer with
114
+ nobody watching, so a table edited while a run is rate-limit-paused is charged the
115
+ re-spend above without being asked. Stop the run first if that is not worth it.
116
+
117
+ ## 1.5.0
118
+ ### Minor Changes
119
+
120
+ - c1b1741: Protocol v4: workflows use the Agent-tier catalogue directly.
121
+
122
+ The wire contract changes, so the three packages move together: `pi-workflows`
123
+ now declares `"@signalridge/pi-subagents": ">=1.9.0"`, and a peer outside that
124
+ range fails the startup handshake with a diagnostic naming both packages rather
125
+ than failing mid-run.
126
+
127
+ The `small | medium | large` workflow-tier vocabulary and its mapping layer are
128
+ gone. A managed request names an Agent tier — a key in the host's own
129
+ `agentTiers` catalogue — and `resolveAgentTier()` is the single resolver for
130
+ every spawn path. Removed with it: the wire's mapped `agentTier` field, the
131
+ `workflow` settings key (retired with a warning naming it; `agentTiers.defaultTier`
132
+ replaces `workflow.defaultTier`), and the per-call `model`/`thinking` selectors,
133
+ which the validator now rejects rather than accepting and ignoring.
134
+
135
+ Every v4 capability is required and every ping carries the host's tier catalogue,
136
+ so an incomplete peer fails one check instead of five optional ones. Fresh
137
+ installs ship `low`/`medium`/`high` profiles, all inheriting their model, and a
138
+ managed call that names no tier falls back to `medium`, so a workflow runs on an
139
+ unconfigured machine without this package choosing a vendor. Because `medium`
140
+ inherits, that fallback lands on the parent session's model — it commits to an
141
+ effort level, not to a vendor, so it does change the thinking level a managed
142
+ call runs at when the parent session is set higher, and it does not move the
143
+ work anywhere cheaper. What it buys is a named policy with a durable snapshot
144
+ and a scope check; a workspace that wants cheaper managed work names a
145
+ `defaultTier` whose profile pins a model. The fallback is scoped to calls that
146
+ cannot inherit a parent model rather than being installed as
147
+ `agentTiers.defaultTier`: a shipped catalogue default would have applied to
148
+ every ordinary `Agent` spawn as well, silencing `defaultModel` and pinning a
149
+ thinking level on machines that configured neither. The catalogue no longer
150
+ ships a `fast` profile; it was the same (model, thinking) pair as `low`, and the
151
+ one agent that named it (Explore) now names `low`. A user agent file that still
152
+ says `agentTier: fast` is reported by the existing unknown-reference check when
153
+ settings and agents load, rather than failing at its first spawn.
154
+
155
+ `agentTiers.defaultTier` therefore has three states, and `/agents → Settings`
156
+ offers all three rather than rendering two of them as one word: a named tier,
157
+ `unset` (managed calls reach the shipped fallback), and `none` (recorded as
158
+ `noDefaultTier`, which withdraws the fallback so managed calls fail closed).
159
+ `setDefaultAgentTier` takes that choice as a tagged value, so a caller cannot
160
+ express "no default" without saying which of the two it means.
161
+
162
+ `resolveAgentTier` gained `requireTier` for that fallback, and the two things
163
+ pre-resolution callers need are both answered by the resolver rather than
164
+ restated beside it: `agentTierApplies()` for "will a tier own this spawn's
165
+ model?", and `selectAgentTier()` for the tier key itself, which the managed
166
+ path needs to label a tombstone and the lifecycle events before the runner has
167
+ resolved anything.
168
+
169
+ Because a tier now owns model resolution outright, the spawn paths stop
170
+ pre-resolving one — and with it they stopped producing the model name the agent
171
+ UI shows. The resolution callback carries that label back instead, so a tier
172
+ that pins a model still names it in the viewer and the agent list, and a profile
173
+ that inherits correctly shows none. The label rides beside the snapshot rather
174
+ than inside it: the snapshot is a durable policy record a managed tombstone
175
+ persists and revalidates, and a cosmetic string does not belong in it.
176
+
177
+ Workflow resume now keys each cached call on the policy for that call's own tier
178
+ rather than on a whole-catalogue fingerprint, so defining or editing an unrelated
179
+ tier no longer forces a full re-execution — but only for a call that names its
180
+ tier. A call that names none keeps the whole-catalogue key, because the host
181
+ resolves those as `call > agent frontmatter > defaultTier` and frontmatter is
182
+ not on this wire: an agent reaches a managed caller as a name. Keyed on
183
+ `defaultTier`, such a call would be wrong twice over — replaying stale work
184
+ after an edit to the tier its agent actually declares, and re-executing after an
185
+ edit to a default it never reached. `agentTierPolicyIdentity()` therefore
186
+ requires a tier rather than accepting `undefined` and folding the default in, so
187
+ the wrong call cannot be written. Journal schema v4 and managed tombstone schema
188
+ v2 are quarantined from older facts rather than migrated.
189
+
190
+ The tier catalogue belongs to the user, names included. The built-in workflows
191
+ and the ad-hoc script ship with this package, so a tier name they use that the
192
+ host does not define is dropped in favour of the host's default rather than
193
+ failing the run — they cannot assert which names exist on someone else's
194
+ machine. A script the user wrote still fails closed on an undefined tier, since
195
+ that is a typo in a catalogue they own. Shipped-ness travels with the script
196
+ rather than with the frame that called it, so a user script that reaches a
197
+ built-in through `workflow(name)` applies the built-in's rule to the built-in;
198
+ the authoring skill now says outright that `low`/`medium`/`high` are the shipped
199
+ profiles rather than names an authored script may assume.
200
+
201
+ The managed routing policy published on the wire reports the default a managed
202
+ call will actually get, fallback included, so a peer's replay identity cannot
203
+ disagree with the host's selection; its sorts are code-unit rather than
204
+ locale-dependent, so the same catalogue fingerprints identically everywhere. The
205
+ workflow peer re-pings per start and resume instead of pinning the catalogue it
206
+ saw at session start, and reports the host-selected tier back through an
207
+ `onTierResolved` callback rather than by mutating the dispatch options.
208
+
209
+ Fixes: a nested spawn no longer relabels an agent's frontmatter tier as
210
+ caller-requested, which had turned a `scopeModels` warning into a refusal and
211
+ misattributed the choice; the scheduler no longer freezes a frontmatter tier into
212
+ a job, so editing the agent file takes effect at fire time; retired managed spawn
213
+ keys are now bounded instead of growing for the life of a session; a
214
+ scope refusal for a tier whose profile inherits its model now names the model
215
+ that would have run instead of the literal `undefined`; and the retired
216
+ `workflow` key warns once per process rather than on every settings read and
217
+ every project write.
218
+
219
+ The tier-key predicate and its length bound now have a single definition, in
220
+ `pi-subagents-protocol`, which `pi-subagents` re-exports. The wire is the
221
+ narrower of the two gates — a key one accepted and the other rejected could
222
+ never reach a peer — so a second copy could only ever be a way for them to
223
+ disagree. That one definition now also governs `blockedProfiles`, which was
224
+ being validated by a helper that trims first: `" low"` became `"low"` there
225
+ while the same value in `profiles` or `defaultTier` was refused outright. All
226
+ three reject.
227
+
228
+ `/agents → Settings` no longer names the fallback tier in the `unset` row's
229
+ label. Which tier `unset` reaches depends on the catalogue, and a workspace that
230
+ edits or deletes the shipped `medium` profile leaves it reaching nothing — at
231
+ which point `unset` and `none` behave identically. The row's description asks
232
+ the resolver what `unset` would currently resolve to and says that, including
233
+ when the answer is "nothing".
234
+
235
+ `pi-workflows` also gained the session-lifecycle handling this routing work
236
+ needed to sit on. The engine has an explicit dispose lifecycle, so a protocol
237
+ probe or a start that is in flight when a session ends rejects with a disposed
238
+ error instead of resolving against an engine nobody owns. The tool, command and
239
+ widget surfaces register exactly once and resolve the *current* engine on each
240
+ use, rather than closing over the one that existed when they were registered —
241
+ a session replacement previously left them pointing at a disposed engine. Tool
242
+ and command handlers report failures as results rather than throwing out of the
243
+ host's dispatch, and the lifecycle handler accepts `subagents:created` and
244
+ `subagents:started` so a spawn's identity is recorded from the first event that
245
+ carries it rather than only at completion.
246
+
247
+ ### Patch Changes
248
+
249
+ - Updated dependencies [c1b1741]
250
+ - @signalridge/pi-subagents-protocol@1.4.0
251
+
3
252
  ## 1.4.0
4
253
  ### Minor Changes
5
254
 
package/README.md CHANGED
@@ -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", strength: "low" }),
58
+ () => agent("Check for secrets", { label: "secrets", strength: "low" }),
59
+ () => agent("Check for dead code", { label: "dead-code", strength: "low" }),
60
60
  ]);
61
61
 
62
62
  phase("synthesize");
@@ -77,15 +77,17 @@ user's own LLM and run on the user's own machine.
77
77
 
78
78
  ### Runtime globals
79
79
 
80
- - `agent(prompt, { label, phase, schema, model, tier, isolation, thread, agentType, toolset, excludeTools, timeoutMs, retries })`
80
+ - `agent(prompt, { label, phase, schema, strength, isolation, thread, agentType, toolset, excludeTools, timeoutMs, retries })`
81
81
  — dispatch one subagent through the managed spawn protocol. Returns text, a
82
82
  schema-validated value, or recoverable `null` after retries. Replayed journal
83
83
  calls consume no real-dispatch cap or token/phase budget. `schema` is a
84
84
  plain JSON Schema; the reply is parsed and validated client-side with bounded
85
85
  repair across attempts, and exhaustion throws `SCHEMA_NONCOMPLIANCE`.
86
- Explicit `model` and `isolation: "worktree"` are resolved and enforced by pi-subagents; `tier` remains the provider-neutral route.
87
- `tier`; see "Model routing"). `thread` chains sequential turns within a named
88
- conversation thread.
86
+ `strength` 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.
89
91
  - `parallel(thunks)` — run thunks concurrently, preserve input order. Recoverable
90
92
  thunk failures become `null`; non-recoverable failures (token budget, agent
91
93
  limit) halt the run after the batch barrier settles. Fan-out that breaches
@@ -148,9 +150,12 @@ workflow, and the review skill before accepting a topology or recovery change.
148
150
  - `workflow_control`: `list`, `get`, `pause`, `resume`, `stop`, or `rm` a run; `rm` writes a durable removal tombstone.
149
151
  - `pi.events` emits `pi-workflows:runtime` snapshots (`{ runId, event }`) for phase, task, quality, retry, and nested-workflow progress; observer failures are contained.
150
152
  - `/workflows`: TUI navigator plus `run`, `status`, `watch`, `stop`, `pause`,
151
- `resume`, `rm`, and `save <name> [runId]` subcommands.
152
- - `/workflows-models`: points at pi-subagents' `/agents → Model tiers` — there
153
- is deliberately no second tier configuration file.
153
+ `resume`, `rm`, `save <name> [runId]`, and `strength` subcommands.
154
+ - `/workflows strength`: `[<low|medium|high> <tier>|off]` — which Agent tier each
155
+ workflow strength runs on, and the tiers this host defines. Unconfigured hosts
156
+ start on a shipped default table (identity, where the host defines the name).
157
+ The tiers themselves are still configured only in pi-subagents'
158
+ `/agents → Model tiers`; there is deliberately no second tier configuration file.
154
159
  - `/effort` and `/ultracode`: standing `off|high|ultra` effort guidance that auto-arms substantive messages.
155
160
  - `/workflows-progress`: `compact|detailed|status|max <N>` controls the persisted live panel.
156
161
  - `/deep-research`, `/adversarial-review`, `/code-review`,
@@ -184,12 +189,185 @@ written atomically with a backup.
184
189
 
185
190
  ## Model routing
186
191
 
187
- 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.
192
+ A workflow speaks one word about cost: **strength**, this package's own name for
193
+ how much effort a step deserves. `low`, `medium`, `high` — that is the whole
194
+ vocabulary, and a script may name nothing else.
195
+
196
+ ```js
197
+ await agent("summarize this diff", { strength: "low" })
198
+ await agent("design the migration", { strength: "high" })
199
+ await agent("the ordinary case") // no strength: see below
200
+ ```
201
+
202
+ There is no default strength. A call that names none dispatches with **no tier**,
203
+ exactly as an unmapped strength does, and pi-subagents resolves it the way it
204
+ resolves any other spawn: the agent type's own frontmatter tier, then
205
+ `agentTiers.defaultTier`. That matters because a tier a call requests outranks
206
+ the agent type's own — so a default strength would have pinned every unlabelled
207
+ call to whatever it mapped to, overruling `Explore`'s shipped `tier: low` and
208
+ re-pricing the one agent this indirection exists to leave alone. Label the calls
209
+ whose cost you mean to steer; leave the rest to the host.
210
+
211
+ A strength is not an Agent tier. A `strengths` table is the only thing that
212
+ binds one to a tier:
213
+
214
+ ```jsonc
215
+ // <agent dir>/workflows/settings.json, or the per-project settings file
216
+ { "strengths": { "low": "cheap-search", "high": "deep" } }
217
+ ```
218
+
219
+ **An unmapped strength dispatches with no tier at all** and takes the agent's
220
+ ordinary default — the agent's own frontmatter tier, then the host's configured
221
+ default. `{}` is a real table meaning exactly that for every strength.
222
+
223
+ ### The default table
224
+
225
+ A host that has configured nothing does not get "no mappings". It gets a table
226
+ this package ships: each strength on the catalogue tier of the same name,
227
+ **wherever the host defines one**. On a stock install that is
228
+ `low → low, medium → medium, high → high`; on a host whose tiers are called
229
+ `cheap` and `deep` it is empty, and every strength is unmapped. It is computed
230
+ against the live catalogue rather than hardcoded, so it is never an assertion
231
+ about someone else's tier names and never produces a start-up complaint about
232
+ configuration you did not write.
233
+
234
+ Without it, every shipped script's `low`/`medium` distinction would collapse
235
+ onto pi-subagents' managed default — which is the *more* expensive rung, so an
236
+ unconfigured machine would run its fan-outs dearer, not cheaper.
237
+
238
+ That it is a **table and not a fallback rule** is the whole design. A rule
239
+ saying "an unmapped strength runs on the tier of the same name" would be
240
+ unremovable, so making a 26-agent fan-out cheaper would still mean editing tier
241
+ `low` itself — dragging the `Explore` agent and everything else that names it. A
242
+ table is replaced by writing one, and a written table that omits `low` leaves it
243
+ unmapped even on a host that defines a tier called `low`. Nothing is ever
244
+ inferred from spelling at dispatch: the binding is always a table entry.
245
+
246
+ So `low → low` in a written table is a real entry and not a no-op — a table you
247
+ write replaces the default outright, so the entries you leave out are unmapped,
248
+ not inherited.
249
+
250
+ ### Editing it
251
+
252
+ `/workflows strength` edits the table and also prints the tiers this host
253
+ defines — the one place both halves are visible, since the strengths come from
254
+ this package and the tiers from pi-subagents:
255
+
256
+ ```
257
+ /workflows strength # show the effective table and the host's tiers
258
+ /workflows strength low cheap # run workflow strength `low` on Agent tier `cheap`
259
+ /workflows strength low off # unmapped; back to the agent's own default
260
+ ```
261
+
262
+ Edits land in **this project's** settings, like every other setting this package
263
+ owns. Because the table replaces rather than merges at each level, the command
264
+ seeds the write from the table a run would actually use — the project file, else
265
+ global, else the shipped default — so setting one entry cannot silently discard
266
+ the rest, and `off` clears a mapping this project never wrote. The write is
267
+ therefore a full copy of the current table into the project; edit
268
+ `<agent dir>/workflows/settings.json` by hand for a machine-wide table.
269
+
270
+ Any key the host's catalogue defines is a legal target — an existing tier, or a
271
+ profile added for exactly this purpose. There is no shortlist and no naming
272
+ convention: the target is checked against the live catalogue at run start, so a
273
+ profile added to `subagents.json` is usable the moment it exists. One hop, never
274
+ chained. An entry whose tier the host does not define is reported once at run
275
+ start and ignored, leaving that strength unmapped — a stale entry costs the
276
+ redirect, not every workflow on the machine.
277
+
278
+ This is not the retired `workflow.tiers` key and must not become it. That one
279
+ carried its own `model` and `thinking` values, which made it a second model
280
+ policy resolved by a second resolver. A value here is a **key in the host's one
281
+ catalogue**: pi-subagents still owns every model, every thinking level, and the
282
+ only `resolveAgentTier()`, and cannot tell a mapped call apart from a spawn that
283
+ named the key itself.
284
+
285
+ There is no per-call `model`, `thinking`, or `tier`, and `meta`/phase metadata
286
+ may not carry them either. A strength is the only model policy a workflow can
287
+ express, which is what keeps a script from pinning a vendor the host did not
288
+ choose.
289
+
290
+ The vocabulary is closed so that a word outside it is a *typo* rather than a
291
+ strength nobody configured: `agent({ strength: "lowe" })` fails before any
292
+ dispatch instead of silently running at the default.
293
+
294
+ The same argument applies one level up, to the option *name*. `agent()` and
295
+ `checkpoint()` reject any key they do not read, rather than ignoring it:
296
+
297
+ ```js
298
+ await agent("x", { tier: "low" }) // → name a strength instead: one of low, medium, high
299
+ await agent("x", { model: "…" }) // → a strength is the only model policy a workflow can express
300
+ await agent("x", { strenght: "low" }) // → valid options are: label, phase, schema, strength, …
301
+ ```
302
+
303
+ An unknown key is dropped before the call hash is built, so it changes neither
304
+ the dispatch nor the resume identity — the run would simply spend at a policy
305
+ nobody chose, and a resume would replay it without noticing. A script is plain
306
+ JavaScript authored against this contract, so there is no other gate: a
307
+ misspelled option would run every step untiered, and a misspelled
308
+ `checkpoint({ headles: "abort" })` would auto-approve in headless mode instead
309
+ of aborting.
310
+
311
+ ### Helpers that dispatch for you
312
+
313
+ `verify()`, `judgePanel()`, and `completenessCheck()` spawn agents on the
314
+ script's behalf, so the script cannot label those calls. Each carries a
315
+ documented default and takes an override, which keeps every dispatch inside the
316
+ vocabulary:
317
+
318
+ ```js
319
+ await verify(finding) // reviewers run at "low"
320
+ await verify(finding, { strength: "high" }) // unless you say otherwise
321
+ await completenessCheck(args, results) // one open-ended read: "medium"
322
+ ```
323
+
324
+ These defaults are an opinion, and an opinion outranks the host's: a tier a call
325
+ requests beats `agentTiers.defaultTier`, so on a machine whose default is `high`
326
+ a `completenessCheck` now runs at whatever `medium` maps to rather than at
327
+ `high`. That is the trade for making the helpers re-routable at all — they
328
+ previously named no tier and could never be steered — and the override above is
329
+ how a script takes it back.
330
+
331
+ ### Resume
332
+
333
+ The table is read fresh at each start and resume — including the resumes the
334
+ engine starts itself, `/workflows resume` and the provider-limit retry — never
335
+ frozen onto a run.
336
+
337
+ Resume identity keys a call on the tier it will actually request, so editing the
338
+ table and resuming re-routes the calls that have not run — and **also re-runs
339
+ the finished calls whose strength moved**, plus everything after the first such
340
+ miss, because their cached answers came from a different tier. Re-routing
341
+ mid-run is therefore a re-spend; if the point of the edit was to spend less,
342
+ start a new run instead of resuming one that is mostly done. The provider-limit
343
+ retry resumes on a timer with no one watching, so a table edited while a run is
344
+ rate-limit-paused is charged the same way without being asked — stop that run
345
+ before editing if the re-spend is not worth it. A nested
346
+ `workflow()` boundary folds the table into its own key for the same reason: the
347
+ value it caches for the whole child frame would otherwise survive a change that
348
+ re-routes the child.
349
+
350
+ Fresh installs of pi-subagents ship `low`/`medium`/`high` tier profiles, all
351
+ inheriting their model — which is what the default table above resolves against,
352
+ so the built-ins run on a new machine, at their own strengths, with no
353
+ configuration. A managed call that still names no tier falls back to `medium`;
354
+ that is where an unmapped strength lands. The fallback is scoped to managed
355
+ calls: it is not the catalogue's `defaultTier`, so an ordinary `Agent` spawn on
356
+ the same machine keeps falling through to `defaultModel` and the parent session.
357
+ A host whose default has been cleared (`noDefaultTier`) rejects an untiered
358
+ managed call rather than quietly inheriting the parent session's model — which
359
+ an unmapped strength can reach.
360
+
361
+ The managed wire, invocation record, journal, tombstone, and resume state all
362
+ carry the one resolved tier — including one the host defaulted to, so a run's
363
+ record says what actually ran. Resume keys each cached call on the policy for
364
+ *that call's* tier: editing an unrelated tier does not invalidate work that never
365
+ used it.
188
366
 
189
367
  ## Intentional adaptations from upstream
190
368
 
191
369
  - **No host-side web tools.** Agents reach the web through configured pi-web-access/MCP tools or an agent type's tool policy.
192
- - **Agent registry and tier catalogue ownership.** `agentType` and `small|medium|large` profiles resolve in pi-subagents, avoiding a second registry/config schema.
370
+ - **Agent registry and tier catalogue ownership.** `agentType` and the tier catalogue resolve in pi-subagents; the workflow package supplies only a tier key its own `strengths` table chose, avoiding a second model/thinking policy.
193
371
  - **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.
194
372
 
195
- State is persisted as `pi.appendEntry("pi-workflows:journal", ...)` custom entries (schema v3), including frozen invocation args, terminal result previews, script revisions, named nested-workflow result boundaries, 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.
373
+ 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.4.0",
3
+ "version": "1.6.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
  },
@@ -2,7 +2,7 @@
2
2
  name: workflow-authoring
3
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.4.0"
5
+ version: "1.6.0"
6
6
  ---
7
7
 
8
8
  # Workflow authoring
@@ -28,10 +28,22 @@ JavaScript.
28
28
  - Bound graph tasks, fan-out items, loops, retries, agents, concurrency, and
29
29
  evidence size. Invocation-level token and time caps are user constraints, not
30
30
  substitutes for algorithmic bounds.
31
- - Route an agent call with `tier: "small"`, `"medium"`, or `"large"`; omit
32
- `tier` to use the pi-subagents workflow default. Never guess nonstandard model
33
- routes, tiers, or agent types—use a name supplied by the current environment
34
- and follow [registry ownership](references/registry-ownership.md).
31
+ - Route an agent call with `strength: "low" | "medium" | "high"` — this
32
+ package's own word for how much effort a step deserves, and the only routing
33
+ word a workflow has. It is **not** an agent-tier key: a `strengths` table
34
+ alone binds a strength to a tier — the user's, or the shipped default on a
35
+ host that configured none — and a strength no table defines dispatches with no
36
+ tier and takes the agent's own default. So name the effort the step deserves
37
+ and never reach for a catalogue name; a word outside the three is rejected
38
+ before dispatch, as is `tier:` itself. There is no default: omitting
39
+ `strength` dispatches with no tier and lets the agent type's own tier stand,
40
+ so label the calls whose cost you mean to steer and leave the rest alone.
41
+ There is no per-call `model`/`thinking`/`tier`, and none may
42
+ appear in workflow meta or phase profiles. `agent()` and `checkpoint()` reject
43
+ any option name they do not read rather than ignoring it, so pass only the
44
+ documented keys. Never guess an agent-type name—use
45
+ names supplied by the current environment and follow
46
+ [registry ownership](references/registry-ownership.md).
35
47
  - Workflows have no imports, filesystem/network APIs, timers, or unrestricted
36
48
  Node APIs. Pass timestamps, randomness, and external decisions through `args`.
37
49
 
@@ -53,7 +65,9 @@ JavaScript.
53
65
  the child can see the parent conversation.
54
66
  6. **Add quality only where it changes a decision.** Use a schema before reading
55
67
  structured fields, then use `verify`, `judgePanel`, `gate`, or
56
- `completenessCheck` with explicit bounds.
68
+ `completenessCheck` with explicit bounds. These dispatch on your behalf, so
69
+ pass `strength` when the default (`low`, or `medium` for
70
+ `completenessCheck`) is not the effort that gate deserves.
57
71
  7. **Aggregate and report.** Return a bounded JSON result containing the answer,
58
72
  coverage, and important failures. If the workflow runs in the background,
59
73
  the run ID is the handle for later control/resume.
@@ -173,6 +187,8 @@ Before shipping a workflow change:
173
187
  - read [debugging](references/debugging.md) when reproducing a failure;
174
188
  - read [the review checklist](references/review.md) before accepting topology;
175
189
  - read [the generated capability index](references/capabilities.md) when a
176
- signature, default, or support boundary is disputed;
190
+ signature, default, or support boundary is disputed; remember that a strength
191
+ is this package's own vocabulary, bound to a host tier only by the user's
192
+ `strengths` table, while model/thinking resolution stays in pi-subagents;
177
193
  - ensure every package-relative example/reference link remains inside the
178
194
  publishable package.
@@ -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.4.0`
4
+ Contract format: `2.0.0`<br>
5
+ Contract content / skill / extension: `1.6.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,15 +11,15 @@ 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>`strength`: "low" \| "medium" \| "high" (optional); default: no tier is sent; 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
17
  | orchestrate | runtime-global | `orchestrate(tasks, options?) => Promise<{ results, tasks }>` | `onError`: "skip-dependents"\|"continue"\|"fail-fast" (optional); default: "skip-dependents" |
18
18
  | workflow | runtime-global | `workflow(savedName, childArgs?) => Promise<unknown>` | — |
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) |
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 |
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)<br>`strength`: "low" \| "medium" \| "high" (optional); default: "low" |
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<br>`strength`: "low" \| "medium" \| "high" (optional); default: "low" |
21
21
  | loopUntilDry | runtime-global | `loopUntilDry({ round, key?, consecutiveEmpty?, maxRounds? }) => Promise<unknown[]>` | `round`: (roundIndex) => unknown[]; default: required<br>`key`: (item) => string (optional); default: JSON.stringify<br>`consecutiveEmpty`: number (optional); default: 2<br>`maxRounds`: number (optional); default: 50 |
22
- | completenessCheck | runtime-global | `completenessCheck(taskArgs, results) => Promise<{ complete, missing? } \| null>` | — |
22
+ | completenessCheck | runtime-global | `completenessCheck(taskArgs, results, options?) => Promise<{ complete, missing? } \| null>` | `strength`: "low" \| "medium" \| "high" (optional); default: "medium" |
23
23
  | retry | runtime-global | `retry(thunk, options?) => Promise<unknown>` | `attempts`: number (optional); default: 3<br>`until`: (result) => boolean (optional); default: accept first result |
24
24
  | gate | runtime-global | `gate(thunk, validator, options?) => Promise<{ ok, value, attempts }>` | `attempts`: number (optional); default: 3 |
25
25
  | checkpoint | runtime-global | `checkpoint(prompt, options?) => Promise<unknown>` | `default`: unknown (optional); default: true when no UI and omitted<br>`headless`: "default" \| "abort" (optional); default: "default"<br>`kind`: "confirm" \| "input" \| "select" (optional); default: "confirm"<br>`choices`: string[] (optional)<br>`timeoutMs`: number (optional) |