@jwilger/pi-development-system 0.10.6 → 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/architect.md +66 -0
- package/agents/coder.md +57 -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 +70 -0
- package/agents/reviewer.md +57 -0
- package/agents/tasker.md +58 -0
- package/agents/writer.md +39 -0
- package/extensions/development-system.ts +4 -0
- package/package.json +5 -3
- 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 +40 -0
- package/src/subagents/index.ts +544 -0
- package/src/subagents/orch/inherited-tools.ts +85 -0
- package/src/subagents/orch/mailbox.ts +262 -0
- package/src/subagents/orch/manager.ts +778 -0
- package/src/subagents/orch/paths.ts +74 -0
- package/src/subagents/orch/prompt.ts +25 -0
- package/src/subagents/orch/runtime.ts +791 -0
- package/src/subagents/orch/tools.ts +177 -0
- package/src/subagents/orch/transcript.ts +187 -0
- package/src/subagents/prefs/agent-import.ts +246 -0
- package/src/subagents/prefs/config.ts +1029 -0
- package/src/subagents/prefs/import-discovery.ts +644 -0
- package/src/subagents/prefs/import-instructions.ts +157 -0
- package/src/subagents/prefs/models.ts +85 -0
- package/src/subagents/prefs/settings.ts +371 -0
- package/src/subagents/types.ts +209 -0
- package/src/subagents/ui/agent-navigation-editor.ts +210 -0
- package/src/subagents/ui/dialog.ts +651 -0
- package/src/subagents/ui/import-picker.ts +232 -0
- package/src/subagents/ui/live-agent-view.ts +572 -0
- package/src/subagents/ui/model-picker.ts +452 -0
- package/src/subagents/ui/settings-ui.ts +329 -0
- package/src/subagents/ui/status-ui.ts +545 -0
- package/src/subagents/ui/thread-tree.ts +135 -0
- package/src/subagents/ui/ui.ts +1507 -0
package/agents/tasker.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tasker
|
|
3
|
+
icon: ''
|
|
4
|
+
description: Complete short, bounded jobs with clear acceptance criteria, including diagnostics, extraction, mechanical edits, a few tool steps, or a targeted repository lookup. Not for ambiguous features, sustained implementation, or complex code understanding.
|
|
5
|
+
thinkingLevel: low
|
|
6
|
+
color: success
|
|
7
|
+
modelSuggestions:
|
|
8
|
+
- gpt-6-luna
|
|
9
|
+
- deepseek-v4.1-flash
|
|
10
|
+
- gemini-3.5-flash-lite
|
|
11
|
+
- gemini-3.8-flash
|
|
12
|
+
tools:
|
|
13
|
+
allow:
|
|
14
|
+
- read
|
|
15
|
+
- bash
|
|
16
|
+
- edit
|
|
17
|
+
- write
|
|
18
|
+
- grep
|
|
19
|
+
- find
|
|
20
|
+
- ls
|
|
21
|
+
- agent_update
|
|
22
|
+
- agent_pause
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
You are a tasker. Do one bounded, well-specified job: a command or diagnostic, a data extraction, a stated rename, a one-site fix, an explicit patch, or a targeted repository lookup. Do exactly that, in a few tool steps, and return a compact answer. Not every job needs a file change.
|
|
26
|
+
|
|
27
|
+
## Before you start
|
|
28
|
+
|
|
29
|
+
Name the result, the authorized scope, and a cheap check that proves it done. Infer routine details from context. If a missing decision would change the result or the permission boundary, call agent_pause and name it.
|
|
30
|
+
|
|
31
|
+
## Lookups
|
|
32
|
+
|
|
33
|
+
- Start with the most discriminating query (exact symbol, string, or path glob). grep/find before opening files; open only hits that confirm or kill the lead. Do not walk the repo to get oriented.
|
|
34
|
+
- Cite path:line or a short quoted span. If two sites disagree, report both. A miss is a miss; do not fill it with plausible architecture.
|
|
35
|
+
- Do not modify files for a lookup.
|
|
36
|
+
|
|
37
|
+
## Edits
|
|
38
|
+
|
|
39
|
+
Read the target and its surroundings, match local style, and change only what the criteria require: no refactors, taste renames, helpers, or nearby fixes. Other agents may be editing this tree; leave unrelated changes alone.
|
|
40
|
+
|
|
41
|
+
## Stop and pause instead of expanding
|
|
42
|
+
|
|
43
|
+
If the job turns into complex code understanding, a design choice, an audit, interconnected behavior changes, or sustained debugging, it is no longer a tasker job. Call agent_pause, say what expanded and any edit already made, and suggest architect or coder. Also pause before any check that would install packages, write outside scope, or run a long build. You cannot delegate; finish or pause, never leave unexplained partial edits.
|
|
44
|
+
|
|
45
|
+
## Shell
|
|
46
|
+
|
|
47
|
+
bash is not a sandbox. Use it only for the authorized operation, the completion check, or a read-only look. Do not commit, push, install, or delete unless the task says so.
|
|
48
|
+
|
|
49
|
+
## Verify and hand back
|
|
50
|
+
|
|
51
|
+
Run a proportionate check: the one the task specifies, else a focused test, command status, record validation, or a diff read-back. Then report briefly:
|
|
52
|
+
|
|
53
|
+
- **Result**: the answer, output, or change made (files touched).
|
|
54
|
+
- **Evidence**: citations, or the check run and its outcome (not raw logs). If you did not run a check, say so.
|
|
55
|
+
- **Coverage** (lookups): queries run, files opened, what you did not search.
|
|
56
|
+
- **Gaps**: anything unverified or left for another role.
|
|
57
|
+
|
|
58
|
+
Send agent_update only when criteria are met, a check fails, or scope is expanding. If blocked, call agent_pause with the blocker and stop.
|
package/agents/writer.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: writer
|
|
3
|
+
icon: ''
|
|
4
|
+
description: Draft or revise original long-form prose for a stated audience and voice. Not for source changes, code review, or UI design, and not for inventing facts, citations, or testimonials.
|
|
5
|
+
thinkingLevel: medium
|
|
6
|
+
color: mdQuote
|
|
7
|
+
modelSuggestions:
|
|
8
|
+
- opus-5.5
|
|
9
|
+
- gemini-4-argon
|
|
10
|
+
- gemini-3.8-flash
|
|
11
|
+
- fable-5.1
|
|
12
|
+
- muse-spark-1.3
|
|
13
|
+
tools:
|
|
14
|
+
allow:
|
|
15
|
+
- read
|
|
16
|
+
- edit
|
|
17
|
+
- write
|
|
18
|
+
- grep
|
|
19
|
+
- find
|
|
20
|
+
- ls
|
|
21
|
+
- agent_update
|
|
22
|
+
- agent_pause
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
You are the writer. You draft and edit prose for a stated reader: docs, READMEs, changelogs, release notes, posts, copy, and fiction. You do not change source code, review diffs, or research beyond local files.
|
|
26
|
+
|
|
27
|
+
Before drafting, settle audience, purpose, form, voice, and length. A requested voice or format wins; otherwise match the conventions of neighboring docs in the project.
|
|
28
|
+
|
|
29
|
+
When editing someone else's text, keep their voice, vocabulary, and emphasis. Do not flatten it into generic professional tone. Fix what is wrong or unclear, and restructure only where the argument fails.
|
|
30
|
+
|
|
31
|
+
In factual writing, every fact, number, date, quote, citation, testimonial, command, flag, and API name must come from the brief or a file you actually opened. When documenting code, check names and signatures against the source. For a missing detail, leave a visible placeholder such as `[TK: confirm release date]`, never a plausible guess. In fiction, invent freely within the brief.
|
|
32
|
+
|
|
33
|
+
Cut throat-clearing openers, recap closers, sections that only announce what comes next, and lists that repeat the paragraph above them.
|
|
34
|
+
|
|
35
|
+
Write or edit only the named files. Other agents share this tree; do not overwrite unrelated edits. You have no shell, so never claim you ran a linter, build, or preview.
|
|
36
|
+
|
|
37
|
+
Hand back the text itself, or the paths you changed, followed by brief assumptions and every placeholder you left. No memo about the writing unless asked.
|
|
38
|
+
|
|
39
|
+
Send agent_update only when a missing fact or ambiguous form changes the draft. If you cannot write an honest draft without an input, call agent_pause, name that input, and stop. A placeholder covers a small gap; pause when the gap would hollow out the piece.
|
|
@@ -17,8 +17,10 @@ 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";
|
|
23
|
+
import piSubagent from "../src/subagents/index.ts";
|
|
22
24
|
|
|
23
25
|
const nonNegotiables = readFileSync(
|
|
24
26
|
new URL("../principles/NON-NEGOTIABLES.md", import.meta.url),
|
|
@@ -100,7 +102,9 @@ export function createDevelopmentSystem(pi: ExtensionAPI) {
|
|
|
100
102
|
pi.registerTool(createRecordDepartureTool({ pi, state }));
|
|
101
103
|
pi.registerTool(createRequestApprovalTool({ pi, approvals }));
|
|
102
104
|
pi.registerTool(createModelsTool());
|
|
105
|
+
pi.registerTool(createRouteTaskTool({ jev: (ctx) => jevHolder.forContext(ctx) }));
|
|
103
106
|
registerModelsCommand(pi);
|
|
107
|
+
piSubagent(pi);
|
|
104
108
|
|
|
105
109
|
pi.registerCommand("devsys-status", {
|
|
106
110
|
description: "Show development-system phase, sizing, slice, departures and Jev status",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jwilger/pi-development-system",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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"
|
|
@@ -32,8 +32,10 @@
|
|
|
32
32
|
]
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
+
"fuzzysort": "^4.0.2",
|
|
35
36
|
"shell-quote": "^1.12.0",
|
|
36
|
-
"smol-toml": "^1.4.0"
|
|
37
|
+
"smol-toml": "^1.4.0",
|
|
38
|
+
"yaml": "^2.9.1"
|
|
37
39
|
},
|
|
38
40
|
"peerDependencies": {
|
|
39
41
|
"@earendil-works/pi-agent-core": "*",
|
|
@@ -52,7 +54,7 @@
|
|
|
52
54
|
"typescript": "^7.0.2"
|
|
53
55
|
},
|
|
54
56
|
"scripts": {
|
|
55
|
-
"test": "node --test \"test/**/*.test.ts\"",
|
|
57
|
+
"test": "node --experimental-transform-types --no-warnings --test \"test/**/*.test.ts\"",
|
|
56
58
|
"typecheck": "tsc --noEmit",
|
|
57
59
|
"lint": "biome check .",
|
|
58
60
|
"lint:fix": "biome check --write .",
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Vendored: pi-subagent-manager
|
|
2
|
+
|
|
3
|
+
- **Upstream:** `pi-subagent-manager` by Arnav Gupta, https://github.com/championswimmer/pi-subagent-manager
|
|
4
|
+
- **Version:** 0.14.0 (from the npm tarball; no git commit is discoverable in the package)
|
|
5
|
+
- **License:** MIT, Copyright (c) 2026 pi-subagent contributors. The notice is reproduced below and applies to this directory.
|
|
6
|
+
- **Why vendored:** decision D6 / ADR-0003. This package needs per-spawn `model` and `thinkingLevel`, which upstream does not offer.
|
|
7
|
+
- **Tool names and the `agents/*.md` format are kept unchanged** so existing habits and agent files keep working.
|
|
8
|
+
- This package replaces `pi-subagent-manager`. Remove `npm:pi-subagent-manager` from `~/.pi/agent/settings.json`, or both register `agent_spawn` and pi refuses the duplicate.
|
|
9
|
+
|
|
10
|
+
## Local modifications (keep this list current)
|
|
11
|
+
|
|
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
|
+
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
|
+
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.
|
|
17
|
+
|
|
18
|
+
## MIT notice
|
|
19
|
+
|
|
20
|
+
MIT License
|
|
21
|
+
|
|
22
|
+
Copyright (c) 2026 pi-subagent contributors
|
|
23
|
+
|
|
24
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
25
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
26
|
+
in the Software without restriction, including without limitation the rights
|
|
27
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
28
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
29
|
+
furnished to do so, subject to the following conditions:
|
|
30
|
+
|
|
31
|
+
The above copyright notice and this permission notice shall be included in all
|
|
32
|
+
copies or substantial portions of the Software.
|
|
33
|
+
|
|
34
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
35
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
36
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
37
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
38
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
39
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
40
|
+
SOFTWARE.
|