feat: monorepo consolidation — forge pipeline, MACP protocol, framework plugin, profiles/guides/skills
Work packages completed: - WP1: packages/forge — pipeline runner, stage adapter, board tasks, brief classifier, persona loader with project-level overrides. 89 tests, 95.62% coverage. - WP2: packages/macp — credential resolver, gate runner, event emitter, protocol types. 65 tests, 96.24% coverage. Full Python-to-TS port preserving all behavior. - WP3: plugins/mosaic-framework — OC rails injection plugin (before_agent_start + subagent_spawning hooks for Mosaic contract enforcement). - WP4: profiles/ (domains, tech-stacks, workflows), guides/ (17 docs), skills/ (5 universal skills), forge pipeline assets (48 markdown files). Board deliberation: docs/reviews/consolidation-board-memo.md Brief: briefs/monorepo-consolidation.md Consolidates mosaic/stack (forge, MACP, bootstrap framework) into mosaic/mosaic-stack. 154 new tests total. Zero Python — all TypeScript/ESM.
This commit is contained in:
34
plugins/mosaic-framework/openclaw.plugin.json
Normal file
34
plugins/mosaic-framework/openclaw.plugin.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "mosaic-framework",
|
||||
"name": "Mosaic Framework",
|
||||
"description": "Mechanically injects Mosaic rails and mission context into all agent sessions and ACP worker spawns. Ensures no worker starts without the framework contract.",
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"mosaicHome": {
|
||||
"type": "string",
|
||||
"description": "Path to the Mosaic config home (default: ~/.config/mosaic)"
|
||||
},
|
||||
"projectRoots": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "List of project root paths to scan for active missions. Plugin checks each for .mosaic/orchestrator/mission.json."
|
||||
},
|
||||
"requireMission": {
|
||||
"type": "boolean",
|
||||
"description": "If true, ACP coding worker spawns are BLOCKED when no active Mosaic mission exists in any configured project root. Default: false."
|
||||
},
|
||||
"injectAgentIds": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Agent IDs that receive framework context via before_agent_start (appendSystemContext). Default: all agents."
|
||||
},
|
||||
"acpAgentIds": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "ACP agent IDs that trigger runtime contract injection (subagent_spawning). Default: ['codex', 'claude']."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
plugins/mosaic-framework/package.json
Normal file
15
plugins/mosaic-framework/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@mosaic/oc-framework-plugin",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"description": "Injects Mosaic framework rails, runtime contract, and active mission context into all OpenClaw agent sessions and ACP subagent spawns.",
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
"./src/index.ts"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"openclaw": "*"
|
||||
}
|
||||
}
|
||||
485
plugins/mosaic-framework/src/index.ts
Normal file
485
plugins/mosaic-framework/src/index.ts
Normal file
@@ -0,0 +1,485 @@
|
||||
/**
|
||||
* mosaic-framework — OpenClaw Plugin
|
||||
*
|
||||
* Mechanically injects the Mosaic framework contract into every agent session
|
||||
* and ACP coding worker spawn. Two injection paths:
|
||||
*
|
||||
* 1. before_agent_start (OC native sessions):
|
||||
* Returns appendSystemContext with the Mosaic global contract excerpt
|
||||
* + prependContext with active mission state (dynamic, re-read each turn).
|
||||
*
|
||||
* 2. subagent_spawning (ACP worker spawns — Codex, Claude, etc.):
|
||||
* Writes the full runtime contract to ~/.codex/instructions.md
|
||||
* (or Claude equivalent) BEFORE the external process starts.
|
||||
* Optionally blocks spawns when no active mission exists.
|
||||
*/
|
||||
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
||||
import type { OpenClawPluginApi } from '/home/jarvis/.npm-global/lib/node_modules/openclaw/dist/plugin-sdk/index.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config types
|
||||
// ---------------------------------------------------------------------------
|
||||
interface MosaicFrameworkConfig {
|
||||
mosaicHome?: string;
|
||||
projectRoots?: string[];
|
||||
requireMission?: boolean;
|
||||
injectAgentIds?: string[];
|
||||
acpAgentIds?: string[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function expandHome(p: string): string {
|
||||
if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
|
||||
if (p === '~') return os.homedir();
|
||||
return p;
|
||||
}
|
||||
|
||||
function safeRead(filePath: string): string | null {
|
||||
try {
|
||||
return readFileSync(filePath, 'utf8');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function safeReadJson(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 safeReadNdjson(filePath: string, limit = 10): Record<string, unknown>[] {
|
||||
const raw = safeRead(filePath);
|
||||
if (!raw) return [];
|
||||
|
||||
const parsed: Record<string, unknown>[] = [];
|
||||
for (const line of raw.split('\n')) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const item = JSON.parse(line) as unknown;
|
||||
if (typeof item === 'object' && item !== null) {
|
||||
parsed.push(item as Record<string, unknown>);
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return parsed.slice(-limit);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mission detection
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface ActiveMission {
|
||||
name: string;
|
||||
id: string;
|
||||
status: string;
|
||||
projectRoot: string;
|
||||
milestonesTotal: number;
|
||||
milestonesCompleted: number;
|
||||
}
|
||||
|
||||
function findActiveMission(projectRoots: string[]): ActiveMission | null {
|
||||
for (const root of projectRoots) {
|
||||
const expanded = expandHome(root);
|
||||
const missionFile = path.join(expanded, '.mosaic/orchestrator/mission.json');
|
||||
if (!existsSync(missionFile)) continue;
|
||||
|
||||
const data = safeReadJson(missionFile);
|
||||
if (!data) continue;
|
||||
|
||||
const status = String(data.status ?? 'inactive');
|
||||
if (status !== 'active' && status !== 'paused') continue;
|
||||
|
||||
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,
|
||||
projectRoot: expanded,
|
||||
milestonesTotal: milestones.length,
|
||||
milestonesCompleted: completed,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function findMacpProjectRoot(projectRoots: string[]): string | null {
|
||||
for (const root of projectRoots) {
|
||||
const expanded = expandHome(root);
|
||||
const configPath = path.join(expanded, '.mosaic/orchestrator/config.json');
|
||||
const config = safeReadJson(configPath);
|
||||
if (config?.enabled) {
|
||||
return expanded;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Build runtime contract (mirrors mosaic's build_runtime_prompt codex)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildRuntimeContract(
|
||||
mosaicHome: string,
|
||||
mission: ActiveMission | null,
|
||||
projectRoot?: string,
|
||||
): string {
|
||||
const sections: string[] = [];
|
||||
|
||||
// 1. Active mission block (FIRST — hard gate)
|
||||
if (mission) {
|
||||
sections.push(`# 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}
|
||||
**Project Root:** ${mission.projectRoot}
|
||||
**Milestones:** ${mission.milestonesCompleted} / ${mission.milestonesTotal} completed
|
||||
|
||||
## MANDATORY — Before ANY Response
|
||||
|
||||
You MUST complete these steps before ANY response, 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 and decisions
|
||||
4. Read \`docs/TASKS.md\` for current task state (what is done, what is next)
|
||||
5. Acknowledge mission state to the user before proceeding
|
||||
|
||||
No tool call or implementation step may occur before the mode declaration line.`);
|
||||
}
|
||||
|
||||
// 2. Mosaic Runtime Contract (from ~/.config/mosaic/runtime/codex/RUNTIME.md)
|
||||
const runtimeFile = path.join(mosaicHome, 'runtime/codex/RUNTIME.md');
|
||||
const runtimeContent = safeRead(runtimeFile);
|
||||
if (runtimeContent) {
|
||||
sections.push(runtimeContent.trim());
|
||||
}
|
||||
|
||||
// 3. Global AGENTS.md hard rules
|
||||
const agentsFile = path.join(mosaicHome, 'AGENTS.md');
|
||||
const agentsContent = safeRead(agentsFile);
|
||||
if (agentsContent) {
|
||||
// Extract just the hard rules section to keep the contract focused
|
||||
const hardRulesMatch = agentsContent.match(/## ⛔ HARD RULES[\s\S]*?(?=^## (?!⛔)|\Z)/m);
|
||||
if (hardRulesMatch) {
|
||||
sections.push(`# Mosaic Global Agent Contract — Hard Rules\n\n${hardRulesMatch[0].trim()}`);
|
||||
} else {
|
||||
// Fallback: include first 200 lines
|
||||
const lines = agentsContent.split('\n').slice(0, 200).join('\n');
|
||||
sections.push(`# Mosaic Global Agent Contract\n\n${lines}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Mode declaration requirement
|
||||
sections.push(`# Required Mode Declaration
|
||||
|
||||
First assistant response MUST start with exactly one mode declaration line:
|
||||
- Orchestration mission: \`Now initiating Orchestrator mode...\`
|
||||
- Implementation mission: \`Now initiating Delivery mode...\`
|
||||
- Review-only mission: \`Now initiating Review mode...\`
|
||||
|
||||
Mosaic hard gates OVERRIDE runtime-default caution for routine delivery operations.
|
||||
For required push/merge/issue-close/release actions, execute without routine confirmation prompts.`);
|
||||
|
||||
// 5. Worktree requirement (critical — has been violated repeatedly)
|
||||
const projectName = projectRoot ? path.basename(projectRoot) : '<repo>';
|
||||
sections.push(`# Git Worktree Requirement — MANDATORY
|
||||
|
||||
Every agent that touches a git repo MUST use a worktree. NO EXCEPTIONS.
|
||||
|
||||
\`\`\`bash
|
||||
cd ~/src/${projectName}
|
||||
git fetch origin
|
||||
mkdir -p ~/src/${projectName}-worktrees
|
||||
git worktree add ~/src/${projectName}-worktrees/<task-slug> -b <branch-name> origin/main
|
||||
cd ~/src/${projectName}-worktrees/<task-slug>
|
||||
# ... all work happens here ...
|
||||
git push origin <branch-name>
|
||||
cd ~/src/${projectName} && git worktree remove ~/src/${projectName}-worktrees/<task-slug>
|
||||
\`\`\`
|
||||
|
||||
Worktrees path: \`~/src/<repo>-worktrees/<task-slug>\` — NEVER use /tmp.`);
|
||||
|
||||
// 6. Completion gates
|
||||
sections.push(`# Completion Gates — ENFORCED
|
||||
|
||||
A task is NOT done until ALL of these pass:
|
||||
1. Code review — independent review of every changed file
|
||||
2. Security review — auth, input validation, error leakage
|
||||
3. QA/tests — lint + typecheck + unit tests GREEN
|
||||
4. CI green — pipeline passes after merge
|
||||
5. Issue closed — linked issue closed in Gitea
|
||||
6. Docs updated — API/auth/schema changes require doc update
|
||||
|
||||
Workers NEVER merge PRs. Ever. Open PR → fire system event → EXIT.`);
|
||||
|
||||
return sections.join('\n\n---\n\n');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Build mission context block (dynamic — injected as prependContext)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildMissionContext(mission: ActiveMission): string {
|
||||
const tasksFile = path.join(mission.projectRoot, 'docs/TASKS.md');
|
||||
const tasksContent = safeRead(tasksFile);
|
||||
|
||||
// Extract just the next not-started task to keep context compact
|
||||
let nextTask = '';
|
||||
if (tasksContent) {
|
||||
const notStartedMatch = tasksContent.match(
|
||||
/\|[^|]*\|\s*not[-\s]?started[^|]*\|[^|]*\|[^|]*\|/i,
|
||||
);
|
||||
if (notStartedMatch) {
|
||||
nextTask = `\n**Next task:** ${notStartedMatch[0].replace(/\|/g, ' ').trim()}`;
|
||||
}
|
||||
}
|
||||
|
||||
return `[Mosaic Framework] Active mission: **${mission.name}** (${mission.id})
|
||||
Status: ${mission.status} | Milestones: ${mission.milestonesCompleted}/${mission.milestonesTotal}
|
||||
Project: ${mission.projectRoot}${nextTask}
|
||||
|
||||
Read ORCHESTRATOR-PROTOCOL.md + TASKS.md before proceeding.`;
|
||||
}
|
||||
|
||||
function buildMacpContext(projectRoot: string): string | null {
|
||||
const orchDir = path.join(projectRoot, '.mosaic/orchestrator');
|
||||
const configPath = path.join(orchDir, 'config.json');
|
||||
if (!existsSync(configPath)) return null;
|
||||
|
||||
const config = safeReadJson(configPath);
|
||||
if (!config?.enabled) return null;
|
||||
|
||||
const tasksPath = path.join(orchDir, 'tasks.json');
|
||||
const tasksPayload = safeReadJson(tasksPath);
|
||||
const tasks = Array.isArray(tasksPayload?.tasks) ? tasksPayload.tasks : [];
|
||||
const counts = {
|
||||
pending: 0,
|
||||
running: 0,
|
||||
completed: 0,
|
||||
failed: 0,
|
||||
escalated: 0,
|
||||
};
|
||||
|
||||
for (const task of tasks) {
|
||||
if (typeof task !== 'object' || task === null) continue;
|
||||
const status = String((task as Record<string, unknown>).status ?? 'pending');
|
||||
if (status in counts) {
|
||||
counts[status as keyof typeof counts] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const lines = [
|
||||
'[MACP Queue]',
|
||||
`Queue: pending=${counts.pending} running=${counts.running} completed=${counts.completed} failed=${counts.failed} escalated=${counts.escalated}`,
|
||||
];
|
||||
|
||||
const events = safeReadNdjson(path.join(orchDir, 'events.ndjson'));
|
||||
if (events.length > 0) {
|
||||
lines.push('Recent activity:');
|
||||
for (const event of events) {
|
||||
const timestamp = String(event.timestamp ?? '?');
|
||||
const eventType = String(event.event_type ?? 'event');
|
||||
const taskId = String(event.task_id ?? '-');
|
||||
const message = String(event.message ?? '').trim();
|
||||
lines.push(`- ${timestamp} | ${eventType} | ${taskId}${message ? ` | ${message}` : ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Write runtime contract to ACP worker config files
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function writeCodexInstructions(mosaicHome: string, mission: ActiveMission | null): void {
|
||||
const contract = buildRuntimeContract(mosaicHome, mission, mission?.projectRoot);
|
||||
const dest = path.join(os.homedir(), '.codex/instructions.md');
|
||||
mkdirSync(path.dirname(dest), { recursive: true });
|
||||
writeFileSync(dest, contract, 'utf8');
|
||||
}
|
||||
|
||||
function writeClaudeInstructions(mosaicHome: string, mission: ActiveMission | null): void {
|
||||
// Claude Code reads from ~/.claude/CLAUDE.md
|
||||
const contract = buildRuntimeContract(mosaicHome, mission, mission?.projectRoot);
|
||||
const dest = path.join(os.homedir(), '.claude/CLAUDE.md');
|
||||
mkdirSync(path.dirname(dest), { recursive: true });
|
||||
// Only write if different to avoid unnecessary disk writes
|
||||
const existing = safeRead(dest);
|
||||
if (existing !== contract) {
|
||||
writeFileSync(dest, contract, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Build static framework preamble for OC native agents (appendSystemContext)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildFrameworkPreamble(mosaicHome: string): string {
|
||||
const agentsFile = path.join(mosaicHome, 'AGENTS.md');
|
||||
const agentsContent = safeRead(agentsFile);
|
||||
|
||||
const lines: string[] = [
|
||||
'# Mosaic Framework Contract (Auto-injected)',
|
||||
'',
|
||||
'You are operating under the Mosaic multi-agent framework.',
|
||||
'The following rules are MANDATORY and OVERRIDE any conflicting defaults.',
|
||||
'',
|
||||
];
|
||||
|
||||
if (agentsContent) {
|
||||
// Extract hard rules section
|
||||
const hardRulesMatch = agentsContent.match(/## ⛔ HARD RULES[\s\S]*?(?=^## [^⛔]|\z)/m);
|
||||
if (hardRulesMatch) {
|
||||
lines.push('## Hard Rules (Compaction-Resistant)\n');
|
||||
lines.push(hardRulesMatch[0].trim());
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(
|
||||
'',
|
||||
'## Completion Gates',
|
||||
'A task is NOT done until: code review ✓ | security review ✓ | tests GREEN ✓ | CI green ✓ | issue closed ✓ | docs updated ✓',
|
||||
'',
|
||||
'## Worker Completion Protocol',
|
||||
'Workers NEVER merge PRs. Implement → lint/typecheck → push branch → open PR → fire system event → EXIT.',
|
||||
'',
|
||||
'## Worktree Requirement',
|
||||
'All code work MUST use a git worktree at `~/src/<repo>-worktrees/<task-slug>`. Never use /tmp.',
|
||||
);
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Plugin registration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export default function register(api: OpenClawPluginApi) {
|
||||
const cfg = (api.config ?? {}) as MosaicFrameworkConfig;
|
||||
|
||||
const mosaicHome = expandHome(cfg.mosaicHome ?? '~/.config/mosaic');
|
||||
const projectRoots = (cfg.projectRoots ?? []).map(expandHome);
|
||||
const requireMission = cfg.requireMission ?? false;
|
||||
const injectAgentIds = cfg.injectAgentIds ?? null; // null = all agents
|
||||
const acpAgentIds = new Set(cfg.acpAgentIds ?? ['codex', 'claude']);
|
||||
|
||||
// Pre-build the static framework preamble (injected once per session start)
|
||||
const frameworkPreamble = buildFrameworkPreamble(mosaicHome);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hook 1: before_agent_start — inject into OC native agent sessions
|
||||
// ---------------------------------------------------------------------------
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
api.on('before_agent_start', async (_event: any, ctx: any) => {
|
||||
const agentId = ctx.agentId ?? 'unknown';
|
||||
|
||||
// Skip if this agent is not in the inject list (when configured)
|
||||
if (injectAgentIds !== null && !injectAgentIds.includes(agentId)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Skip ACP worker sessions — they get injected via subagent_spawning instead
|
||||
if (acpAgentIds.has(agentId)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Read active mission for this turn (dynamic)
|
||||
const mission = projectRoots.length > 0 ? findActiveMission(projectRoots) : null;
|
||||
|
||||
const result: Record<string, string> = {};
|
||||
|
||||
// Static framework preamble → appendSystemContext (cached by provider)
|
||||
result.appendSystemContext = frameworkPreamble;
|
||||
|
||||
// Dynamic mission/MACP state → prependContext (fresh each turn)
|
||||
const sections: string[] = [];
|
||||
if (mission) {
|
||||
sections.push(buildMissionContext(mission));
|
||||
}
|
||||
const macpProjectRoot = mission?.projectRoot ?? findMacpProjectRoot(projectRoots);
|
||||
if (macpProjectRoot) {
|
||||
const macpContext = buildMacpContext(macpProjectRoot);
|
||||
if (macpContext) {
|
||||
sections.push(macpContext);
|
||||
}
|
||||
}
|
||||
if (sections.length > 0) {
|
||||
result.prependContext = sections.join('\n\n');
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hook 2: subagent_spawning — inject runtime contract into ACP workers
|
||||
//
|
||||
// Mission context is intentionally NOT injected here. The runtime contract
|
||||
// includes instructions to read .mosaic/orchestrator/mission.json from the
|
||||
// worker's own CWD — so the worker picks up the correct project mission
|
||||
// itself. Injecting a mission here would risk cross-contamination when
|
||||
// multiple projects have active missions simultaneously.
|
||||
// ---------------------------------------------------------------------------
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
api.on('subagent_spawning', async (event: any, _ctx: any) => {
|
||||
const childAgentId = (event as Record<string, unknown>).agentId as string | undefined;
|
||||
if (!childAgentId) return { status: 'ok' };
|
||||
|
||||
// Only act on ACP coding worker spawns
|
||||
if (!acpAgentIds.has(childAgentId)) {
|
||||
return { status: 'ok' };
|
||||
}
|
||||
|
||||
// Gate: block spawn if requireMission is true and no active mission found in any root
|
||||
if (requireMission) {
|
||||
const mission = projectRoots.length > 0 ? findActiveMission(projectRoots) : null;
|
||||
if (!mission) {
|
||||
return {
|
||||
status: 'error',
|
||||
error: `[mosaic-framework] No active Mosaic mission found. Run 'mosaic coord init' in your project directory first. Scanned: ${projectRoots.join(', ')}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Write runtime contract (global framework rules + load order, no mission context)
|
||||
// The worker will detect its own mission from .mosaic/orchestrator/mission.json in its CWD.
|
||||
try {
|
||||
if (childAgentId === 'codex') {
|
||||
writeCodexInstructions(mosaicHome, null);
|
||||
} else if (childAgentId === 'claude') {
|
||||
writeClaudeInstructions(mosaicHome, null);
|
||||
}
|
||||
} catch (err) {
|
||||
// Log but don't block — better to have a worker without full rails than no worker
|
||||
api.logger?.warn(
|
||||
`[mosaic-framework] Failed to write runtime contract for ${childAgentId}: ${String(err)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return { status: 'ok' };
|
||||
});
|
||||
}
|
||||
10
plugins/mosaic-framework/tsconfig.json
Normal file
10
plugins/mosaic-framework/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user