feat(launch): force-load fleet-critical Pi skills + reconcile skill docs (#555)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful

This commit was merged in pull request #555.
This commit is contained in:
2026-06-19 18:31:02 +00:00
parent 605221d42f
commit 8c45857859
15 changed files with 194 additions and 73 deletions

View File

@@ -1,6 +1,11 @@
import { describe, it, expect, vi, beforeEach, afterEach, type MockInstance } from 'vitest';
import { Command } from 'commander';
import { buildPiSkillArgs, registerRuntimeLaunchers, type RuntimeLaunchHandler } from './launch.js';
import {
buildPiSkillArgs,
piForceSkillNames,
registerRuntimeLaunchers,
type RuntimeLaunchHandler,
} from './launch.js';
/**
* Tests for the commander wiring between `mosaic <runtime>` / `mosaic yolo <runtime>`
@@ -23,6 +28,7 @@ function buildProgram(handler: RuntimeLaunchHandler): Command {
}
const fakeSkills = ['--skill', '/skills/test-driven-development', '--skill', '/skills/pdf'];
const fakeForced = ['--skill', '/skills/mosaic-tools'];
// `process.exit` returns `never`, so vi.spyOn demands a replacement with the
// same signature. We throw from the mock to short-circuit into test-land.
@@ -66,16 +72,42 @@ describe('registerRuntimeLaunchers — non-yolo subcommands', () => {
});
describe('buildPiSkillArgs', () => {
it('defaults to disabling Pi skill discovery to keep startup context small', () => {
expect(buildPiSkillArgs([], {}, fakeSkills)).toEqual(['--no-skills']);
it('disables auto-discovery but force-loads fleet-critical skills by default', () => {
expect(buildPiSkillArgs([], {}, fakeSkills, fakeForced)).toEqual([
'--no-skills',
'--skill',
'/skills/mosaic-tools',
]);
});
it('keeps explicit user skills while disabling automatic discovery', () => {
expect(buildPiSkillArgs(['--skill', '/tmp/custom'], {}, fakeSkills)).toEqual(['--no-skills']);
it('ignores _runtimeArgs (user --skill flags reach Pi via the launch handler, not here)', () => {
expect(buildPiSkillArgs(['--skill', '/tmp/custom'], {}, fakeSkills, fakeForced)).toEqual([
'--no-skills',
'--skill',
'/skills/mosaic-tools',
]);
});
it('supports legacy all-skills mode without double-loading settings skills', () => {
expect(buildPiSkillArgs([], { MOSAIC_PI_SKILL_MODE: 'all' }, fakeSkills)).toEqual([
it('emits only --no-skills when no forced skills are present on disk', () => {
expect(buildPiSkillArgs([], {}, fakeSkills, [])).toEqual(['--no-skills']);
});
it('all-skills mode merges the forced set in without duplicating discovered skills', () => {
expect(buildPiSkillArgs([], { MOSAIC_PI_SKILL_MODE: 'all' }, fakeSkills, fakeForced)).toEqual([
'--no-skills',
'--skill',
'/skills/test-driven-development',
'--skill',
'/skills/pdf',
'--skill',
'/skills/mosaic-tools',
]);
});
it('all-skills mode does not double-load a forced skill already discovered', () => {
expect(
buildPiSkillArgs([], { MOSAIC_PI_SKILL_MODE: 'all' }, fakeSkills, ['--skill', '/skills/pdf']),
).toEqual([
'--no-skills',
'--skill',
'/skills/test-driven-development',
@@ -84,8 +116,27 @@ describe('buildPiSkillArgs', () => {
]);
});
it('supports native Pi discovery when explicitly requested', () => {
expect(buildPiSkillArgs([], { MOSAIC_PI_SKILL_MODE: 'discover' }, fakeSkills)).toEqual([]);
it('force-loads fleet skills even under native Pi discovery', () => {
expect(
buildPiSkillArgs([], { MOSAIC_PI_SKILL_MODE: 'discover' }, fakeSkills, fakeForced),
).toEqual(['--skill', '/skills/mosaic-tools']);
});
});
describe('piForceSkillNames', () => {
it('defaults to mosaic-tools when MOSAIC_PI_FORCE_SKILLS is unset', () => {
expect(piForceSkillNames({})).toEqual(['mosaic-tools']);
});
it('treats an empty string as "disable force-loading" (distinct from unset)', () => {
expect(piForceSkillNames({ MOSAIC_PI_FORCE_SKILLS: '' })).toEqual([]);
});
it('parses a colon list, trimming blanks and whitespace', () => {
expect(piForceSkillNames({ MOSAIC_PI_FORCE_SKILLS: 'mosaic-tools: mosaic-gitea ::' })).toEqual([
'mosaic-tools',
'mosaic-gitea',
]);
});
});

View File

@@ -455,22 +455,78 @@ function normalizePiSkillMode(env: NodeJS.ProcessEnv): PiSkillMode {
return 'none';
}
/**
* Fleet-critical Pi skills that are force-loaded on every Pi launch regardless
* of MOSAIC_PI_SKILL_MODE. They cover the highest-frequency cross-agent and
* git-provider operations where Pi workers historically improvised raw CLIs
* (raw `tmux send-keys`, raw `tea`/`gh`/`glab`) instead of the maintained
* `~/.config/mosaic/tools/` wrappers.
*
* An explicit `--skill <dir>` overrides `--no-skills` for that path, so forcing
* a single targeted skill surfaces the must-use toolkit without loading the full
* ~100-skill catalog (context bloat). Missing skills are skipped silently, so
* this is a no-op until the named skill is synced into ~/.config/mosaic/skills/.
*
* Override with MOSAIC_PI_FORCE_SKILLS (colon-separated skill dir names; set to
* an empty string to disable force-loading entirely).
*/
const DEFAULT_PI_FORCE_SKILLS = ['mosaic-tools'];
export function piForceSkillNames(env: NodeJS.ProcessEnv): string[] {
const override = env['MOSAIC_PI_FORCE_SKILLS'];
if (override === undefined) return DEFAULT_PI_FORCE_SKILLS;
return override
.split(':')
.map((name) => name.trim())
.filter(Boolean);
}
function forcedPiSkillArgs(env: NodeJS.ProcessEnv = process.env): string[] {
const args: string[] = [];
for (const name of piForceSkillNames(env)) {
const skillDir = join(MOSAIC_HOME, 'skills', name);
if (existsSync(join(skillDir, 'SKILL.md'))) {
args.push('--skill', skillDir);
}
}
return args;
}
/** Concatenate `--skill <dir>` arg groups, dropping any directory already seen. */
function mergeSkillArgs(...groups: string[][]): string[] {
const seen = new Set<string>();
const out: string[] = [];
for (const group of groups) {
for (let i = 0; i < group.length; i += 2) {
const dir = group[i + 1];
if (group[i] !== '--skill' || dir === undefined || seen.has(dir)) continue;
seen.add(dir);
out.push('--skill', dir);
}
}
return out;
}
export function buildPiSkillArgs(
_runtimeArgs: string[],
env: NodeJS.ProcessEnv = process.env,
discoveredSkillArgs: string[] = discoverPiSkills(),
forcedSkillArgs: string[] = forcedPiSkillArgs(env),
): string[] {
const mode = normalizePiSkillMode(env);
if (mode === 'discover') {
return [];
// Native Pi discovery handles the rest; still force-load the fleet skills.
return [...forcedSkillArgs];
}
if (mode === 'all') {
return ['--no-skills', ...discoveredSkillArgs];
// 'all' links the full catalog; merge in the forced set so fleet-critical
// skills are guaranteed present even if they live only under skills-local/.
return ['--no-skills', ...mergeSkillArgs(discoveredSkillArgs, forcedSkillArgs)];
}
return ['--no-skills'];
return ['--no-skills', ...forcedSkillArgs];
}
function discoverPiExtension(): string[] {