@signalridge/pi-subagents 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,175 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.0
4
+ ### Minor Changes
5
+
6
+ - c1b1741: Protocol v4: workflows use the Agent-tier catalogue directly.
7
+
8
+ The wire contract changes, so the three packages move together: `pi-workflows`
9
+ now declares `"@signalridge/pi-subagents": ">=1.9.0"`, and a peer outside that
10
+ range fails the startup handshake with a diagnostic naming both packages rather
11
+ than failing mid-run.
12
+
13
+ The `small | medium | large` workflow-tier vocabulary and its mapping layer are
14
+ gone. A managed request names an Agent tier — a key in the host's own
15
+ `agentTiers` catalogue — and `resolveAgentTier()` is the single resolver for
16
+ every spawn path. Removed with it: the wire's mapped `agentTier` field, the
17
+ `workflow` settings key (retired with a warning naming it; `agentTiers.defaultTier`
18
+ replaces `workflow.defaultTier`), and the per-call `model`/`thinking` selectors,
19
+ which the validator now rejects rather than accepting and ignoring.
20
+
21
+ Every v4 capability is required and every ping carries the host's tier catalogue,
22
+ so an incomplete peer fails one check instead of five optional ones. Fresh
23
+ installs ship `low`/`medium`/`high` profiles, all inheriting their model, and a
24
+ managed call that names no tier falls back to `medium`, so a workflow runs on an
25
+ unconfigured machine without this package choosing a vendor. Because `medium`
26
+ inherits, that fallback lands on the parent session's model — it commits to an
27
+ effort level, not to a vendor, so it does change the thinking level a managed
28
+ call runs at when the parent session is set higher, and it does not move the
29
+ work anywhere cheaper. What it buys is a named policy with a durable snapshot
30
+ and a scope check; a workspace that wants cheaper managed work names a
31
+ `defaultTier` whose profile pins a model. The fallback is scoped to calls that
32
+ cannot inherit a parent model rather than being installed as
33
+ `agentTiers.defaultTier`: a shipped catalogue default would have applied to
34
+ every ordinary `Agent` spawn as well, silencing `defaultModel` and pinning a
35
+ thinking level on machines that configured neither. The catalogue no longer
36
+ ships a `fast` profile; it was the same (model, thinking) pair as `low`, and the
37
+ one agent that named it (Explore) now names `low`. A user agent file that still
38
+ says `agentTier: fast` is reported by the existing unknown-reference check when
39
+ settings and agents load, rather than failing at its first spawn.
40
+
41
+ `agentTiers.defaultTier` therefore has three states, and `/agents → Settings`
42
+ offers all three rather than rendering two of them as one word: a named tier,
43
+ `unset` (managed calls reach the shipped fallback), and `none` (recorded as
44
+ `noDefaultTier`, which withdraws the fallback so managed calls fail closed).
45
+ `setDefaultAgentTier` takes that choice as a tagged value, so a caller cannot
46
+ express "no default" without saying which of the two it means.
47
+
48
+ `resolveAgentTier` gained `requireTier` for that fallback, and the two things
49
+ pre-resolution callers need are both answered by the resolver rather than
50
+ restated beside it: `agentTierApplies()` for "will a tier own this spawn's
51
+ model?", and `selectAgentTier()` for the tier key itself, which the managed
52
+ path needs to label a tombstone and the lifecycle events before the runner has
53
+ resolved anything.
54
+
55
+ Because a tier now owns model resolution outright, the spawn paths stop
56
+ pre-resolving one — and with it they stopped producing the model name the agent
57
+ UI shows. The resolution callback carries that label back instead, so a tier
58
+ that pins a model still names it in the viewer and the agent list, and a profile
59
+ that inherits correctly shows none. The label rides beside the snapshot rather
60
+ than inside it: the snapshot is a durable policy record a managed tombstone
61
+ persists and revalidates, and a cosmetic string does not belong in it.
62
+
63
+ Workflow resume now keys each cached call on the policy for that call's own tier
64
+ rather than on a whole-catalogue fingerprint, so defining or editing an unrelated
65
+ tier no longer forces a full re-execution — but only for a call that names its
66
+ tier. A call that names none keeps the whole-catalogue key, because the host
67
+ resolves those as `call > agent frontmatter > defaultTier` and frontmatter is
68
+ not on this wire: an agent reaches a managed caller as a name. Keyed on
69
+ `defaultTier`, such a call would be wrong twice over — replaying stale work
70
+ after an edit to the tier its agent actually declares, and re-executing after an
71
+ edit to a default it never reached. `agentTierPolicyIdentity()` therefore
72
+ requires a tier rather than accepting `undefined` and folding the default in, so
73
+ the wrong call cannot be written. Journal schema v4 and managed tombstone schema
74
+ v2 are quarantined from older facts rather than migrated.
75
+
76
+ The tier catalogue belongs to the user, names included. The built-in workflows
77
+ and the ad-hoc script ship with this package, so a tier name they use that the
78
+ host does not define is dropped in favour of the host's default rather than
79
+ failing the run — they cannot assert which names exist on someone else's
80
+ machine. A script the user wrote still fails closed on an undefined tier, since
81
+ that is a typo in a catalogue they own. Shipped-ness travels with the script
82
+ rather than with the frame that called it, so a user script that reaches a
83
+ built-in through `workflow(name)` applies the built-in's rule to the built-in;
84
+ the authoring skill now says outright that `low`/`medium`/`high` are the shipped
85
+ profiles rather than names an authored script may assume.
86
+
87
+ The managed routing policy published on the wire reports the default a managed
88
+ call will actually get, fallback included, so a peer's replay identity cannot
89
+ disagree with the host's selection; its sorts are code-unit rather than
90
+ locale-dependent, so the same catalogue fingerprints identically everywhere. The
91
+ workflow peer re-pings per start and resume instead of pinning the catalogue it
92
+ saw at session start, and reports the host-selected tier back through an
93
+ `onTierResolved` callback rather than by mutating the dispatch options.
94
+
95
+ Fixes: a nested spawn no longer relabels an agent's frontmatter tier as
96
+ caller-requested, which had turned a `scopeModels` warning into a refusal and
97
+ misattributed the choice; the scheduler no longer freezes a frontmatter tier into
98
+ a job, so editing the agent file takes effect at fire time; retired managed spawn
99
+ keys are now bounded instead of growing for the life of a session; a
100
+ scope refusal for a tier whose profile inherits its model now names the model
101
+ that would have run instead of the literal `undefined`; and the retired
102
+ `workflow` key warns once per process rather than on every settings read and
103
+ every project write.
104
+
105
+ The tier-key predicate and its length bound now have a single definition, in
106
+ `pi-subagents-protocol`, which `pi-subagents` re-exports. The wire is the
107
+ narrower of the two gates — a key one accepted and the other rejected could
108
+ never reach a peer — so a second copy could only ever be a way for them to
109
+ disagree. That one definition now also governs `blockedProfiles`, which was
110
+ being validated by a helper that trims first: `" low"` became `"low"` there
111
+ while the same value in `profiles` or `defaultTier` was refused outright. All
112
+ three reject.
113
+
114
+ `/agents → Settings` no longer names the fallback tier in the `unset` row's
115
+ label. Which tier `unset` reaches depends on the catalogue, and a workspace that
116
+ edits or deletes the shipped `medium` profile leaves it reaching nothing — at
117
+ which point `unset` and `none` behave identically. The row's description asks
118
+ the resolver what `unset` would currently resolve to and says that, including
119
+ when the answer is "nothing".
120
+
121
+ `pi-workflows` also gained the session-lifecycle handling this routing work
122
+ needed to sit on. The engine has an explicit dispose lifecycle, so a protocol
123
+ probe or a start that is in flight when a session ends rejects with a disposed
124
+ error instead of resolving against an engine nobody owns. The tool, command and
125
+ widget surfaces register exactly once and resolve the *current* engine on each
126
+ use, rather than closing over the one that existed when they were registered —
127
+ a session replacement previously left them pointing at a disposed engine. Tool
128
+ and command handlers report failures as results rather than throwing out of the
129
+ host's dispatch, and the lifecycle handler accepts `subagents:created` and
130
+ `subagents:started` so a spawn's identity is recorded from the first event that
131
+ carries it rather than only at completion.
132
+
133
+ ### Patch Changes
134
+
135
+ - c1b1741: Durability and containment fixes found in a repository-wide scan.
136
+
137
+ `pi-gpt-fast` rewrote pi's own global `settings.json` with a plain
138
+ `writeFileSync`. That file is shared with pi and every other extension, and this
139
+ one is the only writer of it; a torn write would have left the user with no pi
140
+ configuration at all rather than just no gpt-fast setting. It now writes a temp
141
+ file and renames, matching every other settings writer in the repository.
142
+ `pi-goal` gets the same treatment for its cross-project goal state file, which
143
+ its own settings module already did.
144
+
145
+ `pi-ralph-wiggum` resolved `/ralph start <path>` straight against the session
146
+ cwd, so `../../notes.md` would create directories and a file outside the project
147
+ the user opened — silently, before the loop started. This extension drives long
148
+ unattended loops, so that command is as likely to come from a model as from a
149
+ person. Task-file paths are now required to stay inside the workspace, on the
150
+ command, on the `ralph_start` tool, and on the paths read back out of persisted
151
+ state, so a state file written earlier or edited by hand cannot pull a file in
152
+ from outside either.
153
+
154
+ The containment test canonicalizes both sides before comparing, because a purely
155
+ lexical one is defeated by a symlink: every segment of `linked/plan.md` reads as
156
+ inside the workspace when `linked` is a door out of it. `realpathSync` throws on
157
+ a path that does not exist yet — the ordinary case, since `start` is usually
158
+ creating the file — so it resolves the deepest existing ancestor and re-attaches
159
+ the not-yet-created suffix, which cannot itself be a link. The traversal test
160
+ matches a `..` segment rather than a `..` prefix; the prefix form also rejected
161
+ `..notes.md`, an ordinary filename sitting in the workspace.
162
+
163
+ Every remaining bare `catch {}` now says why the error is safe to drop, matching
164
+ the convention the rest of the repository already follows.
165
+ - Updated dependencies [c1b1741]
166
+ - @signalridge/pi-subagents-protocol@1.4.0
167
+
168
+ ## 1.8.1
169
+ ### Patch Changes
170
+
171
+ - 28c8aa1: Remove non-functional references to external product names from package descriptions, examples, and comments. Provider identifiers required for runtime compatibility remain unchanged.
172
+
3
173
  ## 1.8.0
4
174
  ### Minor Changes
5
175
 
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,23 +414,24 @@ 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`,
423
- `cheap`, `nightly` are equally valid keys.
422
+ Names are yours. The names below are only an example — `research`,
423
+ `cheap`, `nightly` are equally valid keys. Replace the illustrative provider/model
424
+ values with models available in your environment.
424
425
 
425
426
  ```json
426
427
  {
427
428
  "agentTiers": {
428
429
  "defaultTier": "medium",
429
430
  "profiles": {
430
- "small": { "description": "Fast, cheap exploration", "model": "deepseek/deepseek-v4-flash", "thinking": "max" },
431
- "medium": { "description": "Ordinary planning and review", "model": "openai-codex/gpt-5.6-luna", "thinking": "max" },
432
- "large": { "description": "Architecture and risky review", "model": "openai-codex/gpt-5.6-sol", "thinking": "xhigh" },
433
- "research": { "description": "Long-context research", "model": "kimi/k3", "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" }
434
435
  }
435
436
  }
436
437
  }
@@ -438,7 +439,56 @@ Names are yours. `small`/`medium`/`large` below are only an example — `researc
438
439
 
439
440
  A profile is all-or-nothing: both `model` and `thinking` are required, and either
440
441
  may be the literal `"inherit"` to keep the parent's. `description` is optional and
441
- 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 names a key from this same `agentTiers` catalogue.
447
+ There is no second workflow-tier vocabulary and no mapping layer: a workflow that
448
+ wants cheap work asks for the tier you defined for cheap work.
449
+
450
+ ```js
451
+ // in a workflow script
452
+ await agent("summarize this diff", { tier: "low" })
453
+ await agent("design the migration", { tier: "high" })
454
+ ```
455
+
456
+ The tier is resolved by the same `resolveAgentTier()` path an ordinary Agent
457
+ spawn uses — same precedence, same model lookup, same thinking clamping, same
458
+ availability checks, same immutable resolution snapshot. A tier the host does not
459
+ define is rejected before dispatch, naming the tiers it does define.
460
+
461
+ Model and thinking are deliberately absent from the managed request. A tier is
462
+ the only model policy a workflow can express, so there is no second selector that
463
+ could silently win or be silently ignored.
464
+
465
+ Fresh installs ship an effort ladder: `low`, `medium`, `high`. Every shipped
466
+ profile inherits its model, so a new machine gets a working vocabulary without
467
+ this package ever choosing a vendor for you. A managed call that names no tier
468
+ uses the agent's own tier, then `agentTiers.defaultTier`, and finally falls back
469
+ to `medium` so a workflow runs on an unconfigured machine.
470
+
471
+ `medium` inherits its model, so on an unconfigured machine that fallback runs on
472
+ the parent session's model. What it buys is a call with a *named* policy, a
473
+ durable resolution snapshot and a scope check — not cheaper work. If you want
474
+ managed work to run somewhere cheaper, set a `defaultTier` whose profile pins a
475
+ model.
476
+
477
+ That last fallback is scoped to managed calls. It is deliberately **not** the
478
+ catalogue's `defaultTier`: a shipped default that applied to every ordinary spawn
479
+ would silence [`defaultModel`](#defaultmodel) and pin a thinking level on machines
480
+ that configured neither.
481
+
482
+ So `Default tier` has three settings, not two, and the menu offers all three:
483
+
484
+ | Setting | Ordinary spawn | Managed workflow call |
485
+ | --- | --- | --- |
486
+ | a tier name | that tier | that tier |
487
+ | `unset` | `defaultModel`, then the parent session | the shipped `medium` |
488
+ | `none` | `defaultModel`, then the parent session | rejected |
489
+
490
+ `none` is a policy statement, recorded as `noDefaultTier`; `unset` is the absence
491
+ of one. Deleting the profiles has the same effect on managed calls as `none`.
442
492
 
443
493
  ### How the host discovers tiers
444
494
 
@@ -449,8 +499,8 @@ remember. It sees:
449
499
  ```
450
500
  Available agent tiers:
451
501
 
452
- - small: Fast, cheap exploration
453
- model: deepseek/deepseek-v4-flash
502
+ - low: Fast, cheap exploration
503
+ model: provider/fast-model
454
504
  thinking: max
455
505
  ...
456
506
  Default tier: medium
@@ -476,6 +526,11 @@ frontmatter are read only to warn that they are stale, and the built-in agents
476
526
  pin nothing either. With nothing configured at all, a subagent runs on the
477
527
  parent session's model.
478
528
 
529
+ A **managed workflow call** cannot take steps 4 and 5 — it has no parent session
530
+ to inherit from — so it gets one extra step between 3 and the end: the shipped
531
+ `medium` fallback. That step exists only for callers that would otherwise fail
532
+ closed, which is why it does not displace `defaultModel` for everyone else.
533
+
479
534
  ### `defaultModel`
480
535
 
481
536
  Steps 1–3 are a catalogue; step 4 is one line. Set it when the whole point is
@@ -548,22 +603,13 @@ level nobody chose for that pair. A project profile that fails validation blocks
548
603
  its global namesake rather than reviving it, and `defaultTier` is a simple
549
604
  project-over-global override.
550
605
 
551
- ### Not the same as `workflow.tiers`
552
-
553
- `pi-workflows` has its own tiers, and they stay fixed at `small | medium | large`.
554
- That vocabulary is part of the cross-package protocol: it lets a workflow
555
- definition be validated at parse time and stay portable between machines, neither
556
- of which survives arbitrary names. The two systems share no fields — a spawn
557
- records `agentTier`/`agentTierSnapshot` or `tier`/`tierSnapshot`, never one
558
- standing in for the other.
559
-
560
606
  ### Migrating from `model:`/`thinking:`
561
607
 
562
- Define the profiles once, then replace each agent's `model:`/`thinking:` with
563
- `tier: <name>`. Files that still carry the old fields load and run — the fields
564
- are ignored, with a warning naming the file — so the migration can be done one
565
- agent at a time. Until an agent names a tier it uses `defaultTier`, or the
566
- parent's model when none is set.
608
+ Define the Agent-tier profiles once, then replace each agent's `model:`/`thinking:`
609
+ with `tier: <name>`. Files that still carry the old fields load and run — the
610
+ fields are ignored, with a warning naming the file — so the migration can be done
611
+ one agent at a time. Until an agent names a tier it uses `agentTiers.defaultTier`,
612
+ then `defaultModel`, or the parent's model when none is set.
567
613
 
568
614
  Programmatic callers and the legacy RPC may still pass `model`/`thinking`
569
615
  directly. That is the escape hatch for code, not a way to configure an agent.
@@ -578,11 +624,11 @@ When on, each subagent spawn's effective model is validated against pi's own `en
578
624
 
579
625
  | Model source | Out-of-scope behavior |
580
626
  |---|---|
581
- | Caller-supplied via `Agent({ model: "..." })` | Hard error returned to the orchestrator, listing allowed models |
627
+ | Caller-supplied programmatic `model` (only when no Agent tier applies) | Hard error returned to the orchestrator, listing allowed models |
582
628
  | Pinned in agent frontmatter | Warning toast + the pinned model runs (frontmatter is authoritative) |
583
629
  | Parent-inherited (neither set) | Warning toast + parent's model runs |
584
630
 
585
- **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).
631
+ **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.
586
632
 
587
633
  **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.
588
634
 
@@ -599,24 +645,9 @@ Runtime tuning values set via `/agents` → Settings (max concurrency, default m
599
645
 
600
646
  **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).
601
647
 
602
- **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.
603
-
604
- ```json
605
- {
606
- "workflow": {
607
- "defaultTier": "medium",
608
- "tiers": {
609
- "small": { "model": "inherit", "thinking": "low" },
610
- "medium": { "model": "inherit", "thinking": "medium" },
611
- "large": { "model": "openai-codex/gpt-5.6-luna", "thinking": "max" }
612
- }
613
- }
614
- }
615
- ```
616
-
617
- 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.
648
+ The `workflow` settings key is **retired**. Managed `pi-workflows` calls name an `agentTiers` key directly, so there is no separate workflow routing table; a file that still has one is ignored with a warning naming the key. `agentTiers.defaultTier` replaces what `workflow.defaultTier` used to do. See [One catalogue, including for workflows](#one-catalogue-including-for-workflows).
618
649
 
619
- **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).
650
+ **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.
620
651
 
621
652
  **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.
622
653
 
@@ -673,7 +704,7 @@ Agent lifecycle events are emitted via `pi.events.emit()` so other extensions ca
673
704
  | `subagents:compacted` | Agent's session successfully compacted | `id`, `type`, `description`, `reason` (`"manual"` / `"threshold"` / `"overflow"`), `tokensBefore`, `compactionCount`, optional `owner` |
674
705
  | `subagents:scheduled` | Schedule lifecycle change | `{ type: "added" \| "removed" \| "updated" \| "fired" \| "error", … }` (job/agentId/error fields per type) |
675
706
  | `subagents:scheduler_ready` | Scheduler bound to session, enabled jobs armed | `sessionId`, `jobCount` |
676
- | `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`) |
707
+ | `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) |
677
708
  | `subagents:settings_loaded` | Persisted settings applied at extension init | `settings` (merged global + project) |
678
709
  | `subagents:settings_changed` | `/agents` → Settings mutation was applied | `settings`, `persisted` (`boolean` — `false` on write failure) |
679
710
 
@@ -699,20 +730,20 @@ pi.events.on("subagents:ready", () => {
699
730
 
700
731
  ### Ping
701
732
 
702
- Check if the subagents extension is loaded and get the protocol version:
733
+ Check if the subagents extension is loaded and get the protocol version and current routing-policy fingerprint:
703
734
 
704
735
  ```typescript
705
736
  const requestId = crypto.randomUUID();
706
737
  const unsub = pi.events.on(`subagents:rpc:ping:reply:${requestId}`, (reply) => {
707
738
  unsub();
708
- if (reply.success) console.log("Protocol version:", reply.data.version);
739
+ if (reply.success) console.log("Protocol version:", reply.data.version, "routing policy:", reply.data.routingPolicy.fingerprint);
709
740
  });
710
741
  pi.events.emit("subagents:rpc:ping", { requestId });
711
742
  ```
712
743
 
713
- ### Managed spawn (protocol v3)
744
+ ### Managed spawn (protocol v4)
714
745
 
715
- 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:
746
+ 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:
716
747
 
717
748
  ```json
718
749
  {
@@ -721,15 +752,14 @@ Workflow-owned orchestration uses the additive `subagents:rpc:spawn-managed` cha
721
752
  "type": "Explore",
722
753
  "prompt": "Find the relevant files",
723
754
  "description": "Find relevant files",
724
- "tier": "small",
725
- "model": "provider/model:medium",
755
+ "tier": "low",
726
756
  "excludeTools": ["workflow", "workflow_control"],
727
757
  "isolation": "worktree",
728
758
  "owner": { "extension": "pi-workflows", "runId": "run-id", "nodeId": "node-id", "attemptId": "run-id/node-id/attempt-1" }
729
759
  }
730
760
  ```
731
761
 
732
- 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.
762
+ 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.
733
763
 
734
764
  ### Spawn
735
765
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signalridge/pi-subagents",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
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
  },