Compare commits

..

1 Commits

Author SHA1 Message Date
Jarvis
2b99908de4 feat: unify mosaic CLI — single binary, no PATH conflict
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
- Rename framework bash launcher: mosaic → mosaic-launch
- npm CLI (@mosaic/cli) is now the single 'mosaic' binary on PATH
- Add launcher delegation: mosaic {claude,codex,opencode,pi,yolo}
  delegates to mosaic-launch via execFileSync with inherited stdio
- Add delegated management commands: init, doctor, sync, seq,
  bootstrap, coord, upgrade
- install.sh: remove framework bin PATH requirement, clean up old
  mosaic binary on upgrade, simplified summary output
- framework install.sh: preserve sources/ dir (fixes rsync noise
  about agent-skills)
- session-run.sh: update references to mosaic-launch

Users now only need ~/.npm-global/bin on PATH. The npm CLI finds
mosaic-launch at its absolute path (~/.config/mosaic/bin/mosaic-launch).
2026-04-02 19:30:21 -05:00
71 changed files with 1504 additions and 744 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/gateway", "name": "@mosaic/gateway",
"version": "0.0.2", "version": "0.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "dist/main.js", "main": "dist/main.js",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/web", "name": "@mosaic/web",
"version": "0.0.2", "version": "0.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "next build", "build": "next build",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/agent", "name": "@mosaic/agent",
"version": "0.0.2", "version": "0.0.1-alpha.2",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"exports": { "exports": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/auth", "name": "@mosaic/auth",
"version": "0.0.2", "version": "0.0.1-alpha.2",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/brain", "name": "@mosaic/brain",
"version": "0.0.2", "version": "0.0.1-alpha.2",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"exports": { "exports": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/cli", "name": "@mosaic/cli",
"version": "0.0.4", "version": "0.0.1-alpha.2",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@@ -5,7 +5,7 @@ import { Command } from 'commander';
import { createQualityRailsCli } from '@mosaic/quality-rails'; import { createQualityRailsCli } from '@mosaic/quality-rails';
import { registerAgentCommand } from './commands/agent.js'; import { registerAgentCommand } from './commands/agent.js';
import { registerMissionCommand } from './commands/mission.js'; import { registerMissionCommand } from './commands/mission.js';
// prdy is registered via launch.ts import { registerPrdyCommand } from './commands/prdy.js';
import { registerLaunchCommands } from './commands/launch.js'; import { registerLaunchCommands } from './commands/launch.js';
const _require = createRequire(import.meta.url); const _require = createRequire(import.meta.url);
@@ -298,6 +298,10 @@ registerAgentCommand(program);
registerMissionCommand(program); registerMissionCommand(program);
// ─── prdy ──────────────────────────────────────────────────────────────
registerPrdyCommand(program);
// ─── quality-rails ────────────────────────────────────────────────────── // ─── quality-rails ──────────────────────────────────────────────────────
const qrWrapper = createQualityRailsCli(); const qrWrapper = createQualityRailsCli();

View File

@@ -54,31 +54,19 @@ function checkRuntime(cmd: string): void {
function checkSoul(): void { function checkSoul(): void {
const soulPath = join(MOSAIC_HOME, 'SOUL.md'); const soulPath = join(MOSAIC_HOME, 'SOUL.md');
if (!existsSync(soulPath)) { if (!existsSync(soulPath)) {
console.log('[mosaic] SOUL.md not found. Running setup wizard...'); console.log('[mosaic] SOUL.md not found. Running mosaic init...');
const initBin = join(MOSAIC_HOME, 'bin', 'mosaic-init');
// Prefer the TypeScript wizard (idempotent, detects existing files)
try {
const result = spawnSync(process.execPath, [process.argv[1]!, 'wizard'], {
stdio: 'inherit',
});
if (result.status === 0 && existsSync(soulPath)) return;
} catch {
// Fall through to legacy init
}
// Fallback: legacy bash mosaic-init
const initBin = join(MOSAIC_HOME, 'tools', '_scripts', 'mosaic-init');
if (existsSync(initBin)) { if (existsSync(initBin)) {
spawnSync(initBin, [], { stdio: 'inherit' }); spawnSync(initBin, [], { stdio: 'inherit' });
} else { } else {
console.error('[mosaic] Setup failed. Run: mosaic wizard'); console.error('[mosaic] mosaic-init not found. Run: mosaic wizard');
process.exit(1); process.exit(1);
} }
} }
} }
function checkSequentialThinking(runtime: string): void { function checkSequentialThinking(runtime: string): void {
const checker = join(MOSAIC_HOME, 'tools', '_scripts', 'mosaic-ensure-sequential-thinking'); const checker = join(MOSAIC_HOME, 'bin', 'mosaic-ensure-sequential-thinking');
if (!existsSync(checker)) return; // Skip if checker doesn't exist if (!existsSync(checker)) return; // Skip if checker doesn't exist
const result = spawnSync(checker, ['--check', '--runtime', runtime], { stdio: 'ignore' }); const result = spawnSync(checker, ['--check', '--runtime', runtime], { stdio: 'ignore' });
if (result.status !== 0) { if (result.status !== 0) {
@@ -473,191 +461,23 @@ function execRuntime(cmd: string, args: string[]): void {
} }
} }
// ─── Framework script/tool delegation ─────────────────────────────────────── // ─── Framework script delegation (for tools that remain in bash) ─────────────
function delegateToScript(scriptPath: string, args: string[], env?: Record<string, string>): never { function delegateToFrameworkScript(script: string, args: string[]): never {
const scriptPath = join(MOSAIC_HOME, 'bin', script);
if (!existsSync(scriptPath)) { if (!existsSync(scriptPath)) {
console.error(`[mosaic] Script not found: ${scriptPath}`); console.error(`[mosaic] Script not found: ${scriptPath}`);
process.exit(1); process.exit(1);
} }
try { try {
execFileSync('bash', [scriptPath, ...args], { execFileSync(scriptPath, args, { stdio: 'inherit' });
stdio: 'inherit',
env: { ...process.env, ...env },
});
process.exit(0); process.exit(0);
} catch (err) { } catch (err) {
process.exit((err as { status?: number }).status ?? 1); process.exit((err as { status?: number }).status ?? 1);
} }
} }
function fwScript(name: string): string { // ─── Commander registration ──────────────────────────────────────────────────
return join(MOSAIC_HOME, 'tools', '_scripts', name);
}
function toolScript(toolDir: string, name: string): string {
return join(MOSAIC_HOME, 'tools', toolDir, name);
}
// ─── Coord (mission orchestrator) ───────────────────────────────────────────
const COORD_SUBCMDS: Record<string, string> = {
status: 'session-status.sh',
session: 'session-status.sh',
init: 'mission-init.sh',
mission: 'mission-status.sh',
progress: 'mission-status.sh',
continue: 'continue-prompt.sh',
next: 'continue-prompt.sh',
run: 'session-run.sh',
start: 'session-run.sh',
smoke: 'smoke-test.sh',
test: 'smoke-test.sh',
resume: 'session-resume.sh',
recover: 'session-resume.sh',
};
function runCoord(args: string[]): never {
checkMosaicHome();
let runtime = 'claude';
let yoloFlag = '';
const coordArgs: string[] = [];
for (const arg of args) {
if (arg === '--claude' || arg === '--codex' || arg === '--pi') {
runtime = arg.slice(2);
} else if (arg === '--yolo') {
yoloFlag = '--yolo';
} else {
coordArgs.push(arg);
}
}
const subcmd = coordArgs[0] ?? 'help';
const subArgs = coordArgs.slice(1);
const script = COORD_SUBCMDS[subcmd];
if (!script) {
console.log(`mosaic coord — mission coordinator tools
Commands:
init --name <name> [opts] Initialize a new mission
mission [--project <path>] Show mission progress dashboard
status [--project <path>] Check agent session health
continue [--project <path>] Generate continuation prompt
run [--project <path>] Launch runtime with mission context
smoke Run orchestration smoke checks
resume [--project <path>] Crash recovery
Runtime: --claude (default) | --codex | --pi | --yolo`);
process.exit(subcmd === 'help' ? 0 : 1);
}
if (yoloFlag) subArgs.unshift(yoloFlag);
delegateToScript(toolScript('orchestrator', script), subArgs, {
MOSAIC_COORD_RUNTIME: runtime,
});
}
// ─── Prdy (PRD tools via framework scripts) ─────────────────────────────────
const PRDY_SUBCMDS: Record<string, string> = {
init: 'prdy-init.sh',
update: 'prdy-update.sh',
validate: 'prdy-validate.sh',
check: 'prdy-validate.sh',
status: 'prdy-status.sh',
};
function runPrdyLocal(args: string[]): never {
checkMosaicHome();
let runtime = 'claude';
const prdyArgs: string[] = [];
for (const arg of args) {
if (arg === '--claude' || arg === '--codex' || arg === '--pi') {
runtime = arg.slice(2);
} else {
prdyArgs.push(arg);
}
}
const subcmd = prdyArgs[0] ?? 'help';
const subArgs = prdyArgs.slice(1);
const script = PRDY_SUBCMDS[subcmd];
if (!script) {
console.log(`mosaic prdy — PRD creation and validation
Commands:
init [--project <path>] [--name <feature>] Create docs/PRD.md
update [--project <path>] Update existing PRD
validate [--project <path>] Check PRD completeness
status [--project <path>] Quick PRD health check
Runtime: --claude (default) | --codex | --pi`);
process.exit(subcmd === 'help' ? 0 : 1);
}
delegateToScript(toolScript('prdy', script), subArgs, {
MOSAIC_PRDY_RUNTIME: runtime,
});
}
// ─── Seq (sequential-thinking MCP) ──────────────────────────────────────────
function runSeq(args: string[]): never {
checkMosaicHome();
const action = args[0] ?? 'check';
const rest = args.slice(1);
const checker = fwScript('mosaic-ensure-sequential-thinking');
switch (action) {
case 'check':
delegateToScript(checker, ['--check', ...rest]);
break; // unreachable
case 'fix':
case 'apply':
delegateToScript(checker, rest);
break;
case 'start': {
console.log('[mosaic] Starting sequential-thinking MCP server...');
try {
execFileSync('npx', ['-y', '@modelcontextprotocol/server-sequential-thinking', ...rest], {
stdio: 'inherit',
});
process.exit(0);
} catch (err) {
process.exit((err as { status?: number }).status ?? 1);
}
break;
}
default:
console.error(`[mosaic] Unknown seq subcommand '${action}'. Use: check|fix|start`);
process.exit(1);
}
}
// ─── Upgrade ────────────────────────────────────────────────────────────────
function runUpgrade(args: string[]): never {
checkMosaicHome();
const subcmd = args[0];
if (!subcmd || subcmd === 'release') {
delegateToScript(fwScript('mosaic-release-upgrade'), args.slice(subcmd === 'release' ? 1 : 0));
} else if (subcmd === 'check') {
delegateToScript(fwScript('mosaic-release-upgrade'), ['--dry-run', ...args.slice(1)]);
} else if (subcmd === 'project') {
delegateToScript(fwScript('mosaic-upgrade'), args.slice(1));
} else if (subcmd.startsWith('-')) {
delegateToScript(fwScript('mosaic-release-upgrade'), args);
} else {
delegateToScript(fwScript('mosaic-upgrade'), args);
}
}
// ─── Commander registration ─────────────────────────────────────────────────
export function registerLaunchCommands(program: Command): void { export function registerLaunchCommands(program: Command): void {
// Runtime launchers // Runtime launchers
@@ -689,58 +509,15 @@ export function registerLaunchCommands(program: Command): void {
launchRuntime(runtime as RuntimeName, cmd.args, true); launchRuntime(runtime as RuntimeName, cmd.args, true);
}); });
// Coord (mission orchestrator) // Framework management commands (delegate to bash scripts)
program const frameworkCommands: Record<string, { desc: string; script: string }> = {
.command('coord')
.description('Mission coordinator tools (init, status, run, continue, resume)')
.allowUnknownOption(true)
.allowExcessArguments(true)
.action((_opts: unknown, cmd: Command) => {
runCoord(cmd.args);
});
// Prdy (PRD tools via local framework scripts)
program
.command('prdy')
.description('PRD creation and validation (init, update, validate, status)')
.allowUnknownOption(true)
.allowExcessArguments(true)
.action((_opts: unknown, cmd: Command) => {
runPrdyLocal(cmd.args);
});
// Seq (sequential-thinking MCP management)
program
.command('seq')
.description('sequential-thinking MCP management (check/fix/start)')
.allowUnknownOption(true)
.allowExcessArguments(true)
.action((_opts: unknown, cmd: Command) => {
runSeq(cmd.args);
});
// Upgrade (release + project)
program
.command('upgrade')
.description('Upgrade Mosaic release or project files')
.allowUnknownOption(true)
.allowExcessArguments(true)
.action((_opts: unknown, cmd: Command) => {
runUpgrade(cmd.args);
});
// Direct framework script delegates
const directCommands: Record<string, { desc: string; script: string }> = {
init: { desc: 'Generate SOUL.md (agent identity contract)', script: 'mosaic-init' }, init: { desc: 'Generate SOUL.md (agent identity contract)', script: 'mosaic-init' },
doctor: { desc: 'Health audit — detect drift and missing files', script: 'mosaic-doctor' }, doctor: { desc: 'Health audit — detect drift and missing files', script: 'mosaic-doctor' },
sync: { desc: 'Sync skills from canonical source', script: 'mosaic-sync-skills' }, sync: { desc: 'Sync skills from canonical source', script: 'mosaic-sync-skills' },
bootstrap: { bootstrap: { desc: 'Bootstrap a repo with Mosaic standards', script: 'mosaic-bootstrap-repo' },
desc: 'Bootstrap a repo with Mosaic standards',
script: 'mosaic-bootstrap-repo',
},
}; };
for (const [name, { desc, script }] of Object.entries(directCommands)) { for (const [name, { desc, script }] of Object.entries(frameworkCommands)) {
program program
.command(name) .command(name)
.description(desc) .description(desc)
@@ -748,7 +525,7 @@ export function registerLaunchCommands(program: Command): void {
.allowExcessArguments(true) .allowExcessArguments(true)
.action((_opts: unknown, cmd: Command) => { .action((_opts: unknown, cmd: Command) => {
checkMosaicHome(); checkMosaicHome();
delegateToScript(fwScript(script), cmd.args); delegateToFrameworkScript(script, cmd.args);
}); });
} }
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/coord", "name": "@mosaic/coord",
"version": "0.0.2", "version": "0.0.1-alpha.2",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"exports": { "exports": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/db", "name": "@mosaic/db",
"version": "0.0.2", "version": "0.0.1-alpha.2",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/design-tokens", "name": "@mosaic/design-tokens",
"version": "0.0.2", "version": "0.0.1-alpha.2",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/forge", "name": "@mosaic/forge",
"version": "0.0.2", "version": "0.0.1-alpha.2",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/log", "name": "@mosaic/log",
"version": "0.0.2", "version": "0.0.1-alpha.2",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/macp", "name": "@mosaic/macp",
"version": "0.0.2", "version": "0.0.1-alpha.2",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/memory", "name": "@mosaic/memory",
"version": "0.0.2", "version": "0.0.1-alpha.2",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@@ -1,88 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import {
mkdtempSync,
mkdirSync,
writeFileSync,
readFileSync,
existsSync,
chmodSync,
rmSync,
} from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { syncDirectory } from '../../src/platform/file-ops.js';
describe('syncDirectory', () => {
let tmpDir: string;
beforeEach(() => {
tmpDir = mkdtempSync(join(tmpdir(), 'mosaic-file-ops-'));
});
afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true });
});
it('is a no-op when source and target are the same path', () => {
const dir = join(tmpDir, 'same');
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, 'file.txt'), 'hello');
// Should not throw even with read-only files
const gitDir = join(dir, '.git', 'objects', 'pack');
mkdirSync(gitDir, { recursive: true });
const packFile = join(gitDir, 'pack-abc.idx');
writeFileSync(packFile, 'data');
chmodSync(packFile, 0o444);
expect(() => syncDirectory(dir, dir)).not.toThrow();
});
it('skips nested .git directories when excludeGit is true', () => {
const src = join(tmpDir, 'src');
const dest = join(tmpDir, 'dest');
// Create source with a nested .git
mkdirSync(join(src, 'sources', 'skills', '.git', 'objects'), { recursive: true });
writeFileSync(join(src, 'sources', 'skills', '.git', 'objects', 'pack.idx'), 'git-data');
writeFileSync(join(src, 'sources', 'skills', 'SKILL.md'), 'skill content');
writeFileSync(join(src, 'README.md'), 'readme');
syncDirectory(src, dest, { excludeGit: true });
// .git contents should NOT be copied
expect(existsSync(join(dest, 'sources', 'skills', '.git'))).toBe(false);
// Normal files should be copied
expect(readFileSync(join(dest, 'sources', 'skills', 'SKILL.md'), 'utf-8')).toBe(
'skill content',
);
expect(readFileSync(join(dest, 'README.md'), 'utf-8')).toBe('readme');
});
it('copies nested .git directories when excludeGit is false', () => {
const src = join(tmpDir, 'src');
const dest = join(tmpDir, 'dest');
mkdirSync(join(src, 'sub', '.git'), { recursive: true });
writeFileSync(join(src, 'sub', '.git', 'HEAD'), 'ref: refs/heads/main');
syncDirectory(src, dest, { excludeGit: false });
expect(readFileSync(join(dest, 'sub', '.git', 'HEAD'), 'utf-8')).toBe('ref: refs/heads/main');
});
it('respects preserve option', () => {
const src = join(tmpDir, 'src');
const dest = join(tmpDir, 'dest');
mkdirSync(src, { recursive: true });
mkdirSync(dest, { recursive: true });
writeFileSync(join(src, 'SOUL.md'), 'new soul');
writeFileSync(join(dest, 'SOUL.md'), 'old soul');
writeFileSync(join(src, 'README.md'), 'new readme');
syncDirectory(src, dest, { preserve: ['SOUL.md'] });
expect(readFileSync(join(dest, 'SOUL.md'), 'utf-8')).toBe('old soul');
expect(readFileSync(join(dest, 'README.md'), 'utf-8')).toBe('new readme');
});
});

View File

@@ -65,36 +65,4 @@ describe('detectInstallStage', () => {
expect(state.installAction).toBe('keep'); expect(state.installAction).toBe('keep');
expect(state.soul.agentName).toBe('TestAgent'); expect(state.soul.agentName).toBe('TestAgent');
}); });
it('pre-populates state when reconfiguring', async () => {
mkdirSync(join(tmpDir, 'bin'), { recursive: true });
writeFileSync(join(tmpDir, 'SOUL.md'), 'You are **Jarvis** in this session.');
writeFileSync(join(tmpDir, 'USER.md'), '**Name:** TestUser');
const p = new HeadlessPrompter({
'What would you like to do?': 'reconfigure',
});
const state = createState(tmpDir);
await detectInstallStage(p, state, mockConfig);
expect(state.installAction).toBe('reconfigure');
// Existing values loaded as defaults for reconfiguration
expect(state.soul.agentName).toBe('TestAgent');
expect(state.user.userName).toBe('TestUser');
});
it('does not pre-populate state on fresh reset', async () => {
mkdirSync(join(tmpDir, 'bin'), { recursive: true });
writeFileSync(join(tmpDir, 'SOUL.md'), 'You are **Jarvis** in this session.');
const p = new HeadlessPrompter({
'What would you like to do?': 'reset',
});
const state = createState(tmpDir);
await detectInstallStage(p, state, mockConfig);
expect(state.installAction).toBe('reset');
// Reset should NOT load existing values
expect(state.soul.agentName).toBeUndefined();
});
}); });

View File

@@ -113,8 +113,8 @@ echo "[mosaic] Optional: run orchestrator rail via ~/.config/mosaic/bin/mosaic-o
echo "[mosaic] Optional: run detached orchestrator via bash $TARGET_DIR/scripts/agent/orchestrator-daemon.sh start" echo "[mosaic] Optional: run detached orchestrator via bash $TARGET_DIR/scripts/agent/orchestrator-daemon.sh start"
if [[ -n "$QUALITY_TEMPLATE" ]]; then if [[ -n "$QUALITY_TEMPLATE" ]]; then
if [[ -x "$MOSAIC_HOME/tools/_scripts/mosaic-quality-apply" ]]; then if [[ -x "$MOSAIC_HOME/bin/mosaic-quality-apply" ]]; then
"$MOSAIC_HOME/tools/_scripts/mosaic-quality-apply" --template "$QUALITY_TEMPLATE" --target "$TARGET_DIR" "$MOSAIC_HOME/bin/mosaic-quality-apply" --template "$QUALITY_TEMPLATE" --target "$TARGET_DIR"
if [[ -f "$TARGET_DIR/.mosaic/quality-rails.yml" ]]; then if [[ -f "$TARGET_DIR/.mosaic/quality-rails.yml" ]]; then
sed -i "s/^enabled:.*/enabled: true/" "$TARGET_DIR/.mosaic/quality-rails.yml" sed -i "s/^enabled:.*/enabled: true/" "$TARGET_DIR/.mosaic/quality-rails.yml"
sed -i "s/^template:.*/template: \"$QUALITY_TEMPLATE\"/" "$TARGET_DIR/.mosaic/quality-rails.yml" sed -i "s/^template:.*/template: \"$QUALITY_TEMPLATE\"/" "$TARGET_DIR/.mosaic/quality-rails.yml"

View File

@@ -165,18 +165,18 @@ expect_dir "$MOSAIC_HOME/profiles"
expect_dir "$MOSAIC_HOME/templates/agent" expect_dir "$MOSAIC_HOME/templates/agent"
expect_dir "$MOSAIC_HOME/skills" expect_dir "$MOSAIC_HOME/skills"
expect_dir "$MOSAIC_HOME/skills-local" expect_dir "$MOSAIC_HOME/skills-local"
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets" expect_file "$MOSAIC_HOME/bin/mosaic-link-runtime-assets"
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-ensure-sequential-thinking" expect_file "$MOSAIC_HOME/bin/mosaic-ensure-sequential-thinking"
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-sync-skills" expect_file "$MOSAIC_HOME/bin/mosaic-sync-skills"
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-projects" expect_file "$MOSAIC_HOME/bin/mosaic-projects"
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-quality-apply" expect_file "$MOSAIC_HOME/bin/mosaic-quality-apply"
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-quality-verify" expect_file "$MOSAIC_HOME/bin/mosaic-quality-verify"
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-run" expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-run"
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-sync-tasks" expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-sync-tasks"
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-drain" expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-drain"
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-matrix-publish" expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-publish"
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-matrix-consume" expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-consume"
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-matrix-cycle" expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-cycle"
expect_file "$MOSAIC_HOME/tools/git/ci-queue-wait.sh" expect_file "$MOSAIC_HOME/tools/git/ci-queue-wait.sh"
expect_file "$MOSAIC_HOME/tools/git/pr-ci-wait.sh" expect_file "$MOSAIC_HOME/tools/git/pr-ci-wait.sh"
expect_file "$MOSAIC_HOME/tools/orchestrator-matrix/transport/matrix_transport.py" expect_file "$MOSAIC_HOME/tools/orchestrator-matrix/transport/matrix_transport.py"
@@ -215,8 +215,8 @@ check_runtime_contract_file "$HOME/.config/opencode/AGENTS.md" "$MOSAIC_HOME/run
check_runtime_contract_file "$HOME/.codex/instructions.md" "$MOSAIC_HOME/runtime/codex/instructions.md" "codex" check_runtime_contract_file "$HOME/.codex/instructions.md" "$MOSAIC_HOME/runtime/codex/instructions.md" "codex"
# Sequential-thinking MCP hard requirement. # Sequential-thinking MCP hard requirement.
if [[ -x "$MOSAIC_HOME/tools/_scripts/mosaic-ensure-sequential-thinking" ]]; then if [[ -x "$MOSAIC_HOME/bin/mosaic-ensure-sequential-thinking" ]]; then
if "$MOSAIC_HOME/tools/_scripts/mosaic-ensure-sequential-thinking" --check >/dev/null 2>&1; then if "$MOSAIC_HOME/bin/mosaic-ensure-sequential-thinking" --check >/dev/null 2>&1; then
pass "sequential-thinking MCP configured and available" pass "sequential-thinking MCP configured and available"
else else
warn "sequential-thinking MCP missing or misconfigured" warn "sequential-thinking MCP missing or misconfigured"
@@ -422,8 +422,8 @@ with open('$pi_settings_file', 'w') as f:
fi fi
# 4. Run link-runtime-assets if available # 4. Run link-runtime-assets if available
if [[ -x "$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets" ]]; then if [[ -x "$MOSAIC_HOME/bin/mosaic-link-runtime-assets" ]]; then
"$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets" >/dev/null 2>&1 && fix "Re-ran mosaic-link-runtime-assets" "$MOSAIC_HOME/bin/mosaic-link-runtime-assets" >/dev/null 2>&1 && fix "Re-ran mosaic-link-runtime-assets"
fi fi
echo "[mosaic-doctor] fixes=$fix_count" echo "[mosaic-doctor] fixes=$fix_count"

View File

@@ -60,14 +60,12 @@ Options:
--timezone <tz> Your timezone (e.g., "America/Chicago") --timezone <tz> Your timezone (e.g., "America/Chicago")
--non-interactive Fail if any required value is missing (no prompts) --non-interactive Fail if any required value is missing (no prompts)
--soul-only Only generate SOUL.md --soul-only Only generate SOUL.md
--force Overwrite existing files without prompting
-h, --help Show help -h, --help Show help
USAGE USAGE
} }
NON_INTERACTIVE=0 NON_INTERACTIVE=0
SOUL_ONLY=0 SOUL_ONLY=0
FORCE=0
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case "$1" in case "$1" in
@@ -81,7 +79,6 @@ while [[ $# -gt 0 ]]; do
--timezone) TIMEZONE="$2"; shift 2 ;; --timezone) TIMEZONE="$2"; shift 2 ;;
--non-interactive) NON_INTERACTIVE=1; shift ;; --non-interactive) NON_INTERACTIVE=1; shift ;;
--soul-only) SOUL_ONLY=1; shift ;; --soul-only) SOUL_ONLY=1; shift ;;
--force) FORCE=1; shift ;;
-h|--help) usage; exit 0 ;; -h|--help) usage; exit 0 ;;
*) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;; *) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
esac esac
@@ -142,134 +139,6 @@ prompt_multiline() {
eval "$var_name=\"$value\"" eval "$var_name=\"$value\""
} }
# ── Existing file detection ────────────────────────────────────
detect_existing_config() {
local found=0
local existing_files=()
[[ -f "$SOUL_OUTPUT" ]] && { found=1; existing_files+=("SOUL.md"); }
[[ -f "$USER_OUTPUT" ]] && { found=1; existing_files+=("USER.md"); }
[[ -f "$TOOLS_OUTPUT" ]] && { found=1; existing_files+=("TOOLS.md"); }
if [[ $found -eq 0 || $FORCE -eq 1 ]]; then
return 0 # No existing files or --force: proceed with fresh install
fi
echo "[mosaic-init] Existing configuration detected:"
for f in "${existing_files[@]}"; do
echo " ✓ $f"
done
# Show current agent name if SOUL.md exists
if [[ -f "$SOUL_OUTPUT" ]]; then
local current_name
current_name=$(grep -oP 'You are \*\*\K[^*]+' "$SOUL_OUTPUT" 2>/dev/null || true)
if [[ -n "$current_name" ]]; then
echo " Agent: $current_name"
fi
fi
echo ""
if [[ $NON_INTERACTIVE -eq 1 ]]; then
echo "[mosaic-init] Existing config found. Use --force to overwrite in non-interactive mode."
exit 0
fi
echo "What would you like to do?"
echo " 1) keep — Keep existing files, skip init (default)"
echo " 2) import — Import values from existing files as defaults, then regenerate"
echo " 3) overwrite — Start fresh, overwrite all files"
printf "Choose [1/2/3]: "
read -r choice
case "${choice:-1}" in
1|keep)
echo "[mosaic-init] Keeping existing configuration."
# Still push to runtime adapters in case framework was updated
if [[ -x "$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets" ]]; then
echo "[mosaic-init] Updating runtime adapters..."
"$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets"
fi
echo "[mosaic-init] Done. Launch with: mosaic claude"
exit 0
;;
2|import)
echo "[mosaic-init] Importing values from existing files as defaults..."
import_existing_values
;;
3|overwrite)
echo "[mosaic-init] Starting fresh install..."
# Back up existing files
local ts
ts=$(date +%Y%m%d%H%M%S)
for f in "${existing_files[@]}"; do
local src="$MOSAIC_HOME/$f"
if [[ -f "$src" ]]; then
cp "$src" "${src}.bak.${ts}"
echo " Backed up $f → ${f}.bak.${ts}"
fi
done
;;
*)
echo "[mosaic-init] Invalid choice. Keeping existing configuration."
exit 0
;;
esac
}
import_existing_values() {
# Import SOUL.md values
if [[ -f "$SOUL_OUTPUT" ]]; then
local content
content=$(cat "$SOUL_OUTPUT")
if [[ -z "$AGENT_NAME" ]]; then
AGENT_NAME=$(echo "$content" | grep -oP 'You are \*\*\K[^*]+' 2>/dev/null || true)
fi
if [[ -z "$ROLE_DESCRIPTION" ]]; then
ROLE_DESCRIPTION=$(echo "$content" | grep -oP 'Role identity: \K.+' 2>/dev/null || true)
fi
if [[ -z "$STYLE" ]]; then
if echo "$content" | grep -q 'Be direct, concise'; then
STYLE="direct"
elif echo "$content" | grep -q 'Be warm and conversational'; then
STYLE="friendly"
elif echo "$content" | grep -q 'Use professional, structured'; then
STYLE="formal"
fi
fi
fi
# Import USER.md values
if [[ -f "$USER_OUTPUT" ]]; then
local content
content=$(cat "$USER_OUTPUT")
if [[ -z "$USER_NAME" ]]; then
USER_NAME=$(echo "$content" | grep -oP '\*\*Name:\*\* \K.+' 2>/dev/null || true)
fi
if [[ -z "$PRONOUNS" ]]; then
PRONOUNS=$(echo "$content" | grep -oP '\*\*Pronouns:\*\* \K.+' 2>/dev/null || true)
fi
if [[ -z "$TIMEZONE" ]]; then
TIMEZONE=$(echo "$content" | grep -oP '\*\*Timezone:\*\* \K.+' 2>/dev/null || true)
fi
fi
# Import TOOLS.md values
if [[ -f "$TOOLS_OUTPUT" ]]; then
local content
content=$(cat "$TOOLS_OUTPUT")
if [[ -z "$CREDENTIALS_LOCATION" ]]; then
CREDENTIALS_LOCATION=$(echo "$content" | grep -oP '\*\*Location:\*\* \K.+' 2>/dev/null || true)
fi
fi
}
detect_existing_config
# ── SOUL.md Generation ──────────────────────────────────────── # ── SOUL.md Generation ────────────────────────────────────────
echo "[mosaic-init] Generating SOUL.md — agent identity contract" echo "[mosaic-init] Generating SOUL.md — agent identity contract"
echo "" echo ""
@@ -392,9 +261,9 @@ echo "[mosaic-init] Style: $STYLE"
if [[ $SOUL_ONLY -eq 1 ]]; then if [[ $SOUL_ONLY -eq 1 ]]; then
# Push to runtime adapters and exit # Push to runtime adapters and exit
if [[ -x "$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets" ]]; then if [[ -x "$MOSAIC_HOME/bin/mosaic-link-runtime-assets" ]]; then
echo "[mosaic-init] Updating runtime adapters..." echo "[mosaic-init] Updating runtime adapters..."
"$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets" "$MOSAIC_HOME/bin/mosaic-link-runtime-assets"
fi fi
echo "[mosaic-init] Done. Launch with: mosaic claude" echo "[mosaic-init] Done. Launch with: mosaic claude"
exit 0 exit 0
@@ -544,10 +413,10 @@ fi
# ── Finalize ────────────────────────────────────────────────── # ── Finalize ──────────────────────────────────────────────────
# Push to runtime adapters # Push to runtime adapters
if [[ -x "$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets" ]]; then if [[ -x "$MOSAIC_HOME/bin/mosaic-link-runtime-assets" ]]; then
echo "" echo ""
echo "[mosaic-init] Updating runtime adapters..." echo "[mosaic-init] Updating runtime adapters..."
"$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets" "$MOSAIC_HOME/bin/mosaic-link-runtime-assets"
fi fi
echo "" echo ""

View File

@@ -0,0 +1,845 @@
#!/usr/bin/env bash
set -euo pipefail
# mosaic-launch — Framework agent launcher (called by the mosaic npm CLI)
#
# AGENTS.md is the global policy source for all agent sessions.
# The launcher injects a composed runtime contract (AGENTS + runtime reference).
#
# Usage (via mosaic CLI):
# mosaic claude [args...] Launch Claude Code with runtime contract injected
# mosaic codex [args...] Launch Codex with runtime contract injected
# mosaic opencode [args...] Launch OpenCode with runtime contract injected
# mosaic pi [args...] Launch Pi with runtime contract injected
# mosaic yolo <runtime> [args...] Launch runtime in dangerous-permissions mode
#
# Direct usage:
# mosaic-launch claude [args...]
# mosaic-launch yolo claude [args...]
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
VERSION="0.1.0"
usage() {
cat <<USAGE
mosaic $VERSION — Unified agent launcher
Usage: mosaic <command> [args...]
Agent Launchers:
pi [args...] Launch Pi with runtime contract injected (recommended)
claude [args...] Launch Claude Code with runtime contract injected
opencode [args...] Launch OpenCode with runtime contract injected
codex [args...] Launch Codex with runtime contract injected
yolo <runtime> [args...] Dangerous mode for claude|codex|opencode|pi
--yolo <runtime> [args...] Alias for yolo
Management:
init [args...] Generate SOUL.md (agent identity contract)
doctor [args...] Audit runtime state and detect drift
sync [args...] Sync skills from canonical source
seq [subcommand] sequential-thinking MCP management:
check [--runtime <r>] [--strict]
fix [--runtime <r>]
start
bootstrap <path> Bootstrap a repo with Mosaic standards
upgrade [mode] [args] Upgrade release (default) or project files
upgrade check Check release upgrade status (no changes)
release-upgrade [...] Upgrade installed Mosaic release
project-upgrade [...] Clean up stale SOUL.md/CLAUDE.md in a project
PRD:
prdy <subcommand> PRD creation and validation
init Create docs/PRD.md via guided runtime session
update Update existing PRD via guided runtime session
validate Check PRD completeness (bash-only)
status Quick PRD health check (one-liner)
Coordinator (r0):
coord <subcommand> Manual coordinator tools
init Initialize a new mission
mission Show mission progress dashboard
status Check agent session health
continue Generate continuation prompt
run Generate context and launch selected runtime
resume Crash recovery
Options:
-h, --help Show this help
-v, --version Show version
All arguments after the command are forwarded to the target CLI.
USAGE
}
# Pre-flight checks
check_mosaic_home() {
if [[ ! -d "$MOSAIC_HOME" ]]; then
echo "[mosaic] ERROR: ~/.config/mosaic not found." >&2
echo "[mosaic] Install with: bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh)" >&2
exit 1
fi
}
check_agents_md() {
if [[ ! -f "$MOSAIC_HOME/AGENTS.md" ]]; then
echo "[mosaic] ERROR: ~/.config/mosaic/AGENTS.md not found." >&2
echo "[mosaic] Re-run the installer: bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh)" >&2
exit 1
fi
}
check_soul() {
if [[ ! -f "$MOSAIC_HOME/SOUL.md" ]]; then
echo "[mosaic] SOUL.md not found. Running mosaic init..."
"$MOSAIC_HOME/bin/mosaic-init"
fi
}
check_runtime() {
local cmd="$1"
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "[mosaic] ERROR: '$cmd' not found in PATH." >&2
echo "[mosaic] Install $cmd before launching." >&2
exit 1
fi
}
check_sequential_thinking() {
local runtime="${1:-all}"
local checker="$MOSAIC_HOME/bin/mosaic-ensure-sequential-thinking"
if [[ ! -x "$checker" ]]; then
echo "[mosaic] ERROR: sequential-thinking checker missing: $checker" >&2
exit 1
fi
if ! "$checker" --check --runtime "$runtime" >/dev/null 2>&1; then
echo "[mosaic] ERROR: sequential-thinking MCP is required but not configured." >&2
echo "[mosaic] Fix config: $checker --runtime $runtime" >&2
echo "[mosaic] Or run: mosaic seq fix --runtime $runtime" >&2
echo "[mosaic] Manual server start: mosaic seq start" >&2
exit 1
fi
}
runtime_contract_path() {
local runtime="$1"
case "$runtime" in
claude) echo "$MOSAIC_HOME/runtime/claude/RUNTIME.md" ;;
codex) echo "$MOSAIC_HOME/runtime/codex/RUNTIME.md" ;;
opencode) echo "$MOSAIC_HOME/runtime/opencode/RUNTIME.md" ;;
pi) echo "$MOSAIC_HOME/runtime/pi/RUNTIME.md" ;;
*)
echo "[mosaic] ERROR: unsupported runtime '$runtime' for runtime contract." >&2
exit 1
;;
esac
}
build_runtime_prompt() {
local runtime="$1"
local runtime_file
runtime_file="$(runtime_contract_path "$runtime")"
if [[ ! -f "$runtime_file" ]]; then
echo "[mosaic] ERROR: runtime contract not found: $runtime_file" >&2
exit 1
fi
# Inject active mission context FIRST so the agent sees it immediately
local mission_file=".mosaic/orchestrator/mission.json"
if [[ -f "$mission_file" ]] && command -v jq &>/dev/null; then
local m_status
m_status="$(jq -r '.status // "inactive"' "$mission_file" 2>/dev/null)"
if [[ "$m_status" == "active" || "$m_status" == "paused" ]]; then
local m_name m_id m_count m_completed
m_name="$(jq -r '.name // "unnamed"' "$mission_file")"
m_id="$(jq -r '.mission_id // ""' "$mission_file")"
m_count="$(jq '.milestones | length' "$mission_file")"
m_completed="$(jq '[.milestones[] | select(.status == "completed")] | length' "$mission_file")"
cat <<MISSION_EOF
# ACTIVE MISSION — HARD GATE (Read Before Anything Else)
An active orchestration mission exists in this project. This is a BLOCKING requirement.
**Mission:** $m_name
**ID:** $m_id
**Status:** $m_status
**Milestones:** $m_completed / $m_count completed
## MANDATORY — Before ANY Response to the User
You MUST complete these steps before responding to any user message, including simple greetings:
1. Read \`~/.config/mosaic/guides/ORCHESTRATOR-PROTOCOL.md\` (mission lifecycle protocol)
2. Read \`docs/MISSION-MANIFEST.md\` for full mission scope, milestones, and success criteria
3. Read the latest scratchpad in \`docs/scratchpads/\` for session history, decisions, and corrections
4. Read \`docs/TASKS.md\` for current task state (what is done, what is next)
5. After reading all four, acknowledge the mission state to the user before proceeding
If the user gives a task, execute it within the mission context. If no task is given, present mission status and ask how to proceed.
MISSION_EOF
fi
fi
# Inject PRD status so the agent knows requirements state
local prd_file="docs/PRD.md"
if [[ -f "$prd_file" ]]; then
local prd_sections=0
local prd_assumptions=0
for entry in "Problem Statement|^#{2,3} .*(problem statement|objective)" \
"Scope / Non-Goals|^#{2,3} .*(scope|non.goal|out of scope|in.scope)" \
"User Stories / Requirements|^#{2,3} .*(user stor|stakeholder|user.*requirement)" \
"Functional Requirements|^#{2,3} .*functional requirement" \
"Non-Functional Requirements|^#{2,3} .*non.functional" \
"Acceptance Criteria|^#{2,3} .*acceptance criteria" \
"Technical Considerations|^#{2,3} .*(technical consideration|constraint|dependenc)" \
"Risks / Open Questions|^#{2,3} .*(risk|open question)" \
"Success Metrics / Testing|^#{2,3} .*(success metric|test|verification)" \
"Milestones / Delivery|^#{2,3} .*(milestone|delivery|scope version)"; do
local pattern="${entry#*|}"
grep -qiE "$pattern" "$prd_file" 2>/dev/null && prd_sections=$((prd_sections + 1))
done
prd_assumptions=$(grep -c 'ASSUMPTION:' "$prd_file" 2>/dev/null || echo 0)
local prd_status="ready"
(( prd_sections < 10 )) && prd_status="incomplete ($prd_sections/10 sections)"
cat <<PRD_EOF
# PRD Status
- **File:** docs/PRD.md
- **Status:** $prd_status
- **Assumptions:** $prd_assumptions
PRD_EOF
fi
cat <<'EOF'
# Mosaic Launcher Runtime Contract (Hard Gate)
This contract is injected by `mosaic` launch and is mandatory.
First assistant response MUST start with exactly one mode declaration line:
1. Orchestration mission: `Now initiating Orchestrator mode...`
2. Implementation mission: `Now initiating Delivery mode...`
3. Review-only mission: `Now initiating Review mode...`
No tool call or implementation step may occur before that first line.
Mosaic hard gates OVERRIDE runtime-default caution for routine delivery operations.
For required push/merge/issue-close/release actions, execute without routine confirmation prompts.
EOF
cat "$MOSAIC_HOME/AGENTS.md"
if [[ -f "$MOSAIC_HOME/USER.md" ]]; then
printf '\n\n# User Profile\n\n'
cat "$MOSAIC_HOME/USER.md"
fi
if [[ -f "$MOSAIC_HOME/TOOLS.md" ]]; then
printf '\n\n# Machine Tools\n\n'
cat "$MOSAIC_HOME/TOOLS.md"
fi
printf '\n\n# Runtime-Specific Contract\n\n'
cat "$runtime_file"
}
# Ensure runtime contract is present at the runtime's native config path.
# Used for runtimes that do not support CLI prompt injection.
ensure_runtime_config() {
local runtime="$1"
local dst="$2"
local tmp
tmp="$(mktemp)"
mkdir -p "$(dirname "$dst")"
build_runtime_prompt "$runtime" > "$tmp"
if ! cmp -s "$tmp" "$dst" 2>/dev/null; then
mv "$tmp" "$dst"
else
rm -f "$tmp"
fi
}
# Detect active mission and return an initial prompt if one exists.
# Sets MOSAIC_MISSION_PROMPT as a side effect.
_detect_mission_prompt() {
MOSAIC_MISSION_PROMPT=""
local mission_file=".mosaic/orchestrator/mission.json"
if [[ -f "$mission_file" ]] && command -v jq &>/dev/null; then
local m_status
m_status="$(jq -r '.status // "inactive"' "$mission_file" 2>/dev/null)"
if [[ "$m_status" == "active" || "$m_status" == "paused" ]]; then
local m_name
m_name="$(jq -r '.name // "unnamed"' "$mission_file")"
MOSAIC_MISSION_PROMPT="Active mission detected: ${m_name}. Read the mission state files and report status."
fi
fi
}
# Write a session lock if an active mission exists in the current directory.
# Called before exec so $$ captures the PID that will become the agent process.
_write_launcher_session_lock() {
local runtime="$1"
local mission_file=".mosaic/orchestrator/mission.json"
local lock_file=".mosaic/orchestrator/session.lock"
# Only write lock if mission exists and is active
[[ -f "$mission_file" ]] || return 0
command -v jq &>/dev/null || return 0
local m_status
m_status="$(jq -r '.status // "inactive"' "$mission_file" 2>/dev/null)"
[[ "$m_status" == "active" || "$m_status" == "paused" ]] || return 0
local session_id
session_id="${runtime}-$(date +%Y%m%d-%H%M%S)-$$"
jq -n \
--arg sid "$session_id" \
--arg rt "$runtime" \
--arg pid "$$" \
--arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg pp "$(pwd)" \
--arg mid "" \
'{
session_id: $sid,
runtime: $rt,
pid: ($pid | tonumber),
started_at: $ts,
project_path: $pp,
milestone_id: $mid
}' > "$lock_file"
}
# Clean up session lock on exit (covers normal exit + signals).
# Registered via trap after _write_launcher_session_lock succeeds.
_cleanup_session_lock() {
rm -f ".mosaic/orchestrator/session.lock" 2>/dev/null
}
# Launcher functions
launch_claude() {
check_mosaic_home
check_agents_md
check_soul
check_runtime "claude"
check_sequential_thinking "claude"
_check_resumable_session
# Claude supports --append-system-prompt for direct injection
local runtime_prompt
runtime_prompt="$(build_runtime_prompt "claude")"
# If active mission exists and no user prompt was given, inject initial prompt
_detect_mission_prompt
_write_launcher_session_lock "claude"
trap _cleanup_session_lock EXIT INT TERM
if [[ -n "$MOSAIC_MISSION_PROMPT" && $# -eq 0 ]]; then
echo "[mosaic] Launching Claude Code (active mission detected)..."
exec claude --append-system-prompt "$runtime_prompt" "$MOSAIC_MISSION_PROMPT"
else
echo "[mosaic] Launching Claude Code..."
exec claude --append-system-prompt "$runtime_prompt" "$@"
fi
}
launch_opencode() {
check_mosaic_home
check_agents_md
check_soul
check_runtime "opencode"
check_sequential_thinking "opencode"
_check_resumable_session
# OpenCode reads from ~/.config/opencode/AGENTS.md
ensure_runtime_config "opencode" "$HOME/.config/opencode/AGENTS.md"
_write_launcher_session_lock "opencode"
trap _cleanup_session_lock EXIT INT TERM
echo "[mosaic] Launching OpenCode..."
exec opencode "$@"
}
launch_codex() {
check_mosaic_home
check_agents_md
check_soul
check_runtime "codex"
check_sequential_thinking "codex"
_check_resumable_session
# Codex reads from ~/.codex/instructions.md
ensure_runtime_config "codex" "$HOME/.codex/instructions.md"
_detect_mission_prompt
_write_launcher_session_lock "codex"
trap _cleanup_session_lock EXIT INT TERM
if [[ -n "$MOSAIC_MISSION_PROMPT" && $# -eq 0 ]]; then
echo "[mosaic] Launching Codex (active mission detected)..."
exec codex "$MOSAIC_MISSION_PROMPT"
else
echo "[mosaic] Launching Codex..."
exec codex "$@"
fi
}
launch_pi() {
check_mosaic_home
check_agents_md
check_soul
check_runtime "pi"
# Pi has native thinking levels — no sequential-thinking gate required
_check_resumable_session
local runtime_prompt
runtime_prompt="$(build_runtime_prompt "pi")"
# Build skill args from Mosaic skills directories (canonical + local)
local -a skill_args=()
for skills_root in "$MOSAIC_HOME/skills" "$MOSAIC_HOME/skills-local"; do
[[ -d "$skills_root" ]] || continue
for skill_dir in "$skills_root"/*/; do
[[ -f "${skill_dir}SKILL.md" ]] && skill_args+=(--skill "$skill_dir")
done
done
# Load Mosaic extension if present
local -a ext_args=()
local mosaic_ext="$MOSAIC_HOME/runtime/pi/mosaic-extension.ts"
[[ -f "$mosaic_ext" ]] && ext_args=(--extension "$mosaic_ext")
_detect_mission_prompt
_write_launcher_session_lock "pi"
trap _cleanup_session_lock EXIT INT TERM
if [[ -n "$MOSAIC_MISSION_PROMPT" && $# -eq 0 ]]; then
echo "[mosaic] Launching Pi (active mission detected)..."
exec pi --append-system-prompt "$runtime_prompt" \
"${skill_args[@]}" "${ext_args[@]}" "$MOSAIC_MISSION_PROMPT"
else
echo "[mosaic] Launching Pi..."
exec pi --append-system-prompt "$runtime_prompt" \
"${skill_args[@]}" "${ext_args[@]}" "$@"
fi
}
launch_yolo() {
if [[ $# -eq 0 ]]; then
echo "[mosaic] ERROR: yolo requires a runtime (claude|codex|opencode|pi)." >&2
echo "[mosaic] Example: mosaic yolo claude" >&2
exit 1
fi
local runtime="$1"
shift
case "$runtime" in
claude)
check_mosaic_home
check_agents_md
check_soul
check_runtime "claude"
check_sequential_thinking "claude"
# Claude uses an explicit dangerous permissions flag.
local runtime_prompt
runtime_prompt="$(build_runtime_prompt "claude")"
_detect_mission_prompt
_write_launcher_session_lock "claude"
trap _cleanup_session_lock EXIT INT TERM
if [[ -n "$MOSAIC_MISSION_PROMPT" && $# -eq 0 ]]; then
echo "[mosaic] Launching Claude Code in YOLO mode (active mission detected)..."
exec claude --dangerously-skip-permissions --append-system-prompt "$runtime_prompt" "$MOSAIC_MISSION_PROMPT"
else
echo "[mosaic] Launching Claude Code in YOLO mode (dangerous permissions enabled)..."
exec claude --dangerously-skip-permissions --append-system-prompt "$runtime_prompt" "$@"
fi
;;
codex)
check_mosaic_home
check_agents_md
check_soul
check_runtime "codex"
check_sequential_thinking "codex"
# Codex reads instructions.md from ~/.codex and supports a direct dangerous flag.
ensure_runtime_config "codex" "$HOME/.codex/instructions.md"
_detect_mission_prompt
_write_launcher_session_lock "codex"
trap _cleanup_session_lock EXIT INT TERM
if [[ -n "$MOSAIC_MISSION_PROMPT" && $# -eq 0 ]]; then
echo "[mosaic] Launching Codex in YOLO mode (active mission detected)..."
exec codex --dangerously-bypass-approvals-and-sandbox "$MOSAIC_MISSION_PROMPT"
else
echo "[mosaic] Launching Codex in YOLO mode (dangerous permissions enabled)..."
exec codex --dangerously-bypass-approvals-and-sandbox "$@"
fi
;;
opencode)
check_mosaic_home
check_agents_md
check_soul
check_runtime "opencode"
check_sequential_thinking "opencode"
# OpenCode defaults to allow-all permissions unless user config restricts them.
ensure_runtime_config "opencode" "$HOME/.config/opencode/AGENTS.md"
_write_launcher_session_lock "opencode"
trap _cleanup_session_lock EXIT INT TERM
echo "[mosaic] Launching OpenCode in YOLO mode..."
exec opencode "$@"
;;
pi)
# Pi has no permission restrictions — yolo is identical to normal launch
launch_pi "$@"
;;
*)
echo "[mosaic] ERROR: Unsupported yolo runtime '$runtime'. Use claude|codex|opencode|pi." >&2
exit 1
;;
esac
}
# Delegate to existing scripts
run_init() {
# Prefer wizard if Node.js and bundle are available
local wizard_bin="$MOSAIC_HOME/dist/mosaic-wizard.mjs"
if command -v node >/dev/null 2>&1 && [[ -f "$wizard_bin" ]]; then
exec node "$wizard_bin" "$@"
fi
# Fallback to legacy bash wizard
check_mosaic_home
exec "$MOSAIC_HOME/bin/mosaic-init" "$@"
}
run_doctor() {
check_mosaic_home
exec "$MOSAIC_HOME/bin/mosaic-doctor" "$@"
}
run_sync() {
check_mosaic_home
exec "$MOSAIC_HOME/bin/mosaic-sync-skills" "$@"
}
run_seq() {
check_mosaic_home
local checker="$MOSAIC_HOME/bin/mosaic-ensure-sequential-thinking"
local action="${1:-check}"
case "$action" in
check)
shift || true
exec "$checker" --check "$@"
;;
fix|apply)
shift || true
exec "$checker" "$@"
;;
start)
shift || true
check_runtime "npx"
echo "[mosaic] Starting sequential-thinking MCP server..."
exec npx -y @modelcontextprotocol/server-sequential-thinking "$@"
;;
*)
echo "[mosaic] ERROR: Unknown seq subcommand '$action'." >&2
echo "[mosaic] Use: mosaic seq check|fix|start" >&2
exit 1
;;
esac
}
run_coord() {
check_mosaic_home
local runtime="claude"
local runtime_flag=""
local yolo_flag=""
local -a coord_args=()
while [[ $# -gt 0 ]]; do
case "$1" in
--claude|--codex|--pi)
local selected_runtime="${1#--}"
if [[ -n "$runtime_flag" ]] && [[ "$runtime" != "$selected_runtime" ]]; then
echo "[mosaic] ERROR: --claude, --codex, and --pi are mutually exclusive for 'mosaic coord'." >&2
exit 1
fi
runtime="$selected_runtime"
runtime_flag="$1"
shift
;;
--yolo)
yolo_flag="--yolo"
shift
;;
*)
coord_args+=("$1")
shift
;;
esac
done
local subcmd="${coord_args[0]:-help}"
if (( ${#coord_args[@]} > 1 )); then
set -- "${coord_args[@]:1}"
else
set --
fi
local tool_dir="$MOSAIC_HOME/tools/orchestrator"
case "$subcmd" in
status|session)
MOSAIC_COORD_RUNTIME="$runtime" exec bash "$tool_dir/session-status.sh" "$@"
;;
init)
MOSAIC_COORD_RUNTIME="$runtime" exec bash "$tool_dir/mission-init.sh" "$@"
;;
mission|progress)
MOSAIC_COORD_RUNTIME="$runtime" exec bash "$tool_dir/mission-status.sh" "$@"
;;
continue|next)
MOSAIC_COORD_RUNTIME="$runtime" exec bash "$tool_dir/continue-prompt.sh" "$@"
;;
run|start)
MOSAIC_COORD_RUNTIME="$runtime" exec bash "$tool_dir/session-run.sh" ${yolo_flag:+"$yolo_flag"} "$@"
;;
smoke|test)
MOSAIC_COORD_RUNTIME="$runtime" exec bash "$tool_dir/smoke-test.sh" "$@"
;;
resume|recover)
MOSAIC_COORD_RUNTIME="$runtime" exec bash "$tool_dir/session-resume.sh" "$@"
;;
help|*)
cat <<COORD_USAGE
mosaic coord — r0 manual coordinator tools
Commands:
init --name <name> [opts] Initialize a new mission
mission [--project <path>] Show mission progress dashboard
status [--project <path>] Check agent session health
continue [--project <path>] Generate continuation prompt for next session
run [--project <path>] Generate context and launch selected runtime
smoke Run orchestration behavior smoke checks
resume [--project <path>] Crash recovery (detect dirty state, generate fix)
Runtime:
--claude Use Claude runtime hints/prompts (default)
--codex Use Codex runtime hints/prompts
--pi Use Pi runtime hints/prompts
--yolo Launch runtime in dangerous/skip-permissions mode (run only)
Examples:
mosaic coord init --name "Security Fix" --milestones "Critical,High,Medium"
mosaic coord mission
mosaic coord --codex mission
mosaic coord --pi run
mosaic coord continue --copy
mosaic coord run
mosaic coord run --codex
mosaic coord --yolo run
mosaic coord smoke
mosaic coord continue --codex --copy
COORD_USAGE
;;
esac
}
# Resume advisory — prints warning if active mission or stale session detected
_check_resumable_session() {
local mission_file=".mosaic/orchestrator/mission.json"
local lock_file=".mosaic/orchestrator/session.lock"
command -v jq &>/dev/null || return 0
if [[ -f "$lock_file" ]]; then
local pid
pid="$(jq -r '.pid // 0' "$lock_file" 2>/dev/null)"
if [[ -n "$pid" ]] && [[ "$pid" != "0" ]] && ! kill -0 "$pid" 2>/dev/null; then
# Stale lock from a dead session — clean it up
rm -f "$lock_file"
echo "[mosaic] Cleaned up stale session lock (PID $pid no longer running)."
echo ""
fi
elif [[ -f "$mission_file" ]]; then
local status
status="$(jq -r '.status // "inactive"' "$mission_file" 2>/dev/null)"
if [[ "$status" == "active" ]]; then
echo "[mosaic] Active mission detected. Generate continuation prompt with:"
echo "[mosaic] mosaic coord continue"
echo ""
fi
fi
}
run_prdy() {
check_mosaic_home
local runtime="claude"
local runtime_flag=""
local -a prdy_args=()
while [[ $# -gt 0 ]]; do
case "$1" in
--claude|--codex|--pi)
local selected_runtime="${1#--}"
if [[ -n "$runtime_flag" ]] && [[ "$runtime" != "$selected_runtime" ]]; then
echo "[mosaic] ERROR: --claude, --codex, and --pi are mutually exclusive for 'mosaic prdy'." >&2
exit 1
fi
runtime="$selected_runtime"
runtime_flag="$1"
shift
;;
*)
prdy_args+=("$1")
shift
;;
esac
done
local subcmd="${prdy_args[0]:-help}"
if (( ${#prdy_args[@]} > 1 )); then
set -- "${prdy_args[@]:1}"
else
set --
fi
local tool_dir="$MOSAIC_HOME/tools/prdy"
case "$subcmd" in
init)
MOSAIC_PRDY_RUNTIME="$runtime" exec bash "$tool_dir/prdy-init.sh" "$@"
;;
update)
MOSAIC_PRDY_RUNTIME="$runtime" exec bash "$tool_dir/prdy-update.sh" "$@"
;;
validate|check)
MOSAIC_PRDY_RUNTIME="$runtime" exec bash "$tool_dir/prdy-validate.sh" "$@"
;;
status)
exec bash "$tool_dir/prdy-status.sh" "$@"
;;
help|*)
cat <<PRDY_USAGE
mosaic prdy — PRD creation and validation tools
Commands:
init [--project <path>] [--name <feature>] Create docs/PRD.md via guided runtime session
update [--project <path>] Update existing docs/PRD.md via guided runtime session
validate [--project <path>] Check PRD completeness against Mosaic guide (bash-only)
status [--project <path>] [--format short|json] Quick PRD health check (one-liner)
Runtime:
--claude Use Claude runtime (default)
--codex Use Codex runtime
--pi Use Pi runtime
Examples:
mosaic prdy init --name "User Authentication"
mosaic prdy update
mosaic prdy --pi init --name "User Authentication"
mosaic prdy --codex init --name "User Authentication"
mosaic prdy validate
Output location: docs/PRD.md (per Mosaic PRD guide)
PRDY_USAGE
;;
esac
}
run_bootstrap() {
check_mosaic_home
exec "$MOSAIC_HOME/bin/mosaic-bootstrap-repo" "$@"
}
run_release_upgrade() {
check_mosaic_home
exec "$MOSAIC_HOME/bin/mosaic-release-upgrade" "$@"
}
run_project_upgrade() {
check_mosaic_home
exec "$MOSAIC_HOME/bin/mosaic-upgrade" "$@"
}
run_upgrade() {
check_mosaic_home
# Default: upgrade installed release
if [[ $# -eq 0 ]]; then
run_release_upgrade
fi
case "$1" in
release)
shift
run_release_upgrade "$@"
;;
check)
shift
run_release_upgrade --dry-run "$@"
;;
project)
shift
run_project_upgrade "$@"
;;
# Backward compatibility for historical project-upgrade usage.
--all|--root)
run_project_upgrade "$@"
;;
--dry-run|--ref|--keep|--overwrite|-y|--yes)
run_release_upgrade "$@"
;;
-*)
run_release_upgrade "$@"
;;
*)
run_project_upgrade "$@"
;;
esac
}
# Main router
if [[ $# -eq 0 ]]; then
usage
exit 0
fi
command="$1"
shift
case "$command" in
pi) launch_pi "$@" ;;
claude) launch_claude "$@" ;;
opencode) launch_opencode "$@" ;;
codex) launch_codex "$@" ;;
yolo|--yolo) launch_yolo "$@" ;;
init) run_init "$@" ;;
doctor) run_doctor "$@" ;;
sync) run_sync "$@" ;;
seq) run_seq "$@" ;;
bootstrap) run_bootstrap "$@" ;;
prdy) run_prdy "$@" ;;
coord) run_coord "$@" ;;
upgrade) run_upgrade "$@" ;;
release-upgrade) run_release_upgrade "$@" ;;
project-upgrade) run_project_upgrade "$@" ;;
help|-h|--help) usage ;;
version|-v|--version) echo "mosaic $VERSION" ;;
*)
echo "[mosaic] Unknown command: $command" >&2
echo "[mosaic] Run 'mosaic --help' for usage." >&2
exit 1
;;
esac

View File

@@ -128,8 +128,8 @@ fi
# Pi extension is loaded via --extension flag in the mosaic launcher. # Pi extension is loaded via --extension flag in the mosaic launcher.
# Do NOT copy into ~/.pi/agent/extensions/ — that causes duplicate loading. # Do NOT copy into ~/.pi/agent/extensions/ — that causes duplicate loading.
if [[ -x "$MOSAIC_HOME/tools/_scripts/mosaic-ensure-sequential-thinking" ]]; then if [[ -x "$MOSAIC_HOME/bin/mosaic-ensure-sequential-thinking" ]]; then
"$MOSAIC_HOME/tools/_scripts/mosaic-ensure-sequential-thinking" "$MOSAIC_HOME/bin/mosaic-ensure-sequential-thinking"
fi fi
echo "[mosaic-link] Runtime assets synced (non-symlink mode)" echo "[mosaic-link] Runtime assets synced (non-symlink mode)"

View File

@@ -2,8 +2,8 @@
set -euo pipefail set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
sync_cmd="$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-sync-tasks" sync_cmd="$MOSAIC_HOME/bin/mosaic-orchestrator-sync-tasks"
run_cmd="$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-run" run_cmd="$MOSAIC_HOME/bin/mosaic-orchestrator-run"
do_sync=1 do_sync=1
poll_sec=15 poll_sec=15

View File

@@ -3,9 +3,9 @@ set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
consume="$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-matrix-consume" consume="$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-consume"
run="$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-run" run="$MOSAIC_HOME/bin/mosaic-orchestrator-run"
publish="$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-matrix-publish" publish="$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-publish"
for cmd in "$consume" "$run" "$publish"; do for cmd in "$consume" "$run" "$publish"; do
if [[ ! -x "$cmd" ]]; then if [[ ! -x "$cmd" ]]; then

View File

@@ -151,7 +151,7 @@ case "$cmd" in
[[ -n "$quality_template" ]] && args+=(--quality-template "$quality_template") [[ -n "$quality_template" ]] && args+=(--quality-template "$quality_template")
args+=("$repo") args+=("$repo")
echo "[mosaic-projects] bootstrap: $repo" echo "[mosaic-projects] bootstrap: $repo"
"$MOSAIC_HOME/tools/_scripts/mosaic-bootstrap-repo" "${args[@]}" "$MOSAIC_HOME/bin/mosaic-bootstrap-repo" "${args[@]}"
add_repo "$repo" || true add_repo "$repo" || true
done done
;; ;;
@@ -193,7 +193,7 @@ case "$cmd" in
drain) drain)
args=(--poll-sec "$poll_sec") args=(--poll-sec "$poll_sec")
[[ $no_sync -eq 1 ]] && args+=(--no-sync) [[ $no_sync -eq 1 ]] && args+=(--no-sync)
"$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-drain" "${args[@]}" "$MOSAIC_HOME/bin/mosaic-orchestrator-drain" "${args[@]}"
;; ;;
status) status)
echo "[mosaic-projects] no daemon script in repo; run from bootstrapped repo or re-bootstrap" echo "[mosaic-projects] no daemon script in repo; run from bootstrapped repo or re-bootstrap"

View File

@@ -55,26 +55,7 @@ mkdir -p "$MOSAIC_HOME" "$MOSAIC_SKILLS_DIR" "$MOSAIC_LOCAL_SKILLS_DIR"
if [[ $fetch -eq 1 ]]; then if [[ $fetch -eq 1 ]]; then
if [[ -d "$SKILLS_REPO_DIR/.git" ]]; then if [[ -d "$SKILLS_REPO_DIR/.git" ]]; then
echo "[mosaic-skills] Updating skills source: $SKILLS_REPO_DIR" echo "[mosaic-skills] Updating skills source: $SKILLS_REPO_DIR"
git -C "$SKILLS_REPO_DIR" pull --rebase
# Stash any local changes (dirty index or worktree) before pulling
local_changes=0
if ! git -C "$SKILLS_REPO_DIR" diff --quiet 2>/dev/null || \
! git -C "$SKILLS_REPO_DIR" diff --cached --quiet 2>/dev/null; then
local_changes=1
echo "[mosaic-skills] Stashing local changes..."
git -C "$SKILLS_REPO_DIR" stash push -q -m "mosaic-sync-skills auto-stash"
fi
if ! git -C "$SKILLS_REPO_DIR" pull --rebase; then
echo "[mosaic-skills] WARN: pull failed — continuing with existing checkout" >&2
fi
# Restore stashed changes
if [[ $local_changes -eq 1 ]]; then
echo "[mosaic-skills] Restoring local changes..."
git -C "$SKILLS_REPO_DIR" stash pop -q 2>/dev/null || \
echo "[mosaic-skills] WARN: stash pop had conflicts — check $SKILLS_REPO_DIR" >&2
fi
else else
echo "[mosaic-skills] Cloning skills source to: $SKILLS_REPO_DIR" echo "[mosaic-skills] Cloning skills source to: $SKILLS_REPO_DIR"
mkdir -p "$(dirname "$SKILLS_REPO_DIR")" mkdir -p "$(dirname "$SKILLS_REPO_DIR")"

View File

@@ -2,7 +2,7 @@
set -euo pipefail set -euo pipefail
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
BOOTSTRAP_CMD="$MOSAIC_HOME/tools/_scripts/mosaic-bootstrap-repo" BOOTSTRAP_CMD="$MOSAIC_HOME/bin/mosaic-bootstrap-repo"
roots=("$HOME/src") roots=("$HOME/src")
apply=0 apply=0

View File

@@ -0,0 +1,437 @@
# mosaic.ps1 — Unified agent launcher and management CLI (Windows)
#
# AGENTS.md is the global policy source for all agent sessions.
# The launcher injects a composed runtime contract (AGENTS + runtime reference).
#
# Usage:
# mosaic claude [args...] Launch Claude Code with runtime contract injected
# mosaic opencode [args...] Launch OpenCode with runtime contract injected
# mosaic codex [args...] Launch Codex with runtime contract injected
# mosaic yolo <runtime> [args...] Launch runtime in dangerous-permissions mode
# mosaic --yolo <runtime> [args...] Alias for yolo
# mosaic init [args...] Generate SOUL.md interactively
# mosaic doctor [args...] Health audit
# mosaic sync [args...] Sync skills
$ErrorActionPreference = "Stop"
$MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:USERPROFILE ".config\mosaic" }
$Version = "0.1.0"
function Show-Usage {
Write-Host @"
mosaic $Version - Unified agent launcher
Usage: mosaic <command> [args...]
Agent Launchers:
claude [args...] Launch Claude Code with runtime contract injected
opencode [args...] Launch OpenCode with runtime contract injected
codex [args...] Launch Codex with runtime contract injected
yolo <runtime> [args...] Dangerous mode for claude|codex|opencode
--yolo <runtime> [args...] Alias for yolo
Management:
init [args...] Generate SOUL.md (agent identity contract)
doctor [args...] Audit runtime state and detect drift
sync [args...] Sync skills from canonical source
bootstrap <path> Bootstrap a repo with Mosaic standards
upgrade [mode] [args] Upgrade release (default) or project files
upgrade check Check release upgrade status (no changes)
release-upgrade [...] Upgrade installed Mosaic release
project-upgrade [...] Clean up stale SOUL.md/CLAUDE.md in a project
Options:
-h, --help Show this help
-v, --version Show version
"@
}
function Assert-MosaicHome {
if (-not (Test-Path $MosaicHome)) {
Write-Host "[mosaic] ERROR: ~/.config/mosaic not found." -ForegroundColor Red
Write-Host "[mosaic] Install with: bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh)"
exit 1
}
}
function Assert-AgentsMd {
$agentsPath = Join-Path $MosaicHome "AGENTS.md"
if (-not (Test-Path $agentsPath)) {
Write-Host "[mosaic] ERROR: ~/.config/mosaic/AGENTS.md not found." -ForegroundColor Red
Write-Host "[mosaic] Re-run the installer."
exit 1
}
}
function Assert-Soul {
$soulPath = Join-Path $MosaicHome "SOUL.md"
if (-not (Test-Path $soulPath)) {
Write-Host "[mosaic] SOUL.md not found. Running mosaic init..."
& (Join-Path $MosaicHome "bin\mosaic-init.ps1")
}
}
function Assert-Runtime {
param([string]$Cmd)
if (-not (Get-Command $Cmd -ErrorAction SilentlyContinue)) {
Write-Host "[mosaic] ERROR: '$Cmd' not found in PATH." -ForegroundColor Red
Write-Host "[mosaic] Install $Cmd before launching."
exit 1
}
}
function Assert-SequentialThinking {
$checker = Join-Path $MosaicHome "bin\mosaic-ensure-sequential-thinking.ps1"
if (-not (Test-Path $checker)) {
Write-Host "[mosaic] ERROR: sequential-thinking checker missing: $checker" -ForegroundColor Red
exit 1
}
try {
& $checker -Check *>$null
}
catch {
Write-Host "[mosaic] ERROR: sequential-thinking MCP is required but not configured." -ForegroundColor Red
Write-Host "[mosaic] Run: $checker"
exit 1
}
}
function Get-ActiveMission {
$missionFile = Join-Path (Get-Location) ".mosaic\orchestrator\mission.json"
if (-not (Test-Path $missionFile)) {
return $null
}
try {
$mission = Get-Content $missionFile -Raw | ConvertFrom-Json
}
catch {
return $null
}
$status = [string]$mission.status
if ([string]::IsNullOrWhiteSpace($status)) {
$status = "inactive"
}
if ($status -ne "active" -and $status -ne "paused") {
return $null
}
$name = [string]$mission.name
if ([string]::IsNullOrWhiteSpace($name)) {
$name = "unnamed"
}
$id = [string]$mission.mission_id
if ([string]::IsNullOrWhiteSpace($id)) {
$id = ""
}
$milestones = @($mission.milestones)
$milestoneCount = $milestones.Count
$milestoneCompleted = @($milestones | Where-Object { $_.status -eq "completed" }).Count
return [PSCustomObject]@{
Name = $name
Id = $id
Status = $status
MilestoneCount = $milestoneCount
MilestoneCompleted = $milestoneCompleted
}
}
function Get-MissionContractBlock {
$mission = Get-ActiveMission
if ($null -eq $mission) {
return ""
}
return @"
# ACTIVE MISSION HARD GATE (Read Before Anything Else)
An active orchestration mission exists in this project. This is a BLOCKING requirement.
**Mission:** $($mission.Name)
**ID:** $($mission.Id)
**Status:** $($mission.Status)
**Milestones:** $($mission.MilestoneCompleted) / $($mission.MilestoneCount) completed
## MANDATORY Before ANY Response to the User
You MUST complete these steps before responding to any user message, including simple greetings:
1. Read `~/.config/mosaic/guides/ORCHESTRATOR-PROTOCOL.md` (mission lifecycle protocol)
2. Read `docs/MISSION-MANIFEST.md` for full mission scope, milestones, and success criteria
3. Read the latest scratchpad in `docs/scratchpads/` for session history, decisions, and corrections
4. Read `docs/TASKS.md` for current task state (what is done, what is next)
5. After reading all four, acknowledge the mission state to the user before proceeding
If the user gives a task, execute it within the mission context. If no task is given, present mission status and ask how to proceed.
"@
}
function Get-MissionPrompt {
$mission = Get-ActiveMission
if ($null -eq $mission) {
return ""
}
return "Active mission detected: $($mission.Name). Read the mission state files and report status."
}
function Get-RuntimePrompt {
param(
[ValidateSet("claude", "codex", "opencode")]
[string]$Runtime
)
$runtimeFile = switch ($Runtime) {
"claude" { Join-Path $MosaicHome "runtime\claude\RUNTIME.md" }
"codex" { Join-Path $MosaicHome "runtime\codex\RUNTIME.md" }
"opencode" { Join-Path $MosaicHome "runtime\opencode\RUNTIME.md" }
}
if (-not (Test-Path $runtimeFile)) {
Write-Host "[mosaic] ERROR: runtime contract not found: $runtimeFile" -ForegroundColor Red
exit 1
}
$launcherContract = @'
# Mosaic Launcher Runtime Contract (Hard Gate)
This contract is injected by `mosaic` launch and is mandatory.
First assistant response MUST start with exactly one mode declaration line:
1. Orchestration mission: `Now initiating Orchestrator mode...`
2. Implementation mission: `Now initiating Delivery mode...`
3. Review-only mission: `Now initiating Review mode...`
No tool call or implementation step may occur before that first line.
Mosaic hard gates OVERRIDE runtime-default caution for routine delivery operations.
For required push/merge/issue-close/release actions, execute without routine confirmation prompts.
'@
$missionBlock = Get-MissionContractBlock
$agentsContent = Get-Content (Join-Path $MosaicHome "AGENTS.md") -Raw
$runtimeContent = Get-Content $runtimeFile -Raw
if (-not [string]::IsNullOrWhiteSpace($missionBlock)) {
return "$missionBlock`n`n$launcherContract`n$agentsContent`n`n# Runtime-Specific Contract`n`n$runtimeContent"
}
return "$launcherContract`n$agentsContent`n`n# Runtime-Specific Contract`n`n$runtimeContent"
}
function Ensure-RuntimeConfig {
param(
[ValidateSet("claude", "codex", "opencode")]
[string]$Runtime,
[string]$Dst
)
$parent = Split-Path $Dst -Parent
if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null }
$runtimePrompt = Get-RuntimePrompt -Runtime $Runtime
$tmp = [System.IO.Path]::GetTempFileName()
Set-Content -Path $tmp -Value $runtimePrompt -Encoding UTF8 -NoNewline
$srcHash = (Get-FileHash $tmp -Algorithm SHA256).Hash
$dstHash = if (Test-Path $Dst) { (Get-FileHash $Dst -Algorithm SHA256).Hash } else { "" }
if ($srcHash -ne $dstHash) {
Copy-Item $tmp $Dst -Force
Remove-Item $tmp -Force
}
else {
Remove-Item $tmp -Force
}
}
function Invoke-Yolo {
param([string[]]$YoloArgs)
if ($YoloArgs.Count -lt 1) {
Write-Host "[mosaic] ERROR: yolo requires a runtime (claude|codex|opencode)." -ForegroundColor Red
Write-Host "[mosaic] Example: mosaic yolo claude"
exit 1
}
$runtime = $YoloArgs[0]
$tail = if ($YoloArgs.Count -gt 1) { @($YoloArgs[1..($YoloArgs.Count - 1)]) } else { @() }
switch ($runtime) {
"claude" {
Assert-MosaicHome
Assert-AgentsMd
Assert-Soul
Assert-Runtime "claude"
Assert-SequentialThinking
$agentsContent = Get-RuntimePrompt -Runtime "claude"
Write-Host "[mosaic] Launching Claude Code in YOLO mode (dangerous permissions enabled)..."
& claude --dangerously-skip-permissions --append-system-prompt $agentsContent @tail
return
}
"codex" {
Assert-MosaicHome
Assert-AgentsMd
Assert-Soul
Assert-Runtime "codex"
Assert-SequentialThinking
Ensure-RuntimeConfig -Runtime "codex" -Dst (Join-Path $env:USERPROFILE ".codex\instructions.md")
$missionPrompt = Get-MissionPrompt
if (-not [string]::IsNullOrWhiteSpace($missionPrompt) -and $tail.Count -eq 0) {
Write-Host "[mosaic] Launching Codex in YOLO mode (active mission detected)..."
& codex --dangerously-bypass-approvals-and-sandbox $missionPrompt
}
else {
Write-Host "[mosaic] Launching Codex in YOLO mode (dangerous permissions enabled)..."
& codex --dangerously-bypass-approvals-and-sandbox @tail
}
return
}
"opencode" {
Assert-MosaicHome
Assert-AgentsMd
Assert-Soul
Assert-Runtime "opencode"
Assert-SequentialThinking
Ensure-RuntimeConfig -Runtime "opencode" -Dst (Join-Path $env:USERPROFILE ".config\opencode\AGENTS.md")
Write-Host "[mosaic] Launching OpenCode in YOLO mode..."
& opencode @tail
return
}
default {
Write-Host "[mosaic] ERROR: Unsupported yolo runtime '$runtime'. Use claude|codex|opencode." -ForegroundColor Red
exit 1
}
}
}
if ($args.Count -eq 0) {
Show-Usage
exit 0
}
$command = $args[0]
$remaining = if ($args.Count -gt 1) { @($args[1..($args.Count - 1)]) } else { @() }
switch ($command) {
"claude" {
Assert-MosaicHome
Assert-AgentsMd
Assert-Soul
Assert-Runtime "claude"
Assert-SequentialThinking
# Claude supports --append-system-prompt for direct injection
$agentsContent = Get-RuntimePrompt -Runtime "claude"
Write-Host "[mosaic] Launching Claude Code..."
& claude --append-system-prompt $agentsContent @remaining
}
"opencode" {
Assert-MosaicHome
Assert-AgentsMd
Assert-Soul
Assert-Runtime "opencode"
Assert-SequentialThinking
# OpenCode reads from ~/.config/opencode/AGENTS.md
Ensure-RuntimeConfig -Runtime "opencode" -Dst (Join-Path $env:USERPROFILE ".config\opencode\AGENTS.md")
Write-Host "[mosaic] Launching OpenCode..."
& opencode @remaining
}
"codex" {
Assert-MosaicHome
Assert-AgentsMd
Assert-Soul
Assert-Runtime "codex"
Assert-SequentialThinking
# Codex reads from ~/.codex/instructions.md
Ensure-RuntimeConfig -Runtime "codex" -Dst (Join-Path $env:USERPROFILE ".codex\instructions.md")
$missionPrompt = Get-MissionPrompt
if (-not [string]::IsNullOrWhiteSpace($missionPrompt) -and $remaining.Count -eq 0) {
Write-Host "[mosaic] Launching Codex (active mission detected)..."
& codex $missionPrompt
}
else {
Write-Host "[mosaic] Launching Codex..."
& codex @remaining
}
}
"yolo" {
Invoke-Yolo -YoloArgs $remaining
}
"--yolo" {
Invoke-Yolo -YoloArgs $remaining
}
"init" {
Assert-MosaicHome
& (Join-Path $MosaicHome "bin\mosaic-init.ps1") @remaining
}
"doctor" {
Assert-MosaicHome
& (Join-Path $MosaicHome "bin\mosaic-doctor.ps1") @remaining
}
"sync" {
Assert-MosaicHome
& (Join-Path $MosaicHome "bin\mosaic-sync-skills.ps1") @remaining
}
"bootstrap" {
Assert-MosaicHome
Write-Host "[mosaic] NOTE: mosaic-bootstrap-repo requires bash. Use Git Bash or WSL." -ForegroundColor Yellow
& (Join-Path $MosaicHome "bin\mosaic-bootstrap-repo") @remaining
}
"upgrade" {
Assert-MosaicHome
if ($remaining.Count -eq 0) {
& (Join-Path $MosaicHome "bin\mosaic-release-upgrade.ps1")
break
}
$mode = $remaining[0]
$tail = if ($remaining.Count -gt 1) { $remaining[1..($remaining.Count - 1)] } else { @() }
switch -Regex ($mode) {
"^release$" {
& (Join-Path $MosaicHome "bin\mosaic-release-upgrade.ps1") @tail
}
"^check$" {
& (Join-Path $MosaicHome "bin\mosaic-release-upgrade.ps1") -DryRun @tail
}
"^project$" {
Write-Host "[mosaic] NOTE: mosaic-upgrade requires bash. Use Git Bash or WSL." -ForegroundColor Yellow
& (Join-Path $MosaicHome "bin\mosaic-upgrade") @tail
}
"^(--all|--root)$" {
Write-Host "[mosaic] NOTE: mosaic-upgrade requires bash. Use Git Bash or WSL." -ForegroundColor Yellow
& (Join-Path $MosaicHome "bin\mosaic-upgrade") @remaining
}
"^(--dry-run|--ref|--keep|--overwrite|-y|--yes)$" {
& (Join-Path $MosaicHome "bin\mosaic-release-upgrade.ps1") @remaining
}
"^-.*" {
& (Join-Path $MosaicHome "bin\mosaic-release-upgrade.ps1") @remaining
}
default {
Write-Host "[mosaic] NOTE: treating positional argument as project path." -ForegroundColor Yellow
Write-Host "[mosaic] NOTE: mosaic-upgrade requires bash. Use Git Bash or WSL." -ForegroundColor Yellow
& (Join-Path $MosaicHome "bin\mosaic-upgrade") @remaining
}
}
}
"release-upgrade" {
Assert-MosaicHome
& (Join-Path $MosaicHome "bin\mosaic-release-upgrade.ps1") @remaining
}
"project-upgrade" {
Assert-MosaicHome
Write-Host "[mosaic] NOTE: mosaic-upgrade requires bash. Use Git Bash or WSL." -ForegroundColor Yellow
& (Join-Path $MosaicHome "bin\mosaic-upgrade") @remaining
}
{ $_ -in "help", "-h", "--help" } { Show-Usage }
{ $_ -in "version", "-v", "--version" } { Write-Host "mosaic $Version" }
default {
Write-Host "[mosaic] Unknown command: $command" -ForegroundColor Red
Write-Host "[mosaic] Run 'mosaic --help' for usage."
exit 1
}
}

View File

@@ -1,32 +1,12 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# ─── Mosaic Framework Installer ──────────────────────────────────────────────
#
# Installs/upgrades the framework DATA to ~/.config/mosaic/.
# No executables are placed on PATH — the mosaic npm CLI is the only binary.
#
# Called by tools/install.sh (the unified installer). Can also be run directly.
#
# Environment:
# MOSAIC_HOME — target directory (default: ~/.config/mosaic)
# MOSAIC_INSTALL_MODE — prompt|keep|overwrite (default: prompt)
# MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING — 1 to bypass MCP check
# MOSAIC_SKIP_SKILLS_SYNC — 1 to skip skill sync
# ──────────────────────────────────────────────────────────────────────────────
SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TARGET_DIR="${MOSAIC_HOME:-$HOME/.config/mosaic}" TARGET_DIR="${MOSAIC_HOME:-$HOME/.config/mosaic}"
INSTALL_MODE="${MOSAIC_INSTALL_MODE:-prompt}" INSTALL_MODE="${MOSAIC_INSTALL_MODE:-prompt}" # prompt|keep|overwrite
# Files preserved across upgrades (never overwritten)
PRESERVE_PATHS=("SOUL.md" "USER.md" "TOOLS.md" "memory" "sources") PRESERVE_PATHS=("SOUL.md" "USER.md" "TOOLS.md" "memory" "sources")
# Current framework schema version — bump this when the layout changes. # Colors (disabled if not a terminal)
# The migration system uses this to run upgrade steps.
FRAMEWORK_VERSION=2
# ─── colours ──────────────────────────────────────────────────────────────────
if [[ -t 1 ]]; then if [[ -t 1 ]]; then
GREEN='\033[0;32m' YELLOW='\033[0;33m' RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' RED='\033[0;31m'
CYAN='\033[0;36m' BOLD='\033[1m' RESET='\033[0m' CYAN='\033[0;36m' BOLD='\033[1m' RESET='\033[0m'
@@ -39,29 +19,9 @@ warn() { echo -e " ${YELLOW}⚠${RESET} $1" >&2; }
fail() { echo -e " ${RED}${RESET} $1" >&2; } fail() { echo -e " ${RED}${RESET} $1" >&2; }
step() { echo -e "\n${BOLD}$1${RESET}"; } step() { echo -e "\n${BOLD}$1${RESET}"; }
# ─── helpers ──────────────────────────────────────────────────────────────────
is_existing_install() { is_existing_install() {
[[ -d "$TARGET_DIR" ]] || return 1 [[ -d "$TARGET_DIR" ]] || return 1
[[ -f "$TARGET_DIR/AGENTS.md" || -f "$TARGET_DIR/SOUL.md" ]] [[ -f "$TARGET_DIR/bin/mosaic-launch" || -f "$TARGET_DIR/bin/mosaic" || -f "$TARGET_DIR/AGENTS.md" || -f "$TARGET_DIR/SOUL.md" ]]
}
installed_framework_version() {
local vf="$TARGET_DIR/.framework-version"
if [[ -f "$vf" ]]; then
cat "$vf" 2>/dev/null || echo "0"
else
# No version file = legacy install (version 0 or 1)
if [[ -d "$TARGET_DIR/bin" ]]; then
echo "1" # Has bin/ → pre-migration legacy
else
echo "0" # Fresh or unknown
fi
fi
}
write_framework_version() {
echo "$FRAMEWORK_VERSION" > "$TARGET_DIR/.framework-version"
} }
select_install_mode() { select_install_mode() {
@@ -79,22 +39,33 @@ select_install_mode() {
fi fi
case "$INSTALL_MODE" in case "$INSTALL_MODE" in
keep|overwrite) ;; keep|overwrite)
;;
prompt) prompt)
if [[ -t 0 ]]; then if [[ -t 0 ]]; then
echo "" echo ""
echo "Existing Mosaic install detected at: $TARGET_DIR" echo "Existing Mosaic install detected at: $TARGET_DIR"
echo " 1) keep Update framework, preserve local files (SOUL.md, USER.md, etc.)" echo "Choose reinstall mode:"
echo " 2) overwrite Replace everything" echo " 1) keep Keep local files (SOUL.md, USER.md, TOOLS.md, memory/) while updating framework"
echo " 3) cancel Abort" echo " 2) overwrite Replace everything in $TARGET_DIR"
echo " 3) cancel Abort install"
printf "Selection [1/2/3] (default: 1): " printf "Selection [1/2/3] (default: 1): "
read -r selection read -r selection
case "${selection:-1}" in case "${selection:-1}" in
1|k|K|keep) INSTALL_MODE="keep" ;; 1|k|K|keep|KEEP) INSTALL_MODE="keep" ;;
2|o|O|overwrite) INSTALL_MODE="overwrite" ;; 2|o|O|overwrite|OVERWRITE) INSTALL_MODE="overwrite" ;;
*) fail "Install cancelled."; exit 1 ;; 3|c|C|cancel|CANCEL|n|N|no|NO)
fail "Install cancelled."
exit 1
;;
*)
warn "Unrecognized selection '$selection'; defaulting to keep."
INSTALL_MODE="keep"
;;
esac esac
else else
warn "Existing install detected without interactive input; defaulting to keep local files."
INSTALL_MODE="keep" INSTALL_MODE="keep"
fi fi
;; ;;
@@ -112,9 +83,10 @@ sync_framework() {
fi fi
if command -v rsync >/dev/null 2>&1; then if command -v rsync >/dev/null 2>&1; then
local rsync_args=(-a --delete --exclude ".git" --exclude ".framework-version") local rsync_args=(-a --delete --exclude ".git")
if [[ "$INSTALL_MODE" == "keep" ]]; then if [[ "$INSTALL_MODE" == "keep" ]]; then
local path
for path in "${PRESERVE_PATHS[@]}"; do for path in "${PRESERVE_PATHS[@]}"; do
rsync_args+=(--exclude "$path") rsync_args+=(--exclude "$path")
done done
@@ -124,10 +96,10 @@ sync_framework() {
return return
fi fi
# Fallback: cp-based sync
local preserve_tmp="" local preserve_tmp=""
if [[ "$INSTALL_MODE" == "keep" ]]; then if [[ "$INSTALL_MODE" == "keep" ]]; then
preserve_tmp="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-preserve-XXXXXX")" preserve_tmp="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-preserve-XXXXXX")"
local path
for path in "${PRESERVE_PATHS[@]}"; do for path in "${PRESERVE_PATHS[@]}"; do
if [[ -e "$TARGET_DIR/$path" ]]; then if [[ -e "$TARGET_DIR/$path" ]]; then
mkdir -p "$preserve_tmp/$(dirname "$path")" mkdir -p "$preserve_tmp/$(dirname "$path")"
@@ -136,11 +108,12 @@ sync_framework() {
done done
fi fi
find "$TARGET_DIR" -mindepth 1 -maxdepth 1 ! -name ".git" ! -name ".framework-version" -exec rm -rf {} + find "$TARGET_DIR" -mindepth 1 -maxdepth 1 ! -name ".git" -exec rm -rf {} +
cp -R "$SOURCE_DIR"/. "$TARGET_DIR"/ cp -R "$SOURCE_DIR"/. "$TARGET_DIR"/
rm -rf "$TARGET_DIR/.git" rm -rf "$TARGET_DIR/.git"
if [[ -n "$preserve_tmp" ]]; then if [[ -n "$preserve_tmp" ]]; then
local path
for path in "${PRESERVE_PATHS[@]}"; do for path in "${PRESERVE_PATHS[@]}"; do
if [[ -e "$preserve_tmp/$path" ]]; then if [[ -e "$preserve_tmp/$path" ]]; then
rm -rf "$TARGET_DIR/$path" rm -rf "$TARGET_DIR/$path"
@@ -152,133 +125,136 @@ sync_framework() {
fi fi
} }
# ═══════════════════════════════════════════════════════════════════════════════
# Migrations — run sequentially from the installed version to FRAMEWORK_VERSION
# ═══════════════════════════════════════════════════════════════════════════════
run_migrations() {
local from_version
from_version="$(installed_framework_version)"
if [[ "$from_version" -ge "$FRAMEWORK_VERSION" ]]; then
return # Already current
fi
step "Running migrations (v${from_version} → v${FRAMEWORK_VERSION})"
# ── Migration: v0/v1 → v2 ─────────────────────────────────────────────────
# Remove bin/ directory — all executables now live in the npm CLI.
# Scripts that were in bin/ are now in tools/_scripts/.
if [[ "$from_version" -lt 2 ]]; then
if [[ -d "$TARGET_DIR/bin" ]]; then
ok "Removing legacy bin/ directory (executables now in npm CLI)"
rm -rf "$TARGET_DIR/bin"
fi
# Remove old mosaic PATH entry from shell profiles
for profile in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.profile"; do
if [[ -f "$profile" ]] && grep -qF "$TARGET_DIR/bin" "$profile"; then
# Remove the PATH line and the comment above it
sed -i.mosaic-migration-bak \
-e "\|# Mosaic agent framework|d" \
-e "\|$TARGET_DIR/bin|d" \
"$profile"
ok "Cleaned up old PATH entry from $(basename "$profile")"
rm -f "${profile}.mosaic-migration-bak"
fi
done
# Remove stale rails/ symlink
if [[ -L "$TARGET_DIR/rails" ]]; then
rm -f "$TARGET_DIR/rails"
fi
fi
# ── Future migrations go here ──────────────────────────────────────────────
# if [[ "$from_version" -lt 3 ]]; then
# ...
# fi
}
# ═══════════════════════════════════════════════════════════════════════════════
# Main
# ═══════════════════════════════════════════════════════════════════════════════
step "Installing Mosaic framework" step "Installing Mosaic framework"
mkdir -p "$TARGET_DIR" mkdir -p "$TARGET_DIR"
select_install_mode select_install_mode
if [[ "$INSTALL_MODE" == "keep" ]]; then if [[ "$INSTALL_MODE" == "keep" ]]; then
ok "Install mode: keep local files (SOUL.md, USER.md, TOOLS.md, memory/)" ok "Install mode: keep local SOUL.md/USER.md/TOOLS.md/memory while updating framework"
else else
ok "Install mode: overwrite" ok "Install mode: overwrite existing files"
fi fi
sync_framework sync_framework
# Ensure memory directory exists # Ensure memory directory exists (preserved across upgrades, may not exist on fresh install)
mkdir -p "$TARGET_DIR/memory" mkdir -p "$TARGET_DIR/memory"
chmod +x "$TARGET_DIR"/bin/*
chmod +x "$TARGET_DIR"/install.sh
# Ensure tool scripts are executable # Ensure tool scripts are executable
find "$TARGET_DIR/tools" -name "*.sh" -exec chmod +x {} + 2>/dev/null || true find "$TARGET_DIR/tools" -name "*.sh" -exec chmod +x {} + 2>/dev/null || true
find "$TARGET_DIR/tools/_scripts" -type f -exec chmod +x {} + 2>/dev/null || true
ok "Framework synced to $TARGET_DIR" # Create backward-compat symlink: rails/ → tools/
if [[ -d "$TARGET_DIR/tools" ]]; then
if [[ -d "$TARGET_DIR/rails" ]] && [[ ! -L "$TARGET_DIR/rails" ]]; then
rm -rf "$TARGET_DIR/rails"
fi
ln -sfn "tools" "$TARGET_DIR/rails"
fi
# Run migrations before post-install (migrations may remove old bin/ etc.) ok "Framework installed to $TARGET_DIR"
run_migrations
step "Post-install tasks" step "Post-install tasks"
SCRIPTS="$TARGET_DIR/tools/_scripts" if "$TARGET_DIR/bin/mosaic-link-runtime-assets" >/dev/null 2>&1; then
if [[ -x "$SCRIPTS/mosaic-link-runtime-assets" ]]; then
if "$SCRIPTS/mosaic-link-runtime-assets" >/dev/null 2>&1; then
ok "Runtime assets linked" ok "Runtime assets linked"
else else
warn "Runtime asset linking failed (non-fatal)" warn "Runtime asset linking failed (non-fatal)"
fi fi
fi
if [[ -x "$SCRIPTS/mosaic-ensure-sequential-thinking" ]]; then if "$TARGET_DIR/bin/mosaic-ensure-sequential-thinking" >/dev/null 2>&1; then
if "$SCRIPTS/mosaic-ensure-sequential-thinking" >/dev/null 2>&1; then
ok "sequential-thinking MCP configured" ok "sequential-thinking MCP configured"
else else
if [[ "${MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING:-0}" == "1" ]]; then if [[ "${MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING:-0}" == "1" ]]; then
warn "sequential-thinking MCP setup bypassed (MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING=1)" warn "sequential-thinking MCP setup failed but bypassed (MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING=1)"
else else
fail "sequential-thinking MCP setup failed (hard requirement)." fail "sequential-thinking MCP setup failed (hard requirement)."
fail "Set MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING=1 only for temporary bypass scenarios."
exit 1 exit 1
fi fi
fi fi
if "$TARGET_DIR/bin/mosaic-ensure-excalidraw" >/dev/null 2>&1; then
ok "excalidraw MCP configured"
else
warn "excalidraw MCP setup failed (non-fatal) — run 'mosaic-ensure-excalidraw' to retry"
fi fi
if [[ -x "$SCRIPTS/mosaic-ensure-excalidraw" ]]; then if [[ "${MOSAIC_SKIP_SKILLS_SYNC:-0}" == "1" ]]; then
"$SCRIPTS/mosaic-ensure-excalidraw" >/dev/null 2>&1 && ok "excalidraw MCP configured" || warn "excalidraw MCP setup failed (non-fatal)" ok "Skills sync skipped (MOSAIC_SKIP_SKILLS_SYNC=1)"
else
if "$TARGET_DIR/bin/mosaic-sync-skills" >/dev/null 2>&1; then
ok "Skills synced"
else
warn "Skills sync failed (non-fatal)"
fi
fi fi
if [[ "${MOSAIC_SKIP_SKILLS_SYNC:-0}" != "1" ]] && [[ -x "$SCRIPTS/mosaic-sync-skills" ]]; then if "$TARGET_DIR/bin/mosaic-migrate-local-skills" --apply >/dev/null 2>&1; then
"$SCRIPTS/mosaic-sync-skills" >/dev/null 2>&1 && ok "Skills synced" || warn "Skills sync failed (non-fatal)" ok "Local skills migrated"
else
warn "Local skill migration failed (non-fatal)"
fi fi
if [[ -x "$SCRIPTS/mosaic-migrate-local-skills" ]]; then if "$TARGET_DIR/bin/mosaic-doctor" >/dev/null 2>&1; then
"$SCRIPTS/mosaic-migrate-local-skills" --apply >/dev/null 2>&1 && ok "Local skills migrated" || warn "Local skill migration failed (non-fatal)" ok "Health audit passed"
else
warn "Health audit reported issues — run 'mosaic doctor' for details"
fi fi
if [[ -x "$SCRIPTS/mosaic-doctor" ]]; then step "PATH configuration"
"$SCRIPTS/mosaic-doctor" >/dev/null 2>&1 && ok "Health audit passed" || warn "Health audit reported issues — run 'mosaic doctor' for details"
PATH_LINE="export PATH=\"$TARGET_DIR/bin:\$PATH\""
# Find the right shell profile
if [[ -n "${ZSH_VERSION:-}" ]] || [[ "$(basename "${SHELL:-}")" == "zsh" ]]; then
SHELL_PROFILE="$HOME/.zshrc"
elif [[ -f "$HOME/.bashrc" ]]; then
SHELL_PROFILE="$HOME/.bashrc"
elif [[ -f "$HOME/.profile" ]]; then
SHELL_PROFILE="$HOME/.profile"
else
SHELL_PROFILE="$HOME/.profile"
fi fi
# Write version stamp AFTER everything succeeds PATH_CHANGED=false
write_framework_version if grep -qF "$TARGET_DIR/bin" "$SHELL_PROFILE" 2>/dev/null; then
ok "Already in PATH via $SHELL_PROFILE"
else
{
echo ""
echo "# Mosaic agent framework"
echo "$PATH_LINE"
} >> "$SHELL_PROFILE"
ok "Added to PATH in $SHELL_PROFILE"
PATH_CHANGED=true
fi
# ── Summary ────────────────────────────────────────────────── # ── Summary ──────────────────────────────────────────────────
echo "" echo ""
echo -e "${GREEN}${BOLD} Mosaic framework installed.${RESET}" echo -e "${GREEN}${BOLD} Mosaic installed successfully.${RESET}"
echo "" echo ""
# Collect next steps
NEXT_STEPS=()
if [[ "$PATH_CHANGED" == "true" ]]; then
NEXT_STEPS+=("Run ${CYAN}source $SHELL_PROFILE${RESET} or log out and back in to activate PATH.")
fi
if [[ ! -f "$TARGET_DIR/SOUL.md" ]]; then if [[ ! -f "$TARGET_DIR/SOUL.md" ]]; then
echo -e " Run ${CYAN}mosaic init${RESET} to set up your agent identity." NEXT_STEPS+=("Run ${CYAN}mosaic init${RESET} to set up your agent identity (SOUL.md), user profile (USER.md), and tool config (TOOLS.md).")
elif grep -q "not configured" "$TARGET_DIR/USER.md" 2>/dev/null; then
NEXT_STEPS+=("Run ${CYAN}mosaic init${RESET} to personalize your user profile (USER.md) and tool config (TOOLS.md).")
fi
if [[ ${#NEXT_STEPS[@]} -gt 0 ]]; then
echo -e " ${BOLD}Next steps:${RESET}"
for i in "${!NEXT_STEPS[@]}"; do
echo -e " $((i+1)). ${NEXT_STEPS[$i]}"
done
echo "" echo ""
fi fi

View File

@@ -69,12 +69,12 @@ case "$cmd" in
echo "[agent-framework] orchestrator already running (pid=$(cat "$PID_FILE"))" echo "[agent-framework] orchestrator already running (pid=$(cat "$PID_FILE"))"
exit 0 exit 0
fi fi
nohup "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-drain" --poll-sec "$poll_sec" $sync_arg >"$LOG_FILE" 2>&1 & nohup "$MOSAIC_HOME/bin/mosaic-orchestrator-drain" --poll-sec "$poll_sec" $sync_arg >"$LOG_FILE" 2>&1 &
echo "$!" > "$PID_FILE" echo "$!" > "$PID_FILE"
echo "[agent-framework] orchestrator started (pid=$!, log=$LOG_FILE)" echo "[agent-framework] orchestrator started (pid=$!, log=$LOG_FILE)"
;; ;;
drain) drain)
exec "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-drain" --poll-sec "$poll_sec" $sync_arg exec "$MOSAIC_HOME/bin/mosaic-orchestrator-drain" --poll-sec "$poll_sec" $sync_arg
;; ;;
stop) stop)
if ! is_running; then if ! is_running; then

View File

@@ -80,11 +80,11 @@ echo -e "${C_CYAN}Capsule:${C_RESET} $(next_task_capsule_path "$PROJECT")"
cd "$PROJECT" cd "$PROJECT"
if [[ "$YOLO" == true ]]; then if [[ "$YOLO" == true ]]; then
exec mosaic yolo "$runtime" "$launch_prompt" exec "$MOSAIC_HOME/bin/mosaic-launch" yolo "$runtime" "$launch_prompt"
elif [[ "$runtime" == "claude" ]]; then elif [[ "$runtime" == "claude" ]]; then
exec mosaic claude "$launch_prompt" exec "$MOSAIC_HOME/bin/mosaic-launch" claude "$launch_prompt"
elif [[ "$runtime" == "codex" ]]; then elif [[ "$runtime" == "codex" ]]; then
exec mosaic codex "$launch_prompt" exec "$MOSAIC_HOME/bin/mosaic-launch" codex "$launch_prompt"
fi fi
echo -e "${C_RED}Unsupported coord runtime: $runtime${C_RESET}" >&2 echo -e "${C_RED}Unsupported coord runtime: $runtime${C_RESET}" >&2

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/mosaic", "name": "@mosaic/mosaic",
"version": "0.0.4", "version": "0.0.1-alpha.2",
"description": "Mosaic agent framework — installation wizard and meta package", "description": "Mosaic agent framework — installation wizard and meta package",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",

View File

@@ -1,7 +1,7 @@
import { homedir } from 'node:os'; import { homedir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
export const VERSION = '0.0.2'; export const VERSION = '0.1.0';
export const DEFAULT_MOSAIC_HOME = join(homedir(), '.config', 'mosaic'); export const DEFAULT_MOSAIC_HOME = join(homedir(), '.config', 'mosaic');

View File

@@ -9,7 +9,7 @@ import {
unlinkSync, unlinkSync,
statSync, statSync,
} from 'node:fs'; } from 'node:fs';
import { dirname, join, relative, resolve } from 'node:path'; import { dirname, join, relative } from 'node:path';
const MAX_BACKUPS = 3; const MAX_BACKUPS = 3;
@@ -68,9 +68,6 @@ export function syncDirectory(
target: string, target: string,
options: { preserve?: string[]; excludeGit?: boolean } = {}, options: { preserve?: string[]; excludeGit?: boolean } = {},
): void { ): void {
// Guard: source and target are the same directory — nothing to sync
if (resolve(source) === resolve(target)) return;
const preserveSet = new Set(options.preserve ?? []); const preserveSet = new Set(options.preserve ?? []);
// Collect files from source // Collect files from source
@@ -80,10 +77,9 @@ export function syncDirectory(
const stat = statSync(src); const stat = statSync(src);
if (stat.isDirectory()) { if (stat.isDirectory()) {
const relPath = relative(relBase, src); const relPath = relative(relBase, src);
const dirName = relPath.split('/').pop() ?? '';
// Skip any .git directory (top-level or nested, e.g. sources/agent-skills/.git) // Skip .git
if (options.excludeGit && (dirName === '.git' || relPath.includes('/.git'))) return; if (options.excludeGit && relPath === '.git') return;
// Skip preserved paths at top level // Skip preserved paths at top level
if (preserveSet.has(relPath) && existsSync(dest)) return; if (preserveSet.has(relPath) && existsSync(dest)) return;
@@ -95,9 +91,6 @@ export function syncDirectory(
} else { } else {
const relPath = relative(relBase, src); const relPath = relative(relBase, src);
// Skip files inside .git directories
if (options.excludeGit && relPath.includes('/.git/')) return;
// Skip preserved files at top level // Skip preserved files at top level
if (preserveSet.has(relPath) && existsSync(dest)) return; if (preserveSet.has(relPath) && existsSync(dest)) return;

View File

@@ -87,9 +87,7 @@ export async function detectInstallStage(
], ],
}); });
if (state.installAction === 'keep' || state.installAction === 'reconfigure') { if (state.installAction === 'keep') {
// Load existing values — for 'keep' they're final, for 'reconfigure'
// they become pre-populated defaults so the user can tweak them.
state.soul = await config.readSoul(); state.soul = await config.readSoul();
state.user = await config.readUser(); state.user = await config.readUser();
state.tools = await config.readTools(); state.tools = await config.readTools();

View File

@@ -24,18 +24,6 @@ export async function soulSetupStage(p: WizardPrompter, state: WizardState): Pro
return undefined; return undefined;
}, },
}); });
} else if (state.installAction === 'reconfigure') {
// Show existing value as default so the user can accept or change it
state.soul.agentName = await p.text({
message: 'What name should agents use?',
placeholder: state.soul.agentName,
defaultValue: state.soul.agentName,
validate: (v) => {
if (v.length === 0) return 'Name cannot be empty';
if (v.length > 50) return 'Name must be under 50 characters';
return undefined;
},
});
} }
if (state.mode === 'advanced') { if (state.mode === 'advanced') {
@@ -50,7 +38,7 @@ export async function soulSetupStage(p: WizardPrompter, state: WizardState): Pro
state.soul.roleDescription ??= DEFAULTS.roleDescription; state.soul.roleDescription ??= DEFAULTS.roleDescription;
} }
if (!state.soul.communicationStyle || state.installAction === 'reconfigure') { if (!state.soul.communicationStyle) {
state.soul.communicationStyle = await p.select<CommunicationStyle>({ state.soul.communicationStyle = await p.select<CommunicationStyle>({
message: 'Communication style', message: 'Communication style',
options: [ options: [
@@ -58,7 +46,7 @@ export async function soulSetupStage(p: WizardPrompter, state: WizardState): Pro
{ value: 'friendly', label: 'Friendly', hint: 'Warm but efficient, conversational' }, { value: 'friendly', label: 'Friendly', hint: 'Warm but efficient, conversational' },
{ value: 'formal', label: 'Formal', hint: 'Professional, structured, thorough' }, { value: 'formal', label: 'Formal', hint: 'Professional, structured, thorough' },
], ],
initialValue: state.soul.communicationStyle ?? 'direct', initialValue: 'direct',
}); });
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/prdy", "name": "@mosaic/prdy",
"version": "0.0.2", "version": "0.0.1-alpha.2",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"exports": { "exports": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/quality-rails", "name": "@mosaic/quality-rails",
"version": "0.0.2", "version": "0.0.1-alpha.2",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/queue", "name": "@mosaic/queue",
"version": "0.0.2", "version": "0.0.1-alpha.2",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"exports": { "exports": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/types", "name": "@mosaic/types",
"version": "0.0.2", "version": "0.0.1-alpha.2",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"exports": { "exports": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/discord-plugin", "name": "@mosaic/discord-plugin",
"version": "0.0.2", "version": "0.0.1-alpha.2",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"exports": { "exports": {

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/oc-macp-plugin", "name": "@mosaic/oc-macp-plugin",
"version": "0.0.2", "version": "0.0.1-alpha.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"description": "OpenClaw ACP runtime backend that routes sessions_spawn(runtime:\"macp\") to the Pi MACP runner.", "description": "OpenClaw ACP runtime backend that routes sessions_spawn(runtime:\"macp\") to the Pi MACP runner.",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/oc-framework-plugin", "name": "@mosaic/oc-framework-plugin",
"version": "0.0.2", "version": "0.0.1-alpha.2",
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
"description": "Injects Mosaic framework rails, runtime contract, and active mission context into all OpenClaw agent sessions and ACP subagent spawns.", "description": "Injects Mosaic framework rails, runtime contract, and active mission context into all OpenClaw agent sessions and ACP subagent spawns.",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@mosaic/telegram-plugin", "name": "@mosaic/telegram-plugin",
"version": "0.0.2", "version": "0.0.1-alpha.2",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"exports": { "exports": {

View File

@@ -69,12 +69,12 @@ case "$cmd" in
echo "[agent-framework] orchestrator already running (pid=$(cat "$PID_FILE"))" echo "[agent-framework] orchestrator already running (pid=$(cat "$PID_FILE"))"
exit 0 exit 0
fi fi
nohup "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-drain" --poll-sec "$poll_sec" $sync_arg >"$LOG_FILE" 2>&1 & nohup "$MOSAIC_HOME/bin/mosaic-orchestrator-drain" --poll-sec "$poll_sec" $sync_arg >"$LOG_FILE" 2>&1 &
echo "$!" > "$PID_FILE" echo "$!" > "$PID_FILE"
echo "[agent-framework] orchestrator started (pid=$!, log=$LOG_FILE)" echo "[agent-framework] orchestrator started (pid=$!, log=$LOG_FILE)"
;; ;;
drain) drain)
exec "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-drain" --poll-sec "$poll_sec" $sync_arg exec "$MOSAIC_HOME/bin/mosaic-orchestrator-drain" --poll-sec "$poll_sec" $sync_arg
;; ;;
stop) stop)
if ! is_running; then if ! is_running; then

View File

@@ -114,10 +114,10 @@ version_lt() {
} }
framework_version() { framework_version() {
# Read framework schema version stamp # Read VERSION from the installed mosaic launcher
local vf="$MOSAIC_HOME/.framework-version" local mosaic_bin="$MOSAIC_HOME/bin/mosaic"
if [[ -f "$vf" ]]; then if [[ -f "$mosaic_bin" ]]; then
cat "$vf" 2>/dev/null || true grep -m1 '^VERSION=' "$mosaic_bin" 2>/dev/null | cut -d'"' -f2 || true
fi fi
} }
@@ -145,7 +145,7 @@ if [[ "$FLAG_FRAMEWORK" == "true" ]]; then
FRAMEWORK_CURRENT="$(framework_version)" FRAMEWORK_CURRENT="$(framework_version)"
HAS_FRAMEWORK=false HAS_FRAMEWORK=false
[[ -f "$MOSAIC_HOME/AGENTS.md" ]] || [[ -f "$MOSAIC_HOME/.framework-version" ]] && HAS_FRAMEWORK=true [[ -d "$MOSAIC_HOME/bin" ]] && [[ -f "$MOSAIC_HOME/bin/mosaic" ]] && HAS_FRAMEWORK=true
if [[ -n "$FRAMEWORK_CURRENT" ]]; then if [[ -n "$FRAMEWORK_CURRENT" ]]; then
dim " Installed: framework v${FRAMEWORK_CURRENT}" dim " Installed: framework v${FRAMEWORK_CURRENT}"
@@ -297,9 +297,21 @@ fi
if [[ "$FLAG_CHECK" == "false" ]]; then if [[ "$FLAG_CHECK" == "false" ]]; then
step "Summary" step "Summary"
echo " ${BOLD}mosaic:${RESET} $PREFIX/bin/mosaic" echo " ${BOLD}mosaic CLI:${RESET} $PREFIX/bin/mosaic"
dim " Framework data: $MOSAIC_HOME/" dim " All commands: mosaic claude, mosaic yolo pi, mosaic tui, mosaic doctor, …"
echo "" echo ""
dim " Framework data: $MOSAIC_HOME/"
dim " Launcher backend: $MOSAIC_HOME/bin/mosaic-launch"
# Clean up old mosaic binary from PATH if framework bin is still there
FRAMEWORK_BIN="$MOSAIC_HOME/bin"
if [[ ":$PATH:" == *":$FRAMEWORK_BIN:"* ]]; then
OLD_MOSAIC="$FRAMEWORK_BIN/mosaic"
if [[ -f "$OLD_MOSAIC" ]] && [[ ! -L "$OLD_MOSAIC" ]]; then
info "Removing old framework 'mosaic' binary (replaced by npm CLI)"
rm -f "$OLD_MOSAIC"
fi
fi
# First install guidance # First install guidance
if [[ ! -f "$MOSAIC_HOME/SOUL.md" ]]; then if [[ ! -f "$MOSAIC_HOME/SOUL.md" ]]; then