438 lines
15 KiB
PowerShell
438 lines
15 KiB
PowerShell
# 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: irm https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.ps1 | iex"
|
|
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
|
|
}
|
|
}
|