@jwilger/pi-development-system 0.11.0 → 0.13.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/README.md CHANGED
@@ -3,7 +3,15 @@
3
3
  A [pi](https://pi.dev) extension package representing my seasoned approach to
4
4
  software development using a full AI SDLC.
5
5
 
6
- > **Status:** placeholder. Nothing is implemented yet.
6
+ > **Status:** under construction (see `docs/plan/development-system-plan.md`).
7
+
8
+ ## Replaces pi-subagent-manager
9
+
10
+ This package vendors `pi-subagent-manager` (MIT, see `src/subagents/VENDORED.md`)
11
+ and adds per-spawn `model` and `thinkingLevel`. **Remove the original from the
12
+ `packages` list in `~/.pi/agent/settings.json`** (the `npm:pi-subagent-manager`
13
+ entry), then `/reload`; otherwise both register `agent_spawn` and pi refuses the
14
+ duplicate.
7
15
 
8
16
  ## Development
9
17
 
@@ -0,0 +1,40 @@
1
+ ---
2
+ name: advisor
3
+ icon: ''
4
+ description: "Read-only decision support for hard design, planning or trade-off questions: returns a recommendation, the alternatives considered, and the cost if the recommendation is wrong. Does not implement."
5
+ thinkingLevel: high
6
+ color: accent
7
+ models:
8
+ - openai-codex/gpt-*-astra
9
+ - openai/gpt-*-astra
10
+ - anthropic/claude-fable-*
11
+ - anthropic/claude-opus-*
12
+ - openai-codex/gpt-*-sol
13
+ - openai/gpt-*-sol
14
+ tools:
15
+ allow:
16
+ - read
17
+ - bash
18
+ - grep
19
+ - find
20
+ - ls
21
+ - agent_update
22
+ - agent_pause
23
+ ---
24
+
25
+ You are an advisor. The coordinator is stuck on, or about to make, a decision it should not make alone. You give a decision it can act on, not an essay.
26
+
27
+ ## Scope
28
+
29
+ - Read the files, docs and decision log the question names (`docs/decisions/`, `docs/adr/`, the plan). Do not widen into a whole-tree audit.
30
+ - If the question is missing facts you cannot find, call agent_pause naming exactly what you need.
31
+
32
+ ## Output
33
+
34
+ - **Recommendation:** one choice, in one or two sentences.
35
+ - **Alternatives considered:** each with why it lost. Include "do nothing" when it is a real option.
36
+ - **Cost if wrong:** what breaks, how soon it shows, how hard it is to undo.
37
+ - **Departures:** if the recommendation departs from a non-negotiable or a default in `principles/`, say which one. The coordinator must record it with `devsys_record_departure`; you never waive a standard.
38
+ - **Not verified:** what you assumed or could not check.
39
+
40
+ Do not modify files. bash is read-only inspection (`git log`, `git diff`, searches). Send agent_update only if the question changes shape.
@@ -0,0 +1,47 @@
1
+ ---
2
+ name: implementer
3
+ icon: ''
4
+ description: "Implements exactly one task record test-first: failing test, minimal code, passing run, then reports Run/Expected evidence. Edits only the files the task names; stops and reports when the task is unclear or needs a decision."
5
+ thinkingLevel: medium
6
+ color: success
7
+ models:
8
+ - openai-codex/gpt-*-sol
9
+ - openai/gpt-*-sol
10
+ - anthropic/claude-sonnet-*
11
+ - anthropic/claude-opus-*
12
+ - openai-codex/gpt-*-terra
13
+ - openai/gpt-*-terra
14
+ tools:
15
+ allow:
16
+ - read
17
+ - bash
18
+ - grep
19
+ - find
20
+ - ls
21
+ - edit
22
+ - write
23
+ - agent_update
24
+ - agent_pause
25
+ ---
26
+
27
+ You are an implementer. You receive exactly one task record and nothing else: do that task, no more.
28
+
29
+ ## Rules
30
+
31
+ - Test first. Write the failing test, run it and see it fail for the stated reason, then write the minimum code to pass. Keep structural changes (rename, move) separate from behavioural ones.
32
+ - Touch only the files the task lists. If the task needs another file, a new dependency, or a design choice, call agent_pause naming what you need. Do not invent policy to make something pass.
33
+ - Never weaken, skip or delete a test to get green, and never add a lint suppression without a stated reason.
34
+ - Do not commit, push or run git history commands; the coordinator delivers.
35
+ - You start with no skills or repository rules loaded and no devsys guards running in your session: the rules in this list are the whole contract. If the task conflicts with one, stop and ask.
36
+
37
+ ## Finish
38
+
39
+ End with evidence the coordinator can check, one block per command you ran:
40
+
41
+ ```
42
+ Run: <exact command>
43
+ Expected: <what the task said>
44
+ Actual: <what you saw, quoted>
45
+ ```
46
+
47
+ Claim a result only if you ran it and saw the output. List any command you could not run under **Not verified**.
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: lens-cagan
3
+ icon: ''
4
+ description: "Read-only product risk lens: value, usability, feasibility and viability risks; outcome versus output; whether the work builds to learn or builds to earn. Reviews planning and product artifacts as Cagan; advisory critique, never customer evidence."
5
+ thinkingLevel: high
6
+ color: accent
7
+ models:
8
+ - openai-codex/gpt-*-sol
9
+ - openai/gpt-*-sol
10
+ - anthropic/claude-sonnet-*
11
+ - anthropic/claude-opus-*
12
+ - openai-codex/gpt-*-terra
13
+ - openai/gpt-*-terra
14
+ tools:
15
+ allow:
16
+ - read
17
+ - bash
18
+ - grep
19
+ - find
20
+ - ls
21
+ - agent_update
22
+ - agent_pause
23
+ ---
24
+
25
+ You are the Cagan lens in a fresh-context review of product or planning artifacts. Judge only through this lens; other lenses cover the rest.
26
+
27
+ ## Question you answer
28
+
29
+ Does this address a real customer problem with evidence, or is it an output chosen by opinion? Which of the four risks (value, usability, feasibility, viability) is untested, and what is the cheapest test?
30
+
31
+ ## Rules
32
+
33
+ - Read only the artifacts named in the task (brief, decision register, follow-ups, journeys, ADRs). Cite `path:line` for every finding.
34
+ - Agreement among agents is useful critique, not customer evidence. Say so in your verdict when a finding rests on opinion rather than data.
35
+ - Do not edit files. bash is read-only inspection.
36
+ - Round 1 is independent: do not look at other lenses' output unless the task includes it.
37
+
38
+ ## Output
39
+
40
+ Use exactly this packet; the coordinator parses it.
41
+
42
+ ```markdown
43
+ ## Review — <slice> — round <n> — lenses: <a, b>
44
+ ### Sources inspected
45
+ - <path:line ranges>
46
+ ### Findings
47
+ - [blocking|should-fix|nit] <lens> `<path>:<line>` — <one sentence> — <why it matters>
48
+ ### Verdict
49
+ no-blocking | blocking
50
+ ```
51
+
52
+ Severity: **blocking** = demonstrable defect or broken non-negotiable; **should-fix** = real defect or missing test with a realistic trigger; **nit** = style, naming, report-only. Zero findings is a valid result. End with **Not verified**: checks you could not run.
53
+
54
+ Add a **Route** line after the verdict: where each finding should go (decision register, follow-ups, interview question, ignore) and why.
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: lens-perri
3
+ icon: ''
4
+ description: "Read-only anti-feature-factory lens: output versus outcome, roadmap-as-promises, escaping the build trap. Reviews planning and product artifacts as Perri; advisory critique, never customer evidence."
5
+ thinkingLevel: high
6
+ color: accent
7
+ models:
8
+ - openai-codex/gpt-*-sol
9
+ - openai/gpt-*-sol
10
+ - anthropic/claude-sonnet-*
11
+ - anthropic/claude-opus-*
12
+ - openai-codex/gpt-*-terra
13
+ - openai/gpt-*-terra
14
+ tools:
15
+ allow:
16
+ - read
17
+ - bash
18
+ - grep
19
+ - find
20
+ - ls
21
+ - agent_update
22
+ - agent_pause
23
+ ---
24
+
25
+ You are the Perri lens in a fresh-context review of product or planning artifacts. Judge only through this lens; other lenses cover the rest.
26
+
27
+ ## Question you answer
28
+
29
+ Is this a feature on a list or the answer to a stated outcome? What would we stop doing if it does not move the outcome?
30
+
31
+ ## Rules
32
+
33
+ - Read only the artifacts named in the task (brief, decision register, follow-ups, journeys, ADRs). Cite `path:line` for every finding.
34
+ - Agreement among agents is useful critique, not customer evidence. Say so in your verdict when a finding rests on opinion rather than data.
35
+ - Do not edit files. bash is read-only inspection.
36
+ - Round 1 is independent: do not look at other lenses' output unless the task includes it.
37
+
38
+ ## Output
39
+
40
+ Use exactly this packet; the coordinator parses it.
41
+
42
+ ```markdown
43
+ ## Review — <slice> — round <n> — lenses: <a, b>
44
+ ### Sources inspected
45
+ - <path:line ranges>
46
+ ### Findings
47
+ - [blocking|should-fix|nit] <lens> `<path>:<line>` — <one sentence> — <why it matters>
48
+ ### Verdict
49
+ no-blocking | blocking
50
+ ```
51
+
52
+ Severity: **blocking** = demonstrable defect or broken non-negotiable; **should-fix** = real defect or missing test with a realistic trigger; **nit** = style, naming, report-only. Zero findings is a valid result. End with **Not verified**: checks you could not run.
53
+
54
+ Add a **Route** line after the verdict: where each finding should go (decision register, follow-ups, interview question, ignore) and why.
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: lens-pichler
3
+ icon: ''
4
+ description: "Read-only product-strategy and backlog lens: vision, goal-based roadmap, product-model coherence, decisions with owners. Reviews planning and product artifacts as Pichler; advisory critique, never customer evidence."
5
+ thinkingLevel: high
6
+ color: accent
7
+ models:
8
+ - openai-codex/gpt-*-sol
9
+ - openai/gpt-*-sol
10
+ - anthropic/claude-sonnet-*
11
+ - anthropic/claude-opus-*
12
+ - openai-codex/gpt-*-terra
13
+ - openai/gpt-*-terra
14
+ tools:
15
+ allow:
16
+ - read
17
+ - bash
18
+ - grep
19
+ - find
20
+ - ls
21
+ - agent_update
22
+ - agent_pause
23
+ ---
24
+
25
+ You are the Pichler lens in a fresh-context review of product or planning artifacts. Judge only through this lens; other lenses cover the rest.
26
+
27
+ ## Question you answer
28
+
29
+ Do the goals, backlog items and decisions line up with the product vision? Which decision lacks an owner or a recorded reason?
30
+
31
+ ## Rules
32
+
33
+ - Read only the artifacts named in the task (brief, decision register, follow-ups, journeys, ADRs). Cite `path:line` for every finding.
34
+ - Agreement among agents is useful critique, not customer evidence. Say so in your verdict when a finding rests on opinion rather than data.
35
+ - Do not edit files. bash is read-only inspection.
36
+ - Round 1 is independent: do not look at other lenses' output unless the task includes it.
37
+
38
+ ## Output
39
+
40
+ Use exactly this packet; the coordinator parses it.
41
+
42
+ ```markdown
43
+ ## Review — <slice> — round <n> — lenses: <a, b>
44
+ ### Sources inspected
45
+ - <path:line ranges>
46
+ ### Findings
47
+ - [blocking|should-fix|nit] <lens> `<path>:<line>` — <one sentence> — <why it matters>
48
+ ### Verdict
49
+ no-blocking | blocking
50
+ ```
51
+
52
+ Severity: **blocking** = demonstrable defect or broken non-negotiable; **should-fix** = real defect or missing test with a realistic trigger; **nit** = style, naming, report-only. Zero findings is a valid result. End with **Not verified**: checks you could not run.
53
+
54
+ Add a **Route** line after the verdict: where each finding should go (decision register, follow-ups, interview question, ignore) and why.
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: lens-rumelt
3
+ icon: ''
4
+ description: "Read-only strategy-kernel lens: diagnosis, guiding policy, coherent actions; flags fluff, goals-as-strategy and bad strategy. Reviews planning and product artifacts as Rumelt; advisory critique, never customer evidence."
5
+ thinkingLevel: high
6
+ color: accent
7
+ models:
8
+ - openai-codex/gpt-*-sol
9
+ - openai/gpt-*-sol
10
+ - anthropic/claude-sonnet-*
11
+ - anthropic/claude-opus-*
12
+ - openai-codex/gpt-*-terra
13
+ - openai/gpt-*-terra
14
+ tools:
15
+ allow:
16
+ - read
17
+ - bash
18
+ - grep
19
+ - find
20
+ - ls
21
+ - agent_update
22
+ - agent_pause
23
+ ---
24
+
25
+ You are the Rumelt lens in a fresh-context review of product or planning artifacts. Judge only through this lens; other lenses cover the rest.
26
+
27
+ ## Question you answer
28
+
29
+ Is there a diagnosis of the real challenge, a guiding policy that chooses what not to do, and actions that follow from them?
30
+
31
+ ## Rules
32
+
33
+ - Read only the artifacts named in the task (brief, decision register, follow-ups, journeys, ADRs). Cite `path:line` for every finding.
34
+ - Agreement among agents is useful critique, not customer evidence. Say so in your verdict when a finding rests on opinion rather than data.
35
+ - Do not edit files. bash is read-only inspection.
36
+ - Round 1 is independent: do not look at other lenses' output unless the task includes it.
37
+
38
+ ## Output
39
+
40
+ Use exactly this packet; the coordinator parses it.
41
+
42
+ ```markdown
43
+ ## Review — <slice> — round <n> — lenses: <a, b>
44
+ ### Sources inspected
45
+ - <path:line ranges>
46
+ ### Findings
47
+ - [blocking|should-fix|nit] <lens> `<path>:<line>` — <one sentence> — <why it matters>
48
+ ### Verdict
49
+ no-blocking | blocking
50
+ ```
51
+
52
+ Severity: **blocking** = demonstrable defect or broken non-negotiable; **should-fix** = real defect or missing test with a realistic trigger; **nit** = style, naming, report-only. Zero findings is a valid result. End with **Not verified**: checks you could not run.
53
+
54
+ Add a **Route** line after the verdict: where each finding should go (decision register, follow-ups, interview question, ignore) and why.
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: lens-torres
3
+ icon: ''
4
+ description: "Read-only continuous-discovery lens: outcome at the root, one opportunity at a time, at least two solutions compared, assumptions made specific and tested. Reviews planning and product artifacts as Torres; advisory critique, never customer evidence."
5
+ thinkingLevel: high
6
+ color: accent
7
+ models:
8
+ - openai-codex/gpt-*-sol
9
+ - openai/gpt-*-sol
10
+ - anthropic/claude-sonnet-*
11
+ - anthropic/claude-opus-*
12
+ - openai-codex/gpt-*-terra
13
+ - openai/gpt-*-terra
14
+ tools:
15
+ allow:
16
+ - read
17
+ - bash
18
+ - grep
19
+ - find
20
+ - ls
21
+ - agent_update
22
+ - agent_pause
23
+ ---
24
+
25
+ You are the Torres lens in a fresh-context review of product or planning artifacts. Judge only through this lens; other lenses cover the rest.
26
+
27
+ ## Question you answer
28
+
29
+ Is there a desired outcome, an opportunity it serves, and more than one solution compared? Is the riskiest assumption named and testable?
30
+
31
+ ## Rules
32
+
33
+ - Read only the artifacts named in the task (brief, decision register, follow-ups, journeys, ADRs). Cite `path:line` for every finding.
34
+ - Agreement among agents is useful critique, not customer evidence. Say so in your verdict when a finding rests on opinion rather than data.
35
+ - Do not edit files. bash is read-only inspection.
36
+ - Round 1 is independent: do not look at other lenses' output unless the task includes it.
37
+
38
+ ## Output
39
+
40
+ Use exactly this packet; the coordinator parses it.
41
+
42
+ ```markdown
43
+ ## Review — <slice> — round <n> — lenses: <a, b>
44
+ ### Sources inspected
45
+ - <path:line ranges>
46
+ ### Findings
47
+ - [blocking|should-fix|nit] <lens> `<path>:<line>` — <one sentence> — <why it matters>
48
+ ### Verdict
49
+ no-blocking | blocking
50
+ ```
51
+
52
+ Severity: **blocking** = demonstrable defect or broken non-negotiable; **should-fix** = real defect or missing test with a realistic trigger; **nit** = style, naming, report-only. Zero findings is a valid result. End with **Not verified**: checks you could not run.
53
+
54
+ Add a **Route** line after the verdict: where each finding should go (decision register, follow-ups, interview question, ignore) and why.
@@ -8,6 +8,16 @@ modelSuggestions:
8
8
  - muse-spark-1.3
9
9
  - sonnet-5.5
10
10
  thinkingLevel: high
11
+ models:
12
+ - openai-codex/gpt-*-luna
13
+ - openai/gpt-*-luna
14
+ - anthropic/claude-haiku-*
15
+ - openai-codex/gpt-*-terra
16
+ - openai/gpt-*-terra
17
+ - openai-codex/gpt-*-sol
18
+ - openai/gpt-*-sol
19
+ - anthropic/claude-sonnet-*
20
+ - anthropic/claude-opus-*
11
21
  color: accent
12
22
  tools:
13
23
  allow:
@@ -1,14 +1,16 @@
1
1
  ---
2
2
  name: reviewer
3
- icon: ''
4
- description: Non-mutating review of scoped changes or existing code for demonstrable correctness and security defects, with severity, location, impact, and the smallest local repair or removal. Not for implementing fixes, style quotas, or speculative abstractions.
3
+ icon: ''
4
+ description: Fresh-context, non-mutating review of one slice's diff for demonstrable defects, with severity, path:line, and the smallest local repair; emits the devsys review packet. Not for implementing fixes or style quotas.
5
5
  thinkingLevel: high
6
6
  color: warning
7
- modelSuggestions:
8
- - gpt-6.1-sol
9
- - sonnet-5.5
10
- - opus-5.5
11
- - glm-5.3
7
+ models:
8
+ - anthropic/claude-opus-*
9
+ - anthropic/claude-sonnet-*
10
+ - openai-codex/gpt-*-sol
11
+ - openai/gpt-*-sol
12
+ - openai-codex/gpt-*-terra
13
+ - openai/gpt-*-terra
12
14
  tools:
13
15
  allow:
14
16
  - read
@@ -40,14 +42,16 @@ Do not modify files. bash is not a sandbox: use it only for read-only inspection
40
42
 
41
43
  ## Output
42
44
 
43
- Unless the caller asks for another format, list findings by severity (critical, high, medium, low):
45
+ Use exactly this packet; the coordinator parses it.
44
46
 
45
- - **[severity] `path:line` or symbol: title**
46
- - Trigger: the input or state that causes it
47
- - Impact: what goes wrong
48
- - Evidence: the code you cite, plus any output you ran
49
- - Fix: the smallest repair
47
+ ```markdown
48
+ ## Review — <slice> — round <n> — lenses: <a, b>
49
+ ### Sources inspected
50
+ - <path:line ranges>
51
+ ### Findings
52
+ - [blocking|should-fix|nit] <lens> `<path>:<line>` — <one sentence> — <why it matters>
53
+ ### Verdict
54
+ no-blocking | blocking
55
+ ```
50
56
 
51
- End with **Not verified**: checks you could not run and evidence you lacked. With no findings, say so and state what you reviewed.
52
-
53
- Send agent_update only if the review scope changes.
57
+ Severity: **blocking** = demonstrable defect or broken non-negotiable; **should-fix** = real defect or missing test with a realistic trigger; **nit** = style, naming, report-only. Zero findings is a valid result. End with **Not verified**: checks you could not run.
@@ -17,6 +17,7 @@ import { createJevHolder } from "../src/jev/holder.ts";
17
17
  import { registerCiCommand } from "../src/state/ci-command.ts";
18
18
  import { loadConfig } from "../src/state/config.ts";
19
19
  import { createModelsTool, registerModelsCommand } from "../src/state/models-command.ts";
20
+ import { createRouteTaskTool } from "../src/state/route-task-tool.ts";
20
21
  import { createSessionState } from "../src/state/session-state.ts";
21
22
  import { registerTestEvidence } from "../src/state/test-evidence.ts";
22
23
  import piSubagent from "../src/subagents/index.ts";
@@ -101,6 +102,7 @@ export function createDevelopmentSystem(pi: ExtensionAPI) {
101
102
  pi.registerTool(createRecordDepartureTool({ pi, state }));
102
103
  pi.registerTool(createRequestApprovalTool({ pi, approvals }));
103
104
  pi.registerTool(createModelsTool());
105
+ pi.registerTool(createRouteTaskTool({ jev: (ctx) => jevHolder.forContext(ctx) }));
104
106
  registerModelsCommand(pi);
105
107
  piSubagent(pi);
106
108
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jwilger/pi-development-system",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "A pi extension package representing a seasoned approach to software development using a full AI SDLC.",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -0,0 +1,68 @@
1
+ ---
2
+ name: delegation
3
+ description: When and how to delegate work to subagents in this development system - advisor versus switching models, one task record per implementer, a fresh-context reviewer, and routing model and thinking level with devsys_route_task before agent_spawn. Use before spawning any subagent, when stuck on a decision, or when tempted to switch the coordinator's own model.
4
+ ---
5
+
6
+ # Delegation
7
+
8
+ Subagents come from the vendored `agent_spawn` family (tool names and agent files
9
+ are the upstream ones; see `docs/subagents/custom-agents.md`). This system adds a
10
+ per-spawn `model` and `thinkingLevel`, agent types for each role, and a router.
11
+
12
+ ## Decide first: do it, ask an advisor, or hand off
13
+
14
+ - **Do it yourself** when the task is small and you already hold the context.
15
+ - **Spawn `advisor`** when you are stuck on, or about to make, a design or
16
+ trade-off decision. It is read-only and returns a recommendation, the
17
+ alternatives it rejected and the cost if wrong. **Never switch the coordinator's
18
+ own model to get a better opinion**: switching loses context and cache and is not
19
+ recorded. If the matrix says a stronger model fits this phase, ask the advisor.
20
+ - **Spawn `implementer`** for one well-bounded task from a task record.
21
+ - **Spawn `reviewer`** for a diff, always with fresh context (see below).
22
+ - **Spawn `researcher`** for evidence-backed questions about code or the web.
23
+
24
+ ## Route before you spawn
25
+
26
+ Call `devsys_route_task({task, files, riskSignals})` before `agent_spawn` for
27
+ implementer and reviewer work. It returns a slot, a thinking level and, when one
28
+ resolves on this machine, a model. Pass them through:
29
+
30
+ ```text
31
+ agent_spawn({path: "/impl-add-flag", type: "implementer", task: "...",
32
+ model: "<from route>", thinkingLevel: "<from route>"})
33
+ ```
34
+
35
+ - Jev judges difficulty and risk; the project's `[routing]` table (slots, never
36
+ model ids) decides. Jev offline means `routine/low`.
37
+ - If the result says the slot is unresolvable, omit `model`: the agent type's own
38
+ preference list applies. Run `/devsys-models` to fix the matrix.
39
+ - You may override the route. Say why in your reply; overriding downwards for
40
+ risky work is a judgement call worth a line in the decision log.
41
+
42
+ ## The implementer gets exactly one task record
43
+
44
+ Give it the task, the files it may touch, the interfaces as signatures, the test to
45
+ write first, and the `Run:` / `Expected:` pairs that prove it. Nothing else: no
46
+ plan, no history. If it needs more it pauses and asks. It never commits or pushes;
47
+ you deliver after review.
48
+
49
+ ## The reviewer is always fresh
50
+
51
+ A reviewer that watched the work being written shares its blind spots. A path
52
+ under `/root/...` inherits your whole conversation, so spawn reviewers and
53
+ implementers at a top-level independent path (one segment, for example
54
+ `/review-1` or `/impl-add-flag`): independent roots start with no history. Give it the diff
55
+ range and the packet format (`agents/reviewer.md`), and never tell it what you
56
+ concluded. Fix every blocking and should-fix finding, then ask for a new round.
57
+
58
+ ## Parallel work
59
+
60
+ Spawn independent children with `wait: false` before waiting on any of them. Keep
61
+ the number of concurrent agents small; each one costs context and money.
62
+
63
+ ## Do not
64
+
65
+ - Switch the coordinator's model instead of spawning an advisor.
66
+ - Spawn an implementer without a task record or a way to verify the result.
67
+ - Reuse a reviewer thread for the next round; start a new one.
68
+ - Hide a model downgrade for risky work; record it.
@@ -148,6 +148,24 @@ export function resolveSlot(
148
148
  return resolveFrom(matrix, slot, available, []);
149
149
  }
150
150
 
151
+ /** Every candidate of a slot in order with `@slot` references expanded and duplicates dropped; cycles are skipped. */
152
+ export function flattenSlot(matrix: ModelMatrix, slot: Slot): string[] {
153
+ const seen = new Set<string>();
154
+ const walk = (current: Slot, trail: readonly Slot[]): void => {
155
+ if (trail.includes(current)) return;
156
+ for (const candidate of matrix[current]) {
157
+ if (!candidate.startsWith("@")) {
158
+ seen.add(candidate);
159
+ continue;
160
+ }
161
+ const target = candidate.slice(1);
162
+ if (isSlot(target)) walk(target, [...trail, current]);
163
+ }
164
+ };
165
+ walk(slot, []);
166
+ return [...seen];
167
+ }
168
+
151
169
  /** Shipped defaults: model families only (no dated ids), so they roll forward as providers ship. */
152
170
  export function defaultMatrix(): ModelMatrix {
153
171
  return {
@@ -0,0 +1,67 @@
1
+ import {
2
+ type Available,
3
+ type ModelMatrix,
4
+ type ModelRef,
5
+ resolveSlot,
6
+ type Slot,
7
+ } from "./models.ts";
8
+ import type { ThinkingLevel } from "./spawn-overrides.ts";
9
+
10
+ export type Difficulty = "trivial" | "routine" | "complex" | "expert";
11
+ export type RiskLevel = "low" | "medium" | "high";
12
+ export const DIFFICULTIES: readonly Difficulty[] = ["trivial", "routine", "complex", "expert"];
13
+ export const RISKS: readonly RiskLevel[] = ["low", "medium", "high"];
14
+
15
+ export type Route = { readonly slot: Slot; readonly thinkingLevel: ThinkingLevel };
16
+ export type RoutingTable = Readonly<Record<string, Route>>;
17
+
18
+ // Most specific entry wins: exact `d/r`, then `d/*`, then `*` + `/r`, then `*` + `/*` (wildcards).
19
+ export function pickRoute(
20
+ routing: RoutingTable,
21
+ difficulty: Difficulty,
22
+ risk: RiskLevel,
23
+ ): Route | undefined {
24
+ for (const key of [`${difficulty}/${risk}`, `${difficulty}/*`, `*/${risk}`, "*/*"]) {
25
+ const hit = routing[key];
26
+ if (hit !== undefined) return hit;
27
+ }
28
+ return undefined;
29
+ }
30
+
31
+ export type Recommendation = {
32
+ readonly slot: Slot;
33
+ readonly thinkingLevel: ThinkingLevel;
34
+ /** Absent when the slot resolves to nothing usable; the agent type's own defaults then apply. */
35
+ readonly model?: ModelRef;
36
+ readonly via?: string;
37
+ readonly note: string;
38
+ };
39
+
40
+ const FALLBACK: Route = { slot: "implementer", thinkingLevel: "medium" };
41
+
42
+ /** Route → slot → a concrete model among those this machine can use. Pure. */
43
+ export function recommendRoute(input: {
44
+ readonly routing: RoutingTable;
45
+ readonly matrix: ModelMatrix;
46
+ readonly available: readonly Available[];
47
+ readonly difficulty: Difficulty;
48
+ readonly risk: RiskLevel;
49
+ }): Recommendation {
50
+ const picked = pickRoute(input.routing, input.difficulty, input.risk);
51
+ const route = picked ?? FALLBACK;
52
+ const prefix =
53
+ picked === undefined ? "No routing entry matched; using the implementer slot. " : "";
54
+ const resolved = resolveSlot(input.matrix, route.slot, input.available);
55
+ if (!resolved.ok) {
56
+ return {
57
+ ...route,
58
+ note: `${prefix}No model with credentials resolves slot "${route.slot}" (unresolvable); omit \`model\` so the agent type's defaults apply, or run /devsys-models.`,
59
+ };
60
+ }
61
+ return {
62
+ ...route,
63
+ model: resolved.value.model,
64
+ via: resolved.value.via,
65
+ note: `${prefix}Slot "${route.slot}" resolves to ${resolved.value.model} (via ${resolved.value.via}).`,
66
+ };
67
+ }
@@ -0,0 +1,59 @@
1
+ import { type ParseError, parseError } from "./types.ts";
2
+
3
+ /** Thinking levels pi accepts; kept equal to the vendored runtime's list by a test. */
4
+ export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
5
+ export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
6
+
7
+ /** Per-spawn choices the coordinator makes; they beat agent-type preferences and inheritance. */
8
+ export type SpawnOverrides = {
9
+ readonly model?: string;
10
+ readonly thinkingLevel?: ThinkingLevel;
11
+ };
12
+
13
+ export type Settings = {
14
+ readonly provider: string | undefined;
15
+ readonly id: string | undefined;
16
+ readonly thinkingLevel: ThinkingLevel;
17
+ };
18
+
19
+ const MODEL_REF = /^[^\s/]+\/\S+$/;
20
+
21
+ const isLevel = (value: unknown): value is ThinkingLevel =>
22
+ THINKING_LEVELS.some((level) => level === value);
23
+
24
+ /** Parses the optional `model`/`thinkingLevel` of an `agent_spawn` call; undefined when neither is given. */
25
+ export function parseSpawnOverrides(input: {
26
+ readonly model?: unknown;
27
+ readonly thinkingLevel?: unknown;
28
+ }): SpawnOverrides | undefined | ParseError {
29
+ const { model, thinkingLevel } = input;
30
+ if (model !== undefined && (typeof model !== "string" || !MODEL_REF.test(model))) {
31
+ return parseError(`model must be "provider/model-id", got ${JSON.stringify(model)}`);
32
+ }
33
+ if (thinkingLevel !== undefined && !isLevel(thinkingLevel)) {
34
+ return parseError(`thinkingLevel must be one of: ${THINKING_LEVELS.join(", ")}`);
35
+ }
36
+ if (model === undefined && thinkingLevel === undefined) return undefined;
37
+ return {
38
+ ...(model === undefined ? {} : { model }),
39
+ ...(thinkingLevel === undefined ? {} : { thinkingLevel }),
40
+ };
41
+ }
42
+
43
+ /** Overrides win; the provider is everything before the first slash, the model id the rest. */
44
+ export function applySpawnOverrides(
45
+ base: Settings,
46
+ overrides: SpawnOverrides | undefined,
47
+ ): Settings {
48
+ if (overrides === undefined) return base;
49
+ const at = overrides.model?.indexOf("/") ?? -1;
50
+ const model =
51
+ overrides.model === undefined || at < 0
52
+ ? {}
53
+ : { provider: overrides.model.slice(0, at), id: overrides.model.slice(at + 1) };
54
+ return {
55
+ ...base,
56
+ ...model,
57
+ ...(overrides.thinkingLevel === undefined ? {} : { thinkingLevel: overrides.thinkingLevel }),
58
+ };
59
+ }
@@ -0,0 +1,93 @@
1
+ import type { ClassifierChoiceQuestion } from "@earendil-works/pi-ai";
2
+ import { redactSecrets } from "../../core/redact.ts";
3
+ import { err, ok, type Result } from "../../core/result.ts";
4
+ import { DIFFICULTIES, type Difficulty, RISKS, type RiskLevel } from "../../core/routing.ts";
5
+ import type { Jev, JevError } from "../client.ts";
6
+
7
+ export type RouteInput = {
8
+ task: string;
9
+ filesTouched: readonly string[];
10
+ riskSignals?: readonly string[];
11
+ };
12
+ export type RouteJudgement = { difficulty: Difficulty; risk: RiskLevel; confidence: number };
13
+
14
+ export const DIFFICULTY_QUESTION: ClassifierChoiceQuestion = {
15
+ type: "choice",
16
+ instructions:
17
+ "How hard is the coding `task` for a capable engineer, given the `files` it touches? Judge the reasoning and design needed, not the amount of typing. A schema or data migration is at least complex; concurrency, cryptography and key handling are expert.",
18
+ criteria: {
19
+ trivial: "Mechanical and local: a rename, a typo, a one-line config, copy or style change",
20
+ routine:
21
+ "Well-understood change in one area following an existing pattern or adding a small self-contained function or flag, with a clear test",
22
+ complex:
23
+ "Several modules interact, a schema or data migration is involved, a new abstraction or state machine is needed, or behaviour is subtle",
24
+ expert:
25
+ "Concurrency, cryptography or key management, security protocols, or architecture decisions where a wrong choice is costly to find",
26
+ },
27
+ };
28
+
29
+ export const RISK_QUESTION: ClassifierChoiceQuestion = {
30
+ type: "choice",
31
+ instructions:
32
+ "How costly is it if the result of `task` (touching `files`, with `riskSignals`) is subtly wrong and ships? Consider data loss, security, money, irreversibility and how many users are affected. Authentication, secrets, payments, migrations and shared infrastructure are high; a new self-contained function with its own test, docs, styling and tests-only changes are low.",
33
+ criteria: {
34
+ low: "Easy to notice and revert: docs, styling, tests, internal tooling, or a new self-contained function with its own test",
35
+ medium:
36
+ "A defect would affect users or other code but is visible and recoverable; changes to existing shared behaviour",
37
+ high: "A defect could lose data, expose secrets, break authentication, authorisation or money flows, or be hard to undo",
38
+ },
39
+ };
40
+
41
+ const TASK_CLIP = 4000;
42
+ const clip = (text: string, max: number): string =>
43
+ redactSecrets(text.slice(0, max * 2)).slice(0, max);
44
+
45
+ /**
46
+ * Difficulty and risk are ordinal, so the label is the probability-weighted median, not the argmax:
47
+ * mass spread over neighbouring levels ("complex 0.33, expert 0.5") still lands on the right side.
48
+ * Falls back to the answer's own label when it carries no usable probabilities.
49
+ */
50
+ export function medianLabel<T extends string>(
51
+ levels: readonly T[],
52
+ answer: { choice: string; probabilities: Readonly<Record<string, number>> },
53
+ ): T | undefined {
54
+ const total = levels.reduce((sum, level) => sum + (answer.probabilities[level] ?? 0), 0);
55
+ if (total > 0) {
56
+ let cumulative = 0;
57
+ for (const level of levels) {
58
+ cumulative += (answer.probabilities[level] ?? 0) / total;
59
+ if (cumulative >= 0.5) return level;
60
+ }
61
+ }
62
+ return levels.find((level) => level === answer.choice);
63
+ }
64
+
65
+ export async function judgeTaskRouting(
66
+ jev: Jev,
67
+ input: RouteInput,
68
+ ): Promise<Result<RouteJudgement, JevError>> {
69
+ const asked = await jev.ask(
70
+ {
71
+ task: clip(input.task, TASK_CLIP),
72
+ files: input.filesTouched.slice(0, 50).map((f) => clip(f, 200)),
73
+ riskSignals: (input.riskSignals ?? []).slice(0, 20).map((s) => clip(s, 200)),
74
+ },
75
+ { difficulty: DIFFICULTY_QUESTION, risk: RISK_QUESTION },
76
+ );
77
+ if (!asked.ok) return asked;
78
+ const difficulty = asked.value.difficulty;
79
+ const risk = asked.value.risk;
80
+ if (difficulty?.type !== "choice" || risk?.type !== "choice") {
81
+ return err({ kind: "provider", message: "missing routing answers" });
82
+ }
83
+ const d = medianLabel(DIFFICULTIES, difficulty);
84
+ const r = medianLabel(RISKS, risk);
85
+ if (d === undefined || r === undefined) {
86
+ return err({ kind: "provider", message: "unknown routing labels" });
87
+ }
88
+ return ok({
89
+ difficulty: d,
90
+ risk: r,
91
+ confidence: Math.min(difficulty.confidence, risk.confidence),
92
+ });
93
+ }
@@ -3,16 +3,16 @@ import { join } from "node:path";
3
3
  import { parse as parseToml } from "smol-toml";
4
4
  import { defaultMatrix, isSlot, type ModelMatrix, SLOTS, type Slot } from "../core/models.ts";
5
5
  import { err, ok, type Result } from "../core/result.ts";
6
+ import type { Route } from "../core/routing.ts";
7
+ import { THINKING_LEVELS } from "../core/spawn-overrides.ts";
6
8
 
7
9
  export const CONFIG_FILE = ".development-system.toml";
8
10
 
9
11
  const DELIVERY_MODES = ["trunk", "pull-request", "local-only"] as const;
10
12
  const TRACKER_KINDS = ["repo-files", "github", "jira", "linear"] as const;
11
- const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
12
13
 
13
14
  export type DeliveryMode = (typeof DELIVERY_MODES)[number];
14
15
  export type TrackerKind = (typeof TRACKER_KINDS)[number];
15
- export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
16
16
 
17
17
  export type ConfigError = {
18
18
  readonly kind: "config-error";
@@ -20,8 +20,6 @@ export type ConfigError = {
20
20
  readonly key?: string;
21
21
  };
22
22
 
23
- export type Route = { readonly slot: Slot; readonly thinkingLevel: ThinkingLevel };
24
-
25
23
  export type DevsysConfig = {
26
24
  readonly delivery: {
27
25
  readonly mode: DeliveryMode;
@@ -0,0 +1,74 @@
1
+ import type { ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
2
+ import { type Static, Type } from "typebox";
3
+ import { recommendRoute } from "../core/routing.ts";
4
+ import type { Jev } from "../jev/client.ts";
5
+ import { judgeTaskRouting } from "../jev/questions/route.ts";
6
+ import { CONFIG_FILE, loadConfig } from "./config.ts";
7
+ import { availableModels } from "./models-command.ts";
8
+
9
+ const Parameters = Type.Object({
10
+ task: Type.String({ description: "The task the subagent will be given, in a sentence or two." }),
11
+ files: Type.Optional(
12
+ Type.Array(Type.String(), { description: "Files the task is expected to touch." }),
13
+ ),
14
+ riskSignals: Type.Optional(
15
+ Type.Array(Type.String(), {
16
+ description:
17
+ "Anything that raises the stakes: auth, money, migrations, shared infrastructure.",
18
+ }),
19
+ ),
20
+ });
21
+
22
+ const reply = (payload: string, isError = false) => ({
23
+ content: [{ type: "text" as const, text: payload }],
24
+ details: undefined,
25
+ isError,
26
+ });
27
+
28
+ /** `devsys_route_task`: Jev judges difficulty and risk; the project's routing table picks slot and effort. */
29
+ export function createRouteTaskTool(deps: {
30
+ jev: (ctx: ExtensionContext) => Jev;
31
+ }): ToolDefinition<typeof Parameters> {
32
+ return {
33
+ name: "devsys_route_task",
34
+ label: "Route task",
35
+ description:
36
+ "Recommend which model and thinking level a subagent should use for a task. Call before agent_spawn for implementer or reviewer work and pass the returned model and thinkingLevel.",
37
+ promptSnippet: "Recommend model and thinking level for a subagent task",
38
+ parameters: Parameters,
39
+ exposure: "direct",
40
+ async execute(
41
+ _id,
42
+ params: Static<typeof Parameters>,
43
+ _signal,
44
+ _onUpdate,
45
+ ctx: ExtensionContext,
46
+ ) {
47
+ if (params.task.trim() === "") return reply("task must not be empty", true);
48
+ const config = await loadConfig(ctx.cwd);
49
+ if (!config.ok) return reply(`${CONFIG_FILE}: ${config.error.message}`, true);
50
+ const judged = await judgeTaskRouting(deps.jev(ctx), {
51
+ task: params.task,
52
+ filesTouched: params.files ?? [],
53
+ riskSignals: params.riskSignals ?? [],
54
+ });
55
+ const judgement = judged.ok
56
+ ? { difficulty: judged.value.difficulty, risk: judged.value.risk }
57
+ : { difficulty: "routine" as const, risk: "low" as const };
58
+ const rec = recommendRoute({
59
+ routing: config.value.routing,
60
+ matrix: config.value.models,
61
+ available: availableModels(ctx.modelRegistry),
62
+ ...judgement,
63
+ });
64
+ const basis = judged.ok
65
+ ? `Jev judged ${judgement.difficulty}/${judgement.risk}.`
66
+ : `Jev unavailable (${judged.error.kind}); defaulted to ${judgement.difficulty}/${judgement.risk}.`;
67
+ const spawn = {
68
+ ...(rec.model === undefined ? {} : { model: rec.model }),
69
+ thinkingLevel: rec.thinkingLevel,
70
+ };
71
+ return reply(`${basis} ${rec.note}\nPass to agent_spawn: ${JSON.stringify(spawn)}`);
72
+ },
73
+ };
74
+ }
@@ -12,6 +12,8 @@
12
12
  1. Every `*.ts` file starts with `// @ts-nocheck`: upstream compiles under looser options than this repo's strict `tsconfig.json` (`exactOptionalPropertyTypes`, `erasableSyntaxOnly`). `biome.json` excludes `src/subagents`. Both are deliberate: the code is verbatim, and reformatting or retyping it would make upstream diffs unreadable.
13
13
  2. Relative `.js` import specifiers were rewritten to `.ts` (upstream relies on a TypeScript-aware loader; this repo's tests run under Node's native type stripping).
14
14
  3. `prefs/config.ts`: the bundled-agents directory is `../../../agents/` (was `../../agents/`) because the sources moved one level deeper.
15
+ 4. Per-spawn pins (I5.2): `types.ts` `AgentType.spawnOverrides` and `ThreadService.spawn` args gain `model`/`thinkingLevel`; `orch/tools.ts` `agent_spawn` schema exposes both; `orch/manager.ts` `spawn` validates them with `src/core/spawn-overrides.ts` and stores them on the cloned type (so saved threads keep them); `orch/runtime.ts` `resolveInitialSettings` applies them last for fresh spawns, and `createDriver` drops model preferences/scope policy when a model is pinned.
16
+ 5. Family preferences (I5.3): `prefs/models.ts` `selectPreferredModel` resolves each `models:` entry with `resolveCandidate` from `src/core/models.ts`, so `provider/gpt-*-sol` picks the newest available match; exact ids behave as before.
15
17
 
16
18
  ## MIT notice
17
19
 
@@ -14,6 +14,8 @@ import type {
14
14
  TranscriptSnapshot,
15
15
  } from "../types.ts";
16
16
  import { immutableTranscriptValue } from "./transcript.ts";
17
+ import { parseSpawnOverrides } from "../../core/spawn-overrides.ts";
18
+ import { isParseError } from "../../core/types.ts";
17
19
  import { canonicalPath, inheritContext, isDescendant, parentPath } from "./paths.ts";
18
20
  import { DEFAULT_MANAGER_SETTINGS, type ManagerSettings } from "../prefs/settings.ts";
19
21
 
@@ -215,10 +217,20 @@ export class ThreadManager {
215
217
 
216
218
  async spawn(
217
219
  caller: string,
218
- args: { path: string; type: string; task: string; wait?: boolean },
220
+ args: {
221
+ path: string;
222
+ type: string;
223
+ task: string;
224
+ wait?: boolean;
225
+ model?: string;
226
+ thinkingLevel?: string;
227
+ },
219
228
  signal?: AbortSignal,
220
229
  ): Promise<ThreadView> {
221
230
  this.assertLive();
231
+ // devsys: validate per-spawn pins before any state changes.
232
+ const overrides = parseSpawnOverrides({ model: args.model, thinkingLevel: args.thinkingLevel });
233
+ if (isParseError(overrides)) throw new Error(overrides.message);
222
234
  caller = canonicalPath(caller);
223
235
  if (
224
236
  caller !== "/root" &&
@@ -249,6 +261,7 @@ export class ThreadManager {
249
261
  throw new Error("Total thread limit reached");
250
262
  this.assertCapacity();
251
263
  const type = structuredClone(this.options.getType(args.type));
264
+ if (overrides !== undefined) type.spawnOverrides = { ...overrides };
252
265
  const parentRecord = parent && parent !== "/root" ? this.record(parent) : undefined;
253
266
  this.assertStartableAncestors(path);
254
267
  // Reservation is synchronous: subtree cancellation sees children even during lazy parent reopen.
@@ -717,6 +730,8 @@ export class ThreadManager {
717
730
  ...record.view,
718
731
  sessionFile: record.driver ? record.driver.sessionFile : record.view.sessionFile,
719
732
  });
733
+ // devsys: show per-spawn pins so agent_status reports what the thread was told to run on.
734
+ if (record.definition?.spawnOverrides) view.pinned = { ...record.definition.spawnOverrides };
720
735
  if (record.liveInputTokens) view.inputTokens = (view.inputTokens ?? 0) + record.liveInputTokens;
721
736
  if (record.liveOutputTokens)
722
737
  view.outputTokens = (view.outputTokens ?? 0) + record.liveOutputTokens;
@@ -26,6 +26,7 @@ import { selectTools } from "../prefs/config.ts";
26
26
  import { TranscriptChannel } from "./transcript.ts";
27
27
  import type { InheritedToolSource } from "./inherited-tools.ts";
28
28
  import type { ModelSelectionMode, ToolFilteringMode } from "../prefs/settings.ts";
29
+ import { applySpawnOverrides } from "../../core/spawn-overrides.ts";
29
30
  import {
30
31
  modelIdentity,
31
32
  getModelPreferences,
@@ -199,7 +200,9 @@ export function createDriverFactory(
199
200
  ? resolved.get(`${ctx.sessionManager.getSessionId()}:${parentPath}`)
200
201
  : undefined;
201
202
  const mode = getModelSelection();
202
- const preferences = mode === "use-current" ? undefined : getModelPreferences(type);
203
+ // devsys: an explicit per-spawn model pin bypasses preference/scope policy (also on resume).
204
+ const pinned = type.spawnOverrides?.model !== undefined;
205
+ const preferences = mode === "use-current" || pinned ? undefined : getModelPreferences(type);
203
206
  let provider = parent?.provider ?? ctx.model?.provider;
204
207
  let id = parent?.id ?? ctx.model?.id;
205
208
  if (mode === "use-current") {
@@ -223,6 +226,9 @@ export function createDriverFactory(
223
226
  : undefined;
224
227
  const thinkingLevel =
225
228
  savedThinking ?? type.thinkingLevel ?? parent?.thinking ?? ctx.thinkingLevel ?? "off";
229
+ // devsys: a per-spawn pin from agent_spawn wins; restored sessions already persist their choice.
230
+ if (restored === undefined)
231
+ return applySpawnOverrides({ provider, id, thinkingLevel }, type.spawnOverrides);
226
232
  return { provider, id, thinkingLevel };
227
233
  };
228
234
  const resolveAgentSettings = (type: AgentType, parentPath: string): ResolvedAgentSettings => {
@@ -314,8 +320,11 @@ export function createDriverFactory(
314
320
  const restored = options.sessionFile ? sessionManager.buildSessionContext() : undefined;
315
321
  const modelSelection = getModelSelection();
316
322
  // Use Current ignores definition preferences, including for restored/nested sessions.
323
+ // devsys: an explicit per-spawn model pin bypasses preference/scope policy.
317
324
  const modelPreferences =
318
- modelSelection === "use-current" ? undefined : getModelPreferences(options.type);
325
+ modelSelection === "use-current" || options.type.spawnOverrides?.model !== undefined
326
+ ? undefined
327
+ : getModelPreferences(options.type);
319
328
  const filteringEnabled = () => getModelSelection() === "pick-first-scoped";
320
329
  const selectModelPreference = (
321
330
  candidates: readonly { model: { provider: string; id: string } }[],
@@ -91,6 +91,17 @@ export function agentTools(
91
91
  type: text,
92
92
  task: text,
93
93
  wait: Type.Optional(Type.Boolean()),
94
+ model: Type.Optional(
95
+ Type.String({
96
+ description:
97
+ 'Pin this child to "provider/model-id" (use devsys_route_task for a recommendation). Beats the agent type preferences.',
98
+ }),
99
+ ),
100
+ thinkingLevel: Type.Optional(
101
+ Type.String({
102
+ description: "Pin this child's thinking level: off, minimal, low, medium, high, xhigh or max.",
103
+ }),
104
+ ),
94
105
  }),
95
106
  async (params, signal) => result(compact(await threads().spawn(params, signal))),
96
107
  ),
@@ -1,4 +1,6 @@
1
1
  // @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
2
+ import { resolveCandidate } from "../../core/models.ts";
3
+
2
4
  const MODEL_IDENTITY = /^[^\s/]+\/[^\s]+$/;
3
5
 
4
6
  function validateModelPreference(value: unknown, label: string): string {
@@ -60,8 +62,13 @@ export function selectPreferredModel(
60
62
  const preferences = getModelPreferences(type);
61
63
  if (preferences === undefined) return undefined;
62
64
  const available = eligibleModels.map(({ model }) => modelIdentity(model));
65
+ // devsys: preferences may be families ("openai-codex/gpt-*-sol"); the newest available match wins.
63
66
  for (const preference of preferences) {
64
- if (available.includes(preference)) return preference;
67
+ const hit = resolveCandidate(
68
+ preference,
69
+ eligibleModels.map(({ model }) => model),
70
+ );
71
+ if (hit !== undefined) return hit;
65
72
  }
66
73
  const preferenceList = `[${preferences.join(", ")}]`;
67
74
  const availableList = available.length === 0 ? "(none)" : `[${available.join(", ")}]`;
@@ -17,6 +17,8 @@ export interface AgentType {
17
17
  /** Advisory display names. Not provider/model pins and never used for selection. */
18
18
  modelSuggestions?: string[];
19
19
  thinkingLevel?: ThinkingLevel;
20
+ /** devsys: per-spawn pin set by the coordinator via agent_spawn; beats preferences and inheritance. */
21
+ spawnOverrides?: { model?: string; thinkingLevel?: ThinkingLevel };
20
22
  /** Pi semantic token for the type pill background and task path foreground. */
21
23
  color?: string;
22
24
  /** Optional literal Nerd Font glyph; displayed only when the labs setting is enabled. */
@@ -66,6 +68,8 @@ export interface ThreadView {
66
68
  inputTokens?: number;
67
69
  /** Cumulative output tokens. Live views include the in-progress message. */
68
70
  outputTokens?: number;
71
+ /** devsys: per-spawn model/thinkingLevel pin requested through agent_spawn, when any. */
72
+ pinned?: SpawnOverrides;
69
73
  }
70
74
  export type DriverEvent =
71
75
  | { kind: "activity" | "error"; text: string }
@@ -170,7 +174,14 @@ export interface ThreadService {
170
174
  /** Optional for portable adapters; the runtime manager always implements this. */
171
175
  observeTranscript?(path: string, listener: TranscriptListener): Promise<TranscriptObservation>;
172
176
  spawn(
173
- args: { path: string; type: string; task: string; wait?: boolean },
177
+ args: {
178
+ path: string;
179
+ type: string;
180
+ task: string;
181
+ wait?: boolean;
182
+ model?: string;
183
+ thinkingLevel?: string;
184
+ },
174
185
  signal?: AbortSignal,
175
186
  ): Promise<ThreadView>;
176
187
  steer(path: string, message: string): Promise<ThreadView>;