pi-orchestrator-config 4.8.4 → 4.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -14,8 +14,8 @@ Single extension that provides:
14
14
  | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
15
15
  | **Subagent tool** | Delegate tasks to specialist agents (single, parallel, chain, async modes); optional `model` param for model override |
16
16
  | **`list_models`** | List available models and providers for subagent model override |
17
- | **Async background agents** | Spawn agents in background with `async: true` — results surface automatically when complete. Fullscreen overlay dashboard with live output, keyboard nav, and kill support (`/async-status`: `x` kills, `Space` multi-selects, `a` kills all). On **acpx** parents, optional async is coerced to sync; dream/cron need `internal_operations_provider` + `internal_operations_model`. `cli-*` providers support async natively (see `dev-docs/cli-provider.md`) |
18
- | **CLI / ACPX providers** | Optional `cli_agents` / `acpx_agents` register `cli-*` / `acpx-*` via native `createProvider` (pi ≥ 0.84): `/login cli-<agent>` or `/login acpx-<agent>`, model refresh, filter when unavailable. Missing context window / maxTokens / cost filled from models.dev (`~/.pi/pi-config/models.dev.json`, 1-day refresh). Cold-start default restore (#753): gates `startup`\|`new`, empty `enabledModels`, trusted project merge — see `dev-docs/cli-provider.md`. Headless Cursor `--approve-mcps` when `CLI_APPROVE_MCPS` is set (`false` opts out) or the process is a sidecar (`SIDECAR_PORT`; `startSidecar()` stamps it while running and restores it on `close()`). Executable sidecar consumers should call `bindSidecarListenExit()` so bind/`stopped` fatals exit 1. Headless Gemini defaults `GEMINI_CLI_TRUST_WORKSPACE=true` and preserves an explicit parent value (including `false`). |
17
+ | **Async background agents** | Spawn agents in background with `async: true` — results surface automatically when complete. Fullscreen overlay dashboard with live output, keyboard nav, and kill support (`/async-status`: `x` kills, `Space` multi-selects, `a` kills all). On **acpx** parents, optional async is coerced to sync; dream/cron need `internal_operations_provider` + `internal_operations_model`. `cli-*` providers support async natively (see `docs/cli-provider.md`) |
18
+ | **CLI / ACPX providers** | Optional `cli_agents` / `acpx_agents` register `cli-*` / `acpx-*` via native `createProvider` (pi ≥ 0.84): `/login cli-<agent>` or `/login acpx-<agent>`, model refresh, filter when unavailable. Missing context window / maxTokens / cost filled from models.dev (`~/.pi/pi-config/models.dev.json`, 1-day refresh). Cold-start default restore (#753): gates `startup`\|`resume` (`new` only when no model is selected), empty `enabledModels`, trusted project merge — see `docs/cli-provider.md`. Headless Cursor `--approve-mcps` when `CLI_APPROVE_MCPS` is set (`false` opts out) or the process is a sidecar (`SIDECAR_PORT`; `startSidecar()` stamps it while running and restores it on `close()`). Executable sidecar consumers should call `bindSidecarListenExit()` so bind/`stopped` fatals exit 1. Headless Gemini defaults `GEMINI_CLI_TRUST_WORKSPACE=true` and preserves an explicit parent value (including `false`). |
19
19
  | **`/btw` command** | Quick side questions without polluting conversation history — ephemeral overlay |
20
20
  | **`/async-status` command** | Show status of background agents — select one for live output streaming, multi-select and kill (`x`), or kill everything (`a`) |
21
21
  | **`ask_user` tool** | Structured user input with options and free-text — used by workflows |
@@ -305,7 +305,7 @@ See [`pi-config-settings.example.jsonc`](pi-config-settings.example.jsonc) for a
305
305
 
306
306
  Resolution order: project file → global `~/.pi/pi-config-settings.json` → env var → default.
307
307
 
308
- See `dev-docs/project-settings.md` for the full settings table.
308
+ See `docs/configuration.md` for the full settings table.
309
309
 
310
310
  #### Reviewer Environment Variables
311
311
 
@@ -368,7 +368,7 @@ Rules auto-load alphabetically. Same-filename entries are overridden (project >
368
368
 
369
369
  Rules support settings/feature conditionals at prompt assembly (`assembleRuleText` in
370
370
  `rule-placeholders.ts`). Full syntax, truthiness, and assembly order:
371
- **[dev-docs/project-settings.md](dev-docs/project-settings.md)** (Rules assembly).
371
+ **[docs/rules-and-layers.md](docs/rules-and-layers.md)** (Rules assembly).
372
372
 
373
373
  Markers: `{{IF:key}}…{{/IF}}`, `{{IFNOT:key}}…{{/IFNOT}}`, comparisons
374
374
  `{{IF:key==value}}` / `{{IF:key!=value}}`. Whole-file frontmatter: `requires_setting` /
@@ -483,7 +483,7 @@ that directory. Do not stop running containers merely to test this PR.
483
483
  After register, `filterModels` hides models when unavailable: PATH cleared while
484
484
  agent state remains, restore PATH; after `session_shutdown` (AgentState cleared
485
485
  and `initialized` reset) the next factory re-registers on `/new`|`/resume`|`/fork`
486
- (PATH alone is not enough). See `dev-docs/cli-provider.md`.
486
+ (PATH alone is not enough). See `docs/cli-provider.md`.
487
487
 
488
488
  **ACPX provider agents (optional `acpx_agents`):** ACPX wraps CLI agents via the
489
489
  ACP runtime. Supported agents: `cursor`, `claude`, `gemini`. Enable with
@@ -500,10 +500,10 @@ symlinks package `agents/*.md` into the mounted project:
500
500
 
501
501
  Uses `ln -sfn` (safe if multiple `pi-docker` sessions share the same folder). Those three
502
502
  directories are added to the container global gitignore. **Native** installs do not auto-
503
- sync — copy or symlink yourself (see `dev-docs/cli-provider.md`).
503
+ sync — copy or symlink yourself (see `docs/cli-provider.md`).
504
504
 
505
505
  **Extension ops logs:** `cli-provider` and dreaming write to `~/.pi/logs/` (not the chat
506
- UI). Fallback: `$TMPDIR/pi-logs/`. Details in `dev-docs/cli-provider.md` (Logging).
506
+ UI). Fallback: `$TMPDIR/pi-logs/`. Details in `docs/cli-provider.md` (Logging).
507
507
 
508
508
  ### Pre-built image
509
509
 
@@ -917,6 +917,10 @@ CLI commands: `npx pi-sidecar` (start server), `npx pi-sidecar-start` (backgroun
917
917
 
918
918
  `GET /providers` lists registered providers and whether each supports a session API key, even when no models or credentials are available. The Python client exposes this as `await SidecarClient.get_providers()`.
919
919
 
920
+ The sidecar's internal agent dir is configurable (startSidecar `agentDir` option / `PI_SIDECAR_AGENT_DIR`, default `/tmp/pi-sidecar-agent`) so
921
+ deployments can supply custom providers, auth, and settings — see
922
+ [`packages/pi-sidecar/README.md`](packages/pi-sidecar/README.md).
923
+
920
924
  See [`packages/pi-sidecar/README.md`](packages/pi-sidecar/README.md) for full documentation.
921
925
  See [`packages/pi-sidecar/CONSUMER-GUIDE.md`](packages/pi-sidecar/CONSUMER-GUIDE.md) for integration best practices.
922
926
 
@@ -76,8 +76,8 @@ Use this documentation updates table:
76
76
  | Change | Check these files |
77
77
  |--------|-------------------|
78
78
  | New feature/command/tool | `README.md` (feature table, usage examples) |
79
- | New or modified extension module | `dev-docs/repo-structure.md` (repository structure) |
80
- | New agent added/removed | `dev-docs/repo-structure.md`, routing rules, bug reporting agent list |
79
+ | New or modified extension module | `contributing/repo-structure.md` (repository structure) |
80
+ | New agent added/removed | `contributing/repo-structure.md`, routing rules, bug reporting agent list |
81
81
  | New prompt template | `README.md` (prompt templates table) |
82
82
  | Docker/container changes | `README.md` (Docker section), `Dockerfile` |
83
83
  | New CLI tool or dependency | `README.md` (tools table), `Dockerfile` |
@@ -119,7 +119,7 @@ Flag audit failures as `[WARNING]` with the specific checklist item that failed.
119
119
 
120
120
  ### 5. Cross-File Consistency
121
121
 
122
- - Do README.md, AGENTS.md, and dev-docs agree on commands, structure, and workflows?
122
+ - Do README.md, AGENTS.md, and contributing/ agree on commands, structure, and workflows?
123
123
  - Are version numbers, feature lists, and agent counts consistent?
124
124
  - Do referenced files actually exist?
125
125
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pidiff-ui",
3
3
  "private": true,
4
- "version": "4.8.4",
4
+ "version": "4.8.5",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
@@ -67,7 +67,7 @@ import {
67
67
  isProvidersInitialized,
68
68
  markProvidersInitialized,
69
69
  } from "./initialized-guard.js";
70
- import { restoreDefaultModelOnSessionStart } from "./restore-default-model.js";
70
+ import { registerModelRestoreOnSessionStart } from "./model-restore-wiring.js";
71
71
  import { registerLastThinkingLevel } from "./last-thinking-level.js";
72
72
  import { teardownProvidersOnSessionShutdown } from "./session-shutdown.js";
73
73
 
@@ -631,6 +631,22 @@ export default async function (
631
631
  }
632
632
  };
633
633
 
634
+ // Cold-start default model restore (#753), wired as its own session_start
635
+ // handler (multiple registrations are fine) so the wiring is unit-testable.
636
+ // Registered before the discovery-summary handler; the restore itself is
637
+ // fire-and-forget so ordering with the summary/thinking handler is benign.
638
+ registerModelRestoreOnSessionStart(pi, {
639
+ argv: testOptions?.argv,
640
+ onRestoreModel: (model, setSelected) => {
641
+ log.debug("onRestoreModel handoff", { provider: model.provider, id: model.id });
642
+ return lastThinking.restoreModel(model as Model<any>, setSelected);
643
+ },
644
+ onRestoreSettled: (event, ctx, modelRestore) => {
645
+ log.debug("onRestoreSettled handoff", { reason: (event as { reason?: string })?.reason });
646
+ return void lastThinking.applyAfterModelRestore(event, ctx, modelRestore);
647
+ },
648
+ });
649
+
634
650
  pi.on("session_start", (event, ctx) => {
635
651
  const reason = typeof event?.reason === "string" ? event.reason : "";
636
652
  const summary = `Providers: ${providerSummaryParts.join(", ")}`;
@@ -651,46 +667,6 @@ export default async function (
651
667
  ? { id: ctx.model.id, provider: String(ctx.model.provider) }
652
668
  : undefined,
653
669
  );
654
- // agentDir via PI_CODING_AGENT_DIR / ~/.pi/agent only (ctx has cwd, not agentDir).
655
- // Merge global + cwd/.pi/settings.json (project wins) only when trusted —
656
- // same gate as pi SettingsManager (ctx.isProjectTrusted). Fail closed.
657
- const cwd = typeof ctx.cwd === "string" ? ctx.cwd : undefined;
658
- let projectTrusted = false;
659
- try {
660
- projectTrusted = typeof ctx.isProjectTrusted === "function"
661
- ? ctx.isProjectTrusted() === true
662
- : false;
663
- } catch (err) {
664
- log.debug(
665
- "restore-default-model isProjectTrusted failed",
666
- err instanceof Error ? err.message : String(err),
667
- );
668
- projectTrusted = false;
669
- }
670
- const modelRestore = restoreDefaultModelOnSessionStart({
671
- ctx: {
672
- model: ctx.model
673
- ? { id: ctx.model.id, provider: String(ctx.model.provider) }
674
- : undefined,
675
- modelRegistry: ctx.modelRegistry,
676
- },
677
- reason,
678
- cwd,
679
- projectTrusted,
680
- argv: testOptions?.argv,
681
- getCurrentModel: () =>
682
- ctx.model
683
- ? { id: ctx.model.id, provider: String(ctx.model.provider) }
684
- : undefined,
685
- setModel: (model) => lastThinking.restoreModel(model as Model<any>, (selected) => pi.setModel(selected)),
686
- }).catch((err) => {
687
- log.warn(
688
- "restore-default-model session_start error",
689
- err instanceof Error ? err.message : String(err),
690
- );
691
- return false;
692
- });
693
- void lastThinking.applyAfterModelRestore(event, ctx, modelRestore);
694
670
  });
695
671
 
696
672
  pi.on("model_select", (event) => {
@@ -0,0 +1,120 @@
1
+ /**
2
+ * session_start wiring for the cold-start default model restore (#753).
3
+ *
4
+ * Extracted from extensions/providers/index.ts so the handler wiring —
5
+ * reason forwarding, setModel plumbing, cwd/projectTrusted extraction,
6
+ * fire-and-forget .catch, applyAfterModelRestore handoff — can be unit
7
+ * tested with a fake pi object. A wiring bug here (wrong reason, broken
8
+ * setModel) would otherwise reset the model undetected (#901 finding).
9
+ *
10
+ * Kept free of @earendil-works/pi-ai imports beyond the Model type so unit
11
+ * tests can import under tsx.
12
+ */
13
+
14
+ import type { Model } from "@earendil-works/pi-ai";
15
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
+ import { createLogger } from "../shared/logger.js";
17
+ import { restoreDefaultModelOnSessionStart, type ModelLike } from "./restore-default-model.js";
18
+
19
+ const log = createLogger("providers");
20
+
21
+ /** Raw selection call (pi.setModel) handed to onRestoreModel. */
22
+ export type ModelRestoreSetModel = (model: ModelLike) => Promise<boolean>;
23
+
24
+ export type RegisterModelRestoreOnSessionStartOpts = {
25
+ /** CLI argv to scan for --model / --provider / --models (user override). */
26
+ argv?: string[];
27
+ /**
28
+ * setModel wrapper invoked by the restore. Receives the resolved default
29
+ * model and a raw setSelected that performs the actual selection
30
+ * (pi.setModel). Defaults to calling setSelected directly. index.ts passes
31
+ * lastThinking.restoreModel so thinking preferences follow the switch.
32
+ */
33
+ onRestoreModel?: (
34
+ model: ModelLike,
35
+ setSelected: ModelRestoreSetModel,
36
+ ) => Promise<boolean>;
37
+ /**
38
+ * Hook receiving the fire-and-forget (already caught) restore promise —
39
+ * index.ts passes lastThinking.applyAfterModelRestore so the saved thinking
40
+ * level is applied once the model restore settles. Optional.
41
+ */
42
+ onRestoreSettled?: (
43
+ event: any,
44
+ ctx: any,
45
+ modelRestore: Promise<boolean>,
46
+ ) => void;
47
+ };
48
+
49
+ /**
50
+ * Register a session_start handler that restores the saved default model
51
+ * when the #753 gates pass (see restore-default-model.ts). Fire-and-forget:
52
+ * session_start is not blocked by retries.
53
+ */
54
+ export function registerModelRestoreOnSessionStart(
55
+ pi: ExtensionAPI,
56
+ opts: RegisterModelRestoreOnSessionStartOpts = {},
57
+ ): void {
58
+ const onRestoreModel =
59
+ opts.onRestoreModel ?? ((model, setSelected) => setSelected(model));
60
+ log.debug("model-restore-wiring: registering session_start handler");
61
+
62
+ pi.on("session_start", (event, ctx) => {
63
+ const reason = typeof event?.reason === "string" ? event.reason : "";
64
+ log.debug("model-restore-wiring session_start", { reason });
65
+
66
+ // agentDir via PI_CODING_AGENT_DIR / ~/.pi/agent only (ctx has cwd, not agentDir).
67
+ // Merge global + cwd/.pi/settings.json (project wins) only when trusted —
68
+ // same gate as pi SettingsManager (ctx.isProjectTrusted). Fail closed.
69
+ const cwd = typeof ctx.cwd === "string" ? ctx.cwd : undefined;
70
+ let projectTrusted = false;
71
+ try {
72
+ projectTrusted = typeof ctx.isProjectTrusted === "function"
73
+ ? ctx.isProjectTrusted() === true
74
+ : false;
75
+ } catch (err) {
76
+ log.debug(
77
+ "restore-default-model isProjectTrusted failed",
78
+ {
79
+ cwd,
80
+ reason,
81
+ error: err instanceof Error ? err.message : String(err),
82
+ },
83
+ );
84
+ projectTrusted = false;
85
+ }
86
+ // Fire-and-forget restore so session_start is not blocked by retries (#753).
87
+ // Omit registeredProviders: cli/acpx-only lists falsely fail-fast native defaults.
88
+ const modelRestore = restoreDefaultModelOnSessionStart({
89
+ ctx: {
90
+ model: ctx.model
91
+ ? { id: ctx.model.id, provider: String(ctx.model.provider) }
92
+ : undefined,
93
+ modelRegistry: ctx.modelRegistry,
94
+ },
95
+ reason,
96
+ cwd,
97
+ projectTrusted,
98
+ argv: opts.argv,
99
+ getCurrentModel: () =>
100
+ ctx.model
101
+ ? { id: ctx.model.id, provider: String(ctx.model.provider) }
102
+ : undefined,
103
+ setModel: (model) =>
104
+ onRestoreModel(model, (selected) =>
105
+ pi.setModel(selected as Model<any>),
106
+ ),
107
+ }).catch((err) => {
108
+ log.warn(
109
+ "restore-default-model session_start error",
110
+ err instanceof Error ? err.message : String(err),
111
+ );
112
+ return false;
113
+ });
114
+ if (opts.onRestoreSettled) {
115
+ opts.onRestoreSettled(event, ctx, modelRestore);
116
+ } else {
117
+ void modelRestore;
118
+ }
119
+ });
120
+ }
@@ -7,7 +7,8 @@
7
7
  * On session_start (providers already registered), restore the saved default
8
8
  * when current differs (or is missing), unless the user passed --model /
9
9
  * --provider / --models, enabledModels scopes the model list, or the session
10
- * is fork/reload.
10
+ * is fork/reload. "/new" keeps a selected model but still fills an empty
11
+ * selection (#901).
11
12
  *
12
13
  * Kept free of @earendil-works/pi-ai so unit tests can import under tsx.
13
14
  */
@@ -20,14 +21,17 @@ import { createLogger } from "../shared/logger.js";
20
21
  const log = createLogger("providers");
21
22
 
22
23
  /** session_start reasons where cold-start restore is allowed. */
23
- export const RESTORE_ALLOWED_REASONS = new Set(["startup", "new", "resume"]);
24
+ export const RESTORE_ALLOWED_REASONS = new Set(["startup", "resume"]);
24
25
 
25
26
  export type RestoreDefaultModelOpts = {
26
27
  defaultProvider?: string | null;
27
28
  defaultModelId?: string | null;
28
29
  currentProvider?: string | null;
29
30
  currentModelId?: string | null;
30
- /** session_start reason; only "startup" | "new" | "resume" may restore. */
31
+ /**
32
+ * session_start reason; "startup" | "resume" may restore, and "new" only
33
+ * when no model is currently selected (empty selection is filled).
34
+ */
31
35
  reason?: string | null;
32
36
  /**
33
37
  * CLI argv to scan for --model / --provider / --models (user override).
@@ -78,14 +82,33 @@ export function hasEnabledModelsScope(
78
82
  *
79
83
  * Gates (all must pass):
80
84
  * 1. defaultProvider and defaultModelId both non-empty
81
- * 2. reason is startup|new|resume
85
+ * 2. reason is startup|resume; "new" is allowed only when the current model
86
+ * is missing (an existing selection is kept — nothing to preserve when
87
+ * empty). fork/reload never restore.
82
88
  * 3. current missing OR current provider/id ≠ default
83
- * 4. argv does not contain --model, --provider, or --models
84
- * 5. enabledModels is missing or empty (non-empty scopes like --models)
89
+ * 4. argv does not contain --model, --provider, or --models (blocks in all
90
+ * cases, including "new" with a missing model)
91
+ * 5. enabledModels is missing or empty (non-empty scopes like --models;
92
+ * blocks in all cases)
85
93
  */
86
94
  export function shouldRestoreDefaultModel(opts: RestoreDefaultModelOpts): boolean {
87
95
  const reason = typeof opts.reason === "string" ? opts.reason.trim() : "";
88
- if (!RESTORE_ALLOWED_REASONS.has(reason)) {
96
+
97
+ const currentProvider = typeof opts.currentProvider === "string"
98
+ ? opts.currentProvider.trim()
99
+ : "";
100
+ const currentModelId = typeof opts.currentModelId === "string"
101
+ ? opts.currentModelId.trim()
102
+ : "";
103
+ const currentMissing = !currentProvider || !currentModelId;
104
+
105
+ // Reason gate: startup|resume always eligible; "new" only when no model is
106
+ // selected (fills an empty selection; an existing selection is kept, #901).
107
+ if (
108
+ !RESTORE_ALLOWED_REASONS.has(reason)
109
+ && !(reason === "new" && currentMissing)
110
+ ) {
111
+ log.debug("shouldRestoreDefaultModel reject", { stage: "reason-gate", reason, currentMissing });
89
112
  return false;
90
113
  }
91
114
 
@@ -96,25 +119,26 @@ export function shouldRestoreDefaultModel(opts: RestoreDefaultModelOpts): boolea
96
119
  ? opts.defaultModelId.trim()
97
120
  : "";
98
121
 
99
- if (!provider || !modelId) return false;
122
+ if (!provider || !modelId) {
123
+ log.debug("shouldRestoreDefaultModel reject", { stage: "missing-defaults", reason });
124
+ return false;
125
+ }
100
126
 
101
127
  if (argvHasModelOrProviderOverride(opts.argv)) {
128
+ log.debug("shouldRestoreDefaultModel reject", { stage: "argv-override", reason });
102
129
  return false;
103
130
  }
104
131
 
105
132
  if (hasEnabledModelsScope(opts.enabledModels)) {
133
+ log.debug("shouldRestoreDefaultModel reject", { stage: "enabled-models-scope", reason });
106
134
  return false;
107
135
  }
108
136
 
109
- const currentProvider = typeof opts.currentProvider === "string"
110
- ? opts.currentProvider.trim()
111
- : "";
112
- const currentModelId = typeof opts.currentModelId === "string"
113
- ? opts.currentModelId.trim()
114
- : "";
115
-
116
- if (!currentProvider || !currentModelId) return true;
117
- return currentProvider !== provider || currentModelId !== modelId;
137
+ const decision = currentMissing
138
+ || currentProvider !== provider
139
+ || currentModelId !== modelId;
140
+ log.debug("shouldRestoreDefaultModel decision", { reason, decision });
141
+ return decision;
118
142
  }
119
143
 
120
144
  export type PiAgentDefaults = {
@@ -425,6 +449,10 @@ export async function restoreDefaultModelOnSessionStart(opts: {
425
449
  const currentProvider = current?.provider;
426
450
  const currentModelId = current?.id;
427
451
  const argv = opts.argv ?? process.argv;
452
+ // Normalize once: the decision gate trims the reason, so the mid-flight
453
+ // abort guard must compare the trimmed value too — a padded reason (e.g.
454
+ // " new ") must not split the two code paths.
455
+ const reason = typeof opts.reason === "string" ? opts.reason.trim() : "";
428
456
  const argvOverride = argvHasModelOrProviderOverride(argv);
429
457
  const enabledModelsScope = hasEnabledModelsScope(enabledModels);
430
458
 
@@ -439,7 +467,7 @@ export async function restoreDefaultModelOnSessionStart(opts: {
439
467
  })) {
440
468
  log.debug(
441
469
  "restore-default-model skip",
442
- `reason=${opts.reason ?? "?"} default=${defaultProvider ?? "?"}/${defaultModel ?? "?"} ` +
470
+ `reason=${reason || "?"} default=${defaultProvider ?? "?"}/${defaultModel ?? "?"} ` +
443
471
  `current=${currentProvider ?? "?"}/${currentModelId ?? "?"} ` +
444
472
  `argvOverride=${argvOverride} enabledModelsScope=${enabledModelsScope}` +
445
473
  (enabledModelsScope ? ` enabledModels=${enabledModels!.length}` : ""),
@@ -473,7 +501,7 @@ export async function restoreDefaultModelOnSessionStart(opts: {
473
501
  log.warn(
474
502
  "restore-default-model fail-fast",
475
503
  `registeredProviders lack ${defaultProvider} (count=${count}): ${label} ` +
476
- `(session_start reason=${opts.reason ?? "?"})`,
504
+ `(session_start reason=${reason || "?"})`,
477
505
  );
478
506
  return false;
479
507
  }
@@ -503,6 +531,18 @@ export async function restoreDefaultModelOnSessionStart(opts: {
503
531
  return true;
504
532
  }
505
533
  } else {
534
+ // Empty /new snapshot: any model that appeared is a selection (user or
535
+ // picker) — abort rather than overwrite it with the saved default.
536
+ // The baseline path below is only for startup/resume, where the first
537
+ // live model may be the race-recovered initial pick, not user intent.
538
+ if (reason === "new") {
539
+ log.debug(
540
+ "restore-default-model abort",
541
+ `empty /new session; live selection appeared ` +
542
+ `${live.provider}/${live.id} (${phase} attempt ${attempt}/${retries})`,
543
+ );
544
+ return true;
545
+ }
506
546
  // Empty snapshot: first non-default live model is baseline, not abort.
507
547
  // Only abort when live later *changes* from that mid-flight selection.
508
548
  if (!firstSeenLive) {
@@ -568,7 +608,7 @@ export async function restoreDefaultModelOnSessionStart(opts: {
568
608
  "restore-default-model",
569
609
  `restored ${label} ` +
570
610
  `(was ${currentProvider ?? "none"}/${currentModelId ?? "none"}; ` +
571
- `session_start reason=${opts.reason ?? "?"}; attempt ${attempt}/${retries})`,
611
+ `session_start reason=${reason || "?"}; attempt ${attempt}/${retries})`,
572
612
  );
573
613
  return true;
574
614
  }
@@ -590,7 +630,7 @@ export async function restoreDefaultModelOnSessionStart(opts: {
590
630
  log.warn(
591
631
  "restore-default-model exhausted",
592
632
  `${lastFailReason} after ${retries} attempts ` +
593
- `(session_start reason=${opts.reason ?? "?"})`,
633
+ `(session_start reason=${reason || "?"})`,
594
634
  );
595
635
  return false;
596
636
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-orchestrator-config",
3
- "version": "4.8.4",
3
+ "version": "4.8.5",
4
4
  "description": "Orchestrator pattern for pi: specialist subagents, enforcement, code review loop, workflow commands",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -48,7 +48,7 @@ mentally or separately.
48
48
 
49
49
  The review loop repeats for at most `review_loop_max_cycles` cycles (default `3`, range `1`-`10` only;
50
50
  current value: `{{REVIEW_LOOP_MAX_CYCLES}}`), including the first cycle. Invalid values fall through to the
51
- next resolution layer / default `3` — see `dev-docs/project-settings.md` for the full resolution order.
51
+ next resolution layer / default `3` — see `docs/project-settings.md` for the full resolution order.
52
52
  Disable the review loop via `review_loop_enforcement: false` — not via max_cycles.
53
53
 
54
54
  Cap check is after 5a; it only blocks `go to 2` (re-dispatch of step 2 / all 6 agents,
@@ -5,8 +5,8 @@ After ANY code is added, changed, or removed, check if documentation needs updat
5
5
  | Change | Check these files |
6
6
  |--------|-------------------|
7
7
  | New feature/command/tool | `README.md` (feature table, usage examples) |
8
- | New or modified extension module | `dev-docs/repo-structure.md` (repository structure) |
9
- | New agent added/removed | `dev-docs/repo-structure.md` (structure), `rules/10-agent-routing.md` (routing table), `rules/50-agent-bug-reporting.md` (agent list) |
8
+ | New or modified extension module | `contributing/repo-structure.md` (repository structure) |
9
+ | New agent added/removed | `contributing/repo-structure.md` (structure), `rules/10-agent-routing.md` (routing table), `rules/50-agent-bug-reporting.md` (agent list) |
10
10
  | New prompt template | `README.md` (prompt templates table) |
11
11
  | Docker/container changes | `README.md` (Docker section, shell alias, mounts), `Dockerfile` |
12
12
  | New CLI tool or dependency | `README.md` (tools table), `Dockerfile` |