gentle-pi 0.2.6 → 0.2.8
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 +35 -10
- package/assets/orchestrator.md +2 -0
- package/extensions/gentle-ai.ts +232 -50
- package/extensions/skill-registry.ts +30 -0
- package/package.json +3 -2
- package/tests/runtime-harness.mjs +292 -0
- package/tests/skill-registry.test.ts +14 -0
package/README.md
CHANGED
|
@@ -213,6 +213,7 @@ Behavior:
|
|
|
213
213
|
|
|
214
214
|
- `.atl/` is added to `.gitignore` when needed;
|
|
215
215
|
- the registry refreshes on session start;
|
|
216
|
+
- startup refresh is skipped when Pi starts with `--no-skills` / `-ns`, `--no-skill-registry`, or `GENTLE_PI_NO_SKILL_REGISTRY=1`;
|
|
216
217
|
- `/skill-registry:refresh` forces regeneration;
|
|
217
218
|
- a best-effort watcher refreshes when skill files change;
|
|
218
219
|
- `## Compact Rules` wins when present; otherwise the registry extracts compact rules from `## Hard Rules`, `## Critical Rules`, `## Critical Patterns`, `## Voice Rules`, and `## Decision Gates` using bullets, numbered lists, or simple tables.
|
|
@@ -245,7 +246,7 @@ Saved at:
|
|
|
245
246
|
|
|
246
247
|
Run `/reload` or start a new Pi session after switching persona.
|
|
247
248
|
|
|
248
|
-
## Model assignment
|
|
249
|
+
## Model and effort assignment
|
|
249
250
|
|
|
250
251
|
```text
|
|
251
252
|
/gentle:models
|
|
@@ -257,15 +258,15 @@ The modal discovers:
|
|
|
257
258
|
- user agents in `~/.pi/agent/agents/` and `~/.agents/`;
|
|
258
259
|
- built-in agents from `pi-subagents`.
|
|
259
260
|
|
|
260
|
-
Recommended model shape:
|
|
261
|
+
Recommended model/effort shape:
|
|
261
262
|
|
|
262
|
-
| Agent kind | Recommended model |
|
|
263
|
-
| -------------------------- | ---------------------------------------------------- |
|
|
264
|
-
| Explore, proposal, archive | Fast and cheap is usually enough. |
|
|
265
|
-
| Spec, design, tasks | Strong reasoning model. |
|
|
266
|
-
| Apply | Strong coding and tool-use model. |
|
|
267
|
-
| Verify / review | Strong fresh-context model. |
|
|
268
|
-
| Tiny utilities | Inherit active/default model unless they bottleneck. |
|
|
263
|
+
| Agent kind | Recommended model | Recommended effort (`thinking`) |
|
|
264
|
+
| -------------------------- | ---------------------------------------------------- | ------------------------------- |
|
|
265
|
+
| Explore, proposal, archive | Fast and cheap is usually enough. | `off` to `low` |
|
|
266
|
+
| Spec, design, tasks | Strong reasoning model. | `medium` to `high` |
|
|
267
|
+
| Apply | Strong coding and tool-use model. | `medium` to `high` |
|
|
268
|
+
| Verify / review | Strong fresh-context model. | `high` |
|
|
269
|
+
| Tiny utilities | Inherit active/default model unless they bottleneck. | `inherit` |
|
|
269
270
|
|
|
270
271
|
Saved at:
|
|
271
272
|
|
|
@@ -273,18 +274,42 @@ Saved at:
|
|
|
273
274
|
.pi/gentle-ai/models.json
|
|
274
275
|
```
|
|
275
276
|
|
|
277
|
+
Config shape (per agent):
|
|
278
|
+
|
|
279
|
+
```json
|
|
280
|
+
{
|
|
281
|
+
"sdd-design": {
|
|
282
|
+
"model": "anthropic/claude-sonnet-4",
|
|
283
|
+
"thinking": "high"
|
|
284
|
+
},
|
|
285
|
+
"sdd-archive": {
|
|
286
|
+
"model": "openai/gpt-5-mini"
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Legacy string entries are still accepted and treated as `model`-only config.
|
|
292
|
+
|
|
276
293
|
## Commands
|
|
277
294
|
|
|
278
295
|
| Command | What it does |
|
|
279
296
|
| -------------------------------- | ------------------------------------------------------------ |
|
|
280
297
|
| `/gentle-ai:status` | Shows package, SDD asset, OpenSpec, and model config status. |
|
|
281
|
-
| `/gentle:models` | Opens model assignment UI.
|
|
298
|
+
| `/gentle:models` | Opens model + effort assignment UI. |
|
|
282
299
|
| `/gentle:persona` | Switches persona mode. |
|
|
283
300
|
| `/sdd-init` | Initializes or refreshes `openspec/config.yaml`. |
|
|
284
301
|
| `/gentle-ai:install-sdd` | Reinstalls SDD assets without overwriting local files. |
|
|
285
302
|
| `/gentle-ai:install-sdd --force` | Force-refreshes installed SDD assets. |
|
|
286
303
|
| `/skill-registry:refresh` | Regenerates `.atl/skill-registry.md`. |
|
|
287
304
|
|
|
305
|
+
Startup flag:
|
|
306
|
+
|
|
307
|
+
```text
|
|
308
|
+
pi --no-skill-registry
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Use it when you want skills available normally but do not want Gentle AI to refresh/watch `.atl/skill-registry.md` on startup. `pi -ns` / `pi --no-skills` also skip the registry startup work because Pi is already disabling skill loading.
|
|
312
|
+
|
|
288
313
|
Compatibility aliases:
|
|
289
314
|
|
|
290
315
|
```text
|
package/assets/orchestrator.md
CHANGED
|
@@ -31,6 +31,8 @@ User-facing conversation should stay in the user's language and follow the curre
|
|
|
31
31
|
|
|
32
32
|
Subagent-facing prompts should be written in English by default, even when the user speaks Spanish. Translate the user's request into concise English before delegation. This keeps token usage lower and gives built-in/project subagents a consistent operating language without changing the user-facing persona.
|
|
33
33
|
|
|
34
|
+
Generated artifacts — whether by the parent inline or by subagents — (code, UI copy, comments, identifiers, commit messages, filenames, PR descriptions) default to English, regardless of the user's conversation language. Override only when the user explicitly requests another language for that artifact, or when extending a project whose existing convention is non-English.
|
|
35
|
+
|
|
34
36
|
Exceptions:
|
|
35
37
|
|
|
36
38
|
- Preserve exact user quotes, UI copy, error messages, filenames, commands, and domain terms in their original language when they are evidence.
|
package/extensions/gentle-ai.ts
CHANGED
|
@@ -101,7 +101,12 @@ const SDD_AGENT_NAMES = [
|
|
|
101
101
|
] as const;
|
|
102
102
|
|
|
103
103
|
type SddAgentName = (typeof SDD_AGENT_NAMES)[number];
|
|
104
|
-
type
|
|
104
|
+
type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
105
|
+
interface AgentRoutingEntry {
|
|
106
|
+
model?: string;
|
|
107
|
+
thinking?: ThinkingLevel;
|
|
108
|
+
}
|
|
109
|
+
type AgentModelConfig = Record<string, AgentRoutingEntry>;
|
|
105
110
|
type AgentSource = "project" | "user" | "builtin";
|
|
106
111
|
|
|
107
112
|
interface AgentEntry {
|
|
@@ -113,6 +118,16 @@ interface AgentEntry {
|
|
|
113
118
|
const KEEP_CURRENT = "Keep current";
|
|
114
119
|
const INHERIT_MODEL = "Inherit active/default model";
|
|
115
120
|
const CUSTOM_MODEL = "Custom model id";
|
|
121
|
+
const INHERIT_THINKING = "Inherit effort";
|
|
122
|
+
const THINKING_OPTIONS: (ThinkingLevel | typeof INHERIT_THINKING)[] = [
|
|
123
|
+
INHERIT_THINKING,
|
|
124
|
+
"off",
|
|
125
|
+
"minimal",
|
|
126
|
+
"low",
|
|
127
|
+
"medium",
|
|
128
|
+
"high",
|
|
129
|
+
"xhigh",
|
|
130
|
+
];
|
|
116
131
|
|
|
117
132
|
const MODEL_CONTROL_OPTIONS = [
|
|
118
133
|
KEEP_CURRENT,
|
|
@@ -253,6 +268,32 @@ function writePersonaMode(cwd: string, mode: PersonaMode): void {
|
|
|
253
268
|
writeFileSync(path, `${JSON.stringify({ mode }, null, 2)}\n`);
|
|
254
269
|
}
|
|
255
270
|
|
|
271
|
+
function isThinkingLevel(value: unknown): value is ThinkingLevel {
|
|
272
|
+
return (
|
|
273
|
+
value === "off" ||
|
|
274
|
+
value === "minimal" ||
|
|
275
|
+
value === "low" ||
|
|
276
|
+
value === "medium" ||
|
|
277
|
+
value === "high" ||
|
|
278
|
+
value === "xhigh"
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function normalizeRoutingEntry(value: unknown): AgentRoutingEntry | undefined {
|
|
283
|
+
if (typeof value === "string") {
|
|
284
|
+
const model = value.trim();
|
|
285
|
+
return model.length > 0 ? { model } : undefined;
|
|
286
|
+
}
|
|
287
|
+
if (!isRecord(value)) return undefined;
|
|
288
|
+
const model =
|
|
289
|
+
typeof value.model === "string" && value.model.trim().length > 0
|
|
290
|
+
? value.model.trim()
|
|
291
|
+
: undefined;
|
|
292
|
+
const thinking = isThinkingLevel(value.thinking) ? value.thinking : undefined;
|
|
293
|
+
if (!model && !thinking) return undefined;
|
|
294
|
+
return { model, thinking };
|
|
295
|
+
}
|
|
296
|
+
|
|
256
297
|
function readModelConfig(cwd: string): AgentModelConfig {
|
|
257
298
|
const path = modelConfigPath(cwd);
|
|
258
299
|
if (!existsSync(path)) return {};
|
|
@@ -261,9 +302,8 @@ function readModelConfig(cwd: string): AgentModelConfig {
|
|
|
261
302
|
if (!isRecord(parsed)) return {};
|
|
262
303
|
const config: AgentModelConfig = {};
|
|
263
304
|
for (const [name, value] of Object.entries(parsed)) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
305
|
+
const entry = normalizeRoutingEntry(value);
|
|
306
|
+
if (entry) config[name] = entry;
|
|
267
307
|
}
|
|
268
308
|
return config;
|
|
269
309
|
} catch {
|
|
@@ -274,12 +314,23 @@ function readModelConfig(cwd: string): AgentModelConfig {
|
|
|
274
314
|
function writeModelConfig(cwd: string, config: AgentModelConfig): void {
|
|
275
315
|
const path = modelConfigPath(cwd);
|
|
276
316
|
mkdirSync(dirname(path), { recursive: true });
|
|
277
|
-
|
|
317
|
+
const cleaned: AgentModelConfig = {};
|
|
318
|
+
for (const [name, value] of Object.entries(config)) {
|
|
319
|
+
const entry = normalizeRoutingEntry(value);
|
|
320
|
+
if (entry) cleaned[name] = entry;
|
|
321
|
+
}
|
|
322
|
+
writeFileSync(path, `${JSON.stringify(cleaned, null, 2)}\n`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function cloneModelConfig(config: AgentModelConfig): AgentModelConfig {
|
|
326
|
+
return Object.fromEntries(
|
|
327
|
+
Object.entries(config).map(([name, entry]) => [name, { ...entry }]),
|
|
328
|
+
);
|
|
278
329
|
}
|
|
279
330
|
|
|
280
|
-
function
|
|
331
|
+
function updateFrontmatterRouting(
|
|
281
332
|
content: string,
|
|
282
|
-
|
|
333
|
+
entry: AgentRoutingEntry | undefined,
|
|
283
334
|
): string {
|
|
284
335
|
if (!content.startsWith("---\n")) return content;
|
|
285
336
|
const endIndex = content.indexOf("\n---", 4);
|
|
@@ -288,14 +339,19 @@ function updateFrontmatterModel(
|
|
|
288
339
|
const body = content.slice(endIndex);
|
|
289
340
|
const lines = frontmatter
|
|
290
341
|
.split("\n")
|
|
291
|
-
.filter(
|
|
292
|
-
|
|
342
|
+
.filter(
|
|
343
|
+
(line) => !line.startsWith("model:") && !line.startsWith("thinking:"),
|
|
344
|
+
);
|
|
345
|
+
const toInsert: string[] = [];
|
|
346
|
+
if (entry?.model) toInsert.push(`model: ${entry.model}`);
|
|
347
|
+
if (entry?.thinking) toInsert.push(`thinking: ${entry.thinking}`);
|
|
348
|
+
if (toInsert.length > 0) {
|
|
293
349
|
const descriptionIndex = lines.findIndex((line) =>
|
|
294
350
|
line.startsWith("description:"),
|
|
295
351
|
);
|
|
296
352
|
const insertIndex =
|
|
297
353
|
descriptionIndex >= 0 ? descriptionIndex + 1 : Math.min(1, lines.length);
|
|
298
|
-
lines.splice(insertIndex, 0,
|
|
354
|
+
lines.splice(insertIndex, 0, ...toInsert);
|
|
299
355
|
}
|
|
300
356
|
return `---\n${lines.join("\n")}${body}`;
|
|
301
357
|
}
|
|
@@ -372,7 +428,7 @@ function projectSettingsPath(cwd: string): string {
|
|
|
372
428
|
function updateBuiltinModelOverride(
|
|
373
429
|
cwd: string,
|
|
374
430
|
name: string,
|
|
375
|
-
|
|
431
|
+
entry: AgentRoutingEntry | undefined,
|
|
376
432
|
): boolean {
|
|
377
433
|
const path = projectSettingsPath(cwd);
|
|
378
434
|
let settings: Record<string, unknown> = {};
|
|
@@ -393,8 +449,10 @@ function updateBuiltinModelOverride(
|
|
|
393
449
|
const current = isRecord(agentOverrides[name])
|
|
394
450
|
? { ...agentOverrides[name] }
|
|
395
451
|
: {};
|
|
396
|
-
if (model === undefined) delete current.model;
|
|
397
|
-
else current.model = model;
|
|
452
|
+
if (entry?.model === undefined) delete current.model;
|
|
453
|
+
else current.model = entry.model;
|
|
454
|
+
if (entry?.thinking === undefined) delete current.thinking;
|
|
455
|
+
else current.thinking = entry.thinking;
|
|
398
456
|
if (Object.keys(current).length > 0) agentOverrides[name] = current;
|
|
399
457
|
else delete agentOverrides[name];
|
|
400
458
|
if (Object.keys(agentOverrides).length > 0)
|
|
@@ -414,9 +472,9 @@ function applyModelConfig(
|
|
|
414
472
|
let updated = 0;
|
|
415
473
|
let skipped = 0;
|
|
416
474
|
for (const agent of listDiscoverableAgents(cwd)) {
|
|
417
|
-
const
|
|
475
|
+
const entry = config[agent.name];
|
|
418
476
|
if (agent.source === "builtin") {
|
|
419
|
-
if (updateBuiltinModelOverride(cwd, agent.name,
|
|
477
|
+
if (updateBuiltinModelOverride(cwd, agent.name, entry)) updated += 1;
|
|
420
478
|
else skipped += 1;
|
|
421
479
|
continue;
|
|
422
480
|
}
|
|
@@ -425,7 +483,7 @@ function applyModelConfig(
|
|
|
425
483
|
continue;
|
|
426
484
|
}
|
|
427
485
|
const original = readFileSync(agent.filePath, "utf8");
|
|
428
|
-
const next =
|
|
486
|
+
const next = updateFrontmatterRouting(original, entry);
|
|
429
487
|
if (next === original) {
|
|
430
488
|
skipped += 1;
|
|
431
489
|
continue;
|
|
@@ -437,9 +495,12 @@ function applyModelConfig(
|
|
|
437
495
|
}
|
|
438
496
|
|
|
439
497
|
function describeModelConfig(cwd: string, config: AgentModelConfig): string[] {
|
|
440
|
-
return listDiscoverableAgents(cwd).map(
|
|
441
|
-
|
|
442
|
-
|
|
498
|
+
return listDiscoverableAgents(cwd).map((agent) => {
|
|
499
|
+
const entry = config[agent.name];
|
|
500
|
+
const model = entry?.model ?? "inherit";
|
|
501
|
+
const thinking = entry?.thinking ?? "inherit";
|
|
502
|
+
return `${agent.name}: model=${model}, effort=${thinking}`;
|
|
503
|
+
});
|
|
443
504
|
}
|
|
444
505
|
|
|
445
506
|
async function getPiModelOptions(ctx: ExtensionContext): Promise<string[]> {
|
|
@@ -458,28 +519,33 @@ interface OverlayComponent {
|
|
|
458
519
|
|
|
459
520
|
type ModelPanelResult =
|
|
460
521
|
| { type: "save"; config: AgentModelConfig }
|
|
461
|
-
| { type: "custom"; agent: string | "all" }
|
|
522
|
+
| { type: "custom"; agent: string | "all"; config: AgentModelConfig }
|
|
462
523
|
| { type: "cancel" };
|
|
463
524
|
|
|
464
525
|
const SET_ALL_AGENTS = "Set all agents";
|
|
465
526
|
|
|
466
527
|
class SddModelPanel implements OverlayComponent {
|
|
467
528
|
private cursor = 0;
|
|
468
|
-
private mode: "agents" | "models" = "agents";
|
|
529
|
+
private mode: "agents" | "models" | "effort" = "agents";
|
|
469
530
|
private selectedRow = SET_ALL_AGENTS;
|
|
470
531
|
private modelCursor = 0;
|
|
532
|
+
private effortCursor = 0;
|
|
471
533
|
private query = "";
|
|
472
534
|
private readonly draft: AgentModelConfig;
|
|
473
535
|
private readonly rows: string[];
|
|
536
|
+
private readonly modelOptions: string[];
|
|
537
|
+
private readonly done: (result: ModelPanelResult) => void;
|
|
474
538
|
|
|
475
539
|
constructor(
|
|
476
540
|
initialConfig: AgentModelConfig,
|
|
477
|
-
|
|
541
|
+
modelOptions: string[],
|
|
478
542
|
agents: string[],
|
|
479
|
-
|
|
543
|
+
done: (result: ModelPanelResult) => void,
|
|
480
544
|
) {
|
|
481
|
-
this.draft =
|
|
545
|
+
this.draft = cloneModelConfig(initialConfig);
|
|
482
546
|
this.rows = [SET_ALL_AGENTS, ...agents];
|
|
547
|
+
this.modelOptions = modelOptions;
|
|
548
|
+
this.done = done;
|
|
483
549
|
}
|
|
484
550
|
|
|
485
551
|
invalidate(): void {}
|
|
@@ -489,13 +555,17 @@ class SddModelPanel implements OverlayComponent {
|
|
|
489
555
|
this.handleModelInput(data);
|
|
490
556
|
return;
|
|
491
557
|
}
|
|
558
|
+
if (this.mode === "effort") {
|
|
559
|
+
this.handleEffortInput(data);
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
492
562
|
this.handleAgentInput(data);
|
|
493
563
|
}
|
|
494
564
|
|
|
495
565
|
render(width: number): string[] {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
566
|
+
if (this.mode === "models") return this.renderModelPicker(width);
|
|
567
|
+
if (this.mode === "effort") return this.renderEffortPicker(width);
|
|
568
|
+
return this.renderAgentList(width);
|
|
499
569
|
}
|
|
500
570
|
|
|
501
571
|
private handleAgentInput(data: string): void {
|
|
@@ -517,13 +587,21 @@ class SddModelPanel implements OverlayComponent {
|
|
|
517
587
|
return;
|
|
518
588
|
}
|
|
519
589
|
if (data === "i") {
|
|
520
|
-
this.
|
|
590
|
+
this.applyInherit();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (data === "e") {
|
|
594
|
+
this.selectedRow = this.rows[this.cursor] ?? SET_ALL_AGENTS;
|
|
595
|
+
this.mode = "effort";
|
|
596
|
+
this.effortCursor = 0;
|
|
521
597
|
return;
|
|
522
598
|
}
|
|
523
599
|
if (data === "c") {
|
|
524
600
|
const row = this.rows[this.cursor];
|
|
525
|
-
if (row === SET_ALL_AGENTS)
|
|
526
|
-
|
|
601
|
+
if (row === SET_ALL_AGENTS)
|
|
602
|
+
this.done({ type: "custom", agent: "all", config: this.draft });
|
|
603
|
+
else if (row)
|
|
604
|
+
this.done({ type: "custom", agent: row, config: this.draft });
|
|
527
605
|
return;
|
|
528
606
|
}
|
|
529
607
|
if (!matchesKey(data, "return")) return;
|
|
@@ -578,6 +656,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
578
656
|
this.done({
|
|
579
657
|
type: "custom",
|
|
580
658
|
agent: this.selectedRow === SET_ALL_AGENTS ? "all" : this.selectedRow,
|
|
659
|
+
config: this.draft,
|
|
581
660
|
});
|
|
582
661
|
return;
|
|
583
662
|
}
|
|
@@ -585,7 +664,9 @@ class SddModelPanel implements OverlayComponent {
|
|
|
585
664
|
this.mode = "agents";
|
|
586
665
|
return;
|
|
587
666
|
}
|
|
588
|
-
this.
|
|
667
|
+
this.applyModelSelection(
|
|
668
|
+
selected === INHERIT_MODEL ? undefined : selected,
|
|
669
|
+
);
|
|
589
670
|
this.mode = "agents";
|
|
590
671
|
return;
|
|
591
672
|
}
|
|
@@ -595,18 +676,52 @@ class SddModelPanel implements OverlayComponent {
|
|
|
595
676
|
}
|
|
596
677
|
}
|
|
597
678
|
|
|
598
|
-
private
|
|
679
|
+
private applyModelSelection(model: string | undefined): void {
|
|
599
680
|
const row = this.rows[this.cursor];
|
|
600
681
|
if (row === SET_ALL_AGENTS) {
|
|
601
|
-
for (const name of this.rows.slice(1))
|
|
602
|
-
if (model === undefined) delete this.draft[name];
|
|
603
|
-
else this.draft[name] = model;
|
|
604
|
-
}
|
|
682
|
+
for (const name of this.rows.slice(1)) this.setModel(name, model);
|
|
605
683
|
return;
|
|
606
684
|
}
|
|
607
685
|
if (!row) return;
|
|
608
|
-
|
|
609
|
-
|
|
686
|
+
this.setModel(row, model);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
private applyThinkingSelection(thinking: ThinkingLevel | undefined): void {
|
|
690
|
+
const row = this.selectedRow;
|
|
691
|
+
if (row === SET_ALL_AGENTS) {
|
|
692
|
+
for (const name of this.rows.slice(1)) this.setThinking(name, thinking);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
this.setThinking(row, thinking);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
private applyInherit(): void {
|
|
699
|
+
const row = this.rows[this.cursor];
|
|
700
|
+
if (row === SET_ALL_AGENTS) {
|
|
701
|
+
for (const name of this.rows.slice(1)) this.clearEntry(name);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (row) this.clearEntry(row);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
private setModel(name: string, model: string | undefined): void {
|
|
708
|
+
const current = this.draft[name] ?? {};
|
|
709
|
+
if (model === undefined) delete current.model;
|
|
710
|
+
else current.model = model;
|
|
711
|
+
if (!current.model && !current.thinking) delete this.draft[name];
|
|
712
|
+
else this.draft[name] = current;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
private setThinking(name: string, thinking: ThinkingLevel | undefined): void {
|
|
716
|
+
const current = this.draft[name] ?? {};
|
|
717
|
+
if (thinking === undefined) delete current.thinking;
|
|
718
|
+
else current.thinking = thinking;
|
|
719
|
+
if (!current.model && !current.thinking) delete this.draft[name];
|
|
720
|
+
else this.draft[name] = current;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
private clearEntry(name: string): void {
|
|
724
|
+
delete this.draft[name];
|
|
610
725
|
}
|
|
611
726
|
|
|
612
727
|
private filteredModelOptions(): string[] {
|
|
@@ -644,7 +759,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
644
759
|
lines.push("");
|
|
645
760
|
lines.push(
|
|
646
761
|
line(
|
|
647
|
-
"j/k: navigate • enter: change model / confirm • i: inherit • c: custom • ctrl+s: save • esc: back",
|
|
762
|
+
"j/k: navigate • enter: change model / confirm • e: change effort • i: inherit all • c: custom model • ctrl+s: save • esc: back",
|
|
648
763
|
),
|
|
649
764
|
);
|
|
650
765
|
return lines;
|
|
@@ -680,17 +795,70 @@ class SddModelPanel implements OverlayComponent {
|
|
|
680
795
|
return lines;
|
|
681
796
|
}
|
|
682
797
|
|
|
798
|
+
private handleEffortInput(data: string): void {
|
|
799
|
+
if (matchesKey(data, "ctrl+c")) {
|
|
800
|
+
this.done({ type: "cancel" });
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (matchesKey(data, "escape")) {
|
|
804
|
+
this.mode = "agents";
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
if (matchesKey(data, "down") || data === "j") {
|
|
808
|
+
this.effortCursor = Math.min(
|
|
809
|
+
Math.max(0, THINKING_OPTIONS.length - 1),
|
|
810
|
+
this.effortCursor + 1,
|
|
811
|
+
);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
if (matchesKey(data, "up") || data === "k") {
|
|
815
|
+
this.effortCursor = Math.max(0, this.effortCursor - 1);
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if (!matchesKey(data, "return")) return;
|
|
819
|
+
const selected = THINKING_OPTIONS[this.effortCursor];
|
|
820
|
+
if (selected === INHERIT_THINKING) this.applyThinkingSelection(undefined);
|
|
821
|
+
else this.applyThinkingSelection(selected);
|
|
822
|
+
this.mode = "agents";
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
private renderEffortPicker(width: number): string[] {
|
|
826
|
+
const lines: string[] = [];
|
|
827
|
+
const line = (text = "") =>
|
|
828
|
+
truncateToWidth(text, Math.max(1, width), "…", true);
|
|
829
|
+
lines.push(line(`Select effort for ${this.selectedRow}`));
|
|
830
|
+
lines.push("");
|
|
831
|
+
for (let i = 0; i < THINKING_OPTIONS.length; i++) {
|
|
832
|
+
const focused = i === this.effortCursor;
|
|
833
|
+
lines.push(line(`${focused ? "▸" : " "} ${THINKING_OPTIONS[i]}`));
|
|
834
|
+
}
|
|
835
|
+
lines.push("");
|
|
836
|
+
lines.push(line("j/k: navigate • enter: select • esc: back"));
|
|
837
|
+
return lines;
|
|
838
|
+
}
|
|
839
|
+
|
|
683
840
|
private renderSetAllLabel(row: string): string {
|
|
684
|
-
const
|
|
841
|
+
const models = this.rows
|
|
842
|
+
.slice(1)
|
|
843
|
+
.map((name) => this.draft[name]?.model ?? "inherit");
|
|
844
|
+
const efforts = this.rows
|
|
685
845
|
.slice(1)
|
|
686
|
-
.map((name) => this.draft[name] ?? "inherit");
|
|
687
|
-
const
|
|
688
|
-
const
|
|
689
|
-
|
|
846
|
+
.map((name) => this.draft[name]?.thinking ?? "inherit");
|
|
847
|
+
const firstModel = models[0] ?? "inherit";
|
|
848
|
+
const firstEffort = efforts[0] ?? "inherit";
|
|
849
|
+
const modelLabel = models.every((value) => value === firstModel)
|
|
850
|
+
? firstModel
|
|
851
|
+
: "mixed";
|
|
852
|
+
const effortLabel = efforts.every((value) => value === firstEffort)
|
|
853
|
+
? firstEffort
|
|
854
|
+
: "mixed";
|
|
855
|
+
return `${row.padEnd(20)} model=${modelLabel}, effort=${effortLabel}`;
|
|
690
856
|
}
|
|
691
857
|
|
|
692
858
|
private renderAgentLabel(row: string): string {
|
|
693
|
-
|
|
859
|
+
const model = this.draft[row]?.model ?? "inherit";
|
|
860
|
+
const effort = this.draft[row]?.thinking ?? "inherit";
|
|
861
|
+
return `${row.padEnd(20)} model=${model}, effort=${effort}`;
|
|
694
862
|
}
|
|
695
863
|
}
|
|
696
864
|
|
|
@@ -719,8 +887,11 @@ async function handleModelsCommand(ctx: ExtensionContext): Promise<void> {
|
|
|
719
887
|
let config = readModelConfig(ctx.cwd);
|
|
720
888
|
let result = await showSddModelPanel(ctx, config);
|
|
721
889
|
while (result.type === "custom") {
|
|
890
|
+
config = cloneModelConfig(result.config);
|
|
722
891
|
const current =
|
|
723
|
-
result.agent === "all"
|
|
892
|
+
result.agent === "all"
|
|
893
|
+
? "inherit"
|
|
894
|
+
: (config[result.agent]?.model ?? "inherit");
|
|
724
895
|
const custom = await ctx.ui.input(
|
|
725
896
|
`${result.agent === "all" ? "all agents" : result.agent} custom model id`,
|
|
726
897
|
current === "inherit" ? "provider/model" : current,
|
|
@@ -729,11 +900,22 @@ async function handleModelsCommand(ctx: ExtensionContext): Promise<void> {
|
|
|
729
900
|
const trimmed = custom.trim();
|
|
730
901
|
if (trimmed.length > 0) {
|
|
731
902
|
if (result.agent === "all") {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
903
|
+
const next: AgentModelConfig = { ...config };
|
|
904
|
+
for (const agent of listDiscoverableAgents(ctx.cwd)) {
|
|
905
|
+
next[agent.name] = {
|
|
906
|
+
...(next[agent.name] ?? {}),
|
|
907
|
+
model: trimmed,
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
config = next;
|
|
735
911
|
} else {
|
|
736
|
-
config = {
|
|
912
|
+
config = {
|
|
913
|
+
...config,
|
|
914
|
+
[result.agent]: {
|
|
915
|
+
...(config[result.agent] ?? {}),
|
|
916
|
+
model: trimmed,
|
|
917
|
+
},
|
|
918
|
+
};
|
|
737
919
|
}
|
|
738
920
|
}
|
|
739
921
|
result = await showSddModelPanel(ctx, config);
|
|
@@ -21,6 +21,8 @@ const EXCLUDE_PREFIXES = ["sdd-"];
|
|
|
21
21
|
const ATL_IGNORE_ENTRY = ".atl/";
|
|
22
22
|
const WATCH_DEBOUNCE_MS = 500;
|
|
23
23
|
const REGISTRY_SCHEMA_VERSION = 4;
|
|
24
|
+
const NO_SKILL_REGISTRY_FLAG = "no-skill-registry";
|
|
25
|
+
const NO_SKILL_REGISTRY_ENV = "GENTLE_PI_NO_SKILL_REGISTRY";
|
|
24
26
|
const LEGACY_PROJECT_REGISTRY_REL_PATH = ".pi/extensions/skill-registry.ts";
|
|
25
27
|
const LEGACY_PROJECT_REGISTRY_DISABLED_REL_PATH =
|
|
26
28
|
".pi/extensions/skill-registry.ts.disabled";
|
|
@@ -410,6 +412,26 @@ function regenerateRegistry(cwd: string, force: boolean): RegenResult {
|
|
|
410
412
|
|
|
411
413
|
const watchedCwds = new Set<string>();
|
|
412
414
|
|
|
415
|
+
function isTruthyEnv(value: string | undefined): boolean {
|
|
416
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function hasCliArg(args: string[], ...names: string[]): boolean {
|
|
420
|
+
return args.some((arg) => names.includes(arg));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function shouldSkipSkillRegistryStartup(
|
|
424
|
+
pi: Pick<ExtensionAPI, "getFlag">,
|
|
425
|
+
argv = process.argv.slice(2),
|
|
426
|
+
env = process.env,
|
|
427
|
+
): boolean {
|
|
428
|
+
return (
|
|
429
|
+
pi.getFlag(NO_SKILL_REGISTRY_FLAG) === true ||
|
|
430
|
+
isTruthyEnv(env[NO_SKILL_REGISTRY_ENV]) ||
|
|
431
|
+
hasCliArg(argv, "--no-skills", "-ns")
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
413
435
|
function startSkillRegistryWatcher(cwd: string, notify: (message: string) => void): void {
|
|
414
436
|
if (watchedCwds.has(cwd)) return;
|
|
415
437
|
watchedCwds.add(cwd);
|
|
@@ -444,10 +466,18 @@ export const __testing = {
|
|
|
444
466
|
extractTriggerDescription,
|
|
445
467
|
uniqueExistingDirs,
|
|
446
468
|
dedupeBySkillName,
|
|
469
|
+
shouldSkipSkillRegistryStartup,
|
|
447
470
|
};
|
|
448
471
|
|
|
449
472
|
export default function (pi: ExtensionAPI) {
|
|
473
|
+
pi.registerFlag(NO_SKILL_REGISTRY_FLAG, {
|
|
474
|
+
description: "Skip the Gentle AI skill registry refresh and watcher on startup.",
|
|
475
|
+
type: "boolean",
|
|
476
|
+
default: false,
|
|
477
|
+
});
|
|
478
|
+
|
|
450
479
|
pi.on("session_start", async (_event, ctx) => {
|
|
480
|
+
if (shouldSkipSkillRegistryStartup(pi)) return;
|
|
451
481
|
try {
|
|
452
482
|
ensureAtlIgnored(ctx.cwd);
|
|
453
483
|
const quarantinedLegacy = quarantineLegacyProjectRegistry(ctx.cwd);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"README.md"
|
|
33
33
|
],
|
|
34
34
|
"scripts": {
|
|
35
|
-
"test": "node --experimental-strip-types --test tests/*.test.ts",
|
|
35
|
+
"test": "node --experimental-strip-types --test tests/*.test.ts && pnpm run test:harness",
|
|
36
|
+
"test:harness": "node --experimental-strip-types tests/runtime-harness.mjs",
|
|
36
37
|
"prepack": "pnpm test && node scripts/verify-package-files.mjs",
|
|
37
38
|
"prepublishOnly": "pnpm test && node scripts/verify-package-files.mjs"
|
|
38
39
|
},
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
9
|
+
const EXTENSIONS = [
|
|
10
|
+
"extensions/gentle-ai.ts",
|
|
11
|
+
"extensions/skill-registry.ts",
|
|
12
|
+
"extensions/sdd-init.ts",
|
|
13
|
+
"extensions/startup-banner.ts",
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const EXPECTED_COMMANDS = [
|
|
17
|
+
"gentle-ai:install-sdd",
|
|
18
|
+
"gentle:models",
|
|
19
|
+
"gentle-ai:models",
|
|
20
|
+
"gentleman:models",
|
|
21
|
+
"gentle:persona",
|
|
22
|
+
"gentle-ai:persona",
|
|
23
|
+
"gentleman:persona",
|
|
24
|
+
"gentle-ai:status",
|
|
25
|
+
"sdd-init",
|
|
26
|
+
"skill-registry:refresh",
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function createPi() {
|
|
30
|
+
const hooks = new Map();
|
|
31
|
+
const commands = new Map();
|
|
32
|
+
const flags = new Map();
|
|
33
|
+
const flagValues = new Map([["no-skill-registry", true]]);
|
|
34
|
+
|
|
35
|
+
const pi = {
|
|
36
|
+
on(name, handler) {
|
|
37
|
+
const list = hooks.get(name) ?? [];
|
|
38
|
+
list.push(handler);
|
|
39
|
+
hooks.set(name, list);
|
|
40
|
+
},
|
|
41
|
+
registerCommand(name, definition) {
|
|
42
|
+
commands.set(name, definition);
|
|
43
|
+
},
|
|
44
|
+
registerFlag(name, definition) {
|
|
45
|
+
flags.set(name, definition);
|
|
46
|
+
},
|
|
47
|
+
getFlag(name) {
|
|
48
|
+
return flagValues.get(name) ?? false;
|
|
49
|
+
},
|
|
50
|
+
setFlag(name, value) {
|
|
51
|
+
flagValues.set(name, value);
|
|
52
|
+
},
|
|
53
|
+
getCommands() {
|
|
54
|
+
return Array.from(commands, ([name, definition]) => ({ name, ...definition }));
|
|
55
|
+
},
|
|
56
|
+
getAllTools() {
|
|
57
|
+
return [
|
|
58
|
+
{ name: "read" },
|
|
59
|
+
{ name: "bash" },
|
|
60
|
+
{ name: "edit" },
|
|
61
|
+
{ name: "write" },
|
|
62
|
+
];
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return { pi, hooks, commands, flags };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function createUi() {
|
|
70
|
+
const notifications = [];
|
|
71
|
+
return {
|
|
72
|
+
notifications,
|
|
73
|
+
notify(message, level = "info") {
|
|
74
|
+
notifications.push({ message, level });
|
|
75
|
+
},
|
|
76
|
+
async confirm() {
|
|
77
|
+
return false;
|
|
78
|
+
},
|
|
79
|
+
async select(_label, options) {
|
|
80
|
+
return options[0];
|
|
81
|
+
},
|
|
82
|
+
async input(_label, placeholder) {
|
|
83
|
+
return placeholder;
|
|
84
|
+
},
|
|
85
|
+
custom() {
|
|
86
|
+
return Promise.resolve({ type: "cancel" });
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function createCtx(cwd, hasUI = false) {
|
|
92
|
+
return {
|
|
93
|
+
cwd,
|
|
94
|
+
hasUI,
|
|
95
|
+
ui: createUi(),
|
|
96
|
+
modelRegistry: {
|
|
97
|
+
async getAvailable() {
|
|
98
|
+
return [];
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function tempWorkspace() {
|
|
105
|
+
return mkdtemp(join(tmpdir(), "gentle-pi-runtime-"));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function loadExtensions(pi) {
|
|
109
|
+
for (const [index, rel] of EXTENSIONS.entries()) {
|
|
110
|
+
const mod = await import(`${pathToFileURL(join(ROOT, rel)).href}?runtime-harness=${index}`);
|
|
111
|
+
assert.equal(typeof mod.default, "function", `${rel} must export a default function`);
|
|
112
|
+
mod.default(pi);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function run() {
|
|
117
|
+
const { pi, hooks, commands, flags } = createPi();
|
|
118
|
+
await loadExtensions(pi);
|
|
119
|
+
|
|
120
|
+
for (const name of EXPECTED_COMMANDS) {
|
|
121
|
+
assert.ok(commands.has(name), `missing command ${name}`);
|
|
122
|
+
}
|
|
123
|
+
assert.ok(flags.has("no-skill-registry"), "missing no-skill-registry flag");
|
|
124
|
+
assert.ok(hooks.has("session_start"), "missing session_start hook");
|
|
125
|
+
assert.ok(hooks.has("before_agent_start"), "missing before_agent_start hook");
|
|
126
|
+
assert.ok(hooks.has("tool_call"), "missing tool_call hook");
|
|
127
|
+
|
|
128
|
+
const promptCwd = await tempWorkspace();
|
|
129
|
+
try {
|
|
130
|
+
const promptHook = hooks.get("before_agent_start")[0];
|
|
131
|
+
const promptResult = promptHook({ systemPrompt: "base" }, createCtx(promptCwd));
|
|
132
|
+
assert.match(promptResult.systemPrompt, /base/);
|
|
133
|
+
assert.match(promptResult.systemPrompt, /el Gentleman/);
|
|
134
|
+
} finally {
|
|
135
|
+
await rm(promptCwd, { recursive: true, force: true });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const toolCwd = await tempWorkspace();
|
|
139
|
+
try {
|
|
140
|
+
const toolHook = hooks.get("tool_call")[0];
|
|
141
|
+
assert.equal(await toolHook({ toolName: "bash", input: { command: "git status" } }, createCtx(toolCwd)), undefined);
|
|
142
|
+
const denied = await toolHook({ toolName: "bash", input: { command: "rm -rf /" } }, createCtx(toolCwd));
|
|
143
|
+
assert.equal(denied.block, true);
|
|
144
|
+
assert.match(denied.reason, /destructive/);
|
|
145
|
+
const needsConfirm = await toolHook({ toolName: "bash", input: { command: "git push" } }, createCtx(toolCwd));
|
|
146
|
+
assert.equal(needsConfirm.block, true);
|
|
147
|
+
assert.match(needsConfirm.reason, /confirmation/);
|
|
148
|
+
} finally {
|
|
149
|
+
await rm(toolCwd, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const noUiCwd = await tempWorkspace();
|
|
153
|
+
try {
|
|
154
|
+
for (const handler of hooks.get("session_start")) {
|
|
155
|
+
await handler({ reason: "startup" }, createCtx(noUiCwd, false));
|
|
156
|
+
}
|
|
157
|
+
} finally {
|
|
158
|
+
await rm(noUiCwd, { recursive: true, force: true });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const installCwd = await tempWorkspace();
|
|
162
|
+
try {
|
|
163
|
+
const ctx = createCtx(installCwd, true);
|
|
164
|
+
await commands.get("gentle-ai:install-sdd").handler("", ctx);
|
|
165
|
+
assert.match(ctx.ui.notifications.at(-1).message, /SDD assets installed/);
|
|
166
|
+
} finally {
|
|
167
|
+
await rm(installCwd, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const sddCwd = await tempWorkspace();
|
|
171
|
+
try {
|
|
172
|
+
const ctx = createCtx(sddCwd, true);
|
|
173
|
+
await commands.get("sdd-init").handler("", ctx);
|
|
174
|
+
assert.match(ctx.ui.notifications.at(-1).message, /Wrote openspec\/config\.yaml/);
|
|
175
|
+
} finally {
|
|
176
|
+
await rm(sddCwd, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const modelsCwd = await tempWorkspace();
|
|
180
|
+
try {
|
|
181
|
+
await mkdir(join(modelsCwd, ".pi", "agents"), { recursive: true });
|
|
182
|
+
await mkdir(
|
|
183
|
+
join(modelsCwd, ".pi", "npm", "node_modules", "pi-subagents", "agents"),
|
|
184
|
+
{ recursive: true },
|
|
185
|
+
);
|
|
186
|
+
await writeFile(
|
|
187
|
+
join(
|
|
188
|
+
modelsCwd,
|
|
189
|
+
".pi",
|
|
190
|
+
"npm",
|
|
191
|
+
"node_modules",
|
|
192
|
+
"pi-subagents",
|
|
193
|
+
"agents",
|
|
194
|
+
"worker.md",
|
|
195
|
+
),
|
|
196
|
+
`---\nname: worker\ndescription: Builtin worker\n---\n`,
|
|
197
|
+
);
|
|
198
|
+
await writeFile(
|
|
199
|
+
join(modelsCwd, ".pi", "agents", "sdd-apply.md"),
|
|
200
|
+
`---\nname: sdd-apply\ndescription: Apply phase\n---\n\nbody\n`,
|
|
201
|
+
);
|
|
202
|
+
await mkdir(join(modelsCwd, ".pi", "gentle-ai"), { recursive: true });
|
|
203
|
+
await writeFile(
|
|
204
|
+
join(modelsCwd, ".pi", "gentle-ai", "models.json"),
|
|
205
|
+
JSON.stringify({ "sdd-apply": "openai/gpt-5" }, null, 2),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const ctx = createCtx(modelsCwd, true);
|
|
209
|
+
await hooks.get("session_start")[0]({ reason: "startup" }, ctx);
|
|
210
|
+
const legacyAppliedAgent = await readFile(
|
|
211
|
+
join(modelsCwd, ".pi", "agents", "sdd-apply.md"),
|
|
212
|
+
"utf8",
|
|
213
|
+
);
|
|
214
|
+
assert.match(legacyAppliedAgent, /model: openai\/gpt-5/);
|
|
215
|
+
assert.doesNotMatch(legacyAppliedAgent, /thinking:/);
|
|
216
|
+
|
|
217
|
+
ctx.ui.custom = () =>
|
|
218
|
+
Promise.resolve({
|
|
219
|
+
type: "save",
|
|
220
|
+
config: {
|
|
221
|
+
"sdd-apply": { model: "openai/gpt-5", thinking: "high" },
|
|
222
|
+
worker: { model: "openai/gpt-5-mini", thinking: "low" },
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
await commands.get("gentle:models").handler("", ctx);
|
|
226
|
+
|
|
227
|
+
const savedConfig = JSON.parse(
|
|
228
|
+
await readFile(join(modelsCwd, ".pi", "gentle-ai", "models.json"), "utf8"),
|
|
229
|
+
);
|
|
230
|
+
assert.deepEqual(savedConfig["sdd-apply"], {
|
|
231
|
+
model: "openai/gpt-5",
|
|
232
|
+
thinking: "high",
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const applyAgent = await readFile(
|
|
236
|
+
join(modelsCwd, ".pi", "agents", "sdd-apply.md"),
|
|
237
|
+
"utf8",
|
|
238
|
+
);
|
|
239
|
+
assert.match(applyAgent, /model: openai\/gpt-5/);
|
|
240
|
+
assert.match(applyAgent, /thinking: high/);
|
|
241
|
+
|
|
242
|
+
const settings = JSON.parse(
|
|
243
|
+
await readFile(join(modelsCwd, ".pi", "settings.json"), "utf8"),
|
|
244
|
+
);
|
|
245
|
+
assert.equal(
|
|
246
|
+
settings.subagents.agentOverrides.worker.model,
|
|
247
|
+
"openai/gpt-5-mini",
|
|
248
|
+
);
|
|
249
|
+
assert.equal(settings.subagents.agentOverrides.worker.thinking, "low");
|
|
250
|
+
|
|
251
|
+
let customPanelCalls = 0;
|
|
252
|
+
ctx.ui.input = async () => "custom/provider-model";
|
|
253
|
+
ctx.ui.custom = (factory) =>
|
|
254
|
+
new Promise((resolve) => {
|
|
255
|
+
customPanelCalls += 1;
|
|
256
|
+
const panel = factory(null, null, null, resolve);
|
|
257
|
+
if (customPanelCalls === 1) {
|
|
258
|
+
panel.handleInput("e"); // effort picker for all agents
|
|
259
|
+
for (let i = 0; i < 4; i++) panel.handleInput("j"); // medium
|
|
260
|
+
panel.handleInput("\r");
|
|
261
|
+
panel.handleInput("c"); // custom model from the same unsaved draft
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
panel.handleInput("\u0013"); // ctrl+s saves the draft reopened after custom model input
|
|
265
|
+
});
|
|
266
|
+
await commands.get("gentle:models").handler("", ctx);
|
|
267
|
+
|
|
268
|
+
const customSavedConfig = JSON.parse(
|
|
269
|
+
await readFile(join(modelsCwd, ".pi", "gentle-ai", "models.json"), "utf8"),
|
|
270
|
+
);
|
|
271
|
+
assert.deepEqual(customSavedConfig["sdd-apply"], {
|
|
272
|
+
model: "custom/provider-model",
|
|
273
|
+
thinking: "medium",
|
|
274
|
+
});
|
|
275
|
+
} finally {
|
|
276
|
+
await rm(modelsCwd, { recursive: true, force: true });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const registryCwd = await tempWorkspace();
|
|
280
|
+
try {
|
|
281
|
+
const ctx = createCtx(registryCwd, true);
|
|
282
|
+
await commands.get("skill-registry:refresh").handler("", ctx);
|
|
283
|
+
assert.match(ctx.ui.notifications.at(-1).message, /Skill registry:/);
|
|
284
|
+
} finally {
|
|
285
|
+
await rm(registryCwd, { recursive: true, force: true });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
run().catch((error) => {
|
|
290
|
+
console.error(error);
|
|
291
|
+
process.exitCode = 1;
|
|
292
|
+
});
|
|
@@ -112,3 +112,17 @@ test("uniqueExistingDirs normalizes duplicates and ignores missing roots", () =>
|
|
|
112
112
|
|
|
113
113
|
assert.deepEqual(__testing.uniqueExistingDirs([existing, join(root, "skills/"), join(root, "missing")]), [existing]);
|
|
114
114
|
});
|
|
115
|
+
|
|
116
|
+
test("startup skip honors no skill registry controls", () => {
|
|
117
|
+
const enabled = { getFlag: () => true };
|
|
118
|
+
const disabled = { getFlag: () => false };
|
|
119
|
+
|
|
120
|
+
assert.equal(__testing.shouldSkipSkillRegistryStartup(enabled, [], {}), true);
|
|
121
|
+
assert.equal(__testing.shouldSkipSkillRegistryStartup(disabled, ["--no-skills"], {}), true);
|
|
122
|
+
assert.equal(__testing.shouldSkipSkillRegistryStartup(disabled, ["-ns"], {}), true);
|
|
123
|
+
assert.equal(
|
|
124
|
+
__testing.shouldSkipSkillRegistryStartup(disabled, [], { GENTLE_PI_NO_SKILL_REGISTRY: "1" }),
|
|
125
|
+
true,
|
|
126
|
+
);
|
|
127
|
+
assert.equal(__testing.shouldSkipSkillRegistryStartup(disabled, [], {}), false);
|
|
128
|
+
});
|