@jwilger/pi-development-system 0.12.0 → 0.14.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.
@@ -32,7 +32,7 @@ You are an implementer. You receive exactly one task record and nothing else: do
32
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
33
  - Never weaken, skip or delete a test to get green, and never add a lint suppression without a stated reason.
34
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.
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
36
 
37
37
  ## Finish
38
38
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jwilger/pi-development-system",
3
- "version": "0.12.0",
3
+ "version": "0.14.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"
@@ -28,14 +28,17 @@ implementer and reviewer work. It returns a slot, a thinking level and, when one
28
28
  resolves on this machine, a model. Pass them through:
29
29
 
30
30
  ```text
31
- agent_spawn({path: "/root/add-flag", type: "implementer", task: "...",
31
+ agent_spawn({path: "/impl-add-flag", type: "implementer", task: "...",
32
32
  model: "<from route>", thinkingLevel: "<from route>"})
33
33
  ```
34
34
 
35
35
  - Jev judges difficulty and risk; the project's `[routing]` table (slots, never
36
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.
37
+ - If the result says the slot is unresolvable, spawning without a pin fails the same
38
+ way (the agent's own `models:` list is the same candidate set). Run `/devsys-models`
39
+ to fix the matrix, or pin a model you know works.
40
+ - A pin is an explicit choice: it bypasses `/scoped-models` and the agent's `models:`
41
+ list, so only pin what the route (or you, with a stated reason) selected.
39
42
  - You may override the route. Say why in your reply; overriding downwards for
40
43
  risky work is a judgement call worth a line in the decision log.
41
44
 
@@ -48,8 +51,10 @@ you deliver after review.
48
51
 
49
52
  ## The reviewer is always fresh
50
53
 
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
54
+ A reviewer that watched the work being written shares its blind spots. A path
55
+ under `/root/...` inherits your whole conversation, so spawn reviewers and
56
+ implementers at a top-level independent path (one segment, for example
57
+ `/review-1` or `/impl-add-flag`): independent roots start with no history. Give it the diff
53
58
  range and the packet format (`agents/reviewer.md`), and never tell it what you
54
59
  concluded. Fix every blocking and should-fix finding, then ask for a new round.
55
60
 
@@ -55,7 +55,7 @@ export function recommendRoute(input: {
55
55
  if (!resolved.ok) {
56
56
  return {
57
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.`,
58
+ note: `${prefix}No model with credentials resolves slot "${route.slot}" (unresolvable); an agent spawned without a pin will fail the same way, so run /devsys-models to fix the matrix or pin a model you know works.`,
59
59
  };
60
60
  }
61
61
  return {
@@ -57,3 +57,21 @@ export function applySpawnOverrides(
57
57
  ...(overrides.thinkingLevel === undefined ? {} : { thinkingLevel: overrides.thinkingLevel }),
58
58
  };
59
59
  }
60
+
61
+ export type ModelSource = "current" | "preferences" | "restored" | "inherited";
62
+
63
+ /**
64
+ * Where a subagent's starting model comes from. A per-spawn pin disqualifies the type's
65
+ * preference list (the pin itself is applied afterwards for fresh spawns), so a resumed pinned
66
+ * thread keeps the model it was already running instead of falling back to the preferences.
67
+ */
68
+ export function modelSource(input: {
69
+ mode: "use-current" | "pick-first-scoped" | "pick-first";
70
+ pinned: boolean;
71
+ hasPreferences: boolean;
72
+ hasRestored: boolean;
73
+ }): ModelSource {
74
+ if (input.mode === "use-current") return "current";
75
+ if (!input.pinned && input.hasPreferences) return "preferences";
76
+ return input.hasRestored ? "restored" : "inherited";
77
+ }
@@ -56,7 +56,7 @@ export function medianLabel<T extends string>(
56
56
  let cumulative = 0;
57
57
  for (const level of levels) {
58
58
  cumulative += (answer.probabilities[level] ?? 0) / total;
59
- if (cumulative >= 0.5) return level;
59
+ if (cumulative > 0.5) return level;
60
60
  }
61
61
  }
62
62
  return levels.find((level) => level === answer.choice);
@@ -12,7 +12,7 @@
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.
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` skips the type's preference list when a model is pinned (via `modelSource` in `src/core/spawn-overrides.ts`, so resumed threads keep their model), applies the pin last for fresh spawns, and `createDriver` drops model preferences/scope policy when a model is pinned; `ThreadView.pinned` (`types.ts`) is filled in `orch/manager.ts` `view()` so `agent_status` reports the pins. A pin therefore bypasses `/scoped-models`.
16
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
17
 
18
18
  ## MIT notice
@@ -730,6 +730,8 @@ export class ThreadManager {
730
730
  ...record.view,
731
731
  sessionFile: record.driver ? record.driver.sessionFile : record.view.sessionFile,
732
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 };
733
735
  if (record.liveInputTokens) view.inputTokens = (view.inputTokens ?? 0) + record.liveInputTokens;
734
736
  if (record.liveOutputTokens)
735
737
  view.outputTokens = (view.outputTokens ?? 0) + record.liveOutputTokens;
@@ -26,7 +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
+ import { applySpawnOverrides, modelSource } from "../../core/spawn-overrides.ts";
30
30
  import {
31
31
  modelIdentity,
32
32
  getModelPreferences,
@@ -200,13 +200,21 @@ export function createDriverFactory(
200
200
  ? resolved.get(`${ctx.sessionManager.getSessionId()}:${parentPath}`)
201
201
  : undefined;
202
202
  const mode = getModelSelection();
203
- 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 = pinned ? undefined : getModelPreferences(type);
206
+ const source = modelSource({
207
+ mode,
208
+ pinned,
209
+ hasPreferences: preferences !== undefined,
210
+ hasRestored: restored?.model !== undefined,
211
+ });
204
212
  let provider = parent?.provider ?? ctx.model?.provider;
205
213
  let id = parent?.id ?? ctx.model?.id;
206
- if (mode === "use-current") {
214
+ if (source === "current") {
207
215
  provider = ctx.model?.provider;
208
216
  id = ctx.model?.id;
209
- } else if (preferences !== undefined) {
217
+ } else if (source === "preferences") {
210
218
  const filtering = mode === "pick-first-scoped";
211
219
  const candidates = filtering
212
220
  ? availableScopedModels(normalizeScopedModels(ctx.scopedModels))
@@ -214,7 +222,7 @@ export function createDriverFactory(
214
222
  ({ provider, id } = parseModelIdentity(
215
223
  selectTypeModelPreference(type, candidates, filtering)!,
216
224
  ));
217
- } else if (restored?.model) {
225
+ } else if (source === "restored" && restored?.model) {
218
226
  provider = restored.model.provider;
219
227
  id = restored.model.modelId;
220
228
  }
@@ -68,6 +68,8 @@ export interface ThreadView {
68
68
  inputTokens?: number;
69
69
  /** Cumulative output tokens. Live views include the in-progress message. */
70
70
  outputTokens?: number;
71
+ /** devsys: per-spawn model/thinkingLevel pin requested through agent_spawn, when any. */
72
+ pinned?: SpawnOverrides;
71
73
  }
72
74
  export type DriverEvent =
73
75
  | { kind: "activity" | "error"; text: string }