diff --git a/README.md b/README.md index 2784568..dca4667 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,8 @@ mosaic yolo pi # Pi in yolo mode The launcher verifies your config, checks for `SOUL.md`, injects your `AGENTS.md` standards into the runtime, and forwards all arguments. +Pi launches default to a token-lean skill posture: `mosaic pi` passes `--no-skills` so Pi does not preload every global skill description into the system prompt. Use `MOSAIC_PI_SKILL_MODE=all mosaic pi` for the legacy all-skills catalog, or `MOSAIC_PI_SKILL_MODE=discover mosaic pi` to let Pi use its native settings/project skill discovery. + ### TUI & Gateway ```bash diff --git a/packages/mosaic/framework/tools/git/ci-queue-wait.sh b/packages/mosaic/framework/tools/git/ci-queue-wait.sh index c75de5d..266577b 100755 --- a/packages/mosaic/framework/tools/git/ci-queue-wait.sh +++ b/packages/mosaic/framework/tools/git/ci-queue-wait.sh @@ -137,7 +137,7 @@ gitea_get_branch_head_sha() { local branch="$3" local token="$4" local url="https://${host}/api/v1/repos/${repo}/branches/${branch}" - curl -fsS -H "Authorization: token ${token}" "$url" | python3 -c ' + curl -fsSL -H "Authorization: token ${token}" "$url" | python3 -c ' import json, sys data = json.load(sys.stdin) commit = data.get("commit") or {} @@ -151,7 +151,7 @@ gitea_get_commit_status_json() { local sha="$3" local token="$4" local url="https://${host}/api/v1/repos/${repo}/commits/${sha}/status" - curl -fsS -H "Authorization: token ${token}" "$url" + curl -fsSL -H "Authorization: token ${token}" "$url" } while [[ $# -gt 0 ]]; do diff --git a/packages/mosaic/framework/tools/git/pr-ci-wait.sh b/packages/mosaic/framework/tools/git/pr-ci-wait.sh index 82f33bb..50c8b21 100755 --- a/packages/mosaic/framework/tools/git/pr-ci-wait.sh +++ b/packages/mosaic/framework/tools/git/pr-ci-wait.sh @@ -124,7 +124,7 @@ gitea_get_pr_head_sha() { local repo="$2" local token="$3" local url="https://${host}/api/v1/repos/${repo}/pulls/${PR_NUMBER}" - curl -fsS -H "Authorization: token ${token}" "$url" | python3 -c ' + curl -fsSL -H "Authorization: token ${token}" "$url" | python3 -c ' import json, sys data = json.load(sys.stdin) print((data.get("head") or {}).get("sha", "")) @@ -137,7 +137,7 @@ gitea_get_commit_status_json() { local token="$3" local sha="$4" local url="https://${host}/api/v1/repos/${repo}/commits/${sha}/status" - curl -fsS -H "Authorization: token ${token}" "$url" + curl -fsSL -H "Authorization: token ${token}" "$url" } while [[ $# -gt 0 ]]; do diff --git a/packages/mosaic/src/commands/git-wrapper-redirects.spec.ts b/packages/mosaic/src/commands/git-wrapper-redirects.spec.ts new file mode 100644 index 0000000..84f60ff --- /dev/null +++ b/packages/mosaic/src/commands/git-wrapper-redirects.spec.ts @@ -0,0 +1,22 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +const packageRoot = join(import.meta.dirname, '..', '..'); +const gitToolsDir = join(packageRoot, 'framework', 'tools', 'git'); + +function readGitTool(scriptName: string): string { + return readFileSync(join(gitToolsDir, scriptName), 'utf-8'); +} + +describe('Gitea git wrapper API calls', () => { + it.each(['ci-queue-wait.sh', 'pr-ci-wait.sh'])( + '%s follows Gitea API redirects before parsing JSON', + (scriptName) => { + const script = readGitTool(scriptName); + + expect(script).not.toContain('curl -fsS -H "Authorization: token'); + expect(script).toContain('curl -fsSL -H "Authorization: token'); + }, + ); +}); diff --git a/packages/mosaic/src/commands/launch.spec.ts b/packages/mosaic/src/commands/launch.spec.ts index e1846d2..5c7d1e9 100644 --- a/packages/mosaic/src/commands/launch.spec.ts +++ b/packages/mosaic/src/commands/launch.spec.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi, beforeEach, afterEach, type MockInstance } from 'vitest'; import { Command } from 'commander'; -import { registerRuntimeLaunchers, type RuntimeLaunchHandler } from './launch.js'; +import { buildPiSkillArgs, registerRuntimeLaunchers, type RuntimeLaunchHandler } from './launch.js'; /** * Tests for the commander wiring between `mosaic ` / `mosaic yolo ` @@ -22,6 +22,8 @@ function buildProgram(handler: RuntimeLaunchHandler): Command { return program; } +const fakeSkills = ['--skill', '/skills/test-driven-development', '--skill', '/skills/pdf']; + // `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. const exitThrows = (): never => { @@ -63,6 +65,30 @@ 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('keeps explicit user skills while disabling automatic discovery', () => { + expect(buildPiSkillArgs(['--skill', '/tmp/custom'], {}, fakeSkills)).toEqual(['--no-skills']); + }); + + it('supports legacy all-skills mode without double-loading settings skills', () => { + expect(buildPiSkillArgs([], { MOSAIC_PI_SKILL_MODE: 'all' }, fakeSkills)).toEqual([ + '--no-skills', + '--skill', + '/skills/test-driven-development', + '--skill', + '/skills/pdf', + ]); + }); + + it('supports native Pi discovery when explicitly requested', () => { + expect(buildPiSkillArgs([], { MOSAIC_PI_SKILL_MODE: 'discover' }, fakeSkills)).toEqual([]); + }); +}); + describe('registerRuntimeLaunchers — yolo ', () => { let mockExit: MockInstance; let mockError: MockInstance; diff --git a/packages/mosaic/src/commands/launch.ts b/packages/mosaic/src/commands/launch.ts index 76c1702..de8179f 100644 --- a/packages/mosaic/src/commands/launch.ts +++ b/packages/mosaic/src/commands/launch.ts @@ -447,6 +447,32 @@ function discoverPiSkills(): string[] { return args; } +type PiSkillMode = 'none' | 'all' | 'discover'; + +function normalizePiSkillMode(env: NodeJS.ProcessEnv): PiSkillMode { + const value = env['MOSAIC_PI_SKILL_MODE']?.trim().toLowerCase(); + if (value === 'all' || value === 'discover') return value; + return 'none'; +} + +export function buildPiSkillArgs( + _runtimeArgs: string[], + env: NodeJS.ProcessEnv = process.env, + discoveredSkillArgs: string[] = discoverPiSkills(), +): string[] { + const mode = normalizePiSkillMode(env); + + if (mode === 'discover') { + return []; + } + + if (mode === 'all') { + return ['--no-skills', ...discoveredSkillArgs]; + } + + return ['--no-skills']; +} + function discoverPiExtension(): string[] { const ext = join(MOSAIC_HOME, 'runtime', 'pi', 'mosaic-extension.ts'); return existsSync(ext) ? ['--extension', ext] : []; @@ -523,7 +549,7 @@ function launchRuntime(runtime: RuntimeName, args: string[], yolo: boolean): nev case 'pi': { const prompt = buildRuntimePrompt('pi'); const cliArgs = ['--append-system-prompt', prompt]; - cliArgs.push(...discoverPiSkills()); + cliArgs.push(...buildPiSkillArgs(args)); cliArgs.push(...discoverPiExtension()); if (hasMissionNoArgs) { cliArgs.push(missionPrompt);