@signalridge/pi-workflows 1.5.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,119 @@
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
+
3
117
  ## 1.5.0
4
118
  ### Minor Changes
5
119
 
package/README.md CHANGED
@@ -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: "low" }),
58
- () => agent("Check for secrets", { label: "secrets", tier: "low" }),
59
- () => agent("Check for dead code", { label: "dead-code", tier: "low" }),
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,13 +77,13 @@ 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, 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
- `tier` is the provider-neutral workflow route; `thread` chains sequential turns
86
+ `strength` is the provider-neutral workflow route; `thread` chains sequential turns
87
87
  within a named conversation thread. A legacy programmatic `model` option may
88
88
  still be accepted by non-managed callers, but workflow meta/phase model or
89
89
  thinking fields are rejected and are never a workflow policy source. Pre-schema-v4
@@ -150,9 +150,12 @@ workflow, and the review skill before accepting a topology or recovery change.
150
150
  - `workflow_control`: `list`, `get`, `pause`, `resume`, `stop`, or `rm` a run; `rm` writes a durable removal tombstone.
151
151
  - `pi.events` emits `pi-workflows:runtime` snapshots (`{ runId, event }`) for phase, task, quality, retry, and nested-workflow progress; observer failures are contained.
152
152
  - `/workflows`: TUI navigator plus `run`, `status`, `watch`, `stop`, `pause`,
153
- `resume`, `rm`, and `save <name> [runId]` subcommands.
154
- - `/workflows-models`: points at pi-subagents' `/agents → Model tiers` — there
155
- 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.
156
159
  - `/effort` and `/ultracode`: standing `off|high|ultra` effort guidance that auto-arms substantive messages.
157
160
  - `/workflows-progress`: `compact|detailed|status|max <N>` controls the persisted live panel.
158
161
  - `/deep-research`, `/adversarial-review`, `/code-review`,
@@ -186,52 +189,185 @@ written atomically with a backup.
186
189
 
187
190
  ## Model routing
188
191
 
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.
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.
193
195
 
194
196
  ```js
195
- await agent("summarize this diff", { tier: "low" })
196
- await agent("design the migration", { tier: "high" })
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
197
200
  ```
198
201
 
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.
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.
230
366
 
231
367
  ## Intentional adaptations from upstream
232
368
 
233
369
  - **No host-side web tools.** Agents reach the web through configured pi-web-access/MCP tools or an agent type's tool policy.
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.
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.
235
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.
236
372
 
237
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.5.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",
@@ -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.5.0"
5
+ version: "1.6.0"
6
6
  ---
7
7
 
8
8
  # Workflow authoring
@@ -28,12 +28,21 @@ 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: "<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
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
37
46
  [registry ownership](references/registry-ownership.md).
38
47
  - Workflows have no imports, filesystem/network APIs, timers, or unrestricted
39
48
  Node APIs. Pass timestamps, randomness, and external decisions through `args`.
@@ -56,7 +65,9 @@ JavaScript.
56
65
  the child can see the parent conversation.
57
66
  6. **Add quality only where it changes a decision.** Use a schema before reading
58
67
  structured fields, then use `verify`, `judgePanel`, `gate`, or
59
- `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.
60
71
  7. **Aggregate and report.** Return a bounded JSON result containing the answer,
61
72
  coverage, and important failures. If the workflow runs in the background,
62
73
  the run ID is the handle for later control/resume.
@@ -176,8 +187,8 @@ Before shipping a workflow change:
176
187
  - read [debugging](references/debugging.md) when reproducing a failure;
177
188
  - read [the review checklist](references/review.md) before accepting topology;
178
189
  - 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;
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;
182
193
  - ensure every package-relative example/reference link remains inside the
183
194
  publishable package.
@@ -2,7 +2,7 @@
2
2
  # Workflow capability index
3
3
 
4
4
  Contract format: `2.0.0`<br>
5
- Contract content / skill / extension: `1.5.0`
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>`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 |
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) |
@@ -2,7 +2,7 @@
2
2
  # Exhaustive workflow capability facts
3
3
 
4
4
  Contract format: `2.0.0`<br>
5
- Contract content / skill / extension: `1.5.0`
5
+ Contract content / skill / extension: `1.6.0`
6
6
 
7
7
  Every exact fact below is projected from the installed extension's capability contract. Explanatory judgment belongs in the hand-written references next to this file.
8
8
 
@@ -16,7 +16,7 @@ Every exact fact below is projected from the installed extension's capability co
16
16
  - `label`: string (optional); default: derived from phase and call count
17
17
  - `phase`: string (optional); default: current phase
18
18
  - `schema`: plain JSON Schema (optional)
19
- - `tier`: agent tier key (optional); default: the agent's own tier, else agentTiers.defaultTier
19
+ - `strength`: "low" \| "medium" \| "high" (optional); default: no tier is sent; the agent's own tier, else agentTiers.defaultTier
20
20
  - `isolation`: "worktree" (optional); default: pi-subagents worktree policy
21
21
  - `thread`: string (optional); default: fresh session per call
22
22
  - `toolset`: string (optional); default: configured host/agent toolset hint
@@ -29,11 +29,15 @@ Every exact fact below is projected from the installed extension's capability co
29
29
  - Constraint: resume replays only the longest unchanged prefix; the first miss and every later call execute live
30
30
  - Constraint: replayed calls do not consume the real-dispatch maxAgents or token/phase budgets
31
31
  - Constraint: spawnKeys rotate by generation on resume so pi-subagents never raises a fingerprint conflict
32
- - Constraint: tier names a key in the host's agentTiers catalogue; a key it does not define is rejected before dispatch
32
+ - Constraint: strength is this package's own vocabulary, not an agentTiers key; a word outside low/medium/high is rejected before dispatch
33
+ - Constraint: a strengths table alone binds a strength to an agent tier; an unmapped strength dispatches with no tier and takes the agent's own default
34
+ - Constraint: a call that names no strength also dispatches with no tier; there is no default strength, so an unlabelled call never outranks the agent type's own tier
35
+ - Constraint: an unconfigured host uses a shipped default table: each strength on the catalogue tier of the same name, where the host defines one
33
36
  - Constraint: resolveAgentTier in pi-subagents owns model, thinking, clamping, availability, and the resolution snapshot
34
- - Constraint: there is no per-call model or thinking: a tier is the only model policy a workflow can express
37
+ - Constraint: there is no per-call model, thinking, or tier: a strength is the only model policy a workflow can express
35
38
  - Constraint: same-thread calls must be sequential and preserve a stable workflow thread name
36
39
  - Constraint: threads cannot be combined with worktree isolation
40
+ - Constraint: an option name outside this list is rejected before dispatch, not ignored; tier, model, and thinking name their replacement
37
41
 
38
42
  <a id="runtime-global-parallel"></a>
39
43
  ## parallel
@@ -91,6 +95,7 @@ Every exact fact below is projected from the installed extension's capability co
91
95
  - `reviewers`: number (optional); default: 2
92
96
  - `threshold`: number (optional); default: 0.5
93
97
  - `lens`: string \| string[] (optional)
98
+ - `strength`: "low" \| "medium" \| "high" (optional); default: "low"
94
99
  - Constraint: threshold comparison is inclusive; real is false when no reviewer succeeds
95
100
 
96
101
  <a id="runtime-global-judgepanel"></a>
@@ -101,6 +106,7 @@ Every exact fact below is projected from the installed extension's capability co
101
106
  - Signature: `judgePanel(attempts, options?) => Promise<{ index, attempt, score, judgments } \| undefined>`
102
107
  - `judges`: number (optional); default: 3
103
108
  - `rubric`: string (optional); default: overall quality and correctness
109
+ - `strength`: "low" \| "medium" \| "high" (optional); default: "low"
104
110
  - Constraint: highest mean score wins with stable input index as the tie-break; empty input returns undefined
105
111
 
106
112
  <a id="runtime-global-loopuntildry"></a>
@@ -120,7 +126,8 @@ Every exact fact below is projected from the installed extension's capability co
120
126
 
121
127
  - Classification: `runtime-global`
122
128
  - Support: `supported`
123
- - Signature: `completenessCheck(taskArgs, results) => Promise<{ complete, missing? } \| null>`
129
+ - Signature: `completenessCheck(taskArgs, results, options?) => Promise<{ complete, missing? } \| null>`
130
+ - `strength`: "low" \| "medium" \| "high" (optional); default: "medium"
124
131
  - Constraint: only the first 4,000 characters of serialized result evidence are sent to the critic
125
132
 
126
133
  <a id="runtime-global-retry"></a>
@@ -155,6 +162,7 @@ Every exact fact below is projected from the installed extension's capability co
155
162
  - `timeoutMs`: number (optional)
156
163
  - Constraint: consumes one agent slot and no tokens
157
164
  - Constraint: journaled answers replay only within an unchanged resume prefix
165
+ - Constraint: an option name outside this list is rejected rather than ignored, since a dropped headless silently auto-approves
158
166
 
159
167
  <a id="runtime-global-log"></a>
160
168
  ## log
@@ -8,7 +8,7 @@ Start from the symptom, then reproduce through the real workflow runtime with de
8
8
  | `parallel()` rejects input | Promises were passed instead of thunks | [runtime](runtime.md#topology) |
9
9
  | Synthesis starts early | Fan-out was not awaited as one complete result set | [pattern selection](pattern-selection.md#fan-out-and-synthesize) |
10
10
  | Coverage silently disappears | `null` results were filtered before IDs were ledgered | [lifecycle](lifecycle.md#retry-and-recoverable-failure) |
11
- | Wrong model or thinking is used | The call named no tier and fell to a default, or named one whose profile is not what you expected | [registry ownership](registry-ownership.md#priority) |
11
+ | Wrong model or thinking is used | The call's strength is unmapped and fell to the agent's default, or the active `strengths` table — the user's, or the shipped default — points it at a profile that is not what you expected | [registry ownership](registry-ownership.md#priority) |
12
12
  | Unknown `agentType` log | A live registry name was guessed or is unavailable | [registry ownership](registry-ownership.md#agent-types) |
13
13
  | Budget exceeds the number shown | The budget is a soft pre-call gate and work was in flight | [lifecycle](lifecycle.md#bounds-and-budget) |
14
14
  | Later calls rerun on resume | An earlier call missed or changed, ending the replayable prefix | [lifecycle](lifecycle.md#resume) |
@@ -4,5 +4,5 @@ Keep work IDs outside helper results that may omit failed agents.
4
4
 
5
5
  | Call | Contract |
6
6
  | --- | --- |
7
- | `verify(item, { reviewers: number, threshold: number, lens: string | string[] })` | Defaults: 2 reviewers, inclusive `0.5`, one lens or a cycled array. Returns `{ real, realCount, total, votes }`. Failed reviewers are omitted; successful votes are the denominator; zero survivors means `real: false`. |
8
- | `judgePanel(attempts, { judges: number, rubric: string })` | Defaults: 3 judges and `"overall quality and correctness"`. Failed judgments are omitted. Returns the highest mean `{ index, attempt, score, judgments }`; input order wins ties; empty input returns `undefined`. |
7
+ | `verify(item, { reviewers: number, threshold: number, lens: string | string[], strength })` | Defaults: 2 reviewers, inclusive `0.5`, one lens or a cycled array, and `strength: "low"` — reviewers are numerous and narrow, and the vote carries the gate rather than any one reviewer's depth. Returns `{ real, realCount, total, votes }`. Failed reviewers are omitted; successful votes are the denominator; zero survivors means `real: false`. |
8
+ | `judgePanel(attempts, { judges: number, rubric: string, strength })` | Defaults: 3 judges, `"overall quality and correctness"`, and `strength: "low"`. Failed judgments are omitted. Returns the highest mean `{ index, attempt, score, judgments }`; input order wins ties; empty input returns `undefined`. |
@@ -4,9 +4,9 @@ Model routes and agent types are dynamic references. Their shape and owner are d
4
4
 
5
5
  ## Model routes
6
6
 
7
- pi-subagents owns the agent-tier catalogue; a workflow names a key from it and nothing else. Available tier names depend on active configuration and are intentionally absent from static skill files — read them from the destination context.
7
+ pi-subagents owns the agent-tier catalogue. A workflow never names a key from it: a script names a *strength* (`low`/`medium`/`high`), and the user's `strengths` table is the only thing that binds one to a tier. Tier names depend on active configuration and are intentionally absent from static skill files; you do not need them, because you never write one.
8
8
 
9
- A name a script writes is not a claim that the name exists. Scripts that ship with this package name `low`/`medium`/`high` — the profiles a fresh install receives — and a host that does not define one of those drops it in favour of that host's default, with a log line. A script you write is held to the stricter rule: an undefined tier is a typo in a catalogue you own, so it is rejected before dispatch rather than rerouted. Write the tier you mean for the host you are targeting; do not copy `low`/`medium`/`high` in on the assumption that every machine has them.
9
+ A strength is a statement about the work, not a claim about the host. Every script — shipped or yours — names the same three, and there is no shipped/user distinction, because a strength can never reach the catalogue by itself: the binding is always an entry in a `strengths` table. A host that configured none uses the shipped default table, which is identity wherever the host defines a tier of the same name, so a stock machine honours your `low`/`medium` distinctions without configuration and a host with differently-named tiers leaves every strength unmapped. A user table replaces that default outright, and a strength it omits is unmapped even where a same-named tier exists — which is what lets workflow work be re-priced without touching the tier every ordinary spawn also names.
10
10
 
11
11
  ## Agent types
12
12
 
@@ -14,6 +14,6 @@ The agent registry owns agent-type names and their bound instructions, tools, Ag
14
14
 
15
15
  ## Priority
16
16
 
17
- A call's `tier` takes precedence over an agent type's own tier default, which takes precedence over `agentTiers.defaultTier`. `resolveAgentTier()` in pi-subagents then resolves model, thinking, clamping, availability, and the immutable snapshot as one decision. Neither a call, nor workflow meta, nor a phase may carry model or thinking fields.
17
+ A call's strength resolves through the user's `strengths` table into at most one tier key. That key, when there is one, takes precedence over an agent type's own tier default, which takes precedence over `agentTiers.defaultTier`. A call that names no strength, or one whose strength the table does not define, sends no key and leaves that precedence to the host — which is why there is no default strength: one would overrule every agent type's own tier on calls the script never meant to route. `resolveAgentTier()` in pi-subagents then resolves model, thinking, clamping, availability, and the immutable snapshot as one decision. Neither a call, nor workflow meta, nor a phase may carry model, thinking, or tier fields.
18
18
 
19
- Unavailability is fail-closed: pi-subagents names the tier and does not silently run a different model. A managed call with no tier and no applicable default is rejected rather than inheriting the parent session.
19
+ Unavailability is fail-closed: pi-subagents names the tier and does not silently run a different model. A managed call with no tier and no applicable default is rejected rather than inheriting the parent session — which an unmapped strength can reach on a host whose default has been cleared.