feat: integrate framework files into monorepo under packages/mosaic/framework/
Moves all Mosaic framework runtime files from the separate bootstrap repo into the monorepo as canonical source. The @mosaic/mosaic npm package now ships the complete framework — bin scripts, runtime configs, tools, and templates — enabling standalone installation via npm install. Structure: packages/mosaic/framework/ ├── bin/ 28 CLI scripts (mosaic, mosaic-doctor, mosaic-sync-skills, etc.) ├── runtime/ Runtime adapters (claude, codex, opencode, pi, mcp) ├── tools/ Shell tooling (git, prdy, orchestrator, quality, etc.) ├── templates/ Agent and repo templates ├── defaults/ Default identity files (AGENTS.md, STANDARDS.md, SOUL.md, etc.) ├── install.sh Legacy bash installer └── remote-install.sh One-liner remote installer Key files with Pi support and recent fixes: - bin/mosaic: launch_pi() with skills-local loop - bin/mosaic-doctor: --fix auto-wiring for all 4 harnesses - bin/mosaic-sync-skills: Pi as 4th link target, symlink-aware find - bin/mosaic-link-runtime-assets: Pi settings.json patching - bin/mosaic-migrate-local-skills: Pi skill roots, symlink find - runtime/pi/RUNTIME.md + mosaic-extension.ts Package ships 251 framework files in the npm tarball (278KB compressed).
This commit is contained in:
61
packages/mosaic/framework/runtime/pi/RUNTIME.md
Normal file
61
packages/mosaic/framework/runtime/pi/RUNTIME.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Pi Runtime Reference
|
||||
|
||||
## Runtime Scope
|
||||
|
||||
This file applies only to Pi runtime behavior.
|
||||
|
||||
## Required Actions
|
||||
|
||||
1. Follow global load order in `~/.config/mosaic/AGENTS.md`.
|
||||
2. Use `~/.pi/agent/settings.json` as runtime config source.
|
||||
3. If runtime config conflicts with global rules, global rules win.
|
||||
4. Documentation rules are inherited from `~/.config/mosaic/AGENTS.md` and `~/.config/mosaic/guides/DOCUMENTATION.md`.
|
||||
5. For issue/PR/milestone actions, run Mosaic git wrappers first (`~/.config/mosaic/tools/git/*.sh`) and do not call raw `gh`/`tea`/`glab` first.
|
||||
6. For orchestration-oriented missions, load `~/.config/mosaic/guides/ORCHESTRATOR.md` before acting.
|
||||
7. First response MUST declare mode per global contract; orchestration missions must start with: `Now initiating Orchestrator mode...`
|
||||
8. Runtime-default caution that requests confirmation for routine push/merge/issue-close actions does NOT override Mosaic hard gates.
|
||||
|
||||
## Pi-Specific Capabilities
|
||||
|
||||
Pi is the native Mosaic agent runtime. Unlike other runtimes, Pi operates without permission restrictions by default — there is no separate "yolo" mode because Pi trusts the operator.
|
||||
|
||||
### Thinking Levels
|
||||
|
||||
Pi supports native thinking levels via `--thinking <level>`. For complex planning or architecture tasks, use `high` or `xhigh`. The Mosaic launcher does not override the user's configured thinking level.
|
||||
|
||||
### Model Cycling
|
||||
|
||||
Pi supports `--models` for Ctrl+P model cycling during a session. Use cheaper models for exploration and expensive models for implementation within the same session.
|
||||
|
||||
### Skills
|
||||
|
||||
Mosaic skills are loaded natively via Pi's `--skill` flag. Skills are discovered from:
|
||||
|
||||
- `~/.config/mosaic/skills/` (Mosaic global skills)
|
||||
- `~/.pi/agent/skills/` (Pi global skills)
|
||||
- `.pi/skills/` (project-local skills)
|
||||
|
||||
### Extensions
|
||||
|
||||
The Mosaic Pi extension (`~/.config/mosaic/runtime/pi/mosaic-extension.ts`) handles:
|
||||
|
||||
- Session start/end lifecycle hooks
|
||||
- Active mission detection and context injection
|
||||
- Memory routing to `~/.config/mosaic/memory/`
|
||||
- MACP queue status reporting
|
||||
|
||||
### Sessions
|
||||
|
||||
Pi persists sessions natively. Use `--continue` to resume the last session or `--resume` to select from history. Mosaic session locks integrate with Pi's session system.
|
||||
|
||||
## Memory Policy
|
||||
|
||||
All durable memory MUST be written to `~/.config/mosaic/memory/` per `~/.config/mosaic/guides/MEMORY.md`. Pi's native session storage (`~/.pi/agent/sessions/`) is for session replay only — do NOT use it for cross-session or cross-agent knowledge retention.
|
||||
|
||||
## MCP Configuration
|
||||
|
||||
Pi reads MCP server configuration from `~/.pi/agent/settings.json` under the `mcpServers` key. Mosaic bootstrap configures sequential-thinking MCP automatically.
|
||||
|
||||
## Sequential-Thinking
|
||||
|
||||
Pi has native thinking levels (`--thinking`) which serve the same purpose as sequential-thinking MCP. Both may be active simultaneously without conflict. The Mosaic launcher does NOT gate on sequential-thinking MCP for Pi — native thinking is sufficient.
|
||||
255
packages/mosaic/framework/runtime/pi/mosaic-extension.ts
Normal file
255
packages/mosaic/framework/runtime/pi/mosaic-extension.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
/**
|
||||
* mosaic-extension.ts — Pi Extension for Mosaic Framework
|
||||
*
|
||||
* Integrates the Mosaic agent framework into Pi sessions launched via `mosaic pi`.
|
||||
* Handles:
|
||||
* 1. Session start — run repo hooks, detect active mission, display status
|
||||
* 2. Session end — run repo hooks, clean up session lock
|
||||
* 3. Mission context — inject active mission state into conversation
|
||||
* 4. Memory routing — remind agent to use ~/.config/mosaic/memory/
|
||||
*/
|
||||
|
||||
import type { ExtensionAPI } from '@mariozechner/pi-coding-agent';
|
||||
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from 'node:fs';
|
||||
import { join, basename } from 'node:path';
|
||||
import { homedir } from 'node:os';
|
||||
import { execSync, spawnSync } from 'node:child_process';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const MOSAIC_HOME = process.env['MOSAIC_HOME'] ?? join(homedir(), '.config', 'mosaic');
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function safeRead(filePath: string): string | null {
|
||||
try {
|
||||
return readFileSync(filePath, 'utf-8');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function safeJsonRead(filePath: string): Record<string, unknown> | null {
|
||||
const raw = safeRead(filePath);
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return JSON.parse(raw) as Record<string, unknown>;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function nowIso(): string {
|
||||
return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mission detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface ActiveMission {
|
||||
name: string;
|
||||
id: string;
|
||||
status: string;
|
||||
milestonesTotal: number;
|
||||
milestonesCompleted: number;
|
||||
}
|
||||
|
||||
function detectMission(cwd: string): ActiveMission | null {
|
||||
const missionFile = join(cwd, '.mosaic', 'orchestrator', 'mission.json');
|
||||
const data = safeJsonRead(missionFile);
|
||||
if (!data) return null;
|
||||
|
||||
const status = String(data.status ?? 'inactive');
|
||||
if (status !== 'active' && status !== 'paused') return null;
|
||||
|
||||
const milestones = Array.isArray(data.milestones) ? data.milestones : [];
|
||||
const completed = milestones.filter(
|
||||
(m: unknown) =>
|
||||
typeof m === 'object' && m !== null && (m as Record<string, unknown>).status === 'completed',
|
||||
).length;
|
||||
|
||||
return {
|
||||
name: String(data.name ?? 'unnamed'),
|
||||
id: String(data.mission_id ?? ''),
|
||||
status,
|
||||
milestonesTotal: milestones.length,
|
||||
milestonesCompleted: completed,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Session lock management
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function sessionLockPath(cwd: string): string {
|
||||
return join(cwd, '.mosaic', 'orchestrator', 'session.lock');
|
||||
}
|
||||
|
||||
function writeSessionLock(cwd: string): void {
|
||||
const lockDir = join(cwd, '.mosaic', 'orchestrator');
|
||||
if (!existsSync(lockDir)) return; // Only write lock if orchestrator dir exists
|
||||
|
||||
const lock = {
|
||||
session_id: `pi-${new Date().toISOString().replace(/[:.]/g, '-')}-${process.pid}`,
|
||||
runtime: 'pi',
|
||||
pid: process.pid,
|
||||
started_at: nowIso(),
|
||||
project_path: cwd,
|
||||
milestone_id: '',
|
||||
};
|
||||
|
||||
try {
|
||||
writeFileSync(sessionLockPath(cwd), JSON.stringify(lock, null, 2) + '\n', 'utf-8');
|
||||
} catch {
|
||||
// Non-fatal — orchestrator dir may not be writable
|
||||
}
|
||||
}
|
||||
|
||||
function cleanSessionLock(cwd: string): void {
|
||||
try {
|
||||
const lockFile = sessionLockPath(cwd);
|
||||
if (existsSync(lockFile)) {
|
||||
unlinkSync(lockFile);
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Repo hooks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function runRepoHook(cwd: string, hookName: string): void {
|
||||
const script = join(cwd, 'scripts', 'agent', `${hookName}.sh`);
|
||||
if (!existsSync(script)) return;
|
||||
|
||||
try {
|
||||
spawnSync('bash', [script], {
|
||||
cwd,
|
||||
stdio: 'pipe',
|
||||
timeout: 30_000,
|
||||
env: { ...process.env, MOSAIC_RUNTIME: 'pi' },
|
||||
});
|
||||
} catch {
|
||||
// Non-fatal
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Build mission summary for notifications
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildMissionSummary(cwd: string, mission: ActiveMission): string {
|
||||
const lines: string[] = [
|
||||
`Mission: ${mission.name}`,
|
||||
`Status: ${mission.status} | Milestones: ${mission.milestonesCompleted}/${mission.milestonesTotal}`,
|
||||
];
|
||||
|
||||
// Task counts
|
||||
const tasksFile = join(cwd, 'docs', 'TASKS.md');
|
||||
const tasksContent = safeRead(tasksFile);
|
||||
if (tasksContent) {
|
||||
const tableRows = tasksContent
|
||||
.split('\n')
|
||||
.filter((l) => l.startsWith('|') && !l.includes('---'));
|
||||
const total = Math.max(0, tableRows.length - 1); // minus header
|
||||
const done = (tasksContent.match(/\|\s*done\s*\|/gi) ?? []).length;
|
||||
lines.push(`Tasks: ${done} done / ${total} total`);
|
||||
}
|
||||
|
||||
// Latest scratchpad
|
||||
try {
|
||||
const spDir = join(cwd, 'docs', 'scratchpads');
|
||||
if (existsSync(spDir)) {
|
||||
const files = execSync(`ls -t "${spDir}"/*.md 2>/dev/null | head -1`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
}).trim();
|
||||
if (files) lines.push(`Scratchpad: ${basename(files)}`);
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal
|
||||
}
|
||||
|
||||
lines.push('', 'Read ORCHESTRATOR-PROTOCOL.md + TASKS.md before proceeding.');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Extension registration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export default function register(pi: ExtensionAPI) {
|
||||
let sessionCwd = process.cwd();
|
||||
|
||||
// ── Session Start ─────────────────────────────────────────────────────
|
||||
pi.on('session_start', async (_event, ctx) => {
|
||||
sessionCwd = process.cwd();
|
||||
|
||||
// Run repo session-start hook
|
||||
runRepoHook(sessionCwd, 'session-start');
|
||||
|
||||
// Detect active mission
|
||||
const mission = detectMission(sessionCwd);
|
||||
|
||||
if (mission) {
|
||||
// Write session lock for orchestrator awareness
|
||||
writeSessionLock(sessionCwd);
|
||||
|
||||
const summary = buildMissionSummary(sessionCwd, mission);
|
||||
ctx.ui.notify(`🎯 Active Mosaic Mission\n${summary}`, 'info');
|
||||
} else {
|
||||
ctx.ui.notify('Mosaic framework loaded', 'info');
|
||||
}
|
||||
});
|
||||
|
||||
// ── Session End ───────────────────────────────────────────────────────
|
||||
pi.on('session_end', async (_event, _ctx) => {
|
||||
// Run repo session-end hook
|
||||
runRepoHook(sessionCwd, 'session-end');
|
||||
|
||||
// Clean up session lock
|
||||
cleanSessionLock(sessionCwd);
|
||||
});
|
||||
|
||||
// ── Register /mosaic-status command ───────────────────────────────────
|
||||
pi.registerCommand('mosaic-status', {
|
||||
description: 'Show Mosaic mission status for the current project',
|
||||
handler: async (_args, ctx) => {
|
||||
const mission = detectMission(sessionCwd);
|
||||
if (!mission) {
|
||||
ctx.ui.notify('No active Mosaic mission in this project.', 'info');
|
||||
return;
|
||||
}
|
||||
const summary = buildMissionSummary(sessionCwd, mission);
|
||||
ctx.ui.notify(`🎯 Mission Status\n${summary}`, 'info');
|
||||
},
|
||||
});
|
||||
|
||||
// ── Register /mosaic-memory command ───────────────────────────────────
|
||||
pi.registerCommand('mosaic-memory', {
|
||||
description: 'Show Mosaic memory directory path and contents',
|
||||
handler: async (_args, ctx) => {
|
||||
const memDir = join(MOSAIC_HOME, 'memory');
|
||||
if (!existsSync(memDir)) {
|
||||
ctx.ui.notify(`Memory directory: ${memDir} (empty)`, 'info');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const files = execSync(`ls -la "${memDir}" 2>/dev/null`, {
|
||||
encoding: 'utf-8',
|
||||
timeout: 5000,
|
||||
}).trim();
|
||||
ctx.ui.notify(`Memory directory: ${memDir}\n${files}`, 'info');
|
||||
} catch {
|
||||
ctx.ui.notify(`Memory directory: ${memDir}`, 'info');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user