@signalridge/pi-workflows 1.5.0 → 1.7.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.
Files changed (29) hide show
  1. package/CHANGELOG.md +227 -0
  2. package/README.md +207 -48
  3. package/package.json +2 -2
  4. package/skills/workflow-authoring/SKILL.md +44 -14
  5. package/skills/workflow-authoring/examples/defensive-json-parsing.js +1 -1
  6. package/skills/workflow-authoring/examples/phased-budgets.js +1 -1
  7. package/skills/workflow-authoring/examples/saved-nested-workflows.js +1 -1
  8. package/skills/workflow-authoring/examples/structured-output.js +1 -1
  9. package/skills/workflow-authoring/references/capabilities.md +7 -7
  10. package/skills/workflow-authoring/references/capability-details.md +24 -10
  11. package/skills/workflow-authoring/references/debugging.md +1 -1
  12. package/skills/workflow-authoring/references/lifecycle.md +5 -1
  13. package/skills/workflow-authoring/references/quality-helpers.md +2 -2
  14. package/skills/workflow-authoring/references/registry-ownership.md +4 -4
  15. package/skills/workflow-authoring/references/review.md +8 -4
  16. package/skills/workflow-authoring/references/runtime.md +2 -2
  17. package/skills/workflow-authoring/references/specialized-helpers.md +1 -1
  18. package/skills/workflow-authoring/references/versions.md +1 -1
  19. package/skills/workflow-patterns/SKILL.md +1 -1
  20. package/skills/workflow-review/SKILL.md +6 -4
  21. package/src/builtins.ts +23 -23
  22. package/src/capabilities.ts +37 -12
  23. package/src/engine.ts +70 -26
  24. package/src/index.ts +243 -70
  25. package/src/journal.ts +0 -14
  26. package/src/rpc-client.ts +14 -1
  27. package/src/runtime.ts +452 -103
  28. package/src/strengths.ts +93 -0
  29. package/src/workflow-settings.ts +95 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,232 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.7.0
4
+ ### Minor Changes
5
+
6
+ - f05185e: A workflow now runs at the width of the host's subagent pool rather than a
7
+ number it picked blind, and stops charging queue time against the agent waiting
8
+ in it.
9
+
10
+ Two facts were missing on the pi-workflows side, and they compounded. It had no
11
+ way to learn `maxConcurrent`, so its own width was a constant guess. And a
12
+ dispatched agent was treated as a running one, though a managed spawn takes a
13
+ background slot like any other and waits when the pool is full, so a batch
14
+ dispatched behind a busy pool could fail as timeouts with none of it having run.
15
+
16
+ - The ping reply can now carry the peer's live `maxConcurrent`, and pi-workflows
17
+ makes it both the default width and the ceiling: an unnamed `concurrency` takes
18
+ the pool size, and a named one is clamped to it, with the clamp recorded in the
19
+ run log so a caller who asked for a number learns why it got another. It is
20
+ read at each start and resume rather than frozen onto the run, because the pool
21
+ is a setting the user can change in between. One setting therefore governs the
22
+ whole fleet, which is what the ownership boundary already said and what the
23
+ code can now honour.
24
+ - **This narrows the common case, and that is the point.** The pool defaults to
25
+ 4 background slots, so a default pair now runs a fan-out four at a time where
26
+ it used to dispatch `hardwareConcurrency - 2`. Nothing runs slower for it —
27
+ dispatching twelve into four slots only ever queued eight, ahead of the host's
28
+ own spawns — but the width a run reports is now the width it actually has, and
29
+ `maxConcurrent` is the single place to raise it for workflows and every other
30
+ background agent at once. A host that had already raised it gets the wide
31
+ fan-outs its setting always implied.
32
+ - The field is **requested by name** (`include: ["maxConcurrent"]`) rather than
33
+ volunteered. A ping envelope is parsed with `rejectUnknownKeys`, so a peer that
34
+ simply added a field would make every already-published caller reject the
35
+ handshake and lose workflows entirely. Asking keeps both directions working: an
36
+ older peer answers without it and the caller falls back to 16, and a caller that
37
+ never asks gets the v4 envelope unchanged. No version bump, no capability, no
38
+ lockstep release. Both READMEs document the mechanism, since `include` is now
39
+ part of the public cross-extension contract.
40
+ - `agentTimeoutMs` now counts from the moment the host reports the agent left its
41
+ queue. The clock is still armed at dispatch and restarted on that report, not
42
+ armed only by it, so a report that never arrives degrades to the old behaviour
43
+ rather than to an agent that can hang forever. A report that arrives *after* the
44
+ agent settled is ignored, so a late or duplicate one cannot leave a live timer
45
+ behind to abort a finished agent.
46
+
47
+ The engine already subscribed to `subagents:started` and already filtered it by
48
+ workflow owner, so the start signal is the event it was discarding.
49
+
50
+ `getMaxConcurrent` is required on the RPC bridge rather than optional. The other
51
+ bridge members are capabilities, advertised in the ping and rejected outright
52
+ when missing, so forgetting one fails loudly; a missing pool size has no
53
+ capability bit and no failure — the caller would silently fall back to its own
54
+ guess and run at the wrong width. Required is what makes that omission a compile
55
+ error instead of a quiet wrong answer.
56
+ - f05185e: Stop the run's own limits from being the thing that keeps a fan-out small.
57
+
58
+ Every execution limit a caller can pass — `maxAgents`, `concurrency`,
59
+ `agentRetries`, `tokenBudget`, `agentTimeoutMs` — reached the model as a bare
60
+ number with no stated default, while the authoring skill asked it to "set finite
61
+ bounds that match the work" for three of them. The result was a caller that
62
+ priced the run before writing it: fan-outs of one to eight, `concurrency: 1`,
63
+ and per-agent wall clocks of two to five minutes. Each of those values ends a run
64
+ rather than shaping it — `AGENT_LIMIT_EXCEEDED` partway through, or a whole
65
+ batch failing as timeouts — so the precaution removed coverage and not cost.
66
+
67
+ Four changes, and the first two matter most:
68
+
69
+ - Every numeric tool input now states its default and says to omit it, and the
70
+ guidelines ask for a fan-out sized to the evidence the task has: one agent per
71
+ independent question, ten to thirty for an ordinary review or audit. The gain
72
+ named is coverage, not speed — one agent asked one question reads further into
73
+ it than one agent asked four — because how many of them run at once is the
74
+ host's pool and not the script's to decide. The authoring and review skills no
75
+ longer treat the invocation limits as the place to express a bound — that
76
+ belongs to the script's own task counts, loop rounds, and fan-out widths, which
77
+ is where the topology is decided. `lifecycle.md` also stops citing two settings
78
+ keys that never existed (`defaultAgentTimeoutMs`, `defaultTokenBudget`).
79
+ - `agentTimeoutMs` defaults to 30 minutes rather than 5. It exists to bound a
80
+ hang; an agent's real budget is the turn, tool and token ceiling pi-subagents
81
+ applies to it, which a deep call legitimately spends over many minutes. A
82
+ default an order of magnitude under that budget turned every wide fan-out of
83
+ real work into a batch of timeouts. `null` still removes the wall clock.
84
+ - Concurrency defaults to 16 and clamps at 64, up from a default of
85
+ `hardwareConcurrency - 2` clamped at 16. Core count was the wrong quantity: a
86
+ dispatched agent is a remote call this process waits on, so the old default
87
+ fanned a 14-core host out 12 ways and a 4-core host 2 ways for identical work.
88
+ pi-subagents' `maxConcurrent` remains the authoritative throttle — this is the
89
+ guard against a runaway script, and a host that wants a narrower fleet still
90
+ narrows it in one place for every agent it runs.
91
+ - `strength` gains a criterion instead of three bare words. `high` now means a
92
+ step whose error is expensive to reverse — an architecture decision, a
93
+ destructive migration, a security boundary — normally absent from a fan-out and
94
+ at most one closing step, and the authoring skill and review checklist both say
95
+ so. The ULTRA effort directive stops asking for it outright: it used to end
96
+ `give the deepest steps strength: "high"`, which priced every branch of a wide
97
+ run at the cost of the one that mattered, and now points the exhaustiveness at
98
+ width and reserves `high` for a step that has that property, if the run has one.
99
+ Nothing about the mechanism changed — there is still no default strength, and a
100
+ call naming none still dispatches with no tier.
101
+ - `/workflows run` plans up to 24 tasks instead of 8. The planner's JSON schema
102
+ was the only bound on the one path where the model decides the width itself,
103
+ and 8 was below what an ordinary audit decomposes into. The synthesis step
104
+ absorbs the extra tasks by shrinking its per-worker excerpt: measured against
105
+ the 78,000-character context budget, 8 tasks contribute about 6,000 characters
106
+ each and 24 contribute about 2,500 — three times the coverage at roughly 40%
107
+ of the depth per worker, and about a quarter more total evidence reaching the
108
+ synthesizer. Widening past that would need a larger budget, which the wire's
109
+ 100,000-character prompt cap does not leave room for.
110
+
111
+ ### Patch Changes
112
+
113
+ - Updated dependencies [f05185e]
114
+ - @signalridge/pi-subagents-protocol@1.5.0
115
+
116
+ ## 1.6.0
117
+ ### Minor Changes
118
+
119
+ - fabe89a: A workflow now speaks one routing word of its own: **strength**.
120
+
121
+ `agent({ tier })` is gone. A script names `strength: "low" | "medium" | "high"` —
122
+ this package's own word for how much effort a step deserves — and the new
123
+ `strengths` setting is the only thing that binds one to an Agent tier. A strength
124
+ no table defines dispatches with **no tier at all** and takes the agent's ordinary
125
+ default, so nothing is inferred from spelling at dispatch: the binding is always a
126
+ table entry.
127
+
128
+ That inference was the bug. The built-ins ask for `low`, and so does the `Explore`
129
+ agent and everything that names no tier of its own, so pointing `low` at a cheaper
130
+ model to make a 26-agent fan-out affordable dragged every ordinary search spawn
131
+ with it. Pointing a *strength* elsewhere does not.
132
+
133
+ A host that has configured nothing gets a shipped default table rather than no
134
+ mappings: each strength on the catalogue tier of the same name, wherever the host
135
+ defines one — identity on a stock install, empty on a host whose tiers are called
136
+ `cheap`/`deep`. It is computed against the live catalogue, so it is never an
137
+ assertion about someone else's tier names and never complains at run start about
138
+ configuration nobody wrote. Without it every shipped script's `low`/`medium`
139
+ distinction would collapse onto pi-subagents' managed `medium` fallback — the
140
+ *more* expensive rung — and an unconfigured machine would run its fan-outs dearer
141
+ than before this change.
142
+
143
+ The distinction that makes that safe is that it is a **table, not a fallback
144
+ rule**. A rule would be unremovable, and re-pricing a fan-out would again mean
145
+ editing the shared tier. A table is replaced by writing one, a written table that
146
+ omits `low` leaves it unmapped even where a tier called `low` exists, and `{}` is
147
+ a real table meaning "map nothing".
148
+
149
+ This is not the retired `workflow.tiers` key. That one carried its own `model` and
150
+ `thinking`, making it a second model policy behind a second resolver. A value here
151
+ is a key in the host's one catalogue: pi-subagents still owns every model, every
152
+ thinking level, and the only `resolveAgentTier()`, and cannot tell a mapped call
153
+ apart from a spawn that named the key itself. No protocol change.
154
+
155
+ The vocabulary is closed, which buys back what the passthrough used to provide.
156
+ A word outside the three is a typo, not a strength nobody configured, so it fails
157
+ before dispatch. With no way for a script name to reach the catalogue, the
158
+ shipped-vs-user tier rule (`shippedScript`) had nothing left to decide and is
159
+ removed, along with its `run_created` journal field.
160
+
161
+ The same argument applies one level up, so `agent()` and `checkpoint()` now
162
+ reject any option name they do not read instead of ignoring it. `tier`, `model`,
163
+ and `thinking` answer by naming their replacement; anything else lists the legal
164
+ keys, and a non-object options value is refused rather than reaching a bare
165
+ `TypeError`. An unknown key is dropped before the call hash is built, so it moves
166
+ neither the dispatch nor the resume identity: `agent({ strenght: "low" })` would
167
+ have run every step untiered and replayed that way, and
168
+ `checkpoint({ headles: "abort" })` would have auto-approved in headless mode
169
+ instead of aborting. Neither leaves a log line. A script is plain JavaScript
170
+ authored against this contract, so nothing else was going to catch it. Both key
171
+ lists are pinned to their interfaces by a `satisfies Record<keyof …>` clause, in
172
+ both directions.
173
+
174
+ There is deliberately no default strength: a call that names none dispatches with
175
+ no tier, exactly as an unmapped one does, and the host resolves it the way it
176
+ resolves every other spawn. A default was tried and is wrong — a tier a call
177
+ requests outranks the agent type's own, so on a stock install (`medium → medium`)
178
+ every unlabelled dispatch would have pinned `medium` over whatever the agent
179
+ declared, overruling `Explore`'s shipped `tier: low` and re-pricing in the wrong
180
+ direction the one agent this indirection exists to leave alone. Nothing is lost:
181
+ a script that named no strength stated no opinion, and pi-subagents' ordinary
182
+ precedence is still user-owned policy.
183
+
184
+ What the table must reach is every call a script did label, and the shipped
185
+ scripts label all of theirs. The three helpers that dispatch on the script's
186
+ behalf — `verify`, `judgePanel`, `completenessCheck` — took no strength at all
187
+ and so could never be re-routed; they now accept one, defaulting to `low`, `low`,
188
+ and `medium`.
189
+
190
+ Those defaults are an opinion where there was none, and an opinion outranks the
191
+ host's: a tier a call requests beats `agentTiers.defaultTier`, so on a machine
192
+ whose default is `high` a `completenessCheck` now runs at whatever `medium` maps
193
+ to rather than at `high`. That is the price of making the helpers steerable at
194
+ all — a helper that names no tier can never be pointed anywhere — and the new
195
+ option is how a script takes it back.
196
+
197
+ Fixes a re-spend bug: `strengths` is deliberately never frozen onto a run, but the
198
+ resumes the engine starts itself (`/workflows resume`, the provider-limit retry)
199
+ passed no table, and since a call's identity keys on the tier it requested, every
200
+ finished call missed its journal entry and re-ran untabled. The engine now reads
201
+ the current table for those.
202
+
203
+ `/workflows-models` is gone; the table is edited with `/workflows strength`
204
+ (`[<low|medium|high> <tier>|off]`), which also prints the effective table and the
205
+ tiers this host defines — the one place both halves are visible. It validates the
206
+ strength and the target before writing and reports the resolved result rather than
207
+ echoing the write, so it can no longer claim a change the settings layer dropped.
208
+ `off` yields to a real host tier of that name, and says so when it does, since a
209
+ user who typed the documented keyword and got a mapping has to be told which
210
+ reading won. The view also names where a machine-wide table lives, and — while
211
+ the shipped default is in force — that the default is identity, so workflow work
212
+ is still sharing the tiers ordinary spawns use until a tier is defined for it.
213
+
214
+ Settings commands now write project scope instead of the merged view. Previously
215
+ the first `/effort` or `/workflows-progress` in a project copied every global value
216
+ and default into the project file, after which that project stopped tracking the
217
+ global one.
218
+
219
+ `strengths` needs the opposite treatment, because it replaces rather than merges at
220
+ each level, so `/workflows strength` seeds its write from the table a run would
221
+ actually use — project, else global, else the shipped default. Built on project
222
+ scope alone, setting one entry would have discarded an inherited global table, and
223
+ `off` against an entry the project never wrote would have written nothing and left
224
+ the mapping standing.
225
+
226
+ One caution the docs now carry: the provider-limit retry resumes on a timer with
227
+ nobody watching, so a table edited while a run is rate-limit-paused is charged the
228
+ re-spend above without being asked. Stop the run first if that is not worth it.
229
+
3
230
  ## 1.5.0
4
231
  ### Minor Changes
5
232
 
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
@@ -145,14 +145,40 @@ workflow, and the review skill before accepting a topology or recovery change.
145
145
  ## Tools and commands
146
146
 
147
147
  - `workflow`: run a script (`script`) or a saved/built-in workflow (`name`),
148
- with `args`, `background` (default true), `maxAgents`, `concurrency`,
149
- `agentRetries`, `tokenBudget`, `agentTimeoutMs`, and `resumeFromRunId`.
148
+ with `args`, `background` (default true), `maxAgents` (default 1000),
149
+ `concurrency` (defaults to the host's pool size), `agentRetries` (default 0),
150
+ `tokenBudget` (default unlimited), `agentTimeoutMs` (default 1800000, `null`
151
+ disables), and `resumeFromRunId`. The execution limits are tuned defaults: a
152
+ caller that names one narrower than the script needs stops the run rather
153
+ than shaping it.
154
+
155
+ Concurrency is not configured here at all. A workflow agent occupies one of
156
+ pi-subagents' `maxConcurrent` background slots like any other background
157
+ agent, so that setting is both the width a run defaults to and the ceiling it
158
+ is clamped to — read live from the peer at each start and resume, so one
159
+ setting governs the whole fleet and a run neither guesses it nor exceeds it.
160
+ The peer publishes it only to a caller that requests it by name, so an older
161
+ pair still handshakes; a run that learns no pool size falls back to 16. A
162
+ request above the pool is clamped and the run log says so.
163
+
164
+ In practice that pool is usually the *narrower* number, not the wider one: it
165
+ defaults to 4 background slots, so a default pair runs a fan-out four at a
166
+ time. That is the honest width rather than a new restriction — dispatching
167
+ twelve into four slots only ever queued eight — but it does mean a wide
168
+ fan-out finishes in waves, and `maxConcurrent` is the one place to change
169
+ that, for workflows and for every other background agent at once.
170
+
171
+ `agentTimeoutMs` counts from the moment the host reports the agent left its
172
+ queue, so waiting for a slot is never charged against the work.
150
173
  - `workflow_control`: `list`, `get`, `pause`, `resume`, `stop`, or `rm` a run; `rm` writes a durable removal tombstone.
151
174
  - `pi.events` emits `pi-workflows:runtime` snapshots (`{ runId, event }`) for phase, task, quality, retry, and nested-workflow progress; observer failures are contained.
152
175
  - `/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.
176
+ `resume`, `rm`, `save <name> [runId]`, and `strength` subcommands.
177
+ - `/workflows strength`: `[<low|medium|high> <tier>|off]` — which Agent tier each
178
+ workflow strength runs on, and the tiers this host defines. Unconfigured hosts
179
+ start on a shipped default table (identity, where the host defines the name).
180
+ The tiers themselves are still configured only in pi-subagents'
181
+ `/agents → Model tiers`; there is deliberately no second tier configuration file.
156
182
  - `/effort` and `/ultracode`: standing `off|high|ultra` effort guidance that auto-arms substantive messages.
157
183
  - `/workflows-progress`: `compact|detailed|status|max <N>` controls the persisted live panel.
158
184
  - `/deep-research`, `/adversarial-review`, `/code-review`,
@@ -186,52 +212,185 @@ written atomically with a backup.
186
212
 
187
213
  ## Model routing
188
214
 
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.
215
+ A workflow speaks one word about cost: **strength**, this package's own name for
216
+ how much effort a step deserves. `low`, `medium`, `high` — that is the whole
217
+ vocabulary, and a script may name nothing else.
193
218
 
194
219
  ```js
195
- await agent("summarize this diff", { tier: "low" })
196
- await agent("design the migration", { tier: "high" })
220
+ await agent("summarize this diff", { strength: "low" })
221
+ await agent("design the migration", { strength: "high" })
222
+ await agent("the ordinary case") // no strength: see below
197
223
  ```
198
224
 
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.
225
+ There is no default strength. A call that names none dispatches with **no tier**,
226
+ exactly as an unmapped strength does, and pi-subagents resolves it the way it
227
+ resolves any other spawn: the agent type's own frontmatter tier, then
228
+ `agentTiers.defaultTier`. That matters because a tier a call requests outranks
229
+ the agent type's own — so a default strength would have pinned every unlabelled
230
+ call to whatever it mapped to, overruling `Explore`'s shipped `tier: low` and
231
+ re-pricing the one agent this indirection exists to leave alone. Label the calls
232
+ whose cost you mean to steer; leave the rest to the host.
233
+
234
+ A strength is not an Agent tier. A `strengths` table is the only thing that
235
+ binds one to a tier:
236
+
237
+ ```jsonc
238
+ // <agent dir>/workflows/settings.json, or the per-project settings file
239
+ { "strengths": { "low": "cheap-search", "high": "deep" } }
240
+ ```
241
+
242
+ **An unmapped strength dispatches with no tier at all** and takes the agent's
243
+ ordinary default — the agent's own frontmatter tier, then the host's configured
244
+ default. `{}` is a real table meaning exactly that for every strength.
245
+
246
+ ### The default table
247
+
248
+ A host that has configured nothing does not get "no mappings". It gets a table
249
+ this package ships: each strength on the catalogue tier of the same name,
250
+ **wherever the host defines one**. On a stock install that is
251
+ `low → low, medium → medium, high → high`; on a host whose tiers are called
252
+ `cheap` and `deep` it is empty, and every strength is unmapped. It is computed
253
+ against the live catalogue rather than hardcoded, so it is never an assertion
254
+ about someone else's tier names and never produces a start-up complaint about
255
+ configuration you did not write.
256
+
257
+ Without it, every shipped script's `low`/`medium` distinction would collapse
258
+ onto pi-subagents' managed default — which is the *more* expensive rung, so an
259
+ unconfigured machine would run its fan-outs dearer, not cheaper.
260
+
261
+ That it is a **table and not a fallback rule** is the whole design. A rule
262
+ saying "an unmapped strength runs on the tier of the same name" would be
263
+ unremovable, so making a 26-agent fan-out cheaper would still mean editing tier
264
+ `low` itself — dragging the `Explore` agent and everything else that names it. A
265
+ table is replaced by writing one, and a written table that omits `low` leaves it
266
+ unmapped even on a host that defines a tier called `low`. Nothing is ever
267
+ inferred from spelling at dispatch: the binding is always a table entry.
268
+
269
+ So `low → low` in a written table is a real entry and not a no-op — a table you
270
+ write replaces the default outright, so the entries you leave out are unmapped,
271
+ not inherited.
272
+
273
+ ### Editing it
274
+
275
+ `/workflows strength` edits the table and also prints the tiers this host
276
+ defines — the one place both halves are visible, since the strengths come from
277
+ this package and the tiers from pi-subagents:
278
+
279
+ ```
280
+ /workflows strength # show the effective table and the host's tiers
281
+ /workflows strength low cheap # run workflow strength `low` on Agent tier `cheap`
282
+ /workflows strength low off # unmapped; back to the agent's own default
283
+ ```
284
+
285
+ Edits land in **this project's** settings, like every other setting this package
286
+ owns. Because the table replaces rather than merges at each level, the command
287
+ seeds the write from the table a run would actually use — the project file, else
288
+ global, else the shipped default — so setting one entry cannot silently discard
289
+ the rest, and `off` clears a mapping this project never wrote. The write is
290
+ therefore a full copy of the current table into the project; edit
291
+ `<agent dir>/workflows/settings.json` by hand for a machine-wide table.
292
+
293
+ Any key the host's catalogue defines is a legal target — an existing tier, or a
294
+ profile added for exactly this purpose. There is no shortlist and no naming
295
+ convention: the target is checked against the live catalogue at run start, so a
296
+ profile added to `subagents.json` is usable the moment it exists. One hop, never
297
+ chained. An entry whose tier the host does not define is reported once at run
298
+ start and ignored, leaving that strength unmapped — a stale entry costs the
299
+ redirect, not every workflow on the machine.
300
+
301
+ This is not the retired `workflow.tiers` key and must not become it. That one
302
+ carried its own `model` and `thinking` values, which made it a second model
303
+ policy resolved by a second resolver. A value here is a **key in the host's one
304
+ catalogue**: pi-subagents still owns every model, every thinking level, and the
305
+ only `resolveAgentTier()`, and cannot tell a mapped call apart from a spawn that
306
+ named the key itself.
307
+
308
+ There is no per-call `model`, `thinking`, or `tier`, and `meta`/phase metadata
309
+ may not carry them either. A strength is the only model policy a workflow can
310
+ express, which is what keeps a script from pinning a vendor the host did not
311
+ choose.
312
+
313
+ The vocabulary is closed so that a word outside it is a *typo* rather than a
314
+ strength nobody configured: `agent({ strength: "lowe" })` fails before any
315
+ dispatch instead of silently running at the default.
316
+
317
+ The same argument applies one level up, to the option *name*. `agent()` and
318
+ `checkpoint()` reject any key they do not read, rather than ignoring it:
319
+
320
+ ```js
321
+ await agent("x", { tier: "low" }) // → name a strength instead: one of low, medium, high
322
+ await agent("x", { model: "…" }) // → a strength is the only model policy a workflow can express
323
+ await agent("x", { strenght: "low" }) // → valid options are: label, phase, schema, strength, …
324
+ ```
325
+
326
+ An unknown key is dropped before the call hash is built, so it changes neither
327
+ the dispatch nor the resume identity — the run would simply spend at a policy
328
+ nobody chose, and a resume would replay it without noticing. A script is plain
329
+ JavaScript authored against this contract, so there is no other gate: a
330
+ misspelled option would run every step untiered, and a misspelled
331
+ `checkpoint({ headles: "abort" })` would auto-approve in headless mode instead
332
+ of aborting.
333
+
334
+ ### Helpers that dispatch for you
335
+
336
+ `verify()`, `judgePanel()`, and `completenessCheck()` spawn agents on the
337
+ script's behalf, so the script cannot label those calls. Each carries a
338
+ documented default and takes an override, which keeps every dispatch inside the
339
+ vocabulary:
340
+
341
+ ```js
342
+ await verify(finding) // reviewers run at "low"
343
+ await verify(finding, { strength: "high" }) // unless you say otherwise
344
+ await completenessCheck(args, results) // one open-ended read: "medium"
345
+ ```
346
+
347
+ These defaults are an opinion, and an opinion outranks the host's: a tier a call
348
+ requests beats `agentTiers.defaultTier`, so on a machine whose default is `high`
349
+ a `completenessCheck` now runs at whatever `medium` maps to rather than at
350
+ `high`. That is the trade for making the helpers re-routable at all — they
351
+ previously named no tier and could never be steered — and the override above is
352
+ how a script takes it back.
353
+
354
+ ### Resume
355
+
356
+ The table is read fresh at each start and resume — including the resumes the
357
+ engine starts itself, `/workflows resume` and the provider-limit retry — never
358
+ frozen onto a run.
359
+
360
+ Resume identity keys a call on the tier it will actually request, so editing the
361
+ table and resuming re-routes the calls that have not run — and **also re-runs
362
+ the finished calls whose strength moved**, plus everything after the first such
363
+ miss, because their cached answers came from a different tier. Re-routing
364
+ mid-run is therefore a re-spend; if the point of the edit was to spend less,
365
+ start a new run instead of resuming one that is mostly done. The provider-limit
366
+ retry resumes on a timer with no one watching, so a table edited while a run is
367
+ rate-limit-paused is charged the same way without being asked — stop that run
368
+ before editing if the re-spend is not worth it. A nested
369
+ `workflow()` boundary folds the table into its own key for the same reason: the
370
+ value it caches for the whole child frame would otherwise survive a change that
371
+ re-routes the child.
372
+
373
+ Fresh installs of pi-subagents ship `low`/`medium`/`high` tier profiles, all
374
+ inheriting their model — which is what the default table above resolves against,
375
+ so the built-ins run on a new machine, at their own strengths, with no
376
+ configuration. A managed call that still names no tier falls back to `medium`;
377
+ that is where an unmapped strength lands. The fallback is scoped to managed
378
+ calls: it is not the catalogue's `defaultTier`, so an ordinary `Agent` spawn on
379
+ the same machine keeps falling through to `defaultModel` and the parent session.
380
+ A host whose default has been cleared (`noDefaultTier`) rejects an untiered
381
+ managed call rather than quietly inheriting the parent session's model — which
382
+ an unmapped strength can reach.
383
+
384
+ The managed wire, invocation record, journal, tombstone, and resume state all
385
+ carry the one resolved tier — including one the host defaulted to, so a run's
386
+ record says what actually ran. Resume keys each cached call on the policy for
387
+ *that call's* tier: editing an unrelated tier does not invalidate work that never
388
+ used it.
230
389
 
231
390
  ## Intentional adaptations from upstream
232
391
 
233
392
  - **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.
393
+ - **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
394
  - **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
395
 
237
396
  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.7.0",
4
4
  "description": "Deterministic JavaScript orchestration workflows backed by pi-subagents managed spawning.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -20,7 +20,7 @@
20
20
  "@signalridge/pi-subagents": ">=1.9.0"
21
21
  },
22
22
  "dependencies": {
23
- "@signalridge/pi-subagents-protocol": "^1.4.0",
23
+ "@signalridge/pi-subagents-protocol": "^1.5.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.5.0"
5
+ version: "1.7.0"
6
6
  ---
7
7
 
8
8
  # Workflow authoring
@@ -25,16 +25,44 @@ JavaScript.
25
25
  - Give every work unit a stable ID and every agent call a short unique label.
26
26
  Keep IDs beside results before filtering; `null` is missing coverage, not a
27
27
  successful finding.
28
- - Bound graph tasks, fan-out items, loops, retries, agents, concurrency, and
29
- evidence size. Invocation-level token and time caps are user constraints, not
30
- substitutes for algorithmic bounds.
31
- - Route an agent call with `tier: "<key>"`, naming a tier the destination host
32
- defines. Omitting `tier` uses the agent's own tier, then the host's configured
33
- default; a host whose default has been cleared rejects the call rather than
34
- inheriting the parent model. There is no per-call `model`/`thinking`, and
35
- neither may appear in workflow meta or phase profiles. Never guess a tier or
36
- agent-type name—use names supplied by the current environment and follow
28
+ - Bound the algorithm inside the script: graph tasks, fan-out items, loop
29
+ rounds, semantic retries, and evidence size. The invocation-level limits
30
+ (`maxAgents`, `concurrency`, `agentRetries`, `tokenBudget`, `agentTimeoutMs`)
31
+ are tuned defaults rather than the place to express a bound; each one stops or
32
+ fails a run that outgrows it, so a precautionary value cuts the coverage and
33
+ not the cost. Set one only when the user asked for that limit.
34
+ - Size the fan-out to the evidence the task actually has: one agent per
35
+ independent question, ten to thirty for an ordinary review or audit. The gain
36
+ is coverage — one agent asked one question reads further into it than one
37
+ agent asked four. Speed is a separate matter and not yours to decide: how many
38
+ of those agents run at once is the host's `maxConcurrent` pool, so a wide
39
+ fan-out on a narrow pool still finishes in waves. Split for what gets looked
40
+ at, not for wall-clock.
41
+ - Route an agent call with `strength: "low" | "medium" | "high"` — this
42
+ package's own word for how much effort a step deserves, and the only routing
43
+ word a workflow has. It is **not** an agent-tier key: a `strengths` table
44
+ alone binds a strength to a tier — the user's, or the shipped default on a
45
+ host that configured none — and a strength no table defines dispatches with no
46
+ tier and takes the agent's own default. So name the effort the step deserves
47
+ and never reach for a catalogue name; a word outside the three is rejected
48
+ before dispatch, as is `tier:` itself. There is no default: omitting
49
+ `strength` dispatches with no tier and lets the agent type's own tier stand,
50
+ so label the calls whose cost you mean to steer and leave the rest alone.
51
+ There is no per-call `model`/`thinking`/`tier`, and none may
52
+ appear in workflow meta or phase profiles. `agent()` and `checkpoint()` reject
53
+ any option name they do not read rather than ignoring it, so pass only the
54
+ documented keys. Never guess an agent-type name—use
55
+ names supplied by the current environment and follow
37
56
  [registry ownership](references/registry-ownership.md).
57
+ - Pick that word by what a wrong answer costs, not by how important the step
58
+ feels. `low` is retrieval, navigation, and read-heavy breadth. `medium` is the
59
+ ordinary working rung and where a labelled step belongs unless you can say why
60
+ not. `high` is for a step whose error is expensive to reverse — an architecture
61
+ decision, a destructive migration, a security boundary — and a fan-out normally
62
+ names it zero times, at most once on a closing step that actually decides
63
+ something. It is not a reward for the hardest-sounding task: routing a whole
64
+ fan-out to `high` prices every branch at the cost of the one that mattered, and
65
+ buys depth where the run needed coverage.
38
66
  - Workflows have no imports, filesystem/network APIs, timers, or unrestricted
39
67
  Node APIs. Pass timestamps, randomness, and external decisions through `args`.
40
68
 
@@ -56,7 +84,9 @@ JavaScript.
56
84
  the child can see the parent conversation.
57
85
  6. **Add quality only where it changes a decision.** Use a schema before reading
58
86
  structured fields, then use `verify`, `judgePanel`, `gate`, or
59
- `completenessCheck` with explicit bounds.
87
+ `completenessCheck` with explicit bounds. These dispatch on your behalf, so
88
+ pass `strength` when the default (`low`, or `medium` for
89
+ `completenessCheck`) is not the effort that gate deserves.
60
90
  7. **Aggregate and report.** Return a bounded JSON result containing the answer,
61
91
  coverage, and important failures. If the workflow runs in the background,
62
92
  the run ID is the handle for later control/resume.
@@ -176,8 +206,8 @@ Before shipping a workflow change:
176
206
  - read [debugging](references/debugging.md) when reproducing a failure;
177
207
  - read [the review checklist](references/review.md) before accepting topology;
178
208
  - 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;
209
+ signature, default, or support boundary is disputed; remember that a strength
210
+ is this package's own vocabulary, bound to a host tier only by the user's
211
+ `strengths` table, while model/thinking resolution stays in pi-subagents;
182
212
  - ensure every package-relative example/reference link remains inside the
183
213
  publishable package.