@jwilger/pi-development-system 0.11.0 → 0.12.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 +9 -1
- package/agents/advisor.md +40 -0
- package/agents/implementer.md +47 -0
- package/agents/lens-cagan.md +54 -0
- package/agents/lens-perri.md +54 -0
- package/agents/lens-pichler.md +54 -0
- package/agents/lens-rumelt.md +54 -0
- package/agents/lens-torres.md +54 -0
- package/agents/researcher.md +10 -0
- package/agents/reviewer.md +20 -16
- package/extensions/development-system.ts +2 -0
- package/package.json +1 -1
- package/skills/delegation/SKILL.md +66 -0
- package/src/core/models.ts +18 -0
- package/src/core/routing.ts +67 -0
- package/src/core/spawn-overrides.ts +59 -0
- package/src/jev/questions/route.ts +93 -0
- package/src/state/config.ts +2 -4
- package/src/state/route-task-tool.ts +74 -0
- package/src/subagents/VENDORED.md +2 -0
- package/src/subagents/orch/manager.ts +14 -1
- package/src/subagents/orch/runtime.ts +8 -1
- package/src/subagents/orch/tools.ts +11 -0
- package/src/subagents/prefs/models.ts +8 -1
- package/src/subagents/types.ts +10 -1
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:**
|
|
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
|
+
- Follow the repo's skills and `principles/NON-NEGOTIABLES.md`; they are loaded in your context.
|
|
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.
|
package/agents/researcher.md
CHANGED
|
@@ -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:
|
package/agents/reviewer.md
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: reviewer
|
|
3
|
-
icon: '
|
|
4
|
-
description:
|
|
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
|
-
|
|
8
|
-
-
|
|
9
|
-
- sonnet
|
|
10
|
-
-
|
|
11
|
-
-
|
|
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
|
-
|
|
45
|
+
Use exactly this packet; the coordinator parses it.
|
|
44
46
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
@@ -0,0 +1,66 @@
|
|
|
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: "/root/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. Spawn it
|
|
52
|
+
with `/root/<name>` paths that do not inherit your reasoning, give it the diff
|
|
53
|
+
range and the packet format (`agents/reviewer.md`), and never tell it what you
|
|
54
|
+
concluded. Fix every blocking and should-fix finding, then ask for a new round.
|
|
55
|
+
|
|
56
|
+
## Parallel work
|
|
57
|
+
|
|
58
|
+
Spawn independent children with `wait: false` before waiting on any of them. Keep
|
|
59
|
+
the number of concurrent agents small; each one costs context and money.
|
|
60
|
+
|
|
61
|
+
## Do not
|
|
62
|
+
|
|
63
|
+
- Switch the coordinator's model instead of spawning an advisor.
|
|
64
|
+
- Spawn an implementer without a task record or a way to verify the result.
|
|
65
|
+
- Reuse a reviewer thread for the next round; start a new one.
|
|
66
|
+
- Hide a model downgrade for risky work; record it.
|
package/src/core/models.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/state/config.ts
CHANGED
|
@@ -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: {
|
|
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.
|
|
@@ -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,
|
|
@@ -223,6 +224,9 @@ export function createDriverFactory(
|
|
|
223
224
|
: undefined;
|
|
224
225
|
const thinkingLevel =
|
|
225
226
|
savedThinking ?? type.thinkingLevel ?? parent?.thinking ?? ctx.thinkingLevel ?? "off";
|
|
227
|
+
// devsys: a per-spawn pin from agent_spawn wins; restored sessions already persist their choice.
|
|
228
|
+
if (restored === undefined)
|
|
229
|
+
return applySpawnOverrides({ provider, id, thinkingLevel }, type.spawnOverrides);
|
|
226
230
|
return { provider, id, thinkingLevel };
|
|
227
231
|
};
|
|
228
232
|
const resolveAgentSettings = (type: AgentType, parentPath: string): ResolvedAgentSettings => {
|
|
@@ -314,8 +318,11 @@ export function createDriverFactory(
|
|
|
314
318
|
const restored = options.sessionFile ? sessionManager.buildSessionContext() : undefined;
|
|
315
319
|
const modelSelection = getModelSelection();
|
|
316
320
|
// Use Current ignores definition preferences, including for restored/nested sessions.
|
|
321
|
+
// devsys: an explicit per-spawn model pin bypasses preference/scope policy.
|
|
317
322
|
const modelPreferences =
|
|
318
|
-
modelSelection === "use-current"
|
|
323
|
+
modelSelection === "use-current" || options.type.spawnOverrides?.model !== undefined
|
|
324
|
+
? undefined
|
|
325
|
+
: getModelPreferences(options.type);
|
|
319
326
|
const filteringEnabled = () => getModelSelection() === "pick-first-scoped";
|
|
320
327
|
const selectModelPreference = (
|
|
321
328
|
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
|
-
|
|
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(", ")}]`;
|
package/src/subagents/types.ts
CHANGED
|
@@ -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. */
|
|
@@ -170,7 +172,14 @@ export interface ThreadService {
|
|
|
170
172
|
/** Optional for portable adapters; the runtime manager always implements this. */
|
|
171
173
|
observeTranscript?(path: string, listener: TranscriptListener): Promise<TranscriptObservation>;
|
|
172
174
|
spawn(
|
|
173
|
-
args: {
|
|
175
|
+
args: {
|
|
176
|
+
path: string;
|
|
177
|
+
type: string;
|
|
178
|
+
task: string;
|
|
179
|
+
wait?: boolean;
|
|
180
|
+
model?: string;
|
|
181
|
+
thinkingLevel?: string;
|
|
182
|
+
},
|
|
174
183
|
signal?: AbortSignal,
|
|
175
184
|
): Promise<ThreadView>;
|
|
176
185
|
steer(path: string, message: string): Promise<ThreadView>;
|