gentle-pi 0.3.0 → 0.3.2

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
@@ -49,6 +49,7 @@ Most coding-agent sessions fail for operational reasons, not model reasons:
49
49
  | **Rose startup intro** | Adds a pink rose fade-in, compact project/runtime panel, and visible startup collaboration credit for @aporcelli's `pi-gentle-startup` ideas. |
50
50
  | **Work routing discipline** | Small tasks stay inline. Context-heavy exploration can be delegated. Large or risky changes go through SDD/OpenSpec. |
51
51
  | **SDD/OpenSpec assets** | Installs phase agents and chains for `init`, `explore`, `proposal`, `spec`, `design`, `tasks`, `apply`, `verify`, and `archive`. |
52
+ | **Lazy SDD preflight** | Asks once per session for SDD mode, artifact store, PR chaining strategy, and review budget before the first SDD flow. |
52
53
  | **Subagent orchestration** | Keeps one parent session responsible while child agents explore, implement, test, or review with focused context. |
53
54
  | **Strict TDD support** | When project config declares a test command, apply/verify phases must record RED → GREEN → TRIANGULATE → REFACTOR evidence. |
54
55
  | **Reviewer protection** | Surfaces review workload risk before a task turns into an oversized PR. |
@@ -81,23 +82,24 @@ Then start Pi in a project:
81
82
  pi
82
83
  ```
83
84
 
84
- `gentle-pi` waits until the first SDD request in a session, then runs a one-time SDD preflight and installs local SDD assets without overwriting your edits.
85
+ `gentle-pi` waits until the first SDD flow in a session, then runs a one-time SDD preflight and installs local SDD assets without overwriting your edits. Slash SDD commands trigger this automatically; for natural-language requests, el Gentleman decides when SDD is needed and runs the explicit preflight first.
85
86
 
86
87
  ## Quick start
87
88
 
88
89
  ```text
89
- /gentle-ai:status Check package, SDD assets, OpenSpec, and model config.
90
- /sdd-init Create or refresh openspec/config.yaml.
91
- /gentle:models Assign models to SDD/custom agents.
92
- /gentle:persona Switch between gentleman and neutral persona modes.
90
+ /gentle-ai:status Check package, SDD assets, OpenSpec, and global model config.
91
+ /gentle-ai:sdd-preflight Run or reuse the session SDD preflight explicitly.
92
+ /sdd-init Create or refresh openspec/config.yaml.
93
+ /gentle:models Assign global model/effort routing to SDD/custom agents.
94
+ /gentle:persona Switch between gentleman and neutral persona modes.
93
95
  ```
94
96
 
95
97
  Typical flow:
96
98
 
97
99
  1. Open Pi in your repo.
98
100
  2. Run `/gentle-ai:status`.
99
- 3. Run `/sdd-init` once per project, or when test/project capabilities change.
100
- 4. For a substantial change, ask Pi to use SDD.
101
+ 3. Run `/sdd-init` once per project, or when test/project capabilities change. This also runs the session SDD preflight.
102
+ 4. For a substantial change, ask Pi to use SDD. Natural-language requests are classified by the parent agent, not by brittle runtime regexes.
101
103
  5. Review the phase artifacts instead of trusting floating chat context.
102
104
 
103
105
  ## How the harness decides what to do
@@ -282,12 +284,14 @@ Recommended model/effort shape:
282
284
  | Verify / review | Strong fresh-context model. | `high` |
283
285
  | Tiny utilities | Inherit active/default model unless they bottleneck. | `inherit` |
284
286
 
285
- Saved at:
287
+ Saved globally at:
286
288
 
287
289
  ```text
288
- .pi/gentle-ai/models.json
290
+ ~/.pi/gentle-ai/models.json
289
291
  ```
290
292
 
293
+ Existing project-local `.pi/gentle-ai/models.json` files are still read as a legacy fallback when no global model config exists, but `/gentle:models` writes the shared global config.
294
+
291
295
  Config shape (per agent):
292
296
 
293
297
  ```json
@@ -306,15 +310,15 @@ Legacy string entries are still accepted and treated as `model`-only config.
306
310
 
307
311
  ## Commands
308
312
 
309
- | Command | What it does |
310
- | -------------------------------- | ------------------------------------------------------------ |
311
- | `/gentle-ai:status` | Shows package, SDD asset, OpenSpec, and model config status. |
312
- | `/gentle:models` | Opens model + effort assignment UI. |
313
- | `/gentle:persona` | Switches persona mode. |
314
- | `/sdd-init` | Initializes or refreshes `openspec/config.yaml`. |
315
- | `/gentle-ai:install-sdd` | Reinstalls SDD assets without overwriting local files. |
316
- | `/gentle-ai:install-sdd --force` | Force-refreshes installed SDD assets. |
317
- | `/skill-registry:refresh` | Regenerates `.atl/skill-registry.md`. |
313
+ | Command | What it does |
314
+ | -------------------------------- | ------------------------------------------------------------------- |
315
+ | `/gentle-ai:status` | Shows package, SDD asset, OpenSpec, and global model config status. |
316
+ | `/gentle:models` | Opens global model + effort assignment UI. |
317
+ | `/gentle:persona` | Switches persona mode. |
318
+ | `/sdd-init` | Initializes or refreshes `openspec/config.yaml`. |
319
+ | `/gentle-ai:install-sdd` | Reinstalls SDD assets without overwriting local files. |
320
+ | `/gentle-ai:install-sdd --force` | Force-refreshes installed SDD assets. |
321
+ | `/skill-registry:refresh` | Regenerates `.atl/skill-registry.md`. |
318
322
 
319
323
  Startup flag:
320
324
 
@@ -5,6 +5,13 @@ import {
5
5
  readFileSync,
6
6
  writeFileSync,
7
7
  } from "node:fs";
8
+ import {
9
+ access,
10
+ mkdir,
11
+ readFile,
12
+ readdir,
13
+ writeFile,
14
+ } from "node:fs/promises";
8
15
  import { homedir } from "node:os";
9
16
  import { dirname, join } from "node:path";
10
17
  import { fileURLToPath } from "node:url";
@@ -25,10 +32,26 @@ import {
25
32
 
26
33
  const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
27
34
  const ASSETS_DIR = join(PACKAGE_ROOT, "assets");
28
- const ORCHESTRATOR_PROMPT = readFileSync(
29
- join(ASSETS_DIR, "orchestrator.md"),
30
- "utf8",
31
- ).trim();
35
+
36
+ let orchestratorPromptCache: string | null = null;
37
+ function getOrchestratorPrompt(): string {
38
+ if (orchestratorPromptCache === null) {
39
+ orchestratorPromptCache = readFileSync(
40
+ join(ASSETS_DIR, "orchestrator.md"),
41
+ "utf8",
42
+ ).trim();
43
+ }
44
+ return orchestratorPromptCache;
45
+ }
46
+
47
+ async function pathExists(path: string): Promise<boolean> {
48
+ try {
49
+ await access(path);
50
+ return true;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
32
55
 
33
56
  type PersonaMode = "gentleman" | "neutral";
34
57
 
@@ -76,7 +99,7 @@ Harness principles:
76
99
  - Protect the human reviewer: avoid oversized changes, surface review workload risk, and ask before turning one task into a large multi-area change.
77
100
  - Never claim persistent memory is available because of this package. Memory is provided by separate packages or MCP tools when installed and callable.
78
101
 
79
- ${ORCHESTRATOR_PROMPT}`;
102
+ ${getOrchestratorPrompt()}`;
80
103
  }
81
104
 
82
105
  const DENIED_BASH_PATTERNS: RegExp[] = [
@@ -115,6 +138,10 @@ interface AgentRoutingEntry {
115
138
  thinking?: ThinkingLevel;
116
139
  }
117
140
  type AgentModelConfig = Record<string, AgentRoutingEntry>;
141
+ type ModelConfigFileResult =
142
+ | { status: "missing" }
143
+ | { status: "invalid"; path: string }
144
+ | { status: "valid"; config: AgentModelConfig };
118
145
  type AgentSource = "project" | "user" | "builtin";
119
146
 
120
147
  interface AgentEntry {
@@ -194,7 +221,15 @@ function isRecord(value: unknown): value is Record<string, unknown> {
194
221
  return typeof value === "object" && value !== null && !Array.isArray(value);
195
222
  }
196
223
 
197
- function modelConfigPath(cwd: string): string {
224
+ function gentleAiConfigHome(): string {
225
+ return process.env.GENTLE_PI_CONFIG_HOME ?? join(homedir(), ".pi", "gentle-ai");
226
+ }
227
+
228
+ function modelConfigPath(_cwd: string): string {
229
+ return join(gentleAiConfigHome(), "models.json");
230
+ }
231
+
232
+ function legacyProjectModelConfigPath(cwd: string): string {
198
233
  return join(cwd, ".pi", "gentle-ai", "models.json");
199
234
  }
200
235
 
@@ -246,23 +281,72 @@ function normalizeRoutingEntry(value: unknown): AgentRoutingEntry | undefined {
246
281
  return { model, thinking };
247
282
  }
248
283
 
249
- export function readModelConfig(cwd: string): AgentModelConfig {
250
- const path = modelConfigPath(cwd);
251
- if (!existsSync(path)) return {};
284
+ function readModelConfigFile(path: string): ModelConfigFileResult {
285
+ if (!existsSync(path)) return { status: "missing" };
252
286
  try {
253
287
  const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
254
- if (!isRecord(parsed)) return {};
288
+ if (!isRecord(parsed)) return { status: "invalid", path };
289
+ const config: AgentModelConfig = {};
290
+ for (const [name, value] of Object.entries(parsed)) {
291
+ const entry = normalizeRoutingEntry(value);
292
+ if (entry) config[name] = entry;
293
+ }
294
+ return { status: "valid", config };
295
+ } catch {
296
+ return { status: "invalid", path };
297
+ }
298
+ }
299
+
300
+ async function readModelConfigFileAsync(
301
+ path: string,
302
+ ): Promise<ModelConfigFileResult> {
303
+ if (!(await pathExists(path))) return { status: "missing" };
304
+ try {
305
+ const parsed: unknown = JSON.parse(await readFile(path, "utf8"));
306
+ if (!isRecord(parsed)) return { status: "invalid", path };
255
307
  const config: AgentModelConfig = {};
256
308
  for (const [name, value] of Object.entries(parsed)) {
257
309
  const entry = normalizeRoutingEntry(value);
258
310
  if (entry) config[name] = entry;
259
311
  }
260
- return config;
312
+ return { status: "valid", config };
261
313
  } catch {
262
- return {};
314
+ return { status: "invalid", path };
263
315
  }
264
316
  }
265
317
 
318
+ function readSavedModelConfig(cwd: string): ModelConfigFileResult {
319
+ const globalResult = readModelConfigFile(modelConfigPath(cwd));
320
+ if (globalResult.status !== "missing") return globalResult;
321
+ const legacyResult = readModelConfigFile(legacyProjectModelConfigPath(cwd));
322
+ if (legacyResult.status === "invalid") return { status: "valid", config: {} };
323
+ return legacyResult;
324
+ }
325
+
326
+ async function readSavedModelConfigAsync(
327
+ cwd: string,
328
+ ): Promise<ModelConfigFileResult> {
329
+ const globalResult = await readModelConfigFileAsync(modelConfigPath(cwd));
330
+ if (globalResult.status !== "missing") return globalResult;
331
+ const legacyResult = await readModelConfigFileAsync(
332
+ legacyProjectModelConfigPath(cwd),
333
+ );
334
+ if (legacyResult.status === "invalid") return { status: "valid", config: {} };
335
+ return legacyResult;
336
+ }
337
+
338
+ export function readModelConfig(cwd: string): AgentModelConfig {
339
+ const result = readSavedModelConfig(cwd);
340
+ return result.status === "valid" ? result.config : {};
341
+ }
342
+
343
+ export async function readModelConfigAsync(
344
+ cwd: string,
345
+ ): Promise<AgentModelConfig> {
346
+ const result = await readSavedModelConfigAsync(cwd);
347
+ return result.status === "valid" ? result.config : {};
348
+ }
349
+
266
350
  function writeModelConfig(cwd: string, config: AgentModelConfig): void {
267
351
  const path = modelConfigPath(cwd);
268
352
  mkdirSync(dirname(path), { recursive: true });
@@ -323,6 +407,23 @@ function parseAgentName(filePath: string): string | undefined {
323
407
  return packageName ? `${packageName}.${name}` : name;
324
408
  }
325
409
 
410
+ async function parseAgentNameAsync(
411
+ filePath: string,
412
+ ): Promise<string | undefined> {
413
+ let content: string;
414
+ try {
415
+ content = await readFile(filePath, "utf8");
416
+ } catch {
417
+ return undefined;
418
+ }
419
+ const name = content.match(/^name:\s*["']?([^"'\n]+)["']?\s*$/m)?.[1]?.trim();
420
+ if (!name) return undefined;
421
+ const packageName = content
422
+ .match(/^package:\s*["']?([^"'\n]+)["']?\s*$/m)?.[1]
423
+ ?.trim();
424
+ return packageName ? `${packageName}.${name}` : name;
425
+ }
426
+
326
427
  function listAgentFilesRecursive(dir: string): string[] {
327
428
  if (!existsSync(dir)) return [];
328
429
  const files: string[] = [];
@@ -339,6 +440,30 @@ function listAgentFilesRecursive(dir: string): string[] {
339
440
  return files;
340
441
  }
341
442
 
443
+ async function listAgentFilesRecursiveAsync(dir: string): Promise<string[]> {
444
+ if (!(await pathExists(dir))) return [];
445
+ const files: string[] = [];
446
+ let entries;
447
+ try {
448
+ entries = await readdir(dir, { withFileTypes: true });
449
+ } catch {
450
+ return files;
451
+ }
452
+ for (const entry of entries) {
453
+ const path = join(dir, entry.name);
454
+ if (entry.isDirectory()) {
455
+ files.push(...(await listAgentFilesRecursiveAsync(path)));
456
+ } else if (
457
+ entry.isFile() &&
458
+ entry.name.endsWith(".md") &&
459
+ !entry.name.endsWith(".chain.md")
460
+ ) {
461
+ files.push(path);
462
+ }
463
+ }
464
+ return files;
465
+ }
466
+
342
467
  function listAgentsFromDir(dir: string, source: AgentSource): AgentEntry[] {
343
468
  return listAgentFilesRecursive(dir)
344
469
  .map((filePath): AgentEntry | undefined => {
@@ -348,6 +473,19 @@ function listAgentsFromDir(dir: string, source: AgentSource): AgentEntry[] {
348
473
  .filter((entry): entry is AgentEntry => entry !== undefined);
349
474
  }
350
475
 
476
+ async function listAgentsFromDirAsync(
477
+ dir: string,
478
+ source: AgentSource,
479
+ ): Promise<AgentEntry[]> {
480
+ const filePaths = await listAgentFilesRecursiveAsync(dir);
481
+ const entries: AgentEntry[] = [];
482
+ for (const filePath of filePaths) {
483
+ const name = await parseAgentNameAsync(filePath);
484
+ if (name) entries.push({ name, source, filePath });
485
+ }
486
+ return entries;
487
+ }
488
+
351
489
  function listDiscoverableAgents(cwd: string): AgentEntry[] {
352
490
  const builtinDirs = [
353
491
  join(PACKAGE_ROOT, "..", "pi-subagents", "agents"),
@@ -373,6 +511,37 @@ function listDiscoverableAgents(cwd: string): AgentEntry[] {
373
511
  return [...sddFirst, ...rest];
374
512
  }
375
513
 
514
+ async function listDiscoverableAgentsAsync(cwd: string): Promise<AgentEntry[]> {
515
+ const builtinDirs = [
516
+ join(PACKAGE_ROOT, "..", "pi-subagents", "agents"),
517
+ join(cwd, ".pi", "npm", "node_modules", "pi-subagents", "agents"),
518
+ join(homedir(), ".local", "lib", "node_modules", "pi-subagents", "agents"),
519
+ ];
520
+ const agents: AgentEntry[] = [];
521
+ for (const dir of builtinDirs) {
522
+ agents.push(...(await listAgentsFromDirAsync(dir, "builtin")));
523
+ }
524
+ const otherDirs: Array<[string, AgentSource]> = [
525
+ [join(homedir(), ".pi", "agent", "agents"), "user"],
526
+ [join(homedir(), ".agents"), "user"],
527
+ [join(cwd, ".agents"), "project"],
528
+ [join(cwd, ".pi", "agents"), "project"],
529
+ ];
530
+ for (const [dir, source] of otherDirs) {
531
+ agents.push(...(await listAgentsFromDirAsync(dir, source)));
532
+ }
533
+ const byName = new Map<string, AgentEntry>();
534
+ for (const agent of agents) byName.set(agent.name, agent);
535
+ const discovered = Array.from(byName.values());
536
+ const sddFirst = SDD_AGENT_NAMES.map((name) =>
537
+ discovered.find((agent) => agent.name === name),
538
+ ).filter((agent): agent is AgentEntry => agent !== undefined);
539
+ const rest = discovered
540
+ .filter((agent) => !SDD_AGENT_NAMES.includes(agent.name as SddAgentName))
541
+ .sort((left, right) => left.name.localeCompare(right.name));
542
+ return [...sddFirst, ...rest];
543
+ }
544
+
376
545
  function projectSettingsPath(cwd: string): string {
377
546
  return join(cwd, ".pi", "settings.json");
378
547
  }
@@ -417,6 +586,46 @@ function updateBuiltinModelOverride(
417
586
  return true;
418
587
  }
419
588
 
589
+ async function updateBuiltinModelOverrideAsync(
590
+ cwd: string,
591
+ name: string,
592
+ entry: AgentRoutingEntry | undefined,
593
+ ): Promise<boolean> {
594
+ const path = projectSettingsPath(cwd);
595
+ let settings: Record<string, unknown> = {};
596
+ if (await pathExists(path)) {
597
+ try {
598
+ const parsed: unknown = JSON.parse(await readFile(path, "utf8"));
599
+ if (isRecord(parsed)) settings = parsed;
600
+ } catch {
601
+ settings = {};
602
+ }
603
+ }
604
+ const subagents = isRecord(settings.subagents)
605
+ ? { ...settings.subagents }
606
+ : {};
607
+ const agentOverrides = isRecord(subagents.agentOverrides)
608
+ ? { ...subagents.agentOverrides }
609
+ : {};
610
+ const current = isRecord(agentOverrides[name])
611
+ ? { ...agentOverrides[name] }
612
+ : {};
613
+ if (entry?.model === undefined) delete current.model;
614
+ else current.model = entry.model;
615
+ if (entry?.thinking === undefined) delete current.thinking;
616
+ else current.thinking = entry.thinking;
617
+ if (Object.keys(current).length > 0) agentOverrides[name] = current;
618
+ else delete agentOverrides[name];
619
+ if (Object.keys(agentOverrides).length > 0)
620
+ subagents.agentOverrides = agentOverrides;
621
+ else delete subagents.agentOverrides;
622
+ if (Object.keys(subagents).length > 0) settings.subagents = subagents;
623
+ else delete settings.subagents;
624
+ await mkdir(dirname(path), { recursive: true });
625
+ await writeFile(path, `${JSON.stringify(settings, null, "\t")}\n`);
626
+ return true;
627
+ }
628
+
420
629
  export function applyModelConfig(
421
630
  cwd: string,
422
631
  config: AgentModelConfig,
@@ -446,6 +655,49 @@ export function applyModelConfig(
446
655
  return { updated, skipped };
447
656
  }
448
657
 
658
+ export async function applyModelConfigAsync(
659
+ cwd: string,
660
+ config: AgentModelConfig,
661
+ ): Promise<{ updated: number; skipped: number }> {
662
+ let updated = 0;
663
+ let skipped = 0;
664
+ for (const agent of await listDiscoverableAgentsAsync(cwd)) {
665
+ const entry = config[agent.name];
666
+ if (agent.source === "builtin") {
667
+ if (await updateBuiltinModelOverrideAsync(cwd, agent.name, entry))
668
+ updated += 1;
669
+ else skipped += 1;
670
+ continue;
671
+ }
672
+ if (!agent.filePath || !(await pathExists(agent.filePath))) {
673
+ skipped += 1;
674
+ continue;
675
+ }
676
+ const original = await readFile(agent.filePath, "utf8");
677
+ const next = updateFrontmatterRouting(original, entry);
678
+ if (next === original) {
679
+ skipped += 1;
680
+ continue;
681
+ }
682
+ await writeFile(agent.filePath, next);
683
+ updated += 1;
684
+ }
685
+ return { updated, skipped };
686
+ }
687
+
688
+ export async function applySavedModelConfig(
689
+ ctx: ExtensionContext,
690
+ ): Promise<{ updated: number; skipped: number; invalidPath?: string }> {
691
+ const result = await readSavedModelConfigAsync(ctx.cwd);
692
+ if (result.status === "invalid") {
693
+ return { updated: 0, skipped: 0, invalidPath: result.path };
694
+ }
695
+ return applyModelConfigAsync(
696
+ ctx.cwd,
697
+ result.status === "valid" ? result.config : {},
698
+ );
699
+ }
700
+
449
701
  function describeModelConfig(cwd: string, config: AgentModelConfig): string[] {
450
702
  return listDiscoverableAgents(cwd).map((agent) => {
451
703
  const entry = config[agent.name];
@@ -836,7 +1088,15 @@ async function showSddModelPanel(
836
1088
  }
837
1089
 
838
1090
  async function handleModelsCommand(ctx: ExtensionContext): Promise<void> {
839
- let config = readModelConfig(ctx.cwd);
1091
+ const savedConfig = await readSavedModelConfigAsync(ctx.cwd);
1092
+ if (savedConfig.status === "invalid") {
1093
+ ctx.ui.notify(
1094
+ `el Gentleman cannot open model config because ${savedConfig.path} is invalid JSON or not an object. Fix or remove the file, then run /gentle:models again.`,
1095
+ "warning",
1096
+ );
1097
+ return;
1098
+ }
1099
+ let config = savedConfig.status === "valid" ? savedConfig.config : {};
840
1100
  let result = await showSddModelPanel(ctx, config);
841
1101
  while (result.type === "custom") {
842
1102
  config = cloneModelConfig(result.config);
@@ -874,11 +1134,11 @@ async function handleModelsCommand(ctx: ExtensionContext): Promise<void> {
874
1134
  }
875
1135
  if (result.type !== "save") return;
876
1136
  writeModelConfig(ctx.cwd, result.config);
877
- const applyResult = applyModelConfig(ctx.cwd, result.config);
1137
+ const applyResult = await applyModelConfigAsync(ctx.cwd, result.config);
878
1138
  ctx.ui.notify(
879
1139
  [
880
- "el Gentleman model config saved.",
881
- `Config: ${modelConfigPath(ctx.cwd)}`,
1140
+ "el Gentleman global model config saved.",
1141
+ `Global config: ${modelConfigPath(ctx.cwd)}`,
882
1142
  `Agents updated: ${applyResult.updated}`,
883
1143
  ...describeModelConfig(ctx.cwd, result.config),
884
1144
  ].join("\n"),
@@ -909,17 +1169,35 @@ export default function gentleAi(pi: ExtensionAPI): void {
909
1169
  return ensureSddPreflight(ctx, {
910
1170
  pi,
911
1171
  installAssets: (cwd) => installSddAssets(cwd, false),
912
- applyModelConfig: (cwd) => applyModelConfig(cwd, readModelConfig(cwd)),
1172
+ applyModelConfig: async () => applySavedModelConfig(ctx),
913
1173
  });
914
1174
  }
915
1175
 
916
- pi.on("session_start", (_event, ctx) => {
917
- const modelResult = applyModelConfig(ctx.cwd, readModelConfig(ctx.cwd));
918
- if (ctx.hasUI && modelResult.updated > 0) {
919
- ctx.ui.notify(
920
- `el Gentleman applied SDD model config to ${modelResult.updated} agent(s).`,
921
- "info",
922
- );
1176
+ pi.on("session_start", async (_event, ctx) => {
1177
+ try {
1178
+ const modelResult = await applySavedModelConfig(ctx);
1179
+ if (ctx.hasUI && modelResult.invalidPath) {
1180
+ ctx.ui.notify(
1181
+ `el Gentleman skipped model config because ${modelResult.invalidPath} is invalid JSON or not an object. Fix or remove the file, then run /gentle:models again.`,
1182
+ "warning",
1183
+ );
1184
+ return;
1185
+ }
1186
+ if (ctx.hasUI && modelResult.updated > 0) {
1187
+ ctx.ui.notify(
1188
+ `el Gentleman applied SDD model config to ${modelResult.updated} agent(s).`,
1189
+ "info",
1190
+ );
1191
+ }
1192
+ } catch (error) {
1193
+ if (ctx.hasUI) {
1194
+ const message =
1195
+ error instanceof Error ? error.message : String(error);
1196
+ ctx.ui.notify(
1197
+ `el Gentleman model config sweep failed: ${message}`,
1198
+ "warning",
1199
+ );
1200
+ }
923
1201
  }
924
1202
  });
925
1203
 
@@ -975,7 +1253,7 @@ export default function gentleAi(pi: ExtensionAPI): void {
975
1253
  });
976
1254
 
977
1255
  pi.registerCommand("gentle:models", {
978
- description: "Configure per-agent models for el Gentleman.",
1256
+ description: "Configure global per-agent models for el Gentleman.",
979
1257
  handler: async (_args, ctx) => {
980
1258
  await handleModelsCommand(ctx);
981
1259
  },
@@ -1028,7 +1306,7 @@ export default function gentleAi(pi: ExtensionAPI): void {
1028
1306
  const openspecConfigured = existsSync(
1029
1307
  join(ctx.cwd, "openspec", "config.yaml"),
1030
1308
  );
1031
- const modelConfig = readModelConfig(ctx.cwd);
1309
+ const modelConfig = await readModelConfigAsync(ctx.cwd);
1032
1310
  ctx.ui.notify(
1033
1311
  [
1034
1312
  "el Gentleman package is active.",
@@ -1036,7 +1314,7 @@ export default function gentleAi(pi: ExtensionAPI): void {
1036
1314
  `SDD agents: ${agentsInstalled ? "installed" : "not installed"}`,
1037
1315
  `SDD chains: ${chainsInstalled ? "installed" : "not installed"}`,
1038
1316
  `OpenSpec config: ${openspecConfigured ? "present" : "missing"}`,
1039
- `Model config: ${existsSync(modelConfigPath(ctx.cwd)) ? "present" : "missing"}`,
1317
+ `Global model config: ${existsSync(modelConfigPath(ctx.cwd)) ? "present" : "missing"}`,
1040
1318
  ...describeModelConfig(ctx.cwd, modelConfig),
1041
1319
  ].join("\n"),
1042
1320
  "info",
@@ -6,7 +6,7 @@ import {
6
6
  writeFileSync,
7
7
  } from "node:fs";
8
8
  import { basename, dirname, join, relative } from "node:path";
9
- import { applyModelConfig, readModelConfig } from "./gentle-ai.ts";
9
+ import { applySavedModelConfig } from "./gentle-ai.ts";
10
10
  import { ensureSddPreflight, installSddAssets } from "../lib/sdd-preflight.ts";
11
11
  type ExtensionAPI = any;
12
12
 
@@ -778,7 +778,7 @@ export default function (pi: ExtensionAPI) {
778
778
  await ensureSddPreflight(ctx, {
779
779
  pi,
780
780
  installAssets: (cwd) => installSddAssets(cwd, false),
781
- applyModelConfig: (cwd) => applyModelConfig(cwd, readModelConfig(cwd)),
781
+ applyModelConfig: () => applySavedModelConfig(ctx),
782
782
  });
783
783
  const configPath = join(ctx.cwd, CONFIG_REL_PATH);
784
784
  if (existsSync(configPath)) {