@jwilger/pi-development-system 0.13.0 → 0.15.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jwilger/pi-development-system",
3
- "version": "0.13.0",
3
+ "version": "0.15.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"
@@ -34,8 +34,11 @@ agent_spawn({path: "/impl-add-flag", type: "implementer", task: "...",
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
 
@@ -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 {
@@ -16,7 +16,7 @@ export type Settings = {
16
16
  readonly thinkingLevel: ThinkingLevel;
17
17
  };
18
18
 
19
- const MODEL_REF = /^[^\s/]+\/\S+$/;
19
+ const MODEL_REF = /^[^\s/*]+\/[^\s*]+$/;
20
20
 
21
21
  const isLevel = (value: unknown): value is ThinkingLevel =>
22
22
  THINKING_LEVELS.some((level) => level === value);
@@ -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-available";
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
@@ -38,3 +38,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
38
38
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
39
39
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
40
40
  SOFTWARE.
41
+ 6. Empty scope (I5 review): `orch/runtime.ts` `availableScopedModels` treats an empty `/scoped-models` as no restriction (pi's meaning) instead of "matches nothing", because every devsys agent declares `models:` and would otherwise be unspawnable by default. Pinned models are also re-applied on resume (`resolveInitialSettings`), since a restored session may hold only inherited history.
@@ -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,
@@ -165,6 +165,8 @@ export function createDriverFactory(
165
165
  .map((model) => ({ model }));
166
166
  const availableScopedModels = (scopedModels: readonly ScopedModel[]) => {
167
167
  const available = new Set(availableModelCandidates().map(({ model }) => modelIdentity(model)));
168
+ // devsys: an empty scope means no /scoped-models restriction in pi, not "matches nothing".
169
+ if (scopedModels.length === 0) return [...availableModelCandidates()];
168
170
  return scopedModels.filter(({ model }) => available.has(modelIdentity(model)));
169
171
  };
170
172
  const selectTypeModelPreference = (
@@ -202,13 +204,19 @@ export function createDriverFactory(
202
204
  const mode = getModelSelection();
203
205
  // devsys: an explicit per-spawn model pin bypasses preference/scope policy (also on resume).
204
206
  const pinned = type.spawnOverrides?.model !== undefined;
205
- const preferences = mode === "use-current" || pinned ? undefined : getModelPreferences(type);
207
+ const preferences = pinned ? undefined : getModelPreferences(type);
208
+ const source = modelSource({
209
+ mode,
210
+ pinned,
211
+ hasPreferences: preferences !== undefined,
212
+ hasRestored: restored?.model !== undefined,
213
+ });
206
214
  let provider = parent?.provider ?? ctx.model?.provider;
207
215
  let id = parent?.id ?? ctx.model?.id;
208
- if (mode === "use-current") {
216
+ if (source === "current") {
209
217
  provider = ctx.model?.provider;
210
218
  id = ctx.model?.id;
211
- } else if (preferences !== undefined) {
219
+ } else if (source === "preferences") {
212
220
  const filtering = mode === "pick-first-scoped";
213
221
  const candidates = filtering
214
222
  ? availableScopedModels(normalizeScopedModels(ctx.scopedModels))
@@ -216,7 +224,7 @@ export function createDriverFactory(
216
224
  ({ provider, id } = parseModelIdentity(
217
225
  selectTypeModelPreference(type, candidates, filtering)!,
218
226
  ));
219
- } else if (restored?.model) {
227
+ } else if (source === "restored" && restored?.model) {
220
228
  provider = restored.model.provider;
221
229
  id = restored.model.modelId;
222
230
  }
@@ -226,9 +234,12 @@ export function createDriverFactory(
226
234
  : undefined;
227
235
  const thinkingLevel =
228
236
  savedThinking ?? type.thinkingLevel ?? parent?.thinking ?? ctx.thinkingLevel ?? "off";
229
- // devsys: a per-spawn pin from agent_spawn wins; restored sessions already persist their choice.
230
- if (restored === undefined)
231
- return applySpawnOverrides({ provider, id, thinkingLevel }, type.spawnOverrides);
237
+ // devsys: a per-spawn pin from agent_spawn wins. On resume the thinking level is the saved one,
238
+ // but the pinned model still applies: the restored session may only hold inherited history.
239
+ const pins = type.spawnOverrides;
240
+ if (restored === undefined) return applySpawnOverrides({ provider, id, thinkingLevel }, pins);
241
+ if (pins?.model !== undefined)
242
+ return applySpawnOverrides({ provider, id, thinkingLevel }, { model: pins.model });
232
243
  return { provider, id, thinkingLevel };
233
244
  };
234
245
  const resolveAgentSettings = (type: AgentType, parentPath: string): ResolvedAgentSettings => {