gentle-pi 0.3.10 → 0.4.1
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 +34 -11
- package/assets/orchestrator.md +3 -1
- package/extensions/gentle-ai.ts +295 -28
- package/extensions/skill-registry.ts +38 -10
- package/extensions/startup-banner.ts +189 -25
- package/package.json +1 -1
- package/scripts/verify-package-files.mjs +10 -0
- package/skills/comment-writer/SKILL.md +7 -7
- package/tests/artifact-language.test.ts +40 -1
- package/tests/gentle-ai.test.ts +36 -0
- package/tests/runtime-harness.mjs +161 -5
- package/tests/skill-registry.test.ts +32 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync, watch } from "node:fs";
|
|
2
|
+
import { existsSync, type FSWatcher, watch } from "node:fs";
|
|
3
3
|
import {
|
|
4
4
|
access,
|
|
5
5
|
mkdir,
|
|
@@ -29,6 +29,7 @@ const LEGACY_PROJECT_REGISTRY_DISABLED_REL_PATH =
|
|
|
29
29
|
".pi/extensions/skill-registry.ts.disabled";
|
|
30
30
|
const SKILL_REGISTRY_EXTENSION_SOURCE_KEY =
|
|
31
31
|
"__gentlePiSkillRegistryExtensionSource";
|
|
32
|
+
const activeWatchers = new Set<FSWatcher>();
|
|
32
33
|
|
|
33
34
|
interface SkillRegistryExtensionGlobal {
|
|
34
35
|
[SKILL_REGISTRY_EXTENSION_SOURCE_KEY]?: string;
|
|
@@ -62,6 +63,7 @@ function userSkillDirs(): string[] {
|
|
|
62
63
|
join(home, ".claude/skills"),
|
|
63
64
|
join(home, ".gemini/skills"),
|
|
64
65
|
join(home, ".gemini/antigravity/skills"),
|
|
66
|
+
join(home, ".trae/skills"),
|
|
65
67
|
join(home, ".cursor/skills"),
|
|
66
68
|
join(home, ".copilot/skills"),
|
|
67
69
|
join(home, ".codex/skills"),
|
|
@@ -78,6 +80,7 @@ function projectSkillDirs(cwd: string): string[] {
|
|
|
78
80
|
join(cwd, ".opencode/skills"),
|
|
79
81
|
join(cwd, ".claude/skills"),
|
|
80
82
|
join(cwd, ".gemini/skills"),
|
|
83
|
+
join(cwd, ".trae/skills"),
|
|
81
84
|
join(cwd, ".cursor/skills"),
|
|
82
85
|
join(cwd, ".github/skills"),
|
|
83
86
|
join(cwd, ".codex/skills"),
|
|
@@ -116,11 +119,12 @@ async function findSkillFiles(root: string): Promise<string[]> {
|
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
function parseFrontmatter(source: string): { name?: string; description?: string; body: string } {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
122
|
+
const normalized = source.replace(/\r\n?/g, "\n");
|
|
123
|
+
if (!normalized.startsWith("---\n")) return { body: normalized };
|
|
124
|
+
const end = normalized.indexOf("\n---", 4);
|
|
125
|
+
if (end === -1) return { body: normalized };
|
|
126
|
+
const fm = normalized.slice(4, end);
|
|
127
|
+
const body = normalized.slice(end + 4).replace(/^\n/, "");
|
|
124
128
|
const out: { name?: string; description?: string } = {};
|
|
125
129
|
const lines = fm.split("\n");
|
|
126
130
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -457,6 +461,18 @@ function shouldSkipDuplicateExtensionLoad(
|
|
|
457
461
|
return existingSource !== currentSource;
|
|
458
462
|
}
|
|
459
463
|
|
|
464
|
+
function closeSkillRegistryWatchers(): void {
|
|
465
|
+
for (const watcher of activeWatchers) {
|
|
466
|
+
try {
|
|
467
|
+
watcher.close();
|
|
468
|
+
} catch {
|
|
469
|
+
// Best-effort shutdown; stale handles must not block process exit.
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
activeWatchers.clear();
|
|
473
|
+
watchedCwds.clear();
|
|
474
|
+
}
|
|
475
|
+
|
|
460
476
|
async function startSkillRegistryWatcher(
|
|
461
477
|
cwd: string,
|
|
462
478
|
notify: (message: string) => void,
|
|
@@ -485,7 +501,8 @@ async function startSkillRegistryWatcher(
|
|
|
485
501
|
};
|
|
486
502
|
for (const dir of dirs) {
|
|
487
503
|
try {
|
|
488
|
-
watch(dir, { recursive: true }, refresh);
|
|
504
|
+
const watcher = watch(dir, { recursive: true }, refresh);
|
|
505
|
+
activeWatchers.add(watcher);
|
|
489
506
|
} catch {
|
|
490
507
|
// Some filesystems do not support recursive watches; session_start/manual refresh still work.
|
|
491
508
|
}
|
|
@@ -503,11 +520,20 @@ export const __testing = {
|
|
|
503
520
|
renderRegistry,
|
|
504
521
|
shouldSkipSkillRegistryStartup,
|
|
505
522
|
shouldSkipDuplicateExtensionLoad,
|
|
523
|
+
startSkillRegistryWatcher,
|
|
524
|
+
closeSkillRegistryWatchers,
|
|
525
|
+
activeWatcherCount() {
|
|
526
|
+
return activeWatchers.size;
|
|
527
|
+
},
|
|
506
528
|
};
|
|
507
529
|
|
|
508
530
|
export default function (pi: ExtensionAPI) {
|
|
509
531
|
if (shouldSkipDuplicateExtensionLoad()) return;
|
|
510
532
|
|
|
533
|
+
pi.on("session_shutdown", () => {
|
|
534
|
+
closeSkillRegistryWatchers();
|
|
535
|
+
});
|
|
536
|
+
|
|
511
537
|
pi.registerFlag(NO_SKILL_REGISTRY_FLAG, {
|
|
512
538
|
description: "Skip the Gentle AI skill registry refresh and watcher on startup.",
|
|
513
539
|
type: "boolean",
|
|
@@ -532,9 +558,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
532
558
|
"warning",
|
|
533
559
|
);
|
|
534
560
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
561
|
+
if (ctx.hasUI) {
|
|
562
|
+
await startSkillRegistryWatcher(ctx.cwd, (message) => {
|
|
563
|
+
ctx.ui.notify(message, "info");
|
|
564
|
+
});
|
|
565
|
+
}
|
|
538
566
|
if (quarantinedLegacy) {
|
|
539
567
|
setTimeout(() => {
|
|
540
568
|
void (async () => {
|
|
@@ -4,10 +4,31 @@ import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import { exec } from "node:child_process";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
|
-
import { readFile } from "node:fs/promises";
|
|
7
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
|
|
10
10
|
const execAsync = promisify(exec);
|
|
11
|
+
const PI_AGENT_DIR = join(os.homedir(), ".pi", "agent");
|
|
12
|
+
const PI_NPM_DIR = join(PI_AGENT_DIR, "npm", "node_modules");
|
|
13
|
+
|
|
14
|
+
type BannerColor = "pink" | "cyan" | "yellow" | "green";
|
|
15
|
+
interface BannerConfig {
|
|
16
|
+
showRose: boolean;
|
|
17
|
+
showTextLogo: boolean;
|
|
18
|
+
color: BannerColor;
|
|
19
|
+
}
|
|
20
|
+
const DEFAULT_BANNER_CONFIG: BannerConfig = {
|
|
21
|
+
showRose: true,
|
|
22
|
+
showTextLogo: true,
|
|
23
|
+
color: "pink",
|
|
24
|
+
};
|
|
25
|
+
const BANNER_COLORS: BannerColor[] = ["pink", "cyan", "yellow", "green"];
|
|
26
|
+
const BANNER_PALETTES: Record<BannerColor, { rose: [number, number, number]; label: [number, number, number]; value: [number, number, number]; logoFresh: [number, number, number]; logoDim: [number, number, number] }> = {
|
|
27
|
+
pink: { rose: [255, 118, 195], label: [200, 100, 160], value: [255, 140, 210], logoFresh: [255, 138, 206], logoDim: [95, 30, 60] },
|
|
28
|
+
cyan: { rose: [95, 210, 255], label: [85, 170, 205], value: [130, 225, 255], logoFresh: [105, 220, 255], logoDim: [25, 80, 100] },
|
|
29
|
+
yellow: { rose: [255, 210, 95], label: [210, 165, 65], value: [255, 225, 135], logoFresh: [255, 215, 105], logoDim: [105, 75, 25] },
|
|
30
|
+
green: { rose: [110, 220, 145], label: [85, 175, 115], value: [145, 240, 170], logoFresh: [120, 230, 150], logoDim: [30, 95, 50] },
|
|
31
|
+
};
|
|
11
32
|
|
|
12
33
|
const TEXT_LOGO = [
|
|
13
34
|
" ▄▄▄▀▀▀▀▀██ ▄▄▀▄▄ ▄▄█▀▀▀██ ▀▀█▄ ▄▄▄",
|
|
@@ -47,6 +68,43 @@ function rgb(r: number, g: number, b: number, text: string): string {
|
|
|
47
68
|
return `\x1b[38;2;${r};${g};${b}m${text}\x1b[39m`;
|
|
48
69
|
}
|
|
49
70
|
|
|
71
|
+
function gentleAiConfigHome(): string {
|
|
72
|
+
return process.env.GENTLE_PI_CONFIG_HOME ?? join(os.homedir(), ".pi", "gentle-ai");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function bannerConfigPath(): string {
|
|
76
|
+
return join(gentleAiConfigHome(), "banner.json");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeBannerConfig(value: unknown): BannerConfig {
|
|
80
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return { ...DEFAULT_BANNER_CONFIG };
|
|
81
|
+
const record = value as Record<string, unknown>;
|
|
82
|
+
return {
|
|
83
|
+
showRose: typeof record.showRose === "boolean" ? record.showRose : DEFAULT_BANNER_CONFIG.showRose,
|
|
84
|
+
showTextLogo: typeof record.showTextLogo === "boolean" ? record.showTextLogo : DEFAULT_BANNER_CONFIG.showTextLogo,
|
|
85
|
+
color: BANNER_COLORS.includes(record.color as BannerColor) ? record.color as BannerColor : DEFAULT_BANNER_CONFIG.color,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function readBannerConfig(): Promise<BannerConfig> {
|
|
90
|
+
try {
|
|
91
|
+
return normalizeBannerConfig(JSON.parse(await readFile(bannerConfigPath(), "utf8")));
|
|
92
|
+
} catch {
|
|
93
|
+
return { ...DEFAULT_BANNER_CONFIG };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function writeBannerConfig(config: BannerConfig): Promise<void> {
|
|
98
|
+
const path = bannerConfigPath();
|
|
99
|
+
await mkdir(join(path, ".."), { recursive: true });
|
|
100
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function paletteColor(color: BannerColor, key: keyof typeof BANNER_PALETTES[BannerColor], text: string): string {
|
|
104
|
+
const [r, g, b] = BANNER_PALETTES[color][key];
|
|
105
|
+
return rgb(r, g, b, text);
|
|
106
|
+
}
|
|
107
|
+
|
|
50
108
|
function normalizeAscii(lines: string[]): string[] {
|
|
51
109
|
const trimmed = lines.map((l) => l.replace(/\s+$/g, ""));
|
|
52
110
|
const nonEmpty = trimmed.filter((l) => l.trim().length > 0);
|
|
@@ -433,7 +491,107 @@ function currentIntroMode(): IntroMode {
|
|
|
433
491
|
return pickIntroMode(rows, cols);
|
|
434
492
|
}
|
|
435
493
|
|
|
494
|
+
async function countSddAgents(): Promise<number> {
|
|
495
|
+
try {
|
|
496
|
+
const entries = await readdir(join(PI_AGENT_DIR, "agents"), { withFileTypes: true });
|
|
497
|
+
return entries.filter((entry) => entry.isFile() && /^sdd-.*\.md$/.test(entry.name)).length;
|
|
498
|
+
} catch {
|
|
499
|
+
return 0;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function packageNameFromSpec(spec: unknown): string | undefined {
|
|
504
|
+
if (typeof spec !== "string") return undefined;
|
|
505
|
+
const clean = spec.replace(/^npm:/, "");
|
|
506
|
+
if (clean.startsWith("@")) {
|
|
507
|
+
const parts = clean.split("@");
|
|
508
|
+
return parts.length > 2 ? `@${parts[1]}` : clean;
|
|
509
|
+
}
|
|
510
|
+
return clean.split("@")[0] || undefined;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function countPackageExtensions(packages: unknown[]): Promise<number> {
|
|
514
|
+
let count = 0;
|
|
515
|
+
for (const spec of packages) {
|
|
516
|
+
const name = packageNameFromSpec(spec);
|
|
517
|
+
if (!name) continue;
|
|
518
|
+
try {
|
|
519
|
+
const raw = await readFile(join(PI_NPM_DIR, name, "package.json"), "utf8");
|
|
520
|
+
const pkg = JSON.parse(raw);
|
|
521
|
+
const extensions = pkg?.pi?.extensions;
|
|
522
|
+
if (Array.isArray(extensions)) count += extensions.length;
|
|
523
|
+
} catch {
|
|
524
|
+
// Packages installed from non-npm sources may not live in PI_NPM_DIR.
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return count;
|
|
528
|
+
}
|
|
529
|
+
|
|
436
530
|
export default function (pi: ExtensionAPI) {
|
|
531
|
+
const notifyBannerConfig = (ctx: any, config: BannerConfig) => {
|
|
532
|
+
ctx.ui.notify(
|
|
533
|
+
[
|
|
534
|
+
`Startup banner: rose=${config.showRose ? "on" : "off"}, text logo=${config.showTextLogo ? "on" : "off"}, color=${config.color}`,
|
|
535
|
+
`Config: ${bannerConfigPath()}`,
|
|
536
|
+
"Changes apply on the next startup banner render.",
|
|
537
|
+
].join("\n"),
|
|
538
|
+
"info",
|
|
539
|
+
);
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const registerBannerCommand = (name: string) => {
|
|
543
|
+
pi.registerCommand(name, {
|
|
544
|
+
description: "Configure the Gentle Pi startup banner.",
|
|
545
|
+
handler: async (_args, ctx) => {
|
|
546
|
+
const config = await readBannerConfig();
|
|
547
|
+
const selected = await ctx.ui.select("Startup banner", [
|
|
548
|
+
`Rose: ${config.showRose ? "on" : "off"}`,
|
|
549
|
+
`Text logo: ${config.showTextLogo ? "on" : "off"}`,
|
|
550
|
+
`Color: ${config.color}`,
|
|
551
|
+
]);
|
|
552
|
+
if (!selected) return;
|
|
553
|
+
if (selected.startsWith("Rose:")) config.showRose = !config.showRose;
|
|
554
|
+
else if (selected.startsWith("Text logo:")) config.showTextLogo = !config.showTextLogo;
|
|
555
|
+
else if (selected.startsWith("Color:")) {
|
|
556
|
+
config.color = await ctx.ui.select("Startup banner color", [...BANNER_COLORS]) as BannerColor;
|
|
557
|
+
}
|
|
558
|
+
await writeBannerConfig(config);
|
|
559
|
+
notifyBannerConfig(ctx, config);
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
};
|
|
563
|
+
const registerToggleCommand = (name: string, key: "showRose" | "showTextLogo") => {
|
|
564
|
+
pi.registerCommand(name, {
|
|
565
|
+
description: `Toggle startup banner ${key === "showRose" ? "rose" : "text logo"}.`,
|
|
566
|
+
handler: async (_args, ctx) => {
|
|
567
|
+
const config = await readBannerConfig();
|
|
568
|
+
config[key] = !config[key];
|
|
569
|
+
await writeBannerConfig(config);
|
|
570
|
+
notifyBannerConfig(ctx, config);
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
};
|
|
574
|
+
const registerColorCommand = (name: string) => {
|
|
575
|
+
pi.registerCommand(name, {
|
|
576
|
+
description: "Set startup banner color preset.",
|
|
577
|
+
handler: async (args, ctx) => {
|
|
578
|
+
const config = await readBannerConfig();
|
|
579
|
+
const requested = String(args ?? "").trim() as BannerColor;
|
|
580
|
+
config.color = BANNER_COLORS.includes(requested)
|
|
581
|
+
? requested
|
|
582
|
+
: await ctx.ui.select("Startup banner color", [...BANNER_COLORS]) as BannerColor;
|
|
583
|
+
await writeBannerConfig(config);
|
|
584
|
+
notifyBannerConfig(ctx, config);
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
};
|
|
588
|
+
for (const prefix of ["gentle", "gentle-ai"] as const) {
|
|
589
|
+
registerBannerCommand(`${prefix}:banner`);
|
|
590
|
+
registerToggleCommand(`${prefix}:toggle-rose`, "showRose");
|
|
591
|
+
registerToggleCommand(`${prefix}:toggle-text-logo`, "showTextLogo");
|
|
592
|
+
registerColorCommand(`${prefix}:banner-color`);
|
|
593
|
+
}
|
|
594
|
+
|
|
437
595
|
pi.on("session_start", async (_event, ctx) => {
|
|
438
596
|
if (!ctx.hasUI) return;
|
|
439
597
|
|
|
@@ -451,6 +609,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
451
609
|
|
|
452
610
|
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
453
611
|
|
|
612
|
+
const bannerConfig = await readBannerConfig();
|
|
613
|
+
const palette = BANNER_PALETTES[bannerConfig.color];
|
|
454
614
|
const roseBase = padLines(normalizeAscii(ROSE_LARGE_RAW));
|
|
455
615
|
const logoBase = padLines(TEXT_LOGO);
|
|
456
616
|
|
|
@@ -458,6 +618,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
458
618
|
let mcpServersCount = 0;
|
|
459
619
|
let extensionsCount = 0;
|
|
460
620
|
let packagesCount = 0;
|
|
621
|
+
let sddAgentsCount = 0;
|
|
461
622
|
|
|
462
623
|
const allCommands = pi.getCommands();
|
|
463
624
|
const skills = allCommands.filter((c) => c.source === "skill");
|
|
@@ -493,15 +654,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
493
654
|
setTimeout(() => {
|
|
494
655
|
(async () => {
|
|
495
656
|
try {
|
|
657
|
+
sddAgentsCount = await countSddAgents();
|
|
496
658
|
const raw = await readFile(
|
|
497
|
-
join(
|
|
659
|
+
join(PI_AGENT_DIR, "settings.json"),
|
|
498
660
|
"utf8",
|
|
499
661
|
);
|
|
500
662
|
const cfg = JSON.parse(raw);
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
packagesCount = Array.isArray(cfg.packages) ? cfg.packages.length : 0;
|
|
663
|
+
const packages = Array.isArray(cfg.packages) ? cfg.packages : [];
|
|
664
|
+
packagesCount = packages.length;
|
|
665
|
+
extensionsCount = await countPackageExtensions(packages);
|
|
505
666
|
} catch {
|
|
506
667
|
extensionsCount = 0;
|
|
507
668
|
packagesCount = 0;
|
|
@@ -595,7 +756,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
595
756
|
const sideBySideMinWidth = roseBase.width + 3 + logoBase.width + 4;
|
|
596
757
|
const wideStatsMinWidth = 122;
|
|
597
758
|
const horizontal =
|
|
598
|
-
state.mode === "full" && width >= sideBySideMinWidth;
|
|
759
|
+
state.mode === "full" && bannerConfig.showRose && bannerConfig.showTextLogo && width >= sideBySideMinWidth;
|
|
599
760
|
const wideStats = width >= wideStatsMinWidth;
|
|
600
761
|
|
|
601
762
|
const b = new LayoutBuilder();
|
|
@@ -603,7 +764,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
603
764
|
b.center(width);
|
|
604
765
|
|
|
605
766
|
if (state.mode === "minimal") {
|
|
606
|
-
for (let logoI = 0; logoI < logoBase.lines.length; logoI++) {
|
|
767
|
+
if (bannerConfig.showTextLogo) for (let logoI = 0; logoI < logoBase.lines.length; logoI++) {
|
|
607
768
|
const logoLine = logoBase.lines[logoI];
|
|
608
769
|
b.addRow();
|
|
609
770
|
b.lines[b.lines.length - 1].push(
|
|
@@ -660,8 +821,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
660
821
|
b.center(width);
|
|
661
822
|
}
|
|
662
823
|
} else {
|
|
663
|
-
const showBanner = width >= logoBase.width + 2;
|
|
664
|
-
const showRose = width >= roseBase.width + 2;
|
|
824
|
+
const showBanner = bannerConfig.showTextLogo && width >= logoBase.width + 2;
|
|
825
|
+
const showRose = bannerConfig.showRose && width >= roseBase.width + 2;
|
|
665
826
|
if (showBanner) {
|
|
666
827
|
for (let logoI = 0; logoI < logoBase.lines.length; logoI++) {
|
|
667
828
|
const logoLine = logoBase.lines[logoI];
|
|
@@ -690,7 +851,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
690
851
|
}
|
|
691
852
|
}
|
|
692
853
|
|
|
693
|
-
if (state.mode === "full") {
|
|
854
|
+
if (state.mode === "full" || (!bannerConfig.showRose && !bannerConfig.showTextLogo)) {
|
|
694
855
|
b.addRow();
|
|
695
856
|
b.center(width);
|
|
696
857
|
|
|
@@ -720,8 +881,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
720
881
|
["GIT:", gitBranch],
|
|
721
882
|
["PATH:", ctx.cwd],
|
|
722
883
|
["MCP:", `${mcpServersCount} server(s)`],
|
|
884
|
+
["AGENTS:", `${sddAgentsCount} phases`],
|
|
723
885
|
["PLUGINS:", `${packagesCount} package(s)`],
|
|
724
|
-
["
|
|
886
|
+
["SKILLS:", `${skills.length} loaded`],
|
|
725
887
|
["EXTENSIONS:", `${extensionsCount} active`],
|
|
726
888
|
["VER:", `v${VERSION}`],
|
|
727
889
|
["TOOLS:", `${customTools.length} custom`],
|
|
@@ -752,22 +914,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
752
914
|
);
|
|
753
915
|
addWideRow(
|
|
754
916
|
"AGENTS:",
|
|
755
|
-
`${
|
|
917
|
+
`${sddAgentsCount} phases`,
|
|
756
918
|
"EXTENSIONS:",
|
|
757
919
|
`${extensionsCount} active`,
|
|
758
920
|
);
|
|
759
921
|
addWideRow(
|
|
760
|
-
"
|
|
761
|
-
|
|
922
|
+
"SKILLS:",
|
|
923
|
+
`${skills.length} loaded`,
|
|
762
924
|
"TOOLS:",
|
|
763
925
|
`${customTools.length} custom`,
|
|
764
926
|
);
|
|
927
|
+
addWideRow("VER:", `v${VERSION}`, "", "");
|
|
765
928
|
} else {
|
|
766
929
|
addNarrowRow("GIT:", gitBranch);
|
|
767
930
|
addNarrowRow("PATH:", ctx.cwd);
|
|
768
931
|
addNarrowRow("MCP:", `${mcpServersCount} server(s)`);
|
|
769
932
|
addNarrowRow("PLUGINS:", `${packagesCount} package(s)`);
|
|
770
|
-
addNarrowRow("AGENTS:", `${
|
|
933
|
+
addNarrowRow("AGENTS:", `${sddAgentsCount} phases`);
|
|
934
|
+
addNarrowRow("SKILLS:", `${skills.length} loaded`);
|
|
771
935
|
addNarrowRow("EXTENSIONS:", `${extensionsCount} active`);
|
|
772
936
|
addNarrowRow("VER:", `v${VERSION}`);
|
|
773
937
|
addNarrowRow("TOOLS:", `${customTools.length} custom`);
|
|
@@ -834,9 +998,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
834
998
|
const k = Math.max(0.01, roseOpacity * pulse);
|
|
835
999
|
const f = flashPhase ** 0.4;
|
|
836
1000
|
|
|
837
|
-
const rBase = Math.floor(
|
|
838
|
-
const gBase = Math.floor(
|
|
839
|
-
const bBase = Math.floor(
|
|
1001
|
+
const rBase = Math.floor(palette.rose[0] * k);
|
|
1002
|
+
const gBase = Math.floor(palette.rose[1] * k);
|
|
1003
|
+
const bBase = Math.floor(palette.rose[2] * k);
|
|
840
1004
|
|
|
841
1005
|
if (f > 0.85) {
|
|
842
1006
|
line += `\x1b[1m\x1b[38;2;255;255;255m${cell.char}\x1b[0m`;
|
|
@@ -874,22 +1038,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
874
1038
|
line += `\x1b[1m` + rgb(255, 205, 238, cell.char) + `\x1b[22m`;
|
|
875
1039
|
} else if (cell.type === "logo-fresh") {
|
|
876
1040
|
line += cell.char === "▒"
|
|
877
|
-
?
|
|
878
|
-
:
|
|
1041
|
+
? paletteColor(bannerConfig.color, "logoDim", cell.char)
|
|
1042
|
+
: paletteColor(bannerConfig.color, "logoFresh", cell.char);
|
|
879
1043
|
} else {
|
|
880
1044
|
line += cell.char === "▒"
|
|
881
|
-
?
|
|
882
|
-
:
|
|
1045
|
+
? paletteColor(bannerConfig.color, "logoDim", cell.char)
|
|
1046
|
+
: paletteColor(bannerConfig.color, "value", cell.char);
|
|
883
1047
|
}
|
|
884
1048
|
continue;
|
|
885
1049
|
}
|
|
886
1050
|
|
|
887
1051
|
switch (cell.type) {
|
|
888
1052
|
case "label":
|
|
889
|
-
line +=
|
|
1053
|
+
line += paletteColor(bannerConfig.color, "label", cell.char);
|
|
890
1054
|
break;
|
|
891
1055
|
case "value":
|
|
892
|
-
line +=
|
|
1056
|
+
line += paletteColor(bannerConfig.color, "value", cell.char);
|
|
893
1057
|
break;
|
|
894
1058
|
case "dim":
|
|
895
1059
|
line += theme.fg("dim", cell.char);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
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",
|
|
@@ -6,11 +6,13 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
const root = join(fileURLToPath(new URL("..", import.meta.url)));
|
|
7
7
|
|
|
8
8
|
const requiredPaths = [
|
|
9
|
+
"assets/orchestrator.md",
|
|
9
10
|
"assets/agents/sdd-apply.md",
|
|
10
11
|
"assets/agents/sdd-archive.md",
|
|
11
12
|
"assets/agents/sdd-design.md",
|
|
12
13
|
"assets/agents/sdd-explore.md",
|
|
13
14
|
"assets/agents/sdd-init.md",
|
|
15
|
+
"assets/agents/sdd-onboard.md",
|
|
14
16
|
"assets/agents/sdd-proposal.md",
|
|
15
17
|
"assets/agents/sdd-spec.md",
|
|
16
18
|
"assets/agents/sdd-sync.md",
|
|
@@ -29,7 +31,15 @@ const requiredPaths = [
|
|
|
29
31
|
"prompts/gis.md",
|
|
30
32
|
"prompts/gpr.md",
|
|
31
33
|
"prompts/gwr.md",
|
|
34
|
+
"skills/branch-pr/SKILL.md",
|
|
35
|
+
"skills/chained-pr/SKILL.md",
|
|
36
|
+
"skills/cognitive-doc-design/SKILL.md",
|
|
37
|
+
"skills/comment-writer/SKILL.md",
|
|
32
38
|
"skills/gentle-ai/SKILL.md",
|
|
39
|
+
"skills/issue-creation/SKILL.md",
|
|
40
|
+
"skills/judgment-day/SKILL.md",
|
|
41
|
+
"skills/release/SKILL.md",
|
|
42
|
+
"skills/skill-registry/SKILL.md",
|
|
33
43
|
"skills/work-unit-commits/SKILL.md",
|
|
34
44
|
];
|
|
35
45
|
|
|
@@ -27,7 +27,7 @@ Use it for:
|
|
|
27
27
|
| Keep it short | Prefer 1 to 3 short paragraphs or a tight bullet list. |
|
|
28
28
|
| Explain why | Give the technical reason when asking for a change. |
|
|
29
29
|
| Avoid pile-ons | Comment on the highest-value issue, not every tiny preference. |
|
|
30
|
-
| Match
|
|
30
|
+
| Match target context language | Write in the target context language by default: Spanish issue/thread -> Spanish comment, English issue/thread -> English comment, mixed context -> target message language. If the user explicitly requests a language or tone, follow that request. Do not use the active persona as the source of truth for public comments. For Spanish comments, use neutral/professional Spanish by default unless the user or target context clearly calls for regional tone. |
|
|
31
31
|
| No em dashes | Use commas, periods, or parentheses instead. |
|
|
32
32
|
|
|
33
33
|
## Comment Formula
|
|
@@ -45,25 +45,25 @@ Use it for:
|
|
|
45
45
|
### Request change
|
|
46
46
|
|
|
47
47
|
```markdown
|
|
48
|
-
|
|
48
|
+
Good approach overall. I'd split this into a separate commit because it mixes validation logic with UI wiring.
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
That keeps the reviewer's focus narrower and makes rollback cleaner if the integration fails.
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
### Approve with a note
|
|
54
54
|
|
|
55
55
|
```markdown
|
|
56
|
-
|
|
56
|
+
Approved. The scope is clear and the change is well-contained.
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
For the next PR, add links to the previous and following PRs so the chain stays navigable.
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
### Ask for split
|
|
62
62
|
|
|
63
63
|
```markdown
|
|
64
|
-
|
|
64
|
+
This PR exceeds the 400-line budget, so we need to split it or justify `size:exception`.
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
Suggested order: foundation + tests first, then integration, then docs. That gives each review a clear start and end.
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
## Commands
|
|
@@ -49,7 +49,14 @@ test("orchestrator keeps conversation language separate from generated artifact
|
|
|
49
49
|
);
|
|
50
50
|
assert.match(
|
|
51
51
|
orchestrator,
|
|
52
|
-
/Generated artifacts[\s\S]*default to English, regardless of the user's conversation language/,
|
|
52
|
+
/Generated technical artifacts[\s\S]*default to English, regardless of the user's conversation language or active persona/,
|
|
53
|
+
);
|
|
54
|
+
for (const artifactScope of ["code comments", "tests", "fixtures", "delegated phase outputs"]) {
|
|
55
|
+
assert.match(orchestrator, new RegExp(artifactScope));
|
|
56
|
+
}
|
|
57
|
+
assert.match(
|
|
58
|
+
orchestrator,
|
|
59
|
+
/Public\/contextual comments and replies[\s\S]*target context language by default/,
|
|
53
60
|
);
|
|
54
61
|
});
|
|
55
62
|
|
|
@@ -89,3 +96,35 @@ test("persistent harness prompt assets do not hardcode Spanish SDD artifact copy
|
|
|
89
96
|
|
|
90
97
|
assert.deepEqual(failures, []);
|
|
91
98
|
});
|
|
99
|
+
|
|
100
|
+
test("comment-writer is context-reactive and neutral by default for Spanish comments", async () => {
|
|
101
|
+
const skill = await readFile(join(ROOT, "skills/comment-writer/SKILL.md"), "utf8");
|
|
102
|
+
|
|
103
|
+
for (const required of [
|
|
104
|
+
"target context language",
|
|
105
|
+
"explicitly requests a language",
|
|
106
|
+
"neutral/professional Spanish by default",
|
|
107
|
+
]) {
|
|
108
|
+
assert.match(skill, new RegExp(required.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const regionalDefault of [
|
|
112
|
+
/\bAcá\b/,
|
|
113
|
+
/\bagregá\b/,
|
|
114
|
+
/\bpodés\b/,
|
|
115
|
+
/\btenés\b/,
|
|
116
|
+
/\bfijate\b/,
|
|
117
|
+
/\bdale\b/,
|
|
118
|
+
/\bquerés\b/i,
|
|
119
|
+
]) {
|
|
120
|
+
assert.doesNotMatch(skill, regionalDefault);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const englishExample of [
|
|
124
|
+
"Good approach overall",
|
|
125
|
+
"Approved. The scope is clear",
|
|
126
|
+
"This PR exceeds the 400-line budget",
|
|
127
|
+
]) {
|
|
128
|
+
assert.match(skill, new RegExp(englishExample.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { __testing } from "../extensions/gentle-ai.ts";
|
|
7
|
+
|
|
8
|
+
function writeMarkdown(path: string, content: string): void {
|
|
9
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
10
|
+
writeFileSync(path, content);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test("agent discovery skips skills directories", async (t) => {
|
|
14
|
+
const root = mkdtempSync(join(tmpdir(), "gentle-pi-agents-"));
|
|
15
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
16
|
+
const dotAgents = join(root, ".agents");
|
|
17
|
+
writeMarkdown(join(dotAgents, "reviewer.md"), "name: reviewer\n");
|
|
18
|
+
writeMarkdown(join(dotAgents, "team", "worker.md"), "name: worker\n");
|
|
19
|
+
writeMarkdown(join(dotAgents, "skills", "ai-sdk", "SKILL.md"), "name: ai-sdk\n");
|
|
20
|
+
writeMarkdown(
|
|
21
|
+
join(dotAgents, "skills", "ai-sdk", "references", "evaluation.md"),
|
|
22
|
+
"name: Prompt Evaluation\n",
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const syncAgents = __testing.listAgentsFromDir(dotAgents, "user");
|
|
26
|
+
const asyncAgents = await __testing.listAgentsFromDirAsync(dotAgents, "user");
|
|
27
|
+
|
|
28
|
+
assert.deepEqual(
|
|
29
|
+
syncAgents.map((agent) => agent.name),
|
|
30
|
+
["reviewer", "worker"],
|
|
31
|
+
);
|
|
32
|
+
assert.deepEqual(
|
|
33
|
+
asyncAgents.map((agent) => agent.name),
|
|
34
|
+
["reviewer", "worker"],
|
|
35
|
+
);
|
|
36
|
+
});
|