gentle-pi 0.2.7 → 0.3.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.
@@ -14,6 +14,14 @@ import type {
14
14
  ToolCallEventResult,
15
15
  } from "@earendil-works/pi-coding-agent";
16
16
  import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
17
+ import {
18
+ ensureSddPreflight,
19
+ getSddPreflightPreferences,
20
+ installSddAssets,
21
+ isSddPreflightTrigger,
22
+ renderSddPreflightPrompt,
23
+ type SddPreflightPreferences,
24
+ } from "../lib/sdd-preflight.ts";
17
25
 
18
26
  const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
19
27
  const ASSETS_DIR = join(PACKAGE_ROOT, "assets");
@@ -101,7 +109,12 @@ const SDD_AGENT_NAMES = [
101
109
  ] as const;
102
110
 
103
111
  type SddAgentName = (typeof SDD_AGENT_NAMES)[number];
104
- type AgentModelConfig = Record<string, string>;
112
+ type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
113
+ interface AgentRoutingEntry {
114
+ model?: string;
115
+ thinking?: ThinkingLevel;
116
+ }
117
+ type AgentModelConfig = Record<string, AgentRoutingEntry>;
105
118
  type AgentSource = "project" | "user" | "builtin";
106
119
 
107
120
  interface AgentEntry {
@@ -113,6 +126,16 @@ interface AgentEntry {
113
126
  const KEEP_CURRENT = "Keep current";
114
127
  const INHERIT_MODEL = "Inherit active/default model";
115
128
  const CUSTOM_MODEL = "Custom model id";
129
+ const INHERIT_THINKING = "Inherit effort";
130
+ const THINKING_OPTIONS: (ThinkingLevel | typeof INHERIT_THINKING)[] = [
131
+ INHERIT_THINKING,
132
+ "off",
133
+ "minimal",
134
+ "low",
135
+ "medium",
136
+ "high",
137
+ "xhigh",
138
+ ];
116
139
 
117
140
  const MODEL_CONTROL_OPTIONS = [
118
141
  KEEP_CURRENT,
@@ -167,62 +190,6 @@ async function confirmCommand(
167
190
  };
168
191
  }
169
192
 
170
- function copyDirectoryFiles(
171
- sourceDir: string,
172
- targetDir: string,
173
- force: boolean,
174
- ): { copied: number; skipped: number } {
175
- if (!existsSync(sourceDir)) return { copied: 0, skipped: 0 };
176
- mkdirSync(targetDir, { recursive: true });
177
- let copied = 0;
178
- let skipped = 0;
179
- for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
180
- const sourcePath = join(sourceDir, entry.name);
181
- const targetPath = join(targetDir, entry.name);
182
- if (entry.isDirectory()) {
183
- const child = copyDirectoryFiles(sourcePath, targetPath, force);
184
- copied += child.copied;
185
- skipped += child.skipped;
186
- continue;
187
- }
188
- if (!entry.isFile()) continue;
189
- if (!force && existsSync(targetPath)) {
190
- skipped += 1;
191
- continue;
192
- }
193
- writeFileSync(targetPath, readFileSync(sourcePath));
194
- copied += 1;
195
- }
196
- return { copied, skipped };
197
- }
198
-
199
- function installSddAssets(
200
- cwd: string,
201
- force: boolean,
202
- ): { agents: number; chains: number; support: number; skipped: number } {
203
- const agents = copyDirectoryFiles(
204
- join(ASSETS_DIR, "agents"),
205
- join(cwd, ".pi", "agents"),
206
- force,
207
- );
208
- const chains = copyDirectoryFiles(
209
- join(ASSETS_DIR, "chains"),
210
- join(cwd, ".pi", "chains"),
211
- force,
212
- );
213
- const support = copyDirectoryFiles(
214
- join(ASSETS_DIR, "support"),
215
- join(cwd, ".pi", "gentle-ai", "support"),
216
- force,
217
- );
218
- return {
219
- agents: agents.copied,
220
- chains: chains.copied,
221
- support: support.copied,
222
- skipped: agents.skipped + chains.skipped + support.skipped,
223
- };
224
- }
225
-
226
193
  function isRecord(value: unknown): value is Record<string, unknown> {
227
194
  return typeof value === "object" && value !== null && !Array.isArray(value);
228
195
  }
@@ -253,7 +220,33 @@ function writePersonaMode(cwd: string, mode: PersonaMode): void {
253
220
  writeFileSync(path, `${JSON.stringify({ mode }, null, 2)}\n`);
254
221
  }
255
222
 
256
- function readModelConfig(cwd: string): AgentModelConfig {
223
+ function isThinkingLevel(value: unknown): value is ThinkingLevel {
224
+ return (
225
+ value === "off" ||
226
+ value === "minimal" ||
227
+ value === "low" ||
228
+ value === "medium" ||
229
+ value === "high" ||
230
+ value === "xhigh"
231
+ );
232
+ }
233
+
234
+ function normalizeRoutingEntry(value: unknown): AgentRoutingEntry | undefined {
235
+ if (typeof value === "string") {
236
+ const model = value.trim();
237
+ return model.length > 0 ? { model } : undefined;
238
+ }
239
+ if (!isRecord(value)) return undefined;
240
+ const model =
241
+ typeof value.model === "string" && value.model.trim().length > 0
242
+ ? value.model.trim()
243
+ : undefined;
244
+ const thinking = isThinkingLevel(value.thinking) ? value.thinking : undefined;
245
+ if (!model && !thinking) return undefined;
246
+ return { model, thinking };
247
+ }
248
+
249
+ export function readModelConfig(cwd: string): AgentModelConfig {
257
250
  const path = modelConfigPath(cwd);
258
251
  if (!existsSync(path)) return {};
259
252
  try {
@@ -261,9 +254,8 @@ function readModelConfig(cwd: string): AgentModelConfig {
261
254
  if (!isRecord(parsed)) return {};
262
255
  const config: AgentModelConfig = {};
263
256
  for (const [name, value] of Object.entries(parsed)) {
264
- if (typeof value === "string" && value.trim().length > 0) {
265
- config[name] = value.trim();
266
- }
257
+ const entry = normalizeRoutingEntry(value);
258
+ if (entry) config[name] = entry;
267
259
  }
268
260
  return config;
269
261
  } catch {
@@ -274,12 +266,23 @@ function readModelConfig(cwd: string): AgentModelConfig {
274
266
  function writeModelConfig(cwd: string, config: AgentModelConfig): void {
275
267
  const path = modelConfigPath(cwd);
276
268
  mkdirSync(dirname(path), { recursive: true });
277
- writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
269
+ const cleaned: AgentModelConfig = {};
270
+ for (const [name, value] of Object.entries(config)) {
271
+ const entry = normalizeRoutingEntry(value);
272
+ if (entry) cleaned[name] = entry;
273
+ }
274
+ writeFileSync(path, `${JSON.stringify(cleaned, null, 2)}\n`);
275
+ }
276
+
277
+ function cloneModelConfig(config: AgentModelConfig): AgentModelConfig {
278
+ return Object.fromEntries(
279
+ Object.entries(config).map(([name, entry]) => [name, { ...entry }]),
280
+ );
278
281
  }
279
282
 
280
- function updateFrontmatterModel(
283
+ function updateFrontmatterRouting(
281
284
  content: string,
282
- model: string | undefined,
285
+ entry: AgentRoutingEntry | undefined,
283
286
  ): string {
284
287
  if (!content.startsWith("---\n")) return content;
285
288
  const endIndex = content.indexOf("\n---", 4);
@@ -288,14 +291,19 @@ function updateFrontmatterModel(
288
291
  const body = content.slice(endIndex);
289
292
  const lines = frontmatter
290
293
  .split("\n")
291
- .filter((line) => !line.startsWith("model:"));
292
- if (model !== undefined) {
294
+ .filter(
295
+ (line) => !line.startsWith("model:") && !line.startsWith("thinking:"),
296
+ );
297
+ const toInsert: string[] = [];
298
+ if (entry?.model) toInsert.push(`model: ${entry.model}`);
299
+ if (entry?.thinking) toInsert.push(`thinking: ${entry.thinking}`);
300
+ if (toInsert.length > 0) {
293
301
  const descriptionIndex = lines.findIndex((line) =>
294
302
  line.startsWith("description:"),
295
303
  );
296
304
  const insertIndex =
297
305
  descriptionIndex >= 0 ? descriptionIndex + 1 : Math.min(1, lines.length);
298
- lines.splice(insertIndex, 0, `model: ${model}`);
306
+ lines.splice(insertIndex, 0, ...toInsert);
299
307
  }
300
308
  return `---\n${lines.join("\n")}${body}`;
301
309
  }
@@ -372,7 +380,7 @@ function projectSettingsPath(cwd: string): string {
372
380
  function updateBuiltinModelOverride(
373
381
  cwd: string,
374
382
  name: string,
375
- model: string | undefined,
383
+ entry: AgentRoutingEntry | undefined,
376
384
  ): boolean {
377
385
  const path = projectSettingsPath(cwd);
378
386
  let settings: Record<string, unknown> = {};
@@ -393,8 +401,10 @@ function updateBuiltinModelOverride(
393
401
  const current = isRecord(agentOverrides[name])
394
402
  ? { ...agentOverrides[name] }
395
403
  : {};
396
- if (model === undefined) delete current.model;
397
- else current.model = model;
404
+ if (entry?.model === undefined) delete current.model;
405
+ else current.model = entry.model;
406
+ if (entry?.thinking === undefined) delete current.thinking;
407
+ else current.thinking = entry.thinking;
398
408
  if (Object.keys(current).length > 0) agentOverrides[name] = current;
399
409
  else delete agentOverrides[name];
400
410
  if (Object.keys(agentOverrides).length > 0)
@@ -407,16 +417,16 @@ function updateBuiltinModelOverride(
407
417
  return true;
408
418
  }
409
419
 
410
- function applyModelConfig(
420
+ export function applyModelConfig(
411
421
  cwd: string,
412
422
  config: AgentModelConfig,
413
423
  ): { updated: number; skipped: number } {
414
424
  let updated = 0;
415
425
  let skipped = 0;
416
426
  for (const agent of listDiscoverableAgents(cwd)) {
417
- const model = config[agent.name];
427
+ const entry = config[agent.name];
418
428
  if (agent.source === "builtin") {
419
- if (updateBuiltinModelOverride(cwd, agent.name, model)) updated += 1;
429
+ if (updateBuiltinModelOverride(cwd, agent.name, entry)) updated += 1;
420
430
  else skipped += 1;
421
431
  continue;
422
432
  }
@@ -425,7 +435,7 @@ function applyModelConfig(
425
435
  continue;
426
436
  }
427
437
  const original = readFileSync(agent.filePath, "utf8");
428
- const next = updateFrontmatterModel(original, model);
438
+ const next = updateFrontmatterRouting(original, entry);
429
439
  if (next === original) {
430
440
  skipped += 1;
431
441
  continue;
@@ -437,9 +447,12 @@ function applyModelConfig(
437
447
  }
438
448
 
439
449
  function describeModelConfig(cwd: string, config: AgentModelConfig): string[] {
440
- return listDiscoverableAgents(cwd).map(
441
- (agent) => `${agent.name}: ${config[agent.name] ?? "inherit"}`,
442
- );
450
+ return listDiscoverableAgents(cwd).map((agent) => {
451
+ const entry = config[agent.name];
452
+ const model = entry?.model ?? "inherit";
453
+ const thinking = entry?.thinking ?? "inherit";
454
+ return `${agent.name}: model=${model}, effort=${thinking}`;
455
+ });
443
456
  }
444
457
 
445
458
  async function getPiModelOptions(ctx: ExtensionContext): Promise<string[]> {
@@ -458,16 +471,17 @@ interface OverlayComponent {
458
471
 
459
472
  type ModelPanelResult =
460
473
  | { type: "save"; config: AgentModelConfig }
461
- | { type: "custom"; agent: string | "all" }
474
+ | { type: "custom"; agent: string | "all"; config: AgentModelConfig }
462
475
  | { type: "cancel" };
463
476
 
464
477
  const SET_ALL_AGENTS = "Set all agents";
465
478
 
466
479
  class SddModelPanel implements OverlayComponent {
467
480
  private cursor = 0;
468
- private mode: "agents" | "models" = "agents";
481
+ private mode: "agents" | "models" | "effort" = "agents";
469
482
  private selectedRow = SET_ALL_AGENTS;
470
483
  private modelCursor = 0;
484
+ private effortCursor = 0;
471
485
  private query = "";
472
486
  private readonly draft: AgentModelConfig;
473
487
  private readonly rows: string[];
@@ -480,7 +494,7 @@ class SddModelPanel implements OverlayComponent {
480
494
  agents: string[],
481
495
  done: (result: ModelPanelResult) => void,
482
496
  ) {
483
- this.draft = { ...initialConfig };
497
+ this.draft = cloneModelConfig(initialConfig);
484
498
  this.rows = [SET_ALL_AGENTS, ...agents];
485
499
  this.modelOptions = modelOptions;
486
500
  this.done = done;
@@ -493,13 +507,17 @@ class SddModelPanel implements OverlayComponent {
493
507
  this.handleModelInput(data);
494
508
  return;
495
509
  }
510
+ if (this.mode === "effort") {
511
+ this.handleEffortInput(data);
512
+ return;
513
+ }
496
514
  this.handleAgentInput(data);
497
515
  }
498
516
 
499
517
  render(width: number): string[] {
500
- return this.mode === "models"
501
- ? this.renderModelPicker(width)
502
- : this.renderAgentList(width);
518
+ if (this.mode === "models") return this.renderModelPicker(width);
519
+ if (this.mode === "effort") return this.renderEffortPicker(width);
520
+ return this.renderAgentList(width);
503
521
  }
504
522
 
505
523
  private handleAgentInput(data: string): void {
@@ -521,13 +539,21 @@ class SddModelPanel implements OverlayComponent {
521
539
  return;
522
540
  }
523
541
  if (data === "i") {
524
- this.applySelection(undefined);
542
+ this.applyInherit();
543
+ return;
544
+ }
545
+ if (data === "e") {
546
+ this.selectedRow = this.rows[this.cursor] ?? SET_ALL_AGENTS;
547
+ this.mode = "effort";
548
+ this.effortCursor = 0;
525
549
  return;
526
550
  }
527
551
  if (data === "c") {
528
552
  const row = this.rows[this.cursor];
529
- if (row === SET_ALL_AGENTS) this.done({ type: "custom", agent: "all" });
530
- else if (row) this.done({ type: "custom", agent: row });
553
+ if (row === SET_ALL_AGENTS)
554
+ this.done({ type: "custom", agent: "all", config: this.draft });
555
+ else if (row)
556
+ this.done({ type: "custom", agent: row, config: this.draft });
531
557
  return;
532
558
  }
533
559
  if (!matchesKey(data, "return")) return;
@@ -582,6 +608,7 @@ class SddModelPanel implements OverlayComponent {
582
608
  this.done({
583
609
  type: "custom",
584
610
  agent: this.selectedRow === SET_ALL_AGENTS ? "all" : this.selectedRow,
611
+ config: this.draft,
585
612
  });
586
613
  return;
587
614
  }
@@ -589,7 +616,9 @@ class SddModelPanel implements OverlayComponent {
589
616
  this.mode = "agents";
590
617
  return;
591
618
  }
592
- this.applySelection(selected === INHERIT_MODEL ? undefined : selected);
619
+ this.applyModelSelection(
620
+ selected === INHERIT_MODEL ? undefined : selected,
621
+ );
593
622
  this.mode = "agents";
594
623
  return;
595
624
  }
@@ -599,18 +628,52 @@ class SddModelPanel implements OverlayComponent {
599
628
  }
600
629
  }
601
630
 
602
- private applySelection(model: string | undefined): void {
631
+ private applyModelSelection(model: string | undefined): void {
603
632
  const row = this.rows[this.cursor];
604
633
  if (row === SET_ALL_AGENTS) {
605
- for (const name of this.rows.slice(1)) {
606
- if (model === undefined) delete this.draft[name];
607
- else this.draft[name] = model;
608
- }
634
+ for (const name of this.rows.slice(1)) this.setModel(name, model);
609
635
  return;
610
636
  }
611
637
  if (!row) return;
612
- if (model === undefined) delete this.draft[row];
613
- else this.draft[row] = model;
638
+ this.setModel(row, model);
639
+ }
640
+
641
+ private applyThinkingSelection(thinking: ThinkingLevel | undefined): void {
642
+ const row = this.selectedRow;
643
+ if (row === SET_ALL_AGENTS) {
644
+ for (const name of this.rows.slice(1)) this.setThinking(name, thinking);
645
+ return;
646
+ }
647
+ this.setThinking(row, thinking);
648
+ }
649
+
650
+ private applyInherit(): void {
651
+ const row = this.rows[this.cursor];
652
+ if (row === SET_ALL_AGENTS) {
653
+ for (const name of this.rows.slice(1)) this.clearEntry(name);
654
+ return;
655
+ }
656
+ if (row) this.clearEntry(row);
657
+ }
658
+
659
+ private setModel(name: string, model: string | undefined): void {
660
+ const current = this.draft[name] ?? {};
661
+ if (model === undefined) delete current.model;
662
+ else current.model = model;
663
+ if (!current.model && !current.thinking) delete this.draft[name];
664
+ else this.draft[name] = current;
665
+ }
666
+
667
+ private setThinking(name: string, thinking: ThinkingLevel | undefined): void {
668
+ const current = this.draft[name] ?? {};
669
+ if (thinking === undefined) delete current.thinking;
670
+ else current.thinking = thinking;
671
+ if (!current.model && !current.thinking) delete this.draft[name];
672
+ else this.draft[name] = current;
673
+ }
674
+
675
+ private clearEntry(name: string): void {
676
+ delete this.draft[name];
614
677
  }
615
678
 
616
679
  private filteredModelOptions(): string[] {
@@ -648,7 +711,7 @@ class SddModelPanel implements OverlayComponent {
648
711
  lines.push("");
649
712
  lines.push(
650
713
  line(
651
- "j/k: navigate • enter: change model / confirm • i: inherit • c: custom • ctrl+s: save • esc: back",
714
+ "j/k: navigate • enter: change model / confirm • e: change effort • i: inherit all • c: custom model • ctrl+s: save • esc: back",
652
715
  ),
653
716
  );
654
717
  return lines;
@@ -684,17 +747,70 @@ class SddModelPanel implements OverlayComponent {
684
747
  return lines;
685
748
  }
686
749
 
750
+ private handleEffortInput(data: string): void {
751
+ if (matchesKey(data, "ctrl+c")) {
752
+ this.done({ type: "cancel" });
753
+ return;
754
+ }
755
+ if (matchesKey(data, "escape")) {
756
+ this.mode = "agents";
757
+ return;
758
+ }
759
+ if (matchesKey(data, "down") || data === "j") {
760
+ this.effortCursor = Math.min(
761
+ Math.max(0, THINKING_OPTIONS.length - 1),
762
+ this.effortCursor + 1,
763
+ );
764
+ return;
765
+ }
766
+ if (matchesKey(data, "up") || data === "k") {
767
+ this.effortCursor = Math.max(0, this.effortCursor - 1);
768
+ return;
769
+ }
770
+ if (!matchesKey(data, "return")) return;
771
+ const selected = THINKING_OPTIONS[this.effortCursor];
772
+ if (selected === INHERIT_THINKING) this.applyThinkingSelection(undefined);
773
+ else this.applyThinkingSelection(selected);
774
+ this.mode = "agents";
775
+ }
776
+
777
+ private renderEffortPicker(width: number): string[] {
778
+ const lines: string[] = [];
779
+ const line = (text = "") =>
780
+ truncateToWidth(text, Math.max(1, width), "…", true);
781
+ lines.push(line(`Select effort for ${this.selectedRow}`));
782
+ lines.push("");
783
+ for (let i = 0; i < THINKING_OPTIONS.length; i++) {
784
+ const focused = i === this.effortCursor;
785
+ lines.push(line(`${focused ? "▸" : " "} ${THINKING_OPTIONS[i]}`));
786
+ }
787
+ lines.push("");
788
+ lines.push(line("j/k: navigate • enter: select • esc: back"));
789
+ return lines;
790
+ }
791
+
687
792
  private renderSetAllLabel(row: string): string {
688
- const values = this.rows
793
+ const models = this.rows
689
794
  .slice(1)
690
- .map((name) => this.draft[name] ?? "inherit");
691
- const first = values[0] ?? "inherit";
692
- const allSame = values.every((value) => value === first);
693
- return `${row.padEnd(20)} ${allSame ? first : "mixed"}`;
795
+ .map((name) => this.draft[name]?.model ?? "inherit");
796
+ const efforts = this.rows
797
+ .slice(1)
798
+ .map((name) => this.draft[name]?.thinking ?? "inherit");
799
+ const firstModel = models[0] ?? "inherit";
800
+ const firstEffort = efforts[0] ?? "inherit";
801
+ const modelLabel = models.every((value) => value === firstModel)
802
+ ? firstModel
803
+ : "mixed";
804
+ const effortLabel = efforts.every((value) => value === firstEffort)
805
+ ? firstEffort
806
+ : "mixed";
807
+ return `${row.padEnd(20)} model=${modelLabel}, effort=${effortLabel}`;
694
808
  }
695
809
 
696
810
  private renderAgentLabel(row: string): string {
697
- return `${row.padEnd(20)} ${this.draft[row] ?? "inherit"}`;
811
+ const model = this.draft[row]?.model ?? "inherit";
812
+ const effort = this.draft[row]?.thinking ?? "inherit";
813
+ return `${row.padEnd(20)} model=${model}, effort=${effort}`;
698
814
  }
699
815
  }
700
816
 
@@ -723,8 +839,11 @@ async function handleModelsCommand(ctx: ExtensionContext): Promise<void> {
723
839
  let config = readModelConfig(ctx.cwd);
724
840
  let result = await showSddModelPanel(ctx, config);
725
841
  while (result.type === "custom") {
842
+ config = cloneModelConfig(result.config);
726
843
  const current =
727
- result.agent === "all" ? "inherit" : (config[result.agent] ?? "inherit");
844
+ result.agent === "all"
845
+ ? "inherit"
846
+ : (config[result.agent]?.model ?? "inherit");
728
847
  const custom = await ctx.ui.input(
729
848
  `${result.agent === "all" ? "all agents" : result.agent} custom model id`,
730
849
  current === "inherit" ? "provider/model" : current,
@@ -733,11 +852,22 @@ async function handleModelsCommand(ctx: ExtensionContext): Promise<void> {
733
852
  const trimmed = custom.trim();
734
853
  if (trimmed.length > 0) {
735
854
  if (result.agent === "all") {
736
- config = Object.fromEntries(
737
- listDiscoverableAgents(ctx.cwd).map((agent) => [agent.name, trimmed]),
738
- );
855
+ const next: AgentModelConfig = { ...config };
856
+ for (const agent of listDiscoverableAgents(ctx.cwd)) {
857
+ next[agent.name] = {
858
+ ...(next[agent.name] ?? {}),
859
+ model: trimmed,
860
+ };
861
+ }
862
+ config = next;
739
863
  } else {
740
- config = { ...config, [result.agent]: trimmed };
864
+ config = {
865
+ ...config,
866
+ [result.agent]: {
867
+ ...(config[result.agent] ?? {}),
868
+ model: trimmed,
869
+ },
870
+ };
741
871
  }
742
872
  }
743
873
  result = await showSddModelPanel(ctx, config);
@@ -775,18 +905,16 @@ async function handlePersonaCommand(ctx: ExtensionContext): Promise<void> {
775
905
  }
776
906
 
777
907
  export default function gentleAi(pi: ExtensionAPI): void {
908
+ function runSddPreflight(ctx: ExtensionContext): Promise<SddPreflightPreferences> {
909
+ return ensureSddPreflight(ctx, {
910
+ pi,
911
+ installAssets: (cwd) => installSddAssets(cwd, false),
912
+ applyModelConfig: (cwd) => applyModelConfig(cwd, readModelConfig(cwd)),
913
+ });
914
+ }
915
+
778
916
  pi.on("session_start", (_event, ctx) => {
779
- const result = installSddAssets(ctx.cwd, false);
780
917
  const modelResult = applyModelConfig(ctx.cwd, readModelConfig(ctx.cwd));
781
- if (
782
- ctx.hasUI &&
783
- (result.agents > 0 || result.chains > 0 || result.support > 0)
784
- ) {
785
- ctx.ui.notify(
786
- `Gentle AI SDD assets auto-installed: ${result.agents} agent(s), ${result.chains} chain(s), ${result.support} support file(s).`,
787
- "info",
788
- );
789
- }
790
918
  if (ctx.hasUI && modelResult.updated > 0) {
791
919
  ctx.ui.notify(
792
920
  `el Gentleman applied SDD model config to ${modelResult.updated} agent(s).`,
@@ -795,9 +923,21 @@ export default function gentleAi(pi: ExtensionAPI): void {
795
923
  }
796
924
  });
797
925
 
798
- pi.on("before_agent_start", (event, ctx) => ({
799
- systemPrompt: `${event.systemPrompt}\n\n${buildGentlePrompt(readPersonaMode(ctx.cwd))}`,
800
- }));
926
+ pi.on("input", async (event, ctx) => {
927
+ if (typeof event.text !== "string" || !isSddPreflightTrigger(event.text)) {
928
+ return { action: "continue" };
929
+ }
930
+ await runSddPreflight(ctx);
931
+ return { action: "continue" };
932
+ });
933
+
934
+ pi.on("before_agent_start", (event, ctx) => {
935
+ const prefs = getSddPreflightPreferences(ctx);
936
+ const sddPrompt = prefs ? `\n\n${renderSddPreflightPrompt(prefs)}` : "";
937
+ return {
938
+ systemPrompt: `${event.systemPrompt}\n\n${buildGentlePrompt(readPersonaMode(ctx.cwd))}${sddPrompt}`,
939
+ };
940
+ });
801
941
 
802
942
  pi.on("tool_call", async (event, ctx) => {
803
943
  if (event.toolName !== "bash") return undefined;
@@ -819,6 +959,21 @@ export default function gentleAi(pi: ExtensionAPI): void {
819
959
  },
820
960
  });
821
961
 
962
+ pi.registerCommand("gentle-ai:sdd-preflight", {
963
+ description:
964
+ "Run or reuse the lazy SDD preflight for this Pi session.",
965
+ handler: async (_args, ctx) => {
966
+ await runSddPreflight(ctx);
967
+ },
968
+ });
969
+
970
+ pi.registerCommand("gentle:sdd-preflight", {
971
+ description: "Compatibility alias for /gentle-ai:sdd-preflight.",
972
+ handler: async (_args, ctx) => {
973
+ await runSddPreflight(ctx);
974
+ },
975
+ });
976
+
822
977
  pi.registerCommand("gentle:models", {
823
978
  description: "Configure per-agent models for el Gentleman.",
824
979
  handler: async (_args, ctx) => {
@@ -6,6 +6,8 @@ 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";
10
+ import { ensureSddPreflight, installSddAssets } from "../lib/sdd-preflight.ts";
9
11
  type ExtensionAPI = any;
10
12
 
11
13
  const CONFIG_REL_PATH = "openspec/config.yaml";
@@ -773,6 +775,11 @@ export default function (pi: ExtensionAPI) {
773
775
  description:
774
776
  "Auto-detect project stack and bootstrap openspec/config.yaml for SDD.",
775
777
  handler: async (_args: unknown, ctx: any) => {
778
+ await ensureSddPreflight(ctx, {
779
+ pi,
780
+ installAssets: (cwd) => installSddAssets(cwd, false),
781
+ applyModelConfig: (cwd) => applyModelConfig(cwd, readModelConfig(cwd)),
782
+ });
776
783
  const configPath = join(ctx.cwd, CONFIG_REL_PATH);
777
784
  if (existsSync(configPath)) {
778
785
  ctx.ui.notify(