@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.
@@ -22,9 +22,10 @@ Review author-visible behavior, not formatting preferences. When behavior depend
22
22
 
23
23
  - Does JavaScript consume structured fields only after a small plain JSON Schema guarantees them?
24
24
  - Does synthesis receive complete coverage and failure ledgers?
25
- - Does every `tier` name a key the destination host actually defines, rather than a guessed one?
26
- - Is the tier the only model/thinking policy source, via pi-subagents' `resolveAgentTier()`?
27
- - Did every tier or agent-type name come from context with a name and purpose?
25
+ - Does every call name a `strength` from `low`/`medium`/`high`, chosen for the effort the step deserves rather than copied from a tier name?
26
+ - Does every helper that dispatches for you (`verify`, `judgePanel`, `completenessCheck`) either take its documented default deliberately or pass an explicit `strength`?
27
+ - Is the strength the only model/thinking policy source, resolved through the active `strengths` table and then pi-subagents' `resolveAgentTier()`?
28
+ - Did every agent-type name come from context with a name and purpose?
28
29
 
29
30
  ## Lifecycle
30
31
 
@@ -17,7 +17,7 @@ The runtime supplies `agent`, `parallel`, `pipeline`, `orchestrate`, `workflow`,
17
17
 
18
18
  ## Data and failure
19
19
 
20
- Call `agent(prompt, { label, schema?, tier? })`; it returns text, a schema-validated value, or recoverable `null`. `tier` names a key in the host's agent-tier catalogue. Nonrecoverable limit, validation, budget, unknown-tier, and unavailable-tier failures throw. Record each intended work ID before filtering. A `null` means missing coverage, never a negative finding.
20
+ Call `agent(prompt, { label, schema?, strength? })`; it returns text, a schema-validated value, or recoverable `null`. `strength` is `low`/`medium`/`high` and has no default — omitting it sends no tier. Nonrecoverable limit, validation, budget, unknown-strength, and unavailable-tier failures throw. Record each intended work ID before filtering. A `null` means missing coverage, never a negative finding.
21
21
 
22
22
  Use `agent(prompt, { thread: "implementer" })` when the same managed subagent must receive a later follow-up with its complete conversation intact. Reuse a thread name sequentially; never put same-thread calls in one `parallel()` batch. Threads are scoped to one workflow run/session and cannot use worktree isolation. A pause or process restart may require a fresh thread session.
23
23
 
@@ -25,6 +25,6 @@ When JavaScript reads fields, pass a small plain JSON Schema. Schema noncomplian
25
25
 
26
26
  ## Routing and support
27
27
 
28
- `tier` is a key in the host's agent-tier catalogue, not a model ID. The catalogue and its names belong to the user running the workflow; read the available keys from the destination context rather than assuming any particular name exists. A tier this host does not define is rejected before dispatch — only the scripts shipped with this package reroute to the host default instead, because they cannot know the catalogue they will land in. pi-subagents' `resolveAgentTier()` alone resolves the final model, thinking, clamping, availability, and snapshot. A tier the host does not define is rejected before dispatch. Omitting `tier` uses the agent's own tier, then `agentTiers.defaultTier`; a host whose default has been cleared rejects the call instead of inheriting the parent model. There is no per-call model or thinking, and workflow meta and phases may not carry them either. Worktree isolation remains owned by pi-subagents. See [registry ownership](registry-ownership.md).
28
+ `strength` is this package's own vocabulary — `low`, `medium`, `high` — not a model ID and not an agent-tier key. A `strengths` table is the only thing that binds a strength to a tier: the user's, or a shipped default (identity, wherever the host defines a tier of the same name) on a host that configured none. A strength no table defines dispatches with no tier and takes the agent's own default, then `agentTiers.defaultTier`; a call that names no strength takes that same untiered path, because a default strength would have outranked the agent type's own tier on every unlabelled call. A word outside the three is rejected before dispatch, and so is any option name `agent()` does not read — `tier`, `model`, and `thinking` answer by naming their replacement, anything else lists the legal keys. `checkpoint()` gates its own options the same way. pi-subagents' `resolveAgentTier()` alone resolves the final model, thinking, clamping, availability, and snapshot. A host whose default has been cleared rejects an untiered call instead of inheriting the parent model. There is no per-call model, thinking, or tier, and workflow meta and phases may not carry them either. Worktree isolation remains owned by pi-subagents. See [registry ownership](registry-ownership.md).
29
29
 
30
30
  Generated entries marked `supported` are authoring API. `console` and whole-script Markdown fences are compatibility-only. VM realm facilities are internal. Active model routes and agent types are dynamic. Use `log()` in new scripts.
@@ -6,7 +6,7 @@ Preserve candidate or work identity outside helper results that may omit failed
6
6
 
7
7
  | Helper | Authoring contract |
8
8
  | --- | --- |
9
- | `completenessCheck(args, results)` | Returns `{ complete, missing? }` or recoverable `null`. The critic sees only the first 4,000 serialized characters, so chunk or summarize larger evidence. Treat the verdict as advisory. |
9
+ | `completenessCheck(args, results, { strength })` | Returns `{ complete, missing? }` or recoverable `null`. Defaults to `strength: "medium"`, not `low` like the other gates: one call, no vote to average over, and an open-ended read of the whole task. The critic sees only the first 4,000 serialized characters, so chunk or summarize larger evidence. Treat the verdict as advisory. |
10
10
  | `loopUntilDry({ round, key, consecutiveEmpty, maxRounds })` | `round(index)` is zero-based. Defaults: `JSON.stringify` key, two dry rounds, 50 rounds. Null, non-array, and duplicate-only rounds are dry. Token-budget or agent-limit exhaustion returns the partial array without a termination reason; keep failed-round identity and stopping state outside the helper. |
11
11
 
12
12
  ## Control
@@ -9,5 +9,5 @@ When copying a workflow between installations:
9
9
  1. Check the destination's generated version and exact capability facts.
10
10
  2. Re-check every supported global, option, and constraint used by the script.
11
11
  3. Treat compatibility entries as preservation aids, not portable recommendations.
12
- 4. Re-resolve tier keys and agent types from destination context; both come from the destination host's configuration, and static docs intentionally contain no live catalogue entries.
12
+ 4. Re-resolve agent types from destination context; they come from the destination host's configuration, and static docs intentionally contain no live registry entries. Strengths need no re-resolution — the vocabulary is fixed and the mapping belongs to the destination, whether that is the user's table or the shipped default.
13
13
  5. Prefer runtime behavior when prose and execution disagree, and report the documentation mismatch rather than changing behavior during an authoring fix.
@@ -2,7 +2,7 @@
2
2
  name: workflow-patterns
3
3
  description: Choose and run one of Pi's built-in multi-agent workflows for research, adversarial review, code review, multi-perspective analysis, or codebase auditing. Use when the request matches a named pattern; use workflow-authoring instead for a new script or custom topology.
4
4
  metadata:
5
- version: "1.5.0"
5
+ version: "1.6.0"
6
6
  ---
7
7
 
8
8
  # Built-in workflow patterns
@@ -2,7 +2,7 @@
2
2
  name: workflow-review
3
3
  description: Audit an existing Pi JavaScript workflow for topology, prompt/data flow, limits, failure handling, cancellation, resume safety, and publication compatibility. Use when reviewing or debugging a workflow script or saved run; do not use to run a standard named workflow.
4
4
  metadata:
5
- version: "1.5.0"
5
+ version: "1.6.0"
6
6
  ---
7
7
 
8
8
  # Workflow review
package/src/builtins.ts CHANGED
@@ -55,7 +55,7 @@ const plan = await agent(
55
55
  'You are planning web research for this question:\\n' + question +
56
56
  '\\n\\nProduce ' + angles + ' diverse, specific search queries that together cover the question from different angles. ' +
57
57
  'Use your web search tools when helpful to refine them.',
58
- { label: 'plan queries', tier: 'medium', schema: { type: 'object', properties: { queries: { type: 'array', items: { type: 'string' } } }, required: ['queries'] } }
58
+ { label: 'plan queries', strength: 'medium', schema: { type: 'object', properties: { queries: { type: 'array', items: { type: 'string' } } }, required: ['queries'] } }
59
59
  )
60
60
  const planned = plan && Array.isArray(plan.queries) ? plan.queries.filter((q) => typeof q === 'string' && q.trim().length > 0) : []
61
61
  const queries = (planned.length > 0 ? planned : [question]).slice(0, angles)
@@ -67,7 +67,7 @@ const gathered = await parallel(queries.map((q, i) => () =>
67
67
  '\\n\\nSteps: (1) run a web search for the query; (2) fetch the most relevant result URLs; ' +
68
68
  '(3) extract concrete, verifiable factual claims, each tagged with the exact source URL it came from. ' +
69
69
  'Do NOT invent sources or claims — report only what the fetched pages actually say.',
70
- { label: 'research ' + (i + 1), tier: 'low', schema: { type: 'object', properties: { sources: { type: 'array', items: { type: 'object', properties: { url: { type: 'string' }, claims: { type: 'array', items: { type: 'string' } } }, required: ['url', 'claims'] } } }, required: ['sources'] } }
70
+ { label: 'research ' + (i + 1), strength: 'low', schema: { type: 'object', properties: { sources: { type: 'array', items: { type: 'object', properties: { url: { type: 'string' }, claims: { type: 'array', items: { type: 'string' } } }, required: ['url', 'claims'] } } }, required: ['sources'] } }
71
71
  )
72
72
  ))
73
73
  const allSources = gathered.filter(Boolean).flatMap((g) => (g && g.sources) || [])
@@ -77,7 +77,7 @@ const verdict = await agent(
77
77
  'Cross-check these research sources. Group claims that assert the same fact across different source URLs. ' +
78
78
  'Keep a claim only if it is supported by at least ' + minSupport + ' distinct source URLs OR by one clearly authoritative source. ' +
79
79
  'Discard claims found in a single weak source or that conflict with others.\\n\\nSOURCES JSON:\\n' + JSON.stringify(allSources),
80
- { label: 'cross-check', tier: 'medium', schema: { type: 'object', properties: { supported: { type: 'array', items: { type: 'object', properties: { claim: { type: 'string' }, sources: { type: 'array', items: { type: 'string' } } }, required: ['claim', 'sources'] } }, discarded: { type: 'array', items: { type: 'string' } } }, required: ['supported'] } }
80
+ { label: 'cross-check', strength: 'medium', schema: { type: 'object', properties: { supported: { type: 'array', items: { type: 'object', properties: { claim: { type: 'string' }, sources: { type: 'array', items: { type: 'string' } } }, required: ['claim', 'sources'] } }, discarded: { type: 'array', items: { type: 'string' } } }, required: ['supported'] } }
81
81
  )
82
82
 
83
83
  phase('Report')
@@ -85,7 +85,7 @@ const report = await agent(
85
85
  'Write a concise, well-structured research report that answers the question using ONLY the supported claims below. ' +
86
86
  'Cite source URLs inline next to each claim. If the evidence is thin, say so explicitly.\\n\\n' +
87
87
  'QUESTION: ' + question + '\\n\\nSUPPORTED CLAIMS JSON:\\n' + JSON.stringify((verdict && verdict.supported) || []),
88
- { label: 'write report', tier: 'medium' }
88
+ { label: 'write report', strength: 'medium' }
89
89
  )
90
90
 
91
91
  return { question, queries, supported: (verdict && verdict.supported) || [], report }`;
@@ -107,7 +107,7 @@ const threshold = (args && args.threshold) || 0.5
107
107
  phase('Investigate')
108
108
  const investigation = await agent(
109
109
  'Investigate the following and list concrete, individually-checkable findings:\\n' + task,
110
- { label: 'investigate', tier: 'medium', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }
110
+ { label: 'investigate', strength: 'medium', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }
111
111
  )
112
112
  const findings = (investigation && Array.isArray(investigation.findings) ? investigation.findings : [])
113
113
 
@@ -118,7 +118,7 @@ const judged = await parallel(findings.map((f, i) => () =>
118
118
  'You are a skeptical reviewer. Try to REFUTE this finding for the task below. ' +
119
119
  'Default to real=false when uncertain. Investigate with the available tools if needed.\\n\\n' +
120
120
  'TASK: ' + task + '\\nFINDING: ' + f,
121
- { label: 'refute ' + (i + 1) + '.' + (r + 1), tier: 'low', schema: { type: 'object', properties: { real: { type: 'boolean' }, reason: { type: 'string' } }, required: ['real'] } }
121
+ { label: 'refute ' + (i + 1) + '.' + (r + 1), strength: 'low', schema: { type: 'object', properties: { real: { type: 'boolean' }, reason: { type: 'string' } }, required: ['real'] } }
122
122
  )
123
123
  )).then((votes) => {
124
124
  const valid = votes.filter(Boolean)
@@ -134,7 +134,7 @@ const report = await agent(
134
134
  'Synthesize the surviving findings into a concise review. For each finding, note the skeptic agreement ' +
135
135
  '(realVotes/totalVotes). Do not include findings that failed the refutation pass.\\n\\n' +
136
136
  'TASK: ' + task + '\\n\\nSURVIVING FINDINGS JSON:\\n' + JSON.stringify(survivors),
137
- { label: 'consensus report', tier: 'medium' }
137
+ { label: 'consensus report', strength: 'medium' }
138
138
  )
139
139
 
140
140
  return { findings, judged: judged.filter(Boolean), survivors, report }`;
@@ -154,13 +154,13 @@ const diffSource = (args && args.diffSource) || 'provided diff'
154
154
 
155
155
  phase('Scan')
156
156
  const finders = await parallel([
157
- () => agent('You are a correctness reviewer. Find bugs, race conditions, error-handling gaps, and edge-case failures in this diff.\\n\\n' + diff, { label: 'correctness', tier: 'low', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }),
158
- () => agent('You are a reuse reviewer. Find duplicated logic, reinvented wheels, and missed opportunities to reuse existing code or libraries.\\n\\n' + diff, { label: 'reuse', tier: 'low', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }),
159
- () => agent('You are a simplification reviewer. Find over-engineering, unnecessary complexity, and simpler alternatives.\\n\\n' + diff, { label: 'simplification', tier: 'low', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }),
160
- () => agent('You are an efficiency reviewer. Find performance problems, avoidable allocations, and scaling risks.\\n\\n' + diff, { label: 'efficiency', tier: 'low', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }),
161
- () => agent('You are an architecture reviewer. Assess the change at the right altitude: design fit, layering, boundaries, and future maintenance.\\n\\n' + diff, { label: 'altitude', tier: 'medium', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }),
162
- () => agent('You are a security reviewer. Find trust-boundary, injection, secret-leak, permission, and unsafe-input risks in this diff.\\n\\n' + diff, { label: 'security', tier: 'low', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }),
163
- () => agent('You are a testability reviewer. Find missing regression tests, observability gaps, and rollout/recovery risks in this diff.\\n\\n' + diff, { label: 'testability', tier: 'low', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }),
157
+ () => agent('You are a correctness reviewer. Find bugs, race conditions, error-handling gaps, and edge-case failures in this diff.\\n\\n' + diff, { label: 'correctness', strength: 'low', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }),
158
+ () => agent('You are a reuse reviewer. Find duplicated logic, reinvented wheels, and missed opportunities to reuse existing code or libraries.\\n\\n' + diff, { label: 'reuse', strength: 'low', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }),
159
+ () => agent('You are a simplification reviewer. Find over-engineering, unnecessary complexity, and simpler alternatives.\\n\\n' + diff, { label: 'simplification', strength: 'low', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }),
160
+ () => agent('You are an efficiency reviewer. Find performance problems, avoidable allocations, and scaling risks.\\n\\n' + diff, { label: 'efficiency', strength: 'low', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }),
161
+ () => agent('You are an architecture reviewer. Assess the change at the right altitude: design fit, layering, boundaries, and future maintenance.\\n\\n' + diff, { label: 'altitude', strength: 'medium', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }),
162
+ () => agent('You are a security reviewer. Find trust-boundary, injection, secret-leak, permission, and unsafe-input risks in this diff.\\n\\n' + diff, { label: 'security', strength: 'low', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }),
163
+ () => agent('You are a testability reviewer. Find missing regression tests, observability gaps, and rollout/recovery risks in this diff.\\n\\n' + diff, { label: 'testability', strength: 'low', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } } }, required: ['findings'] } }),
164
164
  ])
165
165
  const candidates = finders.filter(Boolean).flatMap((f) => (f && f.findings) || [])
166
166
 
@@ -168,7 +168,7 @@ phase('Verify')
168
168
  const verified = await parallel(candidates.map((finding, i) => () =>
169
169
  agent('You are a skeptical verifier. For this candidate finding, check whether it is a REAL issue in the diff. ' +
170
170
  'Default to real=false when uncertain.\\n\\nCANDIDATE: ' + finding + '\\n\\nDIFF:\\n' + diff,
171
- { label: 'verify ' + (i + 1), tier: 'low', schema: { type: 'object', properties: { real: { type: 'boolean' }, reason: { type: 'string' } }, required: ['real'] } })
171
+ { label: 'verify ' + (i + 1), strength: 'low', schema: { type: 'object', properties: { real: { type: 'boolean' }, reason: { type: 'string' } }, required: ['real'] } })
172
172
  ))
173
173
 
174
174
  phase('Report')
@@ -179,7 +179,7 @@ const report = await agent(
179
179
  'Write a ranked code review report for this diff (' + diffSource + '). ' +
180
180
  'Include only verified findings, ordered by severity, each with a concrete location and fix suggestion.\\n\\n' +
181
181
  'VERIFIED FINDINGS JSON:\\n' + JSON.stringify(ranked),
182
- { label: 'final report', tier: 'medium' }
182
+ { label: 'final report', strength: 'medium' }
183
183
  )
184
184
 
185
185
  return { candidates, ranked, report }`;
@@ -199,13 +199,13 @@ const perspectives = (args && Array.isArray(args.perspectives) && args.perspecti
199
199
  phase('Perspectives')
200
200
  const views = await parallel(perspectives.map((p, i) => () =>
201
201
  agent('Evaluate this topic from the perspective of: ' + p + '. Be concrete and specific; note risks, trade-offs, and what matters most from this angle.\\n\\nTOPIC: ' + topic,
202
- { label: 'perspective ' + (i + 1), tier: 'low', schema: { type: 'object', properties: { angle: { type: 'string' }, points: { type: 'array', items: { type: 'string' } }, risks: { type: 'array', items: { type: 'string' } } }, required: ['angle', 'points', 'risks'] } })
202
+ { label: 'perspective ' + (i + 1), strength: 'low', schema: { type: 'object', properties: { angle: { type: 'string' }, points: { type: 'array', items: { type: 'string' } }, risks: { type: 'array', items: { type: 'string' } } }, required: ['angle', 'points', 'risks'] } })
203
203
  ))
204
204
 
205
205
  phase('Synthesis')
206
206
  const synthesis = await agent(
207
207
  'Synthesize these perspectives into a balanced assessment. Identify where perspectives agree, where they conflict, and give a bottom-line recommendation with the key trade-offs.\\n\\nTOPIC: ' + topic + '\\n\\nPERSPECTIVES JSON:\\n' + JSON.stringify(views.filter(Boolean)),
208
- { label: 'synthesis', tier: 'medium' }
208
+ { label: 'synthesis', strength: 'medium' }
209
209
  )
210
210
 
211
211
  return { views: views.filter(Boolean), synthesis }`;
@@ -229,21 +229,21 @@ const map = await agent(
229
229
  'Explore the codebase at ' + root + ' and produce a structural map: top-level modules, entry points, ' +
230
230
  'data flow, and where complexity concentrates. Use the available file tools.\\n' +
231
231
  (focus ? 'Focus/checks requested: ' + focus + '\\n' : ''),
232
- { label: 'map', tier: 'medium', schema: { type: 'object', properties: { modules: { type: 'array', items: { type: 'object', properties: { name: { type: 'string' }, purpose: { type: 'string' }, risk: { type: 'string' } }, required: ['name', 'purpose'] } }, entryPoints: { type: 'array', items: { type: 'string' } }, complexityHotspots: { type: 'array', items: { type: 'string' } } }, required: ['modules'] } }
232
+ { label: 'map', strength: 'medium', schema: { type: 'object', properties: { modules: { type: 'array', items: { type: 'object', properties: { name: { type: 'string' }, purpose: { type: 'string' }, risk: { type: 'string' } }, required: ['name', 'purpose'] } }, entryPoints: { type: 'array', items: { type: 'string' } }, complexityHotspots: { type: 'array', items: { type: 'string' } } }, required: ['modules'] } }
233
233
  )
234
234
 
235
235
  phase('Audit')
236
236
  const audits = await parallel([
237
- () => agent('Audit this codebase for correctness risks: error handling, boundary conditions, concurrency, security. Use file tools.\\n\\nSTRUCTURE:\\n' + JSON.stringify(map), { label: 'correctness', tier: 'low', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } }, severity: { type: 'string' } }, required: ['findings'] } }),
238
- () => agent('Audit this codebase for maintainability: coupling, duplication, naming, test coverage, documentation. Use file tools.\\n\\nSTRUCTURE:\\n' + JSON.stringify(map), { label: 'maintainability', tier: 'low', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } }, severity: { type: 'string' } }, required: ['findings'] } }),
239
- () => agent('Audit this codebase for operational health: dependencies, build, deployment, observability, secrets handling. Use file tools.\\n\\nSTRUCTURE:\\n' + JSON.stringify(map), { label: 'operations', tier: 'low', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } }, severity: { type: 'string' } }, required: ['findings'] } }),
237
+ () => agent('Audit this codebase for correctness risks: error handling, boundary conditions, concurrency, security. Use file tools.\\n\\nSTRUCTURE:\\n' + JSON.stringify(map), { label: 'correctness', strength: 'low', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } }, severity: { type: 'string' } }, required: ['findings'] } }),
238
+ () => agent('Audit this codebase for maintainability: coupling, duplication, naming, test coverage, documentation. Use file tools.\\n\\nSTRUCTURE:\\n' + JSON.stringify(map), { label: 'maintainability', strength: 'low', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } }, severity: { type: 'string' } }, required: ['findings'] } }),
239
+ () => agent('Audit this codebase for operational health: dependencies, build, deployment, observability, secrets handling. Use file tools.\\n\\nSTRUCTURE:\\n' + JSON.stringify(map), { label: 'operations', strength: 'low', schema: { type: 'object', properties: { findings: { type: 'array', items: { type: 'string' } }, severity: { type: 'string' } }, required: ['findings'] } }),
240
240
  ])
241
241
 
242
242
  phase('Report')
243
243
  const report = await agent(
244
244
  'Write a codebase health audit report: overall health score, ranked findings across dimensions with concrete locations, ' +
245
245
  'and a prioritized remediation roadmap.\\n\\nSTRUCTURE:\\n' + JSON.stringify(map) + '\\n\\nAUDITS JSON:\\n' + JSON.stringify(audits.filter(Boolean)),
246
- { label: 'audit report', tier: 'medium' }
246
+ { label: 'audit report', strength: 'medium' }
247
247
  )
248
248
 
249
249
  return { map, audits: audits.filter(Boolean), report }`;
@@ -34,6 +34,14 @@ export const WORKFLOW_CAPABILITY_CONTRACT_VERSION = "2.0.0";
34
34
  /**
35
35
  * The runtime globals a script can rely on. `options` documents each helper's
36
36
  * accepted inputs with defaults; `constraints` states the behavioral contract.
37
+ *
38
+ * The strength option's type is spelled out on each of the four helpers that
39
+ * take it, rather than derived from `WORKFLOW_STRENGTHS`, because this array
40
+ * must stay literal-only: `check:capabilities` evaluates it in an empty VM
41
+ * realm so the published contract can be read without executing this package.
42
+ * `strengths.test.ts` pins the four copies against the vocabulary the runtime
43
+ * enforces, which is the drift protection a shared constant would otherwise
44
+ * have provided.
37
45
  */
38
46
  export const WORKFLOW_CAPABILITIES: readonly CapabilityEntry[] = [
39
47
  {
@@ -45,10 +53,10 @@ export const WORKFLOW_CAPABILITIES: readonly CapabilityEntry[] = [
45
53
  { name: "phase", kind: "string", optional: true, default: "current phase" },
46
54
  { name: "schema", kind: "plain JSON Schema", optional: true, default: "none" },
47
55
  {
48
- name: "tier",
49
- kind: "agent tier key",
56
+ name: "strength",
57
+ kind: '"low" | "medium" | "high"',
50
58
  optional: true,
51
- default: "the agent's own tier, else agentTiers.defaultTier",
59
+ default: "no tier is sent; the agent's own tier, else agentTiers.defaultTier",
52
60
  },
53
61
  { name: "isolation", kind: '"worktree"', optional: true, default: "pi-subagents worktree policy" },
54
62
  { name: "thread", kind: "string", optional: true, default: "fresh session per call" },
@@ -64,11 +72,15 @@ export const WORKFLOW_CAPABILITIES: readonly CapabilityEntry[] = [
64
72
  "resume replays only the longest unchanged prefix; the first miss and every later call execute live",
65
73
  "replayed calls do not consume the real-dispatch maxAgents or token/phase budgets",
66
74
  "spawnKeys rotate by generation on resume so pi-subagents never raises a fingerprint conflict",
67
- "tier names a key in the host's agentTiers catalogue; a key it does not define is rejected before dispatch",
75
+ "strength is this package's own vocabulary, not an agentTiers key; a word outside low/medium/high is rejected before dispatch",
76
+ "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",
77
+ "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",
78
+ "an unconfigured host uses a shipped default table: each strength on the catalogue tier of the same name, where the host defines one",
68
79
  "resolveAgentTier in pi-subagents owns model, thinking, clamping, availability, and the resolution snapshot",
69
- "there is no per-call model or thinking: a tier is the only model policy a workflow can express",
80
+ "there is no per-call model, thinking, or tier: a strength is the only model policy a workflow can express",
70
81
  "same-thread calls must be sequential and preserve a stable workflow thread name",
71
82
  "threads cannot be combined with worktree isolation",
83
+ "an option name outside this list is rejected before dispatch, not ignored; tier, model, and thinking name their replacement",
72
84
  ],
73
85
  },
74
86
  {
@@ -136,6 +148,7 @@ export const WORKFLOW_CAPABILITIES: readonly CapabilityEntry[] = [
136
148
  { name: "reviewers", kind: "number", optional: true, default: "2" },
137
149
  { name: "threshold", kind: "number", optional: true, default: "0.5" },
138
150
  { name: "lens", kind: "string | string[]", optional: true, default: "none" },
151
+ { name: "strength", kind: '"low" | "medium" | "high"', optional: true, default: '"low"' },
139
152
  ],
140
153
  constraints: ["threshold comparison is inclusive; real is false when no reviewer succeeds"],
141
154
  },
@@ -146,6 +159,7 @@ export const WORKFLOW_CAPABILITIES: readonly CapabilityEntry[] = [
146
159
  options: [
147
160
  { name: "judges", kind: "number", optional: true, default: "3" },
148
161
  { name: "rubric", kind: "string", optional: true, default: "overall quality and correctness" },
162
+ { name: "strength", kind: '"low" | "medium" | "high"', optional: true, default: '"low"' },
149
163
  ],
150
164
  constraints: ["highest mean score wins with stable input index as the tie-break; empty input returns undefined"],
151
165
  },
@@ -164,8 +178,8 @@ export const WORKFLOW_CAPABILITIES: readonly CapabilityEntry[] = [
164
178
  {
165
179
  name: "completenessCheck",
166
180
  classification: "runtime-global",
167
- signature: "completenessCheck(taskArgs, results) => Promise<{ complete, missing? } | null>",
168
- options: [],
181
+ signature: "completenessCheck(taskArgs, results, options?) => Promise<{ complete, missing? } | null>",
182
+ options: [{ name: "strength", kind: '"low" | "medium" | "high"', optional: true, default: '"medium"' }],
169
183
  constraints: ["only the first 4,000 characters of serialized result evidence are sent to the critic"],
170
184
  },
171
185
  {
@@ -199,6 +213,7 @@ export const WORKFLOW_CAPABILITIES: readonly CapabilityEntry[] = [
199
213
  constraints: [
200
214
  "consumes one agent slot and no tokens",
201
215
  "journaled answers replay only within an unchanged resume prefix",
216
+ "an option name outside this list is rejected rather than ignored, since a dropped headless silently auto-approves",
202
217
  ],
203
218
  },
204
219
  { name: "log", classification: "runtime-global", signature: "log(message) => void", options: [], constraints: [] },
package/src/engine.ts CHANGED
@@ -61,6 +61,7 @@ import {
61
61
  type WorkflowRunResult,
62
62
  type WorkflowRuntimeEvent,
63
63
  } from "./runtime.js";
64
+ import type { WorkflowStrengthTable } from "./strengths.js";
64
65
 
65
66
  export const MAX_ATTEMPTS_PER_NODE = 3;
66
67
 
@@ -88,8 +89,19 @@ export interface ScriptStartOptions {
88
89
  concurrency?: number;
89
90
  agentRetries?: number;
90
91
  tokenBudget?: number | null;
91
- /** True for a script this package ships; see WorkflowRunOptions.shippedScript. */
92
- shippedScript?: boolean;
92
+ /**
93
+ * The user's strength → Agent tier table; see WorkflowRunOptions.strengths.
94
+ *
95
+ * Deliberately absent from `frozenOptions`. The frozen set is the run's
96
+ * inputs — budget, scale, toolset — which a resume must not silently
97
+ * renegotiate. Routing is not an input: a resume already re-reads the host's
98
+ * tier catalogue, and a table frozen against a catalogue that has since moved
99
+ * would be the one thing in the run still pointing at the old world.
100
+ *
101
+ * Being unfrozen makes supplying it every resume's job, including the resumes
102
+ * the engine starts itself — see {@link WorkflowEngine.strengths}.
103
+ */
104
+ strengths?: WorkflowStrengthTable;
93
105
  agentTimeoutMs?: number | null;
94
106
  toolset?: string;
95
107
  excludeTools?: string[];
@@ -105,13 +117,8 @@ export interface ScriptStartOptions {
105
117
  timeoutMs?: number;
106
118
  },
107
119
  ) => Promise<unknown>;
108
- /**
109
- * Resolve a nested `workflow(name)` reference. The result reports whether the
110
- * script it returned ships with this package, because that decides how the
111
- * child frame treats a tier name the host does not define — see
112
- * `WorkflowRunOptions.shippedScript`.
113
- */
114
- loadSavedWorkflow?: (name: string) => { script: string; shippedScript?: boolean } | undefined;
120
+ /** Resolve a nested `workflow(name)` reference to the script it names. */
121
+ loadSavedWorkflow?: (name: string) => string | undefined;
115
122
  mainModel?: string;
116
123
  }
117
124
 
@@ -151,14 +158,7 @@ export interface ScriptRunState {
151
158
  /** Frozen run parameters — resume reuses the original values, ignoring new budget/toolset overrides. */
152
159
  frozenOptions: Pick<
153
160
  ScriptStartOptions,
154
- | "tokenBudget"
155
- | "maxAgents"
156
- | "concurrency"
157
- | "agentRetries"
158
- | "agentTimeoutMs"
159
- | "toolset"
160
- | "shippedScript"
161
- | "excludeTools"
161
+ "tokenBudget" | "maxAgents" | "concurrency" | "agentRetries" | "agentTimeoutMs" | "toolset" | "excludeTools"
162
162
  >;
163
163
  /** Abort controller for this run's script execution. */
164
164
  controller: AbortController;
@@ -289,6 +289,18 @@ export class WorkflowEngine {
289
289
  onRunSettled: (() => void) | undefined;
290
290
  /** Optional in-process observer for phase/task/quality runtime events. */
291
291
  onRuntimeEvent: ((runId: string, event: WorkflowRuntimeEvent) => void) | undefined;
292
+ /**
293
+ * The user's current strength → Agent tier table, read fresh per execution.
294
+ *
295
+ * A callback rather than a value because the table is deliberately unfrozen:
296
+ * "read fresh at each start and resume" has to hold for the resumes the
297
+ * engine starts on its own — `control("resume")` and the provider-limit
298
+ * retry — which have no caller to pass options for them. Without it those
299
+ * resume with no table at all, and since a call's identity keys on the tier
300
+ * it requested, every already-finished call would miss its journal entry and
301
+ * re-run untabled: the exact re-spend this table exists to prevent.
302
+ */
303
+ strengths: (() => WorkflowStrengthTable | undefined) | undefined;
292
304
  private readonly waiters = new Map<
293
305
  string,
294
306
  Set<{ resolve: (run: ScriptRun) => void; reject: (e: unknown) => void; settled: boolean; cleanup: () => void }>
@@ -628,7 +640,6 @@ export class WorkflowEngine {
628
640
  const runId = randomUUID();
629
641
  const scriptHash = hashScript(script);
630
642
  const toolset = options.toolset;
631
- const shippedScript = options.shippedScript === true ? true : undefined;
632
643
  const durableArgs = cloneWorkflowArgs(options.args);
633
644
  const runtimeArgs = cloneWorkflowArgs(durableArgs);
634
645
  const journalArgs = cloneWorkflowArgs(durableArgs);
@@ -657,7 +668,6 @@ export class WorkflowEngine {
657
668
  meta,
658
669
  ...(durableArgs === undefined ? {} : { args: durableArgs }),
659
670
  frozenArgsPresent,
660
- ...(shippedScript === undefined ? {} : { shippedScript }),
661
671
  ...(toolset ? { toolset } : {}),
662
672
  ...(frozenMaxAgents !== undefined ? { frozenMaxAgents } : {}),
663
673
  ...(frozenConcurrency !== undefined ? { frozenConcurrency } : {}),
@@ -686,7 +696,6 @@ export class WorkflowEngine {
686
696
  meta,
687
697
  ...(journalArgs === undefined ? {} : { args: journalArgs }),
688
698
  frozenArgsPresent,
689
- ...(shippedScript === undefined ? {} : { shippedScript }),
690
699
  ...(toolset ? { toolset } : {}),
691
700
  ...(frozenMaxAgents !== undefined ? { frozenMaxAgents } : {}),
692
701
  ...(frozenConcurrency !== undefined ? { frozenConcurrency } : {}),
@@ -705,7 +714,6 @@ export class WorkflowEngine {
705
714
  ...(frozenAgentRetries !== undefined ? { agentRetries: frozenAgentRetries } : {}),
706
715
  ...(frozenAgentTimeoutMs !== undefined ? { agentTimeoutMs: frozenAgentTimeoutMs } : {}),
707
716
  ...(options.toolset ? { toolset: options.toolset } : {}),
708
- ...(shippedScript === undefined ? {} : { shippedScript }),
709
717
  ...(options.excludeTools ? { excludeTools: [...options.excludeTools] } : {}),
710
718
  },
711
719
  controller: new AbortController(),
@@ -802,11 +810,15 @@ export class WorkflowEngine {
802
810
  this.setWorkflowStatus(run, "running");
803
811
  state.controller = new AbortController();
804
812
  // Freeze semantics: original run parameters and args win over new options.
805
- // Only signal/confirm/background/loadSavedWorkflow/mainModel are per-resume context;
806
- // input/budget/scale/toolset are frozen from the original start.
813
+ // Only signal/confirm/background/loadSavedWorkflow/mainModel/strengths are
814
+ // per-resume context; input/budget/scale/toolset are frozen from the
815
+ // original start.
807
816
  const frozen = state.frozenOptions;
808
817
  const resumeOptions: ScriptStartOptions = {
809
818
  ...options,
819
+ // `??`, not an overwrite: a caller that passed a table meant it, and only
820
+ // a caller that passed none is asking the engine to read the current one.
821
+ strengths: options.strengths ?? this.strengths?.(),
810
822
  args: resumeArgs,
811
823
  tokenBudget: frozen.tokenBudget,
812
824
  maxAgents: frozen.maxAgents,
@@ -814,7 +826,6 @@ export class WorkflowEngine {
814
826
  agentRetries: frozen.agentRetries,
815
827
  agentTimeoutMs: frozen.agentTimeoutMs,
816
828
  toolset: frozen.toolset,
817
- shippedScript: frozen.shippedScript,
818
829
  excludeTools: frozen.excludeTools ? [...frozen.excludeTools] : undefined,
819
830
  };
820
831
  state.bufferedTerminals.clear();
@@ -1010,7 +1021,7 @@ export class WorkflowEngine {
1010
1021
  executionNonce: `${generation}-${randomUUID()}`,
1011
1022
  resumeJournal,
1012
1023
  toolset: options.toolset,
1013
- shippedScript: options.shippedScript,
1024
+ strengths: options.strengths,
1014
1025
  excludeTools: options.excludeTools,
1015
1026
  confirm: options.confirm,
1016
1027
  loadSavedWorkflow: options.loadSavedWorkflow,
@@ -1750,7 +1761,6 @@ export class WorkflowEngine {
1750
1761
  ...(run.frozenAgentRetries !== undefined ? { agentRetries: run.frozenAgentRetries } : {}),
1751
1762
  ...(run.frozenAgentTimeoutMs !== undefined ? { agentTimeoutMs: run.frozenAgentTimeoutMs } : {}),
1752
1763
  ...(run.toolset ? { toolset: run.toolset } : {}),
1753
- ...(run.shippedScript === undefined ? {} : { shippedScript: run.shippedScript }),
1754
1764
  ...(run.frozenExcludeTools ? { excludeTools: [...run.frozenExcludeTools] } : {}),
1755
1765
  },
1756
1766
  controller: new AbortController(),