@signalridge/pi-subagents 1.8.1 → 1.9.1

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,192 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.1
4
+ ### Patch Changes
5
+
6
+ - fabe89a: Document how a workflow now reaches this catalogue.
7
+
8
+ The "One catalogue, including for workflows" section said a workflow script names
9
+ a key from `agentTiers` directly, with "no second workflow-tier vocabulary and no
10
+ mapping layer" and `agent({ tier: "low" })` examples. pi-workflows scripts now
11
+ name a *strength* and a table on that side chooses the tier, so every claim in
12
+ that section was inverted and its example is rejected before dispatch.
13
+
14
+ The claim it was protecting is still true and is now stated where it belongs:
15
+ there is no second tier catalogue and no second resolver, a `strengths` value is
16
+ a key in this catalogue and never carries its own `model`/`thinking`, and a
17
+ request arriving here is indistinguishable from a spawn that named the key
18
+ itself. Also notes that the shipped `low`/`medium`/`high` profiles are what
19
+ pi-workflows' default table maps onto, so renaming them leaves workflows on the
20
+ ordinary untiered path rather than breaking them.
21
+
22
+ Drops a stale pointer to `workflow.tiers`, a settings key this package no longer
23
+ reads.
24
+
25
+ ## 1.9.0
26
+ ### Minor Changes
27
+
28
+ - c1b1741: Protocol v4: workflows use the Agent-tier catalogue directly.
29
+
30
+ The wire contract changes, so the three packages move together: `pi-workflows`
31
+ now declares `"@signalridge/pi-subagents": ">=1.9.0"`, and a peer outside that
32
+ range fails the startup handshake with a diagnostic naming both packages rather
33
+ than failing mid-run.
34
+
35
+ The `small | medium | large` workflow-tier vocabulary and its mapping layer are
36
+ gone. A managed request names an Agent tier — a key in the host's own
37
+ `agentTiers` catalogue — and `resolveAgentTier()` is the single resolver for
38
+ every spawn path. Removed with it: the wire's mapped `agentTier` field, the
39
+ `workflow` settings key (retired with a warning naming it; `agentTiers.defaultTier`
40
+ replaces `workflow.defaultTier`), and the per-call `model`/`thinking` selectors,
41
+ which the validator now rejects rather than accepting and ignoring.
42
+
43
+ Every v4 capability is required and every ping carries the host's tier catalogue,
44
+ so an incomplete peer fails one check instead of five optional ones. Fresh
45
+ installs ship `low`/`medium`/`high` profiles, all inheriting their model, and a
46
+ managed call that names no tier falls back to `medium`, so a workflow runs on an
47
+ unconfigured machine without this package choosing a vendor. Because `medium`
48
+ inherits, that fallback lands on the parent session's model — it commits to an
49
+ effort level, not to a vendor, so it does change the thinking level a managed
50
+ call runs at when the parent session is set higher, and it does not move the
51
+ work anywhere cheaper. What it buys is a named policy with a durable snapshot
52
+ and a scope check; a workspace that wants cheaper managed work names a
53
+ `defaultTier` whose profile pins a model. The fallback is scoped to calls that
54
+ cannot inherit a parent model rather than being installed as
55
+ `agentTiers.defaultTier`: a shipped catalogue default would have applied to
56
+ every ordinary `Agent` spawn as well, silencing `defaultModel` and pinning a
57
+ thinking level on machines that configured neither. The catalogue no longer
58
+ ships a `fast` profile; it was the same (model, thinking) pair as `low`, and the
59
+ one agent that named it (Explore) now names `low`. A user agent file that still
60
+ says `agentTier: fast` is reported by the existing unknown-reference check when
61
+ settings and agents load, rather than failing at its first spawn.
62
+
63
+ `agentTiers.defaultTier` therefore has three states, and `/agents → Settings`
64
+ offers all three rather than rendering two of them as one word: a named tier,
65
+ `unset` (managed calls reach the shipped fallback), and `none` (recorded as
66
+ `noDefaultTier`, which withdraws the fallback so managed calls fail closed).
67
+ `setDefaultAgentTier` takes that choice as a tagged value, so a caller cannot
68
+ express "no default" without saying which of the two it means.
69
+
70
+ `resolveAgentTier` gained `requireTier` for that fallback, and the two things
71
+ pre-resolution callers need are both answered by the resolver rather than
72
+ restated beside it: `agentTierApplies()` for "will a tier own this spawn's
73
+ model?", and `selectAgentTier()` for the tier key itself, which the managed
74
+ path needs to label a tombstone and the lifecycle events before the runner has
75
+ resolved anything.
76
+
77
+ Because a tier now owns model resolution outright, the spawn paths stop
78
+ pre-resolving one — and with it they stopped producing the model name the agent
79
+ UI shows. The resolution callback carries that label back instead, so a tier
80
+ that pins a model still names it in the viewer and the agent list, and a profile
81
+ that inherits correctly shows none. The label rides beside the snapshot rather
82
+ than inside it: the snapshot is a durable policy record a managed tombstone
83
+ persists and revalidates, and a cosmetic string does not belong in it.
84
+
85
+ Workflow resume now keys each cached call on the policy for that call's own tier
86
+ rather than on a whole-catalogue fingerprint, so defining or editing an unrelated
87
+ tier no longer forces a full re-execution — but only for a call that names its
88
+ tier. A call that names none keeps the whole-catalogue key, because the host
89
+ resolves those as `call > agent frontmatter > defaultTier` and frontmatter is
90
+ not on this wire: an agent reaches a managed caller as a name. Keyed on
91
+ `defaultTier`, such a call would be wrong twice over — replaying stale work
92
+ after an edit to the tier its agent actually declares, and re-executing after an
93
+ edit to a default it never reached. `agentTierPolicyIdentity()` therefore
94
+ requires a tier rather than accepting `undefined` and folding the default in, so
95
+ the wrong call cannot be written. Journal schema v4 and managed tombstone schema
96
+ v2 are quarantined from older facts rather than migrated.
97
+
98
+ The tier catalogue belongs to the user, names included. The built-in workflows
99
+ and the ad-hoc script ship with this package, so a tier name they use that the
100
+ host does not define is dropped in favour of the host's default rather than
101
+ failing the run — they cannot assert which names exist on someone else's
102
+ machine. A script the user wrote still fails closed on an undefined tier, since
103
+ that is a typo in a catalogue they own. Shipped-ness travels with the script
104
+ rather than with the frame that called it, so a user script that reaches a
105
+ built-in through `workflow(name)` applies the built-in's rule to the built-in;
106
+ the authoring skill now says outright that `low`/`medium`/`high` are the shipped
107
+ profiles rather than names an authored script may assume.
108
+
109
+ The managed routing policy published on the wire reports the default a managed
110
+ call will actually get, fallback included, so a peer's replay identity cannot
111
+ disagree with the host's selection; its sorts are code-unit rather than
112
+ locale-dependent, so the same catalogue fingerprints identically everywhere. The
113
+ workflow peer re-pings per start and resume instead of pinning the catalogue it
114
+ saw at session start, and reports the host-selected tier back through an
115
+ `onTierResolved` callback rather than by mutating the dispatch options.
116
+
117
+ Fixes: a nested spawn no longer relabels an agent's frontmatter tier as
118
+ caller-requested, which had turned a `scopeModels` warning into a refusal and
119
+ misattributed the choice; the scheduler no longer freezes a frontmatter tier into
120
+ a job, so editing the agent file takes effect at fire time; retired managed spawn
121
+ keys are now bounded instead of growing for the life of a session; a
122
+ scope refusal for a tier whose profile inherits its model now names the model
123
+ that would have run instead of the literal `undefined`; and the retired
124
+ `workflow` key warns once per process rather than on every settings read and
125
+ every project write.
126
+
127
+ The tier-key predicate and its length bound now have a single definition, in
128
+ `pi-subagents-protocol`, which `pi-subagents` re-exports. The wire is the
129
+ narrower of the two gates — a key one accepted and the other rejected could
130
+ never reach a peer — so a second copy could only ever be a way for them to
131
+ disagree. That one definition now also governs `blockedProfiles`, which was
132
+ being validated by a helper that trims first: `" low"` became `"low"` there
133
+ while the same value in `profiles` or `defaultTier` was refused outright. All
134
+ three reject.
135
+
136
+ `/agents → Settings` no longer names the fallback tier in the `unset` row's
137
+ label. Which tier `unset` reaches depends on the catalogue, and a workspace that
138
+ edits or deletes the shipped `medium` profile leaves it reaching nothing — at
139
+ which point `unset` and `none` behave identically. The row's description asks
140
+ the resolver what `unset` would currently resolve to and says that, including
141
+ when the answer is "nothing".
142
+
143
+ `pi-workflows` also gained the session-lifecycle handling this routing work
144
+ needed to sit on. The engine has an explicit dispose lifecycle, so a protocol
145
+ probe or a start that is in flight when a session ends rejects with a disposed
146
+ error instead of resolving against an engine nobody owns. The tool, command and
147
+ widget surfaces register exactly once and resolve the *current* engine on each
148
+ use, rather than closing over the one that existed when they were registered —
149
+ a session replacement previously left them pointing at a disposed engine. Tool
150
+ and command handlers report failures as results rather than throwing out of the
151
+ host's dispatch, and the lifecycle handler accepts `subagents:created` and
152
+ `subagents:started` so a spawn's identity is recorded from the first event that
153
+ carries it rather than only at completion.
154
+
155
+ ### Patch Changes
156
+
157
+ - c1b1741: Durability and containment fixes found in a repository-wide scan.
158
+
159
+ `pi-gpt-fast` rewrote pi's own global `settings.json` with a plain
160
+ `writeFileSync`. That file is shared with pi and every other extension, and this
161
+ one is the only writer of it; a torn write would have left the user with no pi
162
+ configuration at all rather than just no gpt-fast setting. It now writes a temp
163
+ file and renames, matching every other settings writer in the repository.
164
+ `pi-goal` gets the same treatment for its cross-project goal state file, which
165
+ its own settings module already did.
166
+
167
+ `pi-ralph-wiggum` resolved `/ralph start <path>` straight against the session
168
+ cwd, so `../../notes.md` would create directories and a file outside the project
169
+ the user opened — silently, before the loop started. This extension drives long
170
+ unattended loops, so that command is as likely to come from a model as from a
171
+ person. Task-file paths are now required to stay inside the workspace, on the
172
+ command, on the `ralph_start` tool, and on the paths read back out of persisted
173
+ state, so a state file written earlier or edited by hand cannot pull a file in
174
+ from outside either.
175
+
176
+ The containment test canonicalizes both sides before comparing, because a purely
177
+ lexical one is defeated by a symlink: every segment of `linked/plan.md` reads as
178
+ inside the workspace when `linked` is a door out of it. `realpathSync` throws on
179
+ a path that does not exist yet — the ordinary case, since `start` is usually
180
+ creating the file — so it resolves the deepest existing ancestor and re-attaches
181
+ the not-yet-created suffix, which cannot itself be a link. The traversal test
182
+ matches a `..` segment rather than a `..` prefix; the prefix form also rejected
183
+ `..notes.md`, an ordinary filename sitting in the workspace.
184
+
185
+ Every remaining bare `catch {}` now says why the error is safe to drop, matching
186
+ the convention the rest of the repository already follows.
187
+ - Updated dependencies [c1b1741]
188
+ - @signalridge/pi-subagents-protocol@1.4.0
189
+
3
190
  ## 1.8.1
4
191
  ### Patch Changes
5
192
 
package/README.md CHANGED
@@ -26,7 +26,7 @@ A [pi](https://pi.dev) extension that brings **Claude Code-style autonomous sub-
26
26
  - **Tool denylist** — block specific tools via `disallowed_tools` frontmatter
27
27
  - **Styled completion notifications** — background agent results render as themed, compact notification boxes (icon, stats, result preview) instead of raw XML. Expandable to show full output. Group completions render each agent individually
28
28
  - **Event bus** — lifecycle events (`subagents:created`, `started`, `completed`, `failed`, `steered`, `compacted`) emitted via `pi.events`, enabling other extensions to react to sub-agent activity
29
- - **Cross-extension RPC** — other Pi extensions can spawn and stop subagents via the `pi.events` event bus (`subagents:rpc:ping`, `subagents:rpc:spawn`, `subagents:rpc:stop`). Protocol v3 adds managed spawning, optional model/thinking/toolset/denylist/thread/worktree hints, owner-scoped stop/quiescence, and standardized reply envelopes; pi-subagents remains the final policy owner. Emits `subagents:ready` on session start
29
+ - **Cross-extension RPC** — other Pi extensions can spawn and stop subagents via the `pi.events` event bus (`subagents:rpc:ping`, `subagents:rpc:spawn`, `subagents:rpc:stop`). Protocol v4 adds managed spawning, a request-level Agent `tier`, toolset/denylist/thread/worktree hints, owner-scoped stop/quiescence, a published Agent-tier routing policy, and standardized reply envelopes; pi-subagents remains the final policy owner. Pre-schema-v2 managed tombstones are quarantined rather than replayed. Emits `subagents:ready` on session start
30
30
  - **Schedule subagents** — pass `schedule` to the `Agent` tool to fire on cron / interval / one-shot. Session-scoped jobs with PID-locked persistence; results land via the same `subagent-notification` followUp path as manual background completions; manage via `/agents → Scheduled jobs`
31
31
  - **Model tiers** — name a (model, thinking) pair once and let the orchestrator pick it by name; the `Agent` tool exposes `tier` and never `model`/`thinking`, so which model runs stays a config decision. Manage the catalogue in `/agents → Model tiers`, pick the default in `/agents → Settings → Default tier`, or set a plain `defaultModel` when one line beats a catalogue
32
32
  - **Model scope enforcement** — opt-in validation that subagent model choices stay within your pi `enabledModels` allowlist (sourced from `/scoped-models`, with both global and project-local pi settings honored). Caller-supplied out-of-scope → hard error to orchestrator; frontmatter-pinned out-of-scope → warning + runs anyway (frontmatter authoritative). Toggle via `/agents → Settings → Scope models`
@@ -414,12 +414,12 @@ When background agents complete, they notify the main agent. The **join mode** c
414
414
 
415
415
  ## Model tiers
416
416
 
417
- A **tier** is one name for a (model, thinking) pair. The host agent picks a tier
418
- by name and nothing else: the `Agent` tool exposes `tier` and does **not** expose
419
- `model` or `thinking`, so which model runs is decided by whoever writes
420
- `subagents.json`, not by the orchestrator improvising per call.
417
+ An **Agent tier** is one name for a (model, thinking) pair. The host agent picks a
418
+ tier by name and nothing else: the `Agent` tool exposes `tier` and does **not**
419
+ expose `model` or `thinking`, so which model runs is decided by the Agent-tier
420
+ catalogue rather than by the orchestrator improvising per call.
421
421
 
422
- Names are yours. `small`/`medium`/`large` below are only an example — `research`,
422
+ Names are yours. The names below are only an example — `research`,
423
423
  `cheap`, `nightly` are equally valid keys. Replace the illustrative provider/model
424
424
  values with models available in your environment.
425
425
 
@@ -428,10 +428,10 @@ values with models available in your environment.
428
428
  "agentTiers": {
429
429
  "defaultTier": "medium",
430
430
  "profiles": {
431
- "small": { "description": "Fast, cheap exploration", "model": "provider/fast-model", "thinking": "max" },
432
- "medium": { "description": "Ordinary planning and review", "model": "provider/reasoning-model", "thinking": "max" },
433
- "large": { "description": "Architecture and risky review", "model": "provider/architecture-model", "thinking": "xhigh" },
434
- "research": { "description": "Long-context research", "model": "provider/long-context-model", "thinking": "max" }
431
+ "low": { "description": "Fast, cheap exploration", "model": "provider/fast-model", "thinking": "max" },
432
+ "medium": { "description": "Ordinary planning and review", "model": "provider/reasoning-model", "thinking": "max" },
433
+ "high": { "description": "Architecture and risky review", "model": "provider/architecture-model", "thinking": "xhigh" },
434
+ "research": { "description": "Long-context research", "model": "provider/long-context-model", "thinking": "max" }
435
435
  }
436
436
  }
437
437
  }
@@ -439,7 +439,82 @@ values with models available in your environment.
439
439
 
440
440
  A profile is all-or-nothing: both `model` and `thinking` are required, and either
441
441
  may be the literal `"inherit"` to keep the parent's. `description` is optional and
442
- defaults to the key; it is what the host reads when choosing between tiers.
442
+ defaults to the key; it is what the host reads when choosing between Agent tiers.
443
+
444
+ ### One catalogue, including for workflows
445
+
446
+ A managed `pi-workflows` call arrives naming a key from this same `agentTiers`
447
+ catalogue. There is no second tier catalogue and no second resolver: this package
448
+ still owns every model, every thinking level, and the only `resolveAgentTier()`.
449
+
450
+ What a workflow *script* writes is not that key. A script names a **strength** —
451
+ `low`, `medium`, `high`, pi-workflows' own word for how much effort a step
452
+ deserves — and a `strengths` table on that side chooses which of your tiers it
453
+ runs on:
454
+
455
+ ```js
456
+ // in a workflow script
457
+ await agent("summarize this diff", { strength: "low" })
458
+ await agent("design the migration", { strength: "high" })
459
+ ```
460
+
461
+ ```jsonc
462
+ // pi-workflows' own settings, edited with `/workflows strength`
463
+ { "strengths": { "low": "cheap-search", "high": "deep" } }
464
+ ```
465
+
466
+ The indirection exists so that re-pricing workflow work does not re-price
467
+ everything else. Workflow fan-outs ask for cheap work by the dozen, and so does
468
+ the `Explore` agent and every spawn that names no tier of its own; if the
469
+ workflow side reached your catalogue directly, making a 26-agent fan-out
470
+ affordable would mean editing the tier all of them share. Pointing a strength
471
+ elsewhere leaves your tiers alone.
472
+
473
+ Nothing about that reaches this side. By the time a request arrives it carries
474
+ one tier key, and this package cannot tell a mapped call apart from a spawn that
475
+ named the key itself. Nor is it a second *policy*: a `strengths` value is a key
476
+ in this catalogue and never carries a `model` or `thinking` of its own — that is
477
+ the line between it and the retired `workflow.tiers` key, which did.
478
+
479
+ The tier is resolved by the same `resolveAgentTier()` path an ordinary Agent
480
+ spawn uses — same precedence, same model lookup, same thinking clamping, same
481
+ availability checks, same immutable resolution snapshot. A tier the host does not
482
+ define is rejected before dispatch, naming the tiers it does define.
483
+
484
+ Model and thinking are deliberately absent from the managed request. A tier is
485
+ the only model policy a workflow can express, so there is no second selector that
486
+ could silently win or be silently ignored.
487
+
488
+ Fresh installs ship an effort ladder: `low`, `medium`, `high`. Every shipped
489
+ profile inherits its model, so a new machine gets a working vocabulary without
490
+ this package ever choosing a vendor for you. Those names are also what
491
+ pi-workflows' shipped default table maps its strengths onto — identity, and only
492
+ where you define the name — so a stock machine runs workflows at the strengths
493
+ their scripts asked for. Rename or remove them and that default simply yields
494
+ nothing: a managed call that names no tier uses the agent's own tier, then
495
+ `agentTiers.defaultTier`, and finally falls back to `medium`.
496
+
497
+ `medium` inherits its model, so on an unconfigured machine that fallback runs on
498
+ the parent session's model. What it buys is a call with a *named* policy, a
499
+ durable resolution snapshot and a scope check — not cheaper work. If you want
500
+ managed work to run somewhere cheaper, set a `defaultTier` whose profile pins a
501
+ model.
502
+
503
+ That last fallback is scoped to managed calls. It is deliberately **not** the
504
+ catalogue's `defaultTier`: a shipped default that applied to every ordinary spawn
505
+ would silence [`defaultModel`](#defaultmodel) and pin a thinking level on machines
506
+ that configured neither.
507
+
508
+ So `Default tier` has three settings, not two, and the menu offers all three:
509
+
510
+ | Setting | Ordinary spawn | Managed workflow call |
511
+ | --- | --- | --- |
512
+ | a tier name | that tier | that tier |
513
+ | `unset` | `defaultModel`, then the parent session | the shipped `medium` |
514
+ | `none` | `defaultModel`, then the parent session | rejected |
515
+
516
+ `none` is a policy statement, recorded as `noDefaultTier`; `unset` is the absence
517
+ of one. Deleting the profiles has the same effect on managed calls as `none`.
443
518
 
444
519
  ### How the host discovers tiers
445
520
 
@@ -450,7 +525,7 @@ remember. It sees:
450
525
  ```
451
526
  Available agent tiers:
452
527
 
453
- - small: Fast, cheap exploration
528
+ - low: Fast, cheap exploration
454
529
  model: provider/fast-model
455
530
  thinking: max
456
531
  ...
@@ -477,6 +552,11 @@ frontmatter are read only to warn that they are stale, and the built-in agents
477
552
  pin nothing either. With nothing configured at all, a subagent runs on the
478
553
  parent session's model.
479
554
 
555
+ A **managed workflow call** cannot take steps 4 and 5 — it has no parent session
556
+ to inherit from — so it gets one extra step between 3 and the end: the shipped
557
+ `medium` fallback. That step exists only for callers that would otherwise fail
558
+ closed, which is why it does not displace `defaultModel` for everyone else.
559
+
480
560
  ### `defaultModel`
481
561
 
482
562
  Steps 1–3 are a catalogue; step 4 is one line. Set it when the whole point is
@@ -523,7 +603,7 @@ the global file defines. The menu writes the merged catalogue back to the
523
603
  project file, so deleting one of several works, but deleting the last one — or
524
604
  clearing a `defaultTier` that only global sets — leaves no `agentTiers` key
525
605
  behind, and the global value is inherited again on the next start. Remove it
526
- from `~/.pi/agent/subagents.json` instead. The same is true of `workflow.tiers`.
606
+ from `~/.pi/agent/subagents.json` instead.
527
607
 
528
608
  ### Refusals
529
609
 
@@ -549,22 +629,13 @@ level nobody chose for that pair. A project profile that fails validation blocks
549
629
  its global namesake rather than reviving it, and `defaultTier` is a simple
550
630
  project-over-global override.
551
631
 
552
- ### Not the same as `workflow.tiers`
553
-
554
- `pi-workflows` has its own tiers, and they stay fixed at `small | medium | large`.
555
- That vocabulary is part of the cross-package protocol: it lets a workflow
556
- definition be validated at parse time and stay portable between machines, neither
557
- of which survives arbitrary names. The two systems share no fields — a spawn
558
- records `agentTier`/`agentTierSnapshot` or `tier`/`tierSnapshot`, never one
559
- standing in for the other.
560
-
561
632
  ### Migrating from `model:`/`thinking:`
562
633
 
563
- Define the profiles once, then replace each agent's `model:`/`thinking:` with
564
- `tier: <name>`. Files that still carry the old fields load and run — the fields
565
- are ignored, with a warning naming the file — so the migration can be done one
566
- agent at a time. Until an agent names a tier it uses `defaultTier`, or the
567
- parent's model when none is set.
634
+ Define the Agent-tier profiles once, then replace each agent's `model:`/`thinking:`
635
+ with `tier: <name>`. Files that still carry the old fields load and run — the
636
+ fields are ignored, with a warning naming the file — so the migration can be done
637
+ one agent at a time. Until an agent names a tier it uses `agentTiers.defaultTier`,
638
+ then `defaultModel`, or the parent's model when none is set.
568
639
 
569
640
  Programmatic callers and the legacy RPC may still pass `model`/`thinking`
570
641
  directly. That is the escape hatch for code, not a way to configure an agent.
@@ -579,11 +650,11 @@ When on, each subagent spawn's effective model is validated against pi's own `en
579
650
 
580
651
  | Model source | Out-of-scope behavior |
581
652
  |---|---|
582
- | Caller-supplied via `Agent({ model: "..." })` | Hard error returned to the orchestrator, listing allowed models |
653
+ | Caller-supplied programmatic `model` (only when no Agent tier applies) | Hard error returned to the orchestrator, listing allowed models |
583
654
  | Pinned in agent frontmatter | Warning toast + the pinned model runs (frontmatter is authoritative) |
584
655
  | Parent-inherited (neither set) | Warning toast + parent's model runs |
585
656
 
586
- **Design:** `scopeModels` is a guardrail against the orchestrator picking unexpected models at runtime, not a hard policy against user-level config. The "frontmatter is authoritative" guarantee from v0.5.1 still holds for `model:` caller params can't override frontmatter, and frontmatter pins run even when out of scope (with a visible warning).
657
+ **Design:** `scopeModels` is a guardrail against unexpected runtime model choices, not a hard policy against user-level config. An applicable Agent tier is checked after its single final resolution; compatibility model inputs are checked only on the no-tier path and cannot override a tier.
587
658
 
588
659
  **Nested spawns** ([nested subagents](#nested-subagents)) apply the same table against the parent's config root. The hard-error case is identical; the warning cases proceed silently, since a subagent session has no UI to toast to.
589
660
 
@@ -600,26 +671,9 @@ Runtime tuning values set via `/agents` → Settings (max concurrency, default m
600
671
 
601
672
  **Precedence:** project overrides global on any field present in both. Missing fields fall back to the hardcoded defaults (max concurrency `4`, default max turns unlimited, grace turns `5`, nested depth `2`, join mode `smart`, defaults enabled).
602
673
 
603
- **Workflow tiers** (`workflow`) are semantic model-plus-thinking profiles used by `@signalridge/pi-workflows` managed spawns. The wire request carries only `"small"`, `"medium"`, or `"large"`; pi-subagents resolves the configured profile at spawn time and records the immutable resolution snapshot in its managed session journal.
604
-
605
- ```json
606
- {
607
- "workflow": {
608
- "defaultTier": "medium",
609
- "tiers": {
610
- "small": { "model": "inherit", "thinking": "low" },
611
- "medium": { "model": "inherit", "thinking": "medium" },
612
- "large": { "model": "provider/architecture-model", "thinking": "max" }
613
- }
614
- }
615
- }
616
- ```
617
-
618
- Replace the illustrative `provider/architecture-model` value with a model available in your environment.
619
-
620
- Profiles are complete `model` + `thinking` tuples. Each field may use `inherit`; a project profile replaces the whole matching global tier entry. Malformed or incomplete entries are retained as durable blocked-tier tombstones and fail closed rather than falling back to a built-in profile or merging field-by-field. An explicit `defaultTier` applies when a workflow task omits its tier. Without a task tier or configured default, the parent model and thinking level are inherited. Agent frontmatter remains authoritative for its explicit `model` and `thinking`; thinking is clamped to the selected model's native supported levels.
674
+ The `workflow` settings key is **retired**; a file that still has one is ignored with a warning naming the key. This file holds no workflow routing of its own: a managed `pi-workflows` call arrives naming a key from `agentTiers`, and which key that is was decided on the pi-workflows side by its own `strengths` table a table of keys into this catalogue, never a second catalogue and never its own `model`/`thinking`. `agentTiers.defaultTier` replaces what `workflow.defaultTier` used to do. See [One catalogue, including for workflows](#one-catalogue-including-for-workflows).
621
675
 
622
- **Default model** (`defaultModel`, unset): the model a subagent runs when no tier picked one — see [`defaultModel`](#defaultmodel) for where it sits in precedence, why an unresolvable value falls back instead of failing, and how `"inherit"` lets a project cancel a global default. **Default tier** (`agentTiers.defaultTier`, unset) is the tier applied when neither the caller nor the agent names one; the profiles it selects from live under [`agentTiers`](#model-tiers).
676
+ **Default model** (`defaultModel`, unset): the model a non-tiered ordinary subagent runs — see [`defaultModel`](#defaultmodel) for where it sits in precedence, why an unresolvable value falls back instead of failing, and how `"inherit"` lets a project cancel a global default. **Default tier** (`agentTiers.defaultTier`, unset) is the tier applied when neither the caller nor the agent names one; the profiles it selects from live under [`agentTiers`](#model-tiers). It has three settings — a tier name, `unset`, and `none` — which the menu offers separately because the last two behave differently for managed workflow calls; see [Model tiers](#model-tiers) for the table.
623
677
 
624
678
  **Strict agent files** (`strictAgentFiles`, default `false`): normal startup skips unreadable or malformed agent definitions with a warning that includes the file path. Enable it to fail closed during the first `session_start`, using that session's `ctx.cwd`, with the path in the error instead of silently running a surviving lower-priority override. A failed validation leaves no root manager or RPC responder behind. Reloads after startup remain lenient, so an accidental edit cannot terminate an active session; the setting applies on the next pi session.
625
679
 
@@ -676,7 +730,7 @@ Agent lifecycle events are emitted via `pi.events.emit()` so other extensions ca
676
730
  | `subagents:compacted` | Agent's session successfully compacted | `id`, `type`, `description`, `reason` (`"manual"` / `"threshold"` / `"overflow"`), `tokensBefore`, `compactionCount`, optional `owner` |
677
731
  | `subagents:scheduled` | Schedule lifecycle change | `{ type: "added" \| "removed" \| "updated" \| "fired" \| "error", … }` (job/agentId/error fields per type) |
678
732
  | `subagents:scheduler_ready` | Scheduler bound to session, enabled jobs armed | `sessionId`, `jobCount` |
679
- | `subagents:ready` | RPC handlers registered and armed — fired on session start; not emitted in a session that excludes pi-subagents | `version: 3`, `capabilities` (`managedSpawn`, `lifecycleOwner`, `ownedStop`, `ownedQuiescence`, `childContext`, `workflowTiers`) |
733
+ | `subagents:ready` | RPC handlers registered and armed — fired on session start; not emitted in a session that excludes pi-subagents | `version: 4`, `capabilities` (`managedSpawn`, `lifecycleOwner`, `ownedStop`, `ownedQuiescence`, `childContext`, `agentTiers`, `managedPolicy` — all required), `routingPolicy` (Agent-tier catalogue + fingerprint) |
680
734
  | `subagents:settings_loaded` | Persisted settings applied at extension init | `settings` (merged global + project) |
681
735
  | `subagents:settings_changed` | `/agents` → Settings mutation was applied | `settings`, `persisted` (`boolean` — `false` on write failure) |
682
736
 
@@ -702,20 +756,20 @@ pi.events.on("subagents:ready", () => {
702
756
 
703
757
  ### Ping
704
758
 
705
- Check if the subagents extension is loaded and get the protocol version:
759
+ Check if the subagents extension is loaded and get the protocol version and current routing-policy fingerprint:
706
760
 
707
761
  ```typescript
708
762
  const requestId = crypto.randomUUID();
709
763
  const unsub = pi.events.on(`subagents:rpc:ping:reply:${requestId}`, (reply) => {
710
764
  unsub();
711
- if (reply.success) console.log("Protocol version:", reply.data.version);
765
+ if (reply.success) console.log("Protocol version:", reply.data.version, "routing policy:", reply.data.routingPolicy.fingerprint);
712
766
  });
713
767
  pi.events.emit("subagents:rpc:ping", { requestId });
714
768
  ```
715
769
 
716
- ### Managed spawn (protocol v3)
770
+ ### Managed spawn (protocol v4)
717
771
 
718
- Workflow-owned orchestration uses the additive `subagents:rpc:spawn-managed` channel. Its request may include the core identity fields plus optional `tier`, exact `model`, `thinking`, `toolset`, `excludeTools`, `thread`, and `isolation: "worktree"` hints:
772
+ Workflow-owned orchestration uses the `subagents:rpc:spawn-managed` channel. Its request may include the core identity fields plus an optional Agent `tier`, `toolset`, `excludeTools`, `thread`, and `isolation: "worktree"`. There is no per-call `model` or `thinking` — the wire validator rejects them:
719
773
 
720
774
  ```json
721
775
  {
@@ -724,15 +778,14 @@ Workflow-owned orchestration uses the additive `subagents:rpc:spawn-managed` cha
724
778
  "type": "Explore",
725
779
  "prompt": "Find the relevant files",
726
780
  "description": "Find relevant files",
727
- "tier": "small",
728
- "model": "provider/model:medium",
781
+ "tier": "low",
729
782
  "excludeTools": ["workflow", "workflow_control"],
730
783
  "isolation": "worktree",
731
784
  "owner": { "extension": "pi-workflows", "runId": "run-id", "nodeId": "node-id", "attemptId": "run-id/node-id/attempt-1" }
732
785
  }
733
786
  ```
734
787
 
735
- The manager validates and resolves every hint against its own model scope, agent configuration, queue, tool, session, and worktree policy. `spawnKey` is idempotent within a root manager; the same normalized request returns the existing agent id and a conflicting request is rejected. A named managed `thread` re-enters one sequential session only while its effective model, thinking, toolset, denylist, isolation, and agent policy fingerprint remain unchanged; a policy change or concurrent call is rejected rather than silently reusing the old session. Managed agents use the normal Agent execution path, queue, FleetView, activity, transcript, compaction, and lifecycle events. Only the automatic main-session completion nudge is suppressed for an owner-scoped record. Managed requests must carry an attempt-scoped owner, and `stop-owned`/`quiesce-owned` fail closed when exact node/generation metadata is missing. During branch replacement, timed-out records are detached and late callbacks are suppressed.
788
+ The manager validates and resolves the tier, agent configuration, queue, tool, session, and worktree policy against its own Agent-tier catalogue and model scope. The resolved tier and its snapshot are retained on the managed invocation/tombstone. `spawnKey` is idempotent within a root manager; the same normalized request returns the existing agent id and a conflicting request is rejected. A named managed `thread` re-enters one sequential session only while its effective model, thinking, toolset, denylist, isolation, and agent policy fingerprint remain unchanged — including the model and thinking its tier currently resolves to, so switching the session model interrupts a thread whose tier inherits it; a policy change or concurrent call is rejected rather than silently reusing the old session. Managed agents use the normal Agent execution path, queue, FleetView, activity, transcript, compaction, and lifecycle events. Only the automatic main-session completion nudge is suppressed for an owner-scoped record. Managed requests must carry an attempt-scoped owner, and `stop-owned`/`quiesce-owned` fail closed when exact node/generation metadata is missing. During branch replacement, timed-out records are detached and late callbacks are suppressed.
736
789
 
737
790
  ### Spawn
738
791
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signalridge/pi-subagents",
3
- "version": "1.8.1",
3
+ "version": "1.9.1",
4
4
  "description": "Signalridge's managed subagent runtime with workflow-owned orchestration RPC.",
5
5
  "author": "tintinweb and signalridge contributors",
6
6
  "license": "MIT",
@@ -32,7 +32,7 @@
32
32
  "dependencies": {
33
33
  "@sinclair/typebox": "^0.34.50",
34
34
  "croner": "^10.0.1",
35
- "@signalridge/pi-subagents-protocol": "^1.3.1",
35
+ "@signalridge/pi-subagents-protocol": "^1.4.0",
36
36
  "@signalridge/pi-ui": "^1.3.0",
37
37
  "nanoid": "^5.0.0"
38
38
  },